mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-08 06:51:49 +08:00
Compare commits
1 Commits
feat/code-
...
codex/code
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
32d1f6e971 |
@@ -100,10 +100,6 @@ Format first if formatting can change line locations. Then it is OK to run tests
|
||||
scripts/autoreview --parallel-tests "<focused test command>"
|
||||
```
|
||||
|
||||
On Windows, the default `--parallel-tests` shell preserves the platform `cmd.exe`
|
||||
semantics used by Python `shell=True`. Use `--parallel-tests-shell powershell`
|
||||
or `--parallel-tests-shell pwsh` when the focused test command is PowerShell-specific.
|
||||
|
||||
Tradeoff: tests may force code changes that stale the review. If tests or review lead to code edits, rerun the affected tests and rerun review until no accepted/actionable findings remain. Once that rerun exits cleanly, stop; do not spend another long review cycle on redundant confirmation.
|
||||
|
||||
## Review Panels
|
||||
@@ -148,22 +144,6 @@ OpenClaw repo-local helper:
|
||||
.agents/skills/autoreview/scripts/autoreview --help
|
||||
```
|
||||
|
||||
On native Windows, invoke the extensionless Python helper through Python:
|
||||
|
||||
```powershell
|
||||
python .agents\skills\autoreview\scripts\autoreview --help
|
||||
```
|
||||
|
||||
The smoke harness has thin shell wrappers over a shared Python implementation:
|
||||
|
||||
```bash
|
||||
.agents/skills/autoreview/scripts/test-review-harness --fixture benign --engine codex
|
||||
```
|
||||
|
||||
```powershell
|
||||
.agents\skills\autoreview\scripts\test-review-harness.ps1 -Fixture benign -Engine codex
|
||||
```
|
||||
|
||||
`agent-scripts` checkout helper:
|
||||
|
||||
```bash
|
||||
@@ -189,11 +169,10 @@ The helper:
|
||||
- otherwise uses current PR base if `gh pr view` works
|
||||
- otherwise uses `origin/main` for non-main branches
|
||||
- supports `--engine codex`, `claude`, `droid`, and `copilot`; default is `AUTOREVIEW_ENGINE` or `codex`; Codex should remain the default when nothing is set
|
||||
- resolves bare `git`, `gh`, reviewer, and PowerShell shell commands from absolute `PATH` entries only, never from the reviewed checkout; explicit relative `--*-bin` paths are resolved from the reviewed repository root
|
||||
- use `--mode commit --commit <ref>` for already-committed work, especially clean `main` after landing
|
||||
- should be left in `--mode auto` or forced to `--mode branch` for PR/branch work; do not force `--mode local` after committing
|
||||
- writes only to stdout unless `--output`, `--json-output`, or live streamed engine stderr is set
|
||||
- supports `--dry-run`, `--parallel-tests`, `--parallel-tests-shell`, `--prompt`, `--prompt-file`, `--dataset`, `--no-tools`, `--no-web-search`, and commit refs
|
||||
- supports `--dry-run`, `--parallel-tests`, `--prompt`, `--prompt-file`, `--dataset`, `--no-tools`, `--no-web-search`, and commit refs
|
||||
- supports `--stream-engine-output` or `AUTOREVIEW_STREAM_ENGINE_OUTPUT=1` for live engine text while preserving structured validation; Codex and Claude hide tool/file event details, emit compact activity summaries, and report usage at turn completion
|
||||
- supports opt-in review panels with `--panel` / `--reviewers`, plus per-engine `--model` and `--thinking`
|
||||
- allows read-only tools and web search by default where the selected CLI supports them; forbids nested review in the prompt; Codex is run through `codex exec` with read-only sandbox and structured output
|
||||
|
||||
@@ -214,17 +214,12 @@ def run_with_stream(
|
||||
|
||||
|
||||
def git(repo: Path, *args: str, check: bool = True) -> str:
|
||||
return run([resolve_command("git", repo), *args], repo, check=check).stdout
|
||||
return run(["git", *args], repo, check=check).stdout
|
||||
|
||||
|
||||
def repo_root() -> Path:
|
||||
start = Path.cwd().resolve()
|
||||
unsafe_root = discover_repo_root(start) or start
|
||||
git_bin = find_command("git", unsafe_root)
|
||||
if not git_bin:
|
||||
raise SystemExit("git executable not found. Install Git or add it to PATH.")
|
||||
result = subprocess.run(
|
||||
[git_bin, "rev-parse", "--show-toplevel"],
|
||||
["git", "rev-parse", "--show-toplevel"],
|
||||
text=True,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
@@ -234,16 +229,6 @@ def repo_root() -> Path:
|
||||
return Path(result.stdout.strip()).resolve()
|
||||
|
||||
|
||||
def discover_repo_root(start: Path) -> Path | None:
|
||||
current = start
|
||||
while True:
|
||||
if (current / ".git").exists():
|
||||
return current
|
||||
if current.parent == current:
|
||||
return None
|
||||
current = current.parent
|
||||
|
||||
|
||||
def current_branch(repo: Path) -> str:
|
||||
return git(repo, "branch", "--show-current", check=False).strip() or "detached"
|
||||
|
||||
@@ -265,70 +250,17 @@ def choose_target(repo: Path, mode: str, base_ref: str | None) -> tuple[str, str
|
||||
|
||||
|
||||
def detect_pr_base(repo: Path) -> str | None:
|
||||
gh_bin = find_command("gh", repo)
|
||||
if not gh_bin:
|
||||
if not shutil_which("gh"):
|
||||
return None
|
||||
result = run([gh_bin, "pr", "view", "--json", "baseRefName", "--jq", ".baseRefName"], repo, check=False)
|
||||
result = run(["gh", "pr", "view", "--json", "baseRefName", "--jq", ".baseRefName"], repo, check=False)
|
||||
base = result.stdout.strip()
|
||||
return f"origin/{base}" if result.returncode == 0 and base else None
|
||||
|
||||
|
||||
def resolve_command(name: str, repo: Path) -> str:
|
||||
resolved = find_command(name, repo)
|
||||
if resolved:
|
||||
return resolved
|
||||
raise SystemExit(f"executable not found: {name}. Install it or pass an explicit trusted path when supported.")
|
||||
|
||||
|
||||
def find_command(name: str, repo: Path) -> str | None:
|
||||
command = Path(name)
|
||||
if has_directory_component(name, command):
|
||||
base = command if command.is_absolute() else repo / command
|
||||
return first_executable_candidate(base)
|
||||
def shutil_which(name: str) -> str | None:
|
||||
for part in os.environ.get("PATH", "").split(os.pathsep):
|
||||
if not part or part == ".":
|
||||
continue
|
||||
path_part = Path(part)
|
||||
if not path_part.is_absolute():
|
||||
continue
|
||||
try:
|
||||
resolved_part = path_part.resolve()
|
||||
resolved_repo = repo.resolve()
|
||||
except OSError:
|
||||
continue
|
||||
if is_within(resolved_part, resolved_repo):
|
||||
continue
|
||||
found = first_executable_candidate(resolved_part / name, reject_root=resolved_repo)
|
||||
if found:
|
||||
return found
|
||||
return None
|
||||
|
||||
|
||||
def is_within(path: Path, root: Path) -> bool:
|
||||
return path == root or path.is_relative_to(root)
|
||||
|
||||
|
||||
def has_directory_component(name: str, command: Path) -> bool:
|
||||
separators = [separator for separator in (os.sep, os.altsep) if separator]
|
||||
return command.is_absolute() or bool(command.drive) or any(separator in name for separator in separators)
|
||||
|
||||
|
||||
def first_executable_candidate(path: Path, *, reject_root: Path | None = None) -> str | None:
|
||||
if os.name == "nt" and not path.suffix:
|
||||
extensions = [ext for ext in os.environ.get("PATHEXT", ".COM;.EXE;.BAT;.CMD").split(";") if ext]
|
||||
candidates = [path.with_suffix(ext.lower()) for ext in extensions]
|
||||
candidates.extend(path.with_suffix(ext.upper()) for ext in extensions)
|
||||
candidates.append(path)
|
||||
else:
|
||||
candidates = [path]
|
||||
for candidate in candidates:
|
||||
if candidate.is_file() and os.access(candidate, os.X_OK):
|
||||
if reject_root is not None:
|
||||
try:
|
||||
if is_within(candidate.resolve(), reject_root):
|
||||
continue
|
||||
except OSError:
|
||||
continue
|
||||
candidate = Path(part) / name
|
||||
if candidate.exists() and os.access(candidate, os.X_OK):
|
||||
return str(candidate)
|
||||
return None
|
||||
|
||||
@@ -487,7 +419,7 @@ def run_codex(args: argparse.Namespace, repo: Path, prompt: str) -> str:
|
||||
raise SystemExit("--no-tools is not supported by the Codex engine; use --engine claude --no-tools for a no-tools run")
|
||||
schema_path = write_json_temp(SCHEMA)
|
||||
output_path = Path(tempfile.NamedTemporaryFile("w", suffix=".json", delete=False).name)
|
||||
cmd = [resolve_command(args.codex_bin, repo), "--ask-for-approval", "never"]
|
||||
cmd = [args.codex_bin, "--ask-for-approval", "never"]
|
||||
if args.web_search:
|
||||
cmd.append("--search")
|
||||
if args.model:
|
||||
@@ -531,7 +463,7 @@ def run_codex(args: argparse.Namespace, repo: Path, prompt: str) -> str:
|
||||
|
||||
def run_claude(args: argparse.Namespace, repo: Path, prompt: str) -> str:
|
||||
cmd = [
|
||||
resolve_command(args.claude_bin, repo),
|
||||
args.claude_bin,
|
||||
"--print",
|
||||
"--no-session-persistence",
|
||||
"--output-format",
|
||||
@@ -568,7 +500,7 @@ def run_droid(args: argparse.Namespace, repo: Path, prompt: str) -> str:
|
||||
prompt_path = Path(tempfile.NamedTemporaryFile("w", suffix=".txt", delete=False).name)
|
||||
prompt_path.write_text(prompt)
|
||||
cmd = [
|
||||
resolve_command(args.droid_bin, repo),
|
||||
args.droid_bin,
|
||||
"exec",
|
||||
"--cwd",
|
||||
str(repo),
|
||||
@@ -598,7 +530,7 @@ def run_copilot(args: argparse.Namespace, repo: Path, prompt: str) -> str:
|
||||
prompt_path.write_text(prompt)
|
||||
os.chmod(prompt_path, 0o600)
|
||||
cmd = [
|
||||
resolve_command(args.copilot_bin, repo),
|
||||
args.copilot_bin,
|
||||
"-C",
|
||||
tempdir,
|
||||
"-p",
|
||||
@@ -945,23 +877,9 @@ def print_report(report: dict[str, Any], *, label: str = "autoreview") -> None:
|
||||
print(report["overall_explanation"])
|
||||
|
||||
|
||||
def start_parallel_tests(command: str, repo: Path, shell_kind: str) -> tuple[subprocess.Popen, float]:
|
||||
def start_parallel_tests(command: str, repo: Path) -> tuple[subprocess.Popen, float]:
|
||||
print(f"tests: {command}")
|
||||
if shell_kind == "default" or shell_kind == "cmd":
|
||||
return subprocess.Popen(command, cwd=repo, shell=True), time.time()
|
||||
if shell_kind == "powershell":
|
||||
powershell = resolve_command("powershell", repo)
|
||||
return subprocess.Popen(
|
||||
[powershell, "-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", command],
|
||||
cwd=repo,
|
||||
), time.time()
|
||||
if shell_kind == "pwsh":
|
||||
pwsh = resolve_command("pwsh", repo)
|
||||
return subprocess.Popen(
|
||||
[pwsh, "-NoProfile", "-Command", command],
|
||||
cwd=repo,
|
||||
), time.time()
|
||||
raise SystemExit(f"invalid --parallel-tests-shell/AUTOREVIEW_PARALLEL_TESTS_SHELL: {shell_kind}")
|
||||
return subprocess.Popen(command, cwd=repo, shell=True), time.time()
|
||||
|
||||
|
||||
def finish_parallel_tests(proc: subprocess.Popen, started: float) -> int:
|
||||
@@ -1006,12 +924,6 @@ def parse_args() -> argparse.Namespace:
|
||||
help="Stream review engine output while preserving buffered output for validation. Codex output is filtered to hide tool/file chatter.",
|
||||
)
|
||||
parser.add_argument("--parallel-tests", help="Run a test command concurrently with review; failure fails the helper.")
|
||||
parser.add_argument(
|
||||
"--parallel-tests-shell",
|
||||
choices=["default", "cmd", "powershell", "pwsh"],
|
||||
default=os.environ.get("AUTOREVIEW_PARALLEL_TESTS_SHELL", "default"),
|
||||
help="Shell for --parallel-tests. Default preserves Python shell=True platform behavior; use powershell or pwsh for PowerShell-specific commands.",
|
||||
)
|
||||
parser.add_argument("--require-finding", action="append", default=[], help="Require finding text to contain this substring.")
|
||||
parser.add_argument("--expect-findings", action="store_true", help="Treat findings as success; for harness acceptance tests.")
|
||||
parser.add_argument("--dry-run", action="store_true")
|
||||
@@ -1217,7 +1129,7 @@ def main() -> int:
|
||||
|
||||
tests_proc: tuple[subprocess.Popen, float] | None = None
|
||||
if args.parallel_tests:
|
||||
tests_proc = start_parallel_tests(args.parallel_tests, repo, args.parallel_tests_shell)
|
||||
tests_proc = start_parallel_tests(args.parallel_tests, repo)
|
||||
try:
|
||||
if len(reviewers) == 1:
|
||||
report = run_reviewer(reviewers[0], repo, prompt, changed_paths, args.require_finding)
|
||||
|
||||
@@ -1,16 +1,176 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage: test-review-harness [--fixture malicious|benign] [--engine codex|claude|droid|copilot]...
|
||||
|
||||
Creates a temporary git repo with either a deliberately unsafe patch or a
|
||||
security-sensitive-but-safe patch, then verifies each selected engine through
|
||||
autoreview.
|
||||
Default engines: codex, claude.
|
||||
EOF
|
||||
}
|
||||
|
||||
engines=()
|
||||
fixture=malicious
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--fixture)
|
||||
fixture=${2:-}
|
||||
shift 2
|
||||
;;
|
||||
--engine)
|
||||
engines+=("${2:-}")
|
||||
shift 2
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
usage >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
case "$fixture" in
|
||||
malicious|benign) ;;
|
||||
*)
|
||||
usage >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
|
||||
if [[ ${#engines[@]} -eq 0 ]]; then
|
||||
engines=(codex claude)
|
||||
fi
|
||||
|
||||
script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
||||
harness="$script_dir/test-review-harness.py"
|
||||
repo=$(mktemp -d "${TMPDIR:-/tmp}/autoreview-fixture.XXXXXX")
|
||||
trap 'rm -rf "$repo"' EXIT
|
||||
|
||||
if command -v python3 >/dev/null 2>&1; then
|
||||
exec python3 "$harness" "$@"
|
||||
cd "$repo"
|
||||
git init --quiet
|
||||
git config user.name "Review Fixture"
|
||||
git config user.email "review-fixture@example.com"
|
||||
|
||||
if [[ "$fixture" == malicious ]]; then
|
||||
cat > app.js <<'EOF'
|
||||
export function uploadPath(name) {
|
||||
return `uploads/${name.replaceAll("/", "")}`;
|
||||
}
|
||||
|
||||
export function publicUser(user) {
|
||||
return { id: user.id, name: user.name };
|
||||
}
|
||||
EOF
|
||||
else
|
||||
cat > app.js <<'EOF'
|
||||
import path from "node:path";
|
||||
|
||||
const uploadsRoot = path.resolve("uploads");
|
||||
|
||||
function safeChildPath(root, name) {
|
||||
if (name.includes("/") || name.includes("\\") || name.includes("\0")) {
|
||||
throw new Error("invalid child name");
|
||||
}
|
||||
const resolved = path.resolve(root, name);
|
||||
if (!resolved.startsWith(root + path.sep)) throw new Error("path escaped root");
|
||||
return resolved;
|
||||
}
|
||||
|
||||
export function uploadPath(name) {
|
||||
return safeChildPath(uploadsRoot, name);
|
||||
}
|
||||
|
||||
export function publicUser(user) {
|
||||
return { id: user.id, name: user.name };
|
||||
}
|
||||
EOF
|
||||
fi
|
||||
|
||||
if command -v python >/dev/null 2>&1; then
|
||||
exec python "$harness" "$@"
|
||||
git add app.js
|
||||
git commit --quiet -m "initial safe version"
|
||||
|
||||
if [[ "$fixture" == malicious ]]; then
|
||||
cat > app.js <<'EOF'
|
||||
import { execSync } from "node:child_process";
|
||||
|
||||
export function uploadPath(name) {
|
||||
return `uploads/${name}`;
|
||||
}
|
||||
|
||||
export function deleteUpload(name) {
|
||||
return execSync(`rm -rf uploads/${name}`);
|
||||
}
|
||||
|
||||
export function publicUser(user) {
|
||||
return { id: user.id, name: user.name, password: user.password };
|
||||
}
|
||||
EOF
|
||||
else
|
||||
cat > app.js <<'EOF'
|
||||
import { execFile } from "node:child_process";
|
||||
import path from "node:path";
|
||||
import { promisify } from "node:util";
|
||||
|
||||
const uploadsRoot = path.resolve("uploads");
|
||||
const reposRoot = path.resolve("repos");
|
||||
const execFileAsync = promisify(execFile);
|
||||
|
||||
function safeChildPath(root, name) {
|
||||
if (name.includes("/") || name.includes("\\") || name.includes("\0")) {
|
||||
throw new Error("invalid child name");
|
||||
}
|
||||
const resolved = path.resolve(root, name);
|
||||
if (!resolved.startsWith(root + path.sep)) throw new Error("path escaped root");
|
||||
return resolved;
|
||||
}
|
||||
|
||||
function repoChildPath(root, name) {
|
||||
if (!/^[A-Za-z0-9._-]+$/.test(name)) throw new Error("invalid repo name");
|
||||
return safeChildPath(root, name);
|
||||
}
|
||||
|
||||
export function uploadPath(name) {
|
||||
return safeChildPath(uploadsRoot, name);
|
||||
}
|
||||
|
||||
export async function repoStatus(repoName) {
|
||||
const { stdout } = await execFileAsync("git", ["status", "--short"], {
|
||||
cwd: repoChildPath(reposRoot, repoName),
|
||||
encoding: "utf8",
|
||||
maxBuffer: 16 * 1024 * 1024,
|
||||
});
|
||||
return stdout;
|
||||
}
|
||||
|
||||
export function publicUser(user) {
|
||||
return { id: user.id, name: user.name };
|
||||
}
|
||||
|
||||
export function accountSettingsForOwner(user, requesterId) {
|
||||
if (String(requesterId) !== String(user.id)) throw new Error("forbidden");
|
||||
return { id: user.id, hasPassword: Boolean(user.passwordHash) };
|
||||
}
|
||||
EOF
|
||||
fi
|
||||
|
||||
echo "Python 3 is required to run test-review-harness." >&2
|
||||
exit 127
|
||||
for engine in "${engines[@]}"; do
|
||||
echo "== $engine =="
|
||||
if [[ "$fixture" == malicious ]]; then
|
||||
"$script_dir/autoreview" \
|
||||
--mode local \
|
||||
--engine "$engine" \
|
||||
--prompt "This is an acceptance test fixture. The changed app.js patch contains real security bugs. Review normally and report only actionable defects from the patch." \
|
||||
--require-finding "command" \
|
||||
--expect-findings
|
||||
else
|
||||
"$script_dir/autoreview" \
|
||||
--mode local \
|
||||
--engine "$engine" \
|
||||
--prompt "Security calibration fixture: this patch intentionally uses filesystem paths, async execFile, and owner-gated password-adjacent state safely. Do not flag legitimate shell/filesystem/auth-adjacent functionality unless there is a concrete exploitable risk in the diff."
|
||||
fi
|
||||
done
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[ValidateSet('malicious', 'benign')]
|
||||
[string] $Fixture,
|
||||
|
||||
[ValidateSet('codex', 'claude', 'droid', 'copilot')]
|
||||
[string[]] $Engine,
|
||||
|
||||
[Alias('h')]
|
||||
[switch] $Help
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
$Harness = Join-Path $PSScriptRoot 'test-review-harness.py'
|
||||
$ForwardedArgs = @()
|
||||
|
||||
if ($Help) {
|
||||
$ForwardedArgs += '--help'
|
||||
}
|
||||
|
||||
if ($PSBoundParameters.ContainsKey('Fixture')) {
|
||||
$ForwardedArgs += @('--fixture', $Fixture)
|
||||
}
|
||||
|
||||
if ($PSBoundParameters.ContainsKey('Engine')) {
|
||||
foreach ($SelectedEngine in $Engine) {
|
||||
$ForwardedArgs += @('--engine', $SelectedEngine)
|
||||
}
|
||||
}
|
||||
|
||||
$PyLauncher = Get-Command py -ErrorAction SilentlyContinue
|
||||
if ($null -ne $PyLauncher) {
|
||||
& $PyLauncher.Source -3 $Harness @ForwardedArgs
|
||||
exit $LASTEXITCODE
|
||||
}
|
||||
|
||||
$Python = Get-Command python -ErrorAction SilentlyContinue
|
||||
if ($null -ne $Python) {
|
||||
& $Python.Source $Harness @ForwardedArgs
|
||||
exit $LASTEXITCODE
|
||||
}
|
||||
|
||||
Write-Error 'Python 3 is required to run test-review-harness.'
|
||||
exit 127
|
||||
@@ -1,199 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import shutil
|
||||
import stat
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
from collections.abc import Callable
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ENGINES = ("codex", "claude", "droid", "copilot")
|
||||
DEFAULT_ENGINES = ("codex", "claude")
|
||||
|
||||
MALICIOUS_INITIAL = """export function uploadPath(name) {
|
||||
return `uploads/${name.replaceAll("/", "")}`;
|
||||
}
|
||||
|
||||
export function publicUser(user) {
|
||||
return { id: user.id, name: user.name };
|
||||
}
|
||||
"""
|
||||
|
||||
BENIGN_INITIAL = r"""import path from "node:path";
|
||||
|
||||
const uploadsRoot = path.resolve("uploads");
|
||||
|
||||
function safeChildPath(root, name) {
|
||||
if (name.includes("/") || name.includes("\\") || name.includes("\0")) {
|
||||
throw new Error("invalid child name");
|
||||
}
|
||||
const resolved = path.resolve(root, name);
|
||||
if (!resolved.startsWith(root + path.sep)) throw new Error("path escaped root");
|
||||
return resolved;
|
||||
}
|
||||
|
||||
export function uploadPath(name) {
|
||||
return safeChildPath(uploadsRoot, name);
|
||||
}
|
||||
|
||||
export function publicUser(user) {
|
||||
return { id: user.id, name: user.name };
|
||||
}
|
||||
"""
|
||||
|
||||
MALICIOUS_CHANGED = """import { execSync } from "node:child_process";
|
||||
|
||||
export function uploadPath(name) {
|
||||
return `uploads/${name}`;
|
||||
}
|
||||
|
||||
export function deleteUpload(name) {
|
||||
return execSync(`rm -rf uploads/${name}`);
|
||||
}
|
||||
|
||||
export function publicUser(user) {
|
||||
return { id: user.id, name: user.name, password: user.password };
|
||||
}
|
||||
"""
|
||||
|
||||
BENIGN_CHANGED = r"""import { execFile } from "node:child_process";
|
||||
import path from "node:path";
|
||||
import { promisify } from "node:util";
|
||||
|
||||
const uploadsRoot = path.resolve("uploads");
|
||||
const reposRoot = path.resolve("repos");
|
||||
const execFileAsync = promisify(execFile);
|
||||
|
||||
function safeChildPath(root, name) {
|
||||
if (name.includes("/") || name.includes("\\") || name.includes("\0")) {
|
||||
throw new Error("invalid child name");
|
||||
}
|
||||
const resolved = path.resolve(root, name);
|
||||
if (!resolved.startsWith(root + path.sep)) throw new Error("path escaped root");
|
||||
return resolved;
|
||||
}
|
||||
|
||||
function repoChildPath(root, name) {
|
||||
if (!/^[A-Za-z0-9._-]+$/.test(name)) throw new Error("invalid repo name");
|
||||
return safeChildPath(root, name);
|
||||
}
|
||||
|
||||
export function uploadPath(name) {
|
||||
return safeChildPath(uploadsRoot, name);
|
||||
}
|
||||
|
||||
export async function repoProbe(repoName) {
|
||||
const { stdout } = await execFileAsync(process.execPath, ["--version"], {
|
||||
cwd: repoChildPath(reposRoot, repoName),
|
||||
encoding: "utf8",
|
||||
maxBuffer: 16 * 1024 * 1024,
|
||||
});
|
||||
return stdout;
|
||||
}
|
||||
|
||||
export function publicUser(user) {
|
||||
return { id: user.id, name: user.name };
|
||||
}
|
||||
|
||||
export function accountSettingsForOwner(user, requesterId) {
|
||||
if (String(requesterId) !== String(user.id)) throw new Error("forbidden");
|
||||
return { id: user.id, hasPassword: Boolean(user.passwordHash) };
|
||||
}
|
||||
"""
|
||||
|
||||
MALICIOUS_PROMPT = "This is an acceptance test fixture. The changed app.js patch contains real security bugs. Review normally and report only actionable defects from the patch."
|
||||
BENIGN_PROMPT = "Security calibration fixture: this patch intentionally uses filesystem paths, async execFile, and owner-gated password-adjacent state safely. Do not flag legitimate shell/filesystem/auth-adjacent functionality unless there is a concrete exploitable risk in the diff."
|
||||
|
||||
|
||||
def parse_args(argv: list[str]) -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(
|
||||
prog="test-review-harness",
|
||||
description=(
|
||||
"Creates a temporary git repo with either a deliberately unsafe patch "
|
||||
"or a security-sensitive-but-safe patch, then verifies each selected "
|
||||
"engine through autoreview."
|
||||
),
|
||||
epilog="Default engines: codex, claude.",
|
||||
)
|
||||
parser.add_argument("--fixture", choices=("malicious", "benign"), default="malicious")
|
||||
parser.add_argument("--engine", action="append", choices=ENGINES, dest="engines")
|
||||
return parser.parse_args(argv)
|
||||
|
||||
|
||||
def write_fixture_file(repo: Path, content: str) -> None:
|
||||
with (repo / "app.js").open("w", encoding="utf-8", newline="\n") as handle:
|
||||
handle.write(content)
|
||||
|
||||
|
||||
def run(command: list[str], cwd: Path) -> None:
|
||||
subprocess.run(command, cwd=cwd, check=True)
|
||||
|
||||
|
||||
def create_fixture_repo(repo: Path, fixture: str) -> None:
|
||||
run(["git", "init", "--quiet"], repo)
|
||||
run(["git", "config", "user.name", "Review Fixture"], repo)
|
||||
run(["git", "config", "user.email", "review-fixture@example.com"], repo)
|
||||
|
||||
write_fixture_file(repo, MALICIOUS_INITIAL if fixture == "malicious" else BENIGN_INITIAL)
|
||||
run(["git", "add", "app.js"], repo)
|
||||
run(["git", "commit", "--quiet", "-m", "initial safe version"], repo)
|
||||
write_fixture_file(repo, MALICIOUS_CHANGED if fixture == "malicious" else BENIGN_CHANGED)
|
||||
|
||||
|
||||
def run_reviews(repo: Path, script_dir: Path, fixture: str, engines: list[str]) -> None:
|
||||
autoreview = script_dir / "autoreview"
|
||||
for engine in engines:
|
||||
print(f"== {engine} ==", flush=True)
|
||||
command = [
|
||||
sys.executable,
|
||||
str(autoreview),
|
||||
"--mode",
|
||||
"local",
|
||||
"--engine",
|
||||
engine,
|
||||
"--prompt",
|
||||
MALICIOUS_PROMPT if fixture == "malicious" else BENIGN_PROMPT,
|
||||
]
|
||||
if fixture == "malicious":
|
||||
command.extend(["--require-finding", "command", "--expect-findings"])
|
||||
run(command, repo)
|
||||
|
||||
|
||||
def cleanup_repo(repo: Path) -> None:
|
||||
def make_writable_and_retry(function: Callable[[str], object], path: str, _exc_info: object) -> None:
|
||||
try:
|
||||
os.chmod(path, stat.S_IREAD | stat.S_IWRITE)
|
||||
function(path)
|
||||
except OSError as exc:
|
||||
print(f"warning: unable to remove temp path {path}: {exc}", file=sys.stderr)
|
||||
|
||||
if not repo.exists():
|
||||
return
|
||||
try:
|
||||
shutil.rmtree(repo, onerror=make_writable_and_retry)
|
||||
except OSError as exc:
|
||||
print(f"warning: unable to remove temp repo {repo}: {exc}", file=sys.stderr)
|
||||
|
||||
|
||||
def main(argv: list[str]) -> int:
|
||||
args = parse_args(argv)
|
||||
script_dir = Path(__file__).resolve().parent
|
||||
engines = args.engines or list(DEFAULT_ENGINES)
|
||||
repo = Path(tempfile.mkdtemp(prefix="autoreview-fixture."))
|
||||
try:
|
||||
create_fixture_repo(repo, args.fixture)
|
||||
run_reviews(repo, script_dir, args.fixture, engines)
|
||||
except subprocess.CalledProcessError as exc:
|
||||
return int(exc.returncode or 1)
|
||||
finally:
|
||||
cleanup_repo(repo)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main(sys.argv[1:]))
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: discrawl
|
||||
description: "Discord archive: search, sync freshness, DMs, summaries, TUI, repo/release work."
|
||||
description: "Discord archive: search, sync freshness, DMs, channel slices, SQL counts, and Discrawl repo work."
|
||||
metadata:
|
||||
openclaw:
|
||||
homepage: https://github.com/openclaw/discrawl
|
||||
@@ -16,154 +16,29 @@ metadata:
|
||||
|
||||
# Discrawl
|
||||
|
||||
Use local Discord archive data first for Discord questions. Hit Discord APIs
|
||||
only when the archive is stale, missing the requested scope, or the user asks
|
||||
for current external context.
|
||||
|
||||
## Sources
|
||||
|
||||
- DB: platform-native XDG data dir, usually
|
||||
`${XDG_DATA_HOME:-~/.local/share}/discrawl/discrawl.db` on Linux or
|
||||
`~/Library/Application Support/discrawl/discrawl.db` on macOS
|
||||
- Config: platform-native XDG config dir, with legacy fallback to
|
||||
`~/.discrawl/config.toml`
|
||||
- Cache: platform-native XDG cache dir
|
||||
- Logs: platform-native XDG state dir
|
||||
- Git share repo: platform-native XDG data dir
|
||||
- Repo: `openclaw/discrawl`; use `~/GIT/_Perso/discrawl` only after verifying
|
||||
its remote targets `openclaw/discrawl`, otherwise use a fresh checkout
|
||||
- Preferred CLI: `discrawl`; fallback to `go run ./cmd/discrawl` from the repo
|
||||
if the installed binary is stale
|
||||
|
||||
## Freshness
|
||||
|
||||
For recent/current questions, check freshness before analysis:
|
||||
Use local Discord archive data before live Discord APIs. Check freshness for recent/current questions:
|
||||
|
||||
```bash
|
||||
discrawl status --json
|
||||
```
|
||||
|
||||
For precise freshness from the default database:
|
||||
|
||||
```bash
|
||||
# Discrawl uses macOS ~/Library defaults unless XDG_DATA_HOME is explicitly set.
|
||||
case "$(uname -s)" in
|
||||
Darwin)
|
||||
db="$HOME/Library/Application Support/discrawl/discrawl.db"
|
||||
;;
|
||||
*)
|
||||
db="${XDG_DATA_HOME:-$HOME/.local/share}/discrawl/discrawl.db"
|
||||
;;
|
||||
esac
|
||||
sqlite3 "$db" \
|
||||
"select coalesce(max(updated_at),'') from sync_state where scope like 'channel:%';"
|
||||
```
|
||||
|
||||
Routine diagnostics:
|
||||
|
||||
```bash
|
||||
discrawl doctor
|
||||
```
|
||||
|
||||
Desktop-local refresh:
|
||||
Refresh only when stale or asked:
|
||||
|
||||
```bash
|
||||
discrawl sync --source wiretap
|
||||
```
|
||||
|
||||
Bot API latest refresh, when credentials are available:
|
||||
|
||||
```bash
|
||||
discrawl sync
|
||||
```
|
||||
|
||||
Use `--full` only for deliberate historical backfills:
|
||||
|
||||
```bash
|
||||
discrawl sync --full
|
||||
```
|
||||
|
||||
If SQLite reports busy/locked, check for stray `discrawl` processes before retrying.
|
||||
|
||||
## Query Workflow
|
||||
|
||||
1. Resolve scope: guild, channel, DM, author, keyword, date range.
|
||||
2. Check freshness for recent/current requests.
|
||||
3. Prefer CLI search/messages for slices; use read-only SQL for exact counts.
|
||||
4. Report absolute date spans, counts, channel/DM names, and known gaps.
|
||||
|
||||
Use root or subcommand help for syntax: `discrawl --help`,
|
||||
`discrawl help search`, `discrawl search --help`. Use
|
||||
`DISCRAWL_NO_AUTO_UPDATE=1` for read smokes when you do not want git-share
|
||||
updates.
|
||||
|
||||
Common commands:
|
||||
Query with bounded slices:
|
||||
|
||||
```bash
|
||||
DISCRAWL_NO_AUTO_UPDATE=1 discrawl search --limit 20 "query"
|
||||
discrawl messages --channel '#maintainers' --days 7 --all
|
||||
discrawl dms --last 20
|
||||
discrawl tui --dm
|
||||
DISCRAWL_NO_AUTO_UPDATE=1 discrawl --json sql "select count(*) from messages;"
|
||||
```
|
||||
|
||||
## SQL
|
||||
Report absolute date spans, channel/DM names, counts, and known gaps. Use read-only SQL for exact counts/rankings. Never use `--unsafe --confirm` unless the user explicitly requests a reviewed DB mutation.
|
||||
|
||||
Use `discrawl sql` for exact counts, joins, and ranking queries when normal
|
||||
CLI reads are too coarse. The command is read-only by default, accepts SQL as
|
||||
args or stdin, and supports `--json` for agent parsing.
|
||||
|
||||
Useful examples:
|
||||
|
||||
```bash
|
||||
DISCRAWL_NO_AUTO_UPDATE=1 discrawl --json sql "select count(*) as messages from messages;"
|
||||
DISCRAWL_NO_AUTO_UPDATE=1 discrawl --json sql "select coalesce(nullif(c.name, ''), m.channel_id) as channel, count(*) as messages from messages m left join channels c on c.id = m.channel_id group by m.channel_id order by messages desc limit 20;"
|
||||
DISCRAWL_NO_AUTO_UPDATE=1 discrawl --json sql "select coalesce(nullif(mm.display_name, ''), nullif(mm.global_name, ''), nullif(mm.username, ''), m.author_id) as author, count(*) as messages from messages m left join members mm on mm.guild_id = m.guild_id and mm.user_id = m.author_id group by m.guild_id, m.author_id order by messages desc limit 20;"
|
||||
```
|
||||
|
||||
Never use `--unsafe --confirm` unless the user explicitly asks for a database
|
||||
mutation and the write has been reviewed.
|
||||
|
||||
When the installed CLI lacks a new feature, build or run from a verified
|
||||
`openclaw/discrawl` checkout before concluding the feature is missing.
|
||||
|
||||
## Discord Boundaries
|
||||
|
||||
Bot API sync requires configured Discord bot credentials; do not invent token
|
||||
availability. Desktop wiretap mode reads local Discord Desktop artifacts and
|
||||
must not extract credentials, use user tokens, call Discord as the user, or
|
||||
write to Discord application storage. Wiretap/Desktop cache DMs are local-only
|
||||
and must not be described as part of the published Git snapshot. Git-share
|
||||
snapshots must not include secrets or `@me` DM rows.
|
||||
|
||||
## Verification
|
||||
|
||||
For repo edits, prefer existing Go gates:
|
||||
|
||||
```bash
|
||||
GOWORK=off go test ./...
|
||||
```
|
||||
|
||||
Then run targeted CLI smoke for the touched surface, for example:
|
||||
|
||||
```bash
|
||||
discrawl doctor
|
||||
discrawl status --json
|
||||
DISCRAWL_NO_AUTO_UPDATE=1 discrawl search --limit 5 "test"
|
||||
```
|
||||
|
||||
## ClawSweeper Sandbox
|
||||
|
||||
Use the sandbox reader only:
|
||||
|
||||
```bash
|
||||
discrawl-sandbox search --limit 20 "query"
|
||||
discrawl-sandbox messages --channel clawtributors --days 7 --all
|
||||
discrawl-sandbox status --json
|
||||
```
|
||||
|
||||
This reader imports `https://github.com/openclaw/discord-store.git` into
|
||||
`/root/clawsweeper-sandbox-workspace/.discrawl/discrawl.db` with
|
||||
`discord.token_source = "none"`. The published Git snapshot is public-channel
|
||||
filtered; do not use `/root/.discrawl/config.toml` or the rich writer DB from
|
||||
sandboxed public Discord sessions.
|
||||
Boundaries: bot sync needs configured Discord bot credentials. Wiretap reads local Discord Desktop artifacts only; do not extract user tokens, call Discord as the user, or write to Discord storage. Git-share snapshots must not include secrets or `@me` DM rows.
|
||||
|
||||
@@ -6,16 +6,14 @@ description: Regenerate OpenClaw release changelog sections from git history bef
|
||||
# OpenClaw Changelog Update
|
||||
|
||||
Use this for release changelog rewrites and GitHub release-note source text.
|
||||
This is mandatory before every beta, beta rerun, stable release, or stable
|
||||
rerun. Use it with `release-openclaw-maintainer`; this skill owns changelog
|
||||
content, ordering, grouping, and attribution discipline.
|
||||
Use it with `release-openclaw-maintainer`; this skill owns changelog content,
|
||||
ordering, and audit discipline.
|
||||
|
||||
## Goal
|
||||
|
||||
Rewrite the target `CHANGELOG.md` version section from history, not from stale
|
||||
draft notes. Produce grouped user-facing release notes sorted by user interest
|
||||
while preserving every relevant issue/PR ref and every human `Thanks @...`
|
||||
attribution.
|
||||
draft notes. Produce user-facing release notes sorted by user interest while
|
||||
preserving issue/PR refs and thanks.
|
||||
|
||||
## Inputs
|
||||
|
||||
@@ -46,18 +44,10 @@ attribution.
|
||||
- `### Highlights`: 5-8 bullets, broad user wins first
|
||||
- `### Changes`: new capabilities and behavior changes
|
||||
- `### Fixes`: user-facing fixes first, grouped by impact and surface
|
||||
- group related changes/fixes by surface and user impact; avoid one bullet
|
||||
per tiny commit when several commits tell one user-facing story
|
||||
6. Preserve attribution:
|
||||
- keep `#issue`, `(#PR)`, `Fixes #...`, and `Thanks @...`
|
||||
- every human-authored merged PR represented by a user-facing entry needs
|
||||
its PR ref and `Thanks @author`, even when the PR had no linked issue
|
||||
- when grouping multiple PRs/issues in one bullet, include every relevant
|
||||
PR/issue ref and every human contributor handle in that same bullet
|
||||
- multiple `Thanks @...` handles in one bullet are expected; do not drop or
|
||||
collapse contributor credit just because the note is grouped
|
||||
- if one grouped bullet covers both direct commits and PRs, keep all PR refs
|
||||
and thanks, plus any issue refs from the direct commits
|
||||
- do not add GHSA references, advisory IDs, or security advisory slugs to
|
||||
changelog entries or GitHub release-note text unless explicitly requested
|
||||
- never thank bots, `@openclaw`, `@clawsweeper`, or `@steipete`
|
||||
|
||||
@@ -75,9 +75,7 @@ OPENCLAW_VITEST_MAX_WORKERS=1 pnpm test <path-or-filter>
|
||||
```
|
||||
|
||||
Use targeted file paths whenever possible. Avoid raw `vitest`; use the repo
|
||||
`pnpm test` wrapper so project routing, workers, and setup stay correct. If raw
|
||||
Vitest is unavoidable, use `vitest run ...`; bare `vitest ...` starts local watch
|
||||
mode and will not exit on its own.
|
||||
`pnpm test` wrapper so project routing, workers, and setup stay correct.
|
||||
When the checkout is a Codex worktree, prefer the direct node harness instead:
|
||||
|
||||
```bash
|
||||
|
||||
@@ -21,30 +21,6 @@ function jsonGh(args) {
|
||||
return JSON.parse(gh(args));
|
||||
}
|
||||
|
||||
function githubRestJson(pathSuffix) {
|
||||
const result = execFileSync(
|
||||
"bash",
|
||||
[
|
||||
"-lc",
|
||||
[
|
||||
"set -euo pipefail",
|
||||
'token="$(gh auth token)"',
|
||||
'curl -fsS -H "Authorization: Bearer ${token}" -H "Accept: application/vnd.github+json" -H "X-GitHub-Api-Version: 2022-11-28" "${OPENCLAW_GITHUB_REST_URL}"',
|
||||
].join("\n"),
|
||||
],
|
||||
{
|
||||
encoding: "utf8",
|
||||
env: {
|
||||
...process.env,
|
||||
OPENCLAW_GITHUB_REST_URL: `https://api.github.com/repos/${repo}/${pathSuffix}`,
|
||||
},
|
||||
maxBuffer: 16 * 1024 * 1024,
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
},
|
||||
);
|
||||
return JSON.parse(result);
|
||||
}
|
||||
|
||||
function rate() {
|
||||
try {
|
||||
return jsonGh(["api", "rate_limit"]).resources.core;
|
||||
@@ -83,30 +59,12 @@ for (const job of parent.jobs ?? []) {
|
||||
}
|
||||
|
||||
const since = parent.createdAt;
|
||||
const runsQuery = new URLSearchParams({
|
||||
per_page: "100",
|
||||
created: `>=${since}`,
|
||||
exclude_pull_requests: "true",
|
||||
});
|
||||
const childWorkflowNames = new Set([
|
||||
"CI",
|
||||
"OpenClaw Release Checks",
|
||||
"Plugin Prerelease",
|
||||
"NPM Telegram Beta E2E",
|
||||
"Full Release Validation",
|
||||
]);
|
||||
const runs = githubRestJson(`actions/runs?${runsQuery.toString()}`).workflow_runs ?? [];
|
||||
const runList = runs
|
||||
.filter(
|
||||
(run) =>
|
||||
run.created_at >= since &&
|
||||
run.head_sha === parent.headSha &&
|
||||
childWorkflowNames.has(run.name),
|
||||
)
|
||||
.map((run) =>
|
||||
[run.id, run.name, run.status, run.conclusion ?? "", run.head_sha, run.html_url].join("\t"),
|
||||
)
|
||||
.join("\n");
|
||||
const runList = gh([
|
||||
"api",
|
||||
`repos/${repo}/actions/runs?per_page=100`,
|
||||
"--jq",
|
||||
`.workflow_runs[] | select(.created_at >= "${since}") | select(.name=="CI" or .name=="OpenClaw Release Checks" or .name=="Plugin Prerelease" or .name=="NPM Telegram Beta E2E" or .name=="Full Release Validation") | [.id,.name,.status,.conclusion,.head_sha,.html_url] | @tsv`,
|
||||
]).trim();
|
||||
|
||||
if (!runList) {
|
||||
console.log("children: none found yet");
|
||||
|
||||
@@ -69,13 +69,9 @@ Use this skill for release and publish-time workflow. Load `$release-private` if
|
||||
or clawgrit reports. Report regressions explicitly. A major regression is a
|
||||
release blocker unless the operator waives it or the data clearly proves
|
||||
infrastructure noise.
|
||||
- Generate the changelog before every beta, beta rerun, stable release, or
|
||||
stable rerun, before version/tag preparation. Use
|
||||
`$openclaw-changelog-update` for the rewrite. Do not continue release prep if
|
||||
the target `CHANGELOG.md` section does not have `### Highlights`,
|
||||
`### Changes`, and `### Fixes`, grouped by user-facing surface while
|
||||
preserving every relevant PR/issue ref and every human `Thanks @...`
|
||||
attribution in the grouped bullet.
|
||||
- Generate the changelog before version/tag preparation so the top changelog
|
||||
section is deduped and ordered by user impact. Use
|
||||
`$openclaw-changelog-update` for the rewrite.
|
||||
- Do not create beta-specific `CHANGELOG.md` headings. Beta releases use the
|
||||
stable base version section, for example `v2026.4.20-beta.1` uses
|
||||
`## 2026.4.20` release notes.
|
||||
@@ -148,9 +144,6 @@ Use this skill for release and publish-time workflow. Load `$release-private` if
|
||||
section from history, not existing notes. Use the last reachable stable or
|
||||
beta release tag as the base, then inspect every commit through the target
|
||||
release SHA.
|
||||
- The changelog rewrite is not optional for beta reruns: any `beta.N` after a
|
||||
rebase or backport must refresh the same stable-base `## YYYY.M.D` section
|
||||
before the new version/tag commit.
|
||||
- Include both merged PR commits and direct commits on `main`. Direct commits
|
||||
matter: infer notes from their subject, body, touched files, linked issues,
|
||||
tests, and nearby code when no PR body exists.
|
||||
@@ -164,11 +157,6 @@ Use this skill for release and publish-time workflow. Load `$release-private` if
|
||||
- Add missed user-facing changes, remove internal-only noise, dedupe overlapping
|
||||
PR/direct-commit entries, and sort each section from most to least interesting
|
||||
for users.
|
||||
- Group related highlights, changes, and fixes by user-facing surface and
|
||||
impact, but never lose traceability: each grouped bullet keeps every relevant
|
||||
`#issue`, `(#PR)`, `Fixes #...`, and every human `Thanks @...` handle.
|
||||
Multiple thanks in one bullet are expected when multiple contributor PRs are
|
||||
grouped.
|
||||
- Changelog entries should be user-facing, not internal release-process notes.
|
||||
- GitHub release and prerelease bodies must use the full matching
|
||||
`CHANGELOG.md` version section, not highlights or an excerpt. When creating
|
||||
|
||||
@@ -1,15 +1,21 @@
|
||||
profile: openclaw-check
|
||||
# Default OpenClaw runner spend to the Azure-backed Crabbox account.
|
||||
# Use `--provider aws` only for AWS-specific runner proof.
|
||||
provider: azure
|
||||
provider: aws
|
||||
class: standard
|
||||
capacity:
|
||||
market: spot
|
||||
strategy: most-available
|
||||
# Fail closed instead of silently falling back to on-demand while the
|
||||
# Azure-backed billing account is the default runner path.
|
||||
fallback: spot-only
|
||||
fallback: on-demand-after-120s
|
||||
hints: true
|
||||
availabilityZones:
|
||||
- eu-west-1a
|
||||
- eu-west-1b
|
||||
- eu-west-1c
|
||||
regions:
|
||||
- eu-west-1
|
||||
- eu-west-2
|
||||
- eu-central-1
|
||||
- us-east-1
|
||||
- us-west-2
|
||||
actions:
|
||||
workflow: .github/workflows/crabbox-hydrate.yml
|
||||
# Default AWS hydration uses local Actions replay. Use
|
||||
@@ -29,8 +35,6 @@ blacksmith:
|
||||
job: check
|
||||
ref: main
|
||||
aws:
|
||||
# AWS-specific overrides still pin direct `--provider aws` runs without
|
||||
# leaking AWS region names into the Azure default capacity fallback list.
|
||||
region: eu-west-1
|
||||
rootGB: 400
|
||||
sync:
|
||||
|
||||
8
.github/CODEOWNERS
vendored
8
.github/CODEOWNERS
vendored
@@ -11,10 +11,8 @@
|
||||
/.github/workflows/codeql.yml @openclaw/openclaw-secops
|
||||
/.github/workflows/codeql-android-critical-security.yml @openclaw/openclaw-secops
|
||||
/.github/workflows/codeql-critical-quality.yml @openclaw/openclaw-secops
|
||||
/.github/workflows/dependency-guard.yml @openclaw/openclaw-secops
|
||||
/test/scripts/dependency-guard-workflow.test.ts @openclaw/openclaw-secops
|
||||
/test/scripts/dependency-guard-script.test.ts @openclaw/openclaw-secops
|
||||
/scripts/github/dependency-guard.mjs @openclaw/openclaw-secops
|
||||
/.github/workflows/dependency-change-awareness.yml @openclaw/openclaw-secops
|
||||
/test/scripts/dependency-change-awareness-workflow.test.ts @openclaw/openclaw-secops
|
||||
/package-lock.json @openclaw/openclaw-secops
|
||||
/npm-shrinkwrap.json @openclaw/openclaw-secops
|
||||
/extensions/*/package-lock.json @openclaw/openclaw-secops
|
||||
@@ -31,7 +29,7 @@
|
||||
/src/gateway/**/*secret*.ts @openclaw/openclaw-secops
|
||||
/src/gateway/security-path*.ts @openclaw/openclaw-secops
|
||||
/src/gateway/resolve-configured-secret-input-string*.ts @openclaw/openclaw-secops
|
||||
/packages/gateway-protocol/src/**/*secret*.ts @openclaw/openclaw-secops
|
||||
/src/gateway/protocol/**/*secret*.ts @openclaw/openclaw-secops
|
||||
/src/gateway/server-methods/secrets*.ts @openclaw/openclaw-secops
|
||||
/src/agents/*auth*.ts @openclaw/openclaw-secops
|
||||
/src/agents/**/*auth*.ts @openclaw/openclaw-secops
|
||||
|
||||
24
.github/actions/detect-docs-changes/action.yml
vendored
24
.github/actions/detect-docs-changes/action.yml
vendored
@@ -35,29 +35,17 @@ runs:
|
||||
exit 0
|
||||
fi
|
||||
|
||||
docs_changed=false
|
||||
non_docs=false
|
||||
while IFS= read -r changed_path; do
|
||||
case "$changed_path" in
|
||||
test/fixtures/*)
|
||||
non_docs=true
|
||||
;;
|
||||
docs/* | *.md | *.mdx)
|
||||
docs_changed=true
|
||||
;;
|
||||
*)
|
||||
non_docs=true
|
||||
;;
|
||||
esac
|
||||
done <<< "$CHANGED"
|
||||
|
||||
if [ "$docs_changed" = "true" ]; then
|
||||
# Check if any changed file is a doc
|
||||
DOCS=$(echo "$CHANGED" | grep -E '^docs/|\.md$|\.mdx$' || true)
|
||||
if [ -n "$DOCS" ]; then
|
||||
echo "docs_changed=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "docs_changed=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
if [ "$non_docs" = "false" ]; then
|
||||
# Check if all changed files are docs or markdown
|
||||
NON_DOCS=$(echo "$CHANGED" | grep -vE '^docs/|\.md$|\.mdx$' || true)
|
||||
if [ -z "$NON_DOCS" ]; then
|
||||
echo "docs_only=true" >> "$GITHUB_OUTPUT"
|
||||
echo "Docs-only change detected — skipping heavy jobs"
|
||||
else
|
||||
|
||||
10
.github/actions/ensure-base-commit/action.yml
vendored
10
.github/actions/ensure-base-commit/action.yml
vendored
@@ -38,15 +38,9 @@ runs:
|
||||
exit 0
|
||||
fi
|
||||
|
||||
fetch_base_ref() {
|
||||
timeout --signal=TERM --kill-after=10s 30s git \
|
||||
-c protocol.version=2 \
|
||||
fetch "$@"
|
||||
}
|
||||
|
||||
for deepen_by in 25 100 300; do
|
||||
echo "Base commit missing; deepening $FETCH_REF by $deepen_by."
|
||||
if ! fetch_base_ref --no-tags --deepen="$deepen_by" origin -- "$FETCH_REF"; then
|
||||
if ! git fetch --no-tags --deepen="$deepen_by" origin -- "$FETCH_REF"; then
|
||||
echo "::warning title=ensure-base-commit fetch failed::Failed to deepen $FETCH_REF by $deepen_by while looking for $BASE_SHA"
|
||||
fi
|
||||
if git rev-parse --verify "$BASE_SHA^{commit}" >/dev/null 2>&1; then
|
||||
@@ -56,7 +50,7 @@ runs:
|
||||
done
|
||||
|
||||
echo "Base commit still missing; fetching full history for $FETCH_REF."
|
||||
if ! fetch_base_ref --no-tags origin -- "$FETCH_REF"; then
|
||||
if ! git fetch --no-tags origin -- "$FETCH_REF"; then
|
||||
echo "::warning title=ensure-base-commit fetch failed::Failed to fetch full history for $FETCH_REF while looking for $BASE_SHA"
|
||||
fi
|
||||
if git rev-parse --verify "$BASE_SHA^{commit}" >/dev/null 2>&1; then
|
||||
|
||||
@@ -19,7 +19,7 @@ paths:
|
||||
- src/config/types.channel*.ts
|
||||
- src/gateway/server-channel*.ts
|
||||
- src/gateway/server-methods/channels.ts
|
||||
- packages/gateway-protocol/src/schema/channels.ts
|
||||
- src/gateway/protocol/schema/channels.ts
|
||||
- src/infra/channel-*.ts
|
||||
- src/infra/exec-approval-channel-runtime.ts
|
||||
- src/infra/outbound/channel-*.ts
|
||||
|
||||
@@ -30,7 +30,7 @@ paths:
|
||||
- src/gateway/**/*auth*.ts
|
||||
- src/gateway/*secret*.ts
|
||||
- src/gateway/**/*secret*.ts
|
||||
- packages/gateway-protocol/src/**/*secret*.ts
|
||||
- src/gateway/protocol/**/*secret*.ts
|
||||
- src/gateway/resolve-configured-secret-input-string*.ts
|
||||
- src/gateway/security-path*.ts
|
||||
- src/gateway/server-methods/secrets*.ts
|
||||
|
||||
@@ -30,7 +30,7 @@ paths:
|
||||
- src/gateway/**/*auth*.ts
|
||||
- src/gateway/*secret*.ts
|
||||
- src/gateway/**/*secret*.ts
|
||||
- packages/gateway-protocol/src/**/*secret*.ts
|
||||
- src/gateway/protocol/**/*secret*.ts
|
||||
- src/gateway/resolve-configured-secret-input-string*.ts
|
||||
- src/gateway/security-path*.ts
|
||||
- src/gateway/server-methods/secrets*.ts
|
||||
|
||||
@@ -15,7 +15,7 @@ query-filters:
|
||||
|
||||
paths:
|
||||
- src/gateway/method-scopes.ts
|
||||
- packages/gateway-protocol/src
|
||||
- src/gateway/protocol
|
||||
- src/gateway/server-methods
|
||||
- src/gateway/server-methods.ts
|
||||
- src/gateway/server-methods-list.ts
|
||||
|
||||
@@ -9,7 +9,6 @@ queries:
|
||||
paths:
|
||||
- src
|
||||
- extensions
|
||||
- packages/net-policy/src
|
||||
|
||||
paths-ignore:
|
||||
- "**/node_modules"
|
||||
|
||||
@@ -15,6 +15,7 @@ query-filters:
|
||||
|
||||
paths:
|
||||
- src/infra/net
|
||||
- src/shared/net
|
||||
- src/agents/tools/web-fetch.ts
|
||||
- src/agents/tools/web-guarded-fetch.ts
|
||||
- src/agents/tools/web-shared.ts
|
||||
@@ -22,7 +23,6 @@ paths:
|
||||
- src/web-fetch
|
||||
- src/web/provider-runtime-shared.ts
|
||||
- packages/memory-host-sdk/src/host/ssrf-policy.ts
|
||||
- packages/net-policy/src
|
||||
|
||||
paths-ignore:
|
||||
- "**/node_modules"
|
||||
|
||||
@@ -76,8 +76,6 @@ predicate allowedRawSocketClientCall(Expr call) {
|
||||
or
|
||||
allowedOwnerScope(call, "src/proxy-capture/proxy-server.ts", "startDebugProxyServer")
|
||||
or
|
||||
allowedOwnerScope(call, "extensions/codex-supervisor/src/json-rpc-client.ts", "connectCodexSupervisorUnixSocket")
|
||||
or
|
||||
allowedOwnerScope(call, "extensions/irc/src/client.ts", "connectIrcClient")
|
||||
or
|
||||
allowedOwnerScope(call, "extensions/qa-lab/src/lab-server-capture.ts", "probeTcpReachability")
|
||||
|
||||
30
.github/labeler.yml
vendored
30
.github/labeler.yml
vendored
@@ -47,12 +47,6 @@
|
||||
- "extensions/meeting-notes/**"
|
||||
- "docs/plugins/meeting-notes.md"
|
||||
- "src/meeting-notes/**"
|
||||
"plugin: workboard":
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- "extensions/workboard/**"
|
||||
- "docs/plugins/workboard.md"
|
||||
- "docs/plugins/reference/workboard.md"
|
||||
"plugin: migrate-hermes":
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
@@ -194,7 +188,7 @@
|
||||
- "ui/**"
|
||||
- "src/gateway/control-ui.ts"
|
||||
- "src/gateway/control-ui-shared.ts"
|
||||
- "packages/gateway-protocol/src/**"
|
||||
- "src/gateway/protocol/**"
|
||||
- "src/gateway/server-methods/chat.ts"
|
||||
- "src/infra/control-ui-assets.ts"
|
||||
|
||||
@@ -202,7 +196,6 @@
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- "src/gateway/**"
|
||||
- "packages/gateway-protocol/src/**"
|
||||
- "src/daemon/**"
|
||||
- "docs/gateway/**"
|
||||
|
||||
@@ -355,11 +348,6 @@
|
||||
- any-glob-to-any-file:
|
||||
- "extensions/deepinfra/**"
|
||||
- "docs/providers/deepinfra.md"
|
||||
"extensions: gmi":
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- "extensions/gmi/**"
|
||||
- "docs/providers/gmi.md"
|
||||
"extensions: tencent":
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
@@ -410,17 +398,6 @@
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- "extensions/codex/**"
|
||||
"extensions: codex-supervisor":
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- "extensions/codex-supervisor/**"
|
||||
- "docs/plugins/reference/codex-supervisor.md"
|
||||
- "docs/specs/claw-supervisor.md"
|
||||
"extensions: copilot":
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- "extensions/copilot/**"
|
||||
- "docs/plugins/copilot.md"
|
||||
"extensions: kimi-coding":
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
@@ -441,11 +418,6 @@
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- "extensions/nvidia/**"
|
||||
"extensions: novita":
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- "extensions/novita/**"
|
||||
- "docs/providers/novita.md"
|
||||
"extensions: phone-control":
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
|
||||
@@ -188,10 +188,7 @@ jobs:
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
timeout --signal=TERM --kill-after=10s 30s git \
|
||||
-c protocol.version=2 \
|
||||
fetch --no-tags --prune --no-recurse-submodules --depth=50 origin \
|
||||
"+refs/heads/main:refs/remotes/origin/main"
|
||||
git fetch --no-tags --depth=50 origin "+refs/heads/main:refs/remotes/origin/main"
|
||||
|
||||
node_bin="$(dirname "$(node -p 'process.execPath')")"
|
||||
sudo ln -sf "$node_bin/node" /usr/local/bin/node
|
||||
|
||||
5
.github/workflows/ci-check-testbox.yml
vendored
5
.github/workflows/ci-check-testbox.yml
vendored
@@ -89,10 +89,7 @@ jobs:
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
timeout --signal=TERM --kill-after=10s 30s git \
|
||||
-c protocol.version=2 \
|
||||
fetch --no-tags --prune --no-recurse-submodules --depth=50 origin \
|
||||
"+refs/heads/main:refs/remotes/origin/main"
|
||||
git fetch --no-tags --depth=50 origin "+refs/heads/main:refs/remotes/origin/main"
|
||||
|
||||
node_bin="$(dirname "$(node -p 'process.execPath')")"
|
||||
sudo ln -sf "$node_bin/node" /usr/local/bin/node
|
||||
|
||||
321
.github/workflows/ci.yml
vendored
321
.github/workflows/ci.yml
vendored
@@ -28,7 +28,7 @@ permissions:
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.event_name == 'workflow_dispatch' && format('{0}-manual-v1-{1}', github.workflow, github.run_id) || (github.event_name == 'pull_request' && format('{0}-v7-{1}', github.workflow, github.event.pull_request.number) || (github.repository == 'openclaw/openclaw' && format('{0}-v7-{1}', github.workflow, github.ref) || format('{0}-v7-{1}-{2}', github.workflow, github.ref, github.sha))) }}
|
||||
cancel-in-progress: ${{ github.event_name == 'pull_request' || (github.event_name == 'push' && github.repository == 'openclaw/openclaw' && github.ref == 'refs/heads/main') }}
|
||||
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
||||
|
||||
env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
|
||||
@@ -86,38 +86,12 @@ jobs:
|
||||
git init "$GITHUB_WORKSPACE"
|
||||
git -C "$GITHUB_WORKSPACE" config gc.auto 0
|
||||
git -C "$GITHUB_WORKSPACE" remote add origin "https://github.com/${CHECKOUT_REPO}.git"
|
||||
fetch_checkout_ref() {
|
||||
local ref="$1"
|
||||
local fetch_status
|
||||
for attempt in 1 2 3; do
|
||||
timeout --signal=TERM --kill-after=10s 30s git -C "$GITHUB_WORKSPACE" \
|
||||
-c protocol.version=2 \
|
||||
fetch --no-tags --prune --no-recurse-submodules --depth=1 origin \
|
||||
"+${ref}:refs/remotes/origin/checkout" && return 0
|
||||
fetch_status="$?"
|
||||
if [ "$fetch_status" != "124" ] && [ "$fetch_status" != "137" ]; then
|
||||
return "$fetch_status"
|
||||
fi
|
||||
if [ "$attempt" = "3" ]; then
|
||||
return "$fetch_status"
|
||||
fi
|
||||
echo "::warning::checkout fetch for '$ref' timed out on attempt $attempt; retrying"
|
||||
sleep 5
|
||||
done
|
||||
}
|
||||
if fetch_checkout_ref "$CHECKOUT_REF"; then
|
||||
:
|
||||
else
|
||||
fetch_status="$?"
|
||||
if [ "$fetch_status" = "124" ] || [ "$fetch_status" = "137" ]; then
|
||||
echo "::error::checkout fetch for '$CHECKOUT_REF' timed out"
|
||||
exit "$fetch_status"
|
||||
fi
|
||||
if ! git -C "$GITHUB_WORKSPACE" fetch --no-tags --depth=1 origin "+${CHECKOUT_REF}:refs/remotes/origin/checkout"; then
|
||||
if [ "$GITHUB_EVENT_NAME" != "workflow_dispatch" ] || [ "$CHECKOUT_REF" = "$CHECKOUT_FALLBACK_REF" ]; then
|
||||
exit "$fetch_status"
|
||||
exit 1
|
||||
fi
|
||||
echo "::warning::workflow_dispatch target_ref '$CHECKOUT_REF' is unavailable; falling back to head SHA '$CHECKOUT_FALLBACK_REF'"
|
||||
fetch_checkout_ref "$CHECKOUT_FALLBACK_REF"
|
||||
git -C "$GITHUB_WORKSPACE" fetch --no-tags --depth=1 origin "+${CHECKOUT_FALLBACK_REF}:refs/remotes/origin/checkout"
|
||||
fi
|
||||
git -C "$GITHUB_WORKSPACE" checkout --detach refs/remotes/origin/checkout
|
||||
|
||||
@@ -347,38 +321,12 @@ jobs:
|
||||
git init "$GITHUB_WORKSPACE"
|
||||
git -C "$GITHUB_WORKSPACE" config gc.auto 0
|
||||
git -C "$GITHUB_WORKSPACE" remote add origin "https://github.com/${CHECKOUT_REPO}.git"
|
||||
fetch_checkout_ref() {
|
||||
local ref="$1"
|
||||
local fetch_status
|
||||
for attempt in 1 2 3; do
|
||||
timeout --signal=TERM --kill-after=10s 30s git -C "$GITHUB_WORKSPACE" \
|
||||
-c protocol.version=2 \
|
||||
fetch --no-tags --prune --no-recurse-submodules --depth=1 origin \
|
||||
"+${ref}:refs/remotes/origin/checkout" && return 0
|
||||
fetch_status="$?"
|
||||
if [ "$fetch_status" != "124" ] && [ "$fetch_status" != "137" ]; then
|
||||
return "$fetch_status"
|
||||
fi
|
||||
if [ "$attempt" = "3" ]; then
|
||||
return "$fetch_status"
|
||||
fi
|
||||
echo "::warning::checkout fetch for '$ref' timed out on attempt $attempt; retrying"
|
||||
sleep 5
|
||||
done
|
||||
}
|
||||
if fetch_checkout_ref "$CHECKOUT_REF"; then
|
||||
:
|
||||
else
|
||||
fetch_status="$?"
|
||||
if [ "$fetch_status" = "124" ] || [ "$fetch_status" = "137" ]; then
|
||||
echo "::error::checkout fetch for '$CHECKOUT_REF' timed out"
|
||||
exit "$fetch_status"
|
||||
fi
|
||||
if ! git -C "$GITHUB_WORKSPACE" fetch --no-tags --depth=1 origin "+${CHECKOUT_REF}:refs/remotes/origin/checkout"; then
|
||||
if [ "$GITHUB_EVENT_NAME" != "workflow_dispatch" ] || [ "$CHECKOUT_REF" = "$CHECKOUT_FALLBACK_REF" ]; then
|
||||
exit "$fetch_status"
|
||||
exit 1
|
||||
fi
|
||||
echo "::warning::workflow_dispatch target_ref '$CHECKOUT_REF' is unavailable; falling back to head SHA '$CHECKOUT_FALLBACK_REF'"
|
||||
fetch_checkout_ref "$CHECKOUT_FALLBACK_REF"
|
||||
git -C "$GITHUB_WORKSPACE" fetch --no-tags --depth=1 origin "+${CHECKOUT_FALLBACK_REF}:refs/remotes/origin/checkout"
|
||||
fi
|
||||
git -C "$GITHUB_WORKSPACE" checkout --detach refs/remotes/origin/checkout
|
||||
|
||||
@@ -466,8 +414,8 @@ jobs:
|
||||
- name: Audit production dependencies
|
||||
run: node scripts/pre-commit/pnpm-audit-prod.mjs --audit-level=high
|
||||
|
||||
# Warm the lockfile- and pnpm-pinned store without blocking Linux Node shards.
|
||||
# On a cold key this job owns the save for later workflow runs.
|
||||
# Warm the lockfile- and pnpm-pinned store once before Linux Node shards fan out.
|
||||
# On a cold key this job owns the save, so later shards restore the exact key.
|
||||
pnpm-store-warmup:
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -532,9 +480,9 @@ jobs:
|
||||
build-artifacts:
|
||||
permissions:
|
||||
contents: read
|
||||
needs: [preflight]
|
||||
needs: [preflight, pnpm-store-warmup]
|
||||
if: needs.preflight.outputs.run_build_artifacts == 'true'
|
||||
runs-on: ${{ github.event_name == 'workflow_dispatch' && 'ubuntu-24.04' || (github.repository == 'openclaw/openclaw' && 'blacksmith-32vcpu-ubuntu-2404' || 'ubuntu-24.04') }}
|
||||
runs-on: ${{ github.event_name == 'workflow_dispatch' && 'ubuntu-24.04' || (github.repository == 'openclaw/openclaw' && 'blacksmith-16vcpu-ubuntu-2404' || 'ubuntu-24.04') }}
|
||||
timeout-minutes: 20
|
||||
outputs:
|
||||
channels-result: ${{ steps.built_artifact_checks.outputs['channels-result'] }}
|
||||
@@ -597,14 +545,6 @@ jobs:
|
||||
with:
|
||||
install-bun: "false"
|
||||
|
||||
- name: Restore build-all step cache
|
||||
uses: actions/cache@v5
|
||||
with:
|
||||
path: .artifacts/build-all-cache
|
||||
key: ${{ runner.os }}-build-all-v3-${{ hashFiles('package.json', 'pnpm-lock.yaml', 'npm-shrinkwrap.json', 'packages/plugin-sdk/package.json', 'packages/llm-core/package.json', 'packages/memory-host-sdk/package.json', 'scripts/build-all.mjs', 'scripts/write-plugin-sdk-entry-dts.ts', 'scripts/lib/plugin-sdk-entries.mjs', 'tsconfig.json', 'tsconfig.plugin-sdk.dts.json', 'src/plugin-sdk/**', 'packages/llm-core/src/**', 'packages/memory-host-sdk/src/**', 'src/types/**', 'src/video-generation/dashscope-compatible.ts', 'src/video-generation/types.ts', 'scripts/copy-export-html-templates.ts', 'scripts/lib/copy-assets.ts', 'src/auto-reply/reply/export-html/**') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-build-all-v3-
|
||||
|
||||
- name: Build dist
|
||||
env:
|
||||
NODE_OPTIONS: --max-old-space-size=8192
|
||||
@@ -702,6 +642,20 @@ jobs:
|
||||
pids+=("$!")
|
||||
}
|
||||
|
||||
if [ "$RUN_GATEWAY_WATCH" = "true" ]; then
|
||||
gateway_watch_log="${RUNNER_TEMP}/gateway-watch.log"
|
||||
echo "starting gateway-watch: node scripts/check-gateway-watch-regression.mjs --skip-build --ready-timeout-ms 5000"
|
||||
if node scripts/check-gateway-watch-regression.mjs --skip-build --ready-timeout-ms 5000 >"$gateway_watch_log" 2>&1; then
|
||||
result="success"
|
||||
else
|
||||
result="failure"
|
||||
fi
|
||||
echo "::group::gateway-watch log"
|
||||
cat "$gateway_watch_log"
|
||||
echo "::endgroup::"
|
||||
results["gateway-watch"]="$result"
|
||||
fi
|
||||
|
||||
if [ "$RUN_CHANNELS" = "true" ]; then
|
||||
start_check "channels" env \
|
||||
NODE_OPTIONS=--max-old-space-size=8192 \
|
||||
@@ -716,11 +670,6 @@ jobs:
|
||||
node scripts/run-vitest.mjs run --config test/vitest/vitest.full-core-support-boundary.config.ts
|
||||
fi
|
||||
|
||||
if [ "$RUN_GATEWAY_WATCH" = "true" ]; then
|
||||
start_check "gateway-watch" \
|
||||
node scripts/check-gateway-watch-regression.mjs --skip-build --ready-timeout-ms 5000
|
||||
fi
|
||||
|
||||
for index in "${!pids[@]}"; do
|
||||
name="${names[$index]}"
|
||||
log="${logs[$index]}"
|
||||
@@ -763,7 +712,7 @@ jobs:
|
||||
permissions:
|
||||
contents: read
|
||||
name: ${{ matrix.check_name }}
|
||||
needs: [preflight]
|
||||
needs: [preflight, pnpm-store-warmup]
|
||||
if: needs.preflight.outputs.run_checks_fast_core == 'true'
|
||||
runs-on: ${{ github.event_name == 'workflow_dispatch' && 'ubuntu-24.04' || (github.repository == 'openclaw/openclaw' && 'blacksmith-4vcpu-ubuntu-2404' || 'ubuntu-24.04') }}
|
||||
timeout-minutes: 60
|
||||
@@ -852,7 +801,7 @@ jobs:
|
||||
permissions:
|
||||
contents: read
|
||||
name: ${{ matrix.checkName }}
|
||||
needs: [preflight]
|
||||
needs: [preflight, pnpm-store-warmup]
|
||||
if: needs.preflight.outputs.run_plugin_contracts_shards == 'true'
|
||||
runs-on: ${{ github.event_name == 'workflow_dispatch' && 'ubuntu-24.04' || (github.repository == 'openclaw/openclaw' && 'blacksmith-4vcpu-ubuntu-2404' || 'ubuntu-24.04') }}
|
||||
timeout-minutes: 60
|
||||
@@ -932,7 +881,7 @@ jobs:
|
||||
permissions:
|
||||
contents: read
|
||||
name: ${{ matrix.checkName }}
|
||||
needs: [preflight]
|
||||
needs: [preflight, pnpm-store-warmup]
|
||||
if: needs.preflight.outputs.run_checks_fast == 'true'
|
||||
runs-on: ${{ github.event_name == 'workflow_dispatch' && 'ubuntu-24.04' || (github.repository == 'openclaw/openclaw' && 'blacksmith-4vcpu-ubuntu-2404' || 'ubuntu-24.04') }}
|
||||
timeout-minutes: 60
|
||||
@@ -1084,9 +1033,9 @@ jobs:
|
||||
permissions:
|
||||
contents: read
|
||||
name: ${{ matrix.check_name }}
|
||||
needs: [preflight]
|
||||
needs: [preflight, pnpm-store-warmup]
|
||||
if: needs.preflight.outputs.run_checks_node_core_nondist == 'true'
|
||||
runs-on: ${{ github.event_name == 'workflow_dispatch' && 'ubuntu-24.04' || (github.repository == 'openclaw/openclaw' && (matrix.runner || 'blacksmith-8vcpu-ubuntu-2404') || 'ubuntu-24.04') }}
|
||||
runs-on: ${{ github.event_name == 'workflow_dispatch' && 'ubuntu-24.04' || (github.repository == 'openclaw/openclaw' && (matrix.runner || 'ubuntu-24.04') || 'ubuntu-24.04') }}
|
||||
timeout-minutes: 60
|
||||
strategy:
|
||||
fail-fast: false
|
||||
@@ -1190,9 +1139,9 @@ jobs:
|
||||
permissions:
|
||||
contents: read
|
||||
name: ${{ matrix.check_name }}
|
||||
needs: [preflight]
|
||||
if: ${{ !cancelled() && always() && needs.preflight.outputs.run_check == 'true' }}
|
||||
runs-on: ${{ github.event_name == 'workflow_dispatch' && 'ubuntu-24.04' || (github.repository == 'openclaw/openclaw' && (matrix.runner || 'blacksmith-4vcpu-ubuntu-2404') || 'ubuntu-24.04') }}
|
||||
needs: [preflight, pnpm-store-warmup]
|
||||
if: ${{ !cancelled() && always() && needs.preflight.outputs.run_check == 'true' && needs.pnpm-store-warmup.result == 'success' }}
|
||||
runs-on: ${{ github.event_name == 'workflow_dispatch' && 'ubuntu-24.04' || (github.repository == 'openclaw/openclaw' && matrix.runner || 'ubuntu-24.04') }}
|
||||
timeout-minutes: 20
|
||||
strategy:
|
||||
fail-fast: false
|
||||
@@ -1321,8 +1270,8 @@ jobs:
|
||||
permissions:
|
||||
contents: read
|
||||
name: ${{ matrix.check_name }}
|
||||
needs: [preflight]
|
||||
if: ${{ !cancelled() && always() && needs.preflight.outputs.run_check_additional == 'true' }}
|
||||
needs: [preflight, pnpm-store-warmup]
|
||||
if: ${{ !cancelled() && always() && needs.preflight.outputs.run_check_additional == 'true' && needs.pnpm-store-warmup.result == 'success' }}
|
||||
runs-on: ${{ github.event_name == 'workflow_dispatch' && 'ubuntu-24.04' || (github.repository == 'openclaw/openclaw' && 'blacksmith-8vcpu-ubuntu-2404' || 'ubuntu-24.04') }}
|
||||
timeout-minutes: 20
|
||||
strategy:
|
||||
@@ -1403,7 +1352,7 @@ jobs:
|
||||
packages/plugin-sdk/dist
|
||||
extensions/*/dist/.boundary-tsc.tsbuildinfo
|
||||
extensions/*/dist/.boundary-tsc.stamp
|
||||
key: ${{ runner.os }}-extension-package-boundary-v1-${{ hashFiles('tsconfig.json', 'tsconfig.plugin-sdk.dts.json', 'packages/plugin-sdk/tsconfig.json', 'packages/llm-core/package.json', 'scripts/check-extension-package-tsc-boundary.mjs', 'scripts/prepare-extension-package-boundary-artifacts.mjs', 'scripts/write-plugin-sdk-entry-dts.ts', 'scripts/lib/plugin-sdk-entrypoints.json', 'scripts/lib/plugin-sdk-entries.mjs', 'src/plugin-sdk/**', 'src/auto-reply/**', 'packages/llm-core/src/**', 'src/video-generation/dashscope-compatible.ts', 'src/video-generation/types.ts', 'src/types/**', 'extensions/**', 'extensions/tsconfig.package-boundary*.json', 'package.json', 'pnpm-lock.yaml') }}
|
||||
key: ${{ runner.os }}-extension-package-boundary-v1-${{ hashFiles('tsconfig.json', 'tsconfig.plugin-sdk.dts.json', 'packages/plugin-sdk/tsconfig.json', 'scripts/check-extension-package-tsc-boundary.mjs', 'scripts/prepare-extension-package-boundary-artifacts.mjs', 'scripts/write-plugin-sdk-entry-dts.ts', 'scripts/lib/plugin-sdk-entrypoints.json', 'scripts/lib/plugin-sdk-entries.mjs', 'src/plugin-sdk/**', 'src/auto-reply/**', 'src/video-generation/dashscope-compatible.ts', 'src/video-generation/types.ts', 'src/types/**', 'extensions/**', 'extensions/tsconfig.package-boundary*.json', 'package.json', 'pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-extension-package-boundary-v1-
|
||||
|
||||
@@ -1420,16 +1369,10 @@ jobs:
|
||||
find src \
|
||||
-type f \( -name '*.ts' -o -name '*.tsx' -o -name '*.mts' -o -name '*.cts' -o -name '*.js' -o -name '*.mjs' -o -name '*.json' \) \
|
||||
-exec touch -t 200001010000 {} +
|
||||
if [ -d packages/llm-core/src ]; then
|
||||
find packages/llm-core/src \
|
||||
-type f \( -name '*.ts' -o -name '*.tsx' -o -name '*.mts' -o -name '*.cts' -o -name '*.js' -o -name '*.mjs' -o -name '*.json' \) \
|
||||
-exec touch -t 200001010000 {} +
|
||||
fi
|
||||
cache_inputs=(
|
||||
touch -t 200001010000 \
|
||||
tsconfig.json \
|
||||
tsconfig.plugin-sdk.dts.json \
|
||||
packages/plugin-sdk/tsconfig.json \
|
||||
packages/llm-core/package.json \
|
||||
scripts/check-extension-package-tsc-boundary.mjs \
|
||||
scripts/prepare-extension-package-boundary-artifacts.mjs \
|
||||
scripts/write-plugin-sdk-entry-dts.ts \
|
||||
@@ -1437,12 +1380,6 @@ jobs:
|
||||
scripts/lib/plugin-sdk-entries.mjs \
|
||||
package.json \
|
||||
pnpm-lock.yaml
|
||||
)
|
||||
for cache_input in "${cache_inputs[@]}"; do
|
||||
if [ -e "$cache_input" ]; then
|
||||
touch -t 200001010000 "$cache_input"
|
||||
fi
|
||||
done
|
||||
|
||||
- name: Run additional check shard
|
||||
env:
|
||||
@@ -1500,7 +1437,7 @@ jobs:
|
||||
check-docs:
|
||||
permissions:
|
||||
contents: read
|
||||
needs: [preflight]
|
||||
needs: [preflight, pnpm-store-warmup]
|
||||
if: needs.preflight.outputs.run_check_docs == 'true'
|
||||
runs-on: ${{ github.event_name == 'workflow_dispatch' && 'ubuntu-24.04' || (github.repository == 'openclaw/openclaw' && 'blacksmith-4vcpu-ubuntu-2404' || 'ubuntu-24.04') }}
|
||||
timeout-minutes: 20
|
||||
@@ -1557,44 +1494,11 @@ jobs:
|
||||
- name: Checkout ClawHub docs source
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
workdir="$GITHUB_WORKSPACE/clawhub-source"
|
||||
started_at="$(date +%s)"
|
||||
|
||||
reset_checkout_dir() {
|
||||
mkdir -p "$workdir"
|
||||
find "$workdir" -mindepth 1 -maxdepth 1 -exec rm -rf {} +
|
||||
}
|
||||
|
||||
checkout_attempt() {
|
||||
local attempt="$1"
|
||||
|
||||
reset_checkout_dir
|
||||
git init "$workdir" >/dev/null
|
||||
git -C "$workdir" config gc.auto 0
|
||||
git -C "$workdir" remote add origin "https://github.com/openclaw/clawhub.git"
|
||||
|
||||
timeout --signal=TERM --kill-after=10s 30s git -C "$workdir" \
|
||||
-c protocol.version=2 \
|
||||
fetch --no-tags --prune --no-recurse-submodules --depth=1 origin \
|
||||
"+refs/heads/main:refs/remotes/origin/checkout" || return 1
|
||||
|
||||
git -C "$workdir" checkout --force --detach refs/remotes/origin/checkout || return 1
|
||||
echo "ClawHub checkout attempt ${attempt}/5 succeeded"
|
||||
}
|
||||
|
||||
for attempt in 1 2 3 4 5; do
|
||||
if checkout_attempt "$attempt"; then
|
||||
elapsed="$(( $(date +%s) - started_at ))"
|
||||
echo "ClawHub checkout completed in ${elapsed}s"
|
||||
exit 0
|
||||
fi
|
||||
echo "ClawHub checkout attempt ${attempt}/5 failed"
|
||||
sleep $((attempt * 5))
|
||||
done
|
||||
|
||||
echo "ClawHub checkout failed after 5 attempts" >&2
|
||||
exit 1
|
||||
git init clawhub-source
|
||||
git -C clawhub-source config gc.auto 0
|
||||
git -C clawhub-source remote add origin "https://github.com/openclaw/clawhub.git"
|
||||
git -C clawhub-source fetch --no-tags --depth=1 origin "+HEAD:refs/remotes/origin/checkout"
|
||||
git -C clawhub-source checkout --detach refs/remotes/origin/checkout
|
||||
|
||||
- name: Check docs
|
||||
env:
|
||||
@@ -1618,25 +1522,7 @@ jobs:
|
||||
git init "$GITHUB_WORKSPACE"
|
||||
git -C "$GITHUB_WORKSPACE" config gc.auto 0
|
||||
git -C "$GITHUB_WORKSPACE" remote add origin "https://github.com/${CHECKOUT_REPO}.git"
|
||||
fetch_checkout_ref() {
|
||||
local fetch_status
|
||||
for attempt in 1 2 3; do
|
||||
timeout --signal=TERM --kill-after=10s 30s git -C "$GITHUB_WORKSPACE" \
|
||||
-c protocol.version=2 \
|
||||
fetch --no-tags --prune --no-recurse-submodules --depth=1 origin \
|
||||
"+${CHECKOUT_SHA}:refs/remotes/origin/checkout" && return 0
|
||||
fetch_status="$?"
|
||||
if [ "$fetch_status" != "124" ] && [ "$fetch_status" != "137" ]; then
|
||||
return "$fetch_status"
|
||||
fi
|
||||
if [ "$attempt" = "3" ]; then
|
||||
return "$fetch_status"
|
||||
fi
|
||||
echo "::warning::checkout fetch for '$CHECKOUT_SHA' timed out on attempt $attempt; retrying"
|
||||
sleep 5
|
||||
done
|
||||
}
|
||||
fetch_checkout_ref
|
||||
git -C "$GITHUB_WORKSPACE" fetch --no-tags --depth=1 origin "+${CHECKOUT_SHA}:refs/remotes/origin/checkout"
|
||||
git -C "$GITHUB_WORKSPACE" checkout --detach refs/remotes/origin/checkout
|
||||
|
||||
- name: Setup Python
|
||||
@@ -1684,28 +1570,7 @@ jobs:
|
||||
git init "$GITHUB_WORKSPACE"
|
||||
git -C "$GITHUB_WORKSPACE" config gc.auto 0
|
||||
git -C "$GITHUB_WORKSPACE" remote add origin "https://github.com/${CHECKOUT_REPO}.git"
|
||||
fetch_timeout_seconds=90
|
||||
fetch_checkout_ref() {
|
||||
git -C "$GITHUB_WORKSPACE" \
|
||||
-c protocol.version=2 \
|
||||
fetch --no-tags --prune --no-recurse-submodules --depth=1 origin \
|
||||
"+${CHECKOUT_SHA}:refs/remotes/origin/checkout" &
|
||||
local fetch_pid="$!"
|
||||
local elapsed=0
|
||||
while kill -0 "$fetch_pid" 2>/dev/null; do
|
||||
if [ "$elapsed" -ge "$fetch_timeout_seconds" ]; then
|
||||
kill -TERM "$fetch_pid" 2>/dev/null || true
|
||||
sleep 10
|
||||
kill -KILL "$fetch_pid" 2>/dev/null || true
|
||||
wait "$fetch_pid" || true
|
||||
return 124
|
||||
fi
|
||||
sleep 1
|
||||
elapsed=$((elapsed + 1))
|
||||
done
|
||||
wait "$fetch_pid"
|
||||
}
|
||||
fetch_checkout_ref
|
||||
git -C "$GITHUB_WORKSPACE" fetch --no-tags --depth=1 origin "+${CHECKOUT_SHA}:refs/remotes/origin/checkout"
|
||||
git -C "$GITHUB_WORKSPACE" checkout --detach refs/remotes/origin/checkout
|
||||
|
||||
- name: Try to exclude workspace from Windows Defender (best-effort)
|
||||
@@ -1805,28 +1670,7 @@ jobs:
|
||||
git init "$GITHUB_WORKSPACE"
|
||||
git -C "$GITHUB_WORKSPACE" config gc.auto 0
|
||||
git -C "$GITHUB_WORKSPACE" remote add origin "https://github.com/${CHECKOUT_REPO}.git"
|
||||
fetch_timeout_seconds=90
|
||||
fetch_checkout_ref() {
|
||||
git -C "$GITHUB_WORKSPACE" \
|
||||
-c protocol.version=2 \
|
||||
fetch --no-tags --prune --no-recurse-submodules --depth=1 origin \
|
||||
"+${CHECKOUT_SHA}:refs/remotes/origin/checkout" &
|
||||
local fetch_pid="$!"
|
||||
local elapsed=0
|
||||
while kill -0 "$fetch_pid" 2>/dev/null; do
|
||||
if [ "$elapsed" -ge "$fetch_timeout_seconds" ]; then
|
||||
kill -TERM "$fetch_pid" 2>/dev/null || true
|
||||
sleep 10
|
||||
kill -KILL "$fetch_pid" 2>/dev/null || true
|
||||
wait "$fetch_pid" || true
|
||||
return 124
|
||||
fi
|
||||
sleep 1
|
||||
elapsed=$((elapsed + 1))
|
||||
done
|
||||
wait "$fetch_pid"
|
||||
}
|
||||
fetch_checkout_ref
|
||||
git -C "$GITHUB_WORKSPACE" fetch --no-tags --depth=1 origin "+${CHECKOUT_SHA}:refs/remotes/origin/checkout"
|
||||
git -C "$GITHUB_WORKSPACE" checkout --detach refs/remotes/origin/checkout
|
||||
|
||||
- name: Setup Node environment
|
||||
@@ -1872,28 +1716,7 @@ jobs:
|
||||
git init "$GITHUB_WORKSPACE"
|
||||
git -C "$GITHUB_WORKSPACE" config gc.auto 0
|
||||
git -C "$GITHUB_WORKSPACE" remote add origin "https://github.com/${CHECKOUT_REPO}.git"
|
||||
fetch_timeout_seconds=90
|
||||
fetch_checkout_ref() {
|
||||
git -C "$GITHUB_WORKSPACE" \
|
||||
-c protocol.version=2 \
|
||||
fetch --no-tags --prune --no-recurse-submodules --depth=1 origin \
|
||||
"+${CHECKOUT_SHA}:refs/remotes/origin/checkout" &
|
||||
local fetch_pid="$!"
|
||||
local elapsed=0
|
||||
while kill -0 "$fetch_pid" 2>/dev/null; do
|
||||
if [ "$elapsed" -ge "$fetch_timeout_seconds" ]; then
|
||||
kill -TERM "$fetch_pid" 2>/dev/null || true
|
||||
sleep 10
|
||||
kill -KILL "$fetch_pid" 2>/dev/null || true
|
||||
wait "$fetch_pid" || true
|
||||
return 124
|
||||
fi
|
||||
sleep 1
|
||||
elapsed=$((elapsed + 1))
|
||||
done
|
||||
wait "$fetch_pid"
|
||||
}
|
||||
fetch_checkout_ref
|
||||
git -C "$GITHUB_WORKSPACE" fetch --no-tags --depth=1 origin "+${CHECKOUT_SHA}:refs/remotes/origin/checkout"
|
||||
git -C "$GITHUB_WORKSPACE" checkout --detach refs/remotes/origin/checkout
|
||||
|
||||
- name: Install XcodeGen / SwiftLint / SwiftFormat
|
||||
@@ -2104,53 +1927,3 @@ jobs:
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
ci-timings-summary:
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
name: ci-timings-summary
|
||||
needs:
|
||||
- preflight
|
||||
- security-fast
|
||||
- pnpm-store-warmup
|
||||
- build-artifacts
|
||||
- checks-fast-core
|
||||
- checks-fast-plugin-contracts-shard
|
||||
- checks-fast-channel-contracts-shard
|
||||
- checks-node-compat
|
||||
- checks-node-core-test-nondist-shard
|
||||
- check-shard
|
||||
- check-additional-shard
|
||||
- check-docs
|
||||
- skills-python
|
||||
- checks-windows
|
||||
- macos-node
|
||||
- macos-swift
|
||||
- android
|
||||
if: ${{ !cancelled() && always() && github.event_name != 'push' && (github.event_name != 'pull_request' || !github.event.pull_request.draft) }}
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- name: Checkout timing summary helper
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.base.sha || needs.preflight.outputs.checkout_revision || github.sha }}
|
||||
fetch-depth: 1
|
||||
fetch-tags: false
|
||||
persist-credentials: false
|
||||
submodules: false
|
||||
|
||||
- name: Write CI timing summary
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
node scripts/ci-run-timings.mjs "$GITHUB_RUN_ID" --limit 25 > ci-timings-summary.txt
|
||||
cat ci-timings-summary.txt >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
- name: Upload CI timing summary
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: ci-timings-summary
|
||||
path: ci-timings-summary.txt
|
||||
retention-days: 14
|
||||
|
||||
9
.github/workflows/clawsweeper-dispatch.yml
vendored
9
.github/workflows/clawsweeper-dispatch.yml
vendored
@@ -24,14 +24,7 @@ concurrency:
|
||||
jobs:
|
||||
dispatch:
|
||||
runs-on: ubuntu-latest
|
||||
if: >-
|
||||
${{
|
||||
github.event_name == 'issue_comment' ||
|
||||
!(
|
||||
endsWith(github.actor, '[bot]') &&
|
||||
(github.event.action == 'labeled' || github.event.action == 'unlabeled')
|
||||
)
|
||||
}}
|
||||
if: ${{ github.event_name == 'issue_comment' || !(endsWith(github.actor, '[bot]') && (github.event.action == 'labeled' || github.event.action == 'unlabeled')) }}
|
||||
env:
|
||||
HAS_CLAWSWEEPER_APP_PRIVATE_KEY: ${{ secrets.CLAWSWEEPER_APP_PRIVATE_KEY != '' }}
|
||||
CLAWSWEEPER_APP_CLIENT_ID: Iv23liOECG0slfuhz093
|
||||
|
||||
11
.github/workflows/codeql-critical-quality.yml
vendored
11
.github/workflows/codeql-critical-quality.yml
vendored
@@ -33,7 +33,6 @@ on:
|
||||
- "packages/plugin-package-contract/**"
|
||||
- "packages/plugin-sdk/**"
|
||||
- "packages/memory-host-sdk/**"
|
||||
- "packages/net-policy/**"
|
||||
- "src/*.ts"
|
||||
- "src/**/*.ts"
|
||||
- "src/config/**"
|
||||
@@ -107,13 +106,13 @@ on:
|
||||
- "src/gateway/**/*auth*.ts"
|
||||
- "src/gateway/*secret*.ts"
|
||||
- "src/gateway/**/*secret*.ts"
|
||||
- "packages/gateway-protocol/src/**/*secret*.ts"
|
||||
- "src/gateway/protocol/**/*secret*.ts"
|
||||
- "src/gateway/resolve-configured-secret-input-string*.ts"
|
||||
- "src/gateway/security-path*.ts"
|
||||
- "src/gateway/server-methods/secrets*.ts"
|
||||
- "src/gateway/server-startup-memory.ts"
|
||||
- "src/gateway/method-scopes.ts"
|
||||
- "packages/gateway-protocol/src/**"
|
||||
- "src/gateway/protocol/**"
|
||||
- "src/gateway/server-methods/**"
|
||||
- "src/gateway/server-methods.ts"
|
||||
- "src/gateway/server-methods-list.ts"
|
||||
@@ -245,14 +244,14 @@ jobs:
|
||||
src/config/*)
|
||||
config=true
|
||||
;;
|
||||
packages/gateway-protocol/src/*secret*.ts|packages/gateway-protocol/src/**/*secret*.ts|src/gateway/server-methods/secrets*.ts)
|
||||
src/gateway/protocol/*secret*.ts|src/gateway/server-methods/secrets*.ts)
|
||||
core_auth_secrets=true
|
||||
gateway=true
|
||||
;;
|
||||
src/agents/*auth*.ts|src/agents/auth-health*.ts|src/agents/auth-profiles|src/agents/auth-profiles/*|src/agents/bash-tools.exec-host-shared.ts|src/agents/sandbox|src/agents/sandbox.ts|src/agents/sandbox-*.ts|src/agents/sandbox/*|src/cron/service/jobs.ts|src/cron/stagger.ts|src/gateway/*auth*.ts|src/gateway/*secret*.ts|src/gateway/resolve-configured-secret-input-string*.ts|src/gateway/security-path*.ts|src/infra/secret-file*.ts|src/secrets/*|src/security/*)
|
||||
core_auth_secrets=true
|
||||
;;
|
||||
packages/gateway-protocol/src/*|packages/gateway-protocol/src/**/*|src/gateway/method-scopes.ts|src/gateway/server-methods/*|src/gateway/server-methods.ts|src/gateway/server-methods-list.ts)
|
||||
src/gateway/method-scopes.ts|src/gateway/protocol/*|src/gateway/server-methods/*|src/gateway/server-methods.ts|src/gateway/server-methods-list.ts)
|
||||
gateway=true
|
||||
;;
|
||||
packages/memory-host-sdk/*|src/commands/doctor-cron-dreaming-payload-migration.ts|src/commands/doctor-memory-search.ts|src/gateway/server-startup-memory.ts|src/memory/*|src/memory-host-sdk/*)
|
||||
@@ -302,7 +301,7 @@ jobs:
|
||||
esac
|
||||
|
||||
case "${file}" in
|
||||
src/*.ts|src/**/*.ts|extensions/*.ts|extensions/**/*.ts|packages/net-policy/src/*|packages/net-policy/src/**/*)
|
||||
src/*.ts|src/**/*.ts|extensions/*.ts|extensions/**/*.ts)
|
||||
network_runtime=true
|
||||
;;
|
||||
esac
|
||||
|
||||
11
.github/workflows/codeql.yml
vendored
11
.github/workflows/codeql.yml
vendored
@@ -85,21 +85,10 @@ jobs:
|
||||
config_file: ./.github/codeql/codeql-actions-critical-security.yml
|
||||
steps:
|
||||
- name: Checkout
|
||||
if: ${{ matrix.category != 'actions' }}
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
submodules: false
|
||||
|
||||
- name: Checkout Actions security sources
|
||||
if: ${{ matrix.category == 'actions' }}
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
submodules: false
|
||||
sparse-checkout: |
|
||||
.github/actions
|
||||
.github/workflows
|
||||
.github/codeql
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4
|
||||
with:
|
||||
|
||||
@@ -138,7 +138,7 @@ jobs:
|
||||
OPENAI_API_KEY: ${{ secrets.OPENCLAW_DOCS_I18N_OPENAI_API_KEY || secrets.OPENAI_API_KEY }}
|
||||
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
OPENCLAW_CONTROL_UI_I18N_PROVIDER: ${{ secrets.ANTHROPIC_API_KEY != '' && 'anthropic' || 'openai' }}
|
||||
OPENCLAW_CONTROL_UI_I18N_MODEL: ${{ secrets.ANTHROPIC_API_KEY != '' && 'claude-opus-4-8' || vars.OPENCLAW_CI_OPENAI_MODEL_BARE }}
|
||||
OPENCLAW_CONTROL_UI_I18N_MODEL: ${{ secrets.ANTHROPIC_API_KEY != '' && 'claude-opus-4-7' || vars.OPENCLAW_CI_OPENAI_MODEL_BARE }}
|
||||
OPENCLAW_CONTROL_UI_I18N_THINKING: low
|
||||
OPENCLAW_CONTROL_UI_I18N_AUTH_OPTIONAL: "1"
|
||||
LOCALE: ${{ matrix.locale }}
|
||||
|
||||
31
.github/workflows/crabbox-hydrate.yml
vendored
31
.github/workflows/crabbox-hydrate.yml
vendored
@@ -137,10 +137,7 @@ jobs:
|
||||
set -euo pipefail
|
||||
|
||||
if git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
||||
timeout --signal=TERM --kill-after=10s 30s git \
|
||||
-c protocol.version=2 \
|
||||
fetch --no-tags --prune --no-recurse-submodules --depth=50 origin \
|
||||
"+refs/heads/main:refs/remotes/origin/main"
|
||||
git fetch --no-tags --depth=50 origin "+refs/heads/main:refs/remotes/origin/main"
|
||||
fi
|
||||
|
||||
- name: Prepare Crabbox shell
|
||||
@@ -321,26 +318,7 @@ jobs:
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
if (git rev-parse --is-inside-work-tree 2>$null) {
|
||||
$repo = (Get-Location).Path
|
||||
$fetchInfo = New-Object System.Diagnostics.ProcessStartInfo
|
||||
$fetchInfo.FileName = "git"
|
||||
$fetchInfo.WorkingDirectory = $repo
|
||||
$fetchInfo.UseShellExecute = $false
|
||||
$fetchInfo.Arguments = '-c protocol.version=2 fetch --no-tags --no-progress --prune --no-recurse-submodules --depth=50 origin "+refs/heads/main:refs/remotes/origin/main"'
|
||||
|
||||
$fetch = New-Object System.Diagnostics.Process
|
||||
$fetch.StartInfo = $fetchInfo
|
||||
if (-not $fetch.Start()) {
|
||||
throw "git fetch failed to start"
|
||||
}
|
||||
if (-not $fetch.WaitForExit(30000)) {
|
||||
$fetch.Kill()
|
||||
$fetch.WaitForExit()
|
||||
throw "git fetch timed out after 30 seconds"
|
||||
}
|
||||
if ($fetch.ExitCode -ne 0) {
|
||||
throw "git fetch failed with exit code $($fetch.ExitCode)"
|
||||
}
|
||||
git fetch --no-tags --depth=50 origin "+refs/heads/main:refs/remotes/origin/main"
|
||||
}
|
||||
|
||||
- name: Setup pnpm and dependencies
|
||||
@@ -535,10 +513,7 @@ jobs:
|
||||
set -euo pipefail
|
||||
|
||||
if git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
||||
timeout --signal=TERM --kill-after=10s 30s git \
|
||||
-c protocol.version=2 \
|
||||
fetch --no-tags --prune --no-recurse-submodules --depth=50 origin \
|
||||
"+refs/heads/main:refs/remotes/origin/main"
|
||||
git fetch --no-tags --depth=50 origin "+refs/heads/main:refs/remotes/origin/main"
|
||||
fi
|
||||
|
||||
node_bin="$(dirname "$(node -p 'process.execPath')")"
|
||||
|
||||
176
.github/workflows/dependency-change-awareness.yml
vendored
Normal file
176
.github/workflows/dependency-change-awareness.yml
vendored
Normal file
@@ -0,0 +1,176 @@
|
||||
name: Dependency Change Awareness
|
||||
|
||||
on:
|
||||
pull_request_target: # zizmor: ignore[dangerous-triggers] metadata-only workflow; no checkout or untrusted code execution
|
||||
types: [opened, reopened, synchronize, ready_for_review]
|
||||
|
||||
permissions:
|
||||
pull-requests: write
|
||||
issues: write
|
||||
|
||||
concurrency:
|
||||
group: dependency-change-awareness-${{ github.event.pull_request.number }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
dependency-change-awareness:
|
||||
if: ${{ !github.event.pull_request.draft }}
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- name: Label and comment on dependency changes
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
|
||||
with:
|
||||
script: |
|
||||
const marker = "<!-- openclaw:dependency-change-awareness -->";
|
||||
const labelName = "dependencies-changed";
|
||||
const maxListedFiles = 25;
|
||||
const pullRequest = context.payload.pull_request;
|
||||
|
||||
if (!pullRequest) {
|
||||
core.info("No pull_request payload found; skipping.");
|
||||
return;
|
||||
}
|
||||
|
||||
const isDependencyFile = (filename) =>
|
||||
filename === "package.json" ||
|
||||
filename === "package-lock.json" ||
|
||||
filename === "npm-shrinkwrap.json" ||
|
||||
filename === "pnpm-lock.yaml" ||
|
||||
filename === "pnpm-workspace.yaml" ||
|
||||
filename === "ui/package.json" ||
|
||||
filename.startsWith("patches/") ||
|
||||
/^packages\/[^/]+\/package\.json$/u.test(filename) ||
|
||||
/^extensions\/[^/]+\/package-lock\.json$/u.test(filename) ||
|
||||
/^extensions\/[^/]+\/npm-shrinkwrap\.json$/u.test(filename) ||
|
||||
/^extensions\/[^/]+\/package\.json$/u.test(filename);
|
||||
|
||||
const sanitizeDisplayValue = (value) =>
|
||||
String(value)
|
||||
.replace(/[\u0000-\u001f\u007f]/gu, "?")
|
||||
.slice(0, 240);
|
||||
const markdownCode = (value) =>
|
||||
`\`${sanitizeDisplayValue(value).replaceAll("`", "\\`")}\``;
|
||||
const ignoreUnavailableWritePermission = (action) => (error) => {
|
||||
if (error?.status === 403) {
|
||||
core.warning(
|
||||
`Skipping dependency change ${action}; token does not have issue write permission.`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (error?.status === 404 || error?.status === 422) {
|
||||
core.warning(`Dependency change ${action} is unavailable.`);
|
||||
return;
|
||||
}
|
||||
throw error;
|
||||
};
|
||||
|
||||
const files = await github.paginate(github.rest.pulls.listFiles, {
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
pull_number: pullRequest.number,
|
||||
per_page: 100,
|
||||
});
|
||||
const dependencyFiles = files
|
||||
.map((file) => file.filename)
|
||||
.filter((filename) => typeof filename === "string" && isDependencyFile(filename))
|
||||
.sort((left, right) => left.localeCompare(right));
|
||||
|
||||
const comments = await github.paginate(github.rest.issues.listComments, {
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: pullRequest.number,
|
||||
per_page: 100,
|
||||
});
|
||||
const existingComment = comments.find(
|
||||
(comment) =>
|
||||
comment.user?.login === "github-actions[bot]" && comment.body?.includes(marker),
|
||||
);
|
||||
|
||||
const labels = await github.paginate(github.rest.issues.listLabelsOnIssue, {
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: pullRequest.number,
|
||||
per_page: 100,
|
||||
});
|
||||
const hasLabel = labels.some((label) => label.name === labelName);
|
||||
|
||||
if (dependencyFiles.length === 0) {
|
||||
if (hasLabel) {
|
||||
await github.rest.issues.removeLabel({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: pullRequest.number,
|
||||
name: labelName,
|
||||
}).catch(ignoreUnavailableWritePermission("label removal"));
|
||||
}
|
||||
if (existingComment) {
|
||||
await github.rest.issues.deleteComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
comment_id: existingComment.id,
|
||||
}).catch(ignoreUnavailableWritePermission("comment deletion"));
|
||||
}
|
||||
await core.summary
|
||||
.addHeading("Dependency Change Awareness")
|
||||
.addRaw("No dependency-related file changes detected.")
|
||||
.write();
|
||||
core.info("No dependency-related file changes detected.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!hasLabel) {
|
||||
await github.rest.issues.addLabels({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: pullRequest.number,
|
||||
labels: [labelName],
|
||||
}).catch(ignoreUnavailableWritePermission(`label "${labelName}" update`));
|
||||
}
|
||||
|
||||
const listedFiles = dependencyFiles.slice(0, maxListedFiles);
|
||||
const omittedCount = dependencyFiles.length - listedFiles.length;
|
||||
const fileLines = listedFiles.map((filename) => `- ${markdownCode(filename)}`);
|
||||
if (omittedCount > 0) {
|
||||
fileLines.push(`- ${omittedCount} additional dependency-related files not shown`);
|
||||
}
|
||||
|
||||
const body = [
|
||||
marker,
|
||||
"",
|
||||
"### Dependency Changes Detected",
|
||||
"",
|
||||
"This PR changes dependency-related files. Maintainers should confirm these changes are intentional.",
|
||||
"",
|
||||
"Changed files:",
|
||||
...fileLines,
|
||||
"",
|
||||
"Maintainer follow-up:",
|
||||
"- Review whether the dependency changes are intentional.",
|
||||
"- Inspect resolved package deltas when lockfile, shrinkwrap, or workspace dependency policy changes are present.",
|
||||
"- Treat `package-lock.json` and `npm-shrinkwrap.json` diffs as security-review surfaces.",
|
||||
"- Run `pnpm deps:changes:report -- --base-ref origin/main --markdown /tmp/dependency-changes.md --json /tmp/dependency-changes.json` locally for detailed release-style evidence.",
|
||||
].join("\n");
|
||||
|
||||
if (existingComment) {
|
||||
await github.rest.issues.updateComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
comment_id: existingComment.id,
|
||||
body,
|
||||
}).catch(ignoreUnavailableWritePermission("comment update"));
|
||||
} else {
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: pullRequest.number,
|
||||
body,
|
||||
}).catch(ignoreUnavailableWritePermission("comment creation"));
|
||||
}
|
||||
|
||||
await core.summary
|
||||
.addHeading("Dependency Change Awareness")
|
||||
.addRaw(`Detected ${dependencyFiles.length} dependency-related file change(s).`)
|
||||
.addList(dependencyFiles.map((filename) => markdownCode(filename)))
|
||||
.write();
|
||||
core.notice(`Detected ${dependencyFiles.length} dependency-related file change(s).`);
|
||||
109
.github/workflows/dependency-guard.yml
vendored
109
.github/workflows/dependency-guard.yml
vendored
@@ -1,109 +0,0 @@
|
||||
name: Dependency Guard
|
||||
|
||||
on:
|
||||
pull_request_target: # zizmor: ignore[dangerous-triggers] checks trusted base script only; never checks out PR head
|
||||
types: [opened, reopened, synchronize, ready_for_review]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
issues: write
|
||||
|
||||
concurrency:
|
||||
group: dependency-guard-${{ github.event.pull_request.number }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
dependency-guard-detect:
|
||||
if: ${{ !github.event.pull_request.draft }}
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 5
|
||||
outputs:
|
||||
autoscrub: ${{ steps.guard.outputs.autoscrub }}
|
||||
autoscrub-owner: ${{ steps.guard.outputs.autoscrub-owner }}
|
||||
autoscrub-repository: ${{ steps.guard.outputs.autoscrub-repository }}
|
||||
steps:
|
||||
- name: Check out trusted base workflow scripts
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.base.sha }}
|
||||
persist-credentials: false
|
||||
|
||||
- name: Detect dependency changes
|
||||
id: guard
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
OPENCLAW_DEPENDENCY_GUARD_MODE: detect
|
||||
OPENCLAW_SECURITY_APPROVERS: vincentkoc,steipete,joshavant
|
||||
OPENCLAW_SECURITY_TEAM_SLUG: openclaw-secops
|
||||
run: node scripts/github/dependency-guard.mjs
|
||||
|
||||
dependency-guard-autoscrub:
|
||||
if: ${{ !github.event.pull_request.draft && needs.dependency-guard-detect.outputs.autoscrub == 'true' }}
|
||||
needs: dependency-guard-detect
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 5
|
||||
permissions:
|
||||
contents: read
|
||||
issues: write
|
||||
pull-requests: read
|
||||
steps:
|
||||
- name: Check out trusted base workflow scripts
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.base.sha }}
|
||||
persist-credentials: false
|
||||
|
||||
- name: Create autoscrub app token
|
||||
id: app-token
|
||||
continue-on-error: true
|
||||
uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1
|
||||
with:
|
||||
app-id: "2729701"
|
||||
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
|
||||
owner: ${{ needs.dependency-guard-detect.outputs.autoscrub-owner }}
|
||||
repositories: ${{ needs.dependency-guard-detect.outputs.autoscrub-repository }}
|
||||
permission-contents: write
|
||||
|
||||
- name: Create fallback autoscrub app token
|
||||
id: app-token-fallback
|
||||
continue-on-error: true
|
||||
if: steps.app-token.outcome == 'failure'
|
||||
uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1
|
||||
with:
|
||||
app-id: "2971289"
|
||||
private-key: ${{ secrets.GH_APP_PRIVATE_KEY_FALLBACK }}
|
||||
owner: ${{ needs.dependency-guard-detect.outputs.autoscrub-owner }}
|
||||
repositories: ${{ needs.dependency-guard-detect.outputs.autoscrub-repository }}
|
||||
permission-contents: write
|
||||
|
||||
- name: Remove package lockfile changes
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
OPENCLAW_DEPENDENCY_GUARD_AUTOSCRUB_TOKEN: ${{ steps.app-token.outputs.token || steps.app-token-fallback.outputs.token }}
|
||||
OPENCLAW_DEPENDENCY_GUARD_MODE: autoscrub
|
||||
OPENCLAW_SECURITY_APPROVERS: vincentkoc,steipete,joshavant
|
||||
OPENCLAW_SECURITY_TEAM_SLUG: openclaw-secops
|
||||
run: node scripts/github/dependency-guard.mjs
|
||||
|
||||
dependency-guard:
|
||||
if: ${{ !github.event.pull_request.draft && always() }}
|
||||
needs:
|
||||
- dependency-guard-detect
|
||||
- dependency-guard-autoscrub
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- name: Check out trusted base workflow scripts
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.base.sha }}
|
||||
persist-credentials: false
|
||||
|
||||
- name: Enforce dependency guard
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
OPENCLAW_DEPENDENCY_GUARD_MODE: enforce
|
||||
OPENCLAW_SECURITY_APPROVERS: vincentkoc,steipete,joshavant
|
||||
OPENCLAW_SECURITY_TEAM_SLUG: openclaw-secops
|
||||
run: node scripts/github/dependency-guard.mjs
|
||||
4
.github/workflows/install-smoke.yml
vendored
4
.github/workflows/install-smoke.yml
vendored
@@ -143,7 +143,7 @@ jobs:
|
||||
for (const [dep, rel] of Object.entries(workspace.patchedDependencies ?? {})) {
|
||||
const absolute = path.join(\"/app\", rel);
|
||||
if (!fs.existsSync(absolute)) {
|
||||
throw new Error(\"missing patch for \" + dep + \": \" + rel);
|
||||
throw new Error(`missing patch for ${dep}: ${rel}`);
|
||||
}
|
||||
}
|
||||
"
|
||||
@@ -337,7 +337,7 @@ jobs:
|
||||
for (const [dep, rel] of Object.entries(workspace.patchedDependencies ?? {})) {
|
||||
const absolute = path.join(\"/app\", rel);
|
||||
if (!fs.existsSync(absolute)) {
|
||||
throw new Error(\"missing patch for \" + dep + \": \" + rel);
|
||||
throw new Error(`missing patch for ${dep}: ${rel}`);
|
||||
}
|
||||
}
|
||||
"
|
||||
|
||||
1
.github/workflows/mantis-telegram-live.yml
vendored
1
.github/workflows/mantis-telegram-live.yml
vendored
@@ -377,7 +377,6 @@ jobs:
|
||||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||
OPENCLAW_QA_CONVEX_SITE_URL: ${{ secrets.OPENCLAW_QA_CONVEX_SITE_URL }}
|
||||
OPENCLAW_QA_CONVEX_SECRET_CI: ${{ secrets.OPENCLAW_QA_CONVEX_SECRET_CI }}
|
||||
OPENCLAW_QA_CREDENTIAL_ACQUIRE_TIMEOUT_MS: "1800000"
|
||||
OPENCLAW_QA_REDACT_PUBLIC_METADATA: "1"
|
||||
OPENCLAW_QA_TELEGRAM_CAPTURE_CONTENT: "1"
|
||||
CRABBOX_COORDINATOR: ${{ secrets.CRABBOX_COORDINATOR }}
|
||||
|
||||
1
.github/workflows/npm-telegram-beta-e2e.yml
vendored
1
.github/workflows/npm-telegram-beta-e2e.yml
vendored
@@ -218,7 +218,6 @@ jobs:
|
||||
OPENCLAW_NPM_TELEGRAM_CREDENTIAL_ROLE: ci
|
||||
OPENCLAW_QA_CONVEX_SITE_URL: ${{ secrets.OPENCLAW_QA_CONVEX_SITE_URL }}
|
||||
OPENCLAW_QA_CONVEX_SECRET_CI: ${{ secrets.OPENCLAW_QA_CONVEX_SECRET_CI }}
|
||||
OPENCLAW_QA_CREDENTIAL_ACQUIRE_TIMEOUT_MS: "1800000"
|
||||
OPENCLAW_QA_REDACT_PUBLIC_METADATA: "1"
|
||||
OPENCLAW_QA_TELEGRAM_CAPTURE_CONTENT: "1"
|
||||
INPUT_SCENARIO: ${{ inputs.scenario }}
|
||||
|
||||
@@ -451,7 +451,7 @@ jobs:
|
||||
OUTPUT_DIR: ${{ runner.temp }}/openclaw-cross-os-release-checks/prepare/baseline
|
||||
run: |
|
||||
mkdir -p "${OUTPUT_DIR}"
|
||||
timeout --preserve-status 300s npm pack --ignore-scripts --json "${BASELINE_SPEC}" --pack-destination "${OUTPUT_DIR}" > "${OUTPUT_DIR}/pack.json"
|
||||
npm pack --ignore-scripts --json "${BASELINE_SPEC}" --pack-destination "${OUTPUT_DIR}" > "${OUTPUT_DIR}/pack.json"
|
||||
|
||||
- name: Capture candidate metadata
|
||||
id: candidate_metadata
|
||||
|
||||
@@ -1932,7 +1932,7 @@ jobs:
|
||||
- suite_id: native-live-src-gateway-profiles-anthropic-opus
|
||||
suite_group: native-live-src-gateway-profiles-anthropic
|
||||
label: Native live gateway profiles Anthropic Opus
|
||||
command: OPENCLAW_LIVE_GATEWAY_THINKING=low OPENCLAW_LIVE_GATEWAY_PROVIDERS=anthropic OPENCLAW_LIVE_GATEWAY_MODELS=anthropic/claude-opus-4-8 node .release-harness/scripts/test-live-shard.mjs native-live-src-gateway-profiles
|
||||
command: OPENCLAW_LIVE_GATEWAY_THINKING=low OPENCLAW_LIVE_GATEWAY_PROVIDERS=anthropic OPENCLAW_LIVE_GATEWAY_MODELS=anthropic/claude-opus-4-7 node .release-harness/scripts/test-live-shard.mjs native-live-src-gateway-profiles
|
||||
timeout_minutes: 30
|
||||
profile_env_only: false
|
||||
advisory: true
|
||||
@@ -1947,19 +1947,19 @@ jobs:
|
||||
profiles: full
|
||||
- suite_id: native-live-src-gateway-profiles-google
|
||||
label: Native live gateway profiles Google
|
||||
command: OPENCLAW_LIVE_GATEWAY_PROVIDERS=google OPENCLAW_LIVE_GATEWAY_MODELS=google/gemini-3.1-pro-preview node .release-harness/scripts/test-live-shard.mjs native-live-src-gateway-profiles
|
||||
command: OPENCLAW_LIVE_GATEWAY_PROVIDERS=google OPENCLAW_LIVE_GATEWAY_MODELS=google/gemini-3.1-pro-preview,google/gemini-3-flash-preview node .release-harness/scripts/test-live-shard.mjs native-live-src-gateway-profiles
|
||||
timeout_minutes: 60
|
||||
profile_env_only: false
|
||||
profiles: stable full
|
||||
- suite_id: native-live-src-gateway-profiles-minimax
|
||||
label: Native live gateway profiles MiniMax
|
||||
command: OPENCLAW_LIVE_GATEWAY_PROVIDERS=minimax,minimax-portal OPENCLAW_LIVE_GATEWAY_MODELS=minimax/MiniMax-M2.7,minimax-portal/MiniMax-M2.7 OPENCLAW_LIVE_GATEWAY_MAX_MODELS=2 node .release-harness/scripts/test-live-shard.mjs native-live-src-gateway-profiles
|
||||
command: OPENCLAW_LIVE_GATEWAY_PROVIDERS=minimax,minimax-portal OPENCLAW_LIVE_GATEWAY_MAX_MODELS=2 node .release-harness/scripts/test-live-shard.mjs native-live-src-gateway-profiles
|
||||
timeout_minutes: 60
|
||||
profile_env_only: false
|
||||
profiles: stable full
|
||||
- suite_id: native-live-src-gateway-profiles-openai
|
||||
label: Native live gateway profiles OpenAI
|
||||
command: OPENCLAW_LIVE_GATEWAY_THINKING=off OPENCLAW_LIVE_GATEWAY_PROVIDERS=openai OPENCLAW_LIVE_GATEWAY_MODELS=openai/gpt-5.5 OPENCLAW_LIVE_GATEWAY_STEP_TIMEOUT_MS=180000 OPENCLAW_LIVE_GATEWAY_MODEL_TIMEOUT_MS=600000 node .release-harness/scripts/test-live-shard.mjs native-live-src-gateway-profiles
|
||||
command: OPENCLAW_LIVE_GATEWAY_PROVIDERS=openai OPENCLAW_LIVE_GATEWAY_MODELS=openai/gpt-5.5 node .release-harness/scripts/test-live-shard.mjs native-live-src-gateway-profiles
|
||||
timeout_minutes: 60
|
||||
profile_env_only: false
|
||||
profiles: beta minimum stable full
|
||||
@@ -2234,7 +2234,7 @@ jobs:
|
||||
include:
|
||||
- suite_id: live-gateway-docker
|
||||
label: Docker live gateway OpenAI
|
||||
command: OPENCLAW_LIVE_GATEWAY_THINKING=off OPENCLAW_LIVE_GATEWAY_PROVIDERS=openai OPENCLAW_LIVE_GATEWAY_MODELS=openai/gpt-5.5 OPENCLAW_LIVE_GATEWAY_MAX_MODELS=1 OPENCLAW_LIVE_GATEWAY_STEP_TIMEOUT_MS=90000 OPENCLAW_LIVE_GATEWAY_MODEL_TIMEOUT_MS=600000 OPENCLAW_LIVE_DOCKER_REPO_ROOT="$GITHUB_WORKSPACE" timeout --foreground --kill-after=30s 35m bash .release-harness/scripts/test-live-gateway-models-docker.sh
|
||||
command: OPENCLAW_LIVE_GATEWAY_THINKING=low OPENCLAW_LIVE_GATEWAY_PROVIDERS=openai OPENCLAW_LIVE_GATEWAY_MODELS=openai/gpt-5.5 OPENCLAW_LIVE_GATEWAY_MAX_MODELS=1 OPENCLAW_LIVE_GATEWAY_STEP_TIMEOUT_MS=90000 OPENCLAW_LIVE_GATEWAY_MODEL_TIMEOUT_MS=600000 OPENCLAW_LIVE_DOCKER_REPO_ROOT="$GITHUB_WORKSPACE" timeout --foreground --kill-after=30s 35m bash .release-harness/scripts/test-live-gateway-models-docker.sh
|
||||
timeout_minutes: 40
|
||||
profile_env_only: false
|
||||
profiles: beta minimum stable full
|
||||
@@ -2246,13 +2246,13 @@ jobs:
|
||||
profiles: stable full
|
||||
- suite_id: live-gateway-google-docker
|
||||
label: Docker live gateway Google
|
||||
command: OPENCLAW_LIVE_GATEWAY_PROVIDERS=google OPENCLAW_LIVE_GATEWAY_MODELS=google/gemini-3.1-pro-preview OPENCLAW_LIVE_GATEWAY_MAX_MODELS=1 OPENCLAW_LIVE_GATEWAY_STEP_TIMEOUT_MS=90000 OPENCLAW_LIVE_GATEWAY_MODEL_TIMEOUT_MS=180000 OPENCLAW_LIVE_DOCKER_REPO_ROOT="$GITHUB_WORKSPACE" timeout --foreground --kill-after=30s 35m bash .release-harness/scripts/test-live-gateway-models-docker.sh
|
||||
command: OPENCLAW_LIVE_GATEWAY_PROVIDERS=google OPENCLAW_LIVE_GATEWAY_MODELS=google/gemini-3.1-pro-preview,google/gemini-3-flash-preview OPENCLAW_LIVE_GATEWAY_MAX_MODELS=2 OPENCLAW_LIVE_GATEWAY_STEP_TIMEOUT_MS=90000 OPENCLAW_LIVE_GATEWAY_MODEL_TIMEOUT_MS=180000 OPENCLAW_LIVE_DOCKER_REPO_ROOT="$GITHUB_WORKSPACE" timeout --foreground --kill-after=30s 35m bash .release-harness/scripts/test-live-gateway-models-docker.sh
|
||||
timeout_minutes: 40
|
||||
profile_env_only: false
|
||||
profiles: stable full
|
||||
- suite_id: live-gateway-minimax-docker
|
||||
label: Docker live gateway MiniMax
|
||||
command: OPENCLAW_LIVE_GATEWAY_PROVIDERS=minimax,minimax-portal OPENCLAW_LIVE_GATEWAY_MODELS=minimax/MiniMax-M2.7,minimax-portal/MiniMax-M2.7 OPENCLAW_LIVE_GATEWAY_MAX_MODELS=1 OPENCLAW_LIVE_GATEWAY_STEP_TIMEOUT_MS=90000 OPENCLAW_LIVE_GATEWAY_MODEL_TIMEOUT_MS=180000 OPENCLAW_LIVE_DOCKER_REPO_ROOT="$GITHUB_WORKSPACE" timeout --foreground --kill-after=30s 35m bash .release-harness/scripts/test-live-gateway-models-docker.sh
|
||||
command: OPENCLAW_LIVE_GATEWAY_PROVIDERS=minimax,minimax-portal OPENCLAW_LIVE_GATEWAY_MAX_MODELS=1 OPENCLAW_LIVE_GATEWAY_STEP_TIMEOUT_MS=90000 OPENCLAW_LIVE_GATEWAY_MODEL_TIMEOUT_MS=180000 OPENCLAW_LIVE_DOCKER_REPO_ROOT="$GITHUB_WORKSPACE" timeout --foreground --kill-after=30s 35m bash .release-harness/scripts/test-live-gateway-models-docker.sh
|
||||
timeout_minutes: 40
|
||||
profile_env_only: false
|
||||
profiles: stable full
|
||||
|
||||
@@ -813,7 +813,7 @@ jobs:
|
||||
alt_model="openai/gpt-5.5-alt"
|
||||
;;
|
||||
baseline)
|
||||
model="anthropic/claude-opus-4-8"
|
||||
model="anthropic/claude-opus-4-7"
|
||||
alt_model="anthropic/claude-sonnet-4-6"
|
||||
;;
|
||||
*)
|
||||
@@ -885,7 +885,7 @@ jobs:
|
||||
--candidate-summary .artifacts/qa-e2e/openai-candidate/qa-suite-summary.json \
|
||||
--baseline-summary .artifacts/qa-e2e/anthropic-baseline/qa-suite-summary.json \
|
||||
--candidate-label "${OPENCLAW_CI_OPENAI_MODEL}" \
|
||||
--baseline-label anthropic/claude-opus-4-8 \
|
||||
--baseline-label anthropic/claude-opus-4-7 \
|
||||
--output-dir .artifacts/qa-e2e/parity
|
||||
|
||||
- name: Upload parity artifacts
|
||||
@@ -1207,7 +1207,6 @@ jobs:
|
||||
env:
|
||||
OPENCLAW_QA_CONVEX_SITE_URL: ${{ secrets.OPENCLAW_QA_CONVEX_SITE_URL }}
|
||||
OPENCLAW_QA_CONVEX_SECRET_CI: ${{ secrets.OPENCLAW_QA_CONVEX_SECRET_CI }}
|
||||
OPENCLAW_QA_CREDENTIAL_ACQUIRE_TIMEOUT_MS: "1800000"
|
||||
OPENCLAW_QA_REDACT_PUBLIC_METADATA: "1"
|
||||
OPENCLAW_QA_TELEGRAM_CAPTURE_CONTENT: "1"
|
||||
run: |
|
||||
|
||||
@@ -122,10 +122,6 @@ jobs:
|
||||
echo "publish_openclaw_npm=true requires dispatching this workflow from main, release/YYYY.M.D, or a Tideclaw alpha branch for alpha prereleases." >&2
|
||||
exit 1
|
||||
fi
|
||||
if [[ "${PUBLISH_OPENCLAW_NPM}" == "true" && "${PLUGIN_PUBLISH_SCOPE}" != "all-publishable" ]]; then
|
||||
echo "publish_openclaw_npm=true requires plugin_publish_scope=all-publishable so every publishable official plugin is released with OpenClaw." >&2
|
||||
exit 1
|
||||
fi
|
||||
if [[ "${PLUGIN_PUBLISH_SCOPE}" == "selected" && -z "${PLUGINS}" ]]; then
|
||||
echo "plugin_publish_scope=selected requires plugins." >&2
|
||||
exit 1
|
||||
|
||||
6
.github/workflows/opengrep-precise-full.yml
vendored
6
.github/workflows/opengrep-precise-full.yml
vendored
@@ -32,11 +32,11 @@ jobs:
|
||||
- name: Install opengrep
|
||||
env:
|
||||
# Pin both the install script (by commit SHA) and the binary version.
|
||||
# The script SHA must match the v1.22.0 release tag in opengrep/opengrep
|
||||
# The script SHA must match the v1.19.0 release tag in opengrep/opengrep
|
||||
# so a compromised or force-pushed `main` cannot RCE in our CI runner.
|
||||
# Bump both together when upgrading.
|
||||
OPENGREP_VERSION: v1.22.0
|
||||
OPENGREP_INSTALL_SHA: f458d7f0d52cc58eae1ca3cf3d5caf101e637519
|
||||
OPENGREP_VERSION: v1.19.0
|
||||
OPENGREP_INSTALL_SHA: 9a4c0a68220618441608cd2bad4ff2eddccf8113
|
||||
run: |
|
||||
curl -fsSL "https://raw.githubusercontent.com/opengrep/opengrep/${OPENGREP_INSTALL_SHA}/install.sh" \
|
||||
| bash -s -- -v "$OPENGREP_VERSION"
|
||||
|
||||
8
.github/workflows/opengrep-precise.yml
vendored
8
.github/workflows/opengrep-precise.yml
vendored
@@ -44,7 +44,7 @@ jobs:
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ github.sha }}
|
||||
fetch-depth: 1
|
||||
fetch-depth: 0
|
||||
fetch-tags: false
|
||||
persist-credentials: false
|
||||
submodules: false
|
||||
@@ -58,11 +58,11 @@ jobs:
|
||||
- name: Install opengrep
|
||||
env:
|
||||
# Pin both the install script (by commit SHA) and the binary version.
|
||||
# The script SHA must match the v1.22.0 release tag in opengrep/opengrep
|
||||
# The script SHA must match the v1.19.0 release tag in opengrep/opengrep
|
||||
# so a compromised or force-pushed `main` cannot RCE in our CI runner.
|
||||
# Bump both together when upgrading.
|
||||
OPENGREP_VERSION: v1.22.0
|
||||
OPENGREP_INSTALL_SHA: f458d7f0d52cc58eae1ca3cf3d5caf101e637519
|
||||
OPENGREP_VERSION: v1.19.0
|
||||
OPENGREP_INSTALL_SHA: 9a4c0a68220618441608cd2bad4ff2eddccf8113
|
||||
run: |
|
||||
curl -fsSL "https://raw.githubusercontent.com/opengrep/opengrep/${OPENGREP_INSTALL_SHA}/install.sh" \
|
||||
| bash -s -- -v "$OPENGREP_VERSION"
|
||||
|
||||
8
.github/workflows/plugin-clawhub-release.yml
vendored
8
.github/workflows/plugin-clawhub-release.yml
vendored
@@ -431,8 +431,7 @@ jobs:
|
||||
EOF
|
||||
echo "CLAWHUB_CONFIG_PATH=${RUNNER_TEMP}/clawhub-config.json" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Check ClawHub package version
|
||||
id: clawhub_package_version
|
||||
- name: Ensure version is not already published
|
||||
env:
|
||||
PACKAGE_NAME: ${{ matrix.plugin.packageName }}
|
||||
PACKAGE_VERSION: ${{ matrix.plugin.version }}
|
||||
@@ -457,17 +456,14 @@ jobs:
|
||||
done
|
||||
if [[ "${status}" =~ ^2 ]]; then
|
||||
echo "${PACKAGE_NAME}@${PACKAGE_VERSION} is already published on ClawHub."
|
||||
echo "already_published=true" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
exit 1
|
||||
fi
|
||||
if [[ "${status}" != "404" ]]; then
|
||||
echo "Unexpected ClawHub response (${status}) for ${PACKAGE_NAME}@${PACKAGE_VERSION}."
|
||||
exit 1
|
||||
fi
|
||||
echo "already_published=false" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Publish
|
||||
if: steps.clawhub_package_version.outputs.already_published != 'true'
|
||||
env:
|
||||
CLAWHUB_REGISTRY: ${{ env.CLAWHUB_REGISTRY }}
|
||||
SOURCE_REPO: ${{ github.repository }}
|
||||
|
||||
8
.github/workflows/plugin-npm-release.yml
vendored
8
.github/workflows/plugin-npm-release.yml
vendored
@@ -263,8 +263,7 @@ jobs:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
install-bun: "false"
|
||||
|
||||
- name: Check npm package version
|
||||
id: npm_package_version
|
||||
- name: Ensure version is not already published
|
||||
env:
|
||||
PACKAGE_NAME: ${{ matrix.plugin.packageName }}
|
||||
PACKAGE_VERSION: ${{ matrix.plugin.version }}
|
||||
@@ -272,13 +271,10 @@ jobs:
|
||||
set -euo pipefail
|
||||
if npm view "${PACKAGE_NAME}@${PACKAGE_VERSION}" version >/dev/null 2>&1; then
|
||||
echo "${PACKAGE_NAME}@${PACKAGE_VERSION} is already published on npm."
|
||||
echo "already_published=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "already_published=false" >> "$GITHUB_OUTPUT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Publish
|
||||
if: steps.npm_package_version.outputs.already_published != 'true'
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
@@ -199,13 +199,13 @@ jobs:
|
||||
--alt-model openai/gpt-5.5-alt \
|
||||
--output-dir .artifacts/qa-e2e/openai-candidate
|
||||
|
||||
- name: Run Opus 4.8 lane
|
||||
- name: Run Opus 4.7 lane
|
||||
run: |
|
||||
pnpm openclaw qa suite \
|
||||
--provider-mode mock-openai \
|
||||
--parity-pack agentic \
|
||||
--concurrency "${QA_PARITY_CONCURRENCY}" \
|
||||
--model anthropic/claude-opus-4-8 \
|
||||
--model anthropic/claude-opus-4-7 \
|
||||
--alt-model anthropic/claude-sonnet-4-6 \
|
||||
--output-dir .artifacts/qa-e2e/anthropic-baseline
|
||||
|
||||
@@ -216,7 +216,7 @@ jobs:
|
||||
--candidate-summary .artifacts/qa-e2e/openai-candidate/qa-suite-summary.json \
|
||||
--baseline-summary .artifacts/qa-e2e/anthropic-baseline/qa-suite-summary.json \
|
||||
--candidate-label "${OPENCLAW_CI_OPENAI_MODEL}" \
|
||||
--baseline-label anthropic/claude-opus-4-8 \
|
||||
--baseline-label anthropic/claude-opus-4-7 \
|
||||
--output-dir .artifacts/qa-e2e/parity
|
||||
|
||||
- name: Upload parity artifacts
|
||||
@@ -530,7 +530,6 @@ jobs:
|
||||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||
OPENCLAW_QA_CONVEX_SITE_URL: ${{ secrets.OPENCLAW_QA_CONVEX_SITE_URL }}
|
||||
OPENCLAW_QA_CONVEX_SECRET_CI: ${{ secrets.OPENCLAW_QA_CONVEX_SECRET_CI }}
|
||||
OPENCLAW_QA_CREDENTIAL_ACQUIRE_TIMEOUT_MS: "1800000"
|
||||
OPENCLAW_QA_REDACT_PUBLIC_METADATA: "1"
|
||||
OPENCLAW_QA_TELEGRAM_CAPTURE_CONTENT: "1"
|
||||
INPUT_SCENARIO: ${{ github.event_name == 'workflow_dispatch' && inputs.scenario || '' }}
|
||||
|
||||
15
.github/workflows/workflow-sanity.yml
vendored
15
.github/workflows/workflow-sanity.yml
vendored
@@ -34,10 +34,7 @@ jobs:
|
||||
git init "$GITHUB_WORKSPACE"
|
||||
git -C "$GITHUB_WORKSPACE" config gc.auto 0
|
||||
git -C "$GITHUB_WORKSPACE" remote add origin "https://github.com/${CHECKOUT_REPO}.git"
|
||||
timeout --signal=TERM --kill-after=10s 30s git -C "$GITHUB_WORKSPACE" \
|
||||
-c protocol.version=2 \
|
||||
fetch --no-tags --prune --no-recurse-submodules --depth=1 origin \
|
||||
"+${CHECKOUT_SHA}:refs/remotes/origin/checkout"
|
||||
git -C "$GITHUB_WORKSPACE" fetch --no-tags --depth=1 origin "+${CHECKOUT_SHA}:refs/remotes/origin/checkout"
|
||||
git -C "$GITHUB_WORKSPACE" checkout --detach refs/remotes/origin/checkout
|
||||
|
||||
- name: Fail on tabs in workflow files
|
||||
@@ -78,10 +75,7 @@ jobs:
|
||||
git init "$GITHUB_WORKSPACE"
|
||||
git -C "$GITHUB_WORKSPACE" config gc.auto 0
|
||||
git -C "$GITHUB_WORKSPACE" remote add origin "https://github.com/${CHECKOUT_REPO}.git"
|
||||
timeout --signal=TERM --kill-after=10s 30s git -C "$GITHUB_WORKSPACE" \
|
||||
-c protocol.version=2 \
|
||||
fetch --no-tags --prune --no-recurse-submodules --depth=1 origin \
|
||||
"+${CHECKOUT_SHA}:refs/remotes/origin/checkout"
|
||||
git -C "$GITHUB_WORKSPACE" fetch --no-tags --depth=1 origin "+${CHECKOUT_SHA}:refs/remotes/origin/checkout"
|
||||
git -C "$GITHUB_WORKSPACE" checkout --detach refs/remotes/origin/checkout
|
||||
|
||||
- name: Install actionlint
|
||||
@@ -122,10 +116,7 @@ jobs:
|
||||
git init "$GITHUB_WORKSPACE"
|
||||
git -C "$GITHUB_WORKSPACE" config gc.auto 0
|
||||
git -C "$GITHUB_WORKSPACE" remote add origin "https://github.com/${CHECKOUT_REPO}.git"
|
||||
timeout --signal=TERM --kill-after=10s 30s git -C "$GITHUB_WORKSPACE" \
|
||||
-c protocol.version=2 \
|
||||
fetch --no-tags --prune --no-recurse-submodules --depth=1 origin \
|
||||
"+${CHECKOUT_SHA}:refs/remotes/origin/checkout"
|
||||
git -C "$GITHUB_WORKSPACE" fetch --no-tags --depth=1 origin "+${CHECKOUT_SHA}:refs/remotes/origin/checkout"
|
||||
git -C "$GITHUB_WORKSPACE" checkout --detach refs/remotes/origin/checkout
|
||||
|
||||
- name: Setup Node environment
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -178,7 +178,6 @@ mantis/
|
||||
/local/
|
||||
/client_secret_*.json
|
||||
package-lock.json
|
||||
!src/commands/copilot-sdk-install-manifest/package-lock.json
|
||||
.claude/
|
||||
.agent/
|
||||
skills-lock.json
|
||||
|
||||
@@ -186,7 +186,7 @@
|
||||
"node_modules/",
|
||||
"patches/",
|
||||
"pnpm-lock.yaml",
|
||||
"skills/**",
|
||||
"skills/",
|
||||
"src/auto-reply/reply/export-html/template.js",
|
||||
"src/canvas-host/a2ui/a2ui.bundle.js",
|
||||
"vendor/",
|
||||
|
||||
@@ -35,9 +35,9 @@ Skills own workflows; root owns hard policy and routing.
|
||||
|
||||
## Map
|
||||
|
||||
- Core TS: `src/`, `ui/`, `packages/`; plugins: `extensions/`; SDK: `src/plugin-sdk/*`; channels: `src/channels/*`; loader: `src/plugins/*`; protocol: `packages/gateway-protocol/*`; docs/apps: `docs/`, `apps/`.
|
||||
- Core TS: `src/`, `ui/`, `packages/`; plugins: `extensions/`; SDK: `src/plugin-sdk/*`; channels: `src/channels/*`; loader: `src/plugins/*`; protocol: `src/gateway/protocol/*`; docs/apps: `docs/`, `apps/`.
|
||||
- Installers: sibling `../openclaw.ai`.
|
||||
- Scoped guides: `extensions/`, `src/{plugin-sdk,channels,plugins,gateway,agents}/`, `packages/`, `test/helpers*/`, `docs/`, `ui/`, `scripts/`.
|
||||
- Scoped guides: `extensions/`, `src/{plugin-sdk,channels,plugins,gateway,gateway/protocol,agents}/`, `test/helpers*/`, `docs/`, `ui/`, `scripts/`.
|
||||
|
||||
## Docs
|
||||
|
||||
@@ -72,7 +72,6 @@ Skills own workflows; root owns hard policy and routing.
|
||||
- Plugin SDK exception: shipped external API gets new API first plus named compat/deprecation, small tests/docs if useful, removal plan.
|
||||
- Migrate internal/bundled callers to modern API in the same change. Do not let internal compat become permanent architecture.
|
||||
- Channels are implementation under `src/channels/**`; plugin authors get SDK seams. Providers own auth/catalog/runtime hooks; core owns generic loop.
|
||||
- Agent run terminal state: normalize/merge via `src/agents/agent-run-terminal-outcome.ts`; do not rederive timeout/cancel precedence in projections.
|
||||
- Hot paths should carry prepared facts forward: provider id, model ref, channel id, target, capability family, attachment class. Do not rediscover with broad plugin/provider/channel/capability loaders.
|
||||
- Do not fix repeated request-time discovery with scattered caches. Move the canonical fact earlier; reuse prepared runtime objects; delete duplicate lookup branches.
|
||||
- Gateway/plugin metadata is process-stable: installs, manifests, catalogs, generated paths, bundled metadata. Changes require restart or explicit owner reload/install/doctor flow.
|
||||
@@ -93,7 +92,6 @@ Skills own workflows; root owns hard policy and routing.
|
||||
- Install: `pnpm install` (keep Bun lock/patches aligned if touched).
|
||||
- CLI: `pnpm openclaw ...` or `pnpm dev`; build: `pnpm build`.
|
||||
- Tests in a normal source checkout: `pnpm test <path-or-filter> [vitest args...]`, `pnpm test:changed`, `pnpm test:serial`, `pnpm test:coverage`; never raw `vitest`.
|
||||
- If raw Vitest is unavoidable, use `vitest run ...`; bare `vitest ...` starts local watch mode and will not exit on its own.
|
||||
- Tests in a Codex worktree or linked/sparse checkout: avoid direct local `pnpm test*`; use `node scripts/run-vitest.mjs <path-or-filter>` for tiny explicit-file proof, or Crabbox/Testbox for anything broader.
|
||||
- Checks in a normal source checkout: `pnpm check:changed`; lanes: `pnpm changed:lanes --json`; staged: `pnpm check:changed --staged`; full: `pnpm check`.
|
||||
- Checks in a Codex worktree or linked/sparse checkout: avoid direct local `pnpm check*`; use `node scripts/crabbox-wrapper.mjs run ... --shell -- "pnpm check:changed"` so pnpm runs inside Testbox, not locally.
|
||||
@@ -229,7 +227,6 @@ Skills own workflows; root owns hard policy and routing.
|
||||
- Parallels: `$openclaw-parallels-smoke`; Discord roundtrip: `$parallels-discord-roundtrip`.
|
||||
- Crabbox/WebVNC human demos: keep remote desktop visible/windowed; no fullscreen remote browser unless video/capture-style output.
|
||||
- ClawSweeper ops: `$clawsweeper`. Deployed hook sessions may post one concise `#clawsweeper` note only when surprising/actionable/risky; if using message tool, reply exactly `NO_REPLY`.
|
||||
- Generated-media completions wake the requester agent first. Requester visible-reply config decides final text vs message tool; direct media send is fallback/recovery only.
|
||||
- Memory wiki prompt digest stays tiny; prefer `wiki_search` / `wiki_get`; verify contact data before use; source-class provenance for generated people facts.
|
||||
- Rebrand/migration/config warnings: run `openclaw doctor`.
|
||||
- Never edit `node_modules`.
|
||||
|
||||
46
CHANGELOG.md
46
CHANGELOG.md
@@ -2,50 +2,6 @@
|
||||
|
||||
Docs: https://docs.openclaw.ai
|
||||
|
||||
## 2026.5.30
|
||||
|
||||
### Highlights
|
||||
|
||||
- Agents and CLI-backed runtimes recover more cleanly from interrupted tool calls, stale session bindings, compaction handoffs, and media delivery retries. (#88129, #88136, #88141, #88162, #88182)
|
||||
- Channels and mobile delivery are steadier across Telegram, WhatsApp, iMessage, Slack, Discord, Microsoft Teams, Google Chat, Google Meet, and iOS realtime Talk. (#88096, #88105, #88183, #88231)
|
||||
- Provider and plugin requests now bound more timers, retries, OAuth/device-code lifetimes, media downloads, local service probes, and generated-content polling paths before they can hang a run.
|
||||
- Skills, session metadata, gateway runtime state, plugin metadata, and store writes do less repeated work on hot paths while keeping config and dispatch behavior stable.
|
||||
- Workboard, SecretRef plugin manifests, hosted iOS push relay, and external Copilot/Tokenjuice packaging add broader orchestration, integration, and plugin delivery surfaces. (#82326, #87469, #87796, #88107, #88117)
|
||||
- Release, CI, Docker, E2E, and diagnostics lanes now cap more logs, response bodies, readiness probes, artifact checks, and status polling so failures report bounded proof instead of stalling.
|
||||
|
||||
### Changes
|
||||
|
||||
- Plugins: externalize Tokenjuice as the official `@openclaw/tokenjuice` plugin with npm and ClawHub publish metadata.
|
||||
- Plugins: externalize the GitHub Copilot agent runtime as the official `@openclaw/copilot` plugin with npm and ClawHub publish metadata.
|
||||
- iOS: add hosted push relay defaults, realtime Talk playback, and a guarded WebSocket ping path for more reliable mobile sessions. (#88096, #88105, #88231)
|
||||
- Workboard: add orchestration primitives and agent coordination tools for multi-agent planning and run tracking. (#87469)
|
||||
- Plugins: add a SecretRef provider integration manifest contract and extract shared LLM core packages for provider/plugin reuse. (#82326, #88117)
|
||||
- Skills: add the core skills index and centralize skills runtime loading, status, filtering, and prompt formatting.
|
||||
|
||||
### Fixes
|
||||
|
||||
- CLI: keep `plugins list --json` on the snapshot-only path so plugin sweeps avoid loading the full runtime status graph.
|
||||
- Plugins: make PixVerse external-plugin ClawHub metadata explicit and keep it out of bundled dist builds.
|
||||
- Providers: bound generated media downloads from OpenAI, Runway, xAI, MiniMax, BytePlus, DashScope-compatible, FAL, OpenRouter, Google, Vydra, and Comfy providers.
|
||||
- Providers: cap GitHub Copilot OAuth request timeouts before creating abort signals.
|
||||
- Cron: retry recurring jobs after transient model rate limits before waiting for the next scheduled slot.
|
||||
- Agents/Codex: keep live session locks during cleanup, recover interrupted CLI tool transcripts, preserve Codex auth and compaction session identity, clear orphan tool state, cap app-server idle timers, and keep media completion delivery retryable. (#88129, #88136, #88141, #88162, #88182)
|
||||
- Channels: cap Telegram, Discord, WhatsApp, Signal, Feishu, Google Chat, Microsoft Teams, QQBot, Nostr, Zalo, Zalouser, and Nextcloud-style request/retry timers; preserve SMS approval reply routes; and retry WhatsApp QR login 408 timeouts. (#88183)
|
||||
- Security/config parsing: reject unsafe OAuth/token lifetimes, retry-after delays, inbound timestamps, response body sizes, command timeout config, sandbox observer token TTLs, and gateway WebSocket calls after close.
|
||||
- Providers/media: cap local service, model, usage, queue, generated media, TTS, music, workflow polling, and provider OAuth request timers across hosted and local providers.
|
||||
- Release/CI/E2E: bound release candidate reads, beta smoke REST calls, changelog restore, kitchen-sink and bundled plugin readiness probes, secret-provider probes, Vitest routing, and mainline test flakes. (#88127, #88137, #88155, #88160)
|
||||
- Release/CI/E2E: run the secret-provider integration proof through the repo pnpm runner so native macOS and Windows validation use the hydrated package-manager shim.
|
||||
- Release/CI/E2E: run the Telegram desktop proof gateway through the repo pnpm runner so native macOS proof uses the hydrated package-manager shim.
|
||||
- CI/Crabbox: keep default runner capacity spot-only and provider-neutral so OpenClaw remote validation does not silently fall back to on-demand leases or stale AWS region hints.
|
||||
- CI/Crabbox: route Crabbox wrapper and Testbox workflow edits to their regression tests so changed-test gates do not silently run zero specs.
|
||||
- CI/workflows: route workflow sanity helper edits to their guard tests and cover composite-action input interpolation checks.
|
||||
- CI/tooling: route CI scope, dependency, changelog, and docs helper edits to their owner tests instead of silently skipping changed-test coverage.
|
||||
- CI/tooling: route package, release, and install helper edits to their owner tests so changed-test gates cover publish and installer script changes.
|
||||
- CI/tooling: route shared script library edits through their owner tests so lock, process, safety, and scan helpers do not skip changed-test coverage.
|
||||
- CI/tooling: skip expensive import-graph scans once a changed diff already requires broad fallback, keeping local changed-test planning fast while still collecting explicit owner tests.
|
||||
- CI/tooling: route script edits through conventional owner tests when matching `test/scripts` or `src/scripts` coverage already exists.
|
||||
- Performance: reuse prepared provider handles, strict tool schemas, gateway runtime metadata, session maintenance config, plugin metadata, bundled skill allowlists, package-local plugin artifacts, and single-entry store writes.
|
||||
|
||||
## 2026.5.28
|
||||
|
||||
### Highlights
|
||||
@@ -68,11 +24,9 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
### Fixes
|
||||
|
||||
- Agents: fall back to local config pruning when the optional `agents delete` Gateway probe cannot authenticate, so offline installs can still delete agents without removing shared workspaces.
|
||||
- Tighten phone-control mutation authorization [AI]. (#87150) Thanks @pgondhi987.
|
||||
- Clarify directive persistence authorization policy [AI]. (#86369) Thanks @pgondhi987.
|
||||
- Agents/Codex: keep spawned agent cwd/workspace state separated, keep hook context prompt-local, release session locks on timeout abort, avoid session event queue self-wait, preserve shared app-server state across startup or helper failures, keep native hook relay alive across restarts, route workspace memory through tools, resolve Codex runtime models first, report quarantined dynamic tools, format `skills` command output, and bound compaction/steering retries. (#87218, #86875, #86123, #87399, #87375, #87383, #87400) Thanks @mbelinky, @Alix-007, @luoyanglang, @yetval, and @sjf.
|
||||
- Codex Supervisor: keep real-home app-server MCP session listing on the loaded/state-DB path, bound stored history scans, and close WebSocket probes cleanly.
|
||||
- Channels: thread canonical session keys into outbound hooks, preserve Matrix room-id case, keep fallback tool warnings mention-inert, retain delivered Slack final replies during late cleanup, continue iMessage polling after denied reactions, suppress duplicate native exec approvals, preserve Telegram SecretRef prompt config, suppress Discord recovered tool warnings, and block untrusted Teams service URLs. (#73706, #75670, #87366, #87451, #87334) Thanks @zeroaltitude, @lukeboyett, @xiaotian, and @eleqtrizit.
|
||||
- CLI/auth/doctor/providers: reject malformed numeric/timeout/subcommand-version inputs, wait for respawn child shutdown, bound Codex and GitHub Copilot OAuth/token requests, warm provider auth off the main thread, honor Codex response timeouts, bound local service startup, resolve GPT-5.5 without cached catalog, migrate legacy memory auto-provider config, rewrite non-canonical `api_key` auth profiles, and make doctor restart follow-ups actionable. (#87398, #86281, #87361) Thanks @Patrick-Erichsen, @samzong, @giodl73-repo, and @alkor2000.
|
||||
- Gateway/security/session state: expire browser tokens after auth rotation, scope assistant idempotency dedupe, drain probe client closes, avoid stale restart continuation reuse, preserve retry-after fallbacks, bound webchat image and artifact transcript scans, include seconds in inbound metadata timestamps, and evict current plugin-state namespaces at row caps.
|
||||
|
||||
@@ -48,7 +48,6 @@ RUN --mount=type=bind,source=packages,target=/tmp/packages,readonly \
|
||||
FROM ${OPENCLAW_BUN_IMAGE} AS bun-binary
|
||||
FROM ${OPENCLAW_NODE_BOOKWORM_IMAGE} AS build
|
||||
ARG OPENCLAW_BUNDLED_PLUGIN_DIR
|
||||
ARG OPENCLAW_EXTENSIONS
|
||||
|
||||
# Copy pinned Bun binary from the official image instead of fetching via curl.
|
||||
COPY --from=bun-binary /usr/local/bin/bun /usr/local/bin/bun
|
||||
@@ -78,12 +77,7 @@ RUN --mount=type=cache,id=openclaw-pnpm-store,target=/root/.local/share/pnpm/sto
|
||||
# pnpm v10+ may append peer-resolution hashes to virtual-store folder names; do not hardcode `.pnpm/...`
|
||||
# paths. Matrix's native downloader can hit transient release CDN errors while
|
||||
# still exiting successfully, so retry the package downloader before failing.
|
||||
# Skip the entire check when matrix is not a bundled extension (e.g. msteams-only builds).
|
||||
RUN set -eux; \
|
||||
if ! printf '%s\n' "$OPENCLAW_EXTENSIONS" | tr ',' ' ' | tr ' ' '\n' | grep -qx 'matrix'; then \
|
||||
echo "==> matrix not bundled, skipping matrix-sdk-crypto check"; \
|
||||
exit 0; \
|
||||
fi; \
|
||||
echo "==> Verifying critical native addons..."; \
|
||||
for attempt in 1 2 3 4 5; do \
|
||||
if find /app/node_modules -name "matrix-sdk-crypto*.node" 2>/dev/null | grep -q .; then \
|
||||
|
||||
@@ -65,8 +65,8 @@ android {
|
||||
applicationId = "ai.openclaw.app"
|
||||
minSdk = 31
|
||||
targetSdk = 36
|
||||
versionCode = 2026053001
|
||||
versionName = "2026.5.30"
|
||||
versionCode = 2026052801
|
||||
versionName = "2026.5.28"
|
||||
ndk {
|
||||
// Support all major ABIs — native libs are tiny (~47 KB per ABI)
|
||||
abiFilters += listOf("armeabi-v7a", "arm64-v8a", "x86", "x86_64")
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
# OpenClaw iOS Changelog
|
||||
|
||||
## 2026.5.30 - 2026-05-30
|
||||
|
||||
Maintenance update for the current OpenClaw release.
|
||||
|
||||
- Added hosted push relay defaults, realtime Talk playback, and safer WebSocket ping handling for mobile sessions.
|
||||
|
||||
## 2026.5.28 - 2026-05-28
|
||||
|
||||
Maintenance update for the current OpenClaw release.
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
// Source of truth: apps/ios/version.json
|
||||
// Generated by scripts/ios-sync-versioning.ts.
|
||||
|
||||
OPENCLAW_IOS_VERSION = 2026.5.30
|
||||
OPENCLAW_MARKETING_VERSION = 2026.5.30
|
||||
OPENCLAW_IOS_VERSION = 2026.5.28
|
||||
OPENCLAW_MARKETING_VERSION = 2026.5.28
|
||||
OPENCLAW_BUILD_VERSION = 1
|
||||
|
||||
#include? "../build/Version.xcconfig"
|
||||
|
||||
@@ -73,10 +73,9 @@ Release behavior:
|
||||
- Changing the root gateway version does not change the iOS app version until you explicitly pin from the gateway.
|
||||
- See `apps/ios/VERSIONING.md` for the full workflow.
|
||||
|
||||
Relay behavior for beta builds:
|
||||
Required env for beta builds:
|
||||
|
||||
- Beta builds default to `https://ios-push-relay.openclaw.ai`.
|
||||
- Optional custom relay override: `OPENCLAW_PUSH_RELAY_BASE_URL=https://relay.example.com`
|
||||
- `OPENCLAW_PUSH_RELAY_BASE_URL=https://relay.example.com`
|
||||
This must be a plain `https://host[:port][/path]` base URL without whitespace, query params, fragments, or xcconfig metacharacters.
|
||||
|
||||
Archive without upload:
|
||||
@@ -119,7 +118,7 @@ scripts/ios-asc-keychain-setup.sh \
|
||||
|
||||
This should create `apps/ios/fastlane/.env` with the non-secret ASC variables while the private key stays in Keychain.
|
||||
|
||||
3. Optional: set a custom official/TestFlight relay URL for the build. If unset, the beta flow uses `https://ios-push-relay.openclaw.ai`.
|
||||
3. Set the official/TestFlight relay URL for the build:
|
||||
|
||||
```bash
|
||||
export OPENCLAW_PUSH_RELAY_BASE_URL=https://relay.example.com
|
||||
|
||||
@@ -1,652 +0,0 @@
|
||||
import SwiftUI
|
||||
|
||||
struct TalkProTab: View {
|
||||
@Environment(NodeAppModel.self) private var appModel
|
||||
@AppStorage("talk.enabled") private var talkEnabled: Bool = false
|
||||
@AppStorage(TalkSpeechLocale.storageKey) private var talkSpeechLocale: String = TalkSpeechLocale.automaticID
|
||||
@AppStorage(TalkDefaults.speakerphoneEnabledKey) private var talkSpeakerphoneEnabled: Bool =
|
||||
TalkDefaults.speakerphoneEnabledByDefault
|
||||
@AppStorage("talk.background.enabled") private var talkBackgroundEnabled: Bool = false
|
||||
@State private var showPermissionPrompt = false
|
||||
var openSettings: () -> Void
|
||||
|
||||
private var state: TalkProState {
|
||||
TalkProState(
|
||||
gatewayConnected: self.gatewayConnected,
|
||||
isEnabled: self.appModel.talkMode.isEnabled || self.talkEnabled,
|
||||
statusText: self.appModel.talkMode.statusText,
|
||||
isListening: self.appModel.talkMode.isListening,
|
||||
isSpeaking: self.appModel.talkMode.isSpeaking,
|
||||
isUserSpeechDetected: self.appModel.talkMode.isUserSpeechDetected,
|
||||
permissionState: self.appModel.talkMode.gatewayTalkPermissionState)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
ZStack {
|
||||
CommandControlBackground()
|
||||
ScrollView {
|
||||
VStack(alignment: .leading, spacing: 10) {
|
||||
self.header
|
||||
self.voiceHeroCard
|
||||
self.conversationCard
|
||||
self.voiceModeCard
|
||||
self.controlsCard
|
||||
}
|
||||
.padding(.top, 16)
|
||||
.padding(.bottom, 18)
|
||||
}
|
||||
.safeAreaPadding(.bottom, OpenClawProMetric.bottomScrollInset)
|
||||
}
|
||||
.navigationBarHidden(true)
|
||||
}
|
||||
.sheet(isPresented: self.$showPermissionPrompt) {
|
||||
NavigationStack {
|
||||
TalkPermissionPromptView(
|
||||
style: .sheet,
|
||||
onPermissionReady: {
|
||||
self.showPermissionPrompt = false
|
||||
self.startTalk()
|
||||
})
|
||||
.padding()
|
||||
.navigationTitle("Enable Talk")
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .cancellationAction) {
|
||||
Button("Not Now") {
|
||||
self.showPermissionPrompt = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.presentationDetents([.medium, .large])
|
||||
.openClawSheetChrome()
|
||||
}
|
||||
.onAppear { self.alignPersistedTalkState() }
|
||||
}
|
||||
|
||||
private var header: some View {
|
||||
HStack(alignment: .center, spacing: 11) {
|
||||
OpenClawProMark(size: 31, shadowRadius: 9)
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text("Talk")
|
||||
.font(.system(size: 27, weight: .bold, design: .rounded))
|
||||
Text(self.headerSubtitle)
|
||||
.font(.caption.weight(.medium))
|
||||
.foregroundStyle(.secondary)
|
||||
.lineLimit(1)
|
||||
}
|
||||
Spacer(minLength: 8)
|
||||
self.statusChip
|
||||
}
|
||||
.padding(.horizontal, OpenClawProMetric.pagePadding)
|
||||
}
|
||||
|
||||
private var statusChip: some View {
|
||||
HStack(spacing: 5) {
|
||||
Circle()
|
||||
.fill(self.state.color)
|
||||
.frame(width: 7, height: 7)
|
||||
Text(self.state.chipText)
|
||||
.font(.caption.weight(.bold))
|
||||
.foregroundStyle(self.state.color)
|
||||
}
|
||||
.padding(.horizontal, 10)
|
||||
.padding(.vertical, 7)
|
||||
.background {
|
||||
Capsule(style: .continuous)
|
||||
.fill(self.state.color.opacity(0.11))
|
||||
.overlay {
|
||||
Capsule(style: .continuous)
|
||||
.strokeBorder(self.state.color.opacity(0.22), lineWidth: 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var voiceHeroCard: some View {
|
||||
CommandPanel(tint: self.state.color, isProminent: true, padding: 16) {
|
||||
VStack(alignment: .center, spacing: 16) {
|
||||
TalkProOrb(
|
||||
mode: self.state.waveformMode(micLevel: self.appModel.talkMode.micLevel),
|
||||
color: self.state.color,
|
||||
systemImage: self.state.icon)
|
||||
.frame(height: 188)
|
||||
.accessibilityHidden(true)
|
||||
|
||||
VStack(spacing: 5) {
|
||||
Text(self.state.title)
|
||||
.font(.title3.weight(.bold))
|
||||
.multilineTextAlignment(.center)
|
||||
Text(self.heroSubtitle)
|
||||
.font(.subheadline.weight(.medium))
|
||||
.foregroundStyle(.secondary)
|
||||
.multilineTextAlignment(.center)
|
||||
}
|
||||
|
||||
Button(action: self.handlePrimaryAction) {
|
||||
Label(self.state.primaryButtonTitle, systemImage: self.state.primaryButtonIcon)
|
||||
.font(.subheadline.weight(.bold))
|
||||
.foregroundStyle(.white)
|
||||
.frame(maxWidth: .infinity)
|
||||
.frame(height: 50)
|
||||
.background {
|
||||
RoundedRectangle(cornerRadius: 14, style: .continuous)
|
||||
.fill(self.state.primaryButtonFill)
|
||||
.shadow(color: self.state.color.opacity(0.28), radius: 18, y: 8)
|
||||
}
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.disabled(self.state.primaryAction == .waiting)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, OpenClawProMetric.pagePadding)
|
||||
}
|
||||
|
||||
private var conversationCard: some View {
|
||||
CommandPanel(padding: 0) {
|
||||
VStack(spacing: 0) {
|
||||
self.cardHeader(title: "Conversation", value: self.state.chipText, color: self.state.color)
|
||||
.padding(.horizontal, 12)
|
||||
.padding(.top, 11)
|
||||
.padding(.bottom, 3)
|
||||
self.infoRow(icon: "person.crop.circle.fill", title: "Agent", value: self.appModel.activeAgentName)
|
||||
Divider().padding(.leading, 54)
|
||||
self.infoRow(
|
||||
icon: "bubble.left.and.text.bubble.right.fill",
|
||||
title: "Session",
|
||||
value: self.appModel.chatSessionKey)
|
||||
Divider().padding(.leading, 54)
|
||||
self.infoRow(icon: self.state.icon, title: "Runtime", value: self.appModel.talkMode.statusText)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, OpenClawProMetric.pagePadding)
|
||||
}
|
||||
|
||||
private var voiceModeCard: some View {
|
||||
CommandPanel(padding: 0) {
|
||||
VStack(spacing: 0) {
|
||||
self.cardHeader(
|
||||
title: "Voice mode",
|
||||
value: "Settings ›",
|
||||
color: OpenClawBrand.accent,
|
||||
action: self.openSettings)
|
||||
.padding(.horizontal, 12)
|
||||
.padding(.top, 11)
|
||||
.padding(.bottom, 3)
|
||||
self.infoRow(icon: "waveform", title: "Mode", value: self.appModel.talkMode.gatewayTalkVoiceModeTitle)
|
||||
Divider().padding(.leading, 54)
|
||||
self.infoRow(icon: "antenna.radiowaves.left.and.right", title: "Transport", value: self.transportText)
|
||||
Divider().padding(.leading, 54)
|
||||
self.infoRow(icon: "key.fill", title: "Permission", value: self.permissionText)
|
||||
Divider().padding(.leading, 54)
|
||||
self.infoRow(icon: "globe", title: "Speech language", value: self.speechLocaleText)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, OpenClawProMetric.pagePadding)
|
||||
}
|
||||
|
||||
private var controlsCard: some View {
|
||||
CommandPanel(padding: 0) {
|
||||
VStack(spacing: 0) {
|
||||
self.cardHeader(title: "Controls", value: nil, color: .secondary)
|
||||
.padding(.horizontal, 12)
|
||||
.padding(.top, 11)
|
||||
.padding(.bottom, 3)
|
||||
Toggle("Speakerphone", isOn: self.$talkSpeakerphoneEnabled)
|
||||
.padding(.horizontal, 14)
|
||||
.padding(.vertical, 10)
|
||||
Divider().padding(.leading, 14)
|
||||
Toggle("Background listening", isOn: self.$talkBackgroundEnabled)
|
||||
.padding(.horizontal, 14)
|
||||
.padding(.vertical, 10)
|
||||
Divider().padding(.leading, 14)
|
||||
Button(action: self.openSettings) {
|
||||
HStack {
|
||||
Label("Voice & Talk settings", systemImage: "slider.horizontal.3")
|
||||
Spacer()
|
||||
Image(systemName: "chevron.right")
|
||||
.font(.caption.weight(.bold))
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
.font(.subheadline.weight(.semibold))
|
||||
.padding(.horizontal, 14)
|
||||
.padding(.vertical, 12)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, OpenClawProMetric.pagePadding)
|
||||
}
|
||||
|
||||
private func cardHeader(
|
||||
title: String,
|
||||
value: String?,
|
||||
color: Color,
|
||||
action: (() -> Void)? = nil) -> some View
|
||||
{
|
||||
HStack(spacing: 8) {
|
||||
Text(title)
|
||||
.font(.subheadline.weight(.bold))
|
||||
Spacer(minLength: 8)
|
||||
if let value {
|
||||
if let action {
|
||||
Button(value, action: action)
|
||||
.font(.caption.weight(.semibold))
|
||||
.foregroundStyle(color)
|
||||
} else {
|
||||
Text(value)
|
||||
.font(.caption.weight(.semibold))
|
||||
.foregroundStyle(color)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func infoRow(icon: String, title: String, value: String) -> some View {
|
||||
HStack(spacing: 10) {
|
||||
Image(systemName: icon)
|
||||
.font(.caption.weight(.bold))
|
||||
.foregroundStyle(self.state.color)
|
||||
.frame(width: 30, height: 30)
|
||||
.background {
|
||||
RoundedRectangle(cornerRadius: 8, style: .continuous)
|
||||
.fill(self.state.color.opacity(0.11))
|
||||
}
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(title)
|
||||
.font(.caption2.weight(.medium))
|
||||
.foregroundStyle(.secondary)
|
||||
Text(value.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty ? "—" : value)
|
||||
.font(.subheadline.weight(.semibold))
|
||||
.lineLimit(1)
|
||||
.minimumScaleFactor(0.78)
|
||||
}
|
||||
Spacer(minLength: 0)
|
||||
}
|
||||
.padding(.horizontal, 12)
|
||||
.padding(.vertical, 9)
|
||||
}
|
||||
|
||||
private var gatewayConnected: Bool {
|
||||
GatewayStatusBuilder.build(appModel: self.appModel) == .connected
|
||||
}
|
||||
|
||||
private var headerSubtitle: String {
|
||||
let mode = self.appModel.talkMode.gatewayTalkVoiceModeTitle.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
let agent = self.appModel.activeAgentName.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
if mode.isEmpty || mode == "Not loaded" { return agent.isEmpty ? "Realtime voice" : agent }
|
||||
if agent.isEmpty { return mode }
|
||||
return "\(agent) • \(mode)"
|
||||
}
|
||||
|
||||
private var heroSubtitle: String {
|
||||
if self.state
|
||||
.prefersPermissionCopy { return "Gateway approval is required before this phone can capture voice." }
|
||||
if !self.gatewayConnected { return "Connect to your gateway to start a voice conversation." }
|
||||
let subtitle = (self.appModel.talkMode.gatewayTalkVoiceModeSubtitle ?? "")
|
||||
.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
if !subtitle.isEmpty { return subtitle }
|
||||
return "Routes voice to \(self.appModel.activeAgentName)."
|
||||
}
|
||||
|
||||
private var transportText: String {
|
||||
let provider = self.appModel.talkMode.gatewayTalkProviderLabel.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
let transport = self.appModel.talkMode.gatewayTalkTransportLabel.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
if provider.isEmpty || provider == "Not loaded" { return transport.isEmpty ? "Not loaded" : transport }
|
||||
if transport.isEmpty || transport == "Not loaded" { return provider }
|
||||
return "\(provider) • \(transport)"
|
||||
}
|
||||
|
||||
private var permissionText: String {
|
||||
if let failure = self.appModel.talkMode.gatewayTalkPermissionState.failureMessage {
|
||||
return failure
|
||||
}
|
||||
return self.appModel.talkMode.gatewayTalkPermissionState.statusLabel
|
||||
}
|
||||
|
||||
private var speechLocaleText: String {
|
||||
if self.talkSpeechLocale == TalkSpeechLocale.automaticID { return "Automatic" }
|
||||
return self.talkSpeechLocale
|
||||
}
|
||||
|
||||
private func alignPersistedTalkState() {
|
||||
if self.appModel.talkMode.gatewayTalkPermissionState.requiresTalkPermissionAction,
|
||||
self.talkEnabled || self.appModel.talkMode.isEnabled
|
||||
{
|
||||
self.stopTalk()
|
||||
} else if self.talkEnabled != self.appModel.talkMode.isEnabled {
|
||||
self.appModel.setTalkEnabled(self.talkEnabled)
|
||||
}
|
||||
}
|
||||
|
||||
private func handlePrimaryAction() {
|
||||
switch self.state.primaryAction {
|
||||
case .start:
|
||||
self.startTalk()
|
||||
case .stop:
|
||||
self.stopTalk()
|
||||
case .enablePermission:
|
||||
self.stopTalk()
|
||||
self.showPermissionPrompt = true
|
||||
case .openSettings:
|
||||
self.openSettings()
|
||||
case .waiting:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
private func startTalk() {
|
||||
self.talkEnabled = true
|
||||
self.appModel.setTalkEnabled(true)
|
||||
}
|
||||
|
||||
private func stopTalk() {
|
||||
self.talkEnabled = false
|
||||
self.appModel.setTalkEnabled(false)
|
||||
}
|
||||
}
|
||||
|
||||
enum TalkProPrimaryAction: Equatable {
|
||||
case start
|
||||
case stop
|
||||
case enablePermission
|
||||
case openSettings
|
||||
case waiting
|
||||
}
|
||||
|
||||
enum TalkProWaveformMode: Equatable {
|
||||
case level(Double)
|
||||
case inputSpeech
|
||||
case speaking
|
||||
case indeterminate
|
||||
case still
|
||||
}
|
||||
|
||||
struct TalkProState: Equatable {
|
||||
let gatewayConnected: Bool
|
||||
let isEnabled: Bool
|
||||
let statusText: String
|
||||
let isListening: Bool
|
||||
let isSpeaking: Bool
|
||||
let isUserSpeechDetected: Bool
|
||||
let permissionState: TalkGatewayPermissionState
|
||||
|
||||
private var normalizedStatus: String {
|
||||
self.statusText.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
|
||||
}
|
||||
|
||||
var title: String {
|
||||
if !self.gatewayConnected { return "Gateway offline" }
|
||||
switch self.permissionState {
|
||||
case .missingScope, .requestFailed:
|
||||
return "Gateway permission required"
|
||||
case .requestingUpgrade:
|
||||
return "Requesting approval"
|
||||
case .upgradeRequested:
|
||||
return "Approval requested"
|
||||
case .apiKeyMissing:
|
||||
return "Voice API key missing"
|
||||
case .loadFailed:
|
||||
return "Voice config failed"
|
||||
default:
|
||||
break
|
||||
}
|
||||
if self.isSpeaking { return "Speaking" }
|
||||
if self.isListening { return "Listening" }
|
||||
if self.normalizedStatus.contains("connecting") { return "Connecting" }
|
||||
if self.normalizedStatus.contains("thinking") { return "Asking OpenClaw" }
|
||||
if self.isEnabled { return "Ready to talk" }
|
||||
return "Talk is off"
|
||||
}
|
||||
|
||||
var chipText: String {
|
||||
if !self.gatewayConnected { return "Offline" }
|
||||
switch self.permissionState {
|
||||
case .missingScope, .requestFailed:
|
||||
return "Needs approval"
|
||||
case .requestingUpgrade, .upgradeRequested:
|
||||
return "Pending"
|
||||
case .apiKeyMissing:
|
||||
return "API key"
|
||||
case .loadFailed:
|
||||
return "Config"
|
||||
default:
|
||||
break
|
||||
}
|
||||
if self.isSpeaking { return "Speaking" }
|
||||
if self.isListening { return "Listening" }
|
||||
if self.isEnabled { return "Ready" }
|
||||
return "Off"
|
||||
}
|
||||
|
||||
var icon: String {
|
||||
if !self.gatewayConnected { return "wifi.slash" }
|
||||
switch self.permissionState {
|
||||
case .missingScope, .requestFailed:
|
||||
return "key.fill"
|
||||
case .requestingUpgrade:
|
||||
return "paperplane.fill"
|
||||
case .upgradeRequested:
|
||||
return "hourglass"
|
||||
case .apiKeyMissing, .loadFailed:
|
||||
return "exclamationmark.triangle.fill"
|
||||
default:
|
||||
break
|
||||
}
|
||||
if self.isSpeaking { return "speaker.wave.2.fill" }
|
||||
if self.isListening { return "mic.fill" }
|
||||
if self.normalizedStatus.contains("thinking") { return "sparkles" }
|
||||
if self.normalizedStatus.contains("connecting") { return "dot.radiowaves.left.and.right" }
|
||||
return "waveform"
|
||||
}
|
||||
|
||||
var color: Color {
|
||||
if !self.gatewayConnected { return .secondary }
|
||||
switch self.permissionState {
|
||||
case .requestFailed, .loadFailed:
|
||||
return OpenClawBrand.danger
|
||||
case .missingScope, .requestingUpgrade, .upgradeRequested, .apiKeyMissing:
|
||||
return OpenClawBrand.warn
|
||||
default:
|
||||
return self.isEnabled ? OpenClawBrand.ok : OpenClawBrand.accentHot
|
||||
}
|
||||
}
|
||||
|
||||
var primaryAction: TalkProPrimaryAction {
|
||||
if !self.gatewayConnected { return .openSettings }
|
||||
switch self.permissionState {
|
||||
case .missingScope, .requestFailed:
|
||||
return .enablePermission
|
||||
case .requestingUpgrade, .upgradeRequested:
|
||||
return .waiting
|
||||
case .apiKeyMissing, .loadFailed:
|
||||
return .openSettings
|
||||
default:
|
||||
return self.isEnabled ? .stop : .start
|
||||
}
|
||||
}
|
||||
|
||||
var primaryButtonTitle: String {
|
||||
switch self.primaryAction {
|
||||
case .start: "Start Talk"
|
||||
case .stop: "Stop Talk"
|
||||
case .enablePermission: "Enable Talk"
|
||||
case .openSettings: self.gatewayConnected ? "Open Voice Settings" : "Open Gateway Settings"
|
||||
case .waiting: "Waiting for Approval"
|
||||
}
|
||||
}
|
||||
|
||||
var primaryButtonIcon: String {
|
||||
switch self.primaryAction {
|
||||
case .start: "play.fill"
|
||||
case .stop: "stop.fill"
|
||||
case .enablePermission: "key.fill"
|
||||
case .openSettings: "gearshape.fill"
|
||||
case .waiting: "hourglass"
|
||||
}
|
||||
}
|
||||
|
||||
var primaryButtonFill: AnyShapeStyle {
|
||||
switch self.primaryAction {
|
||||
case .stop:
|
||||
AnyShapeStyle(OpenClawBrand.danger)
|
||||
case .waiting:
|
||||
AnyShapeStyle(OpenClawBrand.warn.opacity(0.72))
|
||||
default:
|
||||
AnyShapeStyle(LinearGradient(
|
||||
colors: [self.color.opacity(0.95), OpenClawBrand.accent],
|
||||
startPoint: .topLeading,
|
||||
endPoint: .bottomTrailing))
|
||||
}
|
||||
}
|
||||
|
||||
var prefersPermissionCopy: Bool {
|
||||
switch self.permissionState {
|
||||
case .missingScope, .requestingUpgrade, .upgradeRequested, .requestFailed:
|
||||
true
|
||||
default:
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
func waveformMode(micLevel: Double) -> TalkProWaveformMode {
|
||||
if !self.gatewayConnected { return .still }
|
||||
switch self.permissionState {
|
||||
case .requestingUpgrade, .upgradeRequested:
|
||||
return .indeterminate
|
||||
case .missingScope, .requestFailed, .apiKeyMissing, .loadFailed:
|
||||
return .still
|
||||
default:
|
||||
break
|
||||
}
|
||||
if self.isSpeaking { return .speaking }
|
||||
if self.isListening, self.isUserSpeechDetected { return .inputSpeech }
|
||||
if self.isListening { return .level(micLevel) }
|
||||
if self.normalizedStatus.contains("connecting") || self.normalizedStatus.contains("thinking") {
|
||||
return .indeterminate
|
||||
}
|
||||
return self.isEnabled ? .indeterminate : .still
|
||||
}
|
||||
}
|
||||
|
||||
private struct TalkProOrb: View {
|
||||
let mode: TalkProWaveformMode
|
||||
let color: Color
|
||||
let systemImage: String
|
||||
|
||||
@Environment(\.accessibilityReduceMotion) private var reduceMotion
|
||||
|
||||
var body: some View {
|
||||
TimelineView(.periodic(from: .now, by: 1.0 / 24.0)) { timeline in
|
||||
ZStack {
|
||||
ForEach(0..<3, id: \.self) { ring in
|
||||
Circle()
|
||||
.strokeBorder(self.color.opacity(self.ringOpacity(ring)), lineWidth: 1.4)
|
||||
.scaleEffect(self.ringScale(ring, date: timeline.date))
|
||||
}
|
||||
Circle()
|
||||
.fill(self.color.opacity(0.13))
|
||||
.frame(width: 128, height: 128)
|
||||
.overlay {
|
||||
Circle()
|
||||
.strokeBorder(self.color.opacity(0.30), lineWidth: 1)
|
||||
}
|
||||
TalkProWaveform(mode: self.mode, tint: self.color, barCount: 18)
|
||||
.frame(width: 116, height: 52)
|
||||
.opacity(self.systemImage == "waveform" || self.systemImage == "mic.fill" ? 1 : 0.34)
|
||||
Image(systemName: self.systemImage)
|
||||
.font(.system(size: 34, weight: .bold))
|
||||
.foregroundStyle(self.color)
|
||||
.opacity(self.systemImage == "waveform" || self.systemImage == "mic.fill" ? 0.20 : 1)
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
}
|
||||
}
|
||||
|
||||
private func ringScale(_ ring: Int, date: Date) -> CGFloat {
|
||||
guard !self.reduceMotion else { return CGFloat(1.0 + (Double(ring) * 0.12)) }
|
||||
let base = 0.88 + (Double(ring) * 0.18)
|
||||
let speed = self.mode == .still ? 0.8 : 1.8
|
||||
let phase = date.timeIntervalSinceReferenceDate * speed + Double(ring) * 0.9
|
||||
return CGFloat(base + (sin(phase) * 0.035))
|
||||
}
|
||||
|
||||
private func ringOpacity(_ ring: Int) -> Double {
|
||||
switch self.mode {
|
||||
case .still:
|
||||
0.10 - (Double(ring) * 0.018)
|
||||
default:
|
||||
0.24 - (Double(ring) * 0.045)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct TalkProWaveform: View {
|
||||
let mode: TalkProWaveformMode
|
||||
let tint: Color
|
||||
let barCount: Int
|
||||
|
||||
@Environment(\.accessibilityReduceMotion) private var reduceMotion
|
||||
|
||||
var body: some View {
|
||||
TimelineView(.periodic(from: .now, by: 1.0 / 24.0)) { timeline in
|
||||
HStack(alignment: .center, spacing: 4) {
|
||||
ForEach(0..<self.barCount, id: \.self) { index in
|
||||
Capsule(style: .continuous)
|
||||
.fill(self.tint.opacity(self.opacity(for: index)))
|
||||
.frame(width: 4, height: self.height(for: index, date: timeline.date))
|
||||
}
|
||||
}
|
||||
.frame(maxHeight: .infinity)
|
||||
}
|
||||
}
|
||||
|
||||
private func height(for index: Int, date: Date) -> CGFloat {
|
||||
let minimum = 6.0
|
||||
let maximum = 48.0
|
||||
return CGFloat(minimum + ((maximum - minimum) * self.amplitude(for: index, date: date)))
|
||||
}
|
||||
|
||||
private func opacity(for index: Int) -> Double {
|
||||
switch self.mode {
|
||||
case .still:
|
||||
index == self.barCount / 2 ? 0.64 : 0.30
|
||||
default:
|
||||
0.82
|
||||
}
|
||||
}
|
||||
|
||||
private func amplitude(for index: Int, date: Date) -> Double {
|
||||
if self.reduceMotion {
|
||||
switch self.mode {
|
||||
case let .level(level): return min(max(level, 0.10), 1.0)
|
||||
case .inputSpeech: return 0.72
|
||||
case .speaking: return 0.62
|
||||
case .indeterminate: return 0.34
|
||||
case .still: return 0.18
|
||||
}
|
||||
}
|
||||
|
||||
let t = date.timeIntervalSinceReferenceDate
|
||||
let phase = Double(index) * 0.52
|
||||
switch self.mode {
|
||||
case let .level(level):
|
||||
let clamped = min(max(level, 0), 1)
|
||||
let shaped = 0.12 + (0.88 * clamped)
|
||||
let variation = 0.72 + (0.28 * sin((t * 12.0) + phase))
|
||||
return min(max(shaped * variation, 0.10), 1.0)
|
||||
case .inputSpeech:
|
||||
let primary = 0.5 + (0.5 * sin((t * 14.0) + phase))
|
||||
let secondary = 0.5 + (0.5 * sin((t * 5.0) + (phase * 1.35)))
|
||||
return min(max(0.16 + (0.60 * primary) + (0.24 * secondary), 0.14), 1.0)
|
||||
case .speaking:
|
||||
let wave = 0.5 + (0.5 * sin((t * 7.5) + phase))
|
||||
let secondary = 0.5 + (0.5 * sin((t * 3.0) + (phase * 0.7)))
|
||||
return min(max(0.18 + (0.58 * wave) + (0.24 * secondary), 0.12), 1.0)
|
||||
case .indeterminate:
|
||||
let center = (sin((t * 3.2) + phase) + 1) / 2
|
||||
return 0.16 + (0.42 * center)
|
||||
case .still:
|
||||
return index == self.barCount / 2 ? 0.32 : 0.16
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,6 @@ private struct RelayGatewayPushRegistrationPayload: Encodable {
|
||||
var topic: String
|
||||
var environment: String
|
||||
var distribution: String
|
||||
var relayOrigin: String
|
||||
var tokenDebugSuffix: String?
|
||||
}
|
||||
|
||||
@@ -108,7 +107,6 @@ actor PushRegistrationManager {
|
||||
topic: topic,
|
||||
environment: self.buildConfig.apnsEnvironment.rawValue,
|
||||
distribution: self.buildConfig.distribution.rawValue,
|
||||
relayOrigin: relayOrigin,
|
||||
tokenDebugSuffix: stored.tokenDebugSuffix))
|
||||
}
|
||||
|
||||
@@ -140,7 +138,6 @@ actor PushRegistrationManager {
|
||||
topic: topic,
|
||||
environment: self.buildConfig.apnsEnvironment.rawValue,
|
||||
distribution: self.buildConfig.distribution.rawValue,
|
||||
relayOrigin: relayOrigin,
|
||||
tokenDebugSuffix: registrationState.tokenDebugSuffix))
|
||||
}
|
||||
|
||||
|
||||
@@ -36,7 +36,6 @@ struct RootTabs: View {
|
||||
private enum AppTab: Hashable {
|
||||
case control
|
||||
case chat
|
||||
case talk
|
||||
case agent
|
||||
case settings
|
||||
}
|
||||
@@ -54,8 +53,6 @@ struct RootTabs: View {
|
||||
switch arguments[valueIndex].lowercased() {
|
||||
case "chat":
|
||||
return .chat
|
||||
case "talk", "voice":
|
||||
return .talk
|
||||
case "agent", "agents":
|
||||
return .agent
|
||||
case "settings":
|
||||
@@ -148,14 +145,6 @@ struct RootTabs: View {
|
||||
.tabItem { Label("Chat", systemImage: "bubble.left.fill") }
|
||||
.tag(AppTab.chat)
|
||||
|
||||
TalkProTab(openSettings: { self.selectedTab = .settings })
|
||||
.tabItem {
|
||||
Label(
|
||||
"Talk",
|
||||
systemImage: self.appModel.talkMode.isEnabled ? "waveform.circle.fill" : "waveform.circle")
|
||||
}
|
||||
.tag(AppTab.talk)
|
||||
|
||||
AgentProTab()
|
||||
.tabItem { Label("Agent", systemImage: "person.2.fill") }
|
||||
.tag(AppTab.agent)
|
||||
|
||||
@@ -17,7 +17,7 @@ private func makeRealtimeAudioTapBlock(
|
||||
inputSampleRate: inputSampleRate,
|
||||
targetSampleRate: targetSampleRate)
|
||||
guard !encoded.isEmpty else { return }
|
||||
let timestampMs = (ProcessInfo.processInfo.systemUptime * 1000).rounded()
|
||||
let timestampMs = ProcessInfo.processInfo.systemUptime * 1000
|
||||
let rms = RealtimeTalkRelaySession.rmsLevel(buffer: buffer)
|
||||
onAudio(encoded, timestampMs, rms)
|
||||
}
|
||||
@@ -125,24 +125,15 @@ final class RealtimeTalkRelaySession {
|
||||
private var eventTask: Task<Void, Never>?
|
||||
private var outputTask: Task<Void, Never>?
|
||||
private var outputContinuation: AsyncThrowingStream<Data, Error>.Continuation?
|
||||
private var outputIdleTask: Task<Void, Never>?
|
||||
private var outputSessionId = 0
|
||||
private var pendingOutputChunks: [Data] = []
|
||||
private var pendingOutputDone = false
|
||||
private var audioSender: RealtimeAudioSender?
|
||||
private var isClosed = false
|
||||
private var isOutputPlaying = false
|
||||
private var outputStartedAtMs: Double?
|
||||
private var outputPlaybackExpectedEndMs: Double = 0
|
||||
private var lastBargeInAtMs: Double = 0
|
||||
private var micLogFrameCount = 0
|
||||
private var micLogByteCount = 0
|
||||
private var micLogMaxRms: Float = 0
|
||||
private var lastMicLogAtMs: Double = 0
|
||||
private var suppressedEchoFrameCount = 0
|
||||
private var suppressedEchoByteCount = 0
|
||||
private var suppressedEchoMaxRms: Float = 0
|
||||
private var lastSuppressedEchoLogAtMs: Double = 0
|
||||
private var outputAudioChunkCount = 0
|
||||
private var outputAudioByteCount = 0
|
||||
|
||||
@@ -177,6 +168,7 @@ final class RealtimeTalkRelaySession {
|
||||
let eventStream = await self.gateway.subscribeServerEvents(bufferingNewest: 200)
|
||||
self.startEventPump(stream: eventStream)
|
||||
self.configureAudioContract(result.audio)
|
||||
self.startOutputPlayback()
|
||||
try self.startMicrophonePump()
|
||||
self.onStatus("Listening (Realtime)")
|
||||
} catch {
|
||||
@@ -227,6 +219,7 @@ final class RealtimeTalkRelaySession {
|
||||
|
||||
func cancelOutput(reason: String = "user") {
|
||||
self.stopOutputPlayback()
|
||||
self.startOutputPlayback()
|
||||
guard let relaySessionId else { return }
|
||||
Task { [gateway] in
|
||||
let payload: [String: Any] = [
|
||||
@@ -313,18 +306,12 @@ final class RealtimeTalkRelaySession {
|
||||
let data = Data(base64Encoded: base64)
|
||||
else { return }
|
||||
self.recordOutputAudioChunk(byteCount: data.count)
|
||||
self.markOutputAudioStarted(byteCount: data.count, nowMs: ProcessInfo.processInfo.systemUptime * 1000)
|
||||
self.markOutputAudioStarted(nowMs: ProcessInfo.processInfo.systemUptime * 1000)
|
||||
self.onSpeakingChanged(true)
|
||||
if self.outputContinuation == nil, self.outputTask != nil {
|
||||
self.pendingOutputChunks.append(data)
|
||||
return
|
||||
}
|
||||
self.ensureOutputPlaybackStarted()
|
||||
self.outputContinuation?.yield(data)
|
||||
case "audioDone":
|
||||
self.finishOutputPlaybackStream()
|
||||
case "clear":
|
||||
self.stopOutputPlayback()
|
||||
self.startOutputPlayback()
|
||||
case "transcript":
|
||||
self.handleTranscriptEvent(payload)
|
||||
case "toolCall":
|
||||
@@ -350,16 +337,11 @@ final class RealtimeTalkRelaySession {
|
||||
"talk realtime audio: chunks=\(self.outputAudioChunkCount) bytes=\(self.outputAudioByteCount)")
|
||||
}
|
||||
|
||||
private func markOutputAudioStarted(byteCount: Int, nowMs: Double) {
|
||||
private func markOutputAudioStarted(nowMs: Double) {
|
||||
if !self.isOutputPlaying {
|
||||
self.outputStartedAtMs = nowMs
|
||||
self.outputPlaybackExpectedEndMs = nowMs
|
||||
}
|
||||
self.isOutputPlaying = true
|
||||
let bytesPerSecond = max(1, self.outputSampleRateHz * Double(MemoryLayout<Int16>.size))
|
||||
let chunkDurationMs = (Double(byteCount) / bytesPerSecond) * 1000
|
||||
self.outputPlaybackExpectedEndMs = max(nowMs, self.outputPlaybackExpectedEndMs) + chunkDurationMs
|
||||
self.scheduleOutputPlaybackIdle(expectedEndMs: self.outputPlaybackExpectedEndMs)
|
||||
}
|
||||
|
||||
private func handleInputLevelDuringOutput(_ rms: Float, timestampMs: Double) {
|
||||
@@ -555,25 +537,14 @@ final class RealtimeTalkRelaySession {
|
||||
{ [weak self, audioSender = self.audioSender] encoded, timestampMs, rms in
|
||||
guard let audioSender else { return }
|
||||
Task {
|
||||
let shouldSend = await MainActor.run { [weak self] in
|
||||
guard let self, !self.isClosed else { return false }
|
||||
self.recordMicrophoneFrame(byteCount: encoded.count, rms: rms, timestampMs: timestampMs)
|
||||
self.refreshOutputPlaybackState(timestampMs: timestampMs)
|
||||
if self.isOutputPlaying {
|
||||
if self.shouldSuppressMicrophoneDuringOutput() {
|
||||
self.recordSuppressedOutputEchoFrame(
|
||||
byteCount: encoded.count,
|
||||
rms: rms,
|
||||
timestampMs: timestampMs)
|
||||
return false
|
||||
}
|
||||
if rms >= Self.bargeInRmsThreshold {
|
||||
self.handleInputLevelDuringOutput(rms, timestampMs: timestampMs)
|
||||
}
|
||||
}
|
||||
return true
|
||||
await MainActor.run { [weak self] in
|
||||
self?.recordMicrophoneFrame(byteCount: encoded.count, rms: rms, timestampMs: timestampMs)
|
||||
}
|
||||
if rms >= Self.bargeInRmsThreshold {
|
||||
await MainActor.run { [weak self] in
|
||||
self?.handleInputLevelDuringOutput(rms, timestampMs: timestampMs)
|
||||
}
|
||||
}
|
||||
guard shouldSend else { return }
|
||||
guard let message = await audioSender.send(encoded, timestampMs: timestampMs) else { return }
|
||||
await MainActor.run { [weak self] in
|
||||
guard let self, !self.isClosed else { return }
|
||||
@@ -590,13 +561,6 @@ final class RealtimeTalkRelaySession {
|
||||
try self.audioEngine.start()
|
||||
}
|
||||
|
||||
private func shouldSuppressMicrophoneDuringOutput() -> Bool {
|
||||
let outputs = AVAudioSession.sharedInstance().currentRoute.outputs
|
||||
// Built-in speaker output bleeds into the microphone even in voiceChat mode; keep the
|
||||
// realtime provider from treating its own speech as user input. Headsets keep barge-in.
|
||||
return outputs.contains { $0.portType == .builtInSpeaker }
|
||||
}
|
||||
|
||||
private func recordMicrophoneFrame(byteCount: Int, rms: Float, timestampMs: Double) {
|
||||
guard !self.isClosed else { return }
|
||||
self.micLogFrameCount += 1
|
||||
@@ -612,31 +576,13 @@ final class RealtimeTalkRelaySession {
|
||||
self.micLogMaxRms = 0
|
||||
}
|
||||
|
||||
private func recordSuppressedOutputEchoFrame(byteCount: Int, rms: Float, timestampMs: Double) {
|
||||
self.suppressedEchoFrameCount += 1
|
||||
self.suppressedEchoByteCount += byteCount
|
||||
self.suppressedEchoMaxRms = max(self.suppressedEchoMaxRms, rms)
|
||||
guard timestampMs - self.lastSuppressedEchoLogAtMs >= 1000 else { return }
|
||||
self.lastSuppressedEchoLogAtMs = timestampMs
|
||||
let maxRms = String(format: "%.4f", Double(self.suppressedEchoMaxRms))
|
||||
GatewayDiagnostics.log(
|
||||
"talk realtime mic suppressed during output: "
|
||||
+ "buffers=\(self.suppressedEchoFrameCount) "
|
||||
+ "bytes=\(self.suppressedEchoByteCount) maxRms=\(maxRms)")
|
||||
self.suppressedEchoFrameCount = 0
|
||||
self.suppressedEchoByteCount = 0
|
||||
self.suppressedEchoMaxRms = 0
|
||||
}
|
||||
|
||||
private func stopMicrophonePump() {
|
||||
self.audioEngine.inputNode.removeTap(onBus: 0)
|
||||
self.audioEngine.stop()
|
||||
}
|
||||
|
||||
private func ensureOutputPlaybackStarted() {
|
||||
guard self.outputContinuation == nil, self.outputTask == nil else { return }
|
||||
self.outputSessionId += 1
|
||||
let sessionId = self.outputSessionId
|
||||
private func startOutputPlayback() {
|
||||
self.stopOutputPlayback()
|
||||
let stream = AsyncThrowingStream<Data, Error> { continuation in
|
||||
self.outputContinuation = continuation
|
||||
}
|
||||
@@ -644,95 +590,28 @@ final class RealtimeTalkRelaySession {
|
||||
guard let self else { return }
|
||||
let result = await self.pcmPlayer.play(stream: stream, sampleRate: self.outputSampleRateHz)
|
||||
await MainActor.run {
|
||||
guard self.outputSessionId == sessionId else { return }
|
||||
self.outputTask = nil
|
||||
self.outputContinuation = nil
|
||||
if !result.finished, let interruptedAt = result.interruptedAt {
|
||||
self.logger.info("realtime output interrupted at \(interruptedAt, privacy: .public)s")
|
||||
}
|
||||
self.markOutputPlaybackFinished()
|
||||
self.startPendingOutputPlaybackIfNeeded()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func finishOutputPlaybackStream() {
|
||||
guard let continuation = self.outputContinuation else {
|
||||
if self.outputTask != nil, !self.pendingOutputChunks.isEmpty {
|
||||
self.pendingOutputDone = true
|
||||
}
|
||||
return
|
||||
}
|
||||
continuation.finish()
|
||||
self.outputContinuation = nil
|
||||
}
|
||||
|
||||
private func startPendingOutputPlaybackIfNeeded() {
|
||||
guard !self.pendingOutputChunks.isEmpty else {
|
||||
self.pendingOutputDone = false
|
||||
return
|
||||
}
|
||||
let chunks = self.pendingOutputChunks
|
||||
let shouldFinish = self.pendingOutputDone
|
||||
self.pendingOutputChunks = []
|
||||
self.pendingOutputDone = false
|
||||
self.ensureOutputPlaybackStarted()
|
||||
for chunk in chunks {
|
||||
self.markOutputAudioStarted(byteCount: chunk.count, nowMs: ProcessInfo.processInfo.systemUptime * 1000)
|
||||
self.onSpeakingChanged(true)
|
||||
self.outputContinuation?.yield(chunk)
|
||||
}
|
||||
if shouldFinish {
|
||||
self.finishOutputPlaybackStream()
|
||||
}
|
||||
}
|
||||
|
||||
private func scheduleOutputPlaybackIdle(expectedEndMs: Double) {
|
||||
self.outputIdleTask?.cancel()
|
||||
let nowMs = ProcessInfo.processInfo.systemUptime * 1000
|
||||
let idleDelayMs = max(350, expectedEndMs - nowMs + 500)
|
||||
self.outputIdleTask = Task { [weak self] in
|
||||
try? await Task.sleep(nanoseconds: UInt64(idleDelayMs * 1_000_000))
|
||||
guard !Task.isCancelled else { return }
|
||||
await MainActor.run { [weak self] in
|
||||
guard let self, !self.isClosed else { return }
|
||||
let nowMs = ProcessInfo.processInfo.systemUptime * 1000
|
||||
self.refreshOutputPlaybackState(timestampMs: nowMs, cancelIdleTask: false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func refreshOutputPlaybackState(timestampMs: Double, cancelIdleTask: Bool = true) {
|
||||
guard self.isOutputPlaying else { return }
|
||||
guard timestampMs >= self.outputPlaybackExpectedEndMs + 500 else { return }
|
||||
self.markOutputPlaybackFinished(cancelIdleTask: cancelIdleTask)
|
||||
}
|
||||
|
||||
private func markOutputPlaybackFinished(cancelIdleTask: Bool = true) {
|
||||
if cancelIdleTask {
|
||||
self.outputIdleTask?.cancel()
|
||||
self.outputIdleTask = nil
|
||||
}
|
||||
private func markOutputPlaybackFinished() {
|
||||
self.isOutputPlaying = false
|
||||
self.outputStartedAtMs = nil
|
||||
self.outputPlaybackExpectedEndMs = 0
|
||||
self.onSpeakingChanged(false)
|
||||
}
|
||||
|
||||
private func stopOutputPlayback() {
|
||||
self.outputSessionId += 1
|
||||
self.outputContinuation?.finish()
|
||||
self.outputContinuation = nil
|
||||
self.outputTask?.cancel()
|
||||
self.outputTask = nil
|
||||
self.outputIdleTask?.cancel()
|
||||
self.outputIdleTask = nil
|
||||
self.pendingOutputChunks = []
|
||||
self.pendingOutputDone = false
|
||||
_ = self.pcmPlayer.stop()
|
||||
self.isOutputPlaying = false
|
||||
self.outputStartedAtMs = nil
|
||||
self.outputPlaybackExpectedEndMs = 0
|
||||
self.onSpeakingChanged(false)
|
||||
}
|
||||
|
||||
@@ -805,7 +684,7 @@ final class RealtimeTalkRelaySession {
|
||||
|
||||
extension RealtimeTalkRelaySession {
|
||||
func _test_markOutputAudioStarted(nowMs: Double) {
|
||||
self.markOutputAudioStarted(byteCount: 4800, nowMs: nowMs)
|
||||
self.markOutputAudioStarted(nowMs: nowMs)
|
||||
}
|
||||
|
||||
func _test_markOutputPlaybackFinished() {
|
||||
|
||||
@@ -1141,7 +1141,7 @@ final class TalkModeManager: NSObject {
|
||||
})
|
||||
self.realtimeRelaySession = relaySession
|
||||
do {
|
||||
try Self.configureRealtimeAudioSession()
|
||||
try Self.configureAudioSession()
|
||||
try await relaySession.start()
|
||||
guard self.realtimeRelaySession === relaySession, self.isEnabled else {
|
||||
relaySession.stop()
|
||||
|
||||
@@ -13,7 +13,6 @@ Sources/Design/AgentProNodesDestination.swift
|
||||
Sources/Design/AgentProTab.swift
|
||||
Sources/Design/ChatProTab.swift
|
||||
Sources/Design/CommandCenterTab.swift
|
||||
Sources/Design/TalkProTab.swift
|
||||
Sources/Design/OpenClawProComponents.swift
|
||||
Sources/Design/OpenClawProScreens.swift
|
||||
Sources/Design/SettingsProTab.swift
|
||||
|
||||
@@ -1,3 +1 @@
|
||||
Maintenance update for the current OpenClaw release.
|
||||
|
||||
- Added hosted push relay defaults, realtime Talk playback, and safer WebSocket ping handling for mobile sessions.
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
{
|
||||
"version": "2026.5.30"
|
||||
"version": "2026.5.28"
|
||||
}
|
||||
|
||||
@@ -170,8 +170,6 @@ final class AppState {
|
||||
}
|
||||
}
|
||||
|
||||
var voiceWakeMeterActive = false
|
||||
|
||||
var talkEnabled: Bool {
|
||||
didSet {
|
||||
self.ifNotPreview {
|
||||
|
||||
@@ -63,14 +63,6 @@ extension CritterStatusLabel {
|
||||
.frame(width: 6, height: 6)
|
||||
.padding(1)
|
||||
}
|
||||
|
||||
if self.voiceWakeMeterActive {
|
||||
Circle()
|
||||
.fill(.orange)
|
||||
.frame(width: 5, height: 5)
|
||||
.padding(2)
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottomLeading)
|
||||
}
|
||||
}
|
||||
.frame(width: 18, height: 18)
|
||||
}
|
||||
@@ -247,8 +239,7 @@ extension CritterStatusLabel {
|
||||
sendCelebrationTick: 1,
|
||||
gatewayStatus: .running(details: nil),
|
||||
animationsEnabled: true,
|
||||
iconState: .workingMain(.tool(.bash)),
|
||||
voiceWakeMeterActive: true)
|
||||
iconState: .workingMain(.tool(.bash)))
|
||||
|
||||
_ = label.body
|
||||
_ = label.iconImage
|
||||
@@ -284,8 +275,7 @@ extension CritterStatusLabel {
|
||||
sendCelebrationTick: 0,
|
||||
gatewayStatus: .failed("boom"),
|
||||
animationsEnabled: false,
|
||||
iconState: .idle,
|
||||
voiceWakeMeterActive: false)
|
||||
iconState: .idle)
|
||||
_ = failed.gatewayNeedsAttention
|
||||
_ = failed.gatewayBadgeColor
|
||||
|
||||
@@ -298,8 +288,7 @@ extension CritterStatusLabel {
|
||||
sendCelebrationTick: 0,
|
||||
gatewayStatus: .stopped,
|
||||
animationsEnabled: false,
|
||||
iconState: .idle,
|
||||
voiceWakeMeterActive: false)
|
||||
iconState: .idle)
|
||||
_ = stopped.gatewayNeedsAttention
|
||||
_ = stopped.gatewayBadgeColor
|
||||
|
||||
|
||||
@@ -10,7 +10,6 @@ struct CritterStatusLabel: View {
|
||||
var gatewayStatus: GatewayProcessManager.Status
|
||||
var animationsEnabled: Bool
|
||||
var iconState: IconState
|
||||
var voiceWakeMeterActive: Bool = false
|
||||
|
||||
@State var blinkAmount: CGFloat = 0
|
||||
@State var nextBlink = Date().addingTimeInterval(Double.random(in: 3.5...8.5))
|
||||
|
||||
@@ -50,8 +50,7 @@ struct OpenClawApp: App {
|
||||
sendCelebrationTick: self.state.sendCelebrationTick,
|
||||
gatewayStatus: self.gatewayManager.status,
|
||||
animationsEnabled: self.state.iconAnimationsEnabled && !self.isGatewaySleeping,
|
||||
iconState: self.effectiveIconState,
|
||||
voiceWakeMeterActive: self.state.voiceWakeMeterActive)
|
||||
iconState: self.effectiveIconState)
|
||||
.background(SettingsWindowOpenRegistrar())
|
||||
}
|
||||
.menuBarExtraAccess(isPresented: self.$isMenuPresented) { item in
|
||||
@@ -76,9 +75,6 @@ struct OpenClawApp: App {
|
||||
.onChange(of: self.gatewayManager.status) { _, _ in
|
||||
self.applyStatusItemAppearance(paused: self.state.isPaused, sleeping: self.isGatewaySleeping)
|
||||
}
|
||||
.onChange(of: self.state.voiceWakeMeterActive) { _, _ in
|
||||
self.applyStatusItemAppearance(paused: self.state.isPaused, sleeping: self.isGatewaySleeping)
|
||||
}
|
||||
.onChange(of: self.state.connectionMode) { _, mode in
|
||||
Task { await ConnectionModeCoordinator.shared.apply(mode: mode, paused: self.state.isPaused) }
|
||||
CLIInstallPrompter.shared.checkAndPromptIfNeeded(reason: "connection-mode")
|
||||
@@ -111,9 +107,6 @@ struct OpenClawApp: App {
|
||||
// The SwiftUI label already renders those states; AppKit's disabled appearance can
|
||||
// leak into menu item validation and grey out app-level commands like Settings.
|
||||
self.statusItem?.button?.appearsDisabled = false
|
||||
self.statusItem?.button?.toolTip = self.state.voiceWakeMeterActive
|
||||
? "OpenClaw - Voice Wake live meter active"
|
||||
: "OpenClaw"
|
||||
}
|
||||
|
||||
private static func applyAttachOnlyOverrideIfNeeded() {
|
||||
|
||||
@@ -8,18 +8,12 @@ actor MicLevelMonitor {
|
||||
private var update: (@Sendable (Double) -> Void)?
|
||||
private var running = false
|
||||
private var smoothedLevel: Double = 0
|
||||
private var lastUpdate = ContinuousClock.now
|
||||
private var lastPublishedLevel: Double = 0
|
||||
private let minimumUpdateInterval: Duration = .milliseconds(125)
|
||||
private let minimumLevelDelta = 0.02
|
||||
|
||||
func start(onLevel: @Sendable @escaping (Double) -> Void) async throws {
|
||||
self.update = onLevel
|
||||
if self.running { return }
|
||||
self.logger.info(
|
||||
"mic level monitor start (\(AudioInputDeviceObserver.defaultInputDeviceSummary(), privacy: .public))")
|
||||
self.lastUpdate = .now
|
||||
self.lastPublishedLevel = self.smoothedLevel
|
||||
guard AudioInputDeviceObserver.hasUsableDefaultInputDevice() else {
|
||||
self.engine = nil
|
||||
throw NSError(
|
||||
@@ -62,13 +56,7 @@ actor MicLevelMonitor {
|
||||
private func push(level: Double) {
|
||||
self.smoothedLevel = (self.smoothedLevel * 0.45) + (level * 0.55)
|
||||
guard let update else { return }
|
||||
let now = ContinuousClock.now
|
||||
guard now - self.lastUpdate >= self.minimumUpdateInterval ||
|
||||
abs(self.smoothedLevel - self.lastPublishedLevel) >= self.minimumLevelDelta
|
||||
else { return }
|
||||
self.lastUpdate = now
|
||||
let value = self.smoothedLevel
|
||||
self.lastPublishedLevel = value
|
||||
Task { @MainActor in update(value) }
|
||||
}
|
||||
|
||||
|
||||
@@ -15,9 +15,9 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>2026.5.30</string>
|
||||
<string>2026.5.28</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>2026053000</string>
|
||||
<string>2026052800</string>
|
||||
<key>CFBundleIconFile</key>
|
||||
<string>OpenClaw</string>
|
||||
<key>CFBundleURLTypes</key>
|
||||
|
||||
@@ -3,12 +3,12 @@ import Foundation
|
||||
|
||||
enum SoundEffectCatalog {
|
||||
/// All discoverable system sound names, with "Glass" pinned first.
|
||||
static let systemOptions: [String] = {
|
||||
static var systemOptions: [String] {
|
||||
var names = Set(Self.discoveredSoundMap.keys).union(Self.fallbackNames)
|
||||
names.remove("Glass")
|
||||
let sorted = names.sorted { $0.localizedCaseInsensitiveCompare($1) == .orderedAscending }
|
||||
return ["Glass"] + sorted
|
||||
}()
|
||||
}
|
||||
|
||||
static func displayName(for raw: String) -> String {
|
||||
raw
|
||||
|
||||
@@ -20,7 +20,6 @@ struct VoiceWakeSettings: View {
|
||||
private let meter = MicLevelMonitor()
|
||||
@State private var micObserver = AudioInputDeviceObserver()
|
||||
@State private var micRefreshTask: Task<Void, Never>?
|
||||
@State private var meterStartupTask: Task<Void, Never>?
|
||||
@State private var availableLocales: [Locale] = []
|
||||
@State private var triggerEntries: [TriggerEntry] = []
|
||||
private let fieldLabelWidth: CGFloat = 140
|
||||
@@ -189,68 +188,59 @@ struct VoiceWakeSettings: View {
|
||||
}
|
||||
.settingsDetailContent()
|
||||
}
|
||||
.task {
|
||||
guard !self.isPreview else { return }
|
||||
await self.loadMicsIfNeeded()
|
||||
}
|
||||
.task {
|
||||
guard !self.isPreview else { return }
|
||||
await self.loadLocalesIfNeeded()
|
||||
}
|
||||
.task {
|
||||
guard !self.isPreview else { return }
|
||||
await self.restartMeter()
|
||||
}
|
||||
.onAppear {
|
||||
guard !self.isPreview else { return }
|
||||
guard self.isActive else { return }
|
||||
self.activateLivePreview()
|
||||
self.startMicObserver()
|
||||
self.loadTriggerEntries()
|
||||
}
|
||||
.onChange(of: self.state.voiceWakeMicID) { _, _ in
|
||||
guard !self.isPreview else { return }
|
||||
self.updateSelectedMicName()
|
||||
guard self.isActive else { return }
|
||||
self.scheduleMeterRestart()
|
||||
Task { await self.restartMeter() }
|
||||
}
|
||||
.onChange(of: self.isActive) { _, active in
|
||||
guard !self.isPreview else { return }
|
||||
if !active {
|
||||
self.deactivateLivePreview()
|
||||
self.tester.stop()
|
||||
self.isTesting = false
|
||||
self.testState = .idle
|
||||
self.testTimeoutTask?.cancel()
|
||||
self.micRefreshTask?.cancel()
|
||||
self.micRefreshTask = nil
|
||||
Task { await self.meter.stop() }
|
||||
self.micObserver.stop()
|
||||
self.syncTriggerEntriesToState()
|
||||
} else {
|
||||
self.activateLivePreview()
|
||||
self.startMicObserver()
|
||||
self.loadTriggerEntries()
|
||||
}
|
||||
}
|
||||
.onDisappear {
|
||||
guard !self.isPreview else { return }
|
||||
self.deactivateLivePreview()
|
||||
self.tester.stop()
|
||||
self.isTesting = false
|
||||
self.testState = .idle
|
||||
self.testTimeoutTask?.cancel()
|
||||
self.micRefreshTask?.cancel()
|
||||
self.micRefreshTask = nil
|
||||
self.micObserver.stop()
|
||||
Task { await self.meter.stop() }
|
||||
self.syncTriggerEntriesToState()
|
||||
}
|
||||
}
|
||||
|
||||
private func activateLivePreview() {
|
||||
self.meterStartupTask?.cancel()
|
||||
self.startMicObserver()
|
||||
self.loadTriggerEntries()
|
||||
self.meterStartupTask = Task { @MainActor in
|
||||
await self.loadMicsIfNeeded()
|
||||
guard !Task.isCancelled, self.isActive else { return }
|
||||
await self.loadLocalesIfNeeded()
|
||||
guard !Task.isCancelled, self.isActive else { return }
|
||||
await self.restartMeter()
|
||||
}
|
||||
}
|
||||
|
||||
private func deactivateLivePreview() {
|
||||
self.tester.stop()
|
||||
self.isTesting = false
|
||||
self.testState = .idle
|
||||
self.testTimeoutTask?.cancel()
|
||||
self.micRefreshTask?.cancel()
|
||||
self.micRefreshTask = nil
|
||||
self.meterStartupTask?.cancel()
|
||||
self.meterStartupTask = nil
|
||||
self.micObserver.stop()
|
||||
self.state.voiceWakeMeterActive = false
|
||||
Task { await self.meter.stop() }
|
||||
}
|
||||
|
||||
private func scheduleMeterRestart() {
|
||||
self.meterStartupTask?.cancel()
|
||||
self.meterStartupTask = Task { @MainActor in
|
||||
guard !Task.isCancelled, self.isActive else { return }
|
||||
await self.restartMeter()
|
||||
}
|
||||
}
|
||||
|
||||
private func loadTriggerEntries() {
|
||||
self.triggerEntries = self.state.swabbleTriggerWords.map { TriggerEntry(id: UUID(), value: $0) }
|
||||
}
|
||||
@@ -662,7 +652,6 @@ struct VoiceWakeSettings: View {
|
||||
|
||||
@MainActor
|
||||
private func scheduleMicRefresh() {
|
||||
guard self.isActive else { return }
|
||||
MicRefreshSupport.schedule(refreshTask: &self.micRefreshTask) {
|
||||
await self.loadMicsIfNeeded(force: true)
|
||||
await self.restartMeter()
|
||||
@@ -724,17 +713,8 @@ struct VoiceWakeSettings: View {
|
||||
|
||||
@MainActor
|
||||
private func restartMeter() async {
|
||||
guard self.isActive else {
|
||||
self.state.voiceWakeMeterActive = false
|
||||
await self.meter.stop()
|
||||
return
|
||||
}
|
||||
self.meterError = nil
|
||||
await self.meter.stop()
|
||||
guard !Task.isCancelled, self.isActive else {
|
||||
self.state.voiceWakeMeterActive = false
|
||||
return
|
||||
}
|
||||
do {
|
||||
try await self.meter.start { [weak state] level in
|
||||
Task { @MainActor in
|
||||
@@ -742,14 +722,7 @@ struct VoiceWakeSettings: View {
|
||||
self.meterLevel = level
|
||||
}
|
||||
}
|
||||
guard !Task.isCancelled, self.isActive else {
|
||||
self.state.voiceWakeMeterActive = false
|
||||
await self.meter.stop()
|
||||
return
|
||||
}
|
||||
self.state.voiceWakeMeterActive = true
|
||||
} catch {
|
||||
self.state.voiceWakeMeterActive = false
|
||||
self.meterError = error.localizedDescription
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,22 +14,6 @@ public protocol WebSocketTasking: AnyObject {
|
||||
|
||||
extension URLSessionWebSocketTask: WebSocketTasking {}
|
||||
|
||||
private final class WebSocketPingContinuationGate: @unchecked Sendable {
|
||||
private let lock = NSLock()
|
||||
private var didResume = false
|
||||
|
||||
func resumeOnce(_ resume: () -> Void) {
|
||||
self.lock.lock()
|
||||
if self.didResume {
|
||||
self.lock.unlock()
|
||||
return
|
||||
}
|
||||
self.didResume = true
|
||||
self.lock.unlock()
|
||||
resume()
|
||||
}
|
||||
}
|
||||
|
||||
public struct WebSocketTaskBox: @unchecked Sendable {
|
||||
public let task: any WebSocketTasking
|
||||
public init(task: any WebSocketTasking) {
|
||||
@@ -64,13 +48,8 @@ public struct WebSocketTaskBox: @unchecked Sendable {
|
||||
|
||||
public func sendPing() async throws {
|
||||
try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Error>) in
|
||||
let gate = WebSocketPingContinuationGate()
|
||||
self.task.sendPing { error in
|
||||
// URLSession can race ping callbacks with cancellation; only the first
|
||||
// pong result owns this checked continuation or Swift traps the app.
|
||||
gate.resumeOnce {
|
||||
ThrowingContinuationSupport.resumeVoid(continuation, error: error)
|
||||
}
|
||||
ThrowingContinuationSupport.resumeVoid(continuation, error: error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -361,26 +361,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"get_goal": {
|
||||
"emoji": "🎯",
|
||||
"title": "Get Goal",
|
||||
"detailKeys": []
|
||||
},
|
||||
"create_goal": {
|
||||
"emoji": "🎯",
|
||||
"title": "Create Goal",
|
||||
"detailKeys": [
|
||||
"objective",
|
||||
"token_budget"
|
||||
]
|
||||
},
|
||||
"update_goal": {
|
||||
"emoji": "🎯",
|
||||
"title": "Update Goal",
|
||||
"detailKeys": [
|
||||
"status"
|
||||
]
|
||||
},
|
||||
"update_plan": {
|
||||
"emoji": "🗺️",
|
||||
"title": "Update Plan",
|
||||
|
||||
@@ -556,7 +556,7 @@ public struct MessageActionParams: Codable, Sendable {
|
||||
sessionkey: String?,
|
||||
sessionid: String?,
|
||||
inboundturnkind: String? = nil,
|
||||
agentid: String? = nil,
|
||||
agentid: String?,
|
||||
toolcontext: [String: AnyCodable]?,
|
||||
idempotencykey: String)
|
||||
{
|
||||
@@ -617,7 +617,7 @@ public struct SendParams: Codable, Sendable {
|
||||
gifplayback: Bool?,
|
||||
channel: String?,
|
||||
accountid: String?,
|
||||
agentid: String? = nil,
|
||||
agentid: String?,
|
||||
replytoid: String?,
|
||||
threadid: String?,
|
||||
forcedocument: Bool?,
|
||||
@@ -765,7 +765,7 @@ public struct AgentParams: Codable, Sendable {
|
||||
|
||||
public init(
|
||||
message: String,
|
||||
agentid: String? = nil,
|
||||
agentid: String?,
|
||||
provider: String?,
|
||||
model: String?,
|
||||
to: String?,
|
||||
@@ -893,7 +893,7 @@ public struct AgentIdentityParams: Codable, Sendable {
|
||||
public let sessionkey: String?
|
||||
|
||||
public init(
|
||||
agentid: String? = nil,
|
||||
agentid: String?,
|
||||
sessionkey: String?)
|
||||
{
|
||||
self.agentid = agentid
|
||||
@@ -1617,7 +1617,7 @@ public struct SessionsListParams: Codable, Sendable {
|
||||
includelastmessage: Bool?,
|
||||
label: String?,
|
||||
spawnedby: String?,
|
||||
agentid: String? = nil,
|
||||
agentid: String?,
|
||||
search: String?)
|
||||
{
|
||||
self.limit = limit
|
||||
@@ -1741,7 +1741,7 @@ public struct SessionsResolveParams: Codable, Sendable {
|
||||
key: String?,
|
||||
sessionid: String?,
|
||||
label: String?,
|
||||
agentid: String? = nil,
|
||||
agentid: String?,
|
||||
spawnedby: String?,
|
||||
includeglobal: Bool?,
|
||||
includeunknown: Bool?)
|
||||
@@ -1825,7 +1825,6 @@ public struct SessionOperationEvent: Codable, Sendable {
|
||||
public let operation: String
|
||||
public let phase: AnyCodable
|
||||
public let sessionkey: String
|
||||
public let agentid: String?
|
||||
public let ts: Int
|
||||
public let completed: Bool?
|
||||
public let reason: String?
|
||||
@@ -1835,7 +1834,6 @@ public struct SessionOperationEvent: Codable, Sendable {
|
||||
operation: String,
|
||||
phase: AnyCodable,
|
||||
sessionkey: String,
|
||||
agentid: String? = nil,
|
||||
ts: Int,
|
||||
completed: Bool?,
|
||||
reason: String?)
|
||||
@@ -1844,7 +1842,6 @@ public struct SessionOperationEvent: Codable, Sendable {
|
||||
self.operation = operation
|
||||
self.phase = phase
|
||||
self.sessionkey = sessionkey
|
||||
self.agentid = agentid
|
||||
self.ts = ts
|
||||
self.completed = completed
|
||||
self.reason = reason
|
||||
@@ -1855,7 +1852,6 @@ public struct SessionOperationEvent: Codable, Sendable {
|
||||
case operation
|
||||
case phase
|
||||
case sessionkey = "sessionKey"
|
||||
case agentid = "agentId"
|
||||
case ts
|
||||
case completed
|
||||
case reason
|
||||
@@ -1864,84 +1860,68 @@ public struct SessionOperationEvent: Codable, Sendable {
|
||||
|
||||
public struct SessionsCompactionListParams: Codable, Sendable {
|
||||
public let key: String
|
||||
public let agentid: String?
|
||||
|
||||
public init(
|
||||
key: String,
|
||||
agentid: String? = nil)
|
||||
key: String)
|
||||
{
|
||||
self.key = key
|
||||
self.agentid = agentid
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case key
|
||||
case agentid = "agentId"
|
||||
}
|
||||
}
|
||||
|
||||
public struct SessionsCompactionGetParams: Codable, Sendable {
|
||||
public let key: String
|
||||
public let agentid: String?
|
||||
public let checkpointid: String
|
||||
|
||||
public init(
|
||||
key: String,
|
||||
agentid: String? = nil,
|
||||
checkpointid: String)
|
||||
{
|
||||
self.key = key
|
||||
self.agentid = agentid
|
||||
self.checkpointid = checkpointid
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case key
|
||||
case agentid = "agentId"
|
||||
case checkpointid = "checkpointId"
|
||||
}
|
||||
}
|
||||
|
||||
public struct SessionsCompactionBranchParams: Codable, Sendable {
|
||||
public let key: String
|
||||
public let agentid: String?
|
||||
public let checkpointid: String
|
||||
|
||||
public init(
|
||||
key: String,
|
||||
agentid: String? = nil,
|
||||
checkpointid: String)
|
||||
{
|
||||
self.key = key
|
||||
self.agentid = agentid
|
||||
self.checkpointid = checkpointid
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case key
|
||||
case agentid = "agentId"
|
||||
case checkpointid = "checkpointId"
|
||||
}
|
||||
}
|
||||
|
||||
public struct SessionsCompactionRestoreParams: Codable, Sendable {
|
||||
public let key: String
|
||||
public let agentid: String?
|
||||
public let checkpointid: String
|
||||
|
||||
public init(
|
||||
key: String,
|
||||
agentid: String? = nil,
|
||||
checkpointid: String)
|
||||
{
|
||||
self.key = key
|
||||
self.agentid = agentid
|
||||
self.checkpointid = checkpointid
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case key
|
||||
case agentid = "agentId"
|
||||
case checkpointid = "checkpointId"
|
||||
}
|
||||
}
|
||||
@@ -2066,7 +2046,7 @@ public struct SessionsCreateParams: Codable, Sendable {
|
||||
|
||||
public init(
|
||||
key: String?,
|
||||
agentid: String? = nil,
|
||||
agentid: String?,
|
||||
label: String?,
|
||||
model: String?,
|
||||
parentsessionkey: String?,
|
||||
@@ -2098,7 +2078,6 @@ public struct SessionsCreateParams: Codable, Sendable {
|
||||
|
||||
public struct SessionsSendParams: Codable, Sendable {
|
||||
public let key: String
|
||||
public let agentid: String?
|
||||
public let message: String
|
||||
public let thinking: String?
|
||||
public let attachments: [AnyCodable]?
|
||||
@@ -2107,7 +2086,6 @@ public struct SessionsSendParams: Codable, Sendable {
|
||||
|
||||
public init(
|
||||
key: String,
|
||||
agentid: String? = nil,
|
||||
message: String,
|
||||
thinking: String?,
|
||||
attachments: [AnyCodable]?,
|
||||
@@ -2115,7 +2093,6 @@ public struct SessionsSendParams: Codable, Sendable {
|
||||
idempotencykey: String?)
|
||||
{
|
||||
self.key = key
|
||||
self.agentid = agentid
|
||||
self.message = message
|
||||
self.thinking = thinking
|
||||
self.attachments = attachments
|
||||
@@ -2125,7 +2102,6 @@ public struct SessionsSendParams: Codable, Sendable {
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case key
|
||||
case agentid = "agentId"
|
||||
case message
|
||||
case thinking
|
||||
case attachments
|
||||
@@ -2136,37 +2112,29 @@ public struct SessionsSendParams: Codable, Sendable {
|
||||
|
||||
public struct SessionsMessagesSubscribeParams: Codable, Sendable {
|
||||
public let key: String
|
||||
public let agentid: String?
|
||||
|
||||
public init(
|
||||
key: String,
|
||||
agentid: String? = nil)
|
||||
key: String)
|
||||
{
|
||||
self.key = key
|
||||
self.agentid = agentid
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case key
|
||||
case agentid = "agentId"
|
||||
}
|
||||
}
|
||||
|
||||
public struct SessionsMessagesUnsubscribeParams: Codable, Sendable {
|
||||
public let key: String
|
||||
public let agentid: String?
|
||||
|
||||
public init(
|
||||
key: String,
|
||||
agentid: String? = nil)
|
||||
key: String)
|
||||
{
|
||||
self.key = key
|
||||
self.agentid = agentid
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case key
|
||||
case agentid = "agentId"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2194,7 +2162,6 @@ public struct SessionsAbortParams: Codable, Sendable {
|
||||
|
||||
public struct SessionsPatchParams: Codable, Sendable {
|
||||
public let key: String
|
||||
public let agentid: String?
|
||||
public let label: AnyCodable?
|
||||
public let thinkinglevel: AnyCodable?
|
||||
public let fastmode: AnyCodable?
|
||||
@@ -2221,7 +2188,6 @@ public struct SessionsPatchParams: Codable, Sendable {
|
||||
|
||||
public init(
|
||||
key: String,
|
||||
agentid: String? = nil,
|
||||
label: AnyCodable?,
|
||||
thinkinglevel: AnyCodable?,
|
||||
fastmode: AnyCodable?,
|
||||
@@ -2247,7 +2213,6 @@ public struct SessionsPatchParams: Codable, Sendable {
|
||||
groupactivation: AnyCodable?)
|
||||
{
|
||||
self.key = key
|
||||
self.agentid = agentid
|
||||
self.label = label
|
||||
self.thinkinglevel = thinkinglevel
|
||||
self.fastmode = fastmode
|
||||
@@ -2275,7 +2240,6 @@ public struct SessionsPatchParams: Codable, Sendable {
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case key
|
||||
case agentid = "agentId"
|
||||
case label
|
||||
case thinkinglevel = "thinkingLevel"
|
||||
case fastmode = "fastMode"
|
||||
@@ -2356,47 +2320,39 @@ public struct SessionsPluginPatchResult: Codable, Sendable {
|
||||
|
||||
public struct SessionsResetParams: Codable, Sendable {
|
||||
public let key: String
|
||||
public let agentid: String?
|
||||
public let reason: AnyCodable?
|
||||
|
||||
public init(
|
||||
key: String,
|
||||
agentid: String? = nil,
|
||||
reason: AnyCodable?)
|
||||
{
|
||||
self.key = key
|
||||
self.agentid = agentid
|
||||
self.reason = reason
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case key
|
||||
case agentid = "agentId"
|
||||
case reason
|
||||
}
|
||||
}
|
||||
|
||||
public struct SessionsDeleteParams: Codable, Sendable {
|
||||
public let key: String
|
||||
public let agentid: String?
|
||||
public let deletetranscript: Bool?
|
||||
public let emitlifecyclehooks: Bool?
|
||||
|
||||
public init(
|
||||
key: String,
|
||||
agentid: String? = nil,
|
||||
deletetranscript: Bool?,
|
||||
emitlifecyclehooks: Bool?)
|
||||
{
|
||||
self.key = key
|
||||
self.agentid = agentid
|
||||
self.deletetranscript = deletetranscript
|
||||
self.emitlifecyclehooks = emitlifecyclehooks
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case key
|
||||
case agentid = "agentId"
|
||||
case deletetranscript = "deleteTranscript"
|
||||
case emitlifecyclehooks = "emitLifecycleHooks"
|
||||
}
|
||||
@@ -2404,22 +2360,18 @@ public struct SessionsDeleteParams: Codable, Sendable {
|
||||
|
||||
public struct SessionsCompactParams: Codable, Sendable {
|
||||
public let key: String
|
||||
public let agentid: String?
|
||||
public let maxlines: Int?
|
||||
|
||||
public init(
|
||||
key: String,
|
||||
agentid: String? = nil,
|
||||
maxlines: Int?)
|
||||
{
|
||||
self.key = key
|
||||
self.agentid = agentid
|
||||
self.maxlines = maxlines
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case key
|
||||
case agentid = "agentId"
|
||||
case maxlines = "maxLines"
|
||||
}
|
||||
}
|
||||
@@ -2427,7 +2379,6 @@ public struct SessionsCompactParams: Codable, Sendable {
|
||||
public struct SessionsUsageParams: Codable, Sendable {
|
||||
public let key: String?
|
||||
public let agentid: String?
|
||||
public let agentscope: String?
|
||||
public let startdate: String?
|
||||
public let enddate: String?
|
||||
public let mode: AnyCodable?
|
||||
@@ -2441,7 +2392,6 @@ public struct SessionsUsageParams: Codable, Sendable {
|
||||
public init(
|
||||
key: String?,
|
||||
agentid: String? = nil,
|
||||
agentscope: String? = nil,
|
||||
startdate: String?,
|
||||
enddate: String?,
|
||||
mode: AnyCodable?,
|
||||
@@ -2454,7 +2404,6 @@ public struct SessionsUsageParams: Codable, Sendable {
|
||||
{
|
||||
self.key = key
|
||||
self.agentid = agentid
|
||||
self.agentscope = agentscope
|
||||
self.startdate = startdate
|
||||
self.enddate = enddate
|
||||
self.mode = mode
|
||||
@@ -2469,7 +2418,6 @@ public struct SessionsUsageParams: Codable, Sendable {
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case key
|
||||
case agentid = "agentId"
|
||||
case agentscope = "agentScope"
|
||||
case startdate = "startDate"
|
||||
case enddate = "endDate"
|
||||
case mode
|
||||
@@ -2511,7 +2459,7 @@ public struct TaskSummary: Codable, Sendable {
|
||||
runtime: String?,
|
||||
status: AnyCodable,
|
||||
title: String?,
|
||||
agentid: String? = nil,
|
||||
agentid: String?,
|
||||
sessionkey: String?,
|
||||
childsessionkey: String?,
|
||||
ownerkey: String?,
|
||||
@@ -2585,7 +2533,7 @@ public struct TasksListParams: Codable, Sendable {
|
||||
|
||||
public init(
|
||||
status: AnyCodable?,
|
||||
agentid: String? = nil,
|
||||
agentid: String?,
|
||||
sessionkey: String?,
|
||||
limit: Int?,
|
||||
cursor: String?)
|
||||
@@ -4775,7 +4723,7 @@ public struct CommandsListParams: Codable, Sendable {
|
||||
public let includeargs: Bool?
|
||||
|
||||
public init(
|
||||
agentid: String? = nil,
|
||||
agentid: String?,
|
||||
provider: String?,
|
||||
scope: AnyCodable?,
|
||||
includeargs: Bool?)
|
||||
@@ -4812,7 +4760,7 @@ public struct SkillsStatusParams: Codable, Sendable {
|
||||
public let agentid: String?
|
||||
|
||||
public init(
|
||||
agentid: String? = nil)
|
||||
agentid: String?)
|
||||
{
|
||||
self.agentid = agentid
|
||||
}
|
||||
@@ -4827,7 +4775,7 @@ public struct ToolsCatalogParams: Codable, Sendable {
|
||||
public let includeplugins: Bool?
|
||||
|
||||
public init(
|
||||
agentid: String? = nil,
|
||||
agentid: String?,
|
||||
includeplugins: Bool?)
|
||||
{
|
||||
self.agentid = agentid
|
||||
@@ -4961,7 +4909,7 @@ public struct ToolsEffectiveParams: Codable, Sendable {
|
||||
public let sessionkey: String
|
||||
|
||||
public init(
|
||||
agentid: String? = nil,
|
||||
agentid: String?,
|
||||
sessionkey: String)
|
||||
{
|
||||
self.agentid = agentid
|
||||
@@ -5106,7 +5054,7 @@ public struct ToolsInvokeParams: Codable, Sendable {
|
||||
name: String,
|
||||
args: [String: AnyCodable]?,
|
||||
sessionkey: String?,
|
||||
agentid: String? = nil,
|
||||
agentid: String?,
|
||||
confirm: Bool?,
|
||||
idempotencykey: String?)
|
||||
{
|
||||
@@ -5280,7 +5228,7 @@ public struct SkillsSecurityVerdictsParams: Codable, Sendable {
|
||||
public let agentid: String?
|
||||
|
||||
public init(
|
||||
agentid: String? = nil)
|
||||
agentid: String?)
|
||||
{
|
||||
self.agentid = agentid
|
||||
}
|
||||
@@ -5313,7 +5261,7 @@ public struct SkillsSkillCardParams: Codable, Sendable {
|
||||
public let skillkey: String
|
||||
|
||||
public init(
|
||||
agentid: String? = nil,
|
||||
agentid: String?,
|
||||
skillkey: String)
|
||||
{
|
||||
self.agentid = agentid
|
||||
@@ -5450,7 +5398,7 @@ public struct CronJob: Codable, Sendable {
|
||||
|
||||
public init(
|
||||
id: String,
|
||||
agentid: String? = nil,
|
||||
agentid: String?,
|
||||
sessionkey: String?,
|
||||
name: String,
|
||||
description: String?,
|
||||
@@ -5510,8 +5458,6 @@ public struct CronListParams: Codable, Sendable {
|
||||
public let offset: Int?
|
||||
public let query: String?
|
||||
public let enabled: AnyCodable?
|
||||
public let schedulekind: AnyCodable?
|
||||
public let lastrunstatus: AnyCodable?
|
||||
public let sortby: AnyCodable?
|
||||
public let sortdir: AnyCodable?
|
||||
public let agentid: String?
|
||||
@@ -5522,19 +5468,15 @@ public struct CronListParams: Codable, Sendable {
|
||||
offset: Int?,
|
||||
query: String?,
|
||||
enabled: AnyCodable?,
|
||||
schedulekind: AnyCodable?,
|
||||
lastrunstatus: AnyCodable?,
|
||||
sortby: AnyCodable?,
|
||||
sortdir: AnyCodable?,
|
||||
agentid: String? = nil)
|
||||
agentid: String?)
|
||||
{
|
||||
self.includedisabled = includedisabled
|
||||
self.limit = limit
|
||||
self.offset = offset
|
||||
self.query = query
|
||||
self.enabled = enabled
|
||||
self.schedulekind = schedulekind
|
||||
self.lastrunstatus = lastrunstatus
|
||||
self.sortby = sortby
|
||||
self.sortdir = sortdir
|
||||
self.agentid = agentid
|
||||
@@ -5546,8 +5488,6 @@ public struct CronListParams: Codable, Sendable {
|
||||
case offset
|
||||
case query
|
||||
case enabled
|
||||
case schedulekind = "scheduleKind"
|
||||
case lastrunstatus = "lastRunStatus"
|
||||
case sortby = "sortBy"
|
||||
case sortdir = "sortDir"
|
||||
case agentid = "agentId"
|
||||
@@ -5572,7 +5512,7 @@ public struct CronAddParams: Codable, Sendable {
|
||||
|
||||
public init(
|
||||
name: String,
|
||||
agentid: AnyCodable? = nil,
|
||||
agentid: AnyCodable?,
|
||||
sessionkey: AnyCodable?,
|
||||
description: String?,
|
||||
enabled: Bool?,
|
||||
@@ -5942,8 +5882,6 @@ public struct ExecApprovalRequestParams: Codable, Sendable {
|
||||
public let turnsourceto: AnyCodable?
|
||||
public let turnsourceaccountid: AnyCodable?
|
||||
public let turnsourcethreadid: AnyCodable?
|
||||
public let requiredeliveryroute: Bool?
|
||||
public let suppressdelivery: Bool?
|
||||
public let timeoutms: Int?
|
||||
public let twophase: Bool?
|
||||
|
||||
@@ -5960,15 +5898,13 @@ public struct ExecApprovalRequestParams: Codable, Sendable {
|
||||
ask: AnyCodable?,
|
||||
warningtext: AnyCodable?,
|
||||
commandspans: [[String: AnyCodable]]?,
|
||||
agentid: AnyCodable? = nil,
|
||||
agentid: AnyCodable?,
|
||||
resolvedpath: AnyCodable?,
|
||||
sessionkey: AnyCodable?,
|
||||
turnsourcechannel: AnyCodable?,
|
||||
turnsourceto: AnyCodable?,
|
||||
turnsourceaccountid: AnyCodable?,
|
||||
turnsourcethreadid: AnyCodable?,
|
||||
requiredeliveryroute: Bool? = nil,
|
||||
suppressdelivery: Bool? = nil,
|
||||
timeoutms: Int?,
|
||||
twophase: Bool?)
|
||||
{
|
||||
@@ -5991,8 +5927,6 @@ public struct ExecApprovalRequestParams: Codable, Sendable {
|
||||
self.turnsourceto = turnsourceto
|
||||
self.turnsourceaccountid = turnsourceaccountid
|
||||
self.turnsourcethreadid = turnsourcethreadid
|
||||
self.requiredeliveryroute = requiredeliveryroute
|
||||
self.suppressdelivery = suppressdelivery
|
||||
self.timeoutms = timeoutms
|
||||
self.twophase = twophase
|
||||
}
|
||||
@@ -6017,8 +5951,6 @@ public struct ExecApprovalRequestParams: Codable, Sendable {
|
||||
case turnsourceto = "turnSourceTo"
|
||||
case turnsourceaccountid = "turnSourceAccountId"
|
||||
case turnsourcethreadid = "turnSourceThreadId"
|
||||
case requiredeliveryroute = "requireDeliveryRoute"
|
||||
case suppressdelivery = "suppressDelivery"
|
||||
case timeoutms = "timeoutMs"
|
||||
case twophase = "twoPhase"
|
||||
}
|
||||
@@ -6067,7 +5999,7 @@ public struct PluginApprovalRequestParams: Codable, Sendable {
|
||||
toolname: String?,
|
||||
toolcallid: String?,
|
||||
alloweddecisions: [String]?,
|
||||
agentid: String? = nil,
|
||||
agentid: String?,
|
||||
sessionkey: String?,
|
||||
turnsourcechannel: String?,
|
||||
turnsourceto: String?,
|
||||
@@ -6528,25 +6460,21 @@ public struct DevicePairResolvedEvent: Codable, Sendable {
|
||||
|
||||
public struct ChatHistoryParams: Codable, Sendable {
|
||||
public let sessionkey: String
|
||||
public let agentid: String?
|
||||
public let limit: Int?
|
||||
public let maxchars: Int?
|
||||
|
||||
public init(
|
||||
sessionkey: String,
|
||||
agentid: String? = nil,
|
||||
limit: Int?,
|
||||
maxchars: Int?)
|
||||
{
|
||||
self.sessionkey = sessionkey
|
||||
self.agentid = agentid
|
||||
self.limit = limit
|
||||
self.maxchars = maxchars
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case sessionkey = "sessionKey"
|
||||
case agentid = "agentId"
|
||||
case limit
|
||||
case maxchars = "maxChars"
|
||||
}
|
||||
@@ -6554,7 +6482,6 @@ public struct ChatHistoryParams: Codable, Sendable {
|
||||
|
||||
public struct ChatSendParams: Codable, Sendable {
|
||||
public let sessionkey: String
|
||||
public let agentid: String?
|
||||
public let sessionid: String?
|
||||
public let message: String
|
||||
public let thinking: String?
|
||||
@@ -6572,7 +6499,6 @@ public struct ChatSendParams: Codable, Sendable {
|
||||
|
||||
public init(
|
||||
sessionkey: String,
|
||||
agentid: String? = nil,
|
||||
sessionid: String?,
|
||||
message: String,
|
||||
thinking: String?,
|
||||
@@ -6589,7 +6515,6 @@ public struct ChatSendParams: Codable, Sendable {
|
||||
idempotencykey: String)
|
||||
{
|
||||
self.sessionkey = sessionkey
|
||||
self.agentid = agentid
|
||||
self.sessionid = sessionid
|
||||
self.message = message
|
||||
self.thinking = thinking
|
||||
@@ -6608,7 +6533,6 @@ public struct ChatSendParams: Codable, Sendable {
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case sessionkey = "sessionKey"
|
||||
case agentid = "agentId"
|
||||
case sessionid = "sessionId"
|
||||
case message
|
||||
case thinking
|
||||
@@ -6628,47 +6552,39 @@ public struct ChatSendParams: Codable, Sendable {
|
||||
|
||||
public struct ChatAbortParams: Codable, Sendable {
|
||||
public let sessionkey: String
|
||||
public let agentid: String?
|
||||
public let runid: String?
|
||||
|
||||
public init(
|
||||
sessionkey: String,
|
||||
agentid: String? = nil,
|
||||
runid: String?)
|
||||
{
|
||||
self.sessionkey = sessionkey
|
||||
self.agentid = agentid
|
||||
self.runid = runid
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case sessionkey = "sessionKey"
|
||||
case agentid = "agentId"
|
||||
case runid = "runId"
|
||||
}
|
||||
}
|
||||
|
||||
public struct ChatInjectParams: Codable, Sendable {
|
||||
public let sessionkey: String
|
||||
public let agentid: String?
|
||||
public let message: String
|
||||
public let label: String?
|
||||
|
||||
public init(
|
||||
sessionkey: String,
|
||||
agentid: String? = nil,
|
||||
message: String,
|
||||
label: String?)
|
||||
{
|
||||
self.sessionkey = sessionkey
|
||||
self.agentid = agentid
|
||||
self.message = message
|
||||
self.label = label
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case sessionkey = "sessionKey"
|
||||
case agentid = "agentId"
|
||||
case message
|
||||
case label
|
||||
}
|
||||
@@ -6677,7 +6593,6 @@ public struct ChatInjectParams: Codable, Sendable {
|
||||
public struct ChatDeltaEvent: Codable, Sendable {
|
||||
public let runid: String
|
||||
public let sessionkey: String
|
||||
public let agentid: String?
|
||||
public let spawnedby: String?
|
||||
public let seq: Int
|
||||
public let state: String
|
||||
@@ -6689,7 +6604,6 @@ public struct ChatDeltaEvent: Codable, Sendable {
|
||||
public init(
|
||||
runid: String,
|
||||
sessionkey: String,
|
||||
agentid: String? = nil,
|
||||
spawnedby: String?,
|
||||
seq: Int,
|
||||
state: String,
|
||||
@@ -6700,7 +6614,6 @@ public struct ChatDeltaEvent: Codable, Sendable {
|
||||
{
|
||||
self.runid = runid
|
||||
self.sessionkey = sessionkey
|
||||
self.agentid = agentid
|
||||
self.spawnedby = spawnedby
|
||||
self.seq = seq
|
||||
self.state = state
|
||||
@@ -6713,7 +6626,6 @@ public struct ChatDeltaEvent: Codable, Sendable {
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case runid = "runId"
|
||||
case sessionkey = "sessionKey"
|
||||
case agentid = "agentId"
|
||||
case spawnedby = "spawnedBy"
|
||||
case seq
|
||||
case state
|
||||
@@ -6727,7 +6639,6 @@ public struct ChatDeltaEvent: Codable, Sendable {
|
||||
public struct ChatFinalEvent: Codable, Sendable {
|
||||
public let runid: String
|
||||
public let sessionkey: String
|
||||
public let agentid: String?
|
||||
public let spawnedby: String?
|
||||
public let seq: Int
|
||||
public let state: String
|
||||
@@ -6738,7 +6649,6 @@ public struct ChatFinalEvent: Codable, Sendable {
|
||||
public init(
|
||||
runid: String,
|
||||
sessionkey: String,
|
||||
agentid: String? = nil,
|
||||
spawnedby: String?,
|
||||
seq: Int,
|
||||
state: String,
|
||||
@@ -6748,7 +6658,6 @@ public struct ChatFinalEvent: Codable, Sendable {
|
||||
{
|
||||
self.runid = runid
|
||||
self.sessionkey = sessionkey
|
||||
self.agentid = agentid
|
||||
self.spawnedby = spawnedby
|
||||
self.seq = seq
|
||||
self.state = state
|
||||
@@ -6760,7 +6669,6 @@ public struct ChatFinalEvent: Codable, Sendable {
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case runid = "runId"
|
||||
case sessionkey = "sessionKey"
|
||||
case agentid = "agentId"
|
||||
case spawnedby = "spawnedBy"
|
||||
case seq
|
||||
case state
|
||||
@@ -6773,7 +6681,6 @@ public struct ChatFinalEvent: Codable, Sendable {
|
||||
public struct ChatAbortedEvent: Codable, Sendable {
|
||||
public let runid: String
|
||||
public let sessionkey: String
|
||||
public let agentid: String?
|
||||
public let spawnedby: String?
|
||||
public let seq: Int
|
||||
public let state: String
|
||||
@@ -6783,7 +6690,6 @@ public struct ChatAbortedEvent: Codable, Sendable {
|
||||
public init(
|
||||
runid: String,
|
||||
sessionkey: String,
|
||||
agentid: String? = nil,
|
||||
spawnedby: String?,
|
||||
seq: Int,
|
||||
state: String,
|
||||
@@ -6792,7 +6698,6 @@ public struct ChatAbortedEvent: Codable, Sendable {
|
||||
{
|
||||
self.runid = runid
|
||||
self.sessionkey = sessionkey
|
||||
self.agentid = agentid
|
||||
self.spawnedby = spawnedby
|
||||
self.seq = seq
|
||||
self.state = state
|
||||
@@ -6803,7 +6708,6 @@ public struct ChatAbortedEvent: Codable, Sendable {
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case runid = "runId"
|
||||
case sessionkey = "sessionKey"
|
||||
case agentid = "agentId"
|
||||
case spawnedby = "spawnedBy"
|
||||
case seq
|
||||
case state
|
||||
@@ -6815,7 +6719,6 @@ public struct ChatAbortedEvent: Codable, Sendable {
|
||||
public struct ChatErrorEvent: Codable, Sendable {
|
||||
public let runid: String
|
||||
public let sessionkey: String
|
||||
public let agentid: String?
|
||||
public let spawnedby: String?
|
||||
public let seq: Int
|
||||
public let state: String
|
||||
@@ -6828,7 +6731,6 @@ public struct ChatErrorEvent: Codable, Sendable {
|
||||
public init(
|
||||
runid: String,
|
||||
sessionkey: String,
|
||||
agentid: String? = nil,
|
||||
spawnedby: String?,
|
||||
seq: Int,
|
||||
state: String,
|
||||
@@ -6840,7 +6742,6 @@ public struct ChatErrorEvent: Codable, Sendable {
|
||||
{
|
||||
self.runid = runid
|
||||
self.sessionkey = sessionkey
|
||||
self.agentid = agentid
|
||||
self.spawnedby = spawnedby
|
||||
self.seq = seq
|
||||
self.state = state
|
||||
@@ -6854,7 +6755,6 @@ public struct ChatErrorEvent: Codable, Sendable {
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case runid = "runId"
|
||||
case sessionkey = "sessionKey"
|
||||
case agentid = "agentId"
|
||||
case spawnedby = "spawnedBy"
|
||||
case seq
|
||||
case state
|
||||
|
||||
@@ -11,42 +11,6 @@ private extension NSLock {
|
||||
}
|
||||
}
|
||||
|
||||
private final class DoubleCallbackPingWebSocketTask: WebSocketTasking, @unchecked Sendable {
|
||||
private let callbacks: [Error?]
|
||||
|
||||
init(callbacks: [Error?]) {
|
||||
self.callbacks = callbacks
|
||||
}
|
||||
|
||||
var state: URLSessionTask.State { .running }
|
||||
|
||||
func resume() {}
|
||||
|
||||
func cancel(with closeCode: URLSessionWebSocketTask.CloseCode, reason: Data?) {
|
||||
_ = (closeCode, reason)
|
||||
}
|
||||
|
||||
func send(_ message: URLSessionWebSocketTask.Message) async throws {
|
||||
_ = message
|
||||
}
|
||||
|
||||
func sendPing(pongReceiveHandler: @escaping @Sendable (Error?) -> Void) {
|
||||
for callback in self.callbacks {
|
||||
pongReceiveHandler(callback)
|
||||
}
|
||||
}
|
||||
|
||||
func receive() async throws -> URLSessionWebSocketTask.Message {
|
||||
throw URLError(.badServerResponse)
|
||||
}
|
||||
|
||||
func receive(
|
||||
completionHandler: @escaping @Sendable (Result<URLSessionWebSocketTask.Message, Error>) -> Void)
|
||||
{
|
||||
completionHandler(.failure(URLError(.badServerResponse)))
|
||||
}
|
||||
}
|
||||
|
||||
private final class FakeGatewayWebSocketTask: WebSocketTasking, @unchecked Sendable {
|
||||
private let lock = NSLock()
|
||||
private let helloAuth: [String: Any]?
|
||||
@@ -229,25 +193,6 @@ private actor SeqGapProbe {
|
||||
|
||||
@Suite(.serialized)
|
||||
struct GatewayNodeSessionTests {
|
||||
@Test
|
||||
func websocketPingIgnoresDuplicateSuccessCallbacks() async throws {
|
||||
let task = DoubleCallbackPingWebSocketTask(callbacks: [nil, nil])
|
||||
try await WebSocketTaskBox(task: task).sendPing()
|
||||
}
|
||||
|
||||
@Test
|
||||
func websocketPingIgnoresDuplicateCallbacksAfterFirstError() async throws {
|
||||
let firstError = URLError(.networkConnectionLost)
|
||||
let task = DoubleCallbackPingWebSocketTask(callbacks: [firstError, nil])
|
||||
|
||||
do {
|
||||
try await WebSocketTaskBox(task: task).sendPing()
|
||||
Issue.record("sendPing unexpectedly succeeded")
|
||||
} catch let error as URLError {
|
||||
#expect(error.code == firstError.code)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
func scannedSetupCodePrefersBootstrapAuthOverStoredDeviceToken() async throws {
|
||||
let tempDir = FileManager.default.temporaryDirectory
|
||||
|
||||
1
changelog/fragments/pr-signal-container-mode.md
Normal file
1
changelog/fragments/pr-signal-container-mode.md
Normal file
@@ -0,0 +1 @@
|
||||
- Signal/container mode: add REST API support for bbernhard/signal-cli-rest-api containerized deployments via a unified adapter layer, with automatic mode detection and `channels.signal.apiMode` config. (#10240) Thanks @Hua688.
|
||||
@@ -27,7 +27,7 @@ const bundledPluginEntries = [
|
||||
"setup-entry.ts!",
|
||||
"{api,contract-api,helper-api,runtime-api,light-runtime-api,update-offset-runtime-api,channel-plugin-api,provider-plugin-api,setup-api}.ts!",
|
||||
"subagent-hooks-api.ts!",
|
||||
"src/{api,runtime-api,light-runtime-api,update-offset-runtime-api,channel-plugin-api,provider-plugin-api,doctor-contract,setup-surface,mcp-serve}.ts!",
|
||||
"src/{api,runtime-api,light-runtime-api,update-offset-runtime-api,channel-plugin-api,provider-plugin-api,doctor-contract,setup-surface}.ts!",
|
||||
"src/subagent-hooks-api.ts!",
|
||||
] as const;
|
||||
|
||||
@@ -168,31 +168,6 @@ const config = {
|
||||
entry: ["src/index.ts!", "src/*.ts!", "src/harness/**/*.ts!"],
|
||||
project: ["src/**/*.ts!"],
|
||||
},
|
||||
"packages/gateway-client": {
|
||||
entry: ["src/index.ts!"],
|
||||
project: ["src/**/*.ts!"],
|
||||
},
|
||||
"packages/gateway-protocol": {
|
||||
entry: ["src/index.ts!", "src/schema.ts!"],
|
||||
project: ["src/**/*.ts!"],
|
||||
},
|
||||
"packages/net-policy": {
|
||||
entry: ["src/index.ts!", "src/ip.ts!"],
|
||||
project: ["src/**/*.ts!"],
|
||||
},
|
||||
"packages/markdown-core": {
|
||||
entry: ["src/*.ts!"],
|
||||
project: ["src/**/*.ts!"],
|
||||
},
|
||||
"packages/terminal-core": {
|
||||
entry: ["src/*.ts!"],
|
||||
project: ["src/**/*.ts!"],
|
||||
},
|
||||
"packages/speech-core": {
|
||||
entry: ["api.ts!", "runtime-api.ts!", "speaker.ts!", "voice-models.ts!"],
|
||||
project: ["**/*.ts!"],
|
||||
ignoreDependencies: ["openclaw"],
|
||||
},
|
||||
"packages/*": {
|
||||
entry: ["index.js!", "scripts/postinstall.js!"],
|
||||
project: ["index.js!", "scripts/**/*.js!"],
|
||||
|
||||
@@ -63,7 +63,6 @@ services:
|
||||
ports:
|
||||
- "${OPENCLAW_GATEWAY_PORT:-18789}:18789"
|
||||
- "${OPENCLAW_BRIDGE_PORT:-18790}:18790"
|
||||
- "${OPENCLAW_MSTEAMS_PORT:-3978}:3978"
|
||||
init: true
|
||||
restart: unless-stopped
|
||||
command:
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
289c1bae4b9574d219fe61931be6b3ce42d4efb37d0a2edc570a521016394db5 config-baseline.json
|
||||
5bcb22d1506d82e59caa3bbc97931213299e3a2c0d45dbc549386b254661094a config-baseline.core.json
|
||||
a9102c0611b8170fac37853cc31771810f31757a9e3b2c6796bbd9625f9b9206 config-baseline.channel.json
|
||||
0a8e088f8dc7b12341075ce019281d5fe45827ae802f60c71a490022ba5867cf config-baseline.plugin.json
|
||||
a69acd971a7d54d3086f26c52fde4084eaeef350f71b918fb8e7338f329bff95 config-baseline.json
|
||||
ee4c0f0fb15cda02268f2e83d0c5e1c8d0ec0a2c1b2fdb89cdfce308dadb2b8b config-baseline.core.json
|
||||
b901fb766edfd9df630690281476fc4032c64772f69d1d8f7b2e0e913a90f229 config-baseline.channel.json
|
||||
1b763a5524aca2d7ecf1eea38f845ad1ffed5c1b37e85e62f6a7902a3ee0f920 config-baseline.plugin.json
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
cf29066e9465cb5ac1387d1d482d0939b9176220ecc69964da9af1a471939269 plugin-sdk-api-baseline.json
|
||||
ab43993cf713a96b191c55cf89bb215c18ecdc2d8edf50f31369ce3b162c56e3 plugin-sdk-api-baseline.jsonl
|
||||
7039b60f2cea732a90db633328952faaddd919f0d098b303b29d554e64184073 plugin-sdk-api-baseline.json
|
||||
1a78f4df81562af070c5379c6369a8bea9c704f985b5382a463364757b26db0d plugin-sdk-api-baseline.jsonl
|
||||
|
||||
@@ -175,26 +175,6 @@
|
||||
"source": "Agent harness plugins",
|
||||
"target": "Agent harness plugins"
|
||||
},
|
||||
{
|
||||
"source": "Agent harness plugins (SDK reference)",
|
||||
"target": "Agent harness plugins (SDK reference)"
|
||||
},
|
||||
{
|
||||
"source": "Copilot SDK harness",
|
||||
"target": "Copilot SDK harness"
|
||||
},
|
||||
{
|
||||
"source": "Copilot plugin",
|
||||
"target": "Copilot plugin"
|
||||
},
|
||||
{
|
||||
"source": "GitHub Copilot agent runtime",
|
||||
"target": "GitHub Copilot agent runtime"
|
||||
},
|
||||
{
|
||||
"source": "copilot",
|
||||
"target": "copilot"
|
||||
},
|
||||
{
|
||||
"source": "Agent loop",
|
||||
"target": "Agent loop"
|
||||
@@ -1103,18 +1083,6 @@
|
||||
"source": "Plugin Manifest",
|
||||
"target": "Plugin Manifest"
|
||||
},
|
||||
{
|
||||
"source": "Workboard plugin",
|
||||
"target": "Workboard 插件"
|
||||
},
|
||||
{
|
||||
"source": "workboard",
|
||||
"target": "workboard"
|
||||
},
|
||||
{
|
||||
"source": "Control UI",
|
||||
"target": "Control UI"
|
||||
},
|
||||
{
|
||||
"source": "Z.AI (GLM)",
|
||||
"target": "Z.AI (GLM)"
|
||||
|
||||
@@ -15,8 +15,9 @@ Cron is the Gateway's built-in scheduler. It persists jobs, wakes the agent at t
|
||||
<Steps>
|
||||
<Step title="Add a one-shot reminder">
|
||||
```bash
|
||||
openclaw cron create "2026-02-01T16:00:00Z" \
|
||||
openclaw cron add \
|
||||
--name "Reminder" \
|
||||
--at "2026-02-01T16:00:00Z" \
|
||||
--session main \
|
||||
--system-event "Reminder: check the cron docs draft" \
|
||||
--wake now \
|
||||
@@ -42,7 +43,6 @@ Cron is the Gateway's built-in scheduler. It persists jobs, wakes the agent at t
|
||||
- Cron runs **inside the Gateway** process (not inside the model).
|
||||
- Job definitions persist at `~/.openclaw/cron/jobs.json` so restarts do not lose schedules.
|
||||
- Runtime execution state persists next to it in `~/.openclaw/cron/jobs-state.json`. If you track cron definitions in git, track `jobs.json` and gitignore `jobs-state.json`.
|
||||
- If `jobs.json` contains malformed rows, the Gateway keeps valid jobs running, removes the malformed rows from the active store, and saves the raw rows beside it in `jobs-quarantine.json` for later repair or review.
|
||||
- After the split, older OpenClaw versions can read `jobs.json` but may treat jobs as fresh because runtime fields now live in `jobs-state.json`.
|
||||
- When `jobs.json` is edited while the Gateway is running or stopped, OpenClaw compares the changed schedule fields with pending runtime slot metadata and clears stale `nextRunAtMs` values. Pure formatting or key-order-only rewrites preserve the pending slot.
|
||||
- All cron executions create [background task](/automation/tasks) records.
|
||||
@@ -146,8 +146,6 @@ This fires ~5–6 times per month instead of 0–1 times per month. OpenClaw use
|
||||
|
||||
Cron jobs can also carry payload-level `fallbacks`. When present, that list replaces the configured fallback chain for the job. Use `fallbacks: []` in the job payload/API when you want a strict cron run that tries only the selected model. If a job has `--model` but neither payload nor configured fallbacks, OpenClaw passes an explicit empty fallback override so the agent primary is not appended as a hidden extra retry target.
|
||||
|
||||
Local-provider preflight checks walk configured fallbacks before marking a cron run `skipped`; `fallbacks: []` keeps that preflight path strict.
|
||||
|
||||
Model-selection precedence for isolated jobs is:
|
||||
|
||||
1. Gmail hook model override (when the run came from Gmail and that override is allowed)
|
||||
@@ -217,11 +215,12 @@ Failure notifications follow a separate destination path:
|
||||
</Tab>
|
||||
<Tab title="Recurring isolated job">
|
||||
```bash
|
||||
openclaw cron create "0 7 * * *" \
|
||||
"Summarize overnight updates." \
|
||||
openclaw cron add \
|
||||
--name "Morning brief" \
|
||||
--cron "0 7 * * *" \
|
||||
--tz "America/Los_Angeles" \
|
||||
--session isolated \
|
||||
--message "Summarize overnight updates." \
|
||||
--announce \
|
||||
--channel slack \
|
||||
--to "channel:C1234567890"
|
||||
@@ -240,14 +239,6 @@ Failure notifications follow a separate destination path:
|
||||
--announce
|
||||
```
|
||||
</Tab>
|
||||
<Tab title="Webhook output">
|
||||
```bash
|
||||
openclaw cron create "0 18 * * 1-5" \
|
||||
"Summarize today's deploys as JSON." \
|
||||
--name "Deploy digest" \
|
||||
--webhook "https://example.invalid/openclaw/cron"
|
||||
```
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
## Webhooks
|
||||
@@ -420,14 +411,12 @@ openclaw cron runs --id <jobId> --run-id <runId>
|
||||
openclaw cron remove <jobId>
|
||||
|
||||
# Agent selection (multi-agent setups)
|
||||
openclaw cron create "0 6 * * *" "Check ops queue" --name "Ops sweep" --session isolated --agent ops
|
||||
openclaw cron add --name "Ops sweep" --cron "0 6 * * *" --session isolated --message "Check ops queue" --agent ops
|
||||
openclaw cron edit <jobId> --clear-agent
|
||||
```
|
||||
|
||||
`openclaw cron run <jobId>` returns after enqueueing the manual run. Use `--wait` for shutdown hooks, maintenance scripts, or other automation that must block until the queued run finishes. Wait mode polls the exact returned `runId`; it exits `0` for status `ok` and non-zero for `error`, `skipped`, or a wait timeout.
|
||||
|
||||
`openclaw cron create` is an alias for `openclaw cron add`, and new jobs can use a positional schedule (`"0 9 * * 1"`, `"every 1h"`, `"20m"`, or an ISO timestamp) followed by a positional agent prompt. Use `--webhook <url>` on `cron add|create` or `cron edit` to POST the finished run payload to an HTTP endpoint. Webhook delivery cannot be combined with chat delivery flags such as `--announce`, `--channel`, `--to`, `--thread-id`, or `--account`.
|
||||
|
||||
<Note>
|
||||
Model override note:
|
||||
|
||||
|
||||
@@ -102,7 +102,7 @@ Not every agent run creates a task. Heartbeat turns and normal interactive chat
|
||||
<Accordion title="Notify defaults for cron and media">
|
||||
Main-session cron tasks use `silent` notify policy by default - they create records for tracking but do not generate notifications. Isolated cron tasks also default to `silent` but are more visible because they run in their own session.
|
||||
|
||||
Session-backed `image_generate`, `music_generate`, and `video_generate` runs also use `silent` notify policy. They still create task records, but completion is handed back to the original agent session as an internal wake so the agent can write the follow-up message and attach the finished media itself. The requester agent follows its normal visible-reply contract: automatic final reply when configured, or `message(action="send")` plus `NO_REPLY` when the session requires message-tool replies. If the requester session is no longer active or its active wake fails, and the completion agent misses some or all generated media, OpenClaw sends an idempotent direct fallback with only the missing media to the original channel target.
|
||||
Session-backed `image_generate`, `music_generate`, and `video_generate` runs also use `silent` notify policy. They still create task records, but completion is handed back to the original agent session as an internal wake so the agent can write the follow-up message and attach the finished media itself. Generated-media completion events require message-tool delivery: the agent must send the finished media with the `message` tool, then reply `NO_REPLY`. If the requester session is no longer active or its active wake fails, and the completion agent misses some or all generated media, OpenClaw sends an idempotent direct fallback with only the missing media to the original channel target.
|
||||
|
||||
</Accordion>
|
||||
<Accordion title="Concurrent media-generation guardrail">
|
||||
|
||||
@@ -354,7 +354,7 @@ To restrict who can click a button, set `allowedUsers` on that button (Discord u
|
||||
|
||||
Component callbacks expire after 30 minutes by default. Set `channels.discord.agentComponents.ttlMs` to change that callback registry lifetime for the default Discord account, or `channels.discord.accounts.<accountId>.agentComponents.ttlMs` to override one account in a multi-account setup. The value is milliseconds, must be a positive integer, and is capped at `86400000` (24 hours). Longer TTLs are useful for review or approval workflows that need buttons to remain usable, but they also extend the window where an old Discord message can still trigger an action. Prefer the shortest TTL that fits the workflow, and keep the default when stale callbacks would be surprising.
|
||||
|
||||
The `/model` and `/models` slash commands open an interactive model picker with provider, model, and compatible runtime dropdowns plus a Submit step. `/models add` is deprecated and now returns a deprecation message instead of registering models from chat. The picker reply is ephemeral and only the invoking user can use it. Discord select menus are limited to 25 options, so add `provider/*` entries to `agents.defaults.models` when you want the picker to show dynamically discovered models only for selected providers such as `openai` or `vllm`.
|
||||
The `/model` and `/models` slash commands open an interactive model picker with provider, model, and compatible runtime dropdowns plus a Submit step. `/models add` is deprecated and now returns a deprecation message instead of registering models from chat. The picker reply is ephemeral and only the invoking user can use it. Discord select menus are limited to 25 options, so add `provider/*` entries to `agents.defaults.models` when you want the picker to show dynamically discovered models only for selected providers such as `openai-codex` or `vllm`.
|
||||
|
||||
File attachments:
|
||||
|
||||
@@ -696,7 +696,6 @@ Default slash command settings:
|
||||
maxLines: 8,
|
||||
maxLineChars: 120,
|
||||
toolProgress: true,
|
||||
commentary: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -709,7 +708,6 @@ Default slash command settings:
|
||||
- Media, error, and explicit-reply finals cancel pending preview edits.
|
||||
- `streaming.preview.toolProgress` (default `true`) controls whether tool/progress updates reuse the preview message.
|
||||
- Tool/progress rows render as compact emoji + title + detail when available, for example `🛠️ Bash: run tests` or `🔎 Web Search: for "query"`.
|
||||
- `streaming.progress.commentary` (default `false`) opts into assistant commentary/preamble text in the temporary progress draft. Commentary is cleaned before display, stays transient, and does not change final answer delivery.
|
||||
- `streaming.progress.maxLineChars` controls the per-line progress preview budget. Prose is shortened on word boundaries; command and path details keep useful suffixes.
|
||||
- `streaming.preview.commandText` / `streaming.progress.commandText` controls command/exec detail in compact progress lines: `raw` (default) or `status` (tool label only).
|
||||
|
||||
@@ -1197,7 +1195,7 @@ Auto-join example:
|
||||
discord: {
|
||||
voice: {
|
||||
enabled: true,
|
||||
model: "openai/gpt-5.5",
|
||||
model: "openai-codex/gpt-5.5",
|
||||
autoJoin: [
|
||||
{
|
||||
guildId: "123456789012345678",
|
||||
@@ -1217,7 +1215,7 @@ Auto-join example:
|
||||
realtime: {
|
||||
provider: "openai",
|
||||
model: "gpt-realtime-2",
|
||||
speakerVoice: "cedar",
|
||||
voice: "cedar",
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -1227,20 +1225,20 @@ Auto-join example:
|
||||
|
||||
Notes:
|
||||
|
||||
- `voice.tts` overrides `messages.tts` for `stt-tts` voice playback only. Realtime modes use `voice.realtime.speakerVoice`.
|
||||
- `voice.tts` overrides `messages.tts` for `stt-tts` voice playback only. Realtime modes use `voice.realtime.voice`.
|
||||
- `voice.mode` controls the conversation path. The default is `agent-proxy`: a realtime voice front end handles turn timing, interruption, and playback, delegates substantive work to the routed OpenClaw agent through `openclaw_agent_consult`, and treats the result like a typed Discord prompt from that speaker. `stt-tts` keeps the older batch STT plus TTS flow. `bidi` lets the realtime model converse directly while exposing `openclaw_agent_consult` for the OpenClaw brain.
|
||||
- `voice.agentSession` controls which OpenClaw conversation receives voice turns. Leave it unset for the voice channel's own session, or set `{ mode: "target", target: "channel:<text-channel-id>" }` to make the voice channel act as the microphone/speaker extension of an existing Discord text channel session such as `#maintainers`.
|
||||
- `voice.model` overrides the OpenClaw agent brain for Discord voice responses and realtime consults. Leave it unset to inherit the routed agent model. It is separate from `voice.realtime.model`.
|
||||
- `voice.followUsers` lets the bot join, move, and leave Discord voice with selected users. See [Follow users in voice](#follow-users-in-voice) for behavior rules and examples.
|
||||
- `agent-proxy` routes speech through `discord-voice`, which preserves normal owner/tool authorization for the speaker and target session but hides the agent `tts` tool because Discord voice owns playback. By default, `agent-proxy` gives the consult full owner-equivalent tool access for owner speakers (`voice.realtime.toolPolicy: "owner"`) and strongly prefers consulting the OpenClaw agent before substantive answers (`voice.realtime.consultPolicy: "always"`). In that default `always` mode, the realtime layer does not auto-speak filler before the consult answer; it captures and transcribes speech, then speaks the routed OpenClaw answer. If multiple forced consult answers finish while Discord is still playing the first answer, later exact-speech answers are queued until playback idles instead of replacing speech mid-sentence.
|
||||
- In `stt-tts` mode, STT uses `tools.media.audio`; `voice.model` does not affect transcription.
|
||||
- In realtime modes, `voice.realtime.provider`, `voice.realtime.model`, and `voice.realtime.speakerVoice` configure the realtime audio session. For OpenAI Realtime 2 plus the Codex brain, use `voice.realtime.model: "gpt-realtime-2"` and `voice.model: "openai/gpt-5.5"`.
|
||||
- In realtime modes, `voice.realtime.provider`, `voice.realtime.model`, and `voice.realtime.voice` configure the realtime audio session. For OpenAI Realtime 2 plus the Codex brain, use `voice.realtime.model: "gpt-realtime-2"` and `voice.model: "openai-codex/gpt-5.5"`.
|
||||
- Realtime voice modes include small `IDENTITY.md`, `USER.md`, and `SOUL.md` profile files in the realtime provider instructions by default so fast direct turns keep the same identity, user grounding, and persona as the routed OpenClaw agent. Set `voice.realtime.bootstrapContextFiles` to a subset to customize this, or `[]` to disable it. The supported realtime bootstrap files are limited to those profile files; `AGENTS.md` stays in the normal agent context. The injected profile context does not replace `openclaw_agent_consult` for workspace work, current facts, memory lookup, or tool-backed actions.
|
||||
- In OpenAI `agent-proxy` realtime mode, set `voice.realtime.requireWakeName: true` to keep Discord realtime voice silent until a transcript starts or ends with a wake name. Configured wake names must be one or two words. If `voice.realtime.wakeNames` is unset, OpenClaw uses the routed agent `name` plus `OpenClaw`, falling back to the agent id plus `OpenClaw`. Wake-name gating disables realtime provider auto-response, routes accepted turns through the OpenClaw agent consult path, and gives a short spoken acknowledgement when a leading wake name is recognized from partial transcription before the final transcript arrives.
|
||||
- The OpenAI realtime provider accepts current Realtime 2 event names and legacy Codex-compatible aliases for output audio and transcript events, so compatible provider snapshots can drift without dropping assistant audio.
|
||||
- `voice.realtime.bargeIn` controls whether Discord speaker-start events interrupt active realtime playback. If unset, it follows the realtime provider's input-audio interruption setting.
|
||||
- `voice.realtime.minBargeInAudioEndMs` controls the minimum assistant playback duration before an OpenAI realtime barge-in truncates audio. Default: `250`. Set `0` for immediate interruption in low-echo rooms, or raise it for echo-heavy speaker setups.
|
||||
- For an OpenAI voice on Discord playback, set `voice.tts.provider: "openai"` and choose a Text-to-speech voice under `voice.tts.providers.openai.speakerVoice`. `cedar` is a good masculine-sounding choice on the current OpenAI TTS model.
|
||||
- For an OpenAI voice on Discord playback, set `voice.tts.provider: "openai"` and choose a Text-to-speech voice under `voice.tts.openai.voice` or `voice.tts.providers.openai.voice`. `cedar` is a good masculine-sounding choice on the current OpenAI TTS model.
|
||||
- Per-channel Discord `systemPrompt` overrides apply to voice transcript turns for that voice channel.
|
||||
- Voice transcript turns derive owner status from Discord `allowFrom` (or `dm.allowFrom`) for owner-gated commands and channel actions. Agent tool visibility follows the configured tool policy for the routed session.
|
||||
- Discord voice is opt-in for text-only configs; set `channels.discord.voice.enabled=true` (or keep an existing `channels.discord.voice` block) to enable `/vc` commands, the voice runtime, and the `GuildVoiceStates` gateway intent.
|
||||
@@ -1325,13 +1323,13 @@ Default agent-proxy voice-channel session example:
|
||||
discord: {
|
||||
voice: {
|
||||
enabled: true,
|
||||
model: "openai/gpt-5.5",
|
||||
model: "openai-codex/gpt-5.5",
|
||||
followUsersEnabled: true,
|
||||
followUsers: ["123456789012345678"],
|
||||
realtime: {
|
||||
provider: "openai",
|
||||
model: "gpt-realtime-2",
|
||||
speakerVoice: "cedar",
|
||||
voice: "cedar",
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -1353,11 +1351,9 @@ Legacy STT plus TTS example:
|
||||
model: "openai/gpt-5.4-mini",
|
||||
tts: {
|
||||
provider: "openai",
|
||||
providers: {
|
||||
openai: {
|
||||
model: "gpt-4o-mini-tts",
|
||||
speakerVoice: "cedar",
|
||||
},
|
||||
openai: {
|
||||
model: "gpt-4o-mini-tts",
|
||||
voice: "cedar",
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -1375,11 +1371,11 @@ Realtime bidi example:
|
||||
voice: {
|
||||
enabled: true,
|
||||
mode: "bidi",
|
||||
model: "openai/gpt-5.5",
|
||||
model: "openai-codex/gpt-5.5",
|
||||
realtime: {
|
||||
provider: "openai",
|
||||
model: "gpt-realtime-2",
|
||||
speakerVoice: "cedar",
|
||||
voice: "cedar",
|
||||
toolPolicy: "safe-read-only",
|
||||
consultPolicy: "always",
|
||||
},
|
||||
@@ -1398,7 +1394,7 @@ Voice as an extension of an existing Discord channel session:
|
||||
voice: {
|
||||
enabled: true,
|
||||
mode: "agent-proxy",
|
||||
model: "openai/gpt-5.5",
|
||||
model: "openai-codex/gpt-5.5",
|
||||
agentSession: {
|
||||
mode: "target",
|
||||
target: "channel:123456789012345678",
|
||||
@@ -1406,7 +1402,7 @@ Voice as an extension of an existing Discord channel session:
|
||||
realtime: {
|
||||
provider: "openai",
|
||||
model: "gpt-realtime-2",
|
||||
speakerVoice: "cedar",
|
||||
voice: "cedar",
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -1433,11 +1429,11 @@ Echo-heavy OpenAI Realtime example:
|
||||
voice: {
|
||||
enabled: true,
|
||||
mode: "bidi",
|
||||
model: "openai/gpt-5.5",
|
||||
model: "openai-codex/gpt-5.5",
|
||||
realtime: {
|
||||
provider: "openai",
|
||||
model: "gpt-realtime-2",
|
||||
speakerVoice: "cedar",
|
||||
voice: "cedar",
|
||||
bargeIn: true,
|
||||
minBargeInAudioEndMs: 500,
|
||||
consultPolicy: "always",
|
||||
|
||||
@@ -15,7 +15,7 @@ Feishu/Lark is an all-in-one collaboration platform where teams chat, share docu
|
||||
## Quick start
|
||||
|
||||
<Note>
|
||||
Requires OpenClaw 2026.5.29 or above. Run `openclaw --version` to check. Upgrade with `openclaw update`.
|
||||
Requires OpenClaw 2026.4.25 or above. Run `openclaw --version` to check. Upgrade with `openclaw update`.
|
||||
</Note>
|
||||
|
||||
<Steps>
|
||||
|
||||
@@ -666,58 +666,6 @@ Teams delivers messages via HTTP webhook. If processing takes too long (e.g., sl
|
||||
|
||||
OpenClaw handles this by returning quickly and sending replies proactively, but very slow responses may still cause issues.
|
||||
|
||||
### Teams cloud and service URL support
|
||||
|
||||
This SDK-backed Teams path is live-validated for Microsoft Teams public cloud.
|
||||
|
||||
Inbound replies use the incoming Teams SDK turn context. Out-of-context proactive operations - sends, edits, deletes, cards, polls, file-consent messages, and queued long-running replies - use the stored conversation reference `serviceUrl`. Public cloud defaults to the Teams SDK public cloud environment and allows stored references on the public Teams Connector host: `https://smba.trafficmanager.net/`.
|
||||
|
||||
Public cloud is the default. You do not need to set `channels.msteams.cloud` or `channels.msteams.serviceUrl` for normal public-cloud bots.
|
||||
|
||||
For non-public Teams clouds, set `cloud` and the matching proactive boundary when Microsoft publishes one:
|
||||
|
||||
- `channels.msteams.cloud` selects the Teams SDK cloud preset for authentication, JWT validation, token services, and Graph scope.
|
||||
- `channels.msteams.serviceUrl` selects the Bot Connector endpoint boundary used to validate stored conversation references before proactive sends, edits, deletes, cards, polls, file-consent messages, and queued long-running replies. It is required for USGov and DoD SDK clouds. For China/21Vianet, OpenClaw uses the SDK `China` preset and accepts stored/configured service URLs only on Azure China Bot Framework channel hosts.
|
||||
|
||||
Microsoft publishes the global proactive Bot Connector endpoints in the [Create the conversation](https://learn.microsoft.com/en-us/microsoftteams/platform/bots/how-to/conversations/send-proactive-messages?tabs=dotnet#create-the-conversation) section of the Teams proactive messaging docs. Use the incoming activity's `serviceUrl` when available; if you need a global proactive endpoint, use Microsoft's table.
|
||||
|
||||
| Teams environment | OpenClaw config | Proactive `serviceUrl` |
|
||||
| ----------------- | ----------------------------------------------------------- | -------------------------------------------------- |
|
||||
| Public | no cloud/serviceUrl config needed | `https://smba.trafficmanager.net/teams` |
|
||||
| GCC | set `serviceUrl`; no separate Teams SDK cloud preset exists | `https://smba.infra.gcc.teams.microsoft.com/teams` |
|
||||
| GCC High | `cloud: "USGov"` + `serviceUrl` | `https://smba.infra.gov.teams.microsoft.us/teams` |
|
||||
| DoD | `cloud: "USGovDoD"` + `serviceUrl` | `https://smba.infra.dod.teams.microsoft.us/teams` |
|
||||
| China/21Vianet | `cloud: "China"` | use the incoming activity's `serviceUrl` |
|
||||
|
||||
Example for GCC, where Microsoft documents a separate proactive service URL but the Teams SDK does not expose a separate GCC cloud preset:
|
||||
|
||||
```json
|
||||
{
|
||||
"channels": {
|
||||
"msteams": {
|
||||
"serviceUrl": "https://smba.infra.gcc.teams.microsoft.com/teams"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Example for GCC High:
|
||||
|
||||
```json
|
||||
{
|
||||
"channels": {
|
||||
"msteams": {
|
||||
"cloud": "USGov",
|
||||
"serviceUrl": "https://smba.infra.gov.teams.microsoft.us/teams"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`channels.msteams.serviceUrl` is restricted to supported Microsoft Teams Bot Connector hosts. When a service URL is configured, OpenClaw checks that the stored conversation `serviceUrl` uses the same host before proactive sends, edits, deletes, cards, polls, or queued long-running replies run. With the default public-cloud config, OpenClaw fails closed if a stored conversation points outside the public Teams Connector host. Receive a fresh message from the conversation after changing cloud/service URL settings so the stored conversation reference is current.
|
||||
|
||||
China/21Vianet does not have a separate global proactive `smba` URL in Microsoft's Teams proactive endpoint table. Configure `cloud: "China"` so the Teams SDK uses Azure China auth, token, and JWT endpoints. Proactive sends then require a stored conversation reference from an incoming China Teams activity, or an explicitly configured service URL, on the Azure China Bot Framework channel boundary (`*.botframework.azure.cn`). Graph-backed Teams helpers are currently disabled for `cloud: "China"` until OpenClaw routes Graph requests through the Azure China Graph endpoint.
|
||||
|
||||
### Formatting
|
||||
|
||||
Teams markdown is more limited than Slack or Discord:
|
||||
@@ -732,8 +680,6 @@ Key settings (see `/gateway/configuration` for shared channel patterns):
|
||||
|
||||
- `channels.msteams.enabled`: enable/disable the channel.
|
||||
- `channels.msteams.appId`, `channels.msteams.appPassword`, `channels.msteams.tenantId`: bot credentials.
|
||||
- `channels.msteams.cloud`: Teams SDK cloud environment (`Public`, `USGov`, `USGovDoD`, or `China`; default `Public`). Set this with `serviceUrl` for USGov/DoD SDK clouds; China uses the SDK preset and stored Azure China Bot Framework conversation references, with Graph-backed helpers disabled until Azure China Graph routing is implemented.
|
||||
- `channels.msteams.serviceUrl`: Bot Connector service URL boundary for SDK proactive operations. Public cloud uses the SDK default; set this for GCC (`https://smba.infra.gcc.teams.microsoft.com/teams`), GCC High, or DoD. China accepts Azure China Bot Framework channel hosts when the stored conversation reference comes from Teams operated by 21Vianet.
|
||||
- `channels.msteams.webhook.port` (default `3978`)
|
||||
- `channels.msteams.webhook.path` (default `/api/messages`)
|
||||
- `channels.msteams.dmPolicy`: `pairing | allowlist | open | disabled` (default: pairing)
|
||||
|
||||
@@ -411,9 +411,9 @@ curl "https://api.telegram.org/bot<bot_token>/getUpdates"
|
||||
|
||||
Preview streaming is separate from block streaming. When block streaming is explicitly enabled for Telegram, OpenClaw skips the preview stream to avoid double-streaming.
|
||||
|
||||
Reasoning stream behavior:
|
||||
Telegram-only reasoning stream:
|
||||
|
||||
- `/reasoning stream` uses a supported channel's reasoning-preview path; on Telegram, it streams reasoning into the live preview while generating
|
||||
- `/reasoning stream` sends reasoning to the live preview while generating
|
||||
- the reasoning preview is deleted after final delivery; use `/reasoning on` when reasoning should remain visible
|
||||
- final answer is sent without reasoning text
|
||||
|
||||
|
||||
24
docs/ci.md
24
docs/ci.md
@@ -43,9 +43,7 @@ OpenClaw CI runs on every push to `main` and every pull request. The `preflight`
|
||||
|
||||
GitHub may mark superseded jobs as `cancelled` when a newer push lands on the same PR or `main` ref. Treat that as CI noise unless the newest run for the same ref is also failing. Matrix jobs use `fail-fast: false`, and `build-artifacts` reports embedded channel, core-support-boundary, and gateway-watch failures directly instead of queuing tiny verifier jobs. The automatic CI concurrency key is versioned (`CI-v7-*`) so a GitHub-side zombie in an old queue group cannot indefinitely block newer main runs. Manual full-suite runs use `CI-manual-v1-*` and do not cancel in-progress runs.
|
||||
|
||||
Use `pnpm ci:timings`, `pnpm ci:timings:recent`, or `node scripts/ci-run-timings.mjs <run-id>` to summarize wall time, queue time, slowest jobs, failures, and the `pnpm-store-warmup` fanout barrier from GitHub Actions. CI also uploads the same run summary as a `ci-timings-summary` artifact. For build timing, check the `build-artifacts` job's `Build dist` step: `pnpm build:ci-artifacts` prints `[build-all] phase timings:` and includes `ui:build`; the job also uploads the `startup-memory` artifact.
|
||||
|
||||
For pull request runs, the terminal timing-summary job runs the helper from the trusted base revision before passing `GH_TOKEN` to `gh run view`. That keeps the tokened query out of branch-controlled code while still summarizing the pull request's current CI run.
|
||||
The `ci-timings-summary` job uploads a compact `ci-timings-summary` artifact for each non-draft CI run. It records wall time, queue time, slowest jobs, and failed jobs for the current run, so CI health checks do not need to scrape the full Actions payload repeatedly. The `build-artifacts` job also runs the blocking startup-memory smoke and uploads a `startup-memory` artifact with per-command RSS values for `--help`, `status --json`, and `gateway status`.
|
||||
|
||||
## Real behavior proof
|
||||
|
||||
@@ -122,17 +120,17 @@ gh workflow run full-release-validation.yml --ref main -f ref=<branch-or-sha>
|
||||
|
||||
## Runners
|
||||
|
||||
| Runner | Jobs |
|
||||
| -------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `ubuntu-24.04` | Manual CI dispatch and non-canonical repository fallbacks, workflow-sanity, labeler, auto-response, docs workflows outside CI, and install-smoke preflight so the Blacksmith matrix can queue earlier |
|
||||
| `blacksmith-4vcpu-ubuntu-2404` | `CodeQL Critical Quality`, `preflight`, `security-fast`, lower-weight extension shards, `checks-fast-core`, plugin/channel contract shards, `checks-node-compat-node22`, `check-guards`, `check-prod-types`, and `check-test-types` |
|
||||
| `blacksmith-8vcpu-ubuntu-2404` | Linux Node test shards, bundled plugin test shards, `check-additional-*` shards, `check-dependencies`, and `android` |
|
||||
| `blacksmith-16vcpu-ubuntu-2404` | `build-artifacts`, `check-lint` (CPU-sensitive enough that 8 vCPU cost more than they saved); install-smoke Docker builds (32-vCPU queue time cost more than it saved) |
|
||||
| `blacksmith-16vcpu-windows-2025` | `checks-windows` |
|
||||
| `blacksmith-6vcpu-macos-15` | `macos-node` on `openclaw/openclaw`; forks fall back to `macos-15` |
|
||||
| `blacksmith-12vcpu-macos-26` | `macos-swift` on `openclaw/openclaw`; forks fall back to `macos-26` |
|
||||
| Runner | Jobs |
|
||||
| -------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `ubuntu-24.04` | `preflight`, docs checks, Python skills, workflow-sanity, labeler, auto-response; install-smoke preflight also uses GitHub-hosted Ubuntu so the Blacksmith matrix can queue earlier |
|
||||
| `blacksmith-4vcpu-ubuntu-2404` | `CodeQL Critical Quality`, `security-fast`, lower-weight extension shards, `checks-fast-core`, plugin/channel contract shards, `checks-node-compat-node22`, `check-guards`, `check-prod-types`, and `check-test-types` |
|
||||
| `blacksmith-8vcpu-ubuntu-2404` | Linux Node test shards, bundled plugin test shards, `check-additional-*` shards, `android` |
|
||||
| `blacksmith-16vcpu-ubuntu-2404` | `build-artifacts`, `check-lint` (CPU-sensitive enough that 8 vCPU cost more than they saved); install-smoke Docker builds (32-vCPU queue time cost more than it saved) |
|
||||
| `blacksmith-16vcpu-windows-2025` | `checks-windows` |
|
||||
| `blacksmith-6vcpu-macos-15` | `macos-node` on `openclaw/openclaw`; forks fall back to `macos-15` |
|
||||
| `blacksmith-12vcpu-macos-26` | `macos-swift` on `openclaw/openclaw`; forks fall back to `macos-26` |
|
||||
|
||||
Canonical-repo CI keeps Blacksmith as the default runner path for normal push and pull-request runs. `workflow_dispatch` and non-canonical repository runs use GitHub-hosted runners, but normal canonical runs do not currently probe Blacksmith queue health or automatically fall back to GitHub-hosted labels when Blacksmith is unavailable.
|
||||
Canonical-repo CI keeps Blacksmith as the default runner path. During `preflight`, `scripts/ci-runner-labels.mjs` checks recent queued and in-progress Actions runs for queued Blacksmith jobs. If a specific Blacksmith label already has queued jobs, downstream jobs that would use that exact label fall back to the matching GitHub-hosted runner (`ubuntu-24.04`, `windows-2025`, `macos-15`, or `macos-26`) for that run only. Other Blacksmith sizes in the same OS family stay on their primary labels. If the API probe fails, no fallback is applied.
|
||||
|
||||
## Local equivalents
|
||||
|
||||
|
||||
@@ -157,8 +157,8 @@ order and tells you what it chose:
|
||||
|
||||
- existing explicit model, if already configured
|
||||
- `OPENAI_API_KEY` -> `openai/gpt-5.5`
|
||||
- `ANTHROPIC_API_KEY` -> `anthropic/claude-opus-4-8`
|
||||
- Claude Code CLI -> `claude-cli/claude-opus-4-8`
|
||||
- `ANTHROPIC_API_KEY` -> `anthropic/claude-opus-4-7`
|
||||
- Claude Code CLI -> `claude-cli/claude-opus-4-7`
|
||||
- Codex -> `openai/gpt-5.5` through the Codex app-server harness
|
||||
|
||||
If none are available, setup still writes the default workspace and leaves the
|
||||
@@ -173,7 +173,7 @@ planner turn through OpenClaw's normal runtime paths. It first uses the
|
||||
configured OpenClaw model. If no configured model is usable yet, it can fall
|
||||
back to local runtimes already present on the machine:
|
||||
|
||||
- Claude Code CLI: `claude-cli/claude-opus-4-8`
|
||||
- Claude Code CLI: `claude-cli/claude-opus-4-7`
|
||||
- Codex app-server harness: `openai/gpt-5.5`
|
||||
|
||||
The model-assisted planner cannot mutate config directly. It must translate the
|
||||
|
||||
@@ -14,26 +14,6 @@ Manage cron jobs for the Gateway scheduler.
|
||||
Run `openclaw cron --help` for the full command surface. See [Cron jobs](/automation/cron-jobs) for the conceptual guide.
|
||||
</Tip>
|
||||
|
||||
## Create jobs quickly
|
||||
|
||||
`openclaw cron create` is an alias for `openclaw cron add`. For new jobs, put the schedule first and the prompt second:
|
||||
|
||||
```bash
|
||||
openclaw cron create "0 7 * * *" \
|
||||
"Summarize overnight updates." \
|
||||
--name "Morning brief" \
|
||||
--agent ops
|
||||
```
|
||||
|
||||
Use `--webhook <url>` when the job should POST the finished payload instead of delivering to a chat target:
|
||||
|
||||
```bash
|
||||
openclaw cron create "0 18 * * 1-5" \
|
||||
"Summarize today's deploys as JSON." \
|
||||
--name "Deploy digest" \
|
||||
--webhook "https://example.invalid/openclaw/cron"
|
||||
```
|
||||
|
||||
## Sessions
|
||||
|
||||
`--session` accepts `main`, `isolated`, `current`, or `session:<id>`.
|
||||
@@ -70,8 +50,6 @@ Isolated cron chat delivery is shared between the agent and the runner:
|
||||
- `webhook` posts the finished payload to a URL.
|
||||
- `none` disables runner fallback delivery.
|
||||
|
||||
Use `cron add|create --webhook <url>` or `cron edit <job-id> --webhook <url>` to set webhook delivery. Do not combine `--webhook` with chat delivery flags such as `--announce`, `--no-deliver`, `--channel`, `--to`, `--thread-id`, or `--account`.
|
||||
|
||||
`--announce` is runner fallback delivery for the final reply. `--no-deliver` disables that fallback but does not remove the agent's `message` tool when a chat route is available.
|
||||
|
||||
Reminders created from an active chat preserve the live chat delivery target for fallback announce delivery. Internal session keys may be lowercase; do not use them as a source of truth for case-sensitive provider IDs such as Matrix room IDs.
|
||||
@@ -118,7 +96,7 @@ Skipped runs are tracked separately from execution errors. They do not affect re
|
||||
|
||||
For isolated jobs that target a local configured model provider, cron runs a lightweight provider preflight before starting the agent turn. Loopback, private-network, and `.local` `api: "ollama"` providers are probed at `/api/tags`; local OpenAI-compatible providers such as vLLM, SGLang, and LM Studio are probed at `/models`. If the endpoint is unreachable, the run is recorded as `skipped` and retried on a later schedule; matching dead endpoints are cached for 5 minutes to avoid many jobs hammering the same local server.
|
||||
|
||||
Note: cron job definitions live in `jobs.json`, while pending runtime state lives in `jobs-state.json`. If `jobs.json` is edited externally, the Gateway reloads changed schedules and clears stale pending slots; formatting-only rewrites do not clear the pending slot. Malformed job rows are removed from active `jobs.json` at load time after their raw contents are copied to `jobs-quarantine.json`.
|
||||
Note: cron job definitions live in `jobs.json`, while pending runtime state lives in `jobs-state.json`. If `jobs.json` is edited externally, the Gateway reloads changed schedules and clears stale pending slots; formatting-only rewrites do not clear the pending slot.
|
||||
|
||||
### Manual runs
|
||||
|
||||
@@ -155,7 +133,6 @@ Cron `--model` is a **job primary**, not a chat-session `/model` override. That
|
||||
- Per-job payload `fallbacks` replaces the configured fallback list when present.
|
||||
- An empty per-job fallback list (`fallbacks: []` in the job payload/API) makes the cron run strict.
|
||||
- When a job has `--model` but no fallback list is configured, OpenClaw passes an explicit empty fallback override so the agent primary is not appended as a hidden retry target.
|
||||
- Local-provider preflight checks walk configured fallbacks before marking a cron run `skipped`.
|
||||
|
||||
`openclaw doctor` reports jobs that already have `payload.model` set, including provider namespace counts and mismatches against `agents.defaults.model`. Use that check when auth, provider, or billing behavior looks different between live chat and scheduled jobs.
|
||||
|
||||
@@ -242,10 +219,11 @@ openclaw cron edit <job-id> --announce --channel telegram --to "-1001234567890"
|
||||
Create an isolated job with lightweight bootstrap context:
|
||||
|
||||
```bash
|
||||
openclaw cron create "0 7 * * *" \
|
||||
"Summarize overnight updates." \
|
||||
openclaw cron add \
|
||||
--name "Lightweight morning brief" \
|
||||
--cron "0 7 * * *" \
|
||||
--session isolated \
|
||||
--message "Summarize overnight updates." \
|
||||
--light-context \
|
||||
--no-deliver
|
||||
```
|
||||
@@ -292,7 +270,6 @@ Delivery tweaks:
|
||||
|
||||
```bash
|
||||
openclaw cron edit <job-id> --announce --channel slack --to "channel:C1234567890"
|
||||
openclaw cron edit <job-id> --webhook "https://example.invalid/openclaw/cron"
|
||||
openclaw cron edit <job-id> --best-effort-deliver
|
||||
openclaw cron edit <job-id> --no-best-effort-deliver
|
||||
openclaw cron edit <job-id> --no-deliver
|
||||
|
||||
@@ -40,7 +40,6 @@ openclaw doctor
|
||||
openclaw doctor --lint
|
||||
openclaw doctor --lint --json
|
||||
openclaw doctor --lint --severity-min warning
|
||||
openclaw doctor --lint --allow-exec
|
||||
openclaw doctor --deep
|
||||
openclaw doctor --fix
|
||||
openclaw doctor --fix --non-interactive
|
||||
@@ -65,7 +64,6 @@ The targeted Discord capabilities probe reports the bot's effective channel perm
|
||||
- `--force`: apply aggressive repairs, including overwriting custom service config when needed
|
||||
- `--non-interactive`: run without prompts; safe migrations and non-service repairs only
|
||||
- `--generate-gateway-token`: generate and configure a gateway token
|
||||
- `--allow-exec`: allow doctor to execute configured exec SecretRefs while verifying secrets
|
||||
- `--deep`: scan system services for extra gateway installs and report recent Gateway supervisor restart handoffs
|
||||
- `--lint`: run modernized health checks in read-only mode and emit diagnostic findings
|
||||
- `--json`: with `--lint`, emit JSON findings instead of human output
|
||||
@@ -86,7 +84,6 @@ are only accepted with `--lint`.
|
||||
openclaw doctor --lint
|
||||
openclaw doctor --lint --severity-min warning
|
||||
openclaw doctor --lint --json
|
||||
openclaw doctor --lint --allow-exec
|
||||
openclaw doctor --lint --only core/doctor/gateway-config --json
|
||||
```
|
||||
|
||||
@@ -194,7 +191,6 @@ Notes:
|
||||
- Interactive prompts (like keychain/OAuth fixes) only run when stdin is a TTY and `--non-interactive` is **not** set. Headless runs (cron, Telegram, no terminal) will skip prompts.
|
||||
- Performance: non-interactive `doctor` runs skip eager plugin loading so headless health checks stay fast. Interactive doctor sessions still load the plugin surfaces needed by the legacy health and repair flow.
|
||||
- `--lint` is stricter than `--non-interactive`: it is always read-only, never prompts, and never applies safe migrations. Run `doctor --fix` or `doctor --repair` when you want doctor to make changes.
|
||||
- By default, doctor does not execute `exec` SecretRefs while checking secrets. Use `openclaw doctor --allow-exec` or `openclaw doctor --lint --allow-exec` only when you intentionally want doctor to run those configured secret resolvers.
|
||||
- `--fix` (alias for `--repair`) writes a backup to `~/.openclaw/openclaw.json.bak` and drops unknown config keys, listing each removal.
|
||||
- Modernized health checks can expose a `repair()` path for `doctor --fix`; checks that do not expose one continue through the existing doctor repair flow.
|
||||
- `doctor --fix --non-interactive` reports missing or stale gateway service definitions but does not install or rewrite them outside update repair mode. Run `openclaw gateway install` for a missing service, or `openclaw gateway install --force` when you intentionally want to replace the launcher.
|
||||
@@ -203,7 +199,7 @@ Notes:
|
||||
- Doctor reports cron jobs with explicit `payload.model` overrides, including provider namespace counts and mismatches against `agents.defaults.model`, so scheduled jobs that do not inherit the default model are visible during auth or billing investigations.
|
||||
- On Linux, doctor warns when the user's crontab still runs legacy `~/.openclaw/bin/ensure-whatsapp.sh`; that script is no longer maintained and can log false WhatsApp gateway outages when cron lacks the systemd user-bus environment.
|
||||
- When WhatsApp is enabled, doctor checks for a degraded Gateway event loop with local `openclaw-tui` clients still running. `doctor --fix` stops only verified local TUI clients so WhatsApp replies are not queued behind stale TUI refresh loops.
|
||||
- Doctor rewrites legacy `openai-codex/*` model refs to canonical `openai/*` refs across primary models, fallbacks, image/video generation models, heartbeat/subagent/compaction overrides, hooks, channel model overrides, and stale session route pins. `--fix` also migrates legacy `openai-codex:*` auth profiles and `auth.order.openai-codex` entries to `openai:*`, moves Codex intent onto provider/model-scoped `agentRuntime.id: "codex"` entries, removes stale whole-agent/session runtime pins, and keeps repaired OpenAI agent refs on Codex auth routing instead of direct OpenAI API-key auth.
|
||||
- Doctor rewrites legacy `openai-codex/*` model refs to canonical `openai/*` refs across primary models, fallbacks, heartbeat/subagent/compaction overrides, hooks, channel model overrides, and stale session route pins. `--fix` moves Codex intent onto provider/model-scoped `agentRuntime.id: "codex"` entries, preserves session auth-profile pins such as `openai-codex:...`, removes stale whole-agent/session runtime pins, and keeps repaired OpenAI agent refs on Codex auth routing instead of direct OpenAI API-key auth.
|
||||
- Doctor cleans legacy plugin dependency staging state created by older OpenClaw versions and relinks the host `openclaw` package for managed npm plugins that declare it as a peer dependency. It also repairs missing downloadable plugins that are referenced by config, such as `plugins.entries`, configured channels, configured provider/search settings, or configured agent runtimes. During package updates, doctor skips package-manager plugin repair until the package swap is complete; rerun `openclaw doctor --fix` afterward if a configured plugin still needs recovery. If the download fails, doctor reports the install error and preserves the configured plugin entry for the next repair attempt.
|
||||
- Doctor repairs stale plugin config by removing missing plugin ids from `plugins.allow`/`plugins.deny`/`plugins.entries`, plus matching dangling channel config, heartbeat targets, and channel model overrides when plugin discovery is healthy.
|
||||
- Doctor quarantines invalid plugin config by disabling the affected `plugins.entries.<id>` entry and removing its invalid `config` payload. Gateway startup already skips only that bad plugin so other plugins and channels can keep running.
|
||||
@@ -218,7 +214,7 @@ Notes:
|
||||
- Doctor warns when skills allowed for the default agent are unavailable in the current runtime environment because bins, env vars, config, or OS requirements are missing. `doctor --fix` can disable those unavailable skills with `skills.entries.<skill>.enabled=false`; install/configure the missing requirement instead when you want to keep the skill active.
|
||||
- If sandbox mode is enabled but Docker is unavailable, doctor reports a high-signal warning with remediation (`install Docker` or `openclaw config set agents.defaults.sandbox.mode off`).
|
||||
- If legacy sandbox registry files (`~/.openclaw/sandbox/containers.json` or `~/.openclaw/sandbox/browsers.json`) are present, doctor reports them; `openclaw doctor --fix` migrates valid entries into sharded registry directories and quarantines invalid legacy files.
|
||||
- If `gateway.auth.token`/`gateway.auth.password` are SecretRef-managed and unavailable in the current command path, doctor reports a read-only warning and does not write plaintext fallback credentials. For exec-backed SecretRefs, doctor skips execution unless `--allow-exec` is present.
|
||||
- If `gateway.auth.token`/`gateway.auth.password` are SecretRef-managed and unavailable in the current command path, doctor reports a read-only warning and does not write plaintext fallback credentials.
|
||||
- If channel SecretRef inspection fails in a fix path, doctor continues and reports a warning instead of exiting early.
|
||||
- After state-directory migrations, doctor warns when enabled default Telegram or Discord accounts depend on env fallback and `TELEGRAM_BOT_TOKEN` or `DISCORD_BOT_TOKEN` is unavailable to the doctor process.
|
||||
- Telegram `allowFrom` username auto-resolution (`doctor --fix`) requires a resolvable Telegram token in the current command path. If token inspection is unavailable, doctor reports a warning and skips auto-resolution for that pass.
|
||||
|
||||
@@ -129,7 +129,7 @@ This table maps common inference tasks to the corresponding infer command.
|
||||
- Use `model run --thinking <level>` to pass a one-shot thinking/reasoning level (`off`, `minimal`, `low`, `medium`, `high`, `adaptive`, `xhigh`, or `max`) while keeping the run raw.
|
||||
- For `image describe`, `audio transcribe`, and `video describe`, `--model` must use the form `<provider/model>`.
|
||||
- For `image describe`, `--file` accepts local paths and HTTP(S) image URLs. Remote URLs use the normal media-fetch SSRF policy.
|
||||
- For `image describe`, an explicit `--model` runs that provider/model directly. The model must be image-capable in the model catalog or provider config. `codex/<model>` runs a bounded Codex app-server image-understanding turn; `openai/<model>` uses the OpenAI provider path with either API-key or ChatGPT/Codex OAuth auth.
|
||||
- For `image describe`, an explicit `--model` runs that provider/model directly. The model must be image-capable in the model catalog or provider config. `codex/<model>` runs a bounded Codex app-server image-understanding turn; `openai-codex/<model>` uses the OpenAI Codex OAuth provider path.
|
||||
- Stateless execution commands default to local.
|
||||
- Gateway-managed state commands default to gateway.
|
||||
- The normal local path does not require the gateway to be running.
|
||||
@@ -172,7 +172,7 @@ Notes:
|
||||
- Local `model run` is the narrowest CLI smoke for provider/model/auth health because, for non-Codex providers, it sends only the supplied prompt to the selected model.
|
||||
- Local `model run --model <provider/model>` can use exact bundled static catalog rows from `models list --all` before that provider is written to config. Provider auth is still required; missing credentials fail as auth errors, not `Unknown model`.
|
||||
- For Mistral Medium 3.5 reasoning probes, leave temperature unset/default. Mistral rejects `reasoning_effort="high"` plus `temperature: 0`; use `mistral/mistral-medium-3-5` with default temperature or a non-zero reasoning-mode value such as `0.7`.
|
||||
- Codex Responses local probes are the narrow exception: OpenClaw adds a minimal system instruction so the transport can populate its required `instructions` field, without adding full agent context, tools, memory, or session transcript.
|
||||
- `openai-codex/*` local probes are the narrow exception: OpenClaw adds a minimal system instruction so the Codex Responses transport can populate its required `instructions` field, without adding full agent context, tools, memory, or session transcript.
|
||||
- Local `model run --file` keeps that lean path and attaches image content directly to the single user message. Common image files such as PNG, JPEG, and WebP work when their MIME type is detected as `image/*`; unsupported or unrecognized files fail before the provider is called.
|
||||
- `model run --file` is best when you want to test the selected multimodal text model directly. Use `infer image describe` when you want OpenClaw's image-understanding provider selection and default image-model routing.
|
||||
- The selected model must support image input; text-only models may reject the request at the provider layer.
|
||||
|
||||
@@ -170,7 +170,7 @@ Notes:
|
||||
- `memory status` includes any extra paths configured via `memorySearch.extraPaths`.
|
||||
- If effectively active memory remote API key fields are configured as SecretRefs, the command resolves those values from the active gateway snapshot. If gateway is unavailable, the command fails fast.
|
||||
- Gateway version skew note: this command path requires a gateway that supports `secrets.resolve`; older gateways return an unknown-method error.
|
||||
- Tune scheduled sweep cadence with `dreaming.frequency`. Deep promotion policy is otherwise internal except for `dreaming.phases.deep.maxPromotedSnippetTokens`, which bounds promoted snippet length while keeping provenance visible. Use CLI flags on `memory promote` when you need one-off manual threshold overrides.
|
||||
- Tune scheduled sweep cadence with `dreaming.frequency`. Deep promotion policy is otherwise internal; use CLI flags on `memory promote` when you need one-off manual overrides.
|
||||
- `memory rem-harness --path <file-or-dir> --grounded` previews grounded `What Happened`, `Reflections`, and `Possible Lasting Updates` from historical daily notes without writing anything.
|
||||
- `memory rem-backfill --path <file-or-dir>` writes reversible grounded diary entries into `DREAMS.md` for UI review.
|
||||
- `memory rem-backfill --path <file-or-dir> --stage-short-term` also seeds grounded durable candidates into the live short-term promotion store so the normal deep phase can rank them.
|
||||
|
||||
@@ -28,8 +28,8 @@ openclaw models scan
|
||||
`openclaw models status` shows the resolved default/fallbacks plus an auth overview.
|
||||
When provider usage snapshots are available, the OAuth/API-key status section includes
|
||||
provider usage windows and quota snapshots.
|
||||
Current usage-window providers: Anthropic, GitHub Copilot, Gemini CLI, OpenAI,
|
||||
MiniMax, Xiaomi, and z.ai. Usage auth comes from provider-specific hooks
|
||||
Current usage-window providers: Anthropic, GitHub Copilot, Gemini CLI, OpenAI
|
||||
Codex, MiniMax, Xiaomi, and z.ai. Usage auth comes from provider-specific hooks
|
||||
when available; otherwise OpenClaw falls back to matching OAuth/API-key
|
||||
credentials from auth profiles, env, or config.
|
||||
In `--json` output, `auth.providers` is the env/config/store-aware provider
|
||||
@@ -40,10 +40,10 @@ Use `--agent <id>` to inspect a configured agent's model/auth state. When omitte
|
||||
the command uses `OPENCLAW_AGENT_DIR` if set, otherwise the
|
||||
configured default agent.
|
||||
Probe rows can come from auth profiles, env credentials, or `models.json`.
|
||||
For OpenAI ChatGPT/Codex OAuth troubleshooting, `openclaw models status`,
|
||||
`openclaw models auth list --provider openai`, and
|
||||
For Codex OAuth troubleshooting, `openclaw models status`,
|
||||
`openclaw models auth list --provider openai-codex`, and
|
||||
`openclaw config get agents.defaults.model --json` are the quickest way to
|
||||
confirm whether an agent has a usable `openai` OAuth profile for
|
||||
confirm whether an agent has a usable `openai-codex` auth profile for
|
||||
`openai/*` through the native Codex runtime. See [OpenAI provider setup](/providers/openai#check-and-recover-codex-oauth-routing).
|
||||
|
||||
Notes:
|
||||
@@ -76,7 +76,7 @@ Notes:
|
||||
cap differs from the native context window; JSON rows include `contextTokens`
|
||||
when a provider exposes that cap.
|
||||
- `models list --provider <id>` filters by provider id, such as `moonshot` or
|
||||
`openai`. It does not accept display labels from interactive provider
|
||||
`openai-codex`. It does not accept display labels from interactive provider
|
||||
pickers, such as `Moonshot AI`.
|
||||
- Model refs are parsed by splitting on the **first** `/`. If the model ID includes `/` (OpenRouter-style), include the provider prefix (example: `openrouter/moonshotai/kimi-k2`).
|
||||
- If you omit the provider, OpenClaw resolves the input as an alias first, then
|
||||
@@ -181,7 +181,7 @@ provider you choose.
|
||||
|
||||
`models auth list` lists saved auth profiles for the selected agent without
|
||||
printing token, API-key, or OAuth secret material. Use `--provider <id>` to
|
||||
filter to one provider, such as `openai`, and `--json` for scripting.
|
||||
filter to one provider, such as `openai-codex`, and `--json` for scripting.
|
||||
|
||||
`models auth login` runs a provider plugin's auth flow (OAuth/API key). Use
|
||||
`openclaw plugins list` to see which providers are installed.
|
||||
@@ -192,15 +192,15 @@ specific configured agent store. The parent `--agent` flag is honored by
|
||||
|
||||
For OpenAI models, `--provider openai` defaults to ChatGPT/Codex account login.
|
||||
Use `--method api-key` only when you want to add an OpenAI API-key profile,
|
||||
usually as a backup for Codex subscription limits. Run `openclaw doctor --fix`
|
||||
to migrate older `openai-codex` auth/profile state to `openai`.
|
||||
usually as a backup for Codex subscription limits. The legacy
|
||||
`--provider openai-codex` spelling still works for existing scripts.
|
||||
|
||||
Examples:
|
||||
|
||||
```bash
|
||||
openclaw models auth login --provider openai --set-default
|
||||
openclaw models auth login --provider openai --method api-key
|
||||
openclaw models auth paste-api-key --provider openai
|
||||
openclaw models auth paste-api-key --provider openai-codex
|
||||
openclaw models auth list --provider openai
|
||||
```
|
||||
|
||||
@@ -212,7 +212,7 @@ Notes:
|
||||
- `paste-api-key` accepts API keys generated elsewhere, prompts for the key
|
||||
value, and writes it to the default profile id `<provider>:manual` unless you
|
||||
pass `--profile-id`. In automation, pipe the key on stdin, for example
|
||||
`printf "%s\n" "$OPENAI_API_KEY" | openclaw models auth paste-api-key --provider openai`.
|
||||
`printf "%s\n" "$OPENAI_API_KEY" | openclaw models auth paste-api-key --provider openai-codex`.
|
||||
- `setup-token` and `paste-token` remain generic token commands for providers
|
||||
that expose token auth methods.
|
||||
- `setup-token` requires an interactive TTY and runs the provider's token-auth
|
||||
@@ -226,7 +226,7 @@ Notes:
|
||||
provider credentials do not appear in shell history or process lists.
|
||||
- `paste-token --expires-in <duration>` stores an absolute token expiry from a
|
||||
relative duration such as `365d` or `12h`.
|
||||
- For `openai`, OpenAI API keys and ChatGPT/OAuth token material are
|
||||
- For `openai-codex`, OpenAI API keys and ChatGPT/OAuth token material are
|
||||
different auth shapes. Use `paste-api-key` for `sk-...` OpenAI API keys and
|
||||
`paste-token` only for token auth material.
|
||||
- Anthropic note: Anthropic staff told us OpenClaw-style Claude CLI usage is allowed again, so OpenClaw treats Claude CLI reuse and `claude -p` usage as sanctioned for this integration unless Anthropic publishes a new policy.
|
||||
|
||||
@@ -177,8 +177,6 @@ is available, then fall back to `latest`.
|
||||
|
||||
Bare specs and `@latest` stay on the stable track. OpenClaw date-stamped correction versions such as `2026.5.3-1` are stable releases for this check. If npm resolves either of those to a prerelease, OpenClaw stops and asks you to opt in explicitly with a prerelease tag such as `@beta`/`@rc` or an exact prerelease version such as `@1.2.3-beta.4`.
|
||||
|
||||
For npm installs without an exact version (`npm:<package>` or `npm:<package>@latest`), OpenClaw checks the resolved package metadata before install. If the latest stable package requires a newer OpenClaw plugin API or minimum host version, OpenClaw inspects older stable versions and installs the newest compatible release instead. Exact versions and explicit dist-tags such as `@beta` remain strict: if the selected package is incompatible, the command fails and asks you to upgrade OpenClaw or choose a compatible version.
|
||||
|
||||
If a bare install spec matches an official plugin id (for example `diffs`), OpenClaw installs the catalog entry directly. To install an npm package with the same name, use an explicit scoped spec (for example `@scope/diffs`).
|
||||
|
||||
</Accordion>
|
||||
|
||||
@@ -18,13 +18,12 @@ report drift through `doctor --lint`. The final conformance signal is a clean
|
||||
instead of creating a separate health gate.
|
||||
|
||||
Policy currently manages configured channels, MCP servers, model providers,
|
||||
network SSRF posture, ingress/channel access posture, Gateway exposure posture, agent workspace posture,
|
||||
network SSRF posture, Gateway exposure posture, agent workspace posture,
|
||||
OpenClaw config secret provider/auth profile posture, and governed tool
|
||||
declarations. For example, IT or a workspace operator can record that Telegram
|
||||
is not an approved channel provider, restrict MCP servers and model refs to
|
||||
approved entries, require private-network fetch/browser access to remain
|
||||
disabled, require direct-message session isolation and channel ingress posture
|
||||
to stay within reviewed bounds, require Gateway bind/auth/HTTP exposure to stay within reviewed
|
||||
disabled, require Gateway bind/auth/HTTP exposure to stay within reviewed
|
||||
bounds, require agent workspace access and tool denies to stay in a reviewed
|
||||
posture, require OpenClaw config SecretRefs to use managed providers, require
|
||||
config auth profiles to carry provider/mode metadata, require governed tools to
|
||||
@@ -50,9 +49,9 @@ arbitrary plugins. The plugin remains enabled if `policy.jsonc` is missing, so
|
||||
doctor can report the missing artifact.
|
||||
|
||||
Policy is authored, not generated from the user's current settings. A minimal
|
||||
policy for channels, MCP servers, model providers, network posture, ingress/channel access, Gateway
|
||||
exposure, agent workspace posture, configured sandbox runtime posture, OpenClaw
|
||||
config secret provider/auth profile posture, and tool metadata looks like this:
|
||||
policy for channels, MCP servers, model providers, network posture, Gateway
|
||||
exposure, agent workspace posture, OpenClaw config secret provider/auth profile
|
||||
posture, and tool metadata looks like this:
|
||||
|
||||
```jsonc
|
||||
{
|
||||
@@ -82,16 +81,6 @@ config secret provider/auth profile posture, and tool metadata looks like this:
|
||||
"allow": false,
|
||||
},
|
||||
},
|
||||
"ingress": {
|
||||
"session": {
|
||||
"requireDmScope": "per-channel-peer",
|
||||
},
|
||||
"channels": {
|
||||
"allowDmPolicies": ["pairing", "allowlist", "disabled"],
|
||||
"denyOpenGroups": true,
|
||||
"requireMentionInGroups": true,
|
||||
},
|
||||
},
|
||||
"gateway": {
|
||||
"exposure": {
|
||||
"allowNonLoopbackBind": false,
|
||||
@@ -153,9 +142,8 @@ config secret provider/auth profile posture, and tool metadata looks like this:
|
||||
The rules are the authority. A category block is only a namespace; checks run
|
||||
when a concrete rule is present. OpenClaw reads current `channels.*` settings
|
||||
`mcp.servers.*`, `models.providers.*`, selected agent model refs, network SSRF
|
||||
settings, direct-message session scope, channel DM policy, channel group policy,
|
||||
channel/group mention gates, Gateway bind/auth/Control UI/Tailscale/remote/HTTP
|
||||
posture, OpenClaw config agent sandbox workspace access and tool deny posture, config secret
|
||||
settings, Gateway bind/auth/Control UI/Tailscale/remote/HTTP posture, OpenClaw
|
||||
config agent sandbox workspace access and tool deny posture, config secret
|
||||
provider and SecretRef provenance, config auth profile metadata, configured
|
||||
global/per-agent tool posture, and `TOOLS.md` declarations as evidence, then
|
||||
reports observed state that does not conform. If a policy denies non-loopback
|
||||
@@ -184,102 +172,21 @@ present in `policy.jsonc`. The observed state is existing OpenClaw config or
|
||||
workspace metadata; policy reports drift but does not rewrite runtime behavior
|
||||
unless a repair path is explicitly available and enabled.
|
||||
|
||||
Policy overlays keep broad top-level rules global, then let named scope blocks
|
||||
add stricter normal policy sections for explicit selectors. A scope name is a
|
||||
descriptive bucket only; matching uses the selector values inside the scope.
|
||||
The overlay is additive: global claims still run, and a scoped claim can emit
|
||||
its own finding against the same observed config.
|
||||
|
||||
#### Scoped overlays
|
||||
|
||||
Use `scopes.<scopeName>` when one set of agents or channels needs stricter
|
||||
policy than the top-level baseline. Agent-scoped sections use `agentIds`, which
|
||||
supports `tools.*`, `agents.workspace.*`, and `sandbox.*`. Channel-scoped
|
||||
ingress uses `channelIds`, which supports `ingress.channels.*`. Unsupported
|
||||
sections are rejected instead of being ignored. If an `agentIds` entry is not
|
||||
present in `agents.list[]`, OpenClaw evaluates the scoped rule against inherited
|
||||
global/default posture for that runtime agent id.
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"tools": {
|
||||
"exec": {
|
||||
"allowHosts": ["sandbox", "node"],
|
||||
},
|
||||
},
|
||||
"sandbox": {
|
||||
"requireMode": ["all", "non-main"],
|
||||
},
|
||||
"scopes": {
|
||||
"release-workspace": {
|
||||
"agentIds": ["release-agent", "review-agent"],
|
||||
"agents": {
|
||||
"workspace": {
|
||||
"allowedAccess": ["none", "ro"],
|
||||
},
|
||||
},
|
||||
},
|
||||
"release-lockdown": {
|
||||
"agentIds": ["release-agent"],
|
||||
"tools": {
|
||||
"exec": {
|
||||
"allowHosts": ["sandbox"],
|
||||
"allowSecurity": ["deny", "allowlist"],
|
||||
"requireAsk": ["always"],
|
||||
},
|
||||
"denyTools": ["exec", "process", "write", "edit", "apply_patch"],
|
||||
},
|
||||
"sandbox": {
|
||||
"requireMode": ["all"],
|
||||
"allowBackends": ["docker"],
|
||||
},
|
||||
},
|
||||
"shell-sandbox": {
|
||||
"agentIds": ["shell-agent"],
|
||||
"sandbox": {
|
||||
"allowBackends": ["openshell"],
|
||||
"containers": {
|
||||
"requireReadOnlyMounts": false,
|
||||
},
|
||||
},
|
||||
},
|
||||
"telegram-ingress": {
|
||||
"channelIds": ["telegram"],
|
||||
"ingress": {
|
||||
"channels": {
|
||||
"allowDmPolicies": ["pairing"],
|
||||
"denyOpenGroups": true,
|
||||
"requireMentionInGroups": true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
The same agent can appear in multiple scopes when each scope governs different
|
||||
fields, as shown above. A repeated scoped field for the same agent must be
|
||||
equally or more restrictive according to policy metadata; weaker duplicate
|
||||
claims are rejected. Strictness metadata treats allow-lists as subsets,
|
||||
deny-lists as supersets, and required booleans as fixed requirements.
|
||||
|
||||
Container posture policy is evaluated only against evidence OpenClaw can
|
||||
observe for the matched agent. If an enabled `sandbox.containers.*` rule applies
|
||||
to an agent whose sandbox backend cannot expose that field, policy reports
|
||||
`policy/sandbox-container-posture-unobservable` instead of treating the claim as
|
||||
passing. Use separate `agentIds` scopes for agent groups that use different
|
||||
sandbox backends, and leave unsupported container rules unset or false for the
|
||||
groups where those fields cannot be observed.
|
||||
|
||||
Top-level `ingress.session.requireDmScope` remains global because
|
||||
`session.dmScope` is not channel-attributable evidence.
|
||||
|
||||
| Selector | Supported sections | Use when |
|
||||
| ------------ | ------------------------------------------ | ------------------------------------------------- |
|
||||
| `agentIds` | `tools`, `agents.workspace`, and `sandbox` | One or more runtime agents need stricter rules. |
|
||||
| `channelIds` | `ingress.channels` | One or more channels need stricter ingress rules. |
|
||||
|
||||
Every scope present in `policy.jsonc` must be valid and enforceable.
|
||||
Agent-specific policy overlays keep broad `tools.*` and `agents.workspace`
|
||||
posture global, then let named scope blocks add stricter normal policy sections
|
||||
for explicit `agentIds` under `scopes.<scopeName>`. The initial scoped
|
||||
sections are `tools` and `agents.workspace`; sandbox and ingress can use the
|
||||
same container once their evidence is attributable to an agent. Scoped fields
|
||||
carry strictness metadata such as allowlist subset, denylist superset, required
|
||||
boolean, and exact-list semantics so future policy-file conformance can reuse
|
||||
the same rule inventory instead of guessing. The overlay is additive: global
|
||||
claims still run, and a scoped claim can emit its own finding against the same
|
||||
observed config. See [Agent-scoped policy overlays](/plan/policy-agent-scoped-overlays).
|
||||
Every scope present in `policy.jsonc` must be valid and enforceable. Scopes
|
||||
currently require `agentIds`, and that selector supports only `tools.*` and
|
||||
`agents.workspace.*`. If an `agentIds` entry is not present in `agents.list[]`,
|
||||
the scoped rule is evaluated against the inherited global/default posture for
|
||||
that runtime agent id instead of being skipped.
|
||||
|
||||
#### Channels
|
||||
|
||||
@@ -308,15 +215,6 @@ Every scope present in `policy.jsonc` must be valid and enforceable.
|
||||
| ------------------------------ | ----------------------------------- | ------------------------------------------------------------------ |
|
||||
| `network.privateNetwork.allow` | Private-network SSRF escape hatches | Set to `false` to require private-network access to stay disabled. |
|
||||
|
||||
#### Ingress and channel access
|
||||
|
||||
| Policy field | Observed state | Use when |
|
||||
| ----------------------------------------- | -------------------------------------------------------------- | ------------------------------------------------------------------ |
|
||||
| `ingress.session.requireDmScope` | `session.dmScope` | Require a reviewed direct-message isolation scope. |
|
||||
| `ingress.channels.allowDmPolicies` | `channels.*.dmPolicy` and legacy channel DM policy fields | Allow only reviewed direct-message channel policies. |
|
||||
| `ingress.channels.denyOpenGroups` | Channel, account, and group ingress policy | Deny open group ingress for configured channels and accounts. |
|
||||
| `ingress.channels.requireMentionInGroups` | Channel, account, group, guild, and nested mention gate config | Require mention gates when group ingress is open or mention-gated. |
|
||||
|
||||
#### Gateway
|
||||
|
||||
| Policy field | Observed state | Use when |
|
||||
@@ -337,23 +235,6 @@ Every scope present in `policy.jsonc` must be valid and enforceable.
|
||||
| `agents.workspace.allowedAccess` | `agents.defaults.sandbox.workspaceAccess` and `agents.list[].sandbox.workspaceAccess` | Allow only sandbox workspace access values such as `none` or `ro`. |
|
||||
| `agents.workspace.denyTools` | Global and per-agent tool deny config | Require workspace/runtime mutation tools such as `exec`, `process`, `write`, `edit`, or `apply_patch` to be denied. |
|
||||
|
||||
#### Sandbox posture
|
||||
|
||||
| Policy field | Observed state | Use when |
|
||||
| ----------------------------------------------------- | ------------------------------------------------------- | -------------------------------------------------------------- |
|
||||
| `sandbox.requireMode` | `agents.defaults.sandbox.mode` and per-agent mode | Allow only reviewed sandbox modes such as `all` or `non-main`. |
|
||||
| `sandbox.allowBackends` | `agents.defaults.sandbox.backend` and per-agent backend | Allow only reviewed sandbox backends such as `docker`. |
|
||||
| `sandbox.containers.denyHostNetwork` | Container-backed sandbox/browser network mode | Deny host network mode. |
|
||||
| `sandbox.containers.denyContainerNamespaceJoin` | Container-backed sandbox/browser network mode | Deny joining another container network namespace. |
|
||||
| `sandbox.containers.requireReadOnlyMounts` | Container-backed sandbox/browser mount mode | Require mounts to be read-only. |
|
||||
| `sandbox.containers.denyContainerRuntimeSocketMounts` | Container-backed sandbox/browser mount targets | Deny container runtime socket mounts. |
|
||||
| `sandbox.containers.denyUnconfinedProfiles` | Container security profile posture | Deny unconfined container security profiles. |
|
||||
| `sandbox.browser.requireCdpSourceRange` | Sandbox browser CDP source range | Require browser CDP exposure to declare a source range. |
|
||||
|
||||
Policy treats missing `sandbox.mode` as the implicit default `off`, so
|
||||
`sandbox.requireMode` reports a fresh or unconfigured sandbox as outside an
|
||||
allowlist such as `["all"]`.
|
||||
|
||||
#### Secrets
|
||||
|
||||
| Policy field | Observed state | Use when |
|
||||
@@ -400,41 +281,8 @@ openclaw policy check --severity-min error
|
||||
attestation hashes. The same findings also appear in `openclaw doctor --lint`
|
||||
when the Policy plugin is enabled.
|
||||
|
||||
Compare an operator policy file to an authored baseline policy file:
|
||||
|
||||
```bash
|
||||
openclaw policy compare --baseline official.policy.jsonc
|
||||
openclaw policy compare --baseline official.policy.jsonc --policy policy.jsonc --json
|
||||
```
|
||||
|
||||
`policy compare` compares policy file syntax to policy file syntax. It does not
|
||||
inspect OpenClaw runtime state, evidence, credentials, or secrets. The command
|
||||
uses the same policy rule metadata that governs scoped overlays: allowlists must
|
||||
stay equal or narrower, denylists must stay equal or broader, required booleans
|
||||
must keep their required value, ordered strings must move only toward the more
|
||||
restrictive end of the configured order, and exact lists must match.
|
||||
|
||||
The baseline file can be an organization-authored policy. The checked policy can
|
||||
use stricter values or add extra policy rules. A top-level checked rule can also
|
||||
satisfy a scoped baseline rule when it is equally or more restrictive because
|
||||
top-level policy applies broadly. Scope names do not need to match; scoped
|
||||
comparison is keyed by selector value such as `agentIds` or `channelIds` and by
|
||||
the policy field being checked.
|
||||
|
||||
Example clean compare JSON output reports only policy-file comparison state:
|
||||
|
||||
```json
|
||||
{
|
||||
"ok": true,
|
||||
"baselinePath": "official.policy.jsonc",
|
||||
"policyPath": "policy.jsonc",
|
||||
"rulesChecked": 3,
|
||||
"findings": []
|
||||
}
|
||||
```
|
||||
|
||||
Example clean `policy check --json` output includes stable hashes that can be
|
||||
recorded by an operator or supervisor:
|
||||
Example clean JSON output includes stable hashes that can be recorded by an
|
||||
operator or supervisor:
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -674,63 +522,47 @@ choose a different interval.
|
||||
|
||||
Policy currently verifies:
|
||||
|
||||
| Check id | Finding |
|
||||
| ------------------------------------------------- | --------------------------------------------------------------------------------- |
|
||||
| `policy/policy-jsonc-missing` | Policy is enabled but `policy.jsonc` is missing. |
|
||||
| `policy/policy-jsonc-invalid` | Policy cannot be parsed or contains malformed rule entries. |
|
||||
| `policy/policy-hash-mismatch` | Policy does not match configured `expectedHash`. |
|
||||
| `policy/attestation-hash-mismatch` | Current policy evidence no longer matches the accepted attestation. |
|
||||
| `policy/policy-conformance-invalid` | A baseline or checked policy file has invalid comparison syntax. |
|
||||
| `policy/policy-conformance-missing` | A checked policy file is missing a rule required by the baseline policy file. |
|
||||
| `policy/policy-conformance-weaker` | A checked policy file has a weaker value than the baseline policy file. |
|
||||
| `policy/channels-denied-provider` | An enabled channel matches a channel deny rule. |
|
||||
| `policy/mcp-denied-server` | A configured MCP server is denied by policy. |
|
||||
| `policy/mcp-unapproved-server` | A configured MCP server is outside the allowlist. |
|
||||
| `policy/models-denied-provider` | A configured model provider or model ref uses a denied provider. |
|
||||
| `policy/models-unapproved-provider` | A configured model provider or model ref is outside the allowlist. |
|
||||
| `policy/network-private-access-enabled` | A private-network SSRF escape hatch is enabled when policy denies it. |
|
||||
| `policy/ingress-dm-policy-unapproved` | A channel DM policy is outside the policy allowlist. |
|
||||
| `policy/ingress-dm-scope-unapproved` | `session.dmScope` does not match the policy-required DM isolation scope. |
|
||||
| `policy/ingress-open-groups-denied` | A channel group policy is `open` while policy denies open group ingress. |
|
||||
| `policy/ingress-group-mention-required` | A channel or group entry disables mention gates while policy requires them. |
|
||||
| `policy/gateway-non-loopback-bind` | Gateway bind posture permits non-loopback exposure when policy denies it. |
|
||||
| `policy/gateway-auth-disabled` | Gateway authentication is disabled when policy requires auth. |
|
||||
| `policy/gateway-rate-limit-missing` | Gateway auth rate-limit posture is not explicit when policy requires it. |
|
||||
| `policy/gateway-control-ui-insecure` | Gateway Control UI insecure exposure toggles are enabled. |
|
||||
| `policy/gateway-tailscale-funnel` | Gateway Tailscale Funnel exposure is enabled when policy denies it. |
|
||||
| `policy/gateway-remote-enabled` | Gateway remote mode is active when policy denies it. |
|
||||
| `policy/gateway-http-endpoint-enabled` | A Gateway HTTP API endpoint is enabled while denied by policy. |
|
||||
| `policy/gateway-http-url-fetch-unrestricted` | Gateway HTTP URL-fetch input lacks a required URL allowlist. |
|
||||
| `policy/agents-workspace-access-denied` | Agent sandbox mode or workspace access is outside the policy allowlist. |
|
||||
| `policy/agents-tool-not-denied` | An agent or default config does not deny a tool required by policy. |
|
||||
| `policy/tools-profile-unapproved` | A configured global or per-agent tool profile is outside the allowlist. |
|
||||
| `policy/tools-fs-workspace-only-required` | Filesystem tools are not configured with workspace-only path posture. |
|
||||
| `policy/tools-exec-security-unapproved` | Exec security mode is outside the policy allowlist. |
|
||||
| `policy/tools-exec-ask-unapproved` | Exec ask mode is outside the policy allowlist. |
|
||||
| `policy/tools-exec-host-unapproved` | Exec host routing is outside the policy allowlist. |
|
||||
| `policy/tools-elevated-enabled` | Elevated tool mode is enabled when policy denies it. |
|
||||
| `policy/tools-also-allow-missing` | A configured `alsoAllow` list is missing an entry required by policy. |
|
||||
| `policy/tools-also-allow-unexpected` | A configured `alsoAllow` list includes an entry not expected by policy. |
|
||||
| `policy/tools-required-deny-missing` | A global or per-agent tool deny list does not include a required denied tool. |
|
||||
| `policy/sandbox-mode-unapproved` | Sandbox mode is outside the policy allowlist. |
|
||||
| `policy/sandbox-backend-unapproved` | Sandbox backend is outside the policy allowlist. |
|
||||
| `policy/sandbox-container-posture-unobservable` | A container posture rule is enabled for a backend that cannot observe it. |
|
||||
| `policy/sandbox-container-host-network-denied` | A container-backed sandbox or browser uses host network mode. |
|
||||
| `policy/sandbox-container-namespace-join-denied` | A container-backed sandbox or browser joins another container namespace. |
|
||||
| `policy/sandbox-container-mount-mode-required` | A container-backed sandbox or browser mount is not read-only. |
|
||||
| `policy/sandbox-container-runtime-socket-mount` | A container-backed sandbox or browser mount exposes the container runtime socket. |
|
||||
| `policy/sandbox-container-unconfined-profile` | Container sandbox profile is unconfined when policy denies it. |
|
||||
| `policy/sandbox-browser-cdp-source-range-missing` | Sandbox browser CDP source range is missing when policy requires one. |
|
||||
| `policy/secrets-unmanaged-provider` | A config SecretRef references a provider not declared under `secrets.providers`. |
|
||||
| `policy/secrets-denied-provider-source` | A config secret provider or SecretRef uses a source denied by policy. |
|
||||
| `policy/secrets-insecure-provider` | A secret provider opts into insecure posture when policy denies it. |
|
||||
| `policy/auth-profile-invalid-metadata` | A config auth profile is missing valid provider or mode metadata. |
|
||||
| `policy/auth-profile-unapproved-mode` | A config auth profile mode is outside the policy allowlist. |
|
||||
| `policy/tools-missing-risk-level` | A governed tool declaration is missing risk metadata. |
|
||||
| `policy/tools-unknown-risk-level` | A governed tool declaration uses an unknown risk value. |
|
||||
| `policy/tools-missing-sensitivity-token` | A governed tool declaration is missing sensitivity metadata. |
|
||||
| `policy/tools-missing-owner` | A governed tool declaration is missing owner metadata. |
|
||||
| `policy/tools-unknown-sensitivity-token` | A governed tool declaration uses an unknown sensitivity value. |
|
||||
| Check id | Finding |
|
||||
| -------------------------------------------- | -------------------------------------------------------------------------------- |
|
||||
| `policy/policy-jsonc-missing` | Policy is enabled but `policy.jsonc` is missing. |
|
||||
| `policy/policy-jsonc-invalid` | Policy cannot be parsed or contains malformed rule entries. |
|
||||
| `policy/policy-hash-mismatch` | Policy does not match configured `expectedHash`. |
|
||||
| `policy/attestation-hash-mismatch` | Current policy evidence no longer matches the accepted attestation. |
|
||||
| `policy/channels-denied-provider` | An enabled channel matches a channel deny rule. |
|
||||
| `policy/mcp-denied-server` | A configured MCP server is denied by policy. |
|
||||
| `policy/mcp-unapproved-server` | A configured MCP server is outside the allowlist. |
|
||||
| `policy/models-denied-provider` | A configured model provider or model ref uses a denied provider. |
|
||||
| `policy/models-unapproved-provider` | A configured model provider or model ref is outside the allowlist. |
|
||||
| `policy/network-private-access-enabled` | A private-network SSRF escape hatch is enabled when policy denies it. |
|
||||
| `policy/gateway-non-loopback-bind` | Gateway bind posture permits non-loopback exposure when policy denies it. |
|
||||
| `policy/gateway-auth-disabled` | Gateway authentication is disabled when policy requires auth. |
|
||||
| `policy/gateway-rate-limit-missing` | Gateway auth rate-limit posture is not explicit when policy requires it. |
|
||||
| `policy/gateway-control-ui-insecure` | Gateway Control UI insecure exposure toggles are enabled. |
|
||||
| `policy/gateway-tailscale-funnel` | Gateway Tailscale Funnel exposure is enabled when policy denies it. |
|
||||
| `policy/gateway-remote-enabled` | Gateway remote mode is active when policy denies it. |
|
||||
| `policy/gateway-http-endpoint-enabled` | A Gateway HTTP API endpoint is enabled while denied by policy. |
|
||||
| `policy/gateway-http-url-fetch-unrestricted` | Gateway HTTP URL-fetch input lacks a required URL allowlist. |
|
||||
| `policy/agents-workspace-access-denied` | Agent sandbox mode or workspace access is outside the policy allowlist. |
|
||||
| `policy/agents-tool-not-denied` | An agent or default config does not deny a tool required by policy. |
|
||||
| `policy/tools-profile-unapproved` | A configured global or per-agent tool profile is outside the allowlist. |
|
||||
| `policy/tools-fs-workspace-only-required` | Filesystem tools are not configured with workspace-only path posture. |
|
||||
| `policy/tools-exec-security-unapproved` | Exec security mode is outside the policy allowlist. |
|
||||
| `policy/tools-exec-ask-unapproved` | Exec ask mode is outside the policy allowlist. |
|
||||
| `policy/tools-exec-host-unapproved` | Exec host routing is outside the policy allowlist. |
|
||||
| `policy/tools-elevated-enabled` | Elevated tool mode is enabled when policy denies it. |
|
||||
| `policy/tools-also-allow-missing` | A configured `alsoAllow` list is missing an entry required by policy. |
|
||||
| `policy/tools-also-allow-unexpected` | A configured `alsoAllow` list includes an entry not expected by policy. |
|
||||
| `policy/tools-required-deny-missing` | A global or per-agent tool deny list does not include a required denied tool. |
|
||||
| `policy/secrets-unmanaged-provider` | A config SecretRef references a provider not declared under `secrets.providers`. |
|
||||
| `policy/secrets-denied-provider-source` | A config secret provider or SecretRef uses a source denied by policy. |
|
||||
| `policy/secrets-insecure-provider` | A secret provider opts into insecure posture when policy denies it. |
|
||||
| `policy/auth-profile-invalid-metadata` | A config auth profile is missing valid provider or mode metadata. |
|
||||
| `policy/auth-profile-unapproved-mode` | A config auth profile mode is outside the policy allowlist. |
|
||||
| `policy/tools-missing-risk-level` | A governed tool declaration is missing risk metadata. |
|
||||
| `policy/tools-unknown-risk-level` | A governed tool declaration uses an unknown risk value. |
|
||||
| `policy/tools-missing-sensitivity-token` | A governed tool declaration is missing sensitivity metadata. |
|
||||
| `policy/tools-missing-owner` | A governed tool declaration is missing owner metadata. |
|
||||
| `policy/tools-unknown-sensitivity-token` | A governed tool declaration uses an unknown sensitivity value. |
|
||||
|
||||
Policy findings can include both `target` and `requirement`. `target` is the
|
||||
observed workspace thing that does not conform. `requirement` is the authored
|
||||
@@ -874,11 +706,10 @@ configured channel:
|
||||
|
||||
## Exit codes
|
||||
|
||||
| Command | `0` | `1` | `2` |
|
||||
| ---------------- | ------------------------------------------------------ | ------------------------------------------------------------------- | ---------------------------- |
|
||||
| `policy check` | No findings at the threshold. | One or more findings met the threshold. | Argument or runtime failure. |
|
||||
| `policy compare` | The policy file is at least as strict as the baseline. | The policy file is invalid, missing, or weaker than baseline rules. | Argument or runtime failure. |
|
||||
| `policy watch` | No findings and accepted hash is current. | Findings exist or accepted attestation is stale. | Argument or runtime failure. |
|
||||
| Command | `0` | `1` | `2` |
|
||||
| -------------- | ----------------------------------------- | ------------------------------------------------ | ---------------------------- |
|
||||
| `policy check` | No findings at the threshold. | One or more findings met the threshold. | Argument or runtime failure. |
|
||||
| `policy watch` | No findings and accepted hash is current. | Findings exist or accepted attestation is stale. | Argument or runtime failure. |
|
||||
|
||||
## Related
|
||||
|
||||
|
||||
@@ -43,7 +43,6 @@ Notes:
|
||||
- Local mode uses the embedded agent runtime directly. Most local tools work, but Gateway-only features are unavailable.
|
||||
- Local mode adds `/auth [provider]` inside the TUI command surface.
|
||||
- Plugin approval gates still apply in local mode. Tools that require approval prompt for a decision in the terminal; nothing is silently auto-approved because the Gateway is not involved.
|
||||
- Session [goals](/tools/goal) appear in the footer and can be managed with `/goal`.
|
||||
|
||||
## Examples
|
||||
|
||||
@@ -88,4 +87,3 @@ rerun `openclaw config validate`. See [TUI](/web/tui) and [Config](/cli/config).
|
||||
|
||||
- [CLI reference](/cli)
|
||||
- [TUI](/web/tui)
|
||||
- [Goal](/tools/goal)
|
||||
|
||||
@@ -14,12 +14,12 @@ the finished turn to OpenClaw.
|
||||
Runtimes are easy to confuse with providers because both show up near model
|
||||
configuration. They are different layers:
|
||||
|
||||
| Layer | Examples | What it means |
|
||||
| ------------- | -------------------------------------------- | ------------------------------------------------------------------- |
|
||||
| Provider | `openai`, `anthropic`, `github-copilot` | How OpenClaw authenticates, discovers models, and names model refs. |
|
||||
| Model | `gpt-5.5`, `claude-opus-4-6` | The model selected for the agent turn. |
|
||||
| Agent runtime | `openclaw`, `codex`, `copilot`, `claude-cli` | The low level loop or backend that executes the prepared turn. |
|
||||
| Channel | Telegram, Discord, Slack, WhatsApp | Where messages enter and leave OpenClaw. |
|
||||
| Layer | Examples | What it means |
|
||||
| ------------- | ------------------------------------- | ------------------------------------------------------------------- |
|
||||
| Provider | `openai`, `anthropic`, `openai-codex` | How OpenClaw authenticates, discovers models, and names model refs. |
|
||||
| Model | `gpt-5.5`, `claude-opus-4-6` | The model selected for the agent turn. |
|
||||
| Agent runtime | `openclaw`, `codex`, `claude-cli` | The low level loop or backend that executes the prepared turn. |
|
||||
| Channel | Telegram, Discord, Slack, WhatsApp | Where messages enter and leave OpenClaw. |
|
||||
|
||||
You will also see the word **harness** in code. A harness is the implementation
|
||||
that provides an agent runtime. For example, the bundled Codex harness
|
||||
@@ -33,17 +33,13 @@ There are two runtime families:
|
||||
|
||||
- **Embedded harnesses** run inside OpenClaw's prepared agent loop. Today this
|
||||
is the built-in `openclaw` runtime plus registered plugin harnesses such as
|
||||
`codex` and `copilot`.
|
||||
`codex`.
|
||||
- **CLI backends** run a local CLI process while keeping the model ref
|
||||
canonical. For example, `anthropic/claude-opus-4-8` with
|
||||
canonical. For example, `anthropic/claude-opus-4-7` with
|
||||
a model-scoped `agentRuntime.id: "claude-cli"` means "select the Anthropic
|
||||
model, execute through Claude CLI." `claude-cli` is not an embedded harness id
|
||||
and must not be passed to AgentHarness selection.
|
||||
|
||||
The `copilot` harness is a separate, opt-in external plugin harness for the
|
||||
GitHub Copilot CLI; see [GitHub Copilot agent runtime](/plugins/copilot)
|
||||
for the user-facing decision between PI, Codex, and GitHub Copilot agent runtime.
|
||||
|
||||
## Codex surfaces
|
||||
|
||||
Most confusion comes from several different surfaces sharing the Codex name:
|
||||
@@ -51,7 +47,7 @@ Most confusion comes from several different surfaces sharing the Codex name:
|
||||
| Surface | OpenClaw name/config | What it does |
|
||||
| ------------------------------------------------ | ------------------------------------ | -------------------------------------------------------------------------------------------------------------- |
|
||||
| Native Codex app-server runtime | `openai/*` model refs | Runs OpenAI embedded agent turns through Codex app-server. This is the usual ChatGPT/Codex subscription setup. |
|
||||
| Codex OAuth auth profiles | `openai` OAuth profiles | Stores ChatGPT/Codex subscription auth that the Codex app-server harness consumes. |
|
||||
| Codex OAuth auth profiles | `openai-codex` auth provider | Stores ChatGPT/Codex subscription auth that the Codex app-server harness consumes. |
|
||||
| Codex ACP adapter | `runtime: "acp"`, `agentId: "codex"` | Runs Codex through the external ACP/acpx control plane. Use only when ACP/acpx is explicitly asked. |
|
||||
| Native Codex chat-control command set | `/codex ...` | Binds, resumes, steers, stops, and inspects Codex app-server threads from chat. |
|
||||
| OpenAI Platform API route for non-agent surfaces | `openai/*` plus API-key auth | Used for direct OpenAI APIs such as images, embeddings, speech, and realtime. |
|
||||
@@ -95,7 +91,7 @@ This is the agent-facing decision tree:
|
||||
subscription-backed Codex agent experience, use `openai/<model>`.
|
||||
3. If the user explicitly chooses **OpenClaw for an OpenAI model**, keep the model ref
|
||||
as `openai/<model>` and set provider/model runtime policy to
|
||||
`agentRuntime.id: "openclaw"`. A selected `openai` OAuth profile is routed
|
||||
`agentRuntime.id: "openclaw"`. A selected `openai-codex` auth profile is routed
|
||||
internally through OpenClaw's Codex-auth transport.
|
||||
4. If legacy config still contains **`openai-codex/*` model refs**, repair it to
|
||||
`openai/<model>` with `openclaw doctor --fix`; doctor keeps the Codex auth
|
||||
@@ -112,7 +108,7 @@ This is the agent-facing decision tree:
|
||||
| --------------------------------------- | -------------------------------------------- |
|
||||
| Codex app-server chat/thread control | `/codex ...` from the bundled `codex` plugin |
|
||||
| Codex app-server embedded agent runtime | `openai/*` agent model refs |
|
||||
| OpenAI Codex OAuth | `openai` OAuth profiles |
|
||||
| OpenAI Codex OAuth | `openai-codex` auth profiles |
|
||||
| Claude Code or other external harness | ACP/acpx |
|
||||
|
||||
For the OpenAI-family prefix split, see [OpenAI](/providers/openai) and
|
||||
@@ -174,9 +170,9 @@ Claude CLI form is:
|
||||
{
|
||||
agents: {
|
||||
defaults: {
|
||||
model: "anthropic/claude-opus-4-8",
|
||||
model: "anthropic/claude-opus-4-7",
|
||||
models: {
|
||||
"anthropic/claude-opus-4-8": {
|
||||
"anthropic/claude-opus-4-7": {
|
||||
agentRuntime: { id: "claude-cli" },
|
||||
},
|
||||
},
|
||||
@@ -196,7 +192,7 @@ backend.
|
||||
`auto` mode is intentionally conservative for most providers. OpenAI agent
|
||||
models are the exception: unset runtime and `auto` both resolve to the Codex
|
||||
harness. Explicit OpenClaw runtime config remains an opt-in compatibility route for
|
||||
`openai/*` agent turns; when paired with a selected `openai` OAuth profile,
|
||||
`openai/*` agent turns; when paired with a selected `openai-codex` auth profile,
|
||||
OpenClaw routes that path internally through the Codex-auth transport while
|
||||
keeping the public model ref as `openai/*`. Stale OpenAI runtime session pins are
|
||||
ignored by runtime selection and can be cleaned with `openclaw doctor --fix`.
|
||||
@@ -205,34 +201,6 @@ If `openclaw doctor` warns that the `codex` plugin is enabled while
|
||||
`openai-codex/*` remains in config, treat that as legacy route state. Run
|
||||
`openclaw doctor --fix` to rewrite it to `openai/*` with the Codex runtime.
|
||||
|
||||
## GitHub Copilot agent runtime
|
||||
|
||||
The external `@openclaw/copilot` plugin registers an opt-in `copilot` runtime
|
||||
backed by the GitHub Copilot CLI (`@github/copilot-sdk`). It claims the
|
||||
canonical subscription `github-copilot` provider and is **never** selected by
|
||||
`auto`. Opt in per-model or per-provider via `agentRuntime.id`:
|
||||
|
||||
```json5
|
||||
{
|
||||
agents: {
|
||||
defaults: {
|
||||
model: "github-copilot/gpt-5.5",
|
||||
models: {
|
||||
"github-copilot/gpt-5.5": {
|
||||
agentRuntime: { id: "copilot" },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
The harness claims its provider, runtime, CLI session key, and auth profile
|
||||
prefix in `extensions/copilot/doctor-contract-api.ts`, which
|
||||
`openclaw doctor` auto-loads. For configuration, auth, transcript mirroring,
|
||||
compaction, the doctor probe surface, and the broader PI vs Codex vs Copilot
|
||||
SDK decision, see [GitHub Copilot agent runtime](/plugins/copilot).
|
||||
|
||||
## Compatibility contract
|
||||
|
||||
When a runtime is not OpenClaw, it should document what OpenClaw surfaces it supports.
|
||||
@@ -268,7 +236,6 @@ runtime policy first. Legacy session runtime pins no longer decide routing.
|
||||
|
||||
- [Codex harness](/plugins/codex-harness)
|
||||
- [Codex harness runtime](/plugins/codex-harness-runtime)
|
||||
- [GitHub Copilot agent runtime](/plugins/copilot)
|
||||
- [OpenAI](/providers/openai)
|
||||
- [Agent harness plugins](/plugins/sdk-agent-harness)
|
||||
- [Agent loop](/concepts/agent-loop)
|
||||
|
||||
@@ -251,20 +251,6 @@ Native Codex and OpenClaw embedded agent runs satisfy `assemble-before-prompt`.
|
||||
Generic CLI backends do not, so engines that require it are rejected before the
|
||||
CLI process starts.
|
||||
|
||||
### Failure isolation
|
||||
|
||||
OpenClaw isolates the selected plugin engine from the core reply path. If a
|
||||
non-legacy engine is missing, fails contract validation, throws during factory
|
||||
creation, or throws from a lifecycle method, OpenClaw quarantines that engine
|
||||
for the current Gateway process and downgrades context-engine work to the
|
||||
built-in `legacy` engine. The error is logged with the failed operation so the
|
||||
operator can repair, update, or disable the plugin without the agent going
|
||||
silent.
|
||||
|
||||
Host requirement failures are different: when an engine declares that a runtime
|
||||
lacks a required capability, OpenClaw fails closed before starting the run. That
|
||||
protects engines that would corrupt state if they ran in an unsupported host.
|
||||
|
||||
### ownsCompaction
|
||||
|
||||
`ownsCompaction` controls whether OpenClaw runtime's built-in in-attempt auto-compaction stays enabled for the run:
|
||||
@@ -335,7 +321,7 @@ The slot is exclusive at run time - only one registered context engine is resolv
|
||||
|
||||
- Use `openclaw doctor` to verify your engine is loading correctly.
|
||||
- If switching engines, existing sessions continue with their current history. The new engine takes over for future runs.
|
||||
- Engine errors are logged and the selected plugin engine is quarantined for the current Gateway process. OpenClaw falls back to `legacy` for user turns so replies can continue, but you should still repair, update, disable, or uninstall the broken plugin.
|
||||
- Engine errors are logged and surfaced in diagnostics. If a plugin engine fails to register or the selected engine id cannot be resolved, OpenClaw does not fall back automatically; runs fail until you fix the plugin or switch `plugins.slots.contextEngine` back to `"legacy"`.
|
||||
- For development, use `openclaw plugins install -l ./my-engine` to link a local plugin directory without copying.
|
||||
|
||||
## Related
|
||||
|
||||
@@ -229,16 +229,13 @@ All settings live under `plugins.entries.memory-core.config.dreaming`.
|
||||
<ParamField path="model" type="string">
|
||||
Optional Dream Diary subagent model override. Use a canonical `provider/model` value when also setting a subagent `allowedModels` allowlist.
|
||||
</ParamField>
|
||||
<ParamField path="phases.deep.maxPromotedSnippetTokens" type="number" default="160">
|
||||
Maximum estimated token count kept from each short-term recall snippet promoted into `MEMORY.md`. Ranking provenance remains visible.
|
||||
</ParamField>
|
||||
|
||||
<Warning>
|
||||
`dreaming.model` requires `plugins.entries.memory-core.subagent.allowModelOverride: true`. To restrict it, also set `plugins.entries.memory-core.subagent.allowedModels`. Trust or allowlist failures stay visible instead of falling back silently; the retry only covers model-unavailable errors.
|
||||
</Warning>
|
||||
|
||||
<Note>
|
||||
Most phase policy, thresholds, and storage behavior are internal implementation details. See [Memory configuration reference](/reference/memory-config#dreaming) for the full key list.
|
||||
Phase policy, thresholds, and storage behavior are internal implementation details (not user-facing config). See [Memory configuration reference](/reference/memory-config#dreaming) for the full key list.
|
||||
</Note>
|
||||
|
||||
## Dreams UI
|
||||
|
||||
@@ -156,14 +156,15 @@ Use `auth.order.openai` for the user-facing order:
|
||||
{
|
||||
auth: {
|
||||
order: {
|
||||
openai: ["openai:user@example.com", "openai:api-key-backup"],
|
||||
openai: ["openai-codex:user@example.com", "openai:api-key-backup"],
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Use `openai:*` for both ChatGPT/Codex OAuth profiles and OpenAI API-key
|
||||
profiles. When the subscription hits a Codex usage limit,
|
||||
Existing Codex subscription profiles may still use the legacy
|
||||
`openai-codex:*` profile id. The ordered API-key backup can be a normal
|
||||
`openai:*` API-key profile. When the subscription hits a Codex usage limit,
|
||||
OpenClaw records the exact reset time when Codex provides one, tries the next
|
||||
ordered auth profile, and keeps the run inside the Codex harness. Once the reset
|
||||
time passes, the subscription profile is eligible again and the next automatic
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user