Compare commits

..

2 Commits

Author SHA1 Message Date
Dallin Romney
764fc3916e test: probe dependency guard with script change 2026-05-28 18:30:48 -07:00
Dallin Romney
1c33856130 ci: rename dependency guard workflow 2026-05-28 18:23:52 -07:00
2445 changed files with 21138 additions and 137508 deletions

View File

@@ -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.

View File

@@ -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`

View File

@@ -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");

View File

@@ -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

2
.github/CODEOWNERS vendored
View File

@@ -31,7 +31,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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -9,7 +9,6 @@ queries:
paths:
- src
- extensions
- packages/net-policy/src
paths-ignore:
- "**/node_modules"

View File

@@ -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"

View File

@@ -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")

20
.github/labeler.yml vendored
View File

@@ -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/**"
@@ -405,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:

View 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

View File

@@ -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

View File

@@ -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/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/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,7 +1033,7 @@ 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') }}
timeout-minutes: 60
@@ -1190,8 +1139,8 @@ jobs:
permissions:
contents: read
name: ${{ matrix.check_name }}
needs: [preflight]
if: ${{ !cancelled() && always() && needs.preflight.outputs.run_check == 'true' }}
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 || 'blacksmith-4vcpu-ubuntu-2404') || 'ubuntu-24.04') }}
timeout-minutes: 20
strategy:
@@ -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:
@@ -1488,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
@@ -1606,25 +1555,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
@@ -1672,27 +1603,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() {
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 30 ]; 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)
@@ -1792,27 +1703,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() {
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 30 ]; 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
@@ -1858,27 +1749,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() {
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 30 ]; 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
@@ -2113,7 +1984,7 @@ jobs:
- macos-node
- macos-swift
- android
if: ${{ !cancelled() && always() && github.event_name != 'push' && (github.event_name != 'pull_request' || !github.event.pull_request.draft) }}
if: ${{ !cancelled() && always() && (github.event_name != 'pull_request' || !github.event.pull_request.draft) }}
runs-on: ubuntu-24.04
timeout-minutes: 5
steps:

View File

@@ -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

View File

@@ -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

View File

@@ -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 }}

View File

@@ -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')")"

View File

@@ -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 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
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

View File

@@ -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

View File

@@ -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

View File

@@ -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 }}

View File

@@ -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 }}

View File

@@ -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

View File

@@ -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
View File

@@ -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

View File

@@ -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/",

View File

@@ -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.
@@ -228,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`.

View File

@@ -2,20 +2,6 @@
Docs: https://docs.openclaw.ai
## Unreleased
### 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.
### Fixes
- 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.
## 2026.5.28
### Highlights
@@ -42,7 +28,6 @@ Docs: https://docs.openclaw.ai
- 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.

View File

@@ -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

View File

@@ -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
}
}
}

View File

@@ -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))
}

View File

@@ -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)

View File

@@ -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() {

View File

@@ -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()

View File

@@ -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

View File

@@ -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",

View File

@@ -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"
}
}
@@ -2511,7 +2463,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 +2537,7 @@ public struct TasksListParams: Codable, Sendable {
public init(
status: AnyCodable?,
agentid: String? = nil,
agentid: String?,
sessionkey: String?,
limit: Int?,
cursor: String?)
@@ -4775,7 +4727,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 +4764,7 @@ public struct SkillsStatusParams: Codable, Sendable {
public let agentid: String?
public init(
agentid: String? = nil)
agentid: String?)
{
self.agentid = agentid
}
@@ -4827,7 +4779,7 @@ public struct ToolsCatalogParams: Codable, Sendable {
public let includeplugins: Bool?
public init(
agentid: String? = nil,
agentid: String?,
includeplugins: Bool?)
{
self.agentid = agentid
@@ -4961,7 +4913,7 @@ public struct ToolsEffectiveParams: Codable, Sendable {
public let sessionkey: String
public init(
agentid: String? = nil,
agentid: String?,
sessionkey: String)
{
self.agentid = agentid
@@ -5106,7 +5058,7 @@ public struct ToolsInvokeParams: Codable, Sendable {
name: String,
args: [String: AnyCodable]?,
sessionkey: String?,
agentid: String? = nil,
agentid: String?,
confirm: Bool?,
idempotencykey: String?)
{
@@ -5280,7 +5232,7 @@ public struct SkillsSecurityVerdictsParams: Codable, Sendable {
public let agentid: String?
public init(
agentid: String? = nil)
agentid: String?)
{
self.agentid = agentid
}
@@ -5313,7 +5265,7 @@ public struct SkillsSkillCardParams: Codable, Sendable {
public let skillkey: String
public init(
agentid: String? = nil,
agentid: String?,
skillkey: String)
{
self.agentid = agentid
@@ -5450,7 +5402,7 @@ public struct CronJob: Codable, Sendable {
public init(
id: String,
agentid: String? = nil,
agentid: String?,
sessionkey: String?,
name: String,
description: String?,
@@ -5526,7 +5478,7 @@ public struct CronListParams: Codable, Sendable {
lastrunstatus: AnyCodable?,
sortby: AnyCodable?,
sortdir: AnyCodable?,
agentid: String? = nil)
agentid: String?)
{
self.includedisabled = includedisabled
self.limit = limit
@@ -5572,7 +5524,7 @@ public struct CronAddParams: Codable, Sendable {
public init(
name: String,
agentid: AnyCodable? = nil,
agentid: AnyCodable?,
sessionkey: AnyCodable?,
description: String?,
enabled: Bool?,
@@ -5942,8 +5894,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 +5910,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 +5939,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 +5963,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 +6011,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 +6472,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 +6494,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 +6511,6 @@ public struct ChatSendParams: Codable, Sendable {
public init(
sessionkey: String,
agentid: String? = nil,
sessionid: String?,
message: String,
thinking: String?,
@@ -6589,7 +6527,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 +6545,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 +6564,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 +6605,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 +6616,6 @@ public struct ChatDeltaEvent: Codable, Sendable {
public init(
runid: String,
sessionkey: String,
agentid: String? = nil,
spawnedby: String?,
seq: Int,
state: String,
@@ -6700,7 +6626,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 +6638,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 +6651,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 +6661,6 @@ public struct ChatFinalEvent: Codable, Sendable {
public init(
runid: String,
sessionkey: String,
agentid: String? = nil,
spawnedby: String?,
seq: Int,
state: String,
@@ -6748,7 +6670,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 +6681,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 +6693,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 +6702,6 @@ public struct ChatAbortedEvent: Codable, Sendable {
public init(
runid: String,
sessionkey: String,
agentid: String? = nil,
spawnedby: String?,
seq: Int,
state: String,
@@ -6792,7 +6710,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 +6720,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 +6731,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 +6743,6 @@ public struct ChatErrorEvent: Codable, Sendable {
public init(
runid: String,
sessionkey: String,
agentid: String? = nil,
spawnedby: String?,
seq: Int,
state: String,
@@ -6840,7 +6754,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 +6767,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

View 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.

View File

@@ -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,23 +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/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!"],

View File

@@ -1,4 +1,4 @@
ac5e91a6adaf02491d2ff6b983f054c813972da3bf79db68cd1d10887a22c594 config-baseline.json
023e3b85ee79e85f90257e65a1376b1212cf534b6a9cff4b4388c9092e846549 config-baseline.core.json
a9102c0611b8170fac37853cc31771810f31757a9e3b2c6796bbd9625f9b9206 config-baseline.channel.json
2f018852d9682871dd22f0920cafc8994a6c0952e8101229210efa6103ae9536 config-baseline.plugin.json
c61b32fda64ee6cd4d4aa5ed6950c4c681a585d49bf5c127b92e562608a0a303 config-baseline.json
ee4c0f0fb15cda02268f2e83d0c5e1c8d0ec0a2c1b2fdb89cdfce308dadb2b8b config-baseline.core.json
ccb0c68e959854b9d54d66b8c78bfba5fe6f8a37e669e2e7e511b02c4c977122 config-baseline.channel.json
1b763a5524aca2d7ecf1eea38f845ad1ffed5c1b37e85e62f6a7902a3ee0f920 config-baseline.plugin.json

View File

@@ -1,2 +1,2 @@
49a138a9743063067b983c4dd27d047572aef0764c0e5f87a98d91f43d4f8213 plugin-sdk-api-baseline.json
cd7ea2f2b4c1d1d073c3077410d44270244e778f33197567f4127a946cc0f7f7 plugin-sdk-api-baseline.jsonl
91cb45dc1e8aaa3dac9a2c1d3c98c8ff22112e41c305de17f30d0d4420635ee4 plugin-sdk-api-baseline.json
3aa4802ffcb68c4f15e367030994eae10e73b55b5f14c8e23d4e9467fae325fe plugin-sdk-api-baseline.jsonl

View File

@@ -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)"

View File

@@ -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 ~56 times per month instead of 01 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:

View File

@@ -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">

View File

@@ -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).
@@ -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-codex/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.
@@ -1331,7 +1329,7 @@ Default agent-proxy voice-channel session example:
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",
},
},
},
@@ -1379,7 +1375,7 @@ Realtime bidi example:
realtime: {
provider: "openai",
model: "gpt-realtime-2",
speakerVoice: "cedar",
voice: "cedar",
toolPolicy: "safe-read-only",
consultPolicy: "always",
},
@@ -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",
},
},
},
@@ -1437,7 +1433,7 @@ Echo-heavy OpenAI Realtime example:
realtime: {
provider: "openai",
model: "gpt-realtime-2",
speakerVoice: "cedar",
voice: "cedar",
bargeIn: true,
minBargeInAudioEndMs: 500,
consultPolicy: "always",

View File

@@ -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>

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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)

View File

@@ -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`, `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`, `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:
@@ -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" },
},
},
@@ -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)

View File

@@ -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

View File

@@ -30,7 +30,7 @@ Treat them differently from normal config:
## Local model lean mode
`agents.defaults.experimental.localModelLean: true` is a pressure-release valve for weaker local-model setups. When it is on, OpenClaw drops the `browser` and `cron` tools from the agent's tool surface, and it also drops `message` unless the current turn requires message-tool-only visible replies. Nothing else changes. Use `agents.list[].experimental.localModelLean` to enable or disable the same behavior for one configured agent.
`agents.defaults.experimental.localModelLean: true` is a pressure-release valve for weaker local-model setups. When it is on, OpenClaw drops three default tools — `browser`, `cron`, and `message` from the agent's tool surface for every turn. Nothing else changes. Use `agents.list[].experimental.localModelLean` to enable or disable the same behavior for one configured agent.
### Why these three tools
@@ -40,7 +40,7 @@ These three tools have the largest descriptions and the most parameter shapes in
- The model picking the right tool vs. emitting malformed tool calls because there are too many similar-looking schemas.
- The Chat Completions adapter staying inside the server's structured-output limits vs. tripping a 400 on tool-call payload size.
Removing them does not silently rewire OpenClaw — it just makes the tool list shorter. The model still has `read`, `write`, `edit`, `exec`, `apply_patch`, web search/fetch (when configured), memory, and session/agent tools available. When the source reply delivery contract is `message_tool_only`, lean mode keeps `message` so visible replies can still be delivered.
Removing them does not silently rewire OpenClaw — it just makes the tool list shorter. The model still has `read`, `write`, `edit`, `exec`, `apply_patch`, web search/fetch (when configured), memory, and session/agent tools available.
### When to turn it on

View File

@@ -116,7 +116,7 @@ Official provider plugins publish their own model catalog rows. These providers
- CLI: `openclaw onboard --auth-choice apiKey`
- Direct public Anthropic requests support the shared `/fast` toggle and `params.fastMode`, including API-key and OAuth-authenticated traffic sent to `api.anthropic.com`; OpenClaw maps that to Anthropic `service_tier` (`auto` vs `standard_only`)
- Preferred Claude CLI config keeps the model ref canonical and selects the CLI
backend separately: `anthropic/claude-opus-4-8` with
backend separately: `anthropic/claude-opus-4-7` with
model-scoped `agentRuntime.id: "claude-cli"`. Legacy
`claude-cli/claude-opus-4-7` refs still work for compatibility.

View File

@@ -23,7 +23,7 @@ sidebarTitle: "Models CLI"
</Card>
</CardGroup>
Model refs choose a provider and model. They do not usually choose the low-level agent runtime. OpenAI agent refs are the main exception: `openai/gpt-5.5` runs through the Codex app-server runtime by default on the official OpenAI provider. Subscription Copilot refs (`github-copilot/*`) can additionally be opted into the external GitHub Copilot agent runtime plugin — that path stays explicit (no `auto` fallback). Explicit runtime overrides belong on provider/model policy, not on the whole agent or session. In Codex runtime mode, the `openai/gpt-*` ref does not imply API-key billing; auth can come from a Codex account or `openai-codex` auth profile. See [Agent runtimes](/concepts/agent-runtimes) and [GitHub Copilot agent runtime](/plugins/copilot).
Model refs choose a provider and model. They do not usually choose the low-level agent runtime. OpenAI agent refs are the main exception: `openai/gpt-5.5` runs through the Codex app-server runtime by default on the official OpenAI provider. Explicit runtime overrides belong on provider/model policy, not on the whole agent or session. In Codex runtime mode, the `openai/gpt-*` ref does not imply API-key billing; auth can come from a Codex account or `openai-codex` auth profile. See [Agent runtimes](/concepts/agent-runtimes).
## How model selection works
@@ -340,7 +340,7 @@ When live probes run in a TTY, you can select fallbacks interactively. In non-in
## Models registry (`models.json`)
Custom providers in `models.providers` are written into `models.json` under the agent directory (default `~/.openclaw/agents/<agentId>/agent/models.json`). Provider-plugin catalogs are stored as generated plugin-owned catalog shards under the agent's plugin state and loaded automatically. This file is merged by default unless `models.mode` is set to `replace`.
Custom providers in `models.providers` are written into `models.json` under the agent directory (default `~/.openclaw/agents/<agentId>/agent/models.json`). This file is merged by default unless `models.mode` is set to `replace`.
<AccordionGroup>
<Accordion title="Merge mode precedence">

View File

@@ -178,50 +178,6 @@ Progress lines are enabled by default in progress mode. They come from real run
events: tool starts, item updates, task plans, approvals, command output, patch
summaries, and similar agent activity.
Tools can also emit typed progress while a single tool call is still running.
That is how a slow fetch or search can update the visible draft before the tool
returns its final result. The progress update is a partial tool result with
empty model content and explicit public channel metadata:
```json
{
"content": [],
"progress": {
"text": "Fetching page content...",
"visibility": "channel",
"privacy": "public",
"id": "web_fetch:fetching"
}
}
```
OpenClaw renders only the `progress.text` in the channel progress UI. The
normal tool result still arrives later as `content` and `details`, and is the
only part returned to the model.
When adding progress to a tool, use a short, generic message and delay it until
the operation has been pending long enough to be useful:
```typescript
const clearProgressTimer = scheduleToolProgress(
onUpdate,
{ text: "Fetching page content...", id: "web_fetch:fetching" },
5_000,
{ signal },
);
try {
return await runToolWork();
} finally {
clearProgressTimer();
}
```
This pattern means fast calls do not show a progress line, long calls show one
while they are still pending, and canceled calls clear the timer before stale
progress can appear. Progress text is a public UI side channel, so it must not
include secrets, raw arguments, fetched content, command output, or page text.
OpenClaw uses the same formatter for progress drafts and `/verbose`:
```json5

View File

@@ -266,10 +266,6 @@ The doctor checks Convex broker env, validates endpoint settings, and verifies a
Live transport lanes share one contract instead of each inventing their own scenario list shape. `qa-channel` is the broad synthetic product-behavior suite and is not part of the live transport coverage matrix.
Live transport runners should import the shared scenario ids, baseline
coverage helpers, and scenario-selection helper from
`openclaw/plugin-sdk/qa-live-transport-scenarios`.
| Lane | Canary | Mention gating | Bot-to-bot | Allowlist block | Top-level reply | Restart resume | Thread follow-up | Thread isolation | Reaction observation | Help command | Native command registration |
| -------- | ------ | -------------- | ---------- | --------------- | --------------- | -------------- | ---------------- | ---------------- | -------------------- | ------------ | --------------------------- |
| Matrix | x | x | x | x | x | x | x | x | x | | |
@@ -893,13 +889,13 @@ pnpm openclaw qa character-eval \
--model openai/gpt-5.5,thinking=medium,fast \
--model openai/gpt-5.2,thinking=xhigh \
--model openai/gpt-5,thinking=xhigh \
--model anthropic/claude-opus-4-8,thinking=high \
--model anthropic/claude-opus-4-7,thinking=high \
--model anthropic/claude-sonnet-4-6,thinking=high \
--model zai/glm-5.1,thinking=high \
--model moonshot/kimi-k2.5,thinking=high \
--model google/gemini-3.1-pro-preview,thinking=high \
--judge-model openai/gpt-5.5,thinking=xhigh,fast \
--judge-model anthropic/claude-opus-4-8,thinking=high \
--judge-model anthropic/claude-opus-4-7,thinking=high \
--blind-judge-models \
--concurrency 16 \
--judge-concurrency 16
@@ -930,13 +926,13 @@ Candidate and judge model runs both default to concurrency 16. Lower
`--concurrency` or `--judge-concurrency` when provider limits or local gateway
pressure make a run too noisy.
When no candidate `--model` is passed, the character eval defaults to
`openai/gpt-5.5`, `openai/gpt-5.2`, `openai/gpt-5`, `anthropic/claude-opus-4-8`,
`openai/gpt-5.5`, `openai/gpt-5.2`, `openai/gpt-5`, `anthropic/claude-opus-4-7`,
`anthropic/claude-sonnet-4-6`, `zai/glm-5.1`,
`moonshot/kimi-k2.5`, and
`google/gemini-3.1-pro-preview` when no `--model` is passed.
When no `--judge-model` is passed, the judges default to
`openai/gpt-5.5,thinking=xhigh,fast` and
`anthropic/claude-opus-4-8,thinking=high`.
`anthropic/claude-opus-4-7,thinking=high`.
## Related docs

View File

@@ -197,12 +197,6 @@ Matrix:
Preview streaming can also include **tool-progress** updates - short status lines like "searching the web", "reading file", or "calling tool" - that appear in the same preview message while tools are running, ahead of the final reply. In Codex app-server mode, Codex preamble/commentary messages use this same preview path, so short "I am checking..." progress notes can stream into the editable draft without becoming part of the final answer. This keeps multi-step tool turns visually alive rather than silent between the first thinking preview and the final answer.
Long-running tools may emit typed progress before they return. For example,
`web_fetch` arms a five-second timer when it starts: if the fetch is still
pending, the preview can show `Fetching page content...`; if the fetch finishes
or is canceled before then, no progress line is emitted. The later final tool
result is still delivered normally to the model.
Supported surfaces:
- **Discord**, **Slack**, **Telegram**, and **Matrix** stream tool-progress and Codex preamble updates into the live preview edit by default when preview streaming is active. Microsoft Teams uses its native progress stream in personal chats.

View File

@@ -53,8 +53,8 @@ Authoritative advertised **discovery** inventory lives in
## Where the schemas live
- Source: `packages/gateway-protocol/src/schema.ts`
- Runtime validators (AJV): `packages/gateway-protocol/src/index.ts`
- Source: `src/gateway/protocol/schema.ts`
- Runtime validators (AJV): `src/gateway/protocol/index.ts`
- Advertised feature/discovery registry: `src/gateway/server-methods-list.ts`
- Server handshake + method dispatch: `src/gateway/server.impl.ts`
- Node client: `src/gateway/client.ts`
@@ -195,7 +195,7 @@ Example: add a new `system.echo` request that returns `{ ok: true, text }`.
1. **Schema (source of truth)**
Add to `packages/gateway-protocol/src/schema.ts`:
Add to `src/gateway/protocol/schema.ts`:
```ts
export const SystemEchoParamsSchema = Type.Object(
@@ -223,7 +223,7 @@ export type SystemEchoResult = Static<typeof SystemEchoResultSchema>;
2. **Validation**
In `packages/gateway-protocol/src/index.ts`, export an AJV validator:
In `src/gateway/protocol/index.ts`, export an AJV validator:
```ts
export const validateSystemEchoParams = ajv.compile<SystemEchoParams>(SystemEchoParamsSchema);
@@ -272,7 +272,7 @@ Unknown frame types are preserved as raw payloads for forward compatibility.
## Versioning + compatibility
- `PROTOCOL_VERSION` lives in `packages/gateway-protocol/src/version.ts`.
- `PROTOCOL_VERSION` lives in `src/gateway/protocol/version.ts`.
- Clients send `minProtocol` + `maxProtocol`; the server rejects ranges that
do not include its current protocol.
- The Swift models keep unknown frame types to avoid breaking older clients.

View File

@@ -1228,7 +1228,6 @@
"plugins/codex-native-plugins",
"plugins/codex-computer-use",
"plugins/google-meet",
"plugins/workboard",
"plugins/webhooks",
"plugins/admin-http-rpc",
"plugins/voice-call",
@@ -1333,7 +1332,6 @@
"group": "Agent coordination",
"pages": [
"tools/agent-send",
"tools/goal",
"tools/steer",
"tools/subagents",
"tools/acp-agents",

View File

@@ -334,7 +334,7 @@ Higher values preserve more visual detail.
Image-tool compression/detail preference for images loaded from file paths, URLs, and media references.
Default: `auto`.
OpenClaw adapts the resize ladder to the selected image model. For example, Claude Opus 4.8, OpenAI GPT-5.5, Qwen VL, and hosted Llama 4 vision models can use larger images than older/default high-detail vision paths, while multi-image turns are compressed more aggressively in `auto` mode to control token and latency cost.
OpenClaw adapts the resize ladder to the selected image model. For example, Claude Opus 4.7, OpenAI GPT-5.5, Qwen VL, and hosted Llama 4 vision models can use larger images than older/default high-detail vision paths, while multi-image turns are compressed more aggressively in `auto` mode to control token and latency cost.
Values:
@@ -483,7 +483,7 @@ Time format in system prompt. Default: `auto` (OS preference).
defaults: {
model: "openai/gpt-5.5",
models: {
"anthropic/claude-opus-4-8": {
"anthropic/claude-opus-4-7": {
agentRuntime: { id: "claude-cli" },
},
"vllm/*": {
@@ -501,7 +501,7 @@ Time format in system prompt. Default: `auto` (OS preference).
- Runtime precedence is exact model policy first (`agents.list[].models["provider/model"]`, `agents.defaults.models["provider/model"]`, or `models.providers.<provider>.models[]`), then `agents.list[]` / `agents.defaults.models["provider/*"]`, then provider-wide policy at `models.providers.<provider>.agentRuntime`.
- Whole-agent runtime keys are legacy. `agents.defaults.agentRuntime`, `agents.list[].agentRuntime`, session runtime pins, and `OPENCLAW_AGENT_RUNTIME` are ignored by runtime selection. Run `openclaw doctor --fix` to remove stale values.
- OpenAI agent models use the Codex harness by default; provider/model `agentRuntime.id: "codex"` remains valid when you want to make that explicit.
- For Claude CLI deployments, prefer `model: "anthropic/claude-opus-4-8"` plus model-scoped `agentRuntime.id: "claude-cli"`. Legacy `claude-cli/claude-opus-4-7` model refs still work for compatibility, but new config should keep provider/model selection canonical and put the execution backend in provider/model runtime policy.
- For Claude CLI deployments, prefer `model: "anthropic/claude-opus-4-7"` plus model-scoped `agentRuntime.id: "claude-cli"`. Legacy `claude-cli/claude-opus-4-7` model refs still work for compatibility, but new config should keep provider/model selection canonical and put the execution backend in provider/model runtime policy.
- This only controls text agent-turn execution. Media generation, vision, PDF, music, video, and TTS still use their provider/model settings.
**Built-in alias shorthands** (only apply when the model is in `agents.defaults.models`):
@@ -521,7 +521,7 @@ Your configured aliases always win over defaults.
Z.AI GLM-4.x models automatically enable thinking mode unless you set `--thinking off` or define `agents.defaults.models["zai/<model>"].params.thinking` yourself.
Z.AI models enable `tool_stream` by default for tool call streaming. Set `agents.defaults.models["zai/<model>"].params.tool_stream` to `false` to disable it.
Anthropic Claude Opus 4.8 keeps thinking off by default in OpenClaw; when adaptive thinking is explicitly enabled, Anthropic's provider-owned effort default is `high`. Claude 4.6 models default to `adaptive` when no explicit thinking level is set.
Anthropic Claude 4.6 models default to `adaptive` thinking when no explicit thinking level is set.
### `agents.defaults.cliBackends`
@@ -617,7 +617,7 @@ Periodic heartbeat runs.
- `every`: duration string (ms/s/m/h). Default: `30m` (API-key auth) or `1h` (OAuth auth). Set to `0m` to disable.
- `includeSystemPromptSection`: when false, omits the Heartbeat section from the system prompt and skips `HEARTBEAT.md` injection into bootstrap context. Default: `true`.
- `suppressToolErrorWarnings`: when true, suppresses tool error warning payloads during heartbeat runs.
- `timeoutSeconds`: maximum time in seconds allowed for a heartbeat agent turn before it is aborted. Leave unset to use `agents.defaults.timeoutSeconds` when set, otherwise the heartbeat cadence capped at 600 seconds.
- `timeoutSeconds`: maximum time in seconds allowed for a heartbeat agent turn before it is aborted. Leave unset to use `agents.defaults.timeoutSeconds`.
- `directPolicy`: direct/DM delivery policy. `allow` (default) permits direct-target delivery. `block` suppresses direct-target delivery and emits `reason=dm-blocked`.
- `lightContext`: when true, heartbeat runs use lightweight bootstrap context and keep only `HEARTBEAT.md` from workspace bootstrap files.
- `isolatedSession`: when true, each heartbeat runs in a fresh session with no prior conversation history. Same isolation pattern as cron `sessionTarget: "isolated"`. Reduces per-heartbeat token cost from ~100K to ~2-5K tokens.
@@ -1056,7 +1056,7 @@ for provider examples and precedence.
params: { cacheRetention: "none" }, // overrides matching defaults.models params by key
tts: {
providers: {
elevenlabs: { speakerVoiceId: "EXAVITQu4vr4xnSDxMaL" },
elevenlabs: { voiceId: "EXAVITQu4vr4xnSDxMaL" },
},
},
skills: ["docs-search"], // replaces agents.defaults.skills when set
@@ -1415,7 +1415,7 @@ Batches rapid text-only messages from the same sender into a single agent turn.
elevenlabs: {
apiKey: "elevenlabs_api_key",
baseUrl: "https://api.elevenlabs.io",
speakerVoiceId: "voice_id",
voiceId: "voice_id",
modelId: "eleven_multilingual_v2",
seed: 42,
applyTextNormalization: "auto",
@@ -1429,7 +1429,7 @@ Batches rapid text-only messages from the same sender into a single agent turn.
},
},
microsoft: {
speakerVoice: "en-US-AvaMultilingualNeural",
voice: "en-US-AvaMultilingualNeural",
lang: "en-US",
outputFormat: "audio-24khz-48kbitrate-mono-mp3",
},
@@ -1437,7 +1437,7 @@ Batches rapid text-only messages from the same sender into a single agent turn.
apiKey: "openai_api_key",
baseUrl: "https://api.openai.com/v1",
model: "gpt-4o-mini-tts",
speakerVoice: "alloy",
voice: "alloy",
},
},
},
@@ -1465,7 +1465,7 @@ Defaults for Talk mode (macOS/iOS/Android).
provider: "elevenlabs",
providers: {
elevenlabs: {
speakerVoiceId: "elevenlabs_voice_id",
voiceId: "elevenlabs_voice_id",
voiceAliases: {
Clawd: "EXAVITQu4vr4xnSDxMaL",
Roger: "CwhRBWXzGAHq8TQ4Fs17",
@@ -1489,7 +1489,7 @@ Defaults for Talk mode (macOS/iOS/Android).
providers: {
openai: {
model: "gpt-realtime-2",
speakerVoice: "cedar",
voice: "cedar",
},
},
instructions: "Speak warmly and keep answers brief.",

View File

@@ -386,13 +386,12 @@ Controls inline attachment support for `sessions_spawn`.
<AccordionGroup>
<Accordion title="Attachment notes">
- Attachments require `enabled: true`.
- Subagent attachments are materialized into the child workspace at `.openclaw/attachments/<uuid>/` with a `.manifest.json`.
- ACP attachments are image-only and forwarded inline to the ACP runtime after the same file count, per-file byte, and total byte limits pass.
- Attachments are only supported for `runtime: "subagent"`. ACP runtime rejects them.
- Files are materialized into the child workspace at `.openclaw/attachments/<uuid>/` with a `.manifest.json`.
- Attachment content is automatically redacted from transcript persistence.
- Base64 inputs are validated with strict alphabet/padding checks and a pre-decode size guard.
- Subagent attachment file permissions are `0700` for directories and `0600` for files.
- Subagent cleanup follows the `cleanup` policy: `delete` always removes attachments; `keep` retains them only when `retainOnSessionKeep: true`.
- File permissions are `0700` for directories and `0600` for files.
- Cleanup follows the `cleanup` policy: `delete` always removes attachments; `keep` retains them only when `retainOnSessionKeep: true`.
</Accordion>
</AccordionGroup>
@@ -489,8 +488,7 @@ Configuring a custom/local provider `baseUrl` is also the narrow network trust d
- Empty or missing agent `apiKey`/`baseUrl` fall back to `models.providers` in config.
- Matching model `contextWindow`/`maxTokens` use the higher value between explicit config and implicit catalog values.
- Matching model `contextTokens` preserves an explicit runtime cap when present; use it to limit effective context without changing native model metadata.
- Provider-plugin catalogs are stored as generated plugin-owned catalog shards under the agent's plugin state.
- Use `models.mode: "replace"` when you want config to fully rewrite `models.json` and active plugin catalog shards.
- Use `models.mode: "replace"` when you want config to fully rewrite `models.json`.
- Marker persistence is source-authoritative: markers are written from the active source config snapshot (pre-resolution), not from resolved runtime secret values.
</Accordion>

View File

@@ -1268,11 +1268,11 @@ Current builds no longer include the TCP bridge. Nodes connect over the Gateway
}
```
- `maxAttempts`: maximum retries for cron jobs on transient errors (default: `3`; range: `0`-`10`).
- `maxAttempts`: maximum retries for one-shot jobs on transient errors (default: `3`; range: `0`-`10`).
- `backoffMs`: array of backoff delays in ms for each retry attempt (default: `[30000, 60000, 300000]`; 1-10 entries).
- `retryOn`: error types that trigger retries - `"rate_limit"`, `"overloaded"`, `"network"`, `"timeout"`, `"server_error"`. Omit to retry all transient types.
One-shot jobs stay enabled until retry attempts are exhausted, then disable while keeping the final error state. Recurring jobs use the same transient retry policy to run again after backoff before their next scheduled slot; permanent errors or exhausted transient retries fall back to the normal recurring schedule with error backoff.
Applies only to one-shot cron jobs. Recurring jobs use separate failure handling.
### `cron.failureAlert`

View File

@@ -337,9 +337,9 @@ candidate contains redacted secret placeholders such as `***`.
</Accordion>
<Accordion title="Enable relay-backed push for official iOS builds">
Relay-backed push uses the hosted OpenClaw relay by default: `https://ios-push-relay.openclaw.ai`.
Relay-backed push is configured in `openclaw.json`.
To use a custom relay, set this in gateway config:
Set this in gateway config:
```json5
{
@@ -373,8 +373,8 @@ candidate contains redacted secret placeholders such as `***`.
End-to-end flow:
1. Install an official/TestFlight iOS build.
2. Optional: configure `gateway.push.apns.relay.baseUrl` on the gateway only when using a custom relay deployment.
1. Install an official/TestFlight iOS build that was compiled with the same relay base URL.
2. Configure `gateway.push.apns.relay.baseUrl` on the gateway.
3. Pair the iOS app to the gateway and let both node and operator sessions connect.
4. The iOS app fetches the gateway identity, registers with the relay using App Attest plus the app receipt, and then publishes the relay-backed `push.apns.register` payload to the paired gateway.
5. The gateway stores the relay handle and send grant, then uses them for `push.test`, wake nudges, and reconnect wakes.
@@ -387,7 +387,6 @@ candidate contains redacted secret placeholders such as `***`.
Compatibility note:
- `OPENCLAW_APNS_RELAY_BASE_URL` and `OPENCLAW_APNS_RELAY_TIMEOUT_MS` still work as temporary env overrides.
- Custom gateway relay URLs must match the relay base URL baked into the official/TestFlight iOS build.
- `OPENCLAW_APNS_RELAY_ALLOW_HTTP=true` remains a loopback-only development escape hatch; do not persist HTTP relay URLs in config.
See [iOS App](/platforms/ios#relay-backed-push-for-official-builds) for the end-to-end flow and [Authentication and trust flow](/platforms/ios#authentication-and-trust-flow) for the relay security model.

View File

@@ -264,7 +264,6 @@ That stages grounded durable candidates into the short-term dreaming store while
- `routing.transcribeAudio` → `tools.media.audio.models`
- `messages.tts.<provider>` (`openai`/`elevenlabs`/`microsoft`/`edge`) → `messages.tts.providers.<provider>`
- `messages.tts.provider: "edge"` and `messages.tts.providers.edge` → `messages.tts.provider: "microsoft"` and `messages.tts.providers.microsoft`
- TTS speaker selection fields (`voice`/`voiceName`/`voiceId`) → `speakerVoice`/`speakerVoiceId`
- `channels.discord.voice.tts.<provider>` (`openai`/`elevenlabs`/`microsoft`/`edge`) → `channels.discord.voice.tts.providers.<provider>`
- `channels.discord.accounts.<id>.voice.tts.<provider>` (`openai`/`elevenlabs`/`microsoft`/`edge`) → `channels.discord.accounts.<id>.voice.tts.providers.<provider>`
- `plugins.entries.voice-call.config.tts.<provider>` (`openai`/`elevenlabs`/`microsoft`/`edge`) → `plugins.entries.voice-call.config.tts.providers.<provider>`
@@ -374,15 +373,13 @@ That stages grounded durable candidates into the short-term dreaming store while
- payload `provider` delivery aliases → explicit `delivery.channel`
- simple legacy `notify: true` webhook fallback jobs → explicit `delivery.mode="webhook"` with `delivery.to=cron.webhook`
The Gateway also sanitizes malformed cron rows at load time so valid jobs keep running. Raw malformed rows are copied to `jobs-quarantine.json` next to the active store before they are removed from `jobs.json`; doctor reports quarantined rows so you can review or repair them manually.
Doctor only auto-migrates `notify: true` jobs when it can do so without changing behavior. If a job combines legacy notify fallback with an existing non-webhook delivery mode, doctor warns and leaves that job for manual review.
On Linux, doctor also warns when the user's crontab still invokes legacy `~/.openclaw/bin/ensure-whatsapp.sh`. That host-local script is not maintained by current OpenClaw and can write false `Gateway inactive` messages to `~/.openclaw/logs/whatsapp-health.log` when cron cannot reach the systemd user bus. Remove the stale crontab entry with `crontab -e`; use `openclaw channels status --probe`, `openclaw doctor`, and `openclaw gateway status` for current health checks.
</Accordion>
<Accordion title="3c. Session lock cleanup">
Doctor scans every agent session directory for stale write-lock files — files left behind when a session exited abnormally. For each lock file found it reports: the path, PID, whether the PID is still alive, lock age, and whether it is considered stale (dead PID, malformed owner metadata, older than 30 minutes, or a live PID that can be proven to belong to a non-OpenClaw process). In `--fix` / `--repair` mode it removes locks with dead, orphaned, recycled, malformed-old, or non-OpenClaw owners automatically. Old locks that are still owned by a live OpenClaw process are reported but left in place so doctor does not cut off an active transcript writer.
Doctor scans every agent session directory for stale write-lock files — files left behind when a session exited abnormally. For each lock file found it reports: the path, PID, whether the PID is still alive, lock age, and whether it is considered stale (dead PID, older than 30 minutes, or a live PID that can be proven to belong to a non-OpenClaw process). In `--fix` / `--repair` mode it removes stale lock files automatically; otherwise it prints a note and instructs you to rerun with `--fix`.
</Accordion>
<Accordion title="3d. Session transcript branch repair">
Doctor scans agent session JSONL files for the duplicated branch shape created by the 2026.4.24 prompt transcript rewrite bug: an abandoned user turn with OpenClaw internal runtime context plus an active sibling containing the same visible user prompt. In `--fix` / `--repair` mode, doctor backs up each affected file next to the original and rewrites the transcript to the active branch so gateway history and memory readers no longer see duplicate turns.

View File

@@ -63,7 +63,6 @@ Example config:
- Interval: `30m` (or `1h` when Anthropic OAuth/token auth is the detected auth mode, including Claude CLI reuse). Set `agents.defaults.heartbeat.every` or per-agent `agents.list[].heartbeat.every`; use `0m` to disable.
- Prompt body (configurable via `agents.defaults.heartbeat.prompt`): `Read HEARTBEAT.md if it exists (workspace context). Follow it strictly. Do not infer or repeat old tasks from prior chats. If nothing needs attention, reply HEARTBEAT_OK.`
- Timeout: unset heartbeat turns use `agents.defaults.timeoutSeconds` when set. Otherwise, they use the heartbeat cadence capped at 600 seconds. Set `agents.defaults.heartbeat.timeoutSeconds` or per-agent `agents.list[].heartbeat.timeoutSeconds` for longer heartbeat work.
- The heartbeat prompt is sent **verbatim** as the user message. The system prompt includes a "Heartbeat" section only when heartbeats are enabled for the default agent, and the run is flagged internally.
- When heartbeats are disabled with `0m`, normal runs also omit `HEARTBEAT.md` from bootstrap context so the model does not see heartbeat-only instructions.
- Active hours (`heartbeat.activeHours`) are checked in the configured timezone. Outside the window, heartbeats are skipped until the next tick inside the window.
@@ -275,10 +274,6 @@ Use `accountId` to target a specific account on multi-account channels like Tele
<ParamField path="suppressToolErrorWarnings" type="boolean">
When true, suppresses tool error warning payloads during heartbeat runs.
</ParamField>
<ParamField path="timeoutSeconds" type="number" default="global timeout or min(every, 600)">
Maximum seconds allowed for a heartbeat agent turn before it is aborted. Leave unset to use `agents.defaults.timeoutSeconds` when set, otherwise the heartbeat cadence capped at 600 seconds.
</ParamField>
<ParamField path="activeHours" type="object">
Restricts heartbeat runs to a time window. Object with `start` (HH:MM, inclusive; use `00:00` for start-of-day), `end` (HH:MM exclusive; `24:00` allowed for end-of-day), and optional `timezone`.

View File

@@ -315,7 +315,7 @@ If the model loads cleanly but full agent turns misbehave, work top-down — con
openclaw infer model run --gateway --model <provider/model> --prompt "Reply with exactly: pong" --json
```
3. **Try lean mode.** If both probes pass but real agent turns fail with malformed tool calls or oversized prompts, enable `agents.defaults.experimental.localModelLean: true`. It drops the heaviest default tools (`browser`, `cron`, and usually `message`) so the prompt shape is smaller and less brittle. Lean mode keeps `message` when the current source reply contract requires message-tool delivery. See [Experimental Features → Local model lean mode](/concepts/experimental-features#local-model-lean-mode) for the full explanation, when to use it, and how to confirm it is on.
3. **Try lean mode.** If both probes pass but real agent turns fail with malformed tool calls or oversized prompts, enable `agents.defaults.experimental.localModelLean: true`. It drops the three heaviest default tools (`browser`, `cron`, `message`) so the prompt shape is smaller and less brittle. See [Experimental Features → Local model lean mode](/concepts/experimental-features#local-model-lean-mode) for the full explanation, when to use it, and how to confirm it is on.
4. **Disable tools entirely as a last resort.** If lean mode is not enough, set `models.providers.<provider>.models[].compat.supportsTools: false` for that model entry. The agent will then operate without tool calls on that model.

View File

@@ -104,7 +104,7 @@ within their overall connection budget instead of surfacing it as a terminal
handshake failure.
`server`, `features`, `snapshot`, and `policy` are all required by the schema
(`packages/gateway-protocol/src/schema/frames.ts`). `auth` is also required and reports
(`src/gateway/protocol/schema/frames.ts`). `auth` is also required and reports
the negotiated role/scopes. `pluginSurfaceUrls` is optional and maps plugin
surface names, such as `canvas`, to scoped hosted URLs.
@@ -648,7 +648,7 @@ terminal summary, and sanitized error text.
## Versioning
- `PROTOCOL_VERSION` lives in `packages/gateway-protocol/src/version.ts`.
- `PROTOCOL_VERSION` lives in `src/gateway/protocol/version.ts`.
- Clients send `minProtocol` + `maxProtocol`; the server rejects ranges that
do not include its current protocol. Current clients and servers require
protocol v4.
@@ -664,8 +664,8 @@ stable across protocol v4 and are the expected baseline for third-party clients.
| Constant | Default | Source |
| ----------------------------------------- | ----------------------------------------------------- | ------------------------------------------------------------------------------------------ |
| `PROTOCOL_VERSION` | `4` | `packages/gateway-protocol/src/version.ts` |
| `MIN_CLIENT_PROTOCOL_VERSION` | `4` | `packages/gateway-protocol/src/version.ts` |
| `PROTOCOL_VERSION` | `4` | `src/gateway/protocol/version.ts` |
| `MIN_CLIENT_PROTOCOL_VERSION` | `4` | `src/gateway/protocol/version.ts` |
| Request timeout (per RPC) | `30_000` ms | `src/gateway/client.ts` (`requestTimeoutMs`) |
| Preauth / connect-challenge timeout | `15_000` ms | `src/gateway/handshake-timeouts.ts` (config/env can raise the paired server/client budget) |
| Initial reconnect backoff | `1_000` ms | `src/gateway/client.ts` (`backoffMs`) |
@@ -818,7 +818,7 @@ Migration target:
This protocol exposes the **full gateway API** (status, channels, models, chat,
agent, sessions, nodes, approvals, etc.). The exact surface is defined by the
TypeBox schemas in `packages/gateway-protocol/src/schema.ts`.
TypeBox schemas in `src/gateway/protocol/schema.ts`.
## Related

View File

@@ -183,13 +183,6 @@ Define providers under `secrets.providers`:
passEnv: ["PATH", "VAULT_ADDR"],
jsonOnly: true,
},
"team-secrets": {
source: "exec",
pluginIntegration: {
pluginId: "acme-secrets",
integrationId: "secret-store",
},
},
},
defaults: {
env: "default",
@@ -226,11 +219,6 @@ Define providers under `secrets.providers`:
- Pair `allowSymlinkCommand` with `trustedDirs` for package-manager paths (for example `["/opt/homebrew"]`).
- Supports timeout, no-output timeout, output byte limits, env allowlist, and trusted dirs.
- Windows fail-closed note: if ACL verification is unavailable for the command path, resolution fails. For trusted paths only, set `allowInsecurePath: true` on that provider to bypass path security checks.
- Plugin-managed exec providers can use `pluginIntegration` instead of
copied `command`/`args`. OpenClaw resolves the current command details
from the installed plugin manifest during startup/reload. If the plugin is
disabled, removed, untrusted, or no longer declares the integration,
active SecretRefs using that provider fail closed.
Request payload (stdin):

View File

@@ -282,7 +282,7 @@ troubleshooting, see the main [FAQ](/help/faq).
<Accordion title="Are opus / sonnet / gpt built-in shortcuts?">
Yes. OpenClaw ships a few default shorthands (only applied when the model exists in `agents.defaults.models`):
- `opus` → `anthropic/claude-opus-4-8`
- `opus` → `anthropic/claude-opus-4-7`
- `sonnet` → `anthropic/claude-sonnet-4-6`
- `gpt` → `openai/gpt-5.4`
- `gpt-mini` → `openai/gpt-5.4-mini`
@@ -536,11 +536,7 @@ Related: [/concepts/oauth](/concepts/oauth) (OAuth flows, token storage, multi-a
<Accordion title="OAuth vs API key - what is the difference?">
OpenClaw supports both:
- **OAuth / CLI login** often leverages subscription access where the
provider supports it. For Anthropic, OpenClaw's Claude CLI backend uses
Claude Code `claude -p`; Anthropic currently treats that as Agent
SDK/programmatic usage, with a separate monthly Agent SDK credit starting
June 15, 2026.
- **OAuth** often leverages subscription access (where applicable).
- **API keys** use pay-per-token billing.
The wizard explicitly supports Anthropic Claude CLI, OpenAI Codex OAuth, and API keys.

View File

@@ -109,9 +109,7 @@ docker compose up -d openclaw-gateway
<Note>
Run `docker compose` from the repo root. If you enabled `OPENCLAW_EXTRA_MOUNTS`
or `OPENCLAW_HOME_VOLUME`, the setup script writes `docker-compose.extra.yml`;
include it after any standard override file, for example
`-f docker-compose.yml -f docker-compose.override.yml -f docker-compose.extra.yml`
when both override files exist.
include it with `-f docker-compose.yml -f docker-compose.extra.yml`.
</Note>
<Note>

View File

@@ -0,0 +1,205 @@
---
summary: "Per-agent Policy plugin overlays layered on top of global policy rules."
read_when:
- You are designing per-agent policy requirements
- You need to distinguish tool posture policy from workspace policy
- You are configuring stricter policy for one named agent
title: "Agent-scoped policy overlays"
---
# Agent-scoped policy overlays
OpenClaw policy supports global requirements and stricter requirements for
explicit runtime agent ids. Some deployments need one agent to use a tighter
workspace and tool posture than other agents, but deployment-wide rules should
not force every agent to use the same posture.
This page describes the agent-scoped overlay model. The field reference remains
[`openclaw policy`](/cli/policy).
## Design goals
- Keep global policy as the deployment baseline.
- Let a named agent add stricter requirements without weakening global rules.
- Reuse existing policy section shapes where the evidence can be attributed to
an agent.
- Avoid making `agents.workspace` a second tool-permission system.
- Leave global-only checks global until their evidence can be mapped to an
agent.
## Shape
Use `scopes.<scopeName>` for purpose-named agent policy scopes. Each
scope lists the runtime `agentIds` it applies to, then reuses the normal
top-level policy section grammar where the section evidence can be attributed to
those agents. The initial shipped scoped sections are `tools` and
`agents.workspace`; sandbox and ingress stay out of this PR and can join the
same container once those policy PRs land and their evidence carries agent
identity. The scoped field inventory is backed by policy rule metadata that
records each field's strictness semantics for later policy-file conformance.
```jsonc
{
"tools": {
"denyTools": ["process"],
},
"agents": {
"workspace": {
"allowedAccess": ["none", "ro"],
},
},
"scopes": {
"release-agent-lockdown": {
"agentIds": ["release-agent"],
"agents": {
"workspace": {
"allowedAccess": ["none", "ro"],
},
},
"tools": {
"profiles": { "allow": ["minimal", "messaging"] },
"fs": { "requireWorkspaceOnly": true },
"exec": {
"allowSecurity": ["deny", "allowlist"],
"requireAsk": ["always"],
"allowHosts": ["sandbox"],
},
"elevated": { "allow": false },
"alsoAllow": { "expected": ["message", "read"] },
"denyTools": ["exec", "process", "write", "edit", "apply_patch"],
},
},
},
}
```
`agents.workspace` remains the existing all-agent workspace baseline.
`scopes.<scopeName>` is a scoped overlay, not a replacement for global
policy. The scope name is descriptive only; matching uses `agentIds`, not
display names. It deliberately contains normal section names instead of a
bespoke per-agent mini-grammar.
Every scope present in `policy.jsonc` must be valid and enforceable. In this
PR, the only supported selector is `agentIds`, and it supports only `tools.*`
and `agents.workspace.*`.
## Layering semantics
Policy evaluation is additive:
1. Top-level policy applies to all matching evidence.
2. Existing `agents.workspace` applies to defaults and every listed agent.
3. `scopes.<scopeName>` applies to evidence for each normalized runtime
id in `agentIds`.
4. Multiple scope blocks may target the same agent when they govern
different fields, or when a later value for the same field is equally or
more restrictive according to policy metadata.
5. A named-agent overlay can tighten policy, but it cannot make a global
violation acceptable.
If both global and agent-scoped rules fail, findings should point at the rule
that was violated:
```text
oc://policy.jsonc/tools/denyTools
oc://policy.jsonc/scopes/release-agent-lockdown/tools/denyTools
oc://policy.jsonc/scopes/release-agent-lockdown/agents/workspace/allowedAccess
```
That keeps broad tool posture, named-agent tool posture, and workspace posture
auditable as separate requirements even when they observe the same config
fields.
Exact-list claims such as `tools.alsoAllow.expected` compare the configured list
to the expected list and report both missing expected entries and unexpected
extra entries. This is intended for additive posture such as `alsoAllow`, where
one extra entry can widen an agent beyond its reviewed role.
## Policy and config layering
The overlay model separates where policy is authored from where OpenClaw config
is observed:
| Policy scope | Observed config | Applies to | Example result |
| --------------------------------------- | ---------------------------------------------------- | --------------------------------- | ----------------------------------------------------------------------------- |
| Top-level `tools.*` | Global `tools.*` and inherited agent tool posture | All agents using matching posture | Deny `gateway` exec host for every agent unless the global policy allows it. |
| Top-level `tools.*` | `agents.list[].tools.*` overrides | Any agent with an override | Flag one agent that overrides `tools.exec.host` to an unapproved value. |
| `scopes.<scopeName>.tools.*` | Matching `agents.list[]` entry and inherited posture | Only that named agent | Let most agents use `node` exec host while one agent must use only `sandbox`. |
| `agents.workspace` | Defaults and every listed agent workspace posture | Defaults and all listed agents | Require every agent workspace access to be `none` or `ro`. |
| `scopes.<scopeName>.agents.workspace.*` | Matching `agents.list[]` workspace posture | Only that named agent | Require one agent to be read-only without requiring the same for `main`. |
Per-agent overlays are additive. A named-agent rule can be stricter than the
top-level rule, but it cannot make a global violation acceptable. For allow-list
rules, the effective allowed set is the intersection of the global rule and the
named-agent overlay when both are present.
For example, if top-level `tools.exec.allowHosts` permits `["sandbox", "node"]`
and `scopes.release-agent-lockdown.tools.exec.allowHosts` permits only
`["sandbox"]`, `release-agent` fails when its effective exec host is `node`;
another agent can still pass
with `node`.
## Tool posture versus workspace posture
Tool posture belongs under `tools` because it describes what tool behavior a
configuration may expose. The existing `tools.*` policy observes both global
`tools.*` config and per-agent `agents.list[].tools.*` overrides.
Workspace posture belongs under `workspace` because it describes sandbox mode
and workspace access. The workspace section should not grow into a general tool
policy namespace. If one agent needs stricter tool restrictions to make its
workspace posture meaningful, put those restrictions in the same agent overlay
under `scopes.<scopeName>.tools`.
For a restricted release agent, the intended split is:
```jsonc
{
"scopes": {
"release-agent-lockdown": {
"agentIds": ["release-agent"],
"agents": {
"workspace": { "allowedAccess": ["none", "ro"] },
},
"tools": {
"denyTools": ["exec", "process", "write", "edit", "apply_patch"],
},
},
},
}
```
## Section eligibility
An agent-scoped section should be added only when policy evidence carries an
agent id or can be attributed to one without guessing.
| Section | Initial agent-scoped status | Reason |
| ----------- | --------------------------- | ------------------------------------------------------------------------ |
| `workspace` | Include | Agent sandbox/workspace evidence already has agent identity. |
| `tools` | Include | Tool posture evidence includes global and per-agent tool config. |
| `sandbox` | Pipeline follow-up | Keep out until the sandbox posture PR lands and evidence can be scoped. |
| `ingress` | Pipeline follow-up | Keep out until ingress/channel posture lands with agent attribution. |
| `models` | Include when mapped | Selected model refs can be agent-specific. |
| `mcp` | Include when mapped | Use only when MCP server evidence is attributable to an agent. |
| `auth` | Defer | Auth profile metadata is a config catalog unless agent binding is clear. |
| `channels` | Defer | Channel provider posture is deployment-level until routing is scoped. |
| `gateway` | Keep global | Gateway exposure/auth/http posture is process-level. |
| `network` | Keep global | Private-network SSRF posture is runtime-level. |
| `secrets` | Keep global first | Secret provider posture is shared unless refs are agent-attributed. |
## Compatibility
The implementation is additive:
- keep all existing top-level policy fields valid;
- keep `agents.workspace` semantics unchanged;
- validate `scopes` before evaluating scoped rules;
- reject unsupported scoped sections clearly until their evidence and policy
contracts are implemented;
- do not reinterpret top-level `tools.requireMetadata` as agent-scoped, because
tool metadata describes the declared workspace tool catalog;
- include agent-scoped evidence in the attestation hash when any scoped rule is
present.
This lets broad tool posture remain a top-level policy contract while named
agents add stricter observable claims without weakening the global baseline.

View File

@@ -75,9 +75,7 @@ openclaw gateway call node.list --params "{}"
Official distributed iOS builds use the external push relay instead of publishing the raw APNs
token to the gateway.
By default, official/TestFlight builds and gateways use the hosted relay at `https://ios-push-relay.openclaw.ai`.
Custom relay deployments can override the gateway relay URL:
Gateway-side requirement:
```json5
{
@@ -100,7 +98,7 @@ How the flow works:
- The iOS app fetches the paired gateway identity and includes it in relay registration, so the relay-backed registration is delegated to that specific gateway.
- The app forwards that relay-backed registration to the paired gateway with `push.apns.register`.
- The gateway uses that stored relay handle for `push.test`, background wakes, and wake nudges.
- Custom gateway relay URLs must match the relay URL baked into the official/TestFlight iOS build.
- The gateway relay base URL must match the relay URL baked into the official/TestFlight iOS build.
- If the app later connects to a different gateway or a build with a different relay base URL, it refreshes the relay registration instead of reusing the old binding.
What the gateway does **not** need for this path:
@@ -111,7 +109,7 @@ What the gateway does **not** need for this path:
Expected operator flow:
1. Install the official/TestFlight iOS build.
2. Optional: set `gateway.push.apns.relay.baseUrl` on the gateway only when using a custom relay deployment.
2. Set `gateway.push.apns.relay.baseUrl` on the gateway.
3. Pair the app to the gateway and let it finish connecting.
4. The app publishes `push.apns.register` automatically after it has an APNs token, the operator session is connected, and relay registration succeeds.
5. After that, `push.test`, reconnect wakes, and wake nudges can use the stored relay-backed registration.
@@ -130,7 +128,6 @@ compatible but does not count as a durable last-seen update.
Compatibility note:
- `OPENCLAW_APNS_RELAY_BASE_URL` still works as a temporary env override for the gateway.
- `OPENCLAW_PUSH_RELAY_BASE_URL` still works as a temporary env override for official/TestFlight iOS builds.
## Authentication and trust flow

View File

@@ -85,25 +85,25 @@ For an already-running app-server, use WebSocket transport:
Supported `appServer` fields:
| Field | Default | Meaning |
| --------------------------------------------- | ------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `transport` | `"stdio"` | `"stdio"` spawns Codex; `"websocket"` connects to `url`. |
| `command` | managed Codex binary | Executable for stdio transport. Leave unset to use the managed binary. |
| `args` | `["app-server", "--listen", "stdio://"]` | Arguments for stdio transport. |
| `url` | unset | WebSocket app-server URL. |
| `authToken` | unset | Bearer token for WebSocket transport. |
| `headers` | `{}` | Extra WebSocket headers. |
| `clearEnv` | `[]` | Extra environment variable names removed from the spawned stdio app-server process after OpenClaw builds its inherited environment. |
| `requestTimeoutMs` | `60000` | Timeout for app-server control-plane calls. |
| `turnCompletionIdleTimeoutMs` | `60000` | Quiet window after Codex accepts a turn or after a turn-scoped app-server request while OpenClaw waits for `turn/completed`. |
| `postToolRawAssistantCompletionIdleTimeoutMs` | `300000` | Completion-idle guard used after a tool handoff when Codex emits raw assistant completion or progress but does not send `turn/completed`. Use this for trusted or heavy workloads where post-tool synthesis can legitimately stay quiet longer than the final assistant release budget. |
| `mode` | `"yolo"` unless local Codex requirements disallow YOLO | Preset for YOLO or guardian-reviewed execution. |
| `approvalPolicy` | `"never"` or an allowed guardian approval policy | Native Codex approval policy sent to thread start, resume, and turn. |
| `sandbox` | `"danger-full-access"` or an allowed guardian sandbox | Native Codex sandbox mode sent to thread start and resume. Active OpenClaw sandboxes narrow `danger-full-access` turns to Codex `workspace-write`; the turn network flag follows OpenClaw sandbox egress. |
| `approvalsReviewer` | `"user"` or an allowed guardian reviewer | Use `"auto_review"` to let Codex review native approval prompts when allowed. |
| `defaultWorkspaceDir` | current process directory | Workspace used by `/codex bind` when `--cwd` is omitted. |
| `serviceTier` | unset | Optional Codex app-server service tier. `"priority"` enables fast-mode routing, `"flex"` requests flex processing, and `null` clears the override. Legacy `"fast"` is accepted as `"priority"`. |
| `experimental.sandboxExecServer` | `false` | Preview opt-in that registers an OpenClaw sandbox-backed Codex environment with Codex app-server 0.132.0 or newer so native Codex execution can run inside the active OpenClaw sandbox. |
| Field | Default | Meaning |
| --------------------------------------------- | ------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `transport` | `"stdio"` | `"stdio"` spawns Codex; `"websocket"` connects to `url`. |
| `command` | managed Codex binary | Executable for stdio transport. Leave unset to use the managed binary. |
| `args` | `["app-server", "--listen", "stdio://"]` | Arguments for stdio transport. |
| `url` | unset | WebSocket app-server URL. |
| `authToken` | unset | Bearer token for WebSocket transport. |
| `headers` | `{}` | Extra WebSocket headers. |
| `clearEnv` | `[]` | Extra environment variable names removed from the spawned stdio app-server process after OpenClaw builds its inherited environment. |
| `requestTimeoutMs` | `60000` | Timeout for app-server control-plane calls. |
| `turnCompletionIdleTimeoutMs` | `60000` | Quiet window after Codex accepts a turn or after a turn-scoped app-server request while OpenClaw waits for `turn/completed`. |
| `postToolRawAssistantCompletionIdleTimeoutMs` | unset | Completion-idle guard used after a tool handoff when Codex emits raw assistant completion or progress but does not send `turn/completed`. Defaults to the assistant completion idle timeout when unset. Use this for trusted or heavy workloads where post-tool synthesis can legitimately stay quiet longer than the final assistant release budget. |
| `mode` | `"yolo"` unless local Codex requirements disallow YOLO | Preset for YOLO or guardian-reviewed execution. |
| `approvalPolicy` | `"never"` or an allowed guardian approval policy | Native Codex approval policy sent to thread start, resume, and turn. |
| `sandbox` | `"danger-full-access"` or an allowed guardian sandbox | Native Codex sandbox mode sent to thread start and resume. Active OpenClaw sandboxes narrow `danger-full-access` turns to Codex `workspace-write`; the turn network flag follows OpenClaw sandbox egress. |
| `approvalsReviewer` | `"user"` or an allowed guardian reviewer | Use `"auto_review"` to let Codex review native approval prompts when allowed. |
| `defaultWorkspaceDir` | current process directory | Workspace used by `/codex bind` when `--cwd` is omitted. |
| `serviceTier` | unset | Optional Codex app-server service tier. `"priority"` enables fast-mode routing, `"flex"` requests flex processing, and `null` clears the override. Legacy `"fast"` is accepted as `"priority"`. |
| `experimental.sandboxExecServer` | `false` | Preview opt-in that registers an OpenClaw sandbox-backed Codex environment with Codex app-server 0.132.0 or newer so native Codex execution can run inside the active OpenClaw sandbox. |
The plugin blocks older or unversioned app-server handshakes. Codex app-server
must report stable version `0.125.0` or newer.
@@ -118,11 +118,7 @@ prompts that nobody is around to answer.
If Codex's local system requirements file disallows implicit YOLO approval,
reviewer, or sandbox values, OpenClaw treats the implicit default as guardian
instead and selects allowed guardian permissions. `tools.exec.mode: "auto"`
also forces guardian-reviewed Codex approvals and does not preserve unsafe
legacy `approvalPolicy: "never"` or `sandbox: "danger-full-access"` overrides;
set `tools.exec.mode: "full"` for an intentional no-approval posture.
Hostname-matching
instead and selects allowed guardian permissions. Hostname-matching
`[[remote_sandbox_config]]` entries in the same requirements file are honored
for the sandbox default decision.
@@ -337,15 +333,10 @@ Codex then goes quiet without `turn/completed`, OpenClaw best-effort interrupts
the native turn and releases the session lane. Post-tool raw assistant progress
keeps waiting for `turn/completed` while a completion-idle guard stays armed; the
guard uses `appServer.postToolRawAssistantCompletionIdleTimeoutMs` when
configured and defaults to five minutes otherwise. Replay-safe stdio app-server
failures, including turn-completion idle timeouts without assistant, tool,
active-item, or side-effect evidence, are retried once on a fresh app-server
attempt. Unsafe timeouts still retire the stuck app-server client and release
the OpenClaw session lane. They also clear the stale native thread binding and
surface a recoverable timeout message for user or maintainer judgment instead of
being replayed automatically. Timeout diagnostics include the last
app-server notification method and, for raw assistant response items, the item
type, role, id, and a bounded assistant text preview.
configured and falls back to the assistant completion idle timeout otherwise.
Timeout diagnostics include the last app-server notification method and, for raw
assistant response items, the item type, role, id, and a bounded assistant text
preview.
## Model discovery

View File

@@ -362,35 +362,30 @@ turn instead of relying on Codex host-side sandboxing. Shell access is exposed
through OpenClaw sandbox-backed dynamic tools such as `sandbox_exec` and
`sandbox_process` when the normal exec/process tools are available.
Use normalized OpenClaw exec mode when you want Codex native auto-review before
sandbox escapes or extra permissions:
Use guardian mode when you want Codex native auto-review before sandbox escapes
or extra permissions:
```json5
{
tools: {
exec: {
mode: "auto",
},
},
plugins: {
entries: {
codex: {
enabled: true,
config: {
appServer: {
mode: "guardian",
serviceTier: "priority",
},
},
},
},
},
}
```
For Codex app-server sessions, OpenClaw maps `tools.exec.mode: "auto"` to Codex
Guardian-reviewed approvals, usually
Guardian mode expands to Codex app-server approvals, usually
`approvalPolicy: "on-request"`, `approvalsReviewer: "auto_review"`, and
`sandbox: "workspace-write"` when the local requirements allow those values.
In `tools.exec.mode: "auto"`, OpenClaw does not preserve legacy unsafe Codex
`approvalPolicy: "never"` or `sandbox: "danger-full-access"` overrides; use
`tools.exec.mode: "full"` for an intentional no-approval Codex posture. The
legacy `plugins.entries.codex.config.appServer.mode: "guardian"` preset still
works, but `tools.exec.mode: "auto"` is the normalized OpenClaw surface.
For every app-server field, auth order, environment isolation, discovery, and
timeout behavior, see [Codex harness reference](/plugins/codex-harness-reference).
@@ -525,25 +520,25 @@ Supported top-level Codex plugin fields:
Supported `appServer` fields:
| Field | Default | Meaning |
| --------------------------------------------- | ------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `transport` | `"stdio"` | `"stdio"` spawns Codex; `"websocket"` connects to `url`. |
| `command` | managed Codex binary | Executable for stdio transport. Leave unset to use the managed binary; set it only for an explicit override. |
| `args` | `["app-server", "--listen", "stdio://"]` | Arguments for stdio transport. |
| `url` | unset | WebSocket app-server URL. |
| `authToken` | unset | Bearer token for WebSocket transport. |
| `headers` | `{}` | Extra WebSocket headers. |
| `clearEnv` | `[]` | Extra environment variable names removed from the spawned stdio app-server process after OpenClaw builds its inherited environment. OpenClaw keeps per-agent `CODEX_HOME` and inherited `HOME` for local launches. |
| `codeModeOnly` | `false` | Opt into Codex's code-mode-only tool surface. OpenClaw dynamic tools remain registered with Codex so nested `tools.*` calls return through the app-server `item/tool/call` bridge. |
| `requestTimeoutMs` | `60000` | Timeout for app-server control-plane calls. |
| `turnCompletionIdleTimeoutMs` | `60000` | Quiet window after Codex accepts a turn or after a turn-scoped app-server request while OpenClaw waits for `turn/completed`. Raise this for slow post-tool or status-only synthesis phases. |
| `postToolRawAssistantCompletionIdleTimeoutMs` | `300000` | Completion-idle guard used after a tool handoff when Codex emits raw assistant completion or progress but does not send `turn/completed`. Use this for trusted or heavy workloads where post-tool synthesis can legitimately stay quiet longer than the final assistant release budget. |
| `mode` | `"yolo"` unless local Codex requirements disallow YOLO | Preset for YOLO or guardian-reviewed execution. Local stdio requirements that omit `danger-full-access`, `never` approval, or the `user` reviewer make the implicit default guardian. |
| `approvalPolicy` | `"never"` or an allowed guardian approval policy | Native Codex approval policy sent to thread start/resume/turn. Guardian defaults prefer `"on-request"` when allowed. |
| `sandbox` | `"danger-full-access"` or an allowed guardian sandbox | Native Codex sandbox mode sent to thread start/resume. Guardian defaults prefer `"workspace-write"` when allowed, otherwise `"read-only"`. When an OpenClaw sandbox is active, `danger-full-access` turns use Codex `workspace-write` with network access derived from the OpenClaw sandbox egress setting. |
| `approvalsReviewer` | `"user"` or an allowed guardian reviewer | Use `"auto_review"` to let Codex review native approval prompts when allowed, otherwise `guardian_subagent` or `user`. `guardian_subagent` remains a legacy alias. |
| `serviceTier` | unset | Optional Codex app-server service tier. `"priority"` enables fast-mode routing, `"flex"` requests flex processing, `null` clears the override, and legacy `"fast"` is accepted as `"priority"`. |
| `experimental.sandboxExecServer` | `false` | Preview opt-in that registers an OpenClaw sandbox-backed Codex environment with Codex app-server 0.132.0 or newer so native Codex execution can run inside the active OpenClaw sandbox. |
| Field | Default | Meaning |
| --------------------------------------------- | ------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `transport` | `"stdio"` | `"stdio"` spawns Codex; `"websocket"` connects to `url`. |
| `command` | managed Codex binary | Executable for stdio transport. Leave unset to use the managed binary; set it only for an explicit override. |
| `args` | `["app-server", "--listen", "stdio://"]` | Arguments for stdio transport. |
| `url` | unset | WebSocket app-server URL. |
| `authToken` | unset | Bearer token for WebSocket transport. |
| `headers` | `{}` | Extra WebSocket headers. |
| `clearEnv` | `[]` | Extra environment variable names removed from the spawned stdio app-server process after OpenClaw builds its inherited environment. OpenClaw keeps per-agent `CODEX_HOME` and inherited `HOME` for local launches. |
| `codeModeOnly` | `false` | Opt into Codex's code-mode-only tool surface. OpenClaw dynamic tools remain registered with Codex so nested `tools.*` calls return through the app-server `item/tool/call` bridge. |
| `requestTimeoutMs` | `60000` | Timeout for app-server control-plane calls. |
| `turnCompletionIdleTimeoutMs` | `60000` | Quiet window after Codex accepts a turn or after a turn-scoped app-server request while OpenClaw waits for `turn/completed`. Raise this for slow post-tool or status-only synthesis phases. |
| `postToolRawAssistantCompletionIdleTimeoutMs` | unset | Completion-idle guard used after a tool handoff when Codex emits raw assistant completion or progress but does not send `turn/completed`. Defaults to the assistant completion idle timeout when unset. Use this for trusted or heavy workloads where post-tool synthesis can legitimately stay quiet longer than the final assistant release budget. |
| `mode` | `"yolo"` unless local Codex requirements disallow YOLO | Preset for YOLO or guardian-reviewed execution. Local stdio requirements that omit `danger-full-access`, `never` approval, or the `user` reviewer make the implicit default guardian. |
| `approvalPolicy` | `"never"` or an allowed guardian approval policy | Native Codex approval policy sent to thread start/resume/turn. Guardian defaults prefer `"on-request"` when allowed. |
| `sandbox` | `"danger-full-access"` or an allowed guardian sandbox | Native Codex sandbox mode sent to thread start/resume. Guardian defaults prefer `"workspace-write"` when allowed, otherwise `"read-only"`. When an OpenClaw sandbox is active, `danger-full-access` turns use Codex `workspace-write` with network access derived from the OpenClaw sandbox egress setting. |
| `approvalsReviewer` | `"user"` or an allowed guardian reviewer | Use `"auto_review"` to let Codex review native approval prompts when allowed, otherwise `guardian_subagent` or `user`. `guardian_subagent` remains a legacy alias. |
| `serviceTier` | unset | Optional Codex app-server service tier. `"priority"` enables fast-mode routing, `"flex"` requests flex processing, `null` clears the override, and legacy `"fast"` is accepted as `"priority"`. |
| `experimental.sandboxExecServer` | `false` | Preview opt-in that registers an OpenClaw sandbox-backed Codex environment with Codex app-server 0.132.0 or newer so native Codex execution can run inside the active OpenClaw sandbox. |
OpenClaw-owned dynamic tool calls are bounded independently from
`appServer.requestTimeoutMs`: Codex `item/tool/call` requests use a 90 second
@@ -574,15 +569,10 @@ goes quiet without `turn/completed`, OpenClaw best-effort interrupts the native
turn and releases the session lane. Post-tool raw assistant progress keeps
waiting for `turn/completed` while a completion-idle guard stays armed; the guard
uses `appServer.postToolRawAssistantCompletionIdleTimeoutMs` when configured and
defaults to five minutes otherwise. Replay-safe stdio app-server failures,
including turn-completion idle timeouts without assistant, tool, active-item, or
side-effect evidence, are retried once on a fresh app-server attempt. Unsafe
timeouts still retire the stuck app-server client and release the OpenClaw
session lane. They also clear the stale native thread binding and surface a
recoverable timeout message for user or maintainer judgment instead of being
replayed automatically. Timeout diagnostics include the last app-server
notification method and, for raw assistant response items, the item type, role,
id, and a bounded assistant text preview.
falls back to the assistant completion idle timeout otherwise. Timeout
diagnostics include the last app-server notification method and, for raw
assistant response items, the item type, role, id, and a bounded assistant text
preview.
Environment overrides remain available for local testing:

View File

@@ -1,356 +0,0 @@
---
summary: "Run OpenClaw embedded agent turns through the external GitHub Copilot SDK harness"
title: "Copilot SDK harness"
read_when:
- You want to use the GitHub Copilot SDK harness for an agent
- You need configuration examples for the `copilot` runtime
- You are wiring an agent to subscription Copilot (github / openclaw / copilot) and want it to run through the Copilot CLI
---
The external `@openclaw/copilot` plugin lets OpenClaw run embedded subscription
Copilot agent turns through the GitHub Copilot CLI (`@github/copilot-sdk`)
instead of the built-in PI harness.
Use the Copilot SDK harness when you want the Copilot CLI session to own the
low-level agent loop: native tool execution, native compaction
(`infiniteSessions`), and CLI-managed thread state under `copilotHome`.
OpenClaw still owns chat channels, session files, model selection, OpenClaw
dynamic tools (bridged), approvals, media delivery, the visible transcript
mirror, `/btw` side questions (handled by the in-tree PI fallback — see
[Side questions (`/btw`)](#side-questions-btw)), and `openclaw doctor`.
For the broader model/provider/runtime split, start with
[Agent runtimes](/concepts/agent-runtimes).
## Requirements
- OpenClaw with the `@openclaw/copilot` plugin installed.
- If your config uses `plugins.allow`, include `copilot` (the manifest
id declared by the plugin). A restrictive
allowlist that uses the npm-style `@openclaw/copilot` package name
will leave the plugin blocked and the runtime will not load
even with `agentRuntime.id: "copilot"`.
- A GitHub Copilot subscription that can drive the Copilot CLI (or a
`gitHubToken` env / auth-profile entry for headless / cron runs).
- A writable `copilotHome` directory. The harness defaults to
`~/.openclaw/agents/<agentId>/copilot` for full per-agent isolation. The
platform default (`%APPDATA%\copilot` on Windows, `$XDG_CONFIG_HOME/copilot`
or `~/.config/copilot` elsewhere) is used as the doctor probe fallback when
no explicit home is set.
`openclaw doctor` runs the plugin
[doctor contract](#doctor-and-probes) for the extension; failures there are
the canonical way to confirm the environment is ready before opting an agent
in.
## Plugin install
The Copilot runtime is an external plugin so the core `openclaw` package does
not carry the `@github/copilot-sdk` dependency or its platform-specific
`@github/copilot-<platform>-<arch>` CLI binary. Together they add roughly
260 MB, so install them only for agents that opt into this runtime:
```bash
openclaw plugins install @openclaw/copilot
```
The wizard installs the plugin the first time you select a
`github-copilot/*` model **and** your config opts the model (or its
provider) into the Copilot agent runtime via
`agentRuntime: { id: "copilot" }` (see [Quickstart](#quickstart) below).
Without the opt-in, openclaw uses its built-in GitHub Copilot provider
and never installs the runtime plugin.
The runtime resolves the SDK in this order:
1. `import("@github/copilot-sdk")` from the installed `@openclaw/copilot`
package.
2. The well-known fallback dir `~/.openclaw/npm-runtime/copilot/` (the
legacy on-demand install target).
A missing SDK surfaces a single error with code `COPILOT_SDK_MISSING`
and the plugin reinstall command above.
## Quickstart
Pin one model (or one provider) to the harness:
```json5
{
agents: {
defaults: {
model: "github-copilot/gpt-5.5",
models: {
"github-copilot/gpt-5.5": {
agentRuntime: { id: "copilot" },
},
},
},
},
}
```
Both routes are equivalent. Use `agentRuntime.id` on a single model entry
when only that model should be routed through the harness; set
`agentRuntime.id` on a provider when every model under that provider should
use it.
## Supported providers
The harness advertises support for the canonical `github-copilot` provider
(the same id owned by `extensions/github-copilot`):
- `github-copilot`
Anything outside that set falls through `selection.ts`'s `auto_pi` branch back
to PI.
## Auth
Per-agent precedence, applied during `runCopilotAttempt`:
1. **Explicit `useLoggedInUser: true`** on the attempt input. Uses the Copilot
CLI's logged-in user resolved under the agent's `copilotHome`.
2. **Explicit `gitHubToken`** on the attempt input (with `profileId` +
`profileVersion`). Useful for direct CLI invocations and tests where the
caller wants to bypass auth-profile resolution.
3. **Contract-resolved `resolvedApiKey` + `authProfileId`** from the
`EmbeddedRunAttemptParams` shape. This is the **production main path**:
core resolves the agent's configured `github-copilot` auth profile
(via `src/infra/provider-usage.auth.ts:resolveProviderAuths`) before
invoking the harness, and the harness consumes both fields directly.
This makes a `github-copilot:<profile>` auth profile work end-to-end
for headless / cron / multi-profile setups without env vars.
4. **Env-var fallback** for direct CLI / dogfood runs where no auth
profile is configured. The runtime checks the following vars in
precedence order, mirroring the shipped `github-copilot` provider
(`extensions/github-copilot/auth.ts`) and the documented Copilot SDK
setup:
1. `OPENCLAW_GITHUB_TOKEN` -- harness-specific override; set this
to pin a token for the OpenClaw harness without disturbing
system-wide `gh` / Copilot CLI config.
2. `COPILOT_GITHUB_TOKEN` -- standard Copilot SDK / CLI env var.
3. `GH_TOKEN` -- standard `gh` CLI env var (matches the existing
`github-copilot` provider precedence).
4. `GITHUB_TOKEN` -- generic GitHub token fallback.
The first non-empty value wins; empty strings are treated as
absent. The synthesised pool profile id is `env:<NAME>` and the
profileVersion is a non-reversible sha256 fingerprint of the
token, so rotating the env value cleanly busts the client pool.
5. **Default `useLoggedInUser`** when no token signal is available.
Each agent gets a dedicated `copilotHome` so Copilot CLI tokens, sessions, and
config do not leak between agents on the same machine. The default is
`<agentDir>/copilot` when the host hands the harness an agent directory
(isolating SDK state from OpenClaw's `models.json` / `auth-profiles.json` in
the same directory), or `~/.openclaw/agents/<agentId>/copilot` otherwise.
Override with `copilotHome: <path>` on the attempt input when you need a
custom location (for example, a shared mount for migration).
`probeCopilotAuthShape` (see [Doctor and probes](#doctor-and-probes)) is the
pure shape check that validates which of the modes above will be used.
It does not perform a live SDK handshake.
## Configuration surface
The harness reads its config from per-attempt input
(`runCopilotAttempt({...})`) plus a small set of env defaults inside
`extensions/copilot/src/`:
- `copilotHome` — per-agent CLI state directory (defaults documented above).
- `model` — string or `{ provider, id, api? }`. When omitted, OpenClaw uses
the agent's normal model selection and the harness verifies the resolved
provider is in the supported set.
- `reasoningEffort``"low" | "medium" | "high" | "xhigh"`. Maps from
OpenClaw's `ThinkLevel` / `ReasoningLevel` resolution in
`auto-reply/thinking.ts`.
- `infiniteSessionConfig` — optional override for the SDK
`infiniteSessions` block driven by `harness.compact`. Defaults are safe to
leave as-is.
- `hooksConfig` — optional bridge config exposing OpenClaw
before/after-message-write hooks to the SDK loop.
- `permissionPolicy` — optional override for the SDK's
`onPermissionRequest` handler used for built-in SDK tool kinds
(`shell`, `write`, `read`, `url`, `mcp`, `memory`, `hook`). Defaults
to `rejectAllPolicy` as a safety net; in practice the SDK never
invokes any of those kinds because every bridged OpenClaw tool is
registered with `overridesBuiltInTool: true` and
`skipPermission: true` so 100% of tool calls flow through OpenClaw's
wrapped `execute()`. See [Permissions and ask_user](#permissions-and-ask_user).
- `enableSessionTelemetry` — opt-in OpenTelemetry routing via
`telemetry-bridge.ts`.
Nothing in the rest of OpenClaw needs to know about these fields. Other
plugins, channels, and core code only see the standard
`AgentHarnessAttemptParams` / `AgentHarnessAttemptResult` shape.
## Compaction
When `harness.compact` runs, the Copilot SDK harness:
1. Enables `infiniteSessions` on the SDK session.
2. Lets the SDK perform its native compaction.
3. Writes an OpenClaw-shaped marker at
`workspacePath/files/openclaw-compaction-<ts>.json` so existing OpenClaw
transcript readers still see a familiar artifact.
The OpenClaw side transcript mirror (see below) continues to receive the
post-compaction messages, so user-facing chat history stays consistent.
## Transcript mirroring
`runCopilotAttempt` dual-writes each turn's mirrorable messages into the
OpenClaw audit transcript via
`extensions/copilot/src/dual-write-transcripts.ts`. The mirror is
per-session scoped (`copilot:${sessionId}`) and uses a per-message
identity (`${role}:${sha256_16(role,content)}`) so re-emits of prior-turn
entries collide with existing on-disk keys and do not duplicate.
The mirror is wrapped in two layers of failure containment so a transcript
write failure cannot fail the attempt: an internal best-effort wrapper and a
defense-in-depth `.catch(...)` at the attempt level. Failures are logged but
not surfaced.
## Side questions (`/btw`)
`/btw` is **not** native on this harness. `createCopilotAgentHarness()`
deliberately leaves `harness.runSideQuestion` undefined, so OpenClaw's `/btw`
dispatcher (`src/agents/btw.ts`) falls through to the same in-tree PI fallback
path it uses for every non-Codex runtime: the configured model provider is
called directly with a short side-question prompt and streamed back via
`streamSimple` (no CLI session, no extra pool slot).
This keeps Copilot CLI sessions reserved for the agent's main turn loop, and
keeps `/btw` behavior identical to other PI-backed runtimes. The contract is
asserted in
[`extensions/copilot/harness.test.ts`](https://github.com/openclaw/openclaw/blob/main/extensions/copilot/harness.test.ts)
under `describe("runSideQuestion")`.
## Doctor and probes
`extensions/copilot/doctor-contract-api.ts` is auto-loaded by
`src/plugins/doctor-contract-registry.ts`. It contributes:
- An empty `legacyConfigRules` (no retired fields at MVP).
- A no-op `normalizeCompatibilityConfig` (kept so future field retirements
have a stable in-tree home).
- One `sessionRouteStateOwners` entry claiming provider `github-copilot`;
runtime `copilot`; CLI session key `copilot`; auth profile
prefix `github-copilot:`.
`extensions/copilot/src/doctor-probes.ts` exports three imperative probes
that hosts (including `openclaw doctor`) can call to verify the environment:
| Probe | What it checks | Reasons it can fail |
| -------------------------- | --------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- |
| `probeCopilotCliVersion` | `copilot --version` exits 0 with a non-empty version string | `non-zero-exit`, `empty-version`, `spawn-failed`, `spawn-error`, `probe-timeout` |
| `probeCopilotHomeWritable` | `mkdir -p copilotHome` + write + rm a marker file | `copilothome-not-writable` (with the underlying fs error in `details.rawError`) |
| `probeCopilotAuthShape` | At least one of `useLoggedInUser`, `gitHubToken`, or `profileId`+`profileVersion` | `no-auth-source` |
Each probe accepts a DI seam (`spawnFn`, `fsApi`) so tests do not spawn the
real Copilot CLI or touch the host fs.
## Limitations
- The harness only claims the canonical `github-copilot` provider at MVP.
Additional providers (BYOK or otherwise) should land in follow-up PRs that
ship the adapter alongside the wire-up.
- The harness does not deliver TUI; PI's TUI is unaffected and remains the
fallback for whatever runtimes do not have a peer surface.
- PI session state is not migrated when an agent switches to `copilot`.
Selection is per attempt; existing PI sessions remain valid.
- **Interactive `ask_user` is not yet wired.** The SDK's
`onUserInputRequest` handler is intentionally not registered, which
per the SDK contract hides the `ask_user` tool from the model
entirely. Agents running under this harness make best-judgment
decisions from the initial prompt rather than asking clarifying
questions mid-turn. A follow-up will port the codex pattern at
`extensions/codex/src/app-server/user-input-bridge.ts` to route SDK
`UserInputRequest`s through the OpenClaw channel/TUI prompt path; the
dormant scaffolding in `extensions/copilot/src/user-input-bridge.ts`
is the surface that follow-up will wire.
## Permissions and ask_user
Permission enforcement for bridged OpenClaw tools happens **inside the
tool wrapper**, not via the SDK's `onPermissionRequest` callback. The
same `wrapToolWithBeforeToolCallHook` that PI uses
(`src/agents/pi-tools.before-tool-call.ts`) is applied by
`createOpenClawCodingTools` to every coding tool: loop detection,
trusted plugin policies, before-tool-call hooks, and two-phase plugin
approvals via the gateway (`plugin.approval.request`) all run with the
exact same code path as native PI attempts.
To let that wrapper own the decision, the SDK Tool returned by
`convertOpenClawToolToSdkTool` is marked with:
- `overridesBuiltInTool: true` — replaces the Copilot CLI's built-in
tool of the same name (edit, read, write, bash, …) so every tool
invocation routes back to OpenClaw.
- `skipPermission: true` — tells the SDK not to fire
`onPermissionRequest({kind: "custom-tool"})` before invoking the tool.
The wrapped `execute()` performs the richer OpenClaw policy check
internally; an SDK-level prompt would either short-circuit OpenClaw's
enforcement (if we allow-all) or block every tool call (if we
reject-all) — neither matches PI parity.
The in-tree codex harness uses the same split: bridged OpenClaw tools
are wrapped (`extensions/codex/src/app-server/dynamic-tools.ts`) and
the codex-app-server's _own_ native approval kinds
(`item/commandExecution/requestApproval`,
`item/fileChange/requestApproval`,
`item/permissions/requestApproval`) are routed through
`plugin.approval.request`
(`extensions/codex/src/app-server/approval-bridge.ts`). The Copilot SDK
equivalent — fail-closed `rejectAllPolicy` for any non-`custom-tool`
kind that ever reaches `onPermissionRequest` — is the same safety net,
and it does not fire in practice because `overridesBuiltInTool: true`
displaces every built-in.
For the wrapped-tool layer to make policy decisions equivalent to PI,
the harness forwards the full PI attempt-tool context to
`createOpenClawCodingTools` — identity (`senderIsOwner`,
`memberRoleIds`, `ownerOnlyToolAllowlist`, …), channel/routing
(`groupId`, `currentChannelId`, `replyToMode`, message-tool toggles),
auth (`authProfileStore`), run identity
(`sessionKey`/`runSessionKey` derived from `sandboxSessionKey`,
`runId`), model context (`modelApi`, `modelContextWindowTokens`,
`modelCompat`, `modelHasVision`), and run hooks (`onToolOutcome`,
`onYield`). Without those fields, owner-only allowlists silently
behave as deny-by-default, plugin-trust policies cannot resolve to the
right scope, and `session_status: "current"` resolves to a stale
sandbox key. The bridge builder is in
`extensions/copilot/src/tool-bridge.ts` and mirrors the PI
authoritative call at
`src/agents/pi-embedded-runner/run/attempt.ts:1029-1117`. Two PI fields
are intentionally **not** forwarded at MVP and tracked as follow-ups:
`sandbox` (the harness does not yet route through `resolveSandboxContext`)
and the PI tool-search/code-mode machinery
(`toolSearchCatalogRef`, `includeCoreTools`,
`includeToolSearchControls`, `toolSearchCatalogExecutor`,
`toolConstructionPlan`), which has no analog at the SDK boundary.
### Session-level GitHub token
The Copilot SDK contract distinguishes the **client-level** GitHub
token (`CopilotClientOptions.gitHubToken`, used to authenticate the
CLI process itself) from the **session-level** token
(`SessionConfig.gitHubToken`, which determines content exclusion,
model routing, and quota for that session and is honored on both
`createSession` and `resumeSession`). The harness resolves auth once
via `resolveCopilotAuth` and sets both fields when the auth mode is
`gitHubToken` (an explicit `auth.gitHubToken` or a contract-resolved
`resolvedApiKey` from a configured `github-copilot` auth profile).
When the resolved mode is `useLoggedInUser`, the session-level field
is omitted so the SDK keeps deriving identity from the logged-in
identity.
`ask_user` is intentionally hidden — see Limitations above.
## Related
- [Agent runtimes](/concepts/agent-runtimes)
- [Codex harness](/plugins/codex-harness)
- [Agent harness plugins (SDK reference)](/plugins/sdk-agent-harness)

View File

@@ -1124,7 +1124,7 @@ Optional overrides:
introMessage: "Say exactly: I'm here.",
providers: {
google: {
speakerVoice: "Kore",
voice: "Kore",
},
},
},
@@ -1141,7 +1141,7 @@ ElevenLabs for both agent-mode listening and speaking:
providers: {
elevenlabs: {
modelId: "eleven_v3",
speakerVoiceId: "pMsXgVXv3BLzUgSXRplE",
voiceId: "pMsXgVXv3BLzUgSXRplE",
},
},
},
@@ -1169,11 +1169,11 @@ ElevenLabs for both agent-mode listening and speaking:
```
The persistent Meet voice comes from
`messages.tts.providers.elevenlabs.speakerVoiceId`. Agent replies can also use
per-reply `[[tts:speakerVoiceId=... model=eleven_v3]]` directives when TTS model
`messages.tts.providers.elevenlabs.voiceId`. Agent replies can also use
per-reply `[[tts:voiceId=... model=eleven_v3]]` directives when TTS model
overrides are enabled, but config is the deterministic default for meetings.
On join, the logs should show `transcriptionProvider=elevenlabs` and each
spoken reply should log `provider=elevenlabs model=eleven_v3 speakerVoiceId=<voiceId>`.
spoken reply should log `provider=elevenlabs model=eleven_v3 voice=<voiceId>`.
Twilio-only config:

View File

@@ -126,10 +126,9 @@ observation-only.
**Messages and delivery**
- **`inbound_claim`** - claim an inbound message before agent routing (synthetic replies)
- `message_received` observe inbound content, sender, thread, and metadata
- **`message_sending`** rewrite outbound content or cancel delivery
- **`reply_payload_sending`** — mutate or cancel normalized reply payloads before delivery
- `message_sent` — observe outbound delivery success or failure
- `message_received` - observe inbound content, sender, thread, and metadata
- **`message_sending`** - rewrite outbound content or cancel delivery
- `message_sent` - observe outbound delivery success or failure
- **`before_dispatch`** - inspect or rewrite an outbound dispatch before channel handoff
- **`reply_dispatch`** - participate in the final reply-dispatch pipeline
@@ -385,8 +384,6 @@ Use message hooks for channel-level routing and delivery policy:
- `message_received`: observe inbound content, sender, `threadId`, `messageId`,
`senderId`, optional run/session correlation, and metadata.
- `message_sending`: rewrite `content` or return `{ cancel: true }`.
- `reply_payload_sending`: rewrite normalized `ReplyPayload` objects (including
`presentation`, `delivery`, media refs, and text) or return `{ cancel: true }`.
- `message_sent`: observe final success or failure.
For audio-only TTS replies, `content` may contain the hidden spoken transcript
@@ -408,13 +405,6 @@ Decision rules:
- `message_sending` with `cancel: false` is treated as no decision.
- Rewritten `content` continues to lower-priority hooks unless a later hook
cancels delivery.
- `reply_payload_sending` runs after payload normalization and before channel
delivery, including replies routed back to the originating channel. Handlers
run sequentially and each handler sees the latest payload produced by
higher-priority handlers.
- `reply_payload_sending` payloads do not expose runtime trust markers such as
`trustedLocalMedia`; plugins can edit payload shape but cannot grant local
media trust.
- `message_sending` can return `cancelReason` and bounded `metadata` with a
cancellation. New message lifecycle APIs expose this as a suppressed delivery
outcome with reason `cancelled_by_message_sending_hook`; legacy direct

View File

@@ -169,7 +169,6 @@ or npm install metadata. Those belong in your plugin code and `package.json`.
| `modelIdNormalization` | No | `object` | Provider-owned model-id alias/prefix cleanup that must run before provider runtime loads. |
| `providerEndpoints` | No | `object[]` | Manifest-owned endpoint host/baseUrl metadata for provider routes that core must classify before provider runtime loads. |
| `providerRequest` | No | `object` | Cheap provider-family and request-compatibility metadata used by generic request policy before provider runtime loads. |
| `secretProviderIntegrations` | No | `Record<string, object>` | Declarative SecretRef exec provider presets that setup or install surfaces can offer without hardcoding provider-specific integrations in core. |
| `cliBackends` | No | `string[]` | CLI inference backend ids owned by this plugin. Used for startup auto-activation from explicit config refs. |
| `syntheticAuthRefs` | No | `string[]` | Provider or CLI backend refs whose plugin-owned synthetic auth hook should be probed during cold model discovery before runtime loads. |
| `nonSecretAuthMarkers` | No | `string[]` | Bundled-plugin-owned placeholder API key values that represent non-secret local, OAuth, or ambient credential state. |
@@ -1081,72 +1080,6 @@ Provider fields:
| `compatibilityFamily` | `"moonshot"` | Optional provider-family compatibility bucket for shared request helpers. |
| `openAICompletions` | `object` | OpenAI-compatible completions request flags, currently `supportsStreamingUsage`. |
## secretProviderIntegrations reference
Use `secretProviderIntegrations` when a plugin can publish a reusable SecretRef
exec provider preset. OpenClaw reads this metadata before plugin runtime loads,
stores plugin ownership in `secrets.providers.<alias>.pluginIntegration`, and
leaves actual secret resolution to the SecretRef runtime.
Presets are exposed only for bundled plugins and installed plugins discovered
from the managed plugin install roots, such as git and ClawHub installs.
```json
{
"secretProviderIntegrations": {
"secret-store": {
"providerAlias": "team-secrets",
"displayName": "Team secrets",
"source": "exec",
"command": "${node}",
"args": ["./bin/resolve-secrets.mjs"]
}
}
}
```
The map key is the integration id. If `providerAlias` is omitted, OpenClaw uses
the integration id as the SecretRef provider alias. Provider aliases must match
the normal SecretRef provider alias pattern, for example `team-secrets` or
`onepassword-work`.
When an operator selects the preset, OpenClaw writes a provider reference like:
```json
{
"secrets": {
"providers": {
"team-secrets": {
"source": "exec",
"pluginIntegration": {
"pluginId": "acme-secrets",
"integrationId": "secret-store"
}
}
}
}
}
```
At startup/reload, OpenClaw resolves that provider by loading current plugin
manifest metadata, checking that the owning plugin is installed and active, and
materializing the exec command from the manifest. Disabling or removing the
plugin revokes the provider for active SecretRefs. Operators who want standalone
exec configuration can still write manual `command`/`args` providers directly.
Only `source: "exec"` presets are currently supported. `command` must be
`${node}`, and `args[0]` must be a `./` plugin-root-relative resolver script.
OpenClaw materializes it at startup/reload to the current Node executable and
the absolute in-plugin script path. Node options such as `--require`, `--import`,
`--loader`, `--env-file`, `--eval`, and `--print` are not part of the manifest
preset contract. Operators who need non-Node commands can configure standalone
manual exec providers directly.
OpenClaw derives `trustedDirs` for manifest presets from the plugin root and,
for `${node}` presets, the current Node executable directory. Manifest-authored
`trustedDirs` are ignored. Other exec provider options such as `timeoutMs`,
`maxOutputBytes`, `jsonOnly`, `env`, `passEnv`, and `allowInsecurePath` pass
through to the normal SecretRef exec provider config.
## modelPricing reference
Use `modelPricing` when a provider needs control-plane pricing behavior before

View File

@@ -64,7 +64,6 @@ commands.
| [chutes](/plugins/reference/chutes) | Adds Chutes model provider support to OpenClaw. | `@openclaw/chutes-provider`<br />included in OpenClaw | providers: chutes |
| [clickclack](/plugins/reference/clickclack) | Adds the Clickclack channel surface for sending and receiving OpenClaw messages. | `@openclaw/clickclack`<br />included in OpenClaw | channels: clickclack |
| [cloudflare-ai-gateway](/plugins/reference/cloudflare-ai-gateway) | Adds Cloudflare AI Gateway model provider support to OpenClaw. | `@openclaw/cloudflare-ai-gateway-provider`<br />included in OpenClaw | providers: cloudflare-ai-gateway |
| [codex-supervisor](/plugins/reference/codex-supervisor) | Supervise Codex app-server sessions from OpenClaw. | `@openclaw/codex-supervisor`<br />included in OpenClaw | contracts: tools |
| [comfy](/plugins/reference/comfy) | Adds ComfyUI model provider support to OpenClaw. | `@openclaw/comfy-provider`<br />included in OpenClaw | providers: comfy; contracts: imageGenerationProviders, musicGenerationProviders, videoGenerationProviders |
| [copilot-proxy](/plugins/reference/copilot-proxy) | Adds Copilot Proxy model provider support to OpenClaw. | `@openclaw/copilot-proxy`<br />included in OpenClaw | providers: copilot-proxy |
| [deepgram](/plugins/reference/deepgram) | Adds media understanding provider support. Adds realtime transcription provider support. | `@openclaw/deepgram-provider`<br />included in OpenClaw | contracts: mediaUnderstandingProviders, realtimeTranscriptionProviders |
@@ -125,6 +124,7 @@ commands.
| [telegram](/plugins/reference/telegram) | Adds the Telegram channel surface for sending and receiving OpenClaw messages. | `@openclaw/telegram`<br />included in OpenClaw | channels: telegram |
| [tencent](/plugins/reference/tencent) | Adds Tencent TokenHub model provider support to OpenClaw. | `@openclaw/tencent-provider`<br />included in OpenClaw | providers: tencent-tokenhub |
| [together](/plugins/reference/together) | Adds Together model provider support to OpenClaw. | `@openclaw/together-provider`<br />included in OpenClaw | providers: together; contracts: videoGenerationProviders |
| [tokenjuice](/plugins/reference/tokenjuice) | Compacts exec and bash tool results with tokenjuice reducers. | `@openclaw/tokenjuice`<br />included in OpenClaw | contracts: agentToolResultMiddleware |
| [tts-local-cli](/plugins/reference/tts-local-cli) | Adds text-to-speech provider support. | `@openclaw/tts-local-cli`<br />included in OpenClaw | contracts: speechProviders |
| [venice](/plugins/reference/venice) | Adds Venice model provider support to OpenClaw. | `@openclaw/venice-provider`<br />included in OpenClaw | providers: venice |
| [vercel-ai-gateway](/plugins/reference/vercel-ai-gateway) | Adds Vercel AI Gateway model provider support to OpenClaw. | `@openclaw/vercel-ai-gateway-provider`<br />included in OpenClaw | providers: vercel-ai-gateway |
@@ -134,7 +134,6 @@ commands.
| [vydra](/plugins/reference/vydra) | Adds Vydra model provider support to OpenClaw. | `@openclaw/vydra-provider`<br />included in OpenClaw | providers: vydra; contracts: imageGenerationProviders, speechProviders, videoGenerationProviders |
| [web-readability](/plugins/reference/web-readability) | Extract readable article content from local HTML web fetch responses. | `@openclaw/web-readability-plugin`<br />included in OpenClaw | contracts: webContentExtractors |
| [webhooks](/plugins/reference/webhooks) | Authenticated inbound webhooks that bind external automation to OpenClaw TaskFlows. | `@openclaw/webhooks`<br />included in OpenClaw | plugin |
| [workboard](/plugins/reference/workboard) | Dashboard workboard for agent-owned issues and sessions. | `@openclaw/workboard`<br />included in OpenClaw | contracts: tools |
| [xai](/plugins/reference/xai) | Adds xAI model provider support to OpenClaw. | `@openclaw/xai-plugin`<br />included in OpenClaw | providers: xai; contracts: imageGenerationProviders, mediaUnderstandingProviders, realtimeTranscriptionProviders, speechProviders, tools, videoGenerationProviders, webSearchProviders |
| [xiaomi](/plugins/reference/xiaomi) | Adds Xiaomi model provider support to OpenClaw. | `@openclaw/xiaomi-provider`<br />included in OpenClaw | providers: xiaomi; contracts: speechProviders |
| [zai](/plugins/reference/zai) | Adds Z.AI model provider support to OpenClaw. | `@openclaw/zai-provider`<br />included in OpenClaw | providers: zai; contracts: mediaUnderstandingProviders |
@@ -149,7 +148,6 @@ commands.
| [anthropic-vertex](/plugins/reference/anthropic-vertex) | OpenClaw Anthropic Vertex provider plugin for Claude models on Google Vertex AI. | `@openclaw/anthropic-vertex-provider`<br />npm; ClawHub | providers: anthropic-vertex |
| [brave](/plugins/reference/brave) | OpenClaw Brave Search provider plugin for web search. | `@openclaw/brave-plugin`<br />npm; ClawHub | contracts: webSearchProviders |
| [codex](/plugins/reference/codex) | OpenClaw Codex app-server harness and model provider plugin with a Codex-managed GPT catalog. | `@openclaw/codex`<br />npm; ClawHub | providers: codex; contracts: mediaUnderstandingProviders, migrationProviders |
| [copilot](/plugins/reference/copilot) | Registers the GitHub Copilot agent runtime. | `@openclaw/copilot`<br />npm; ClawHub: `clawhub:@openclaw/copilot` | plugin |
| [diagnostics-otel](/plugins/reference/diagnostics-otel) | OpenClaw diagnostics OpenTelemetry exporter for metrics and traces. | `@openclaw/diagnostics-otel`<br />npm; ClawHub: `clawhub:@openclaw/diagnostics-otel` | plugin |
| [diagnostics-prometheus](/plugins/reference/diagnostics-prometheus) | OpenClaw diagnostics Prometheus exporter for runtime metrics. | `@openclaw/diagnostics-prometheus`<br />npm; ClawHub: `clawhub:@openclaw/diagnostics-prometheus` | plugin |
| [diffs](/plugins/reference/diffs) | OpenClaw read-only diff viewer plugin and file renderer for agents. | `@openclaw/diffs`<br />npm; ClawHub | contracts: tools; skills |
@@ -166,12 +164,11 @@ commands.
| [nextcloud-talk](/plugins/reference/nextcloud-talk) | OpenClaw Nextcloud Talk channel plugin for conversations. | `@openclaw/nextcloud-talk`<br />npm; ClawHub | channels: nextcloud-talk |
| [nostr](/plugins/reference/nostr) | OpenClaw Nostr channel plugin for NIP-04 encrypted direct messages. | `@openclaw/nostr`<br />npm; ClawHub | channels: nostr |
| [openshell](/plugins/reference/openshell) | OpenClaw sandbox backend for the NVIDIA OpenShell CLI with mirrored local workspaces and SSH command execution. | `@openclaw/openshell-sandbox`<br />npm; ClawHub | plugin |
| [pixverse](/plugins/reference/pixverse) | OpenClaw PixVerse video generation provider plugin. | `@openclaw/pixverse-provider`<br />npm; ClawHub: `clawhub:@openclaw/pixverse-provider` | contracts: videoGenerationProviders |
| [pixverse](/plugins/reference/pixverse) | OpenClaw PixVerse video generation provider plugin. | `@openclaw/pixverse-provider`<br />npm; ClawHub | contracts: videoGenerationProviders |
| [qqbot](/plugins/reference/qqbot) | OpenClaw QQ Bot channel plugin for group and direct-message workflows. | `@openclaw/qqbot`<br />npm; ClawHub | channels: qqbot; contracts: tools; skills |
| [slack](/plugins/reference/slack) | OpenClaw Slack channel plugin for channels, DMs, commands, and app events. | `@openclaw/slack`<br />npm; ClawHub | channels: slack |
| [synology-chat](/plugins/reference/synology-chat) | Synology Chat channel plugin for OpenClaw channels and direct messages. | `@openclaw/synology-chat`<br />npm; ClawHub | channels: synology-chat |
| [tlon](/plugins/reference/tlon) | OpenClaw Tlon/Urbit channel plugin for chat workflows. | `@openclaw/tlon`<br />npm; ClawHub | channels: tlon; skills |
| [tokenjuice](/plugins/reference/tokenjuice) | Compacts exec and bash tool results with tokenjuice reducers. | `@openclaw/tokenjuice`<br />npm; ClawHub: `clawhub:@openclaw/tokenjuice` | contracts: agentToolResultMiddleware |
| [twitch](/plugins/reference/twitch) | OpenClaw Twitch channel plugin for chat and moderation workflows. | `@openclaw/twitch`<br />npm; ClawHub | channels: twitch |
| [voice-call](/plugins/reference/voice-call) | OpenClaw voice-call plugin for Twilio, Telnyx, and Plivo phone calls. | `@openclaw/voice-call`<br />npm; ClawHub | contracts: tools |
| [whatsapp](/plugins/reference/whatsapp) | OpenClaw WhatsApp channel plugin for WhatsApp Web chats. | `@openclaw/whatsapp`<br />ClawHub: `clawhub:@openclaw/whatsapp`; npm | channels: whatsapp |

View File

@@ -36,9 +36,7 @@ pnpm plugins:inventory:gen
| [clickclack](/plugins/reference/clickclack) | Adds the Clickclack channel surface for sending and receiving OpenClaw messages. | `@openclaw/clickclack`<br />included in OpenClaw | channels: clickclack |
| [cloudflare-ai-gateway](/plugins/reference/cloudflare-ai-gateway) | Adds Cloudflare AI Gateway model provider support to OpenClaw. | `@openclaw/cloudflare-ai-gateway-provider`<br />included in OpenClaw | providers: cloudflare-ai-gateway |
| [codex](/plugins/reference/codex) | OpenClaw Codex app-server harness and model provider plugin with a Codex-managed GPT catalog. | `@openclaw/codex`<br />npm; ClawHub | providers: codex; contracts: mediaUnderstandingProviders, migrationProviders |
| [codex-supervisor](/plugins/reference/codex-supervisor) | Supervise Codex app-server sessions from OpenClaw. | `@openclaw/codex-supervisor`<br />included in OpenClaw | contracts: tools |
| [comfy](/plugins/reference/comfy) | Adds ComfyUI model provider support to OpenClaw. | `@openclaw/comfy-provider`<br />included in OpenClaw | providers: comfy; contracts: imageGenerationProviders, musicGenerationProviders, videoGenerationProviders |
| [copilot](/plugins/reference/copilot) | Registers the GitHub Copilot agent runtime. | `@openclaw/copilot`<br />npm; ClawHub: `clawhub:@openclaw/copilot` | plugin |
| [copilot-proxy](/plugins/reference/copilot-proxy) | Adds Copilot Proxy model provider support to OpenClaw. | `@openclaw/copilot-proxy`<br />included in OpenClaw | providers: copilot-proxy |
| [deepgram](/plugins/reference/deepgram) | Adds media understanding provider support. Adds realtime transcription provider support. | `@openclaw/deepgram-provider`<br />included in OpenClaw | contracts: mediaUnderstandingProviders, realtimeTranscriptionProviders |
| [deepinfra](/plugins/reference/deepinfra) | Adds DeepInfra model provider support to OpenClaw. | `@openclaw/deepinfra-provider`<br />included in OpenClaw | providers: deepinfra; contracts: imageGenerationProviders, mediaUnderstandingProviders, memoryEmbeddingProviders, speechProviders, videoGenerationProviders |
@@ -99,7 +97,7 @@ pnpm plugins:inventory:gen
| [openrouter](/plugins/reference/openrouter) | Adds OpenRouter model provider support to OpenClaw. | `@openclaw/openrouter-provider`<br />included in OpenClaw | providers: openrouter; contracts: imageGenerationProviders, mediaUnderstandingProviders, musicGenerationProviders, speechProviders, videoGenerationProviders |
| [openshell](/plugins/reference/openshell) | OpenClaw sandbox backend for the NVIDIA OpenShell CLI with mirrored local workspaces and SSH command execution. | `@openclaw/openshell-sandbox`<br />npm; ClawHub | plugin |
| [perplexity](/plugins/reference/perplexity) | Adds web search provider support. | `@openclaw/perplexity-plugin`<br />included in OpenClaw | contracts: webSearchProviders |
| [pixverse](/plugins/reference/pixverse) | OpenClaw PixVerse video generation provider plugin. | `@openclaw/pixverse-provider`<br />npm; ClawHub: `clawhub:@openclaw/pixverse-provider` | contracts: videoGenerationProviders |
| [pixverse](/plugins/reference/pixverse) | OpenClaw PixVerse video generation provider plugin. | `@openclaw/pixverse-provider`<br />npm; ClawHub | contracts: videoGenerationProviders |
| [policy](/plugins/reference/policy) | Adds policy-backed doctor checks for workspace conformance. | `@openclaw/policy`<br />included in OpenClaw | plugin |
| [qa-channel](/plugins/reference/qa-channel) | Adds the QA Channel surface for sending and receiving OpenClaw messages. | `@openclaw/qa-channel`<br />source checkout only | channels: qa-channel |
| [qa-lab](/plugins/reference/qa-lab) | OpenClaw QA lab plugin with private debugger UI and scenario runner. | `@openclaw/qa-lab`<br />source checkout only | plugin |
@@ -122,7 +120,7 @@ pnpm plugins:inventory:gen
| [tencent](/plugins/reference/tencent) | Adds Tencent TokenHub model provider support to OpenClaw. | `@openclaw/tencent-provider`<br />included in OpenClaw | providers: tencent-tokenhub |
| [tlon](/plugins/reference/tlon) | OpenClaw Tlon/Urbit channel plugin for chat workflows. | `@openclaw/tlon`<br />npm; ClawHub | channels: tlon; skills |
| [together](/plugins/reference/together) | Adds Together model provider support to OpenClaw. | `@openclaw/together-provider`<br />included in OpenClaw | providers: together; contracts: videoGenerationProviders |
| [tokenjuice](/plugins/reference/tokenjuice) | Compacts exec and bash tool results with tokenjuice reducers. | `@openclaw/tokenjuice`<br />npm; ClawHub: `clawhub:@openclaw/tokenjuice` | contracts: agentToolResultMiddleware |
| [tokenjuice](/plugins/reference/tokenjuice) | Compacts exec and bash tool results with tokenjuice reducers. | `@openclaw/tokenjuice`<br />included in OpenClaw | contracts: agentToolResultMiddleware |
| [tts-local-cli](/plugins/reference/tts-local-cli) | Adds text-to-speech provider support. | `@openclaw/tts-local-cli`<br />included in OpenClaw | contracts: speechProviders |
| [twitch](/plugins/reference/twitch) | OpenClaw Twitch channel plugin for chat and moderation workflows. | `@openclaw/twitch`<br />npm; ClawHub | channels: twitch |
| [venice](/plugins/reference/venice) | Adds Venice model provider support to OpenClaw. | `@openclaw/venice-provider`<br />included in OpenClaw | providers: venice |
@@ -135,7 +133,6 @@ pnpm plugins:inventory:gen
| [web-readability](/plugins/reference/web-readability) | Extract readable article content from local HTML web fetch responses. | `@openclaw/web-readability-plugin`<br />included in OpenClaw | contracts: webContentExtractors |
| [webhooks](/plugins/reference/webhooks) | Authenticated inbound webhooks that bind external automation to OpenClaw TaskFlows. | `@openclaw/webhooks`<br />included in OpenClaw | plugin |
| [whatsapp](/plugins/reference/whatsapp) | OpenClaw WhatsApp channel plugin for WhatsApp Web chats. | `@openclaw/whatsapp`<br />ClawHub: `clawhub:@openclaw/whatsapp`; npm | channels: whatsapp |
| [workboard](/plugins/reference/workboard) | Dashboard workboard for agent-owned issues and sessions. | `@openclaw/workboard`<br />included in OpenClaw | contracts: tools |
| [xai](/plugins/reference/xai) | Adds xAI model provider support to OpenClaw. | `@openclaw/xai-plugin`<br />included in OpenClaw | providers: xai; contracts: imageGenerationProviders, mediaUnderstandingProviders, realtimeTranscriptionProviders, speechProviders, tools, videoGenerationProviders, webSearchProviders |
| [xiaomi](/plugins/reference/xiaomi) | Adds Xiaomi model provider support to OpenClaw. | `@openclaw/xiaomi-provider`<br />included in OpenClaw | providers: xiaomi; contracts: speechProviders |
| [zai](/plugins/reference/zai) | Adds Z.AI model provider support to OpenClaw. | `@openclaw/zai-provider`<br />included in OpenClaw | providers: zai; contracts: mediaUnderstandingProviders |

View File

@@ -1,23 +0,0 @@
---
summary: "Supervise Codex app-server sessions from OpenClaw."
read_when:
- You are installing, configuring, or auditing the codex-supervisor plugin
title: "Codex Supervisor plugin"
---
# Codex Supervisor plugin
Supervise Codex app-server sessions from OpenClaw.
## Distribution
- Package: `@openclaw/codex-supervisor`
- Install route: included in OpenClaw
## Surface
contracts: tools
## Session Listing
`codex_sessions_list` defaults to loaded Codex sessions only. Set `include_stored` to include stored history; the plugin uses Codex app-server's state-DB-only listing path and caps stored results at 200 by default. Pass `max_stored_sessions` to lower or raise that cap, up to 1000.

View File

@@ -1,23 +0,0 @@
---
summary: "Registers the GitHub Copilot agent runtime."
read_when:
- You are installing, configuring, or auditing the copilot plugin
title: "Copilot plugin"
---
# Copilot plugin
Registers the GitHub Copilot agent runtime.
## Distribution
- Package: `@openclaw/copilot`
- Install route: npm; ClawHub: `clawhub:@openclaw/copilot`
## Surface
plugin
## Related docs
- [copilot](/plugins/copilot)

View File

@@ -12,7 +12,7 @@ OpenClaw PixVerse video generation provider plugin.
## Distribution
- Package: `@openclaw/pixverse-provider`
- Install route: npm; ClawHub: `clawhub:@openclaw/pixverse-provider`
- Install route: npm; ClawHub
## Surface

View File

@@ -18,55 +18,6 @@ Adds policy-backed doctor checks for workspace conformance.
plugin
<!-- openclaw-plugin-reference:manual-start -->
## Behavior
The Policy plugin contributes doctor health checks for policy-managed OpenClaw
settings and governed workspace declarations. Policy currently covers channel
conformance, governed tool metadata, MCP server posture, model-provider posture,
private-network access posture, Gateway exposure posture, agent workspace/tool
posture, configured global/per-agent tool posture, configured sandbox runtime
posture, ingress/channel access posture, and OpenClaw config secret
provider/auth profile posture.
Policy stores authored requirements in `policy.jsonc`, observes existing
OpenClaw settings and workspace declarations as evidence, and reports drift
through `openclaw policy check` and `openclaw doctor --lint`. A clean policy
check emits policy, evidence, findings, and attestation hashes that operators
can record for audit.
`openclaw policy compare --baseline <file>` compares one policy file to another
policy file. It is config-level conformance only: it uses policy rule metadata
to verify that the checked policy is not missing or weaker than the authored
baseline, and it does not inspect runtime state, credentials, or secret values.
Tool posture rules can require approved profiles, workspace-only filesystem
tools, bounded exec security/ask/host settings, disabled elevated mode, exact
`alsoAllow` entries, and required tool deny entries. The evidence records
additive `alsoAllow` entries because they can widen effective tool posture.
These checks observe config conformance only; they do not read runtime approval
state or add runtime enforcement.
Sandbox posture rules can require approved sandbox modes/backends, deny host
container networking, deny container namespace joins, require read-only container
mounts, deny container runtime socket mounts and unconfined container profiles,
and require sandbox browser CDP source ranges.
These checks observe config conformance only; they do not read runtime approval
state, inspect live containers, or add runtime enforcement.
Named policy scopes under `scopes.<scopeName>` can add stricter normal policy
sections for the selector they list. `agentIds` supports `tools`,
`agents.workspace`, and `sandbox`; `channelIds` supports `ingress.channels`.
Runtime agent ids that are not explicitly listed in `agents.list[]` are checked
against inherited global/default posture rather than silently passing with no
evidence. Every scope present in `policy.jsonc` must be valid and enforceable
for its selector. Overlay rules are additional claims, so they do not weaken
top-level policy and can produce their own findings when the same observed
config violates both scopes.
<!-- openclaw-plugin-reference:manual-end -->
## Related docs
- [policy](/cli/policy)

View File

@@ -12,7 +12,7 @@ Compacts exec and bash tool results with tokenjuice reducers.
## Distribution
- Package: `@openclaw/tokenjuice`
- Install route: npm; ClawHub: `clawhub:@openclaw/tokenjuice`
- Install route: included in OpenClaw
## Surface

View File

@@ -1,23 +0,0 @@
---
summary: "Dashboard workboard for agent-owned issues and sessions."
read_when:
- You are installing, configuring, or auditing the workboard plugin
title: "Workboard plugin"
---
# Workboard plugin
Dashboard workboard for agent-owned issues and sessions.
## Distribution
- Package: `@openclaw/workboard`
- Install route: included in OpenClaw
## Surface
contracts: tools
## Related docs
- [workboard](/plugins/workboard)

View File

@@ -238,9 +238,9 @@ model entry:
{
"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"
}

View File

@@ -155,7 +155,6 @@ Most channel plugins do not need approval-specific code.
- If custom approval auth intentionally allows only same-chat fallback, return `markImplicitSameChatApprovalAuthorization({ authorized: true })` from `openclaw/plugin-sdk/approval-auth-runtime`; otherwise core treats the result as explicit approver authorization.
- If a channel-owned native callback resolves approvals directly, use `isImplicitSameChatApprovalAuthorization(...)` before resolving so implicit fallback still goes through the channel's normal actor authorization.
- If a channel needs native approval delivery, keep channel code focused on target normalization plus transport/presentation facts. Use `createChannelExecApprovalProfile`, `createChannelNativeOriginTargetResolver`, `createChannelApproverDmTargetResolver`, and `createApproverRestrictedNativeApprovalCapability` from `openclaw/plugin-sdk/approval-runtime`. Put the channel-specific facts behind `approvalCapability.nativeRuntime`, ideally via `createChannelApprovalNativeRuntimeAdapter(...)` or `createLazyChannelApprovalNativeRuntimeAdapter(...)`, so core can assemble the handler and own request filtering, routing, dedupe, expiry, gateway subscription, and routed-elsewhere notices. `nativeRuntime` is split into a few smaller seams:
- Use `createNativeApprovalChannelRouteGates` from `openclaw/plugin-sdk/approval-native-runtime` when a channel supports both session-origin native delivery and explicit approval forwarding targets. The helper centralizes approval config selection, `mode` handling, agent/session filters, account binding, session-target matching, and target-list matching while callers still own the channel id, default forwarding mode, account lookup, transport-enabled check, target normalization, and turn-source target resolution. Do not use it to create core-owned channel policy defaults; pass the channel's documented default mode explicitly.
- `createChannelNativeOriginTargetResolver` uses the shared channel-route matcher by default for `{ to, accountId, threadId }` targets. Pass `targetsMatch` only when a channel has provider-specific equivalence rules, such as Slack timestamp prefix matching.
- Pass `normalizeTargetForMatch` to `createChannelNativeOriginTargetResolver` when the channel needs to canonicalize provider ids before the default route matcher or a custom `targetsMatch` callback runs, while preserving the original target for delivery. Use `normalizeTarget` only when the resolved delivery target itself should be canonicalized.
- `availability` - whether the account is configured and whether a request should be handled

View File

@@ -568,7 +568,6 @@ releases.
| `plugin-sdk/dedupe-runtime` | Dedupe helpers | In-memory dedupe caches |
| `plugin-sdk/file-access-runtime` | File access helpers | Safe local-file/media path helpers |
| `plugin-sdk/transport-ready-runtime` | Transport readiness helpers | `waitForTransportReady` |
| `plugin-sdk/exec-approvals-runtime` | Exec approval policy helpers | `loadExecApprovals`, `resolveExecApprovalsFromFile`, `ExecApprovalsFile` |
| `plugin-sdk/collection-runtime` | Bounded cache helpers | `pruneMapToMaxSize` |
| `plugin-sdk/diagnostic-runtime` | Diagnostic gating helpers | `isDiagnosticFlagEnabled`, `isDiagnosticsEnabled` |
| `plugin-sdk/error-runtime` | Error formatting helpers | `formatUncaughtError`, `isApprovalNotFoundError`, error graph helpers |

View File

@@ -144,7 +144,6 @@ and pairing-path families.
| `plugin-sdk/self-hosted-provider-setup` | Focused OpenAI-compatible self-hosted provider setup helpers |
| `plugin-sdk/cli-backend` | CLI backend defaults + watchdog constants |
| `plugin-sdk/provider-auth-runtime` | Runtime API-key resolution helpers for provider plugins |
| `plugin-sdk/provider-oauth-runtime` | Generic provider OAuth callback types, callback-page rendering, PKCE/state helpers, and abort helpers |
| `plugin-sdk/provider-auth-api-key` | API-key onboarding/profile-write helpers such as `upsertApiKeyProfile` |
| `plugin-sdk/provider-auth-result` | Standard OAuth auth-result builder |
| `plugin-sdk/provider-env-vars` | Provider auth env-var lookup helpers |
@@ -180,7 +179,7 @@ and pairing-path families.
| `plugin-sdk/approval-gateway-runtime` | Shared approval gateway-resolution helper |
| `plugin-sdk/approval-handler-adapter-runtime` | Lightweight native approval adapter loading helpers for hot channel entrypoints |
| `plugin-sdk/approval-handler-runtime` | Broader approval handler runtime helpers; prefer the narrower adapter/gateway seams when they are enough |
| `plugin-sdk/approval-native-runtime` | Native approval target, account-binding, route-gate, forwarding fallback, and local native exec prompt suppression helpers |
| `plugin-sdk/approval-native-runtime` | Native approval target + account-binding helpers and local native exec prompt suppression |
| `plugin-sdk/approval-reaction-runtime` | Hardcoded approval reaction bindings, reaction prompt payloads, reaction target stores, and compatibility export for local native exec prompt suppression |
| `plugin-sdk/approval-reply-runtime` | Exec/plugin approval reply payload helpers |
| `plugin-sdk/approval-runtime` | Exec/plugin approval payload helpers, native approval routing/runtime helpers, and structured approval display helpers such as `formatApprovalDisplayPath` |
@@ -193,7 +192,6 @@ and pairing-path families.
| `plugin-sdk/allow-from` | `formatAllowFromLowercase` |
| `plugin-sdk/channel-secret-runtime` | Narrow secret-contract collection helpers for channel/plugin secret surfaces |
| `plugin-sdk/secret-ref-runtime` | Narrow `coerceSecretRef` and SecretRef typing helpers for secret-contract/config parsing |
| `plugin-sdk/secret-provider-integration` | Type-only SecretRef provider integration manifest and preset contracts for plugins that publish external secret provider presets |
| `plugin-sdk/security-runtime` | Shared trust, DM gating, root-bounded file/path helpers including create-only writes, sync/async atomic file replacement, sibling temp writes, cross-device move fallback, private file-store helpers, symlink-parent guards, external-content, sensitive text redaction, constant-time secret comparison, and secret-collection helpers |
| `plugin-sdk/ssrf-policy` | Host allowlist and private-network SSRF policy helpers |
| `plugin-sdk/ssrf-dispatcher` | Narrow pinned-dispatcher helpers without the broad infra runtime surface |
@@ -221,7 +219,6 @@ and pairing-path families.
| `plugin-sdk/lazy-runtime` | Lazy runtime import/binding helpers such as `createLazyRuntimeModule`, `createLazyRuntimeMethod`, and `createLazyRuntimeSurface` |
| `plugin-sdk/process-runtime` | Process exec helpers |
| `plugin-sdk/cli-runtime` | CLI formatting, wait, version, argument-invocation, and lazy command-group helpers |
| `plugin-sdk/qa-live-transport-scenarios` | Shared live transport QA scenario ids, baseline coverage helpers, and scenario-selection helper |
| `plugin-sdk/gateway-method-runtime` | Reserved Gateway method dispatch helper for plugin HTTP routes that declare `contracts.gatewayMethodDispatch: ["authenticated-request"]` |
| `plugin-sdk/gateway-runtime` | Gateway client, event-loop-ready client start helper, gateway CLI RPC, gateway protocol errors, and channel-status patch helpers |
| `plugin-sdk/config-contracts` | Focused type-only config surface for plugin config shapes such as `OpenClawConfig` and channel/provider config types |
@@ -240,7 +237,6 @@ and pairing-path families.
| `plugin-sdk/session-store-runtime` | Session workflow helpers (`getSessionEntry`, `listSessionEntries`, `patchSessionEntry`, `upsertSessionEntry`), legacy session store path/session-key helpers, updated-at reads, and deprecated whole-store mutation helpers |
| `plugin-sdk/cron-store-runtime` | Cron store path/load/save helpers |
| `plugin-sdk/state-paths` | State/OAuth dir path helpers |
| `plugin-sdk/plugin-state-runtime` | Plugin sidecar SQLite keyed-state types |
| `plugin-sdk/routing` | Route/session-key/account binding helpers such as `resolveAgentRoute`, `buildAgentSessionKey`, and `resolveDefaultAgentBoundAccountId` |
| `plugin-sdk/status-helpers` | Shared channel/account status summary helpers, runtime-state defaults, and issue metadata helpers |
| `plugin-sdk/target-resolver-runtime` | Shared target resolver helpers |
@@ -285,7 +281,6 @@ and pairing-path families.
| `plugin-sdk/secure-random-runtime` | Secure token/UUID helpers |
| `plugin-sdk/system-event-runtime` | System event queue helpers |
| `plugin-sdk/transport-ready-runtime` | Transport readiness wait helper |
| `plugin-sdk/exec-approvals-runtime` | Exec approval policy file helpers without the broad infra-runtime barrel |
| `plugin-sdk/infra-runtime` | Deprecated compatibility shim; use the focused runtime subpaths above |
| `plugin-sdk/collection-runtime` | Small bounded cache helpers |
| `plugin-sdk/diagnostic-runtime` | Diagnostic flag, event, and trace-context helpers |

View File

@@ -115,7 +115,7 @@ Voice-call credentials accept SecretRefs. `plugins.entries.voice-call.config.twi
responseSystemPrompt: "You are a concise baseball card specialist.",
tts: {
providers: {
openai: { speakerVoice: "alloy" },
openai: { voice: "alloy" },
},
},
},
@@ -322,7 +322,7 @@ for tool work, current information, memory lookups, or workspace state.
google: {
apiKey: "${GEMINI_API_KEY}",
model: "gemini-2.5-flash-native-audio-preview-12-2025",
speakerVoice: "Kore",
voice: "Kore",
silenceDurationMs: 500,
startSensitivity: "high",
},
@@ -455,7 +455,7 @@ speech on calls. You can override it under the plugin config with the
provider: "elevenlabs",
providers: {
elevenlabs: {
speakerVoiceId: "pMsXgVXv3BLzUgSXRplE",
voiceId: "pMsXgVXv3BLzUgSXRplE",
modelId: "eleven_multilingual_v2",
},
},
@@ -486,7 +486,7 @@ Behavior notes:
tts: {
provider: "openai",
providers: {
openai: { speakerVoice: "alloy" },
openai: { voice: "alloy" },
},
},
},
@@ -505,7 +505,7 @@ Behavior notes:
providers: {
elevenlabs: {
apiKey: "elevenlabs_key",
speakerVoiceId: "pMsXgVXv3BLzUgSXRplE",
voiceId: "pMsXgVXv3BLzUgSXRplE",
modelId: "eleven_multilingual_v2",
},
},
@@ -528,7 +528,7 @@ Behavior notes:
providers: {
openai: {
model: "gpt-4o-mini-tts",
speakerVoice: "marin",
voice: "marin",
},
},
},
@@ -599,7 +599,7 @@ you can usually override only the provider voice:
tts: {
provider: "openai",
providers: {
openai: { speakerVoice: "coral" },
openai: { voice: "coral" },
},
},
numbers: {
@@ -608,7 +608,7 @@ you can usually override only the provider voice:
responseSystemPrompt: "You are a concise baseball card specialist.",
tts: {
providers: {
openai: { speakerVoice: "alloy" },
openai: { voice: "alloy" },
},
},
},

View File

@@ -1,226 +0,0 @@
---
summary: "Optional dashboard workboard for agent-owned cards and session handoff"
read_when:
- You want a Kanban-style workboard in the Control UI
- You are enabling or disabling the bundled Workboard plugin
- You want to track planned agent work without an external project manager
title: "Workboard plugin"
---
The Workboard plugin adds an optional Kanban-style board to the
[Control UI](/web/control-ui). Use it to collect agent-sized work cards, assign
them to agents, and jump from a card into the linked dashboard session.
Workboard is intentionally small. It tracks local operating work for an
OpenClaw Gateway; it is not a replacement for GitHub Issues, Linear, Jira, or
other team project management systems.
## Default state
Workboard is a bundled plugin and is disabled by default unless you enable it
in plugin config.
Enable it with:
```bash
openclaw plugins enable workboard
openclaw gateway restart
```
Then open the dashboard:
```bash
openclaw dashboard
```
The Workboard tab appears in the dashboard navigation. If the tab is visible
but the plugin is disabled or blocked by `plugins.allow` / `plugins.deny`, the
view shows a plugin-unavailable state instead of local card data.
## What cards contain
Each card stores:
- title and notes
- status: `backlog`, `todo`, `running`, `review`, `blocked`, or `done`
- priority: `low`, `normal`, `high`, or `urgent`
- labels
- optional agent id
- optional linked session, run, task, or source URL
- optional execution metadata for a Codex or Claude session started from the card
- compact metadata for attempts, comments, links, proof, artifacts, claims, diagnostics, notifications, templates, archive state, and stale-session detection
- recent card events such as created, moved, linked, claimed, heartbeat, attempt, proof, artifact, diagnostic, notification, archive, stale, or agent-updated changes
Cards are stored in the plugin's Gateway state. They are local to the Gateway
state directory and move with the rest of that Gateway's OpenClaw state.
Workboard keeps compact per-card metadata so operators can see how a card moved
through the board without opening the linked session. Events, attempt summaries,
proof snippets, related links, comments, archive markers, and stale-session
markers are intentionally local metadata; they do not replace session
transcripts or GitHub issue history.
## Card executions
Unlinked cards can start work from the card. Start uses the Gateway's configured
default agent and model. Codex and Claude actions are optional explicit model
choices:
- Run Codex or Run Claude creates a dashboard session, sends the card prompt,
and marks the card `running`.
- Open Codex or Open Claude creates a linked dashboard session without sending
the card prompt or moving the card, so you can work manually while it stays
attached to the board.
Execution metadata stores the selected engine, mode, model ref, session key,
run id, and lifecycle status on the card. Codex executions use
`openai/gpt-5.5`; Claude executions use `anthropic/claude-sonnet-4-6`.
Each linked execution also records an attempt summary on the same card record.
The attempt summary keeps the engine, mode, model, run id, timestamps, status,
and rolling failure count so repeated failures remain visible on the board.
## Agent coordination
Workboard also exposes optional agent tools for board-aware workflows:
- `workboard_list` lists compact cards with claim and diagnostic state.
- `workboard_read` returns one card plus bounded worker context built from notes,
attempts, comments, links, proof, artifacts, and active diagnostics.
- `workboard_claim` claims a card for the calling agent and moves backlog or todo
cards into `running`.
- `workboard_heartbeat` refreshes the claim heartbeat during longer runs.
- `workboard_release` releases the claim after completion, pause, or handoff and
can move the card to a next status.
- `workboard_comment`, `workboard_proof`, and `workboard_unblock` let an agent
add handoff notes, attach proof or artifact references, and move blocked work
back to `todo`.
Claimed cards reject agent-tool mutations from other agents unless the caller
has the claim token returned by `workboard_claim`. Dashboard operators still use
the normal Gateway RPC surface and can recover or reassign cards.
Workboard diagnostics are computed from local card metadata. The built-in checks
flag assigned cards that wait too long, running cards without recent heartbeat,
blocked cards that need attention, repeated failures, done cards without proof,
and running cards that only have a loose session link.
## Session lifecycle sync
Cards can be linked to existing dashboard sessions or to the session created
when you start work from a card. Linked cards show the session lifecycle inline:
running, stale, linked idle, done, failed, or missing.
If the linked session is missing, the card stays linked for context and still
offers start controls so you can restart work into a fresh dashboard session.
If an active linked session stops reporting recent activity, Workboard marks the
card stale and stores the marker as card metadata until the lifecycle clears it.
You can also capture an existing dashboard session from the Sessions tab with
Add to Workboard. The card is linked to that session, uses the session label or
recent user prompt as the title, and seeds notes from the recent user prompt plus
the latest assistant response when chat history is available.
Workboard follows the linked session while the card is still in an active work
state:
- active linked session -> `running`
- completed linked session -> `review`
- failed, killed, timed out, or aborted linked session -> `blocked`
Manual review states win. If you move a card to `review`, `blocked`, or `done`,
Workboard stops auto-moving that card until you move it back to `todo` or
`running`.
## Dashboard workflow
1. Open the Workboard tab in the Control UI.
2. Create a card with a title, notes, priority, labels, optional agent, and
optional linked session.
3. Or open Sessions and choose Add to Workboard for an existing session.
4. Drag the card between columns or use the column controls.
5. Start work from the card to create or reuse a dashboard session.
6. Open the linked session from the card while the agent works.
7. Let lifecycle sync move running work into review or blocked, then manually
move the card to done when accepted.
Starting a card uses normal Gateway sessions. The Workboard plugin only stores
card metadata and links; the conversation transcript, model selection, and run
lifecycle stay owned by the regular session system.
Use Stop on a live linked card to abort the active session run. Workboard marks
that card `blocked` so it remains visible for follow-up.
New cards can start from Workboard templates for bugfixes, docs, releases, PR
reviews, or plugin work. Templates prefill title, notes, labels, and priority,
and the selected template id is stored as card metadata.
## Permissions
The plugin registers Gateway RPC methods under the `workboard.*` namespace:
- `workboard.cards.list` requires `operator.read`
- `workboard.cards.export` requires `operator.read`
- `workboard.cards.diagnostics` requires `operator.read`
- `workboard.cards.diagnostics.refresh` requires `operator.write`
- create, update, move, delete, comment, link, proof, artifact, claim, heartbeat,
release, unblock, bulk, and archive methods require `operator.write`
Browsers connected with read-only operator access can inspect the board but
cannot mutate cards.
## Configuration
Workboard has no plugin-specific config today. Enable or disable it with the
standard plugin entry:
```json5
{
plugins: {
entries: {
workboard: {
enabled: true,
config: {},
},
},
},
}
```
Disable it again with:
```bash
openclaw plugins disable workboard
openclaw gateway restart
```
## Troubleshooting
### The tab says Workboard is unavailable
Check plugin policy:
```bash
openclaw plugins inspect workboard --runtime --json
```
If `plugins.allow` is configured, add `workboard` to that allowlist. If
`plugins.deny` contains `workboard`, remove it before enabling the plugin.
### Cards do not save
Confirm the browser connection has `operator.write` access. Read-only operator
sessions can list cards but cannot create, edit, move, or delete them.
### Starting a card does not open the expected session
Workboard creates links to normal dashboard sessions. Check the card's agent id
and linked session, then open the Sessions or Chat view to inspect the actual
run state.
## Related
- [Control UI](/web/control-ui)
- [Plugins](/tools/plugin)
- [Manage plugins](/plugins/manage-plugins)
- [Sessions](/concepts/session)

View File

@@ -8,27 +8,22 @@ title: "Anthropic"
Anthropic builds the **Claude** model family. OpenClaw supports two auth routes:
- **API key** — direct Anthropic API access with usage-based billing (`anthropic/*` models)
- **Claude CLI** — reuse an existing Claude Code login on the same host
- **Claude CLI** — reuse an existing Claude CLI login on the same host
<Warning>
OpenClaw's Claude CLI backend runs the installed Claude Code CLI in
non-interactive print mode. Anthropic's current Claude Code docs describe
`claude -p` as Agent SDK/programmatic usage. Starting June 15, 2026, Anthropic
says subscription-plan `claude -p` usage no longer draws from normal Claude
plan limits; it draws from a separate monthly Agent SDK credit first, then from
usage credits at standard API rates when those credits are enabled.
Anthropic staff told us OpenClaw-style Claude CLI usage is allowed again, so
OpenClaw treats Claude CLI reuse and `claude -p` usage as sanctioned unless
Anthropic publishes a new policy.
Interactive Claude Code still draws from the signed-in Claude plan limits. API
key auth remains direct pay-as-you-go API billing. For long-lived gateway hosts,
shared automation, and predictable production spend, use an Anthropic API key.
For long-lived gateway hosts, Anthropic API keys are still the clearest and
most predictable production path.
Anthropic's current public docs:
- [Claude Code CLI reference](https://code.claude.com/docs/en/cli-usage)
- [Use the Claude Agent SDK with your Claude plan](https://support.claude.com/en/articles/15036540-use-the-claude-agent-sdk-with-your-claude-plan)
- [Use Claude Code with your Pro or Max plan](https://support.claude.com/en/articles/11145838-use-claude-code-with-your-pro-or-max-plan)
- [Use Claude Code with your Team or Enterprise plan](https://support.claude.com/en/articles/11845131-using-claude-code-with-your-team-or-enterprise-plan)
- [Manage Claude Code costs](https://code.claude.com/docs/en/costs)
- [Claude Code CLI reference](https://code.claude.com/docs/en/cli-reference)
- [Claude Agent SDK overview](https://platform.claude.com/docs/en/agent-sdk/overview)
- [Using Claude Code with your Pro or Max plan](https://support.claude.com/en/articles/11145838-using-claude-code-with-your-pro-or-max-plan)
- [Using Claude Code with your Team or Enterprise plan](https://support.anthropic.com/en/articles/11845131-using-claude-code-with-your-team-or-enterprise-plan/)
</Warning>
@@ -66,7 +61,7 @@ Anthropic's current public docs:
```json5
{
env: { ANTHROPIC_API_KEY: "example-anthropic-key-not-real" },
agents: { defaults: { model: { primary: "anthropic/claude-opus-4-8" } } },
agents: { defaults: { model: { primary: "anthropic/claude-opus-4-6" } } },
}
```
@@ -118,9 +113,9 @@ Anthropic's current public docs:
{
agents: {
defaults: {
model: { primary: "anthropic/claude-opus-4-8" },
model: { primary: "anthropic/claude-opus-4-7" },
models: {
"anthropic/claude-opus-4-8": {
"anthropic/claude-opus-4-7": {
agentRuntime: { id: "claude-cli" },
},
},
@@ -133,36 +128,16 @@ Anthropic's current public docs:
compatibility, but new config should keep provider/model selection as
`anthropic/*` and put the execution backend in provider/model runtime policy.
### Billing and `claude -p`
OpenClaw uses Claude Code's non-interactive `claude -p` path for Claude CLI
runs. Anthropic currently treats that path as Agent SDK/programmatic usage:
- Until June 15, 2026, subscription-plan handling follows Anthropic's active
Claude Code rules for the signed-in account.
- Starting June 15, 2026, subscription-plan `claude -p` usage draws from the
user's monthly Agent SDK credit first, then from usage credits at standard
API rates if usage credits are enabled.
- Console/API-key logins use pay-as-you-go API billing and do not receive
the subscription Agent SDK credit.
Anthropic can change Claude Code billing and rate-limit behavior without an
OpenClaw release. Check `claude auth status`, `/status`, and
Anthropic's linked docs when billing predictability matters.
<Tip>
For shared production automation, use an Anthropic API key instead of
Claude CLI. OpenClaw also supports subscription-style options from
[OpenAI Codex](/providers/openai), [Qwen Cloud](/providers/qwen),
[MiniMax](/providers/minimax), and [Z.AI / GLM](/providers/zai).
If you want the clearest billing path, use an Anthropic API key instead. OpenClaw also supports subscription-style options from [OpenAI Codex](/providers/openai), [Qwen Cloud](/providers/qwen), [MiniMax](/providers/minimax), and [Z.AI / GLM](/providers/zai).
</Tip>
</Tab>
</Tabs>
## Thinking defaults (Claude 4.8 and 4.6)
## Thinking defaults (Claude 4.6)
Claude Opus 4.8 keeps thinking off by default in OpenClaw. When you explicitly enable adaptive thinking with `/think high|xhigh|max`, OpenClaw sends Anthropic's Opus 4.8 effort values; Claude 4.6 models default to `adaptive`.
Claude 4.6 models default to `adaptive` thinking in OpenClaw when no explicit thinking level is set.
Override per-message with `/think:<level>` or in model params:
@@ -171,8 +146,8 @@ Override per-message with `/think:<level>` or in model params:
agents: {
defaults: {
models: {
"anthropic/claude-opus-4-8": {
params: { thinking: "high" },
"anthropic/claude-opus-4-6": {
params: { thinking: "adaptive" },
},
},
},
@@ -292,7 +267,7 @@ OpenClaw supports Anthropic's prompt caching feature for API-key auth.
| Property | Value |
| --------------- | --------------------- |
| Default model | `claude-opus-4-8` |
| Default model | `claude-opus-4-7` |
| Supported input | Images, PDF documents |
When an image or PDF is attached to a conversation, OpenClaw automatically
@@ -302,7 +277,7 @@ OpenClaw supports Anthropic's prompt caching feature for API-key auth.
<Accordion title="1M context window">
Anthropic's 1M context window is available on GA-capable Claude 4.x models
such as Opus 4.8, Opus 4.7, Opus 4.6, and Sonnet 4.6. OpenClaw sizes those models at
such as Opus 4.6, Opus 4.7, and Sonnet 4.6. OpenClaw sizes those models at
1M automatically:
```json5
@@ -333,8 +308,8 @@ OpenClaw supports Anthropic's prompt caching feature for API-key auth.
</Accordion>
<Accordion title="Claude Opus 4.8 1M context">
`anthropic/claude-opus-4-8` and its `claude-cli` variant have a 1M context
<Accordion title="Claude Opus 4.7 1M context">
`anthropic/claude-opus-4-7` and its `claude-cli` variant have a 1M context
window by default — no `params.context1m: true` needed.
</Accordion>
</AccordionGroup>

View File

@@ -45,7 +45,7 @@ provider-owned output format through `X-Microsoft-OutputFormat`.
provider: "azure-speech",
providers: {
"azure-speech": {
speakerVoice: "en-US-JennyNeural",
voice: "en-US-JennyNeural",
lang: "en-US",
},
},
@@ -69,7 +69,7 @@ provider-owned output format through `X-Microsoft-OutputFormat`.
| `region` | `messages.tts.providers.azure-speech.region` | Azure Speech resource region. Falls back to `AZURE_SPEECH_REGION` or `SPEECH_REGION`. |
| `endpoint` | `messages.tts.providers.azure-speech.endpoint` | Optional Azure Speech endpoint/base URL override. |
| `baseUrl` | `messages.tts.providers.azure-speech.baseUrl` | Optional Azure Speech base URL override. |
| `speakerVoice` | `messages.tts.providers.azure-speech.speakerVoice` | Azure voice ShortName (default `en-US-JennyNeural`). Legacy alias: `voice`. |
| `voice` | `messages.tts.providers.azure-speech.voice` | Azure voice ShortName (default `en-US-JennyNeural`). |
| `lang` | `messages.tts.providers.azure-speech.lang` | SSML language code (default `en-US`). |
| `outputFormat` | `messages.tts.providers.azure-speech.outputFormat` | Audio-file output format (default `audio-24khz-48kbitrate-mono-mp3`). |
| `voiceNoteOutputFormat` | `messages.tts.providers.azure-speech.voiceNoteOutputFormat` | Voice-note output format (default `ogg-24khz-16bit-mono-opus`). |

Some files were not shown because too many files have changed in this diff Show More