Compare commits

..

1 Commits

Author SHA1 Message Date
Vincent Koc
806e796cc2 fix(install): keep llama cpp runtime required 2026-06-23 13:43:24 +08:00
1070 changed files with 8316 additions and 47864 deletions

View File

@@ -146,7 +146,7 @@ Default guidance:
Default Completeness bands:
- `Clawesome` (95-100): complete across expected workflows, variants, and
- `Lovable` (95-100): complete across expected workflows, variants, and
recovery branches, with only minor polish gaps.
- `Stable` (80-95): the expected workflow set is broadly present, with only
bounded missing branches.
@@ -172,7 +172,7 @@ Default Completeness bands:
Bands:
- `Clawesome`: 95-100
- `Lovable`: 95-100
- `Stable`: 80-95
- `Beta`: 70-80
- `Alpha`: 50-70

View File

@@ -1,196 +0,0 @@
---
name: openclaw-ci-limits
description: Manage OpenClaw GitHub Actions and Blacksmith CI capacity, runner-registration budgets, fanout caps, main-push debounce, shard sizing, hosted-runner offload, queue health, and safe ramp-down/ramp-up changes. Use when tuning `.github/workflows/*`, `docs/ci.md`, CI runner labels, matrix `max-parallel`, ClawSweeper/Blacksmith burst protection, CodeQL runner placement, or investigating slow/queued OpenClaw CI.
---
# OpenClaw CI Limits
Use this skill for CI capacity changes, not ordinary test failure triage. The
goal is to keep OpenClaw fast while staying below GitHub's self-hosted runner
registration edge limit.
## Core Facts
- The scarce resource is Blacksmith runner registrations, not Blacksmith vCPU
capacity.
- GitHub runner registrations are capped at 1,500 per 5 minutes per repository,
organization, or enterprise. The `openclaw` organization shares one bucket.
- Core REST quota does not draw down this bucket. Check
`actions_runner_registration` separately; core quota can be healthy while
runner registration is throttled.
- Use 1,000 registrations per 5 minutes as the operating target. Leave the last
third for other repos, retries, and burst overlap.
- Jobs that route, notify, summarize, choose shards, or run short CodeQL quality
scans should stay on GitHub-hosted runners unless measured evidence says
Blacksmith is required.
## First Checks
Before changing CI, collect current pressure:
```bash
ghx api rate_limit --jq '{core:.resources.core,graphql:.resources.graphql,search:.resources.search,actions_runner_registration:.resources.actions_runner_registration}'
ghx run list -R openclaw/openclaw --limit 20 --json databaseId,status,conclusion,workflowName,event,headBranch,createdAt,updatedAt,url
ghx run list -R openclaw/clawsweeper --limit 20 --json databaseId,status,conclusion,workflowName,event,headBranch,createdAt,updatedAt,url
curl -fsS https://clawsweeper.openclaw.ai/api/status | jq '{generated_at,fleet,diagnostics:{errors:.diagnostics.errors}}'
curl -fsS https://clawsweeper.openclaw.ai/api/exact-review-queue | jq '.'
node scripts/ci-run-timings.mjs --latest-main
node scripts/ci-run-timings.mjs --recent 10
```
Read:
- `.github/workflows/ci.yml`
- `.github/workflows/codeql-critical-quality.yml`
- `docs/ci.md`
- `test/scripts/ci-workflow-guards.test.ts`
- touched planner files under `scripts/lib/*ci*`, `scripts/lib/*test-plan*`, or
`scripts/ci-changed-scope.mjs`
## Diagnose The Bottleneck
Classify the issue before changing caps:
- **Runner-registration throttle:** many jobs queued before runner assignment,
Blacksmith/GitHub reports 403/429 or spam-style 422 responses from
`generate-jitconfig`, and API core quota is still healthy. Treat 422 as this
signal only when the request payload is otherwise valid. Fix burstiness and
Blacksmith job count.
- **Blacksmith capacity:** Blacksmith dashboard shows actual concurrency caps or
unavailable capacity. Do not solve this with GitHub workflow fanout alone.
- **OpenClaw test runtime:** jobs start quickly but one lane dominates wall time.
Use `$openclaw-test-performance` instead of runner tuning.
- **Real failing CI:** one job fails after starting. Use `$github:gh-fix-ci` or
`$openclaw-testing`, not this skill.
- **ClawSweeper backlog:** exact-review queue grows while CI is healthy. Tune
ClawSweeper workers in `openclaw/clawsweeper`, not OpenClaw CI.
## Registration Budget Math
Estimate worst-case registrations for a change before editing:
```text
new Blacksmith registrations ~= number of Blacksmith jobs that can become queued
inside one 5 minute window
```
For matrix jobs, count every row that can start in the 5-minute window.
`strategy.max-parallel` only caps simultaneous rows; short rows can turn over
and register more runners before the window resets. Use job duration, retries,
and queue turnover to justify any lower estimate. Add non-matrix Blacksmith jobs
such as `preflight`, `security-fast`, `build-artifacts`, and platform lanes.
For repeated pushes, multiply by the number of runs expected to reach
Blacksmith admission in the same 5-minute window, including runs canceled after
admission. The debounce only suppresses pushes that arrive while
`runner-admission` is still sleeping; once Blacksmith jobs register, those
registrations are spent even if a later push cancels the run. If timing is
uncertain, count every sequential push in the window.
Reject a change unless the org-level worst case stays below 1,000 registrations
per 5 minutes with headroom for ClawSweeper, ClawHub, Clownfish, OpenClaw RTT,
and Clawbench.
## Safe Levers
Prefer these in order:
1. Add or preserve concurrency groups that cancel superseded PR and canonical
`main` runs before Blacksmith work starts.
2. Keep the `runner-admission` hosted debounce for canonical `main` pushes.
Change `OPENCLAW_MAIN_CI_DEBOUNCE_SECONDS` only with evidence.
3. Move high-frequency, short, non-build jobs to `ubuntu-24.04`.
4. Reduce matrix rows by bundling related tests inside one runner job when the
combined job stays under timeout and keeps useful failure names.
5. Lower `strategy.max-parallel` for bursty Blacksmith matrices.
6. Right-size runners from timing evidence. Use fewer/larger jobs only when
elapsed time improves enough to justify registration count.
7. Split truly slow tests with `$openclaw-test-performance`; do not hide a slow
test problem by registering more runners.
Do not:
- add another Blacksmith installation expecting a higher registration bucket;
- move CodeQL Critical Quality back to Blacksmith;
- raise all `max-parallel` values at once;
- make manual `workflow_dispatch` runs cancel normal push/PR validation;
- delete coverage just to reduce runner count;
- treat cancelled superseded runs as failures without checking the newest run
for the same ref.
## Current OpenClaw Knobs
These are intentionally guarded by `test/scripts/ci-workflow-guards.test.ts`:
- `CI` concurrency key version and `cancel-in-progress` for PRs and canonical
`main` pushes.
- `runner-admission` on `ubuntu-24.04` with
`OPENCLAW_MAIN_CI_DEBOUNCE_SECONDS=90`.
- `preflight` and `security-fast` needing `runner-admission`.
- CI matrix caps: fast/check lanes at 8, compact Node PR plan at current caps,
Windows and Android at 2.
- `build-artifacts` on `blacksmith-16vcpu-ubuntu-2404`.
- lower-weight Node/check shards on `blacksmith-4vcpu-ubuntu-2404`.
- heavy retained Linux/Android shards on `blacksmith-8vcpu-ubuntu-2404`.
- CodeQL Critical Quality on `ubuntu-24.04` with no `blacksmith-` labels.
When changing one knob, update `docs/ci.md` and the guard test in the same PR.
## Validation
For workflow-only or docs/skill-only changes in a Codex worktree:
```bash
node scripts/run-vitest.mjs test/scripts/ci-workflow-guards.test.ts
node scripts/check-workflows.mjs
node scripts/docs-list.js
./node_modules/.bin/oxfmt --check .github/workflows/ci.yml .github/workflows/codeql-critical-quality.yml docs/ci.md test/scripts/ci-workflow-guards.test.ts .agents/skills/openclaw-ci-limits/SKILL.md .agents/skills/openclaw-ci-limits/agents/openai.yaml
git diff --check
```
If `pnpm docs:list` tries to reconcile dependencies in a linked Codex worktree,
stop and use `node scripts/docs-list.js`.
For a PR before requesting maintainer approval:
```bash
.agents/skills/autoreview/scripts/autoreview --mode branch --base origin/main
ghx pr checks <pr> -R openclaw/openclaw --watch --interval 15
```
Use hosted exact-head gates for CI workflow tuning. Do not burn local
`pnpm test` on unrelated full-suite proof.
Only after the maintainer explicitly asks you to prepare or land the PR, run the
repo-native mutating wrapper:
```bash
scripts/pr review-init <pr>
scripts/pr review-artifacts-init <pr>
scripts/pr review-validate-artifacts <pr>
OPENCLAW_TESTBOX=1 scripts/pr prepare-run <pr>
```
`prepare-run` can push a prepared commit to the PR branch. Only run
`scripts/pr merge-run <pr>` after the maintainer has explicitly asked you to
land the PR. Both commands mutate GitHub state.
## Post-Land Monitoring
After merge, watch at least one fresh main cycle and the adjacent repos:
```bash
ghx run list -R openclaw/openclaw --limit 20 --json databaseId,status,conclusion,workflowName,event,headBranch,createdAt,updatedAt,url
for repo in openclaw/clawsweeper openclaw/clawhub openclaw/clownfish openclaw/openclaw-rtt openclaw/clawbench; do
ghx run list -R "$repo" --limit 12 --json databaseId,status,conclusion,workflowName,event,headBranch,createdAt,updatedAt,url
done
curl -fsS https://clawsweeper.openclaw.ai/api/exact-review-queue | jq '.'
```
Report:
- exact PR/commit landed;
- expected registration reduction or added headroom;
- CI run status and slowest/queued jobs;
- ClawSweeper queue pending, dispatching, leased, oldest pending age;
- any real failures that remain outside runner registration.

View File

@@ -1,4 +0,0 @@
interface:
display_name: "OpenClaw CI Limits"
short_description: "Tune OpenClaw CI fanout and runner budgets"
default_prompt: "Use $openclaw-ci-limits to inspect OpenClaw CI pressure, tune runner-registration fanout safely, and document the exact validation before landing."

View File

@@ -1,23 +0,0 @@
# OpenClaw Maturity Scorecard Agent
You are refreshing the OpenClaw maturity score source for a release scorecard.
Goal: use the `$claw-score` skill to refresh `qa/maturity-scores.yaml` for every active surface in `taxonomy.yaml`, using the current repository and the release evidence artifacts in `.artifacts/maturity-evidence`.
Allowed tracked paths:
- `qa/maturity-scores.yaml`
Hard limits:
- Do not edit generated docs, taxonomy, workflows, scripts, package metadata, lockfiles, tests, or application code.
- Do not render docs. The workflow renders docs after validating the score source.
- Keep the score source schema valid for QA Lab maturity score validation.
Required workflow:
1. Use the `$claw-score` skill before editing.
2. Read `taxonomy.yaml`, any existing maturity score file, and the release evidence artifacts.
3. Refresh scores for every active surface in `taxonomy.yaml`.
4. Run the QA Lab maturity score validation used by this repository.
5. If no defensible score update is possible, leave a valid `qa/maturity-scores.yaml` and explain the uncertainty in the final message.

1
.github/labeler.yml vendored
View File

@@ -118,7 +118,6 @@
- any-glob-to-any-file:
- "extensions/qa-lab/**"
- "qa/scenarios/**"
- "docs/maturity/**"
- "docs/concepts/qa-e2e-automation.md"
- "docs/concepts/personal-agent-benchmark-pack.md"
- "docs/channels/qa-channel.md"

View File

@@ -198,19 +198,10 @@ jobs:
"+refs/heads/main:refs/remotes/origin/main"
node_bin="$(dirname "$(node -p 'process.execPath')")"
link_node_tool() {
local tool="$1"
local source="$node_bin/$tool"
local target="/usr/local/bin/$tool"
if [ -e "$target" ] && [ "$(readlink -f "$source")" = "$(readlink -f "$target")" ]; then
return
fi
sudo ln -sf "$source" "$target"
}
link_node_tool node
link_node_tool npm
link_node_tool npx
link_node_tool corepack
sudo ln -sf "$node_bin/node" /usr/local/bin/node
sudo ln -sf "$node_bin/npm" /usr/local/bin/npm
sudo ln -sf "$node_bin/npx" /usr/local/bin/npx
sudo ln -sf "$node_bin/corepack" /usr/local/bin/corepack
sudo tee /usr/local/bin/pnpm >/dev/null <<'PNPM'
#!/usr/bin/env bash
exec /usr/local/bin/corepack pnpm "$@"

View File

@@ -116,19 +116,10 @@ jobs:
"+refs/heads/main:refs/remotes/origin/main"
node_bin="$(dirname "$(node -p 'process.execPath')")"
link_node_tool() {
local tool="$1"
local source="$node_bin/$tool"
local target="/usr/local/bin/$tool"
if [ -e "$target" ] && [ "$(readlink -f "$source")" = "$(readlink -f "$target")" ]; then
return
fi
sudo ln -sf "$source" "$target"
}
link_node_tool node
link_node_tool npm
link_node_tool npx
link_node_tool corepack
sudo ln -sf "$node_bin/node" /usr/local/bin/node
sudo ln -sf "$node_bin/npm" /usr/local/bin/npm
sudo ln -sf "$node_bin/npx" /usr/local/bin/npx
sudo ln -sf "$node_bin/corepack" /usr/local/bin/corepack
sudo tee /usr/local/bin/pnpm >/dev/null <<'PNPM'
#!/usr/bin/env bash
exec /usr/local/bin/corepack pnpm "$@"

View File

@@ -105,19 +105,10 @@ jobs:
"+refs/heads/main:refs/remotes/origin/main"
node_bin="$(dirname "$(node -p 'process.execPath')")"
link_node_tool() {
local tool="$1"
local source="$node_bin/$tool"
local target="/usr/local/bin/$tool"
if [ -e "$target" ] && [ "$(readlink -f "$source")" = "$(readlink -f "$target")" ]; then
return
fi
sudo ln -sf "$source" "$target"
}
link_node_tool node
link_node_tool npm
link_node_tool npx
link_node_tool corepack
sudo ln -sf "$node_bin/node" /usr/local/bin/node
sudo ln -sf "$node_bin/npm" /usr/local/bin/npm
sudo ln -sf "$node_bin/npx" /usr/local/bin/npx
sudo ln -sf "$node_bin/corepack" /usr/local/bin/corepack
sudo tee /usr/local/bin/pnpm >/dev/null <<'PNPM'
#!/usr/bin/env bash
exec /usr/local/bin/corepack pnpm "$@"

View File

@@ -100,7 +100,6 @@ jobs:
run_macos_node: ${{ steps.manifest.outputs.run_macos_node }}
macos_node_matrix: ${{ steps.manifest.outputs.macos_node_matrix }}
run_macos_swift: ${{ steps.manifest.outputs.run_macos_swift }}
run_ios_build: ${{ steps.manifest.outputs.run_ios_build }}
run_android_job: ${{ steps.manifest.outputs.run_android_job }}
android_matrix: ${{ steps.manifest.outputs.android_matrix }}
steps:
@@ -205,7 +204,6 @@ jobs:
OPENCLAW_CI_DOCS_CHANGED: ${{ github.event_name == 'workflow_dispatch' && 'true' || steps.docs_scope.outputs.docs_changed }}
OPENCLAW_CI_RUN_NODE: ${{ github.event_name == 'workflow_dispatch' && 'true' || steps.changed_scope.outputs.run_node || 'false' }}
OPENCLAW_CI_RUN_MACOS: ${{ github.event_name == 'workflow_dispatch' && 'true' || steps.changed_scope.outputs.run_macos || 'false' }}
OPENCLAW_CI_RUN_IOS_BUILD: ${{ github.event_name == 'workflow_dispatch' && 'true' || steps.changed_scope.outputs.run_ios_build || 'false' }}
OPENCLAW_CI_RUN_ANDROID: ${{ github.event_name == 'workflow_dispatch' && (inputs.release_gate || inputs.include_android) && 'true' || steps.changed_scope.outputs.run_android || 'false' }}
OPENCLAW_CI_RUN_WINDOWS: ${{ github.event_name == 'workflow_dispatch' && 'true' || steps.changed_scope.outputs.run_windows || 'false' }}
OPENCLAW_CI_RUN_NODE_FAST_ONLY: ${{ github.event_name == 'workflow_dispatch' && 'false' || steps.changed_scope.outputs.run_node_fast_only || 'false' }}
@@ -269,8 +267,6 @@ jobs:
const runPluginContractShards = runNodeFull || runNodeFastPluginContracts;
const runMacos =
parseBoolean(process.env.OPENCLAW_CI_RUN_MACOS) && !docsOnly && isCanonicalRepository;
const runIosBuild =
parseBoolean(process.env.OPENCLAW_CI_RUN_IOS_BUILD) && !docsOnly && isCanonicalRepository;
const runAndroid =
parseBoolean(process.env.OPENCLAW_CI_RUN_ANDROID) && !docsOnly && isCanonicalRepository;
const runWindows =
@@ -365,7 +361,6 @@ jobs:
runMacos ? [{ check_name: "macos-node", runtime: "node", task: "test" }] : [],
),
run_macos_swift: runMacos,
run_ios_build: runIosBuild,
run_android_job: runAndroid,
android_matrix: createMatrix(
runAndroid
@@ -1182,9 +1177,7 @@ jobs:
timeout-minutes: ${{ matrix.timeout_minutes || 60 }}
strategy:
fail-fast: false
# The canonical main path waits for the admission debounce above, so
# modestly widen this large matrix without recreating registration bursts.
max-parallel: 16
max-parallel: 12
matrix: ${{ fromJson(needs.preflight.outputs.checks_node_core_nondist_matrix) }}
steps:
- name: Checkout
@@ -2167,76 +2160,6 @@ jobs:
done
exit 1
ios-build:
permissions:
contents: read
name: "ios-build"
needs: [preflight]
if: needs.preflight.outputs.run_ios_build == 'true'
runs-on: ${{ github.event_name == 'workflow_dispatch' && 'macos-26' || (github.repository == 'openclaw/openclaw' && 'blacksmith-12vcpu-macos-26' || 'macos-26') }}
timeout-minutes: 45
steps:
- name: Checkout
env:
CHECKOUT_REPO: ${{ github.repository }}
CHECKOUT_SHA: ${{ needs.preflight.outputs.checkout_revision }}
run: |
set -euo pipefail
git init "$GITHUB_WORKSPACE"
git -C "$GITHUB_WORKSPACE" config gc.auto 0
git -C "$GITHUB_WORKSPACE" remote add origin "https://github.com/${CHECKOUT_REPO}.git"
fetch_timeout_seconds=90
fetch_checkout_ref() {
git -C "$GITHUB_WORKSPACE" \
-c protocol.version=2 \
fetch --no-tags --prune --no-recurse-submodules --depth=1 origin \
"+${CHECKOUT_SHA}:refs/remotes/origin/checkout" &
local fetch_pid="$!"
local elapsed=0
while kill -0 "$fetch_pid" 2>/dev/null; do
if [ "$elapsed" -ge "$fetch_timeout_seconds" ]; then
kill -TERM "$fetch_pid" 2>/dev/null || true
sleep 10
kill -KILL "$fetch_pid" 2>/dev/null || true
wait "$fetch_pid" || true
return 124
fi
sleep 1
elapsed=$((elapsed + 1))
done
wait "$fetch_pid"
}
fetch_checkout_ref
git -C "$GITHUB_WORKSPACE" checkout --detach refs/remotes/origin/checkout
- name: Select Xcode 26
run: |
set -euo pipefail
for xcode_app in /Applications/Xcode_26.5.app /Applications/Xcode-26.5.0.app; do
if [ -d "$xcode_app/Contents/Developer" ]; then
sudo xcode-select -s "$xcode_app/Contents/Developer"
break
fi
done
xcodebuild -version
xcode_version="$(xcodebuild -version | awk 'NR == 1 { print $2 }')"
if [[ "$xcode_version" != 26.* ]]; then
echo "error: expected Xcode 26.x, got $xcode_version" >&2
exit 1
fi
swift --version
- name: Setup Node environment
uses: ./.github/actions/setup-node-env
with:
install-bun: "false"
- name: Install iOS Swift tooling
run: brew install xcodegen swiftlint swiftformat
- name: Build iOS app
run: pnpm ios:build
android:
permissions:
contents: read
@@ -2388,7 +2311,6 @@ jobs:
- checks-windows
- macos-node
- macos-swift
- ios-build
- android
if: ${{ !cancelled() && always() && github.event_name != 'push' && (github.event_name != 'pull_request' || !github.event.pull_request.draft) }}
runs-on: ubuntu-24.04

View File

@@ -152,7 +152,7 @@ jobs:
quality-shards:
name: Select Critical Quality shards
if: ${{ github.event_name != 'pull_request' || !github.event.pull_request.draft }}
runs-on: ubuntu-24.04
runs-on: blacksmith-4vcpu-ubuntu-2404
timeout-minutes: 5
outputs:
agent: ${{ steps.detect.outputs.agent }}
@@ -333,7 +333,7 @@ jobs:
name: Critical Quality (core-auth-secrets)
needs: quality-shards
if: ${{ needs.quality-shards.outputs.core_auth_secrets == 'true' && (github.event_name != 'pull_request' || !github.event.pull_request.draft) && (github.event_name == 'pull_request' || github.event_name != 'workflow_dispatch' || inputs.profile == 'all' || inputs.profile == 'core-auth-secrets') }}
runs-on: ubuntu-24.04
runs-on: blacksmith-4vcpu-ubuntu-2404
timeout-minutes: 25
steps:
- name: Checkout
@@ -356,7 +356,7 @@ jobs:
name: Critical Quality (config-boundary)
needs: quality-shards
if: ${{ needs.quality-shards.outputs.config == 'true' && (github.event_name != 'pull_request' || !github.event.pull_request.draft) && (github.event_name == 'pull_request' || github.event_name != 'workflow_dispatch' || inputs.profile == 'all' || inputs.profile == 'config-boundary') }}
runs-on: ubuntu-24.04
runs-on: blacksmith-4vcpu-ubuntu-2404
timeout-minutes: 25
steps:
- name: Checkout
@@ -379,7 +379,7 @@ jobs:
name: Critical Quality (gateway-runtime-boundary)
needs: quality-shards
if: ${{ needs.quality-shards.outputs.gateway == 'true' && (github.event_name != 'pull_request' || !github.event.pull_request.draft) && (github.event_name == 'pull_request' || github.event_name != 'workflow_dispatch' || inputs.profile == 'all' || inputs.profile == 'gateway-runtime-boundary') }}
runs-on: ubuntu-24.04
runs-on: blacksmith-4vcpu-ubuntu-2404
timeout-minutes: 25
steps:
- name: Checkout
@@ -402,7 +402,7 @@ jobs:
name: Critical Quality (channel-runtime-boundary)
needs: quality-shards
if: ${{ needs.quality-shards.outputs.channel == 'true' && (github.event_name != 'pull_request' || !github.event.pull_request.draft) && (github.event_name == 'pull_request' || github.event_name != 'workflow_dispatch' || inputs.profile == 'all' || inputs.profile == 'channel-runtime-boundary') }}
runs-on: ubuntu-24.04
runs-on: blacksmith-4vcpu-ubuntu-2404
timeout-minutes: 25
steps:
- name: Checkout
@@ -425,7 +425,7 @@ jobs:
name: Critical Quality (network-runtime-boundary)
needs: quality-shards
if: ${{ needs.quality-shards.outputs.network_runtime == 'true' && (github.event_name != 'pull_request' || !github.event.pull_request.draft) && (github.event_name == 'pull_request' || github.event_name != 'workflow_dispatch' || inputs.profile == 'all' || inputs.profile == 'network-runtime-boundary') }}
runs-on: ubuntu-24.04
runs-on: blacksmith-4vcpu-ubuntu-2404
timeout-minutes: 25
steps:
- name: Checkout
@@ -509,7 +509,7 @@ jobs:
name: Critical Quality (agent-runtime-boundary)
needs: quality-shards
if: ${{ needs.quality-shards.outputs.agent == 'true' && (github.event_name != 'pull_request' || !github.event.pull_request.draft) && (github.event_name == 'pull_request' || github.event_name != 'workflow_dispatch' || inputs.profile == 'all' || inputs.profile == 'agent-runtime-boundary') }}
runs-on: ubuntu-24.04
runs-on: blacksmith-4vcpu-ubuntu-2404
timeout-minutes: 25
steps:
- name: Checkout
@@ -532,7 +532,7 @@ jobs:
name: Critical Quality (mcp-process-runtime-boundary)
needs: quality-shards
if: ${{ needs.quality-shards.outputs.mcp_process == 'true' && (github.event_name != 'pull_request' || !github.event.pull_request.draft) && (github.event_name == 'pull_request' || github.event_name != 'workflow_dispatch' || inputs.profile == 'all' || inputs.profile == 'mcp-process-runtime-boundary') }}
runs-on: ubuntu-24.04
runs-on: blacksmith-4vcpu-ubuntu-2404
timeout-minutes: 25
steps:
- name: Checkout
@@ -555,7 +555,7 @@ jobs:
name: Critical Quality (memory-runtime-boundary)
needs: quality-shards
if: ${{ needs.quality-shards.outputs.memory == 'true' && (github.event_name != 'pull_request' || !github.event.pull_request.draft) && (github.event_name == 'pull_request' || github.event_name != 'workflow_dispatch' || inputs.profile == 'all' || inputs.profile == 'memory-runtime-boundary') }}
runs-on: ubuntu-24.04
runs-on: blacksmith-4vcpu-ubuntu-2404
timeout-minutes: 25
steps:
- name: Checkout
@@ -578,7 +578,7 @@ jobs:
name: Critical Quality (session-diagnostics-boundary)
needs: quality-shards
if: ${{ needs.quality-shards.outputs.session_diagnostics == 'true' && (github.event_name != 'pull_request' || !github.event.pull_request.draft) && (github.event_name == 'pull_request' || github.event_name != 'workflow_dispatch' || inputs.profile == 'all' || inputs.profile == 'session-diagnostics-boundary') }}
runs-on: ubuntu-24.04
runs-on: blacksmith-4vcpu-ubuntu-2404
timeout-minutes: 25
steps:
- name: Checkout
@@ -601,7 +601,7 @@ jobs:
name: Critical Quality (plugin-sdk-reply-runtime)
needs: quality-shards
if: ${{ needs.quality-shards.outputs.plugin_sdk_reply == 'true' && (github.event_name != 'pull_request' || !github.event.pull_request.draft) && (github.event_name == 'pull_request' || github.event_name != 'workflow_dispatch' || inputs.profile == 'all' || inputs.profile == 'plugin-sdk-reply-runtime') }}
runs-on: ubuntu-24.04
runs-on: blacksmith-4vcpu-ubuntu-2404
timeout-minutes: 25
steps:
- name: Checkout
@@ -624,7 +624,7 @@ jobs:
name: Critical Quality (provider-runtime-boundary)
needs: quality-shards
if: ${{ needs.quality-shards.outputs.provider == 'true' && (github.event_name != 'pull_request' || !github.event.pull_request.draft) && (github.event_name == 'pull_request' || github.event_name != 'workflow_dispatch' || inputs.profile == 'all' || inputs.profile == 'provider-runtime-boundary') }}
runs-on: ubuntu-24.04
runs-on: blacksmith-4vcpu-ubuntu-2404
timeout-minutes: 25
steps:
- name: Checkout
@@ -646,7 +646,7 @@ jobs:
ui-control-plane:
name: Critical Quality (ui-control-plane)
if: ${{ github.event_name != 'pull_request' && (github.event_name != 'workflow_dispatch' || inputs.profile == 'all') }}
runs-on: ubuntu-24.04
runs-on: blacksmith-4vcpu-ubuntu-2404
timeout-minutes: 25
steps:
- name: Checkout
@@ -668,7 +668,7 @@ jobs:
web-media-runtime-boundary:
name: Critical Quality (web-media-runtime-boundary)
if: ${{ github.event_name != 'pull_request' && (github.event_name != 'workflow_dispatch' || inputs.profile == 'all') }}
runs-on: ubuntu-24.04
runs-on: blacksmith-4vcpu-ubuntu-2404
timeout-minutes: 25
steps:
- name: Checkout
@@ -691,7 +691,7 @@ jobs:
name: Critical Quality (plugin-boundary)
needs: quality-shards
if: ${{ needs.quality-shards.outputs.plugin == 'true' && (github.event_name != 'pull_request' || !github.event.pull_request.draft) && (github.event_name == 'pull_request' || github.event_name != 'workflow_dispatch' || inputs.profile == 'all' || inputs.profile == 'plugin-boundary') }}
runs-on: ubuntu-24.04
runs-on: blacksmith-4vcpu-ubuntu-2404
timeout-minutes: 25
steps:
- name: Checkout
@@ -714,7 +714,7 @@ jobs:
name: Critical Quality (plugin-sdk-package-contract)
needs: quality-shards
if: ${{ needs.quality-shards.outputs.plugin_sdk_package == 'true' && (github.event_name != 'pull_request' || !github.event.pull_request.draft) && (github.event_name == 'pull_request' || github.event_name != 'workflow_dispatch' || inputs.profile == 'all' || inputs.profile == 'plugin-sdk-package-contract') }}
runs-on: ubuntu-24.04
runs-on: blacksmith-4vcpu-ubuntu-2404
timeout-minutes: 25
steps:
- name: Checkout

View File

@@ -22,6 +22,12 @@ on:
push:
branches:
- main
paths:
- ".github/actions/**"
- ".github/codeql/**"
- ".github/workflows/**"
- "packages/**"
- "src/**"
schedule:
- cron: "0 6 * * *"
@@ -49,32 +55,32 @@ jobs:
include:
- language: javascript-typescript
category: core-auth-secrets
runs_on: ubuntu-24.04
runs_on: blacksmith-8vcpu-ubuntu-2404
timeout_minutes: 25
config_file: ./.github/codeql/codeql-core-auth-secrets-critical-security.yml
- language: javascript-typescript
category: channel-runtime-boundary
runs_on: ubuntu-24.04
runs_on: blacksmith-8vcpu-ubuntu-2404
timeout_minutes: 25
config_file: ./.github/codeql/codeql-channel-runtime-boundary-critical-security.yml
- language: javascript-typescript
category: network-ssrf-boundary
runs_on: ubuntu-24.04
runs_on: blacksmith-4vcpu-ubuntu-2404
timeout_minutes: 25
config_file: ./.github/codeql/codeql-network-ssrf-boundary-critical-security.yml
- language: javascript-typescript
category: mcp-process-tool-boundary
runs_on: ubuntu-24.04
runs_on: blacksmith-4vcpu-ubuntu-2404
timeout_minutes: 25
config_file: ./.github/codeql/codeql-mcp-process-tool-boundary-critical-security.yml
- language: javascript-typescript
category: plugin-trust-boundary
runs_on: ubuntu-24.04
runs_on: blacksmith-4vcpu-ubuntu-2404
timeout_minutes: 25
config_file: ./.github/codeql/codeql-plugin-trust-boundary-critical-security.yml
- language: actions
category: actions
runs_on: ubuntu-24.04
runs_on: blacksmith-8vcpu-ubuntu-2404
timeout_minutes: 10
config_file: ./.github/codeql/codeql-actions-critical-security.yml
steps:

View File

@@ -171,19 +171,10 @@ jobs:
set -euo pipefail
node_bin="$(dirname "$(node -p 'process.execPath')")"
link_node_tool() {
local tool="$1"
local source="$node_bin/$tool"
local target="/usr/local/bin/$tool"
if [ -e "$target" ] && [ "$(readlink -f "$source")" = "$(readlink -f "$target")" ]; then
return
fi
sudo ln -sf "$source" "$target"
}
link_node_tool node
link_node_tool npm
link_node_tool npx
link_node_tool corepack
sudo ln -sf "$node_bin/node" /usr/local/bin/node
sudo ln -sf "$node_bin/npm" /usr/local/bin/npm
sudo ln -sf "$node_bin/npx" /usr/local/bin/npx
sudo ln -sf "$node_bin/corepack" /usr/local/bin/corepack
sudo tee /usr/local/bin/pnpm >/dev/null <<'PNPM'
#!/usr/bin/env bash
exec /usr/local/bin/corepack pnpm "$@"
@@ -499,7 +490,7 @@ jobs:
if (-not $env:CRABBOX_ID -or $env:CRABBOX_ID -notmatch '^[A-Za-z0-9._-]+$') {
Write-Error "Invalid crabbox_id"
}
$actionsRoot = "C:\ProgramData\crabbox\actions"
$actionsRoot = Join-Path $HOME ".crabbox\actions"
New-Item -ItemType Directory -Force $actionsRoot | Out-Null
$state = Join-Path $actionsRoot "$env:CRABBOX_ID.env"
$envFile = Join-Path $actionsRoot "$env:CRABBOX_ID.env.ps1"
@@ -555,7 +546,7 @@ jobs:
if ($env:CRABBOX_KEEP_ALIVE_MINUTES -match '^[0-9]+$') {
$minutes = [int]$env:CRABBOX_KEEP_ALIVE_MINUTES
}
$stop = Join-Path "C:\ProgramData\crabbox\actions" "$env:CRABBOX_ID.stop"
$stop = Join-Path $HOME ".crabbox\actions\$env:CRABBOX_ID.stop"
$deadline = (Get-Date).AddMinutes($minutes)
while ((Get-Date) -lt $deadline) {
if (Test-Path $stop) {
@@ -593,19 +584,10 @@ jobs:
fi
node_bin="$(dirname "$(node -p 'process.execPath')")"
link_node_tool() {
local tool="$1"
local source="$node_bin/$tool"
local target="/usr/local/bin/$tool"
if [ -e "$target" ] && [ "$(readlink -f "$source")" = "$(readlink -f "$target")" ]; then
return
fi
sudo ln -sf "$source" "$target"
}
link_node_tool node
link_node_tool npm
link_node_tool npx
link_node_tool corepack
sudo ln -sf "$node_bin/node" /usr/local/bin/node
sudo ln -sf "$node_bin/npm" /usr/local/bin/npm
sudo ln -sf "$node_bin/npx" /usr/local/bin/npx
sudo ln -sf "$node_bin/corepack" /usr/local/bin/corepack
sudo tee /usr/local/bin/pnpm >/dev/null <<'PNPM'
#!/usr/bin/env bash
exec /usr/local/bin/corepack pnpm "$@"

View File

@@ -12,40 +12,6 @@ on:
required: true
default: main
type: string
expected_sha:
description: Optional full SHA that ref must resolve to
required: false
default: ""
type: string
workflow_call:
inputs:
qa_evidence_run_id:
description: Optional workflow run id containing qa-evidence.json
required: false
default: ""
type: string
ref:
description: OpenClaw branch, tag, or SHA containing the maturity score source
required: true
type: string
expected_sha:
description: Optional full SHA that ref must resolve to
required: false
default: ""
type: string
secrets:
OPENAI_API_KEY:
description: OpenAI API key used by live QA profile scenarios
required: true
OPENCLAW_MATURITY_SCORECARD_AGENT_OPENAI_API_KEY:
description: Optional OpenAI API key used by maturity scorecard agent steps
required: false
GH_APP_PRIVATE_KEY:
description: Optional GitHub App private key for generated docs PR creation
required: false
GH_APP_PRIVATE_KEY_FALLBACK:
description: Optional fallback GitHub App private key for generated docs PR creation
required: false
permissions:
actions: read
@@ -77,25 +43,14 @@ jobs:
- name: Validate selected ref
id: validate
env:
EXPECTED_SHA: ${{ inputs.expected_sha }}
INPUT_REF: ${{ inputs.ref }}
shell: bash
run: |
set -euo pipefail
selected_revision="$(git rev-parse HEAD)"
expected_sha="${EXPECTED_SHA,,}"
trusted_reason=""
if [[ -n "${expected_sha// }" && ! "$expected_sha" =~ ^[0-9a-f]{40}$ ]]; then
echo "expected_sha must be a full 40-character SHA; got: ${EXPECTED_SHA}" >&2
exit 1
fi
if [[ -n "${expected_sha// }" && "${selected_revision,,}" != "$expected_sha" ]]; then
echo "Ref '${INPUT_REF}' resolved to ${selected_revision}, expected ${EXPECTED_SHA}." >&2
exit 1
fi
git fetch --no-tags origin +refs/heads/main:refs/remotes/origin/main
if git merge-base --is-ancestor "$selected_revision" refs/remotes/origin/main; then
@@ -132,9 +87,8 @@ jobs:
if: ${{ inputs.qa_evidence_run_id == '' }}
uses: ./.github/workflows/qa-profile-evidence.yml
with:
ref: ${{ inputs.ref }}
expected_sha: ${{ needs.validate_selected_ref.outputs.selected_revision }}
qa_profile: release
ref: ${{ needs.validate_selected_ref.outputs.selected_revision }}
qa_profile: all
secrets:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
@@ -238,8 +192,8 @@ jobs:
}
const evidence = JSON.parse(fs.readFileSync(evidencePath, "utf8"));
if (evidence.profile !== "release") {
throw new Error(`qa-evidence.json profile must be release, got ${JSON.stringify(evidence.profile)}`);
if (evidence.profile !== "all") {
throw new Error(`qa-evidence.json profile must be all, got ${JSON.stringify(evidence.profile)}`);
}
const artifactDir = path.dirname(evidencePath);
@@ -256,75 +210,14 @@ jobs:
const manifestPath = path.join(artifactDir, manifestNames[0]);
const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
const manifestProfile = manifest.qaProfile ?? evidence.profile;
if (manifestProfile !== "release") {
throw new Error(`QA evidence manifest profile must be release, got ${JSON.stringify(manifestProfile)}`);
if (manifestProfile !== "all") {
throw new Error(`QA evidence manifest profile must be all, got ${JSON.stringify(manifestProfile)}`);
}
if (manifest.targetSha !== targetSha) {
throw new Error(`QA evidence manifest targetSha ${manifest.targetSha} does not match selected ref ${targetSha}`);
}
NODE
- name: Ensure maturity scorecard agent key exists
env:
OPENAI_API_KEY: ${{ secrets.OPENCLAW_MATURITY_SCORECARD_AGENT_OPENAI_API_KEY || secrets.OPENAI_API_KEY }}
run: |
set -euo pipefail
if [[ -z "${OPENAI_API_KEY:-}" ]]; then
echo "Missing OPENCLAW_MATURITY_SCORECARD_AGENT_OPENAI_API_KEY or OPENAI_API_KEY secret." >&2
exit 1
fi
- name: Run Codex maturity scorecard agent
uses: openai/codex-action@e0fdf01220eb9a88167c4898839d273e3f2609d1
env:
MATURITY_EVIDENCE_DIR: .artifacts/maturity-evidence
MATURITY_SCORES_PATH: qa/maturity-scores.yaml
MATURITY_TAXONOMY_PATH: taxonomy.yaml
with:
openai-api-key: ${{ secrets.OPENCLAW_MATURITY_SCORECARD_AGENT_OPENAI_API_KEY || secrets.OPENAI_API_KEY }}
prompt-file: .github/codex/prompts/maturity-scorecard-agent.md
model: ${{ vars.OPENCLAW_CI_OPENAI_MODEL_BARE }}
effort: high
sandbox: workspace-write
safety-strategy: drop-sudo
- name: Enforce focused maturity score patch
run: |
set -euo pipefail
git restore --staged :/
allowed='^qa/maturity-scores\.yaml$'
bad_tracked="$(
git diff --name-only HEAD -- | while IFS= read -r path; do
if [[ ! "$path" =~ $allowed ]]; then
printf '%s\n' "$path"
fi
done
)"
if [[ -n "$bad_tracked" ]]; then
echo "Maturity scorecard agent touched forbidden tracked paths:"
printf '%s\n' "$bad_tracked"
exit 1
fi
bad_untracked="$(
git ls-files --others --exclude-standard | while IFS= read -r path; do
if [[ "$path" != "qa/maturity-scores.yaml" ]]; then
printf '%s\n' "$path"
fi
done
)"
if [[ -n "$bad_untracked" ]]; then
echo "Maturity scorecard agent created forbidden untracked paths:"
printf '%s\n' "$bad_untracked"
exit 1
fi
if [[ ! -f qa/maturity-scores.yaml ]]; then
echo "Maturity scorecard agent must produce qa/maturity-scores.yaml." >&2
exit 1
fi
- name: Validate maturity score sources
run: |
node --import tsx --input-type=module <<'NODE'
@@ -367,7 +260,6 @@ jobs:
--strict-inputs
- name: Create generated docs PR app token
if: ${{ github.event_name == 'workflow_dispatch' }}
id: app-token
continue-on-error: true
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3
@@ -378,7 +270,7 @@ jobs:
permission-pull-requests: write
- name: Create generated docs PR fallback app token
if: ${{ github.event_name == 'workflow_dispatch' && steps.app-token.outcome == 'failure' }}
if: ${{ steps.app-token.outcome == 'failure' }}
id: app-token-fallback
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3
with:
@@ -388,7 +280,6 @@ jobs:
permission-pull-requests: write
- name: Open generated docs PR
if: ${{ github.event_name == 'workflow_dispatch' }}
env:
GH_TOKEN: ${{ steps.app-token.outputs.token || steps.app-token-fallback.outputs.token }}
QA_EVIDENCE_RUN_ID: ${{ inputs.qa_evidence_run_id }}
@@ -400,7 +291,7 @@ jobs:
exit 1
fi
if [[ -z "$(git status --porcelain -- qa/maturity-scores.yaml docs/maturity/scorecard.md docs/maturity/taxonomy.md)" ]]; then
if [[ -z "$(git status --porcelain -- qa/maturity-scores.yaml docs/maturity-scores.yaml docs/maturity/scorecard.md docs/maturity/taxonomy.md)" ]]; then
{
echo
echo "- Pull request: skipped; generated scorecard matches selected ref"
@@ -420,6 +311,9 @@ jobs:
git fetch --no-tags --depth=1 origin "refs/heads/${branch}:refs/remotes/origin/${branch}" || true
git switch -C "$branch"
git add qa/maturity-scores.yaml docs/maturity/scorecard.md docs/maturity/taxonomy.md
if git ls-files --error-unmatch docs/maturity-scores.yaml >/dev/null 2>&1 || [[ -e docs/maturity-scores.yaml ]]; then
git add docs/maturity-scores.yaml
fi
git commit -m "docs: update maturity scorecard"
git push --force-with-lease origin "$branch"

View File

@@ -609,6 +609,7 @@ jobs:
requires_repo_e2e: true
requires_live_suites: false
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
OPENCLAW_E2E_WORKERS: "1"
OPENCLAW_VITEST_MAX_WORKERS: "1"
steps:
@@ -642,74 +643,9 @@ jobs:
set -euo pipefail
case "${{ matrix.suite_id }}" in
openshell-e2e)
echo "OPENCLAW_E2E_OPENSHELL_CONFIG_HOME=$HOME/.config" >> "$GITHUB_ENV"
;;
esac
- name: Install OpenShell CLI
if: |
(inputs.live_suite_filter == '' || inputs.live_suite_filter == matrix.suite_id) &&
matrix.suite_id == 'openshell-e2e'
shell: bash
run: |
set -euo pipefail
export OPENSHELL_VERSION=v0.0.68
curl -LsSf https://raw.githubusercontent.com/NVIDIA/OpenShell/d64542f69d06694cbd203b64929d286dd0533bbb/install.sh | sh
openshell --version
- name: Bootstrap OpenShell gateway
if: |
(inputs.live_suite_filter == '' || inputs.live_suite_filter == matrix.suite_id) &&
matrix.suite_id == 'openshell-e2e'
shell: bash
run: |
set -euo pipefail
mtls_dir="$HOME/.config/openshell/gateways/openshell/mtls"
gateway_tls_dir="$RUNNER_TEMP/openshell-gateway-certs"
fallback_pid=""
if ! openshell --gateway openshell sandbox list >/dev/null 2>&1; then
rm -rf "$gateway_tls_dir"
openshell-gateway generate-certs \
--output-dir "$gateway_tls_dir" \
--server-san 127.0.0.1 \
--server-san localhost \
--server-san host.openshell.internal
rm -rf "$mtls_dir"
mkdir -p "$mtls_dir"
cp "$gateway_tls_dir/ca.crt" "$mtls_dir/ca.crt"
cp "$gateway_tls_dir/client/tls.crt" "$mtls_dir/tls.crt"
cp "$gateway_tls_dir/client/tls.key" "$mtls_dir/tls.key"
openshell gateway remove openshell >/dev/null 2>&1 || true
OPENSHELL_LOCAL_TLS_DIR="$gateway_tls_dir" nohup openshell-gateway \
--bind-address 0.0.0.0 \
--port 17670 \
--drivers docker \
--tls-cert "$gateway_tls_dir/server/tls.crt" \
--tls-key "$gateway_tls_dir/server/tls.key" \
--tls-client-ca "$mtls_dir/ca.crt" \
>"$RUNNER_TEMP/openshell-gateway.log" 2>&1 &
fallback_pid=$!
echo "OPENCLAW_OPENSHELL_FALLBACK_PID=$fallback_pid" >> "$GITHUB_ENV"
for _ in $(seq 1 30); do
if openshell gateway add --local --name openshell https://127.0.0.1:17670; then
break
fi
sleep 1
done
openshell gateway select openshell
for _ in $(seq 1 60); do
if openshell --gateway openshell sandbox list >/dev/null 2>&1; then
break
fi
sleep 1
done
fi
if [[ -z "$fallback_pid" ]]; then
echo "OPENCLAW_OPENSHELL_FALLBACK_PID=" >> "$GITHUB_ENV"
fi
openshell --gateway openshell sandbox list >/dev/null
openshell gateway list
- name: Validate suite credentials
if: inputs.live_suite_filter == '' || inputs.live_suite_filter == matrix.suite_id
shell: bash
@@ -729,15 +665,6 @@ jobs:
(inputs.live_suite_filter == '' || inputs.live_suite_filter == matrix.suite_id)
run: ${{ matrix.command }}
- name: Stop fallback OpenShell gateway
if: always() && matrix.suite_id == 'openshell-e2e'
shell: bash
run: |
set -euo pipefail
if [[ -n "${OPENCLAW_OPENSHELL_FALLBACK_PID:-}" ]]; then
kill "$OPENCLAW_OPENSHELL_FALLBACK_PID" 2>/dev/null || true
fi
validate_docker_e2e:
needs: [validate_selected_ref, prepare_docker_e2e_image, plan_release_workflow_matrices]
if: inputs.include_release_path_suites && inputs.docker_lanes == '' && needs.plan_release_workflow_matrices.outputs.docker_e2e_count != '0'

View File

@@ -151,39 +151,11 @@ jobs:
echo "present=false" >> "$GITHUB_OUTPUT"
fi
- name: Resolve OpenClaw target ref
id: target
if: steps.lane.outputs.run == 'true'
env:
GH_TOKEN: ${{ github.token }}
TARGET_REF_INPUT: ${{ inputs.target_ref }}
shell: bash
run: |
set -euo pipefail
requested="${TARGET_REF_INPUT:-}"
if [[ -z "$requested" ]]; then
echo "checkout_ref=${GITHUB_SHA}" >> "$GITHUB_OUTPUT"
echo "tested_ref=${GITHUB_REF_NAME}" >> "$GITHUB_OUTPUT"
exit 0
fi
encoded_ref="$(node -e 'process.stdout.write(encodeURIComponent(process.argv[1]))' "$requested")"
if ! resolved_sha="$(gh api "repos/${GITHUB_REPOSITORY}/commits/${encoded_ref}" --jq '.sha')"; then
echo "::error::Unable to resolve OpenClaw target_ref '${requested}'." >&2
exit 1
fi
if [[ ! "$resolved_sha" =~ ^[0-9a-f]{40}$ ]]; then
echo "::error::OpenClaw target_ref '${requested}' resolved to invalid SHA '${resolved_sha}'." >&2
exit 1
fi
echo "checkout_ref=${resolved_sha}" >> "$GITHUB_OUTPUT"
echo "tested_ref=${requested}" >> "$GITHUB_OUTPUT"
- name: Checkout OpenClaw
if: steps.lane.outputs.run == 'true'
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
ref: ${{ steps.target.outputs.checkout_ref }}
ref: ${{ inputs.target_ref || github.ref }}
fetch-depth: 1
persist-credentials: false

View File

@@ -44,11 +44,6 @@ on:
required: false
default: false
type: boolean
run_maturity_scorecard:
description: Render advisory maturity scorecard release docs; default release checks rely on dedicated package, QA, live, and E2E gates
required: false
default: false
type: boolean
rerun_group:
description: Release check group to run
required: false
@@ -111,7 +106,6 @@ jobs:
mode: ${{ steps.inputs.outputs.mode }}
release_profile: ${{ steps.inputs.outputs.release_profile }}
run_release_soak: ${{ steps.inputs.outputs.run_release_soak }}
run_maturity_scorecard: ${{ steps.inputs.outputs.run_maturity_scorecard }}
rerun_group: ${{ steps.inputs.outputs.rerun_group }}
live_suite_filter: ${{ steps.inputs.outputs.live_suite_filter }}
cross_os_suite_filter: ${{ steps.inputs.outputs.cross_os_suite_filter }}
@@ -285,7 +279,6 @@ jobs:
RELEASE_MODE_INPUT: ${{ inputs.mode }}
RELEASE_PROFILE_INPUT: ${{ inputs.release_profile }}
RELEASE_RUN_RELEASE_SOAK_INPUT: ${{ inputs.run_release_soak }}
RELEASE_RUN_MATURITY_SCORECARD_INPUT: ${{ inputs.run_maturity_scorecard }}
RELEASE_RERUN_GROUP_INPUT: ${{ inputs.rerun_group }}
RELEASE_LIVE_SUITE_FILTER_INPUT: ${{ inputs.live_suite_filter }}
RELEASE_CROSS_OS_SUITE_FILTER_INPUT: ${{ inputs.cross_os_suite_filter }}
@@ -326,12 +319,6 @@ jobs:
else
run_release_soak=true
fi
run_maturity_scorecard="$(printf '%s' "$RELEASE_RUN_MATURITY_SCORECARD_INPUT" | tr '[:upper:]' '[:lower:]')"
if [[ "$run_maturity_scorecard" != "true" && "$run_maturity_scorecard" != "1" && "$run_maturity_scorecard" != "yes" ]]; then
run_maturity_scorecard=false
else
run_maturity_scorecard=true
fi
release_profile="$RELEASE_PROFILE_INPUT"
if [[ "$release_profile" == "minimum" ]]; then
release_profile=beta
@@ -435,7 +422,6 @@ jobs:
printf 'mode=%s\n' "$RELEASE_MODE_INPUT"
printf 'release_profile=%s\n' "$release_profile"
printf 'run_release_soak=%s\n' "$run_release_soak"
printf 'run_maturity_scorecard=%s\n' "$run_maturity_scorecard"
printf 'rerun_group=%s\n' "$RELEASE_RERUN_GROUP_INPUT"
printf 'live_suite_filter=%s\n' "$RELEASE_LIVE_SUITE_FILTER_INPUT"
printf 'cross_os_suite_filter=%s\n' "$RELEASE_CROSS_OS_SUITE_FILTER_INPUT"
@@ -458,7 +444,6 @@ jobs:
RELEASE_MODE: ${{ inputs.mode }}
RELEASE_PROFILE: ${{ steps.inputs.outputs.release_profile }}
RUN_RELEASE_SOAK: ${{ steps.inputs.outputs.run_release_soak }}
RUN_MATURITY_SCORECARD: ${{ steps.inputs.outputs.run_maturity_scorecard }}
RELEASE_RERUN_GROUP: ${{ inputs.rerun_group }}
RELEASE_LIVE_SUITE_FILTER: ${{ inputs.live_suite_filter }}
RELEASE_CROSS_OS_SUITE_FILTER: ${{ inputs.cross_os_suite_filter }}
@@ -476,7 +461,6 @@ jobs:
echo "- Cross-OS mode: \`${RELEASE_MODE}\`"
echo "- Release profile: \`${RELEASE_PROFILE}\`"
echo "- Release soak lanes: \`${RUN_RELEASE_SOAK}\`"
echo "- Maturity scorecard docs: \`${RUN_MATURITY_SCORECARD}\`"
echo "- Rerun group: \`${RELEASE_RERUN_GROUP}\`"
if [[ -n "${RELEASE_LIVE_SUITE_FILTER// }" ]]; then
echo "- Live suite filter: \`${RELEASE_LIVE_SUITE_FILTER}\`"
@@ -783,20 +767,6 @@ jobs:
OPENCLAW_QA_CONVEX_SITE_URL: ${{ secrets.OPENCLAW_QA_CONVEX_SITE_URL }}
OPENCLAW_QA_CONVEX_SECRET_CI: ${{ secrets.OPENCLAW_QA_CONVEX_SECRET_CI }}
maturity_scorecard_release_checks:
name: Render maturity scorecard release docs
needs: [resolve_target]
if: contains(fromJSON('["all","qa"]'), needs.resolve_target.outputs.rerun_group) && needs.resolve_target.outputs.run_maturity_scorecard == 'true'
permissions:
actions: read
contents: read
uses: ./.github/workflows/maturity-scorecard.yml
with:
ref: ${{ needs.resolve_target.outputs.ref }}
expected_sha: ${{ needs.resolve_target.outputs.revision }}
secrets:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
qa_lab_parity_lane_release_checks:
name: Run QA Lab parity lane (${{ matrix.lane }})
needs: [resolve_target]
@@ -883,7 +853,7 @@ jobs:
name: release-qa-parity-${{ matrix.lane }}-${{ needs.resolve_target.outputs.revision }}
path: .artifacts/qa-e2e/
retention-days: 14
if-no-files-found: error
if-no-files-found: warn
- name: Record advisory status
if: always()
@@ -989,7 +959,7 @@ jobs:
name: release-qa-parity-${{ needs.resolve_target.outputs.revision }}
path: .artifacts/qa-e2e/
retention-days: 14
if-no-files-found: error
if-no-files-found: warn
- name: Record advisory status
if: always()
@@ -1161,7 +1131,7 @@ jobs:
name: release-qa-runtime-parity-${{ needs.resolve_target.outputs.revision }}
path: .artifacts/qa-e2e/
retention-days: 14
if-no-files-found: error
if-no-files-found: warn
- name: Record advisory status
if: always()
@@ -1271,13 +1241,13 @@ jobs:
--output .artifacts/qa-e2e/runtime-parity-standard-report/qa-runtime-tool-coverage-report.md
- name: Upload runtime tool coverage artifacts
if: ${{ always() && steps.verify_runtime_parity_status.outputs.ready == 'true' }}
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: release-qa-runtime-tool-coverage-${{ needs.resolve_target.outputs.revision }}
path: .artifacts/qa-e2e/runtime-parity-standard-report/
retention-days: 14
if-no-files-found: error
if-no-files-found: warn
qa_live_matrix_release_checks:
name: Run QA Lab live Matrix lane
@@ -1357,7 +1327,7 @@ jobs:
name: release-qa-live-matrix-${{ needs.resolve_target.outputs.revision }}
path: .artifacts/qa-e2e/
retention-days: 14
if-no-files-found: error
if-no-files-found: warn
- name: Record advisory status
if: always()
@@ -1497,7 +1467,7 @@ jobs:
name: release-qa-live-telegram-${{ needs.resolve_target.outputs.revision }}
path: .artifacts/qa-e2e/
retention-days: 14
if-no-files-found: error
if-no-files-found: warn
- name: Record advisory status
if: always()
@@ -1637,7 +1607,7 @@ jobs:
name: release-qa-live-discord-${{ needs.resolve_target.outputs.revision }}
path: .artifacts/qa-e2e/
retention-days: 14
if-no-files-found: error
if-no-files-found: warn
- name: Record advisory status
if: always()
@@ -1780,7 +1750,7 @@ jobs:
name: release-qa-live-whatsapp-${{ needs.resolve_target.outputs.revision }}
path: .artifacts/qa-e2e/
retention-days: 14
if-no-files-found: error
if-no-files-found: warn
- name: Record advisory status
if: always()
@@ -1920,7 +1890,7 @@ jobs:
name: release-qa-live-slack-${{ needs.resolve_target.outputs.revision }}
path: .artifacts/qa-e2e/
retention-days: 14
if-no-files-found: error
if-no-files-found: warn
- name: Record advisory status
if: always()
@@ -1976,7 +1946,6 @@ jobs:
- docker_e2e_release_checks
- package_acceptance_release_checks
- qa_lab_parity_lane_release_checks
- maturity_scorecard_release_checks
- qa_lab_parity_report_release_checks
- qa_lab_runtime_parity_release_checks
- runtime_tool_coverage_release_checks
@@ -2062,7 +2031,6 @@ jobs:
"docker_e2e_release_checks=${{ needs.docker_e2e_release_checks.result }}" \
"package_acceptance_release_checks=${{ needs.package_acceptance_release_checks.result }}" \
"qa_lab_parity_lane_release_checks=${{ needs.qa_lab_parity_lane_release_checks.result }}" \
"maturity_scorecard_release_checks=${{ needs.maturity_scorecard_release_checks.result }}" \
"qa_lab_parity_report_release_checks=${{ needs.qa_lab_parity_report_release_checks.result }}" \
"qa_lab_runtime_parity_release_checks=${{ needs.qa_lab_runtime_parity_release_checks.result }}" \
"runtime_tool_coverage_release_checks=${{ needs.runtime_tool_coverage_release_checks.result }}" \

View File

@@ -1466,9 +1466,9 @@ jobs:
fi
- name: Upload postpublish evidence
if: ${{ always() && inputs.publish_openclaw_npm }}
if: ${{ always() }}
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: openclaw-release-postpublish-evidence-${{ inputs.tag }}
path: ${{ runner.temp }}/openclaw-release-postpublish-evidence
if-no-files-found: error
if-no-files-found: ignore

View File

@@ -66,5 +66,5 @@ jobs:
with:
name: opengrep-full-sarif
path: .opengrep-out/precise.sarif
if-no-files-found: error
if-no-files-found: warn
retention-days: 30

View File

@@ -97,5 +97,5 @@ jobs:
with:
name: opengrep-pr-diff-sarif
path: .opengrep-out/precise.sarif
if-no-files-found: error
if-no-files-found: warn
retention-days: 30

View File

@@ -89,13 +89,6 @@ jobs:
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
with:
script: |
// Reusable workflow jobs inherit the caller event but run as
// github-actions[bot]; selected ref validation still gates secrets.
if (context.actor === "github-actions[bot]") {
core.info("Skipping manual actor permission check for a reusable workflow call.");
core.setOutput("authorized", "true");
return;
}
if (context.eventName !== "workflow_dispatch") {
core.info(`Skipping manual actor permission check for ${context.eventName}.`);
core.setOutput("authorized", "true");
@@ -250,9 +243,6 @@ jobs:
NODE_OPTIONS: --max-old-space-size=8192
run: node scripts/build-all.mjs qaRuntime
- name: Ensure Playwright Chromium
run: node scripts/ensure-playwright-chromium.mjs
- name: Run QA profile
id: run_profile
env:

View File

@@ -2,45 +2,6 @@
Docs: https://docs.openclaw.ai
## 2026.6.10
### Highlights
- **Automatic fast mode for talks:** OpenClaw can enable fast mode for short conversational turns, then return to normal mode for longer runs with bounded fallback and delivery behavior. (#85104) Thanks @alexph-dev and @vincentkoc.
- **More reliable model routing:** Zai model synthesis, GLM overload failover, and native reasoning-level selection now follow the active model catalog more consistently. (#94461, #93241, #94067, #94136) Thanks @Pandah97, @chrysb, @0xghost42, @zhengli0922, @openperf, @civiltox, and @BorClaw.
- **Safer session and channel state:** channel switches reset stale origin fields, and cron delivery awareness stays attached to the target session. (#95328, #93580) Thanks @ZengWen-DT, @jalehman, @gorkem2020, and @scotthuang.
- **Trusted policies survive hook composition:** composed hook registries keep the trusted tool policies required by approval-sensitive flows. (#94545) Thanks @jesse-merhi.
### Changes
- **Agent and channel runtime:** fast-mode state now survives retries, fallback transitions, progress events, and embedded/CLI/ACP normalization; session and channel routing retain the current target and delivery context. (#85104, #93580, #95328) Thanks @alexph-dev, @vincentkoc, @scotthuang, @ZengWen-DT, @jalehman, and @gorkem2020.
- **Provider behavior:** model catalogs now supply the correct Zai base URL, overload classification, and native reasoning controls for live-discovered models. (#94461, #93241, #94067, #94136) Thanks @Pandah97, @chrysb, @0xghost42, @zhengli0922, @openperf, @civiltox, and @BorClaw.
### Fixes
- **Fast-mode and policy correctness:** fallback cutoffs and reset notices are bounded, repeated progress events remain visible, Codex service-tier state is normalized, and trusted policies are not lost when hook registries are composed. (#85104, #94545) Thanks @alexph-dev, @vincentkoc, and @jesse-merhi.
- **Model and delivery edge cases:** Zai and GLM failover paths use the right runtime metadata, while stale channel-origin state no longer leaks across session changes. (#94461, #93241, #95328) Thanks @Pandah97, @chrysb, @0xghost42, @zhengli0922, @ZengWen-DT, @jalehman, and @gorkem2020.
- **Provider plugin onboarding:** setup refreshes provider plugin registry metadata after installing setup-selected provider plugins, so auth continuation uses the newly installed provider instead of stale registry state. (#95792) Thanks @snowzlmbot.
### Complete contribution record
This audited record covers the complete v2026.6.9..HEAD history: 12 merged PRs. The generation manifest also supplies direct commits as editorial input; the grouped notes above prioritize user impact.
#### Pull requests
- **PR #86627** Keep core doctor health in contribution order. Thanks @giodl73-repo.
- **PR #93580** fix: preserve cron delivery awareness for target sessions. Thanks @scotthuang and @jalehman.
- **PR #95030** refactor: add SDK transcript identity target API. Thanks @jalehman.
- **PR #94838** refactor(copilot): complete harness lifecycle parity. Thanks @vincentkoc.
- **PR #95328** fix(sessions): reset stale per-channel origin fields on channel switch. Related #95325. Thanks @ZengWen-DT and @jalehman and @gorkem2020.
- **PR #94461** fix(zai): fall back to manifest baseUrl for synthesized GLM-5 models. Related #94269. Thanks @Pandah97 and @chrysb.
- **PR #93241** fix(agents): classify Zhipu GLM overload as overloaded for failover. Related #93211. Thanks @0xghost42 and @zhengli0922.
- **PR #94067** fix(channels): resolve native /think menu levels via runtime catalog for live-discovered models. Related #93835. Thanks @openperf and @civiltox.
- **PR #94136** fix(zai): expose GLM-5.2 reasoning levels [AI-assisted]. Thanks @BorClaw.
- **PR #85104** feat: fast talks auto mode. Related #85087. Thanks @alexph-dev.
- **PR #94545** fix: keep trusted policies with hook registry. Thanks @jesse-merhi.
- **PR #95792** fix(onboard): refresh provider plugin registry after setup installs. Related #95765. Thanks @snowzlmbot.
## 2026.6.9
### Highlights
@@ -76,7 +37,6 @@ This audited record covers the complete v2026.6.8..HEAD history: 423 merged PRs.
#### Pull requests
- **PR #92154** fix(qqbot): gate private group commands and close strict command visibility gaps. Thanks @sliverp.
- **PR #90463** refactor: add session accessor seam with gateway consumer. Thanks @jalehman.
- **PR #88656** Drop reasoning-only length turns from replay. Thanks @abel-zer0.
- **PR #92856** feat(webui): add session workspace rail. Thanks @Solvely-Colin.

View File

@@ -2,5 +2,5 @@
# Source of truth: apps/android/version.json
# Generated by scripts/android-sync-versioning.ts.
OPENCLAW_ANDROID_VERSION_NAME=2026.6.10
OPENCLAW_ANDROID_VERSION_CODE=2026061001
OPENCLAW_ANDROID_VERSION_NAME=2026.6.9
OPENCLAW_ANDROID_VERSION_CODE=2026060901

View File

@@ -1,3 +0,0 @@
Adds settings detail panels, refreshes the Android overview controls, and routes exec approvals into the in-app inbox.
Improves chat acknowledgement handling, gateway pairing readiness, microphone foreground-service behavior, and release screenshot reliability.

View File

@@ -1,4 +1,4 @@
{
"version": "2026.6.10",
"versionCode": 2026061001
"version": "2026.6.9",
"versionCode": 2026060901
}

View File

@@ -1,185 +0,0 @@
# App Review Notes
Use these steps to exercise the live OpenClaw iOS App Review Gateway.
## Demo Account / Setup
Use the OpenClaw iOS app with the live review Gateway setup code included in
the `Notes` field of this App Review submission.
The setup code is a single generated code string. It already contains the public
Gateway host and setup credential.
## Setup Walkthrough
1. Open the OpenClaw app.
2. Tap `Continue`.
3. On `Connect Gateway`, tap `Set Up Manually`.
4. In the `Setup Code` section, tap the `Paste setup code` field.
5. Paste the setup code string from the App Review submission `Notes` field.
6. Tap `Apply Setup Code`.
7. If `Trust and connect` appears, tap `Trust and connect`.
8. Wait for the `Connected` screen.
9. On `Connected`, tap `Open OpenClaw`.
10. Confirm the `Control` screen shows `Gateway Online`.
11. Tap `Settings`.
12. Tap `Approvals`.
13. Tap `Open Notifications`.
14. Tap `Enable Notifications`.
15. On `Enable OpenClaw Hosted Push Relay?`, tap `Continue`.
16. If iOS asks whether OpenClaw may send notifications, tap `Allow`.
17. Confirm `Notifications` shows `Enabled`.
## Chat
1. Tap the `Chat` tab.
2. Tap the text field labeled `Message main...`.
3. Send this exact message:
```text
Start Apple review checklist.
```
Expected result: the assistant replies with the available App Review demos.
## Approval Demo
1. Tap the `Chat` tab.
2. Tap the text field labeled `Message main...`.
3. Send this exact message:
```text
Run the approval demo.
```
Expected result: the iPhone shows `Exec approval required` with the harmless
command `printf 'OpenClaw App Review approval demo complete\n'`. Tap
`Allow Once`. The chat then replies:
```text
The approval demo completed.
```
## Talk
1. Tap the `Talk` tab.
2. Tap `Start Talk`.
3. If iOS asks for microphone access, tap `Allow`.
4. If iOS asks for Speech Recognition access, tap `Allow`.
5. Confirm the screen changes to `Ready to talk` and shows `Stop Talk`.
6. Say:
```text
Summarize this review setup in one sentence.
```
Expected result: the assistant responds by voice. Tap `Stop Talk` when done.
## Talk + Background Audio
1. Tap the `Talk` tab.
2. Confirm `Speakerphone` is on.
3. Confirm `Background listening` is on.
4. Tap `Start Talk`.
5. If iOS asks for microphone access, tap `Allow`.
6. If iOS asks for Speech Recognition access, tap `Allow`.
7. Confirm `Stop Talk` is visible.
8. Say:
```text
Tell me when you can hear me.
```
9. While Talk is active, send OpenClaw to the background by returning to the
Home Screen or locking the iPhone. Do not force quit the app.
10. Continue speaking then wait for assistant audio reply.
Expected result: realtime Talk audio continues while OpenClaw is backgrounded.
Reopen OpenClaw, confirm Talk is still active, then tap `Stop Talk`.
## Gateway Status
1. Tap `Control`.
2. Tap `Instances`.
3. Confirm the screen shows `Gateway online`.
4. Confirm at least one `agent` row is connected.
5. Confirm the iPhone review device appears in the connected instances list.
## Push Notification
1. Tap the `Chat` tab.
2. Tap the text field labeled `Message main...`.
3. Send this exact message:
```text
Start push notification demo.
```
4. Immediately send OpenClaw to the background and lock the iPhone. Do not
force quit the app.
Expected result: the iPhone Lock Screen receives a visible `OpenClaw`
notification with this body:
```text
OpenClaw App Review push notification demo
```
Tap the notification and unlock the iPhone if prompted. If OpenClaw opens on
`Control`, tap `Chat`. Expected chat reply:
```text
The push notification demo completed.
```
## Push Wake / Status
1. Tap the `Chat` tab.
2. Send this exact message:
```text
Start push wake demo.
```
3. Immediately send OpenClaw to the background and lock the iPhone. Do not
force quit the app.
4. Wait for the `OpenClaw` notification on the Lock Screen. It normally appears
about 10 seconds after the message is sent.
5. Tap the notification and unlock the iPhone if prompted. If OpenClaw opens on
`Control`, tap `Chat`.
Expected result: the app reconnects to the live Gateway and Chat replies:
```text
The push wake and node status demo completed.
```
## Device Permissions
1. Tap `Settings`.
2. Tap `Permissions`.
3. Confirm these current app controls are available:
- `Camera`
- `Location` with `Off`, `While Using`, and `Always`
- `Keep Awake`
4. Expand `Privacy & Access`.
5. Confirm these request controls are available:
- `Contacts` / `Request Access`
- `Calendar (Add Events)` / `Request Access`
- `Calendar (View Events)` / `Request Full Access`
- `Reminders` / `Request Access`
## Share Sheet
1. Open Safari.
2. Navigate to `https://example.com`.
3. Tap the Safari toolbar `More` button.
4. Tap `Share`.
5. Tap `OpenClaw`.
6. Confirm the OpenClaw share extension appears and shows
`Edit text, then tap Send.` and `Send to OpenClaw`.
7. Tap `Send to OpenClaw`.
Expected result: the OpenClaw share extension sends the shared Safari page to
the live review Gateway and shows `Sent to OpenClaw.` Returning to OpenClaw
Chat shows the shared `Example Domain` page.

View File

@@ -1,11 +1,5 @@
# OpenClaw iOS Changelog
## 2026.6.10 - 2026-06-21
Maintenance update for the current OpenClaw beta release.
- Improved notification cleanup, Watch app compatibility, and native file input handling.
## 2026.6.9 - 2026-06-20
Maintenance update for the current OpenClaw release.

View File

@@ -2,8 +2,8 @@
// Source of truth: apps/ios/version.json
// Generated by scripts/ios-sync-versioning.ts.
OPENCLAW_IOS_VERSION = 2026.6.10
OPENCLAW_MARKETING_VERSION = 2026.6.10
OPENCLAW_IOS_VERSION = 2026.6.9
OPENCLAW_MARKETING_VERSION = 2026.6.9
OPENCLAW_BUILD_VERSION = 1
#include? "../build/Version.xcconfig"

View File

@@ -68,9 +68,9 @@ Release behavior:
- App Store release uses manual `Apple Distribution` signing with profile names pinned in `apps/ios/Config/AppStoreSigning.json`.
- Fastlane owns one-time Developer Portal setup, encrypted `match` signing sync to the repo/branch pinned in `apps/ios/Config/AppStoreSigning.json`, and release handling.
- App Store release also switches the app to `OpenClawPushMode=appStore`, which derives relay transport, official distribution, the canonical production relay, production APNs, production relay profile, `appleStrict` proof, and the App-Attest-capable entitlement file.
- `pnpm ios:release:upload` generates App Store screenshots, uploads release notes, and attaches `apps/ios/APP-REVIEW-NOTES.md` as a rendered PDF before archiving and uploading the IPA.
- `pnpm ios:release:upload` generates App Store screenshots and uploads release notes before archiving and uploading the IPA.
- The release archive is validated before upload by inspecting the exported IPA's signed entitlements, embedded App Store profile, and push mode. The upload fails if the IPA is not an App Store production relay build.
- App Review submission is manual in App Store Connect. The release lane uploads a build, public metadata, and the App Review PDF attachment, but it does not submit for review or upload the App Store Connect `Notes` field.
- App Review submission is manual in App Store Connect. The release lane uploads a build and metadata, but does not submit for review.
- The release flow does not modify `apps/ios/.local-signing.xcconfig` or `apps/ios/LocalSigning.xcconfig`.
- `apps/ios/version.json` is the pinned iOS release version source.
- `apps/ios/CHANGELOG.md` is the iOS-only changelog and release-note source.
@@ -178,7 +178,7 @@ pnpm ios:release:upload
- verifies synced iOS versioning artifacts
- resolves the next App Store Connect build number for that short version
- generates deterministic App Store screenshots
- uploads release notes, screenshots, and the App Review PDF attachment to the editable App Store version
- uploads release notes and screenshots to the editable App Store version
- generates `apps/ios/build/AppStoreRelease.xcconfig`
- archives `OpenClaw`
- validates the exported IPA's push mode, signed entitlements, and embedded App Store profile

View File

@@ -152,7 +152,6 @@ extension SettingsProTab {
}
let notificationSettings = await UNUserNotificationCenter.current().notificationSettings()
self.applyNotificationStatus(notificationSettings.authorizationStatus)
self.registerForRemoteNotificationsIfEnrollmentReady()
let issueCount = SettingsDiagnostics.issueCount(
gatewayConnected: self.gatewayDiagnosticConnected,
@@ -326,7 +325,6 @@ extension SettingsProTab {
self.setupStatusText = "Tailscale is off on this device. Turn it on, then try again."
return false
}
self.gatewayController.requestLocalNetworkAccess(reason: "settings_preflight")
self.setupStatusText = "Checking gateway reachability..."
let ok = await TCPProbe.probe(host: trimmed, port: port, timeoutSeconds: 3, queueLabel: "gateway.preflight")
if !ok {
@@ -419,7 +417,6 @@ extension SettingsProTab {
let status = settings.authorizationStatus
Task { @MainActor in
self.applyNotificationStatus(status)
self.registerForRemoteNotificationsIfEnrollmentReady()
}
}
}
@@ -440,7 +437,6 @@ extension SettingsProTab {
func requestNotificationAuthorizationFromSettings() {
guard !self.isRequestingNotificationAuthorization else { return }
PushEnrollmentConsent.markDisclosureAccepted()
self.isRequestingNotificationAuthorization = true
Task {
let granted = await (try? UNUserNotificationCenter.current().requestAuthorization(options: [
@@ -452,19 +448,12 @@ extension SettingsProTab {
await MainActor.run {
self.isRequestingNotificationAuthorization = false
self.notificationStatus = SettingsNotificationStatus(settings.authorizationStatus)
guard granted else { return }
self.registerForRemoteNotificationsIfEnrollmentReady()
guard granted, self.notificationStatus.allowsNotifications else { return }
UIApplication.shared.registerForRemoteNotifications()
}
}
}
@MainActor
func registerForRemoteNotificationsIfEnrollmentReady() {
guard PushEnrollmentConsent.disclosureAccepted else { return }
guard self.notificationStatus.allowsNotifications else { return }
UIApplication.shared.registerForRemoteNotifications()
}
@MainActor
func applyNotificationStatus(_ status: UNAuthorizationStatus) {
self.notificationStatus = SettingsNotificationStatus(status)

View File

@@ -127,8 +127,6 @@ final class GatewayConnectionController {
private let discovery = GatewayDiscoveryModel()
private let discoveryEnabled: Bool
private weak var appModel: NodeAppModel?
private var localNetworkAccessRequested: Bool
private var currentScenePhase: ScenePhase = .inactive
private var didAutoConnect = false
private var pendingServiceResolvers: [String: GatewayServiceResolver] = [:]
private var pendingTrustConnect: PendingTrustConnect?
@@ -139,14 +137,9 @@ final class GatewayConnectionController {
let useTLS: Bool
}
init(
appModel: NodeAppModel,
startDiscovery: Bool = true,
deferDiscoveryUntilLocalNetworkRequest: Bool = false)
{
init(appModel: NodeAppModel, startDiscovery: Bool = true) {
self.discoveryEnabled = startDiscovery
self.appModel = appModel
self.localNetworkAccessRequested = !deferDiscoveryUntilLocalNetworkRequest
GatewaySettingsStore.bootstrapPersistence()
let defaults = UserDefaults.standard
@@ -155,7 +148,7 @@ final class GatewayConnectionController {
self.updateFromDiscovery()
self.observeDiscovery()
if self.discoveryEnabled, self.localNetworkAccessRequested {
if self.discoveryEnabled {
self.discovery.start()
}
}
@@ -164,29 +157,11 @@ final class GatewayConnectionController {
self.discovery.setDebugLoggingEnabled(enabled)
}
func requestLocalNetworkAccess(reason: String) {
guard self.discoveryEnabled else {
self.discovery.stop()
self.updateFromDiscovery()
return
}
self.localNetworkAccessRequested = true
GatewayDiagnostics.log("local network access requested reason=\(reason)")
guard self.currentScenePhase != .background else { return }
self.discovery.start()
self.updateFromDiscovery()
self.attemptAutoReconnectIfNeeded()
}
func setScenePhase(_ phase: ScenePhase) {
self.currentScenePhase = phase
guard self.discoveryEnabled else {
self.discovery.stop()
return
}
guard self.localNetworkAccessRequested else { return }
switch phase {
case .background:
@@ -206,10 +181,6 @@ final class GatewayConnectionController {
self.updateFromDiscovery()
return
}
guard self.localNetworkAccessRequested else {
self.requestLocalNetworkAccess(reason: "restart_discovery")
return
}
self.discovery.stop()
self.didAutoConnect = false
@@ -226,7 +197,6 @@ final class GatewayConnectionController {
_ gateway: GatewayDiscoveryModel.DiscoveredGateway,
forceReconnect: Bool = false) async -> String?
{
self.requestLocalNetworkAccess(reason: "connect_discovered_gateway")
let instanceId = UserDefaults.standard.string(forKey: "node.instanceId")?
.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
if instanceId.isEmpty {
@@ -305,7 +275,6 @@ final class GatewayConnectionController {
authOverride: ManualAuthOverride? = nil,
forceReconnect: Bool = false) async
{
self.requestLocalNetworkAccess(reason: "connect_manual")
let instanceId = GatewaySettingsStore.currentInstanceID()
let token =
authOverride.map(\.token) ?? GatewaySettingsStore.loadGatewayToken(instanceId: instanceId)
@@ -371,7 +340,6 @@ final class GatewayConnectionController {
}
func connectLastKnown() async {
self.requestLocalNetworkAccess(reason: "connect_last_known")
guard let last = GatewaySettingsStore.loadLastGatewayConnection() else { return }
switch last {
case let .manual(host, port, useTLS, _):

View File

@@ -4103,9 +4103,6 @@ extension NodeAppModel {
private func registerAPNsTokenIfNeeded() async {
let usesRelayTransport = await self.pushRegistrationManager.usesRelayTransport
guard await self.canPublishAPNsRegistration(usesRelayTransport: usesRelayTransport) else {
return
}
guard self.gatewayConnected else {
if usesRelayTransport {
GatewayDiagnostics.pushRelay.skipped("gateway_offline")
@@ -4166,23 +4163,6 @@ extension NodeAppModel {
}
}
private func canPublishAPNsRegistration(usesRelayTransport: Bool) async -> Bool {
guard PushEnrollmentConsent.disclosureAccepted else {
if usesRelayTransport {
GatewayDiagnostics.pushRelay.skipped("enrollment_disclosure_not_accepted")
}
return false
}
let status = await self.notificationAuthorizationStatus()
guard Self.isNotificationAuthorizationAllowed(status) else {
if usesRelayTransport {
GatewayDiagnostics.pushRelay.skipped("notifications_not_authorized")
}
return false
}
return true
}
private func fetchPushRelayGatewayIdentity() async throws -> PushRelayGatewayIdentity {
let response = try await self.operatorGateway.request(
method: "gateway.identity.get",
@@ -5146,10 +5126,6 @@ extension NodeAppModel {
self.setOperatorConnected(connected)
}
func _test_canPublishAPNsRegistration(usesRelayTransport: Bool = true) async -> Bool {
await self.canPublishAPNsRegistration(usesRelayTransport: usesRelayTransport)
}
nonisolated static func _test_makeWatchChatItems(from raw: [OpenClawKit.AnyCodable]) -> [OpenClawWatchChatItem] {
self.makeWatchChatItems(from: raw)
}

View File

@@ -73,16 +73,10 @@ struct OnboardingWizardView: View {
private static let pairingAutoResumeTicker = Timer.publish(every: 2.0, on: .main, in: .common).autoconnect()
let allowSkip: Bool
let onRequestLocalNetworkAccess: (String) -> Void
let onClose: () -> Void
init(
allowSkip: Bool,
onRequestLocalNetworkAccess: @escaping (String) -> Void,
onClose: @escaping () -> Void)
{
init(allowSkip: Bool, onClose: @escaping () -> Void) {
self.allowSkip = allowSkip
self.onRequestLocalNetworkAccess = onRequestLocalNetworkAccess
self.onClose = onClose
_step = State(
initialValue: OnboardingStateStore.shouldPresentFirstRunIntro() ? .intro : .welcome)
@@ -237,7 +231,6 @@ struct OnboardingWizardView: View {
}
.onAppear {
self.initializeState()
self.requestLocalNetworkAccessIfPastIntro(reason: "onboarding_appear")
}
.onDisappear {
self.discoveryRestartTask?.cancel()
@@ -871,20 +864,10 @@ extension OnboardingWizardView {
private func advanceFromIntro() {
OnboardingStateStore.markFirstRunIntroSeen()
self.requestLocalNetworkAccess(reason: "onboarding_continue")
self.statusLine = "In your OpenClaw chat, run /pair qr, then scan the code here."
self.step = .welcome
}
private func requestLocalNetworkAccessIfPastIntro(reason: String) {
guard self.step != .intro else { return }
self.requestLocalNetworkAccess(reason: reason)
}
private func requestLocalNetworkAccess(reason: String) {
self.onRequestLocalNetworkAccess(reason)
}
private func navigateBack() {
guard let target = self.step.previous else { return }
self.connectingGatewayID = nil

View File

@@ -123,28 +123,8 @@ final class OpenClawAppDelegate: NSObject, UIApplicationDelegate, @preconcurrenc
let notificationCenter = UNUserNotificationCenter.current()
notificationCenter.delegate = self
ExecApprovalNotificationBridge.registerCategory(center: notificationCenter)
Task { @MainActor in
await self.registerForRemoteNotificationsIfEnrollmentReady(application)
}
return true
}
private func registerForRemoteNotificationsIfEnrollmentReady(_ application: UIApplication) async {
guard PushEnrollmentConsent.disclosureAccepted else { return }
guard await Self.isNotificationAuthorizationAllowed() else { return }
application.registerForRemoteNotifications()
}
private static func isNotificationAuthorizationAllowed() async -> Bool {
let settings = await UNUserNotificationCenter.current().notificationSettings()
switch settings.authorizationStatus {
case .authorized, .provisional, .ephemeral:
return true
case .denied, .notDetermined:
return false
@unknown default:
return false
}
return true
}
func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
@@ -646,8 +626,7 @@ struct OpenClawApp: App {
_gatewayController = State(
initialValue: GatewayConnectionController(
appModel: appModel,
startDiscovery: !Self.screenshotModeEnabled,
deferDiscoveryUntilLocalNetworkRequest: true))
startDiscovery: !Self.screenshotModeEnabled))
}
var body: some Scene {

View File

@@ -1,19 +0,0 @@
import Foundation
enum PushEnrollmentConsent {
static let disclosureAcceptedKey = "push.enrollment.disclosureAccepted"
static var disclosureAccepted: Bool {
UserDefaults.standard.bool(forKey: disclosureAcceptedKey)
}
static func markDisclosureAccepted() {
UserDefaults.standard.set(true, forKey: self.disclosureAcceptedKey)
}
#if DEBUG
static func reset() {
UserDefaults.standard.removeObject(forKey: self.disclosureAcceptedKey)
}
#endif
}

View File

@@ -683,7 +683,6 @@ struct RootTabs: View {
self.updateIdleTimer()
self.updateHomeCanvasState()
guard newValue == .active else { return }
self.maybeRequestLocalNetworkAccess(reason: "scene_active")
Task {
await self.appModel.refreshGatewayOverviewIfConnected()
await MainActor.run {
@@ -730,10 +729,6 @@ struct RootTabs: View {
.onChange(of: self.onboardingRequestID) { _, _ in
self.evaluateOnboardingPresentation(force: true)
}
.onChange(of: self.showOnboarding) { _, newValue in
guard !newValue else { return }
self.maybeRequestLocalNetworkAccess(reason: "onboarding_dismissed")
}
.onChange(of: self.appModel.openChatRequestID) { _, _ in
self.selectSidebarDestination(.chat)
}
@@ -772,9 +767,6 @@ struct RootTabs: View {
.fullScreenCover(isPresented: self.$showOnboarding) {
OnboardingWizardView(
allowSkip: self.onboardingAllowSkip,
onRequestLocalNetworkAccess: { reason in
self.requestLocalNetworkAccess(reason: reason)
},
onClose: {
self.showOnboarding = false
})
@@ -1053,14 +1045,13 @@ extension RootTabs {
shouldPresentOnLaunch: OnboardingStateStore.shouldPresentOnLaunch(appModel: self.appModel))
switch route {
case .none:
self.maybeRequestLocalNetworkAccess(reason: "root_appear")
break
case .onboarding:
self.onboardingAllowSkip = true
self.showOnboarding = true
case .settings:
self.didAutoOpenSettings = true
self.selectSidebarDestination(.gateway)
self.maybeRequestLocalNetworkAccess(reason: "root_appear")
}
}
@@ -1087,7 +1078,6 @@ extension RootTabs {
guard route == .settings else { return }
self.didAutoOpenSettings = true
self.selectSidebarDestination(.gateway)
self.maybeRequestLocalNetworkAccess(reason: "auto_open_settings")
}
private func maybeOpenSettingsForGatewaySetup() {
@@ -1098,19 +1088,6 @@ extension RootTabs {
self.presentedSheet = nil
self.didAutoOpenSettings = true
self.selectSidebarDestination(.gateway)
self.requestLocalNetworkAccess(reason: "gateway_setup_deeplink")
}
private func maybeRequestLocalNetworkAccess(reason: String) {
guard self.didEvaluateOnboarding else { return }
guard self.scenePhase == .active else { return }
guard !self.showOnboarding else { return }
self.requestLocalNetworkAccess(reason: reason)
}
private func requestLocalNetworkAccess(reason: String) {
guard !self.appModel.isAppleReviewDemoModeEnabled else { return }
self.gatewayController.requestLocalNetworkAccess(reason: reason)
}
private func applyInitialChatSessionIfNeeded() {

View File

@@ -76,7 +76,6 @@ Sources/Permissions/PermissionRequestBridge.swift
Sources/Push/ExecApprovalNotificationBridge.swift
Sources/Push/BackgroundAliveBeacon.swift
Sources/Push/PushBuildConfig.swift
Sources/Push/PushEnrollmentConsent.swift
Sources/Push/PushRegistrationManager.swift
Sources/Push/PushRelayClient.swift
Sources/Push/PushRelayKeychainStore.swift

View File

@@ -1377,24 +1377,6 @@ private final class MockBootstrapNotificationCenter: NotificationCentering, @unc
#expect(center.addCalls == 1)
}
@Test @MainActor func apnsRegistrationRequiresDisclosureAndNotificationAuthorization() async {
let center = MockBootstrapNotificationCenter()
center.status = .authorized
let appModel = NodeAppModel(notificationCenter: center)
PushEnrollmentConsent.reset()
defer { PushEnrollmentConsent.reset() }
#expect(await appModel._test_canPublishAPNsRegistration() == false)
#expect(await appModel._test_canPublishAPNsRegistration(usesRelayTransport: false) == false)
PushEnrollmentConsent.markDisclosureAccepted()
center.status = .notDetermined
#expect(await appModel._test_canPublishAPNsRegistration() == false)
center.status = .authorized
#expect(await appModel._test_canPublishAPNsRegistration())
}
@Test @MainActor func chatPushWithoutSpeechReturnsUnavailableWhenNotificationsOff() async throws {
let center = MockBootstrapNotificationCenter()
center.status = .notDetermined

View File

@@ -550,20 +550,6 @@ struct RootTabsSourceGuardTests {
#expect(docsSource.contains(".accessibilityHint(\"Opens Settings / Gateway\")"))
}
@Test func `push enrollment stays behind notification disclosure flow`() throws {
let appSource = try String(contentsOf: Self.openClawAppSourceURL(), encoding: .utf8)
let actionsSource = try String(contentsOf: Self.settingsProTabActionsSourceURL(), encoding: .utf8)
let modelSource = try String(contentsOf: Self.nodeAppModelSourceURL(), encoding: .utf8)
#expect(appSource.contains("PushEnrollmentConsent.disclosureAccepted"))
#expect(appSource.contains("await Self.isNotificationAuthorizationAllowed()"))
#expect(actionsSource.contains("PushEnrollmentConsent.markDisclosureAccepted()"))
#expect(actionsSource.contains("self.registerForRemoteNotificationsIfEnrollmentReady()"))
#expect(modelSource.contains("PushEnrollmentConsent.disclosureAccepted"))
#expect(modelSource.contains("notifications_not_authorized"))
#expect(modelSource.contains("enrollment_disclosure_not_accepted"))
}
@Test func `gateway settings keeps pairing trust diagnostics and tailscale actions`() throws {
let settingsSource = try String(contentsOf: Self.settingsProTabSourceURL(), encoding: .utf8)
let sectionsSource = try String(contentsOf: Self.settingsProTabSectionsSourceURL(), encoding: .utf8)
@@ -594,7 +580,6 @@ struct RootTabsSourceGuardTests {
#expect(actionsSource.contains("self.gatewayController.refreshActiveGatewayRegistrationFromSettings()"))
#expect(actionsSource.contains("self.gatewayController.restartDiscovery()"))
#expect(actionsSource.contains("await self.appModel.refreshGatewayOverviewIfConnected()"))
#expect(actionsSource.contains("self.gatewayController.requestLocalNetworkAccess(reason: \"settings_preflight\")"))
#expect(actionsSource.contains("await TCPProbe.probe(host: trimmed, port: port"))
#expect(actionsSource.contains("Check Tailscale or LAN."))
#expect(actionsSource.contains("Tailscale is off on this device. Turn it on, then try again."))
@@ -611,32 +596,6 @@ struct RootTabsSourceGuardTests {
#expect(controllerSource.contains("trustRotatedGatewayCertificate(from problem: GatewayConnectionProblem)"))
}
@Test func `local network access is requested from visible gateway flows`() throws {
let appSource = try String(contentsOf: Self.openClawAppSourceURL(), encoding: .utf8)
let rootSource = try String(contentsOf: Self.rootTabsSourceURL(), encoding: .utf8)
let onboardingSource = try String(contentsOf: Self.onboardingWizardSourceURL(), encoding: .utf8)
let actionsSource = try String(contentsOf: Self.settingsProTabActionsSourceURL(), encoding: .utf8)
let controllerSource = try String(contentsOf: Self.gatewayConnectionControllerSourceURL(), encoding: .utf8)
#expect(appSource.contains("deferDiscoveryUntilLocalNetworkRequest: true"))
#expect(controllerSource.contains("func requestLocalNetworkAccess(reason: String)"))
#expect(controllerSource.contains("guard self.localNetworkAccessRequested else"))
#expect(controllerSource.contains("self.requestLocalNetworkAccess(reason: \"connect_manual\")"))
#expect(controllerSource.contains("self.requestLocalNetworkAccess(reason: \"connect_discovered_gateway\")"))
#expect(controllerSource.contains("self.requestLocalNetworkAccess(reason: \"connect_last_known\")"))
#expect(rootSource.contains("self.maybeRequestLocalNetworkAccess(reason: \"root_appear\")"))
#expect(rootSource.contains("self.maybeRequestLocalNetworkAccess(reason: \"scene_active\")"))
#expect(rootSource.contains("self.maybeRequestLocalNetworkAccess(reason: \"onboarding_dismissed\")"))
#expect(rootSource.contains("self.requestLocalNetworkAccess(reason: \"gateway_setup_deeplink\")"))
#expect(rootSource.contains("guard self.didEvaluateOnboarding else { return }"))
#expect(rootSource.contains("onRequestLocalNetworkAccess: { reason in"))
#expect(onboardingSource.contains("self.requestLocalNetworkAccess(reason: \"onboarding_continue\")"))
#expect(onboardingSource.contains("self.requestLocalNetworkAccessIfPastIntro(reason: \"onboarding_appear\")"))
#expect(actionsSource.contains("self.gatewayController.requestLocalNetworkAccess(reason: \"settings_preflight\")"))
}
@Test func `gateway settings preview matrix covers primary states`() throws {
let supportSource = try String(contentsOf: Self.settingsProTabSupportSourceURL(), encoding: .utf8)
@@ -827,20 +786,6 @@ struct RootTabsSourceGuardTests {
.appendingPathComponent("Sources/Design/SettingsProTab.swift")
}
private static func onboardingWizardSourceURL() -> URL {
URL(fileURLWithPath: #filePath)
.deletingLastPathComponent()
.deletingLastPathComponent()
.appendingPathComponent("Sources/Onboarding/OnboardingWizardView.swift")
}
private static func openClawAppSourceURL() -> URL {
URL(fileURLWithPath: #filePath)
.deletingLastPathComponent()
.deletingLastPathComponent()
.appendingPathComponent("Sources/OpenClawApp.swift")
}
private static func notificationPermissionGuidanceDialogSourceURL() -> URL {
URL(fileURLWithPath: #filePath)
.deletingLastPathComponent()

View File

@@ -15,12 +15,13 @@
import Foundation
import XCTest
@MainActor
var deviceLanguage = ""
var locale = ""
func setupSnapshot(_ app: XCUIApplication, waitForAnimations: Bool = true) {
Snapshot.setupSnapshot(app, waitForAnimations: waitForAnimations)
}
@MainActor
func snapshot(_ name: String, waitForLoadingIndicator: Bool) {
if waitForLoadingIndicator {
Snapshot.snapshot(name)
@@ -32,7 +33,6 @@ func snapshot(_ name: String, waitForLoadingIndicator: Bool) {
/// - Parameters:
/// - name: The name of the snapshot
/// - timeout: Amount of seconds to wait until the network loading indicator disappears. Pass `0` if you don't want to wait.
@MainActor
func snapshot(_ name: String, timeWaitingForIdle timeout: TimeInterval = 20) {
Snapshot.snapshot(name, timeWaitingForIdle: timeout)
}
@@ -52,7 +52,6 @@ enum SnapshotError: Error, CustomDebugStringConvertible {
}
@objcMembers
@MainActor
open class Snapshot: NSObject {
static var app: XCUIApplication?
static var waitForAnimations = true
@@ -60,8 +59,6 @@ open class Snapshot: NSObject {
static var screenshotsDirectory: URL? {
return cacheDirectory?.appendingPathComponent("screenshots", isDirectory: true)
}
static var deviceLanguage = ""
static var currentLocale = ""
open class func setupSnapshot(_ app: XCUIApplication, waitForAnimations: Bool = true) {
@@ -106,17 +103,17 @@ open class Snapshot: NSObject {
do {
let trimCharacterSet = CharacterSet.whitespacesAndNewlines
currentLocale = try String(contentsOf: path, encoding: .utf8).trimmingCharacters(in: trimCharacterSet)
locale = try String(contentsOf: path, encoding: .utf8).trimmingCharacters(in: trimCharacterSet)
} catch {
NSLog("Couldn't detect/set locale...")
}
if currentLocale.isEmpty && !deviceLanguage.isEmpty {
currentLocale = Locale(identifier: deviceLanguage).identifier
if locale.isEmpty && !deviceLanguage.isEmpty {
locale = Locale(identifier: deviceLanguage).identifier
}
if !currentLocale.isEmpty {
app.launchArguments += ["-AppleLocale", "\"\(currentLocale)\""]
if !locale.isEmpty {
app.launchArguments += ["-AppleLocale", "\"\(locale)\""]
}
}
@@ -168,7 +165,7 @@ open class Snapshot: NSObject {
}
let screenshot = XCUIScreen.main.screenshot()
#if os(iOS) && !targetEnvironment(macCatalyst)
#if os(iOS)
let image = XCUIDevice.shared.orientation.isLandscape ? fixLandscapeOrientation(image: screenshot.image) : screenshot.image
#else
let image = screenshot.image
@@ -184,7 +181,7 @@ open class Snapshot: NSObject {
let path = screenshotsDir.appendingPathComponent("\(simulator)-\(name).png")
#if swift(<5.0)
try UIImagePNGRepresentation(image)?.write(to: path, options: .atomic)
UIImagePNGRepresentation(image)?.write(to: path, options: .atomic)
#else
try image.pngData()?.write(to: path, options: .atomic)
#endif
@@ -284,7 +281,6 @@ private extension XCUIElementQuery {
return self.containing(isNetworkLoadingIndicator)
}
@MainActor
var deviceStatusBars: XCUIElementQuery {
guard let app = Snapshot.app else {
fatalError("XCUIApplication is not set. Please call setupSnapshot(app) before snapshot().")
@@ -310,4 +306,4 @@ private extension CGFloat {
// Please don't remove the lines below
// They are used to detect outdated configuration files
// SnapshotHelperVersion [1.30]
// SnapshotHelperVersion [1.27]

View File

@@ -96,7 +96,7 @@ Pinned iOS version `2026.4.10` maps to:
- creates or verifies Developer Portal bundle IDs/services through Fastlane `produce`
- syncs encrypted App Store signing assets with Fastlane `match`
- increments App Store Connect build numbers for the pinned short version
- uploads screenshots, release notes, and the rendered App Review PDF attachment before archiving a release build
- uploads screenshots and release notes before archiving a release build
## Release-note resolution order
@@ -156,4 +156,4 @@ Fastlane and Xcode should consume only the pinned iOS version from `apps/ios/ver
Changing `package.json.version` alone must not change the iOS app version until a maintainer explicitly runs the pin step.
App Review submission must remain manual. Automation may create/update the editable App Store version, upload screenshots, upload release notes, upload the App Review PDF attachment, and upload builds, but it should not upload the App Store Connect `Notes` field or submit a build for review.
App Review submission must remain manual. Automation may create/update the editable App Store version, upload screenshots, upload release notes, and upload builds, but it should not submit a build for review.

View File

@@ -5,30 +5,12 @@ require "fileutils"
require "tmpdir"
require "tempfile"
require "cgi"
require "digest/md5"
default_platform(:ios)
APP_STORE_APP_IDENTIFIER = "ai.openclawfoundation.app"
DEFAULT_APP_STORE_CONNECT_KEYCHAIN_SERVICE = "openclaw-app-store-connect-key"
DEFAULT_SNAPSHOT_DEVICE_FAMILIES = [
{
label: "iPhone",
patterns: [
/\AiPhone .* Pro Max\z/,
/\AiPhone .* Plus\z/,
/\AiPhone .*\z/
]
},
{
label: "13-inch iPad",
patterns: [
/\AiPad Pro 13-inch/,
/\AiPad Air 13-inch/,
/\AiPad .*13-inch/
]
}
].freeze
DEFAULT_SNAPSHOT_DEVICES = ["iPhone 16 Pro Max", "iPad Pro 13-inch (M4)"].freeze
DEFAULT_WATCH_SNAPSHOT_DEVICE = "Apple Watch Ultra 3 (49mm)"
WATCH_SCREENSHOT_MODE_DEFAULTS_KEY = "openclaw.watch.screenshotMode"
WATCH_SNAPSHOT_STATUS_BAR_TIME = "09:41"
@@ -48,14 +30,6 @@ PUBLIC_METADATA_FILENAMES = [
"subtitle.txt",
"support_url.txt"
].freeze
APP_REVIEW_NOTES_METADATA_FILENAMES = [
"notes.txt",
"review_notes.txt"
].freeze
APP_STORE_SCREENSHOT_LIMIT_PER_SET = 10
APP_STORE_SCREENSHOT_SET_DELETE_TIMEOUT_SECONDS = 120
APP_STORE_SCREENSHOT_PROCESSING_TIMEOUT_SECONDS = 3600
APP_STORE_SCREENSHOT_PROCESSING_POLL_SECONDS = 5
def load_env_file(path)
return unless File.exist?(path)
@@ -88,6 +62,10 @@ def release_notes_upload_requested?
ENV["DELIVER_RELEASE_NOTES"] == "1"
end
def screenshot_paths
Dir[File.join(__dir__, "screenshots", "**", "*.png")]
end
def validate_required_screenshots!(paths)
missing_families = REQUIRED_SCREENSHOT_FAMILIES.filter_map do |name, pattern|
name unless paths.any? { |path| File.basename(path).match?(pattern) }
@@ -99,23 +77,11 @@ end
def snapshot_devices
raw = ENV["OPENCLAW_SNAPSHOT_DEVICES"].to_s.strip
return default_snapshot_devices if raw.empty?
return DEFAULT_SNAPSHOT_DEVICES if raw.empty?
raw.split(",").map(&:strip).reject(&:empty?)
end
def default_snapshot_devices
names = available_simulator_devices.map { |device| device["name"].to_s }.reject(&:empty?).uniq
DEFAULT_SNAPSHOT_DEVICE_FAMILIES.map do |family|
match = family.fetch(:patterns).filter_map do |pattern|
names.find { |name| name.match?(pattern) }
end.first
UI.user_error!("No available #{family.fetch(:label)} simulator found for App Store screenshots.") if match.nil?
match
end
end
def watch_snapshot_device
raw = ENV["OPENCLAW_WATCH_SNAPSHOT_DEVICE"].to_s.strip
raw.empty? ? DEFAULT_WATCH_SNAPSHOT_DEVICE : raw
@@ -147,51 +113,6 @@ def resolve_simulator_device(name)
fallback
end
def install_ready_for_review_edit_state_lookup!
require "spaceship"
app_class = Spaceship::ConnectAPI::App
app_class.class_eval do
unless method_defined?(:openclaw_get_edit_app_store_version_without_ready_for_review)
alias_method :openclaw_get_edit_app_store_version_without_ready_for_review, :get_edit_app_store_version
end
unless method_defined?(:openclaw_fetch_edit_app_info_without_ready_for_review)
alias_method :openclaw_fetch_edit_app_info_without_ready_for_review, :fetch_edit_app_info
end
def get_edit_app_store_version(client: nil, platform: nil, includes: Spaceship::ConnectAPI::AppStoreVersion::ESSENTIAL_INCLUDES)
version = openclaw_get_edit_app_store_version_without_ready_for_review(client: client, platform: platform, includes: includes)
return version if version
# First public releases can leave the only version in READY_FOR_REVIEW.
# Fastlane 2.236.1 excludes that state and then tries to create an illegal
# second version; use the existing review-ready version as the edit target.
client ||= Spaceship::ConnectAPI
platform ||= Spaceship::ConnectAPI::Platform::IOS
filter = {
appVersionState: Spaceship::ConnectAPI::AppStoreVersion::AppVersionState::READY_FOR_REVIEW,
platform: platform
}
get_app_store_versions(client: client, filter: filter, includes: includes)
.sort_by { |candidate| Gem::Version.new(candidate.version_string) }
.last
end
def fetch_edit_app_info(client: nil, includes: Spaceship::ConnectAPI::AppInfo::ESSENTIAL_INCLUDES)
app_info = openclaw_fetch_edit_app_info_without_ready_for_review(client: client, includes: includes)
return app_info if app_info
client ||= Spaceship::ConnectAPI
client
.get_app_infos(app_id: id, includes: includes)
.to_models
.find { |candidate| candidate.state == Spaceship::ConnectAPI::AppInfo::State::READY_FOR_REVIEW }
end
end
end
def bundle_identifier_for_product(product_path)
info_plist_path = File.join(product_path, "Info.plist")
UI.user_error!("Expected Info.plist at #{info_plist_path}.") unless File.exist?(info_plist_path)
@@ -289,7 +210,6 @@ def normalize_watch_screenshot_status_bar(path)
script = <<~SWIFT
import AppKit
import Foundation
import ImageIO
let path = CommandLine.arguments[1]
let timeText = CommandLine.arguments[2]
@@ -301,37 +221,36 @@ def normalize_watch_screenshot_status_bar(path)
exit(2)
}
let width = cgImage.width
let height = cgImage.height
let drawWidth = CGFloat(width)
let drawHeight = CGFloat(height)
let colorSpace = CGColorSpace(name: CGColorSpace.sRGB) ?? CGColorSpaceCreateDeviceRGB()
guard let bitmapContext = CGContext(
data: nil,
width: width,
height: height,
bitsPerComponent: 8,
bytesPerRow: width * 4,
space: colorSpace,
bitmapInfo: CGImageAlphaInfo.noneSkipLast.rawValue)
let width = CGFloat(cgImage.width)
let height = CGFloat(cgImage.height)
guard let bitmap = NSBitmapImageRep(
bitmapDataPlanes: nil,
pixelsWide: Int(width),
pixelsHigh: Int(height),
bitsPerSample: 8,
samplesPerPixel: 4,
hasAlpha: true,
isPlanar: false,
colorSpaceName: .deviceRGB,
bytesPerRow: 0,
bitsPerPixel: 0),
let graphicsContext = NSGraphicsContext(bitmapImageRep: bitmap)
else {
fputs("Failed to create normalized screenshot bitmap at \\(path)\\n", stderr)
exit(3)
}
let graphicsContext = NSGraphicsContext(cgContext: bitmapContext, flipped: false)
bitmap.size = NSSize(width: width, height: height)
NSGraphicsContext.saveGraphicsState()
NSGraphicsContext.current = graphicsContext
NSColor.black.setFill()
NSBezierPath(rect: NSRect(x: 0, y: 0, width: drawWidth, height: drawHeight)).fill()
source.draw(
in: NSRect(x: 0, y: 0, width: drawWidth, height: drawHeight),
from: NSRect(x: 0, y: 0, width: drawWidth, height: drawHeight),
operation: .sourceOver,
in: NSRect(x: 0, y: 0, width: width, height: height),
from: NSRect(x: 0, y: 0, width: width, height: height),
operation: .copy,
fraction: 1.0)
NSColor.black.setFill()
NSBezierPath(rect: NSRect(x: drawWidth - 146, y: drawHeight - 92, width: 124, height: 70)).fill()
NSBezierPath(rect: NSRect(x: width - 146, y: height - 92, width: 124, height: 70)).fill()
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.alignment = .right
@@ -341,26 +260,17 @@ def normalize_watch_screenshot_status_bar(path)
.paragraphStyle: paragraphStyle,
]
timeText.draw(
in: NSRect(x: drawWidth - 134, y: drawHeight - 82, width: 102, height: 44),
in: NSRect(x: width - 134, y: height - 82, width: 102, height: 44),
withAttributes: attributes)
NSGraphicsContext.restoreGraphicsState()
guard let output = bitmapContext.makeImage(),
let destination = CGImageDestinationCreateWithURL(
URL(fileURLWithPath: path) as CFURL,
"public.png" as CFString,
1,
nil)
guard let png = bitmap.representation(using: .png, properties: [:])
else {
fputs("Failed to encode normalized screenshot at \\(path)\\n", stderr)
exit(4)
}
CGImageDestinationAddImage(destination, output, nil)
guard CGImageDestinationFinalize(destination) else {
fputs("Failed to write normalized screenshot at \\(path)\\n", stderr)
exit(5)
}
try png.write(to: URL(fileURLWithPath: path))
SWIFT
Tempfile.create(["openclaw-watch-status-bar", ".swift"]) do |file|
@@ -737,37 +647,6 @@ def release_notes_metadata_path
temp_root
end
def app_review_notes_markdown_path
File.join(ios_root, "APP-REVIEW-NOTES.md")
end
def app_review_notes_pdf_path
File.join(ios_root, "build", "app-review", "APP-REVIEW-NOTES.pdf")
end
def generate_app_review_notes_pdf!
source = app_review_notes_markdown_path
UI.user_error!("Missing App Review notes at #{source}.") unless File.exist?(source)
output = app_review_notes_pdf_path
FileUtils.mkdir_p(File.dirname(output))
sh(shell_join(["xcrun", "swift", File.join(repo_root, "scripts", "ios-app-review-notes-pdf.swift"), source, output]))
output
end
def assert_no_app_review_notes_field_metadata!(metadata_path)
notes_dir = File.join(metadata_path, "review_information")
APP_REVIEW_NOTES_METADATA_FILENAMES.each do |filename|
path = File.join(notes_dir, filename)
next unless File.exist?(path)
UI.user_error!(
"Refusing to upload App Review Notes metadata from #{path}. " \
"Maintain the App Store Connect Notes field manually so the live setup code is not stored in this repo."
)
end
end
def public_metadata_path
source = File.join(__dir__, "metadata")
temp_root = Dir.mktmpdir("openclaw-app-store-metadata")
@@ -781,259 +660,6 @@ def public_metadata_path
temp_root
end
def app_store_screenshot_root
File.join(__dir__, "screenshots")
end
def app_store_screenshot_manifest
require "deliver/loader"
Deliver::Loader.load_app_screenshots(app_store_screenshot_root, false)
end
def resolve_app_store_connect_app(app_identifier:, app_id:)
require "spaceship"
app = if env_present?(app_id) && !env_present?(app_identifier)
Spaceship::ConnectAPI::App.get(app_id: app_id)
else
Spaceship::ConnectAPI::App.find(app_identifier || APP_STORE_APP_IDENTIFIER)
end
UI.user_error!("Could not find App Store Connect app #{app_identifier || app_id || APP_STORE_APP_IDENTIFIER}.") unless app
app
end
def resolve_app_store_connect_version(app:, short_version:)
version = app.get_edit_app_store_version(platform: Spaceship::ConnectAPI::Platform::IOS)
UI.user_error!("Could not find an editable App Store Connect version for #{app.name}.") unless version
if version.version_string != short_version
UI.user_error!(
"Editable App Store Connect version mismatch for #{app.name}: expected #{short_version}, got #{version.version_string}."
)
end
version
end
def app_store_screenshot_sets_for_display_type(localization:, display_type:)
localization
.get_app_screenshot_sets(includes: "appScreenshots")
.select { |set| set.screenshot_display_type == display_type }
end
def clear_app_store_screenshot_sets!(localization:)
existing_sets = localization.get_app_screenshot_sets(includes: "appScreenshots")
return if existing_sets.empty?
existing_sets.each do |set|
UI.message("Deleting existing #{localization.locale} #{set.screenshot_display_type} screenshot set #{set.id}.")
set.delete!
end
deadline = Time.now + APP_STORE_SCREENSHOT_SET_DELETE_TIMEOUT_SECONDS
loop do
sets = localization.get_app_screenshot_sets(includes: "appScreenshots")
return if sets.empty?
if Time.now >= deadline
UI.user_error!(
"Timed out waiting for App Store Connect to delete #{localization.locale} screenshot sets: #{sets.map(&:id).join(', ')}."
)
end
sleep(3)
end
end
def app_store_screenshot_expected_rows(screenshots)
screenshots.map do |screenshot|
{
checksum: Digest::MD5.file(screenshot.path).hexdigest,
file_name: File.basename(screenshot.path)
}
end
end
def app_store_screenshot_actual_rows(app_screenshot_set)
(app_screenshot_set.app_screenshots || []).map do |screenshot|
{
checksum: screenshot.source_file_checksum,
file_name: screenshot.file_name,
state: (screenshot.asset_delivery_state || {})["state"]
}
end
end
def format_app_store_screenshot_rows(rows)
rows.map do |row|
[row[:file_name], row[:checksum], row[:state]].compact.join(" ")
end.join(", ")
end
def app_store_screenshot_processing_timeout_seconds
raw = ENV["DELIVER_SCREENSHOT_PROCESSING_TIMEOUT"].to_s.strip
return APP_STORE_SCREENSHOT_PROCESSING_TIMEOUT_SECONDS if raw.empty?
unless raw.match?(/\A\d+\z/) && raw.to_i.positive?
UI.user_error!("Invalid DELIVER_SCREENSHOT_PROCESSING_TIMEOUT '#{raw}'. Expected a positive number of seconds.")
end
raw.to_i
end
def app_store_screenshot_state_counts(screenshots)
screenshots.each_with_object({}) do |screenshot, counts|
state = (screenshot.asset_delivery_state || {})["state"] || "UNKNOWN"
counts[state] ||= 0
counts[state] += 1
end
end
def wait_for_app_store_screenshots_processing!(screenshot_ids:, locale:, display_type:)
timeout_seconds = app_store_screenshot_processing_timeout_seconds
deadline = Time.now + timeout_seconds
loop do
screenshots = screenshot_ids.map do |screenshot_id|
Spaceship::ConnectAPI.get_app_screenshot(app_screenshot_id: screenshot_id).first
end
failed = screenshots.select(&:error?)
unless failed.empty?
details = failed.map { |screenshot| "#{screenshot.file_name}: #{screenshot.error_messages.join(', ')}" }
UI.user_error!("App Store Connect failed processing #{locale} #{display_type} screenshots: #{details.join('; ')}.")
end
return screenshots if screenshots.all?(&:complete?)
if Time.now >= deadline
states = app_store_screenshot_state_counts(screenshots)
UI.user_error!(
"Timed out after #{timeout_seconds}s waiting for App Store Connect to process #{locale} #{display_type} screenshots: #{states}."
)
end
UI.verbose("Waiting for #{locale} #{display_type} screenshots to finish processing: #{app_store_screenshot_state_counts(screenshots)}.")
sleep(APP_STORE_SCREENSHOT_PROCESSING_POLL_SECONDS)
end
end
def validate_app_store_screenshot_target_counts!(screenshots_by_target)
screenshots_by_target.each do |(locale, display_type), screenshots|
next if screenshots.length <= APP_STORE_SCREENSHOT_LIMIT_PER_SET
UI.user_error!(
"Found #{screenshots.length} screenshots for #{locale} #{display_type}; App Store Connect allows #{APP_STORE_SCREENSHOT_LIMIT_PER_SET}."
)
end
end
def verify_app_store_screenshot_set!(app_screenshot_set:, screenshots:, locale:, display_type:)
expected = app_store_screenshot_expected_rows(screenshots)
timeout_seconds = app_store_screenshot_processing_timeout_seconds
deadline = Time.now + timeout_seconds
actual = []
loop do
app_screenshot_set = Spaceship::ConnectAPI::AppScreenshotSet.get(app_screenshot_set_id: app_screenshot_set.id)
actual = app_store_screenshot_actual_rows(app_screenshot_set)
actual_identity = actual.map { |row| { checksum: row[:checksum], file_name: row[:file_name] } }
incomplete = actual.reject { |row| row[:state] == "COMPLETE" }
return if actual_identity == expected && incomplete.empty?
if actual.length > expected.length
UI.user_error!(
"App Store Connect screenshot verification failed for #{locale} #{display_type}. " \
"Expected: #{format_app_store_screenshot_rows(expected)}. " \
"Actual: #{format_app_store_screenshot_rows(actual)}."
)
end
if Time.now >= deadline
UI.user_error!(
"Timed out after #{timeout_seconds}s waiting for App Store Connect screenshot verification for #{locale} #{display_type}. " \
"Expected: #{format_app_store_screenshot_rows(expected)}. " \
"Actual: #{format_app_store_screenshot_rows(actual)}."
)
end
UI.verbose(
"Waiting for App Store Connect screenshot verification for #{locale} #{display_type}: " \
"#{format_app_store_screenshot_rows(actual)}."
)
sleep(APP_STORE_SCREENSHOT_PROCESSING_POLL_SECONDS)
end
end
def replace_app_store_screenshot_set!(localization:, display_type:, screenshots:)
existing_sets = app_store_screenshot_sets_for_display_type(localization: localization, display_type: display_type)
unless existing_sets.empty?
UI.user_error!(
"App Store Connect still has #{localization.locale} #{display_type} screenshot sets after reset: #{existing_sets.map(&:id).join(', ')}."
)
end
UI.message("Creating #{localization.locale} #{display_type} screenshot set.")
app_screenshot_set = localization.create_app_screenshot_set(attributes: { screenshotDisplayType: display_type })
uploaded_ids = screenshots.map.with_index do |screenshot, index|
started_at = Time.now
uploaded = app_screenshot_set.upload_screenshot(path: screenshot.path, wait_for_processing: false)
UI.message(
"Uploaded #{localization.locale} #{display_type} screenshot #{index + 1}/#{screenshots.length}: " \
"#{File.basename(screenshot.path)} (#{(Time.now - started_at).round(1)}s)."
)
uploaded.id
end
wait_for_app_store_screenshots_processing!(
screenshot_ids: uploaded_ids,
locale: localization.locale,
display_type: display_type
)
app_screenshot_set = Spaceship::ConnectAPI::AppScreenshotSet.get(app_screenshot_set_id: app_screenshot_set.id)
app_screenshot_set = app_screenshot_set.reorder_screenshots(app_screenshot_ids: uploaded_ids)
verify_app_store_screenshot_set!(
app_screenshot_set: app_screenshot_set,
screenshots: screenshots,
locale: localization.locale,
display_type: display_type
)
end
# Fastlane deliver can duplicate complete screenshots when its verification retry
# runs before App Store Connect consistently lists processed assets. Keep the
# screenshot write path serial and assert the remote set equals the local files.
def upload_app_store_screenshots_deterministically!(app_identifier:, app_id:, short_version:, screenshots:)
app = resolve_app_store_connect_app(app_identifier: app_identifier, app_id: app_id)
version = resolve_app_store_connect_version(app: app, short_version: short_version)
localizations_by_locale = version.get_app_store_version_localizations.each_with_object({}) do |localization, index|
index[localization.locale] = localization
end
screenshots_by_target = screenshots
.sort_by { |screenshot| [screenshot.language.to_s, screenshot.display_type.to_s, File.basename(screenshot.path)] }
.group_by { |screenshot| [screenshot.language, screenshot.display_type] }
validate_app_store_screenshot_target_counts!(screenshots_by_target)
missing_locales = screenshots_by_target.keys.map(&:first).uniq.reject { |locale| localizations_by_locale.key?(locale) }
unless missing_locales.empty?
UI.user_error!(
"App Store Connect localizations are missing for screenshot locales #{missing_locales.join(', ')}. " \
"Upload metadata for these locales before uploading screenshots."
)
end
screenshots_by_target.keys.map(&:first).uniq.each do |locale|
clear_app_store_screenshot_sets!(localization: localizations_by_locale.fetch(locale))
end
screenshots_by_target.each do |(locale, display_type), target_screenshots|
replace_app_store_screenshot_set!(
localization: localizations_by_locale.fetch(locale),
display_type: display_type,
screenshots: target_screenshots
)
end
UI.success("Uploaded and verified #{screenshots.length} App Store screenshots for #{short_version}.")
end
def read_ios_version_metadata
script_path = File.join(repo_root, "scripts", "ios-version.ts")
stdout, stderr, status = Open3.capture3(
@@ -1303,7 +929,7 @@ platform :ios do
ENV.delete("XCODE_XCCONFIG_FILE")
end
desc "Generate screenshots, update App Store metadata and review attachment, then upload an App Store build"
desc "Generate screenshots, update App Store version metadata, then upload an App Store build"
lane :release_upload do
unless ENV["OPENCLAW_IOS_RELEASE_WRAPPER"] == "1"
UI.user_error!("Use `pnpm ios:release:upload`; direct Fastlane TestFlight upload is disabled.")
@@ -1333,9 +959,8 @@ platform :ios do
ENV.delete("XCODE_XCCONFIG_FILE")
end
desc "Upload App Store metadata, App Review PDF attachment, and optionally screenshots"
desc "Upload App Store metadata (and optionally screenshots)"
lane :metadata do
install_ready_for_review_edit_state_lookup!
sync_ios_versioning!
version_metadata = read_ios_version_metadata
api_key = app_store_connect_api_key_config
@@ -1346,22 +971,19 @@ platform :ios do
app_id = nil unless env_present?(app_id)
if screenshot_upload_requested?
screenshots_to_upload = app_store_screenshot_manifest
if screenshots_to_upload.empty?
paths = screenshot_paths
if paths.empty?
UI.user_error!("DELIVER_SCREENSHOTS=1 but no PNG screenshots were found under apps/ios/fastlane/screenshots.")
end
validate_required_screenshots!(screenshots_to_upload.map(&:path))
validate_required_screenshots!(paths)
end
assert_no_app_review_notes_field_metadata!(File.join(__dir__, "metadata"))
metadata_path = public_metadata_path
skip_metadata = ENV["DELIVER_METADATA"] != "1"
if release_notes_upload_requested? && skip_metadata
metadata_path = release_notes_metadata_path
skip_metadata = false
end
assert_no_app_review_notes_field_metadata!(metadata_path) unless skip_metadata
app_review_attachment_file = skip_metadata ? nil : generate_app_review_notes_pdf!
deliver_options = {
api_key: api_key,
@@ -1371,11 +993,10 @@ platform :ios do
primary_category: "PRODUCTIVITY",
secondary_category: "UTILITIES",
metadata_path: metadata_path,
skip_screenshots: true,
skip_screenshots: !screenshot_upload_requested?,
skip_metadata: skip_metadata,
skip_binary_upload: true,
overwrite_screenshots: false,
app_review_attachment_file: app_review_attachment_file,
overwrite_screenshots: screenshot_upload_requested?,
skip_app_version_update: false,
submit_for_review: false,
run_precheck_before_submit: false
@@ -1388,14 +1009,6 @@ platform :ios do
end
deliver(**deliver_options)
if screenshot_upload_requested?
upload_app_store_screenshots_deterministically!(
app_identifier: app_identifier,
app_id: app_id,
short_version: version_metadata[:short_version],
screenshots: screenshots_to_upload
)
end
end
desc "Generate deterministic iOS screenshots for App Store metadata"

View File

@@ -104,7 +104,7 @@ Generate deterministic App Store screenshots:
pnpm ios:screenshots
```
The screenshot lane runs the app with `--openclaw-screenshot-mode`, which enters the built-in connected screenshot fixture instead of pairing with a live gateway. By default it chooses one available large iPhone simulator and one available 13-inch iPad simulator from the installed Xcode runtime; override devices with a comma-separated `OPENCLAW_SNAPSHOT_DEVICES` value when the requested simulators exist locally.
The screenshot lane runs the app with `--openclaw-screenshot-mode`, which enters the built-in connected screenshot fixture instead of pairing with a live gateway. By default it captures the tab set on `iPhone 16 Pro Max` and `iPad Pro 13-inch (M4)`; override devices with a comma-separated `OPENCLAW_SNAPSHOT_DEVICES` value when the requested simulators exist locally.
Upload to App Store Connect:
@@ -169,5 +169,5 @@ Versioning rules:
- Local App Store signing uses a temporary generated xcconfig with profile names from `apps/ios/Config/AppStoreSigning.json` and leaves local development signing overrides untouched
- App Store release uses `OpenClawPushMode=appStore`, which derives the canonical production hosted relay, production APNs, production relay profile, and `appleStrict` proof. The release lane rejects custom production relay URL overrides.
- The exported IPA is validated before upload by inspecting its push mode, signed entitlements, and embedded App Store profile.
- `pnpm ios:release:upload` generates and uploads screenshots, release notes, and the App Review PDF attachment before archiving, then uploads the IPA without submitting it for App Review or uploading the App Store Connect `Notes` field
- `pnpm ios:release:upload` generates and uploads screenshots and release notes before archiving, then uploads the IPA without submitting it for App Review
- See `apps/ios/VERSIONING.md` for the detailed workflow

View File

@@ -2,9 +2,10 @@ project("OpenClaw.xcodeproj")
scheme("OpenClawUITests")
configuration("Debug")
# The Fastfile screenshot lane resolves concrete device names from the installed
# Xcode simulators. Fastlane validates Snapfile devices before lane overrides, so
# this file intentionally does not hardcode simulator model names.
devices([
"iPhone 16 Pro Max",
"iPad Pro 13-inch (M4)",
])
languages([
"en-US",

View File

@@ -2,7 +2,7 @@
This directory is used by `fastlane deliver` for App Store Connect text metadata.
## Upload public metadata and App Review attachment
## Upload metadata only
```bash
cd apps/ios
@@ -10,9 +10,9 @@ APP_STORE_CONNECT_APP_ID=YOUR_APP_STORE_CONNECT_APP_ID \
DELIVER_METADATA=1 fastlane ios metadata
```
## Release notes and App Review attachment
## Release notes only
`pnpm ios:release:upload` uses this mode before archiving so the editable App Store version has current release notes and the App Review PDF attachment without rewriting all metadata:
`pnpm ios:release:upload` uses this mode before archiving so the editable App Store version has current release notes without rewriting all metadata:
```bash
cd apps/ios
@@ -46,12 +46,11 @@ Or set `APP_STORE_CONNECT_API_KEY_PATH`.
- Locale files live under `metadata/en-US/`.
- `release_notes.txt` is generated from `apps/ios/CHANGELOG.md`; after changelog updates, run `pnpm ios:version:sync`.
- `apps/ios/APP-REVIEW-NOTES.md` is rendered to `apps/ios/build/app-review/APP-REVIEW-NOTES.pdf` and uploaded as the App Review attachment when metadata is uploaded.
- Release notes resolve from `## <pinned iOS version>` first, then fall back to `## Unreleased` while a TestFlight train is still in progress.
- When starting a new production release train, pin the iOS version first with `pnpm ios:version:pin -- --from-gateway`.
- The release upload flow uploads release notes, screenshots, and the App Review PDF attachment before the IPA, and never submits for App Review.
- The release upload flow uploads release notes and screenshots before the IPA, and never submits for App Review.
- `privacy_url.txt` is set to `https://openclaw.ai/privacy`.
- If app lookup fails in `deliver`, set one of:
- `APP_STORE_CONNECT_APP_IDENTIFIER` (bundle ID)
- `APP_STORE_CONNECT_APP_ID` (numeric App Store Connect app ID, e.g. from `/apps/<id>/...` URL)
- App Review submission is manual. Keep review contact, demo account, and the App Store Connect `Notes` field outside this repo and enter them directly in App Store Connect when submitting for review. Do not add `metadata/review_information/notes.txt`; the lane refuses to upload that field.
- App Review submission is manual. Keep review contact, demo account, and reviewer notes outside this repo and enter them directly in App Store Connect when submitting for review.

View File

@@ -5,7 +5,7 @@ Pair this iOS app with your OpenClaw Gateway to use your iPhone as a secure node
What you can do:
- Pair with your private OpenClaw Gateway by QR code or setup code
- Chat with your assistant from iPhone
- Use realtime Talk mode and background Talk
- Use realtime Talk mode and push-to-talk
- Review Gateway action approvals from your iPhone
- Share text, links, and media directly from iOS into OpenClaw
- Enable device capabilities such as camera, screen, location, photos, contacts, calendar, and reminders when you choose

View File

@@ -1,3 +1,5 @@
Maintenance update for the current OpenClaw beta release.
Maintenance update for the current OpenClaw release.
- Improved notification cleanup, Watch app compatibility, and native file input handling.
- Added Apple Watch controls for common agent actions.
- Improved Gateway setup, notification settings, and share-extension identity handling.
- Updated the Watch app integration for current Xcode compatibility.

View File

@@ -1,3 +1,3 @@
{
"version": "2026.6.10"
"version": "2026.6.9"
}

View File

@@ -108,6 +108,24 @@
"revision" : "7c6ad0fc39d0763e0b699210e4124afd5041c5df",
"version" : "1.6.4"
}
},
{
"identity" : "swiftui-math",
"kind" : "remoteSourceControl",
"location" : "https://github.com/gonzalezreal/swiftui-math",
"state" : {
"revision" : "0b5c2cfaaec8d6193db206f675048eeb5ce95f71",
"version" : "0.1.0"
}
},
{
"identity" : "textual",
"kind" : "remoteSourceControl",
"location" : "https://github.com/gonzalezreal/textual",
"state" : {
"revision" : "5b06b811c0f5313b6b84bbef98c635a630638c38",
"version" : "0.3.1"
}
}
],
"version" : 3

View File

@@ -20,7 +20,6 @@ let package = Package(
.package(url: "https://github.com/apple/swift-log.git", from: "1.10.1"),
.package(url: "https://github.com/sparkle-project/Sparkle", from: "2.9.0"),
.package(url: "https://github.com/steipete/Peekaboo.git", exact: "3.5.2"),
.package(url: "https://github.com/pointfreeco/swift-concurrency-extras", from: "1.3.1"),
.package(path: "../shared/OpenClawKit"),
.package(path: "../swabble"),
],
@@ -55,7 +54,6 @@ let package = Package(
.product(name: "Sparkle", package: "Sparkle"),
.product(name: "PeekabooBridge", package: "Peekaboo"),
.product(name: "PeekabooAutomationKit", package: "Peekaboo"),
.product(name: "ConcurrencyExtras", package: "swift-concurrency-extras"),
],
exclude: [
"Resources/Info.plist",

View File

@@ -15,9 +15,9 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>2026.6.10</string>
<string>2026.6.9</string>
<key>CFBundleVersion</key>
<string>2026061000</string>
<string>2026060900</string>
<key>CFBundleIconFile</key>
<string>OpenClaw</string>
<key>CFBundleURLTypes</key>

View File

@@ -19,6 +19,7 @@ let package = Package(
],
dependencies: [
.package(url: "https://github.com/steipete/ElevenLabsKit", exact: "0.1.1"),
.package(url: "https://github.com/gonzalezreal/textual", exact: "0.3.1"),
],
targets: [
.target(
@@ -44,6 +45,10 @@ let package = Package(
name: "OpenClawChatUI",
dependencies: [
"OpenClawKit",
.product(
name: "Textual",
package: "textual",
condition: .when(platforms: [.macOS, .iOS])),
],
path: "Sources/OpenClawChatUI",
swiftSettings: [

View File

@@ -1,5 +1,5 @@
import Foundation
import SwiftUI
import Textual
public enum ChatMarkdownVariant: String, CaseIterable, Sendable {
case standard
@@ -22,28 +22,46 @@ struct ChatMarkdownRenderer: View {
var body: some View {
let processed = ChatMarkdownPreprocessor.preprocess(markdown: self.text)
VStack(alignment: .leading, spacing: 10) {
Text(self.markdownText(processed.cleaned))
.font(self.font)
.foregroundStyle(self.textColor)
.tint(self.linkColor)
.textSelection(.enabled)
.lineSpacing(self.variant == .compact ? 2 : 4)
StructuredText(markdown: processed.cleaned)
.modifier(ChatMarkdownStyle(
variant: self.variant,
context: self.context,
font: self.font,
textColor: self.textColor))
if !processed.images.isEmpty {
InlineImageList(images: processed.images)
}
}
}
}
private var linkColor: Color {
self.context == .user ? self.textColor : OpenClawChatTheme.accent
private struct ChatMarkdownStyle: ViewModifier {
let variant: ChatMarkdownVariant
let context: ChatMarkdownRenderer.Context
let font: Font
let textColor: Color
func body(content: Content) -> some View {
Group {
if self.variant == .compact {
content.textual.structuredTextStyle(.default)
} else {
content.textual.structuredTextStyle(.gitHub)
}
}
.font(self.font)
.foregroundStyle(self.textColor)
.textual.inlineStyle(self.inlineStyle)
.textual.textSelection(.enabled)
}
private func markdownText(_ markdown: String) -> AttributedString {
let options = AttributedString.MarkdownParsingOptions(
interpretedSyntax: .full,
failurePolicy: .returnPartiallyParsedIfPossible)
return (try? AttributedString(markdown: markdown, options: options)) ?? AttributedString(markdown)
private var inlineStyle: InlineStyle {
let linkColor: Color = self.context == .user ? self.textColor : OpenClawChatTheme.accent
let codeScale: CGFloat = self.variant == .compact ? 0.85 : 0.9
return InlineStyle()
.code(.monospaced, .fontScale(codeScale))
.link(.foregroundColor(linkColor))
}
}

View File

@@ -1,3 +1,4 @@
// Bundled A2UI runtime resource embedded by OpenClawKit.
var __defProp$1 = Object.defineProperty;
var __exportAll = (all, no_symbols) => {
let target = {};
@@ -11935,10 +11936,6 @@ var __runInitializers = function(thisArg, initializers, value) {
};
return _classThis;
})();
/**
* Canvas A2UI browser bootstrap that installs theme overrides and native bridge
* helpers.
*/
const modalStyles = i$10`
dialog {
position: fixed;

View File

@@ -1,4 +1,4 @@
1b953a19c347a27a0f9e856f23769b0c48d051354be4c88778c215231817fe8a config-baseline.json
f3fcfb358d8b8a1f0fa8676090339ff8df1b28ef6c7e80705a979a5c70e2a323 config-baseline.core.json
671979e86e4c4f59415d0a20879e838f9bbd883b3d29eeb02cb5131db8d187fe config-baseline.channel.json
94529978588d6e3776a86780b22cf9ff46a6f9957f2f178d3829403fad451ca7 config-baseline.plugin.json
ee542300a1f9d5c23e772d47f2acfcc92ee0a4da210974306790bf2220b80277 config-baseline.json
6349131baaa1828f2a071f42e4d7b17c8966c59b6588c8a4c1a32ea5ea4dcd5e config-baseline.core.json
de674ef01dad2828bb711a4648dc5a00f696f71c3c59004131d9475769bc1ff8 config-baseline.channel.json
ce2a731077f0f0135b7eaf01b00a60abfa0d2776aba4be237491d492af0c8a02 config-baseline.plugin.json

View File

@@ -1,2 +1,2 @@
0418a175983d6e17f535ebb49d07371ceed57c7002f8991113d548f02b1d17d1 plugin-sdk-api-baseline.json
319e947cff12d9c2c5781b6f97f9b6b1c4f8a251dc1e87703c534a37614325cf plugin-sdk-api-baseline.jsonl
da3373338b7f9c5f5639ad8233a32897d2346a0babe69a77386a7bff154cdcb1 plugin-sdk-api-baseline.json
17404d885e0d64ebc8e3c99443921058a8f1aebf76a5e612eb1f0cd7817d48f0 plugin-sdk-api-baseline.jsonl

View File

@@ -24,14 +24,6 @@ This directory owns docs authoring, Mintlify link rules, and docs i18n policy.
- `scripts/docs-sync-publish.mjs` excludes and prunes `docs/internal/**` from the public `openclaw/docs` publish repo if a page is force-added later.
- Internal docs may mention repo paths, private app names, 1Password item names, and runbooks, but never include secret values.
## Maturity Scorecard Editing
`taxonomy.yaml` and `qa/maturity-scores.yaml` are the source inputs; generated maturity docs under `docs/maturity/` are projections and should not be hand-edited for score, LTS, taxonomy, QA profile, or evidence tables.
`scripts/qa/render-maturity-docs.ts` owns generation; use `pnpm maturity:render` to refresh committed docs and `pnpm maturity:check` to verify them.
`.github/workflows/maturity-scorecard.yml` renders artifact previews and can open generated-doc PRs; `.github/workflows/openclaw-release-checks.yml` dispatches it for release QA.
Keep deterministic `qa-evidence.json.scorecard` data in GitHub Actions artifacts unless a maintainer explicitly asks for a sanitized committed projection.
Human overrides must change source state in a PR and explain the reason plus public or redacted evidence.
## Docs i18n
- Foreign-language docs are not maintained in this repo. The generated publish output lives in the separate `openclaw/docs` repo (often cloned locally as `../openclaw-docs`).

View File

@@ -155,7 +155,6 @@ Notes:
- `onchar` still responds to explicit @mentions.
- `channels.mattermost.requireMention` is honored for legacy configs but `chatmode` is preferred.
- After the bot sends a visible reply in a channel thread, later messages in that same thread are answered without a new @mention or `onchar` prefix, so multi-turn thread conversations keep flowing. Participation is remembered for 7 days of thread inactivity (refreshed on each reply) and persists across gateway restarts. Threads the bot has only observed are unaffected; start a new top-level message to require an explicit mention again.
## Threading and sessions

View File

@@ -151,7 +151,6 @@ to a group, then mention it or configure the group to run without a mention.
groups: {
"*": {
requireMention: true,
commandLevel: "all",
historyLimit: 50,
tools: { deny: ["exec", "read", "write"] },
},
@@ -159,7 +158,6 @@ to a group, then mention it or configure the group to run without a mention.
name: "Release room",
requireMention: false,
ignoreOtherMentions: true,
commandLevel: "safety",
historyLimit: 20,
prompt: "Keep replies short and operational.",
},
@@ -174,9 +172,6 @@ to a group, then mention it or configure the group to run without a mention.
settings include:
- `requireMention`: require an @mention before the bot replies. Default: `true`.
- `commandLevel`: control which built-in slash commands can run in groups.
Default: `all`, which preserves the pre-existing QQBot group behavior when the
setting is omitted.
- `ignoreOtherMentions`: drop messages that mention someone else but not the bot.
- `historyLimit`: keep recent non-mention group messages as context for the next mentioned turn. Set `0` to disable.
- `tools`: allow/deny tools for the whole group.
@@ -184,17 +179,6 @@ settings include:
- `name`: friendly label used in logs and group context.
- `prompt`: per-group behavior prompt appended to the agent context.
`commandLevel` accepts:
- `all`: keep recognized built-in commands available as before. Some commands may
stay hidden from menus, but authorized users can still run them in the group.
- `safety`: allow common collaboration commands such as `/help`, `/btw`, and
`/stop`; ask users to run sensitive commands such as `/config`, `/tools`, and
`/bash` in private chat.
- `strict`: only allow the group-session controls needed for strict group
operation. `/stop` still stays urgent so an authorized sender can interrupt an
active run.
Old QQBot `toolPolicy` entries are retired. Run `openclaw doctor --fix` to migrate them to `tools`.
Activation modes are `mention` and `always`. `requireMention: true` maps to

View File

@@ -42,7 +42,6 @@ or an explicit manual dispatch.
| `checks-windows` | Windows-specific process/path tests plus shared runtime import specifier regressions | Windows-relevant changes |
| `macos-node` | macOS TypeScript test lane using the shared built artifacts | macOS-relevant changes |
| `macos-swift` | Swift lint, build, and tests for the macOS app | macOS-relevant changes |
| `ios-build` | Xcode project generation plus the iOS app simulator build | iOS app, shared app kit, or Swabble changes |
| `android` | Android unit tests for both flavors plus one debug APK build | Android-relevant changes |
| `test-performance-agent` | Daily Codex slow-test optimization after trusted activity | Main CI success or manual dispatch |
| `openclaw-performance` | Daily/on-demand Kova runtime performance reports with mock-provider, deep-profile, and GPT 5.5 live lanes | Scheduled and manual dispatch |
@@ -53,7 +52,7 @@ or an explicit manual dispatch.
2. `preflight` decides which lanes exist at all. The `docs-scope` and `changed-scope` logic are steps inside this job, not standalone jobs.
3. `security-fast`, `check-*`, `check-additional-*`, `check-docs`, and `skills-python` fail quickly without waiting on the heavier artifact and platform matrix jobs.
4. `build-artifacts` overlaps with the fast Linux lanes so downstream consumers can start as soon as the shared build is ready.
5. Heavier platform and runtime lanes fan out after that: `checks-fast-core`, `checks-fast-contracts-plugins-*`, `checks-fast-contracts-channels-*`, `checks-node-core-*`, `checks-windows`, `macos-node`, `macos-swift`, `ios-build`, and `android`.
5. Heavier platform and runtime lanes fan out after that: `checks-fast-core`, `checks-fast-contracts-plugins-*`, `checks-fast-contracts-channels-*`, `checks-node-core-*`, `checks-windows`, `macos-node`, `macos-swift`, and `android`.
GitHub may mark superseded jobs as `cancelled` when a newer push lands on the same PR or `main` ref. Treat that as CI noise unless the newest run for the same ref is also failing. Matrix jobs use `fail-fast: false`, and `build-artifacts` reports embedded channel, core-support-boundary, and gateway-watch failures directly instead of queuing tiny verifier jobs. The automatic CI concurrency key is versioned (`CI-v7-*`) so a GitHub-side zombie in an old queue group cannot indefinitely block newer main runs. Manual full-suite runs use `CI-manual-v1-*` and do not cancel in-progress runs.
@@ -81,7 +80,7 @@ When the check fails, update the PR body instead of pushing another code commit.
Scope logic lives in `scripts/ci-changed-scope.mjs` and is covered by unit tests in `src/scripts/ci-changed-scope.test.ts`. Manual dispatch skips changed-scope detection and makes the preflight manifest act as if every scoped area changed.
- **CI workflow edits** validate the Node CI graph plus workflow linting, but do not force Windows, iOS, Android, or macOS native builds by themselves; those platform lanes stay scoped to platform source changes.
- **CI workflow edits** validate the Node CI graph plus workflow linting, but do not force Windows, Android, or macOS native builds by themselves; those platform lanes stay scoped to platform source changes.
- **Workflow Sanity** runs `actionlint`, `zizmor` over all workflow YAML files, the composite-action interpolation guard, and the conflict-marker guard. The PR-scoped `security-fast` job also runs `zizmor` over changed workflow files so workflow security findings fail early in the main CI graph.
- **Docs on `main` pushes** are checked by the standalone `Docs` workflow with the same ClawHub docs mirror used by CI, so mixed code+docs pushes do not also queue the CI `check-docs` shard. Pull requests and manual CI still run `check-docs` from CI when docs changed.
- **TUI PTY** runs in the `checks-node-core-runtime-tui-pty` Linux Node shard for TUI changes. The shard runs `test/vitest/vitest.tui-pty.config.ts` with `OPENCLAW_TUI_PTY_INCLUDE_LOCAL=1`, so it covers both the deterministic `TuiBackend` fixture lane and the slower `tui --local` smoke that mocks only the external model endpoint.
@@ -121,7 +120,7 @@ Treat GitHub titles, comments, bodies, review text, branch names, and commit mes
## Manual dispatches
Manual CI dispatches run the same job graph as normal CI but force every non-Android scoped lane on: Linux Node shards, bundled-plugin shards, plugin and channel contract shards, Node 22 compatibility, `check-*`, `check-additional-*`, built-artifact smoke checks, docs checks, Python skills, Windows, macOS, iOS build, and Control UI i18n. Standalone manual CI dispatches run Android only with `include_android=true`; the full release umbrella enables Android by passing `include_android=true`. Plugin prerelease static checks, the release-only `agentic-plugins` shard, the full extension batch sweep, and plugin prerelease Docker lanes are excluded from CI. The Docker prerelease suite runs only when `Full Release Validation` dispatches the separate `Plugin Prerelease` workflow with the release-validation gate enabled.
Manual CI dispatches run the same job graph as normal CI but force every non-Android scoped lane on: Linux Node shards, bundled-plugin shards, plugin and channel contract shards, Node 22 compatibility, `check-*`, `check-additional-*`, built-artifact smoke checks, docs checks, Python skills, Windows, macOS, and Control UI i18n. Standalone manual CI dispatches run Android only with `include_android=true`; the full release umbrella enables Android by passing `include_android=true`. Plugin prerelease static checks, the release-only `agentic-plugins` shard, the full extension batch sweep, and plugin prerelease Docker lanes are excluded from CI. The Docker prerelease suite runs only when `Full Release Validation` dispatches the separate `Plugin Prerelease` workflow with the release-validation gate enabled.
Manual runs use a unique concurrency group so a release-candidate full suite is not cancelled by another push or PR run on the same ref. The optional `target_ref` input lets a trusted caller run that graph against a branch, tag, or full commit SHA while using the workflow file from the selected dispatch ref.
@@ -133,30 +132,15 @@ gh workflow run full-release-validation.yml --ref main -f ref=<branch-or-sha>
## Runners
| Runner | Jobs |
| ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `ubuntu-24.04` | Manual CI dispatch and non-canonical repository fallbacks, CodeQL JavaScript/actions quality scans, workflow-sanity, labeler, auto-response, docs workflows outside CI, and install-smoke preflight so the Blacksmith matrix can queue earlier |
| `blacksmith-4vcpu-ubuntu-2404` | `preflight`, `security-fast`, lower-weight extension shards, `checks-fast-core`, plugin/channel contract shards, most bundled/lower-weight Linux Node shards, `check-guards`, `check-prod-types`, `check-test-types`, selected `check-additional-*` shards, and `check-dependencies` |
| `blacksmith-8vcpu-ubuntu-2404` | Retained heavy Linux Node suites, boundary/extension-heavy `check-additional-*` shards, and `android` |
| `blacksmith-16vcpu-ubuntu-2404` | `build-artifacts`, `check-lint` (CPU-sensitive enough that 8 vCPU cost more than they saved); install-smoke Docker builds (32-vCPU queue time cost more than it saved) |
| `blacksmith-8vcpu-windows-2025` | `checks-windows` |
| `blacksmith-6vcpu-macos-15` | `macos-node` on `openclaw/openclaw`; forks fall back to `macos-15` |
| `blacksmith-12vcpu-macos-26` | `macos-swift` and `ios-build` on `openclaw/openclaw`; forks fall back to `macos-26` |
## Runner registration budget
GitHub caps self-hosted runner registrations at 1,500 runners per 5 minutes per
repository, organization, or enterprise. The limit is shared by all Blacksmith
runner registrations in the `openclaw` organization, so adding another
Blacksmith installation does not add a new bucket.
Treat Blacksmith labels as the scarce resource for burst control. Jobs that
only route, notify, summarize, select shards, or run short CodeQL scans should
stay on GitHub-hosted runners unless they have measured Blacksmith-specific
needs. Any new Blacksmith matrix, larger `max-parallel`, or high-frequency
workflow must show its worst-case registration count and keep the org-level
target below 1,000 registrations per 5 minutes, leaving headroom for concurrent
repositories and retried jobs.
| Runner | Jobs |
| ------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `ubuntu-24.04` | Manual CI dispatch and non-canonical repository fallbacks, workflow-sanity, labeler, auto-response, docs workflows outside CI, and install-smoke preflight so the Blacksmith matrix can queue earlier |
| `blacksmith-4vcpu-ubuntu-2404` | `CodeQL Critical Quality`, `preflight`, `security-fast`, lower-weight extension shards, `checks-fast-core`, plugin/channel contract shards, most bundled/lower-weight Linux Node shards, `check-guards`, `check-prod-types`, `check-test-types`, selected `check-additional-*` shards, and `check-dependencies` |
| `blacksmith-8vcpu-ubuntu-2404` | Retained heavy Linux Node suites, boundary/extension-heavy `check-additional-*` shards, and `android` |
| `blacksmith-16vcpu-ubuntu-2404` | `build-artifacts`, `check-lint` (CPU-sensitive enough that 8 vCPU cost more than they saved); install-smoke Docker builds (32-vCPU queue time cost more than it saved) |
| `blacksmith-8vcpu-windows-2025` | `checks-windows` |
| `blacksmith-6vcpu-macos-15` | `macos-node` on `openclaw/openclaw`; forks fall back to `macos-15` |
| `blacksmith-12vcpu-macos-26` | `macos-swift` on `openclaw/openclaw`; forks fall back to `macos-26` |
Canonical-repo CI keeps Blacksmith as the default runner path for normal push and pull-request runs. `workflow_dispatch` and non-canonical repository runs use GitHub-hosted runners, but normal canonical runs do not currently probe Blacksmith queue health or automatically fall back to GitHub-hosted labels when Blacksmith is unavailable.
@@ -178,7 +162,6 @@ pnpm test:channels
pnpm test:contracts:channels
pnpm check:docs # docs format + lint + broken links
pnpm build # build dist when CI artifact/smoke checks matter
pnpm ios:build # generate and build the iOS app project
pnpm ci:timings # summarize the latest origin/main push CI run
pnpm ci:timings:recent # compare recent successful main CI runs
node scripts/ci-run-timings.mjs <run-id> # summarize wall time, queue time, and slowest jobs
@@ -215,7 +198,7 @@ Every lane uploads GitHub artifacts. When `CLAWGRIT_REPORTS_TOKEN` is configured
## Full Release Validation
`Full Release Validation` is the manual umbrella workflow for "run everything before release." It accepts a branch, tag, or full commit SHA, dispatches the manual `CI` workflow with that target, dispatches `Plugin Prerelease` for release-only plugin/package/static/Docker proof, and dispatches `OpenClaw Release Checks` for install smoke, package acceptance, cross-OS package checks, maturity scorecard rendering from QA profile evidence, QA Lab parity, Matrix, and Telegram lanes. Stable and full profiles always include exhaustive live/E2E and Docker release-path soak coverage; the beta profile can opt in with `run_release_soak=true`. The canonical package Telegram E2E runs inside Package Acceptance, so a full candidate does not start a duplicate live poller. After publishing, pass `release_package_spec` to reuse the shipped npm package across release checks, Package Acceptance, Docker, cross-OS, and Telegram without rebuilding. Use `npm_telegram_package_spec` only for a focused published-package Telegram rerun. The Codex plugin live package lane uses the same selected state by default: published `release_package_spec=openclaw@<tag>` derives `codex_plugin_spec=npm:@openclaw/codex@<tag>`, while SHA/artifact runs pack `extensions/codex` from the selected ref. Set `codex_plugin_spec` explicitly for custom plugin sources such as `npm:`, `npm-pack:`, or `git:` specs.
`Full Release Validation` is the manual umbrella workflow for "run everything before release." It accepts a branch, tag, or full commit SHA, dispatches the manual `CI` workflow with that target, dispatches `Plugin Prerelease` for release-only plugin/package/static/Docker proof, and dispatches `OpenClaw Release Checks` for install smoke, package acceptance, cross-OS package checks, QA Lab parity, Matrix, and Telegram lanes. Stable and full profiles always include exhaustive live/E2E and Docker release-path soak coverage; the beta profile can opt in with `run_release_soak=true`. The canonical package Telegram E2E runs inside Package Acceptance, so a full candidate does not start a duplicate live poller. After publishing, pass `release_package_spec` to reuse the shipped npm package across release checks, Package Acceptance, Docker, cross-OS, and Telegram without rebuilding. Use `npm_telegram_package_spec` only for a focused published-package Telegram rerun. The Codex plugin live package lane uses the same selected state by default: published `release_package_spec=openclaw@<tag>` derives `codex_plugin_spec=npm:@openclaw/codex@<tag>`, while SHA/artifact runs pack `extensions/codex` from the selected ref. Set `codex_plugin_spec` explicitly for custom plugin sources such as `npm:`, `npm-pack:`, or `git:` specs.
See [Full release validation](/reference/full-release-validation) for the
stage matrix, exact workflow job names, profile differences, artifacts, and
@@ -503,7 +486,7 @@ The pull request guard stays light: it only starts for changes under `.github/ac
### Critical Quality categories
`CodeQL Critical Quality` is the matching non-security shard. It runs only error-severity, non-security JavaScript/TypeScript quality queries over narrow high-value surfaces on GitHub-hosted Linux runners so quality scans do not spend Blacksmith runner-registration budget. Its pull request guard is intentionally smaller than the scheduled profile: non-draft PRs only run the matching `agent-runtime-boundary`, `config-boundary`, `core-auth-secrets`, `channel-runtime-boundary`, `gateway-runtime-boundary`, `memory-runtime-boundary`, `mcp-process-runtime-boundary`, `provider-runtime-boundary`, `session-diagnostics-boundary`, `plugin-boundary`, `plugin-sdk-package-contract`, and `plugin-sdk-reply-runtime` shards for agent command/model/tool execution and reply dispatch code, config schema/migration/IO code, auth/secrets/sandbox/security code, core channel and bundled channel plugin runtime, gateway protocol/server-method, memory runtime/SDK glue, MCP/process/outbound delivery, provider runtime/model catalog, session diagnostics/delivery queues, plugin loader, Plugin SDK/package-contract, or Plugin SDK reply runtime changes. CodeQL config and quality workflow changes run all twelve PR quality shards.
`CodeQL Critical Quality` is the matching non-security shard. It runs only error-severity, non-security JavaScript/TypeScript quality queries over narrow high-value surfaces on the smaller Blacksmith Linux runner. Its pull request guard is intentionally smaller than the scheduled profile: non-draft PRs only run the matching `agent-runtime-boundary`, `config-boundary`, `core-auth-secrets`, `channel-runtime-boundary`, `gateway-runtime-boundary`, `memory-runtime-boundary`, `mcp-process-runtime-boundary`, `provider-runtime-boundary`, `session-diagnostics-boundary`, `plugin-boundary`, `plugin-sdk-package-contract`, and `plugin-sdk-reply-runtime` shards for agent command/model/tool execution and reply dispatch code, config schema/migration/IO code, auth/secrets/sandbox/security code, core channel and bundled channel plugin runtime, gateway protocol/server-method, memory runtime/SDK glue, MCP/process/outbound delivery, provider runtime/model catalog, session diagnostics/delivery queues, plugin loader, Plugin SDK/package-contract, or Plugin SDK reply runtime changes. CodeQL config and quality workflow changes run all twelve PR quality shards.
Manual dispatch accepts:

View File

@@ -25,7 +25,7 @@ OpenClaw agent or Gateway.
openclaw skills search "calendar"
openclaw skills install @owner/<slug>
openclaw skills update @owner/<slug>
openclaw skills verify @owner/<slug>
openclaw skills verify <slug>
openclaw plugins search "calendar"
openclaw plugins install clawhub:<package>

View File

@@ -38,11 +38,11 @@ openclaw skills update @owner/<slug> --global
openclaw skills update --all
openclaw skills update --all --agent <id>
openclaw skills update --all --global
openclaw skills verify @owner/<slug>
openclaw skills verify @owner/<slug> --version <version>
openclaw skills verify @owner/<slug> --tag <tag>
openclaw skills verify @owner/<slug> --card
openclaw skills verify @owner/<slug> --global
openclaw skills verify <slug>
openclaw skills verify <slug> --version <version>
openclaw skills verify <slug> --tag <tag>
openclaw skills verify <slug> --card
openclaw skills verify <slug> --global
openclaw skills list
openclaw skills list --eligible
openclaw skills list --json
@@ -105,11 +105,8 @@ Notes:
target the shared managed skills directory instead of the workspace.
- `update --all` updates tracked ClawHub installs in the selected workspace, or
in the shared managed skills directory when combined with `--global`.
- `verify @owner/<slug>` prints ClawHub's `clawhub.skill.verify.v1` JSON
envelope by default. There is no `--json` flag because JSON is already the
default. Bare slugs remain accepted for compatibility when the skill is
already installed or unambiguous, but owner-qualified refs avoid publisher
ambiguity.
- `verify <slug>` prints ClawHub's `clawhub.skill.verify.v1` JSON envelope by
default. There is no `--json` flag because JSON is already the default.
- When ClawHub returns server-resolved source provenance, verify JSON also
includes a commit-pinned `openclaw.verifiedSourceUrl`. Unavailable or
self-declared source URLs stay only in the raw provenance envelope and are not

View File

@@ -22,7 +22,7 @@ openclaw gateway restart
## Usage
```bash
openclaw workboard list [--board <id>] [--status <status>] [--include-archived] [--json]
openclaw workboard list [--board <id>] [--status <status>] [--json]
openclaw workboard create <title...> [--notes <text>] [--status <status>] [--priority <priority>] [--agent <id>] [--board <id>] [--labels <items>] [--json]
openclaw workboard show <id> [--json]
openclaw workboard dispatch [--url <url>] [--token <token>] [--timeout <ms>] [--json]
@@ -50,16 +50,11 @@ Columns are id prefix, status, priority, board id, optional agent id, and title.
Flags:
| Flag | Purpose |
| -------------------- | --------------------------------------------- |
| `--board <id>` | Limit results to one board namespace |
| `--status <status>` | Limit results to one Workboard status |
| `--include-archived` | Include archived cards in compact text output |
| `--json` | Print the full card list as machine JSON |
Compact text output hides archived cards by default so the CLI matches the
`/workboard list` command. Pass `--include-archived` to show them. JSON output
keeps the full card list, including archived cards, for existing automation.
| Flag | Purpose |
| ------------------- | ---------------------------------------- |
| `--board <id>` | Limit results to one board namespace |
| `--status <status>` | Limit results to one Workboard status |
| `--json` | Print the full card list as machine JSON |
## `create`

View File

@@ -68,7 +68,7 @@ Slim evidence omits per-entry `execution` and sets `evidenceMode: "slim"`;
```bash
pnpm openclaw qa run \
--qa-profile smoke-ci \
--category channel-framework.conversation-routing-and-delivery \
--category agent-runtime-and-provider-execution.agent-turn-execution \
--provider-mode mock-openai \
--output-dir .artifacts/qa-e2e/smoke-ci-profile-dispatch
```
@@ -178,21 +178,10 @@ QA Lab, so package Docker release lanes do not run `qa` commands. Use
`pnpm qa:observability:smoke` from a built source checkout when changing
diagnostics instrumentation.
For a transport-real Matrix smoke lane that does not require model-provider
credentials, run the fast profile with the deterministic mock OpenAI provider:
For a transport-real Matrix smoke lane, run:
```bash
OPENCLAW_QA_MATRIX_NO_REPLY_WINDOW_MS=3000 \
pnpm openclaw qa matrix --provider-mode mock-openai --profile fast --fail-fast
```
For the live-frontier provider lane, supply OpenAI-compatible credentials
explicitly:
```bash
OPENCLAW_LIVE_OPENAI_KEY="${OPENAI_API_KEY}" \
OPENCLAW_QA_MATRIX_NO_REPLY_WINDOW_MS=3000 \
pnpm openclaw qa matrix --provider-mode live-frontier --profile fast --fail-fast
pnpm openclaw qa matrix --profile fast --fail-fast
```
The full CLI reference, profile/scenario catalog, env vars, and artifact layout for this lane live in [Matrix QA](/concepts/qa-matrix). At a glance: it provisions a disposable Tuwunel homeserver in Docker, registers temporary driver/SUT/observer users, runs the real Matrix plugin inside a child QA gateway scoped to that transport (no `qa-channel`), then writes a Markdown report, JSON summary, observed-events artifact, and combined output log under `.artifacts/qa-e2e/matrix-<timestamp>/`.
@@ -212,10 +201,9 @@ environment. That viewer profile is only for visual capture; the pass/fail
decision still comes from the Discord REST oracle.
CI uses the same command surface in `.github/workflows/qa-live-transports-convex.yml`.
Scheduled and default manual runs execute the fast Matrix profile with
QA-provided live-frontier credentials, `--fast`, and
`OPENCLAW_QA_MATRIX_NO_REPLY_WINDOW_MS=3000`. Manual `matrix_profile=all` fans
out into the five profile shards.
Scheduled and default manual runs execute the fast Matrix profile with live
frontier credentials, `--fast`, and `OPENCLAW_QA_MATRIX_NO_REPLY_WINDOW_MS=3000`.
Manual `matrix_profile=all` fans out into the five profile shards.
For transport-real Telegram, Discord, Slack, and WhatsApp smoke lanes:
@@ -978,7 +966,6 @@ output and whose artifact paths are resolved relative to that producer
`qa run --qa-profile`, the same `qa-evidence.json` also includes the profile
scorecard summary for the selected taxonomy categories.
Treat it as a discovery aid, not a gate replacement; the selected scenario still needs the right provider mode, live transport, Multipass, Testbox, or release lane for the behavior under test.
For scorecard context, see [Maturity scorecard](/maturity/scorecard).
For character and style checks, run the same scenario across multiple live model
refs and write a judged Markdown report:
@@ -1036,7 +1023,6 @@ When no `--judge-model` is passed, the judges default to
## Related docs
- [Matrix QA](/concepts/qa-matrix)
- [Maturity scorecard](/maturity/scorecard)
- [Personal agent benchmark pack](/concepts/personal-agent-benchmark-pack)
- [QA Channel](/channels/qa-channel)
- [Testing](/help/testing)

View File

@@ -30,68 +30,6 @@ title: "Usage tracking"
- CLI: `openclaw channels list` prints the same usage snapshot alongside provider config (use `--no-usage` to skip).
- macOS menu bar: "Usage" section under Context (only if available).
## Default usage footer mode
`/usage off|tokens|full` sets the footer for a session and is remembered for that
session. `messages.responseUsage` seeds that mode for sessions that have not
chosen one, so the footer can be on by default without typing `/usage` each time.
Set one mode for every channel, or a per-channel map with a `default` fallback:
```jsonc
{
"messages": {
"responseUsage": "tokens",
// or: { "default": "off", "discord": "full" }
},
}
```
### Three distinct session states
A session's `responseUsage` field has three representable states, each with
different semantics:
| State | Stored value | Effective mode |
| ------------------- | ------------------------------- | --------------------------------------------------------------------- |
| **Unset / inherit** | `undefined` (absent) | Falls through to `messages.responseUsage` config default, then `off`. |
| **Explicit off** | `"off"` (stored) | Always off — a non-off config default cannot re-enable the footer. |
| **Explicit on** | `"tokens"` or `"full"` (stored) | That mode, regardless of config default. |
### Precedence
Effective mode = session override → channel config entry → `default``off`.
An explicit `/usage off` is **persisted** as the literal value `"off"` in the
session, not the same as "unset." This means a non-off `messages.responseUsage`
default cannot turn the footer back on once the user has explicitly disabled it.
### Resetting vs. turning off
- `/usage off` — forces the footer off and persists that choice. A configured
non-off default cannot override this.
- `/usage reset` (aliases: `inherit`, `clear`, `default`) — clears the session
override. The session then **inherits** the effective config default
(`messages.responseUsage`). If no default is configured, the footer is off
(unchanged from before). Use this to "go back to default" without explicitly
turning the footer on.
- A full session reset (`/reset` or `/new`) or a session rollover **preserves**
the explicit usage-mode preference so the user's display choice survives
session rollovers. Only `/usage reset` (and its aliases) actually clears the
override.
### Toggle behavior
`/usage` with no arguments cycles: off → tokens → full → off. The starting point
for the cycle is the **effective** current mode (session override falling through
to the config default when unset), so the cycle is always consistent with what
the user sees in the footer.
### Config
With no config the prior behavior holds (footer off until `/usage`). Use
`/usage reset` to clear a session override and re-inherit the configured default.
## Custom `/usage full` footer
`/usage full` shows a built-in compact footer with model, reasoning, fast/slow,

View File

@@ -68,14 +68,6 @@
"source": "/reference/openclaw-sdk-api-design",
"destination": "/gateway/external-apps"
},
{
"source": "/reference/maturity-scorecard",
"destination": "/maturity/scorecard"
},
{
"source": "/reference/maturity-taxonomy",
"destination": "/maturity/taxonomy"
},
{
"source": "/mcp",
"destination": "/cli/mcp"
@@ -1860,8 +1852,6 @@
{
"group": "Release and CI",
"pages": [
"maturity/scorecard",
"maturity/taxonomy",
"reference/RELEASING",
"reference/full-release-validation",
"reference/release-performance-sweep",

View File

@@ -199,10 +199,6 @@ claude auth status --text
openclaw models auth login --provider anthropic --method cli --set-default
```
Docker installs need Claude Code installed and logged in inside the persisted
container home, not only on the host. See
[Claude CLI backend in Docker](/install/docker#claude-cli-backend-in-docker).
Use `agents.defaults.cliBackends.claude-cli.command` only when the `claude`
binary is not already on `PATH`.

View File

@@ -204,55 +204,6 @@ Controls elevated exec access outside the sandbox:
}
```
Agent entries can inject an environment only into their own `exec` child
processes. Use a SecretRef for credentials and set `inheritHostEnv: false` when the
Gateway process environment must not be inherited:
```json5
{
agents: {
list: [
{
id: "referrals",
tools: {
exec: {
inheritHostEnv: false,
env: {
GREENHOUSE_TOKEN: {
source: "env",
provider: "default",
id: "REFERRALS_GREENHOUSE_TOKEN",
},
},
},
},
},
],
},
}
```
`agents.list[].tools.exec.env` applies to `exec` only; it does not mutate
`process.env` or automatically inject credentials into model-provider or plugin
APIs. Trusted in-process plugin code can still inspect the materialized runtime
config, so this is not a plugin isolation boundary.
Configured values override same-named per-call values from the model. Trusted
`resolve_exec_env` hook output and channel context are applied afterward. Host
exec still rejects `PATH` and dangerous runtime/startup keys. Sandbox exec
already starts from a minimal environment. With `inheritHostEnv: false`,
Gateway exec also skips login-shell PATH discovery and cached shell-startup
state; configure `pathPrepend` or absolute commands when needed. For
`host: "node"`, configure scoped environment and inheritance isolation on the
node host. Both this map and `inheritHostEnv: false` are rejected because the
Gateway cannot clear the remote service environment or safely hold a scoped
credential back during remote approval preparation.
Treat this map as credential-bearing configuration: every command the agent can
run can read and exfiltrate these values, and command output can reveal them.
Plaintext values are reported by `openclaw secrets audit`; prefer SecretRefs.
Already-running background commands retain the environment captured when they
started after a config or secret reload.
### `tools.loopDetection`
Tool-loop safety checks are **disabled by default**. Set `enabled: true` to activate detection. Settings can be defined globally in `tools.loopDetection` and overridden per-agent at `agents.list[].tools.loopDetection`.

View File

@@ -525,47 +525,6 @@ the config fields that accept SecretRefs.
</Accordion>
</AccordionGroup>
## Per-agent exec environment variables
`agents.list[].tools.exec.env` supports SecretInput values, so a credential can
be resolved during Gateway activation and injected only into that agent's
`exec` child processes:
```json5
{
agents: {
list: [
{
id: "referrals",
tools: {
exec: {
inheritHostEnv: false,
env: {
GREENHOUSE_TOKEN: {
source: "env",
provider: "default",
id: "REFERRALS_GREENHOUSE_TOKEN",
},
},
},
},
},
],
},
}
```
This surface is exec-specific. It does not mutate the Gateway process
environment or automatically inject credentials into model-provider or plugin
APIs. Trusted in-process plugin code can inspect the materialized runtime
config. An unresolved active ref fails Gateway activation. SecretRefs are
materialized in the Gateway's protected in-memory config snapshot, so this
scopes subprocess injection rather than creating a same-process or same-OS-user
security boundary. Every command available to the agent can read these values,
command output can reveal them, and plaintext entries are reported by
`openclaw secrets audit`. Configure scoped environment on a node host itself;
agent exec env is rejected for `host: "node"`.
## MCP server environment variables
MCP server env vars configured via `plugins.entries.acpx.config.mcpServers` support SecretInput. This keeps API keys and tokens out of plaintext config:

View File

@@ -20,7 +20,6 @@ of Docker runners. This doc is a "how we test" guide:
- [QA overview](/concepts/qa-e2e-automation) - architecture, command surface, scenario authoring.
- [Matrix QA](/concepts/qa-matrix) - reference for `pnpm openclaw qa matrix`.
- [Maturity scorecard](/maturity/scorecard) - how release QA evidence supports stability and LTS decisions.
- [QA channel](/channels/qa-channel) - the synthetic transport plugin used by repo-backed scenarios.
This page covers running the regular test suites and Docker/Parallels runners. The QA-specific runners section below ([QA-specific runners](#qa-specific-runners)) lists the concrete `qa` invocations and points back at the references above.
@@ -741,20 +740,17 @@ Native dependency policy:
- Command: `pnpm test:e2e:openshell`
- File: `extensions/openshell/src/backend.e2e.test.ts`
- Scope:
- Reuses an active local OpenShell gateway
- Starts an isolated OpenShell gateway on the host via Docker
- Creates a sandbox from a temporary local Dockerfile
- Exercises OpenClaw's OpenShell backend over real `sandbox ssh-config` + SSH exec
- Verifies remote-canonical filesystem behavior through the sandbox fs bridge
- Expectations:
- Opt-in only; not part of the default `pnpm test:e2e` run
- Requires a local `openshell` CLI plus a working Docker daemon
- Requires an active local OpenShell gateway and its config source
- Uses isolated `HOME` / `XDG_CONFIG_HOME`, then destroys the test sandbox
- Uses isolated `HOME` / `XDG_CONFIG_HOME`, then destroys the test gateway and sandbox
- Useful overrides:
- `OPENCLAW_E2E_OPENSHELL=1` to enable the test when running the broader e2e suite manually
- `OPENCLAW_E2E_OPENSHELL_COMMAND=/path/to/openshell` to point at a non-default CLI binary or wrapper script
- `OPENCLAW_E2E_OPENSHELL_CONFIG_HOME=/path/to/config` to expose the registered gateway config to the isolated test
- `OPENCLAW_E2E_OPENSHELL_HOST_IP=172.18.0.1` to override the Docker gateway IP used by the host policy fixture
### Live (real providers + real models)

View File

@@ -279,100 +279,6 @@ If you use your own Compose file or `docker run` command, add the same host
mapping yourself, for example
`--add-host=host.docker.internal:host-gateway`.
### Claude CLI backend in Docker
The official OpenClaw Docker image does not pre-install Claude Code. Install and
log in to Claude Code inside the container user that runs OpenClaw, then persist
that container home so image upgrades do not erase the binary or Claude auth
state.
For new Docker installs, enable a persistent `/home/node` volume before running
setup:
```bash
export OPENCLAW_IMAGE="ghcr.io/openclaw/openclaw:latest"
export OPENCLAW_HOME_VOLUME="openclaw_home"
./scripts/docker/setup.sh
```
For an existing Docker install, stop the stack first and reload the current
Docker `.env` values before rerunning setup. The setup script does not read
`.env` on its own; it rewrites `.env` from the current shell and defaults. For
the generated `.env`, run:
```bash
set -a
. ./.env
set +a
export OPENCLAW_HOME_VOLUME="${OPENCLAW_HOME_VOLUME:-openclaw_home}"
./scripts/docker/setup.sh
```
If your `.env` contains values your shell cannot source, manually re-export the
existing values you rely on first, such as `OPENCLAW_IMAGE`, ports, bind mode,
custom paths, `OPENCLAW_EXTRA_MOUNTS`, sandbox, and skip-onboarding settings.
The generated overlay mounts the home volume for both `openclaw-gateway` and
`openclaw-cli`.
Run the remaining commands with the generated Compose overlay so both services
mount the persisted home. If your setup also uses `docker-compose.override.yml`,
include it before `docker-compose.extra.yml`.
Install Claude Code in that persisted home:
```bash
docker compose -f docker-compose.yml -f docker-compose.extra.yml run --rm \
--entrypoint sh openclaw-cli -lc \
'curl -fsSL https://claude.ai/install.sh | bash'
```
The native installer writes the `claude` binary under
`/home/node/.local/bin/claude`. Tell OpenClaw to use that container path:
```bash
docker compose -f docker-compose.yml -f docker-compose.extra.yml run --rm \
openclaw-cli config set \
agents.defaults.cliBackends.claude-cli.command \
/home/node/.local/bin/claude
```
Log in and verify from inside the same persisted container home:
```bash
docker compose -f docker-compose.yml -f docker-compose.extra.yml run --rm \
--entrypoint /home/node/.local/bin/claude openclaw-cli auth login
docker compose -f docker-compose.yml -f docker-compose.extra.yml run --rm \
--entrypoint /home/node/.local/bin/claude openclaw-cli auth status --text
docker compose -f docker-compose.yml -f docker-compose.extra.yml run --rm \
openclaw-cli models auth login \
--provider anthropic --method cli --set-default
docker compose -f docker-compose.yml -f docker-compose.extra.yml run --rm \
openclaw-cli models list --provider anthropic
```
After that, you can use the bundled `claude-cli` backend:
```bash
docker compose -f docker-compose.yml -f docker-compose.extra.yml run --rm \
openclaw-cli agent \
--agent main \
--model claude-cli/claude-sonnet-4-6 \
--message "Say hello from Docker Claude CLI"
```
`OPENCLAW_HOME_VOLUME` persists the native Claude Code install under
`/home/node/.local/bin` and `/home/node/.local/share/claude`, plus Claude Code
settings and auth state under `/home/node/.claude` and `/home/node/.claude.json`.
Persisting only `/home/node/.openclaw` is not enough for Claude CLI reuse. If
you use `OPENCLAW_EXTRA_MOUNTS` instead of a home volume, mount all of those
Claude paths into both Docker services.
<Note>
For shared production automation or predictable Anthropic billing, prefer the
Anthropic API-key path. Claude CLI reuse follows Claude Code's installed
version, account login, billing, and update behavior.
</Note>
### Bonjour / mDNS
Docker bridge networking usually does not forward Bonjour/mDNS multicast

View File

@@ -110,18 +110,14 @@ systemctl --user daemon-reload
### Windows (Scheduled Task)
Default task name is `OpenClaw Gateway` (or `OpenClaw Gateway (<profile>)`).
The task script lives under your state dir as `gateway.cmd`; current installs may
also create a windowless `gateway.vbs` launcher that Task Scheduler runs instead
of opening `gateway.cmd` directly.
The task script lives under your state dir.
```powershell
schtasks /Delete /F /TN "OpenClaw Gateway"
Remove-Item -Force "$env:USERPROFILE\.openclaw\gateway.cmd" -ErrorAction SilentlyContinue
Remove-Item -Force "$env:USERPROFILE\.openclaw\gateway.vbs" -ErrorAction SilentlyContinue
Remove-Item -Force "$env:USERPROFILE\.openclaw\gateway.cmd"
```
If you used a profile, delete the matching task name and the `gateway.cmd` /
`gateway.vbs` files under `~\.openclaw-<profile>`.
If you used a profile, delete the matching task name and `~\.openclaw-<profile>\gateway.cmd`.
## Normal install vs source checkout

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -124,11 +124,8 @@ openclaw gateway status --json
```
Native Windows CLI and Gateway flows are supported and continue to improve.
Managed startup uses Windows Scheduled Tasks when available. The task keeps the
readable `gateway.cmd` script in the OpenClaw state dir, but launches it through
a generated `gateway.vbs` WScript wrapper so the background Gateway does not open
a visible console window. If task creation is denied, OpenClaw falls back to a
per-user Startup-folder login item.
Managed startup uses Windows Scheduled Tasks when available and falls back to a
per-user Startup-folder login item if task creation is denied.
To install the Gateway service:

View File

@@ -103,65 +103,8 @@ The harness advertises support for the canonical `github-copilot` provider
- `github-copilot`
It also supports custom `models.providers` entries when the selected model has
a non-empty `baseUrl` and one of these API shapes:
- `openai-responses`
- `openai-completions`
- `ollama` (OpenAI-compatible completions)
- `azure-openai-responses`
- `anthropic-messages`
Native provider ids such as `openai`, `anthropic`, `google`, and `ollama` remain
owned by their native runtimes. Use a distinct custom provider id when routing
an endpoint through Copilot BYOK.
Copilot BYOK endpoints must be public-network HTTPS URLs. The harness gives the
Copilot SDK a per-attempt loopback proxy URL, then forwards provider traffic
through OpenClaw's guarded fetch path so DNS pinning and SSRF policy stay
owned by OpenClaw. Use the native OpenClaw runtime for local Ollama, LM Studio,
or LAN model servers.
## BYOK
Copilot BYOK uses the SDK's session-level custom provider contract. OpenClaw
passes the resolved model endpoint, API key, bearer-token mode, headers, model
id, and context/output limits without moving provider transport logic into
core.
For example:
```json5
{
agents: {
defaults: {
model: "custom-proxy/llama-3.1-8b",
models: {
"custom-proxy/llama-3.1-8b": {
agentRuntime: { id: "copilot" },
},
},
},
},
models: {
mode: "merge",
providers: {
"custom-proxy": {
baseUrl: "https://api.example.com/v1",
apiKey: "${CUSTOM_PROXY_API_KEY}",
api: "openai-responses",
authHeader: true,
models: [{ id: "llama-3.1-8b", name: "Llama 3.1 8B" }],
},
},
},
}
```
BYOK sessions are separately keyed from subscription sessions and from other
endpoints or credential fingerprints. Rotating the key, headers, model, or
endpoint creates a fresh Copilot SDK session instead of resuming incompatible
state.
Anything outside that set falls through `selection.ts`'s `auto_pi` branch back
to PI.
## Auth
@@ -208,11 +151,10 @@ Override with `copilotHome: <path>` on the attempt input when you need a
custom location (for example, a shared mount for migration).
Live harness tests use `OPENCLAW_COPILOT_AGENT_LIVE_TOKEN` when a direct token
is needed. The shared live-test setup intentionally scrubs
`COPILOT_GITHUB_TOKEN`, `GH_TOKEN`, and `GITHUB_TOKEN` after staging real auth
profiles into the isolated test home, so passing a `gh auth token` value
through the dedicated live-test variable avoids false skips without exposing
the token to unrelated suites.
is needed. The shared live-test setup intentionally scrubs `COPILOT_GITHUB_TOKEN`,
`GH_TOKEN`, and `GITHUB_TOKEN` after staging real auth profiles into the isolated
test home, so passing a `gh auth token` value through the dedicated live-test
variable avoids false skips without exposing the token to unrelated suites.
## Configuration surface
@@ -221,9 +163,9 @@ The harness reads its config from per-attempt input
`extensions/copilot/src/`:
- `copilotHome` — per-agent CLI state directory (defaults documented above).
- `model` — string or `{ provider, id, api?, baseUrl?, headers?, authHeader? }`.
When omitted, OpenClaw uses the agent's normal model selection and the
harness verifies the resolved provider is supported.
- `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`.
@@ -310,17 +252,21 @@ under `describe("runSideQuestion")`.
## Limitations
- The harness claims `github-copilot` plus unowned custom BYOK provider ids.
Manifest-owned native provider ids stay on their owning runtime even when
`agentRuntime.id` is forced to `copilot`.
- 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.
- `ask_user` uses the same OpenClaw prompt-and-reply path as the Codex
harness. When the Copilot SDK asks for user input, OpenClaw posts a
blocking prompt to the active channel/TUI and the next queued user
message resolves the SDK request.
- **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.
## Permissions and ask_user
@@ -382,15 +328,11 @@ the tool bridge. The bridge also forwards the bounded tool-construction
controls it can enforce at the SDK boundary: `includeCoreTools`, the
runtime tool allowlist, and `toolConstructionPlan`.
The bridge also uses the shared harness tool-surface helper from
`openclaw/plugin-sdk/agent-harness-tool-runtime` for PI parity. When
tool-search is enabled, the SDK sees compact control tools plus a hidden
catalog executor instead of every OpenClaw tool schema. When code mode is
enabled, the helper builds the same code-mode control surface and catalog
lifecycle used by other agent harnesses. Local-model lean defaults,
runtime-compatible schema filtering, directory hydration, and catalog
cleanup all stay in the shared helper so Copilot and Codex-adjacent
harnesses do not drift.
The remaining PI tool-search/code-mode fields are intentionally **not**
forwarded at MVP and tracked as follow-ups: `toolSearchCatalogRef`,
`includeToolSearchControls`, and `toolSearchCatalogExecutor`. Those
controls drive PI's native tool-search UI and have no direct Copilot SDK
analog yet.
### Session-level GitHub token
@@ -407,10 +349,7 @@ 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` uses `SessionConfig.onUserInputRequest`. The bridge accepts
choice indexes or labels for fixed-choice requests, accepts free-form
answers when the SDK request allows them, and cancels a pending request
when the OpenClaw attempt is aborted.
`ask_user` is intentionally hidden — see Limitations above.
## Related

View File

@@ -46,8 +46,10 @@ The default model is `embeddinggemma-300m-qat-Q8_0.gguf`. You can also point
## Native Runtime
Use Node 24 for the smoothest native install path. Source checkouts using pnpm
may need to approve and rebuild the native dependency:
Use Node 24 for the smoothest native install path. Source checkouts leave the
native build unapproved by default so normal workspace installs do not compile
llama.cpp. When you actually use local GGUF embeddings from source, approve and
rebuild the provider runtime:
```bash
pnpm approve-builds

View File

@@ -196,23 +196,6 @@ finish. Both helpers accept the same `{ event, ctx }` payload as
`runAgentHarnessAgentEndHook(...)`; their failures do not alter the completed
attempt result.
### User input and tool surfaces
Native harnesses that expose a runtime-level user-input request should use the
user-input helpers from `openclaw/plugin-sdk/agent-harness-runtime` to format
the prompt, deliver it through OpenClaw's blocking reply path, and normalize
choice/free-form answers back into the runtime's native response shape. The
helper keeps channel/TUI presentation consistent while each harness keeps its
own protocol parsing and pending-request lifecycle.
Native harnesses that need PI-like compact tool routing should use
`createAgentHarnessToolSurfaceRuntime(...)` from
`openclaw/plugin-sdk/agent-harness-tool-runtime`. It owns
tool-search/code-mode control selection, local-model lean defaults,
runtime-compatible schema filtering, hidden catalog execution, directory
hydration, and catalog cleanup. Harnesses still own their SDK-specific tool
conversion and native execution callback.
### Native Codex harness mode
The bundled `codex` harness is the native Codex mode for embedded OpenClaw

View File

@@ -104,12 +104,9 @@ Anthropic's current public docs:
<Warning>
Claude CLI reuse expects the OpenClaw process to run on the same host as the
Claude CLI login. Docker installs can persist a container home and log in to
Claude Code there; see
[Claude CLI backend in Docker](/install/docker#claude-cli-backend-in-docker).
Other container installs such as [Podman](/install/podman) do not mount host
`~/.claude` into setup or runtime; use an Anthropic API key there, or choose
a provider with OpenClaw-managed OAuth such as
Claude CLI login. Container installs such as [Podman](/install/podman) do
not mount host `~/.claude` into setup or runtime; use an Anthropic API key
there, or choose a provider with OpenClaw-managed OAuth such as
[OpenAI Codex](/providers/openai).
</Warning>

View File

@@ -84,8 +84,8 @@ Choose the Token Plan auth choice that matches the regional base URL shown in Xi
| Model ref | Input | Context | Max output | Reasoning | Notes |
| --------------------------------- | ----------- | --------- | ---------- | --------- | ------------- |
| `xiaomi-token-plan/mimo-v2.5-pro` | text | 1,048,576 | 131,072 | Yes | Default model |
| `xiaomi-token-plan/mimo-v2.5` | text, image | 1,048,576 | 131,072 | Yes | Multimodal |
| `xiaomi-token-plan/mimo-v2.5-pro` | text | 1,048,576 | 32,000 | Yes | Default model |
| `xiaomi-token-plan/mimo-v2.5` | text, image | 1,048,576 | 32,000 | Yes | Multimodal |
<Tip>
Token Plan onboarding validates the key shape and warns when a `tp-...` key is entered into the pay-as-you-go path, or an `sk-...` key is entered into the Token Plan path.
@@ -222,7 +222,7 @@ Token Plan:
reasoning: true,
input: ["text"],
contextWindow: 1048576,
maxTokens: 131072,
maxTokens: 32000,
},
{
id: "mimo-v2.5",
@@ -230,7 +230,7 @@ Token Plan:
reasoning: true,
input: ["text", "image"],
contextWindow: 1048576,
maxTokens: 131072,
maxTokens: 32000,
},
],
},

View File

@@ -37,7 +37,6 @@ Scope intent:
- `agents.defaults.memorySearch.remote.apiKey`
- `agents.list[].tts.providers.*.apiKey`
- `agents.list[].memorySearch.remote.apiKey`
- `agents.list[].tools.exec.env.*`
- `talk.providers.*.apiKey`
- `talk.realtime.providers.*.apiKey`
- `messages.tts.providers.*.apiKey`

View File

@@ -29,13 +29,6 @@
"secretShape": "secret_input",
"optIn": true
},
{
"id": "agents.list[].tools.exec.env.*",
"configFile": "openclaw.json",
"path": "agents.list[].tools.exec.env.*",
"secretShape": "secret_input",
"optIn": true
},
{
"id": "agents.list[].tts.providers.*.apiKey",
"configFile": "openclaw.json",

View File

@@ -76,8 +76,6 @@ Use these in chat:
configured for the active model.
- `/usage off|tokens|full` → appends a **per-response usage footer** to every reply.
- Persists per session (stored as `responseUsage`).
- `/usage reset` (aliases: `inherit`, `clear`, `default`) — clears the session
override so the session re-inherits the configured default.
- `/usage full` shows estimated cost only when OpenClaw has usage metadata and
local pricing for the active model. Otherwise it shows tokens only.
- `/usage cost` → shows a local cost summary from OpenClaw session logs.

View File

@@ -135,753 +135,3 @@ html.dark .nav-tabs-underline {
grid-template-columns: 1fr;
}
}
.maturity-hero {
display: grid;
gap: 14px;
margin: 10px 0 38px;
padding: 4px 0 26px 20px;
border-bottom: 1px solid color-mix(in oklab, rgb(var(--primary)) 22%, transparent);
border-left: 3px solid rgb(var(--primary));
}
.maturity-hero-compact {
margin-bottom: 34px;
}
.maturity-hero h2 {
max-width: 46rem;
margin: 0;
font-size: clamp(26px, 3vw, 38px);
line-height: 1.08;
letter-spacing: -0.01em;
}
.maturity-hero-title {
max-width: 46rem;
margin: 0;
font-size: clamp(26px, 3vw, 38px);
font-weight: 750;
line-height: 1.08;
letter-spacing: -0.01em;
}
.maturity-hero > p:not(.maturity-kicker):not(.maturity-jump-links) {
max-width: 58rem;
margin: 0;
font-size: 16px;
line-height: 1.65;
opacity: 0.76;
}
.maturity-kicker {
margin: 0;
color: rgb(var(--primary));
font-size: 10px;
font-weight: 750;
letter-spacing: 0.1em;
line-height: 1.3;
text-transform: uppercase;
}
.maturity-jump-links {
margin: 0;
font-size: 13px;
line-height: 1.5;
}
.maturity-jump-links a {
text-decoration: none;
}
.maturity-jump-links a:hover {
text-decoration: underline;
text-underline-offset: 3px;
}
.maturity-score-stable,
.maturity-band-stable {
color: #4ca574;
}
.maturity-score-beta,
.maturity-band-beta {
color: #849fd2;
}
.maturity-score-alpha,
.maturity-band-alpha {
color: #d39a4b;
}
.maturity-score-experimental,
.maturity-band-experimental {
color: #dc7669;
}
.maturity-score-clawesome,
.maturity-band-clawesome {
color: #46b59a;
}
.maturity-level-pill {
display: inline-flex;
align-items: center;
gap: 6px;
width: max-content;
max-width: 100%;
padding: 3px 8px;
border: 1px solid color-mix(in oklab, currentColor 32%, transparent);
border-radius: 999px;
background: color-mix(in oklab, currentColor 10%, transparent);
color: inherit;
font-size: 10px;
font-weight: 750;
line-height: 1.25;
white-space: nowrap;
}
.maturity-level-code {
font-size: 9px;
letter-spacing: 0.04em;
opacity: 0.72;
}
.maturity-level-experimental {
color: #dc7669;
}
.maturity-level-alpha {
color: #d39a4b;
}
.maturity-level-beta {
color: #849fd2;
}
.maturity-level-stable {
color: #4ca574;
}
.maturity-level-clawesome {
color: #46b59a;
}
.maturity-summary-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
margin: 14px 0 20px;
border-top: 1px solid color-mix(in oklab, rgb(var(--primary)) 18%, transparent);
border-bottom: 1px solid color-mix(in oklab, rgb(var(--primary)) 18%, transparent);
}
.maturity-summary-item {
display: grid;
gap: 10px;
min-width: 0;
padding: 18px 20px 18px 0;
}
.maturity-summary-item + .maturity-summary-item {
padding-left: 20px;
border-left: 1px solid color-mix(in oklab, rgb(var(--primary)) 14%, transparent);
}
.maturity-summary-heading {
display: flex;
align-items: baseline;
gap: 9px;
}
.maturity-summary-value {
display: inline-block;
font-size: 30px;
font-weight: 750;
letter-spacing: -0.04em;
}
.maturity-summary-heading > span:not(.maturity-summary-value) {
font-size: 13px;
font-weight: 700;
}
.maturity-summary-bar {
height: 7px;
overflow: hidden;
background: color-mix(in oklab, currentColor 14%, transparent);
}
.maturity-summary-bar span {
display: block;
width: calc(var(--score) * 1%);
height: 100%;
background: currentColor;
}
.maturity-summary-meta {
display: flex;
flex-wrap: wrap;
gap: 4px 10px;
font-size: 11px;
line-height: 1.4;
}
.maturity-summary-meta span:first-child {
font-weight: 700;
}
.maturity-summary-meta span:last-child {
opacity: 0.62;
}
.maturity-band-list {
display: flex;
margin: 12px 0 30px;
border-top: 1px solid color-mix(in oklab, rgb(var(--primary)) 16%, transparent);
border-bottom: 1px solid color-mix(in oklab, rgb(var(--primary)) 16%, transparent);
}
.maturity-band {
display: grid;
flex: 1 1 0;
gap: 3px;
padding: 10px 12px 11px 0;
}
.maturity-band + .maturity-band {
padding-left: 12px;
border-left: 1px solid color-mix(in oklab, rgb(var(--primary)) 12%, transparent);
}
.maturity-band-title {
font-size: 12px;
font-weight: 700;
}
.maturity-band-title + span {
color: inherit;
font-size: 11px;
opacity: 0.7;
}
.maturity-band > span:last-child {
color: inherit;
font-size: 11px;
opacity: 0.7;
}
.maturity-band span {
color: inherit;
font-size: 11px;
opacity: 0.7;
}
.maturity-band .maturity-level-pill {
font-size: 10px;
opacity: 1;
}
.maturity-band .maturity-level-pill span {
font-size: inherit;
opacity: inherit;
}
.maturity-score {
display: grid;
gap: 4px;
min-width: 0;
color: inherit;
font-size: 11px;
font-weight: 700;
}
.maturity-score-label {
display: flex;
justify-content: space-between;
gap: 6px;
line-height: 1.2;
}
.maturity-score-label > span:first-child {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.maturity-score-label > span:last-child {
flex: 0 0 auto;
}
.maturity-score-label .maturity-level-pill {
gap: 4px;
padding: 2px 6px;
font-size: 9px;
}
.maturity-score-label-text {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.maturity-summary-meta .maturity-level-pill {
opacity: 1;
}
.maturity-meter {
display: inline-block;
width: 100%;
height: 3px;
overflow: hidden;
background: color-mix(in oklab, currentColor 15%, transparent);
vertical-align: middle;
}
.maturity-meter > span {
display: block;
width: 0;
height: 100%;
background: currentColor;
}
.maturity-score-unscored,
.maturity-lts-none {
color: inherit;
opacity: 0.52;
}
.maturity-surface-table {
display: grid;
gap: 0;
margin: 8px 0 22px;
}
.maturity-surface-row {
display: grid;
grid-template-columns: minmax(190px, 1.55fr) repeat(3, minmax(110px, 1fr)) minmax(72px, 0.55fr);
gap: 12px;
align-items: center;
padding: 13px 0;
border-top: 1px solid color-mix(in oklab, currentColor 14%, transparent);
}
.maturity-surface-row-header {
padding: 0 0 9px;
border-top: 0;
color: inherit;
font-size: 10px;
font-weight: 750;
letter-spacing: 0.04em;
opacity: 0.56;
text-transform: uppercase;
}
.maturity-surface-name {
display: grid;
gap: 3px;
min-width: 0;
border-bottom: 0 !important;
text-decoration: none;
}
.maturity-surface-name:hover .maturity-surface-title {
color: rgb(var(--primary));
}
.maturity-surface-title {
overflow-wrap: anywhere;
font-size: 13px;
font-weight: 700;
line-height: 1.25;
}
.maturity-surface-meta {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 6px;
min-width: 0;
}
.maturity-surface-meta > span:not(.maturity-level-pill) {
font-size: 11px;
opacity: 0.62;
}
.maturity-surface-meta .maturity-level-pill {
opacity: 1;
}
.maturity-surface-metric {
min-width: 0;
}
.maturity-surface-metric-label {
display: none;
font-size: 10px;
opacity: 0.62;
}
.maturity-surface-support {
justify-self: start;
}
.maturity-lts {
display: inline-flex;
align-items: center;
gap: 5px;
color: inherit;
font-size: 11px;
font-weight: 700;
white-space: nowrap;
}
.maturity-lts::before {
width: 6px;
height: 6px;
border-radius: 50%;
background: currentColor;
content: "";
}
.maturity-lts-partial {
color: #d39a4b;
}
.maturity-lts-full {
color: #4ca574;
}
.maturity-evidence-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
margin: 14px 0 24px;
border-top: 1px solid color-mix(in oklab, rgb(var(--primary)) 16%, transparent);
border-bottom: 1px solid color-mix(in oklab, rgb(var(--primary)) 16%, transparent);
}
.maturity-evidence-card {
display: grid;
gap: 4px;
min-width: 0;
padding: 14px 16px 14px 0;
}
.maturity-evidence-card + .maturity-evidence-card {
padding-left: 16px;
border-left: 1px solid color-mix(in oklab, rgb(var(--primary)) 12%, transparent);
}
.maturity-evidence-title {
font-size: 13px;
font-weight: 700;
}
.maturity-evidence-card span {
font-size: 11px;
line-height: 1.4;
opacity: 0.68;
}
.maturity-readiness-summary {
margin: 0 0 12px;
font-size: 12px;
opacity: 0.65;
}
.maturity-readiness-list {
display: grid;
margin: 0;
border-top: 1px solid color-mix(in oklab, rgb(var(--primary)) 14%, transparent);
}
.maturity-readiness-row {
display: grid;
grid-template-columns: minmax(0, 1.7fr) minmax(120px, 0.8fr) minmax(110px, 0.7fr);
gap: 14px;
align-items: center;
padding: 11px 0;
border-bottom: 1px solid color-mix(in oklab, rgb(var(--primary)) 10%, transparent);
font-size: 11px;
}
.maturity-readiness-row-header {
padding: 8px 0;
border-bottom-color: color-mix(in oklab, rgb(var(--primary)) 14%, transparent);
font-size: 10px;
font-weight: 750;
letter-spacing: 0.04em;
opacity: 0.56;
text-transform: uppercase;
}
.maturity-readiness-area {
display: grid;
gap: 3px;
min-width: 0;
}
.maturity-readiness-title {
overflow-wrap: anywhere;
font-weight: 700;
}
.maturity-readiness-status {
font-size: 10px;
opacity: 0.62;
}
.maturity-readiness-status-ready {
color: #4ca574;
}
.maturity-readiness-status-partially-reviewed {
color: #d39a4b;
}
.maturity-readiness-status-needs-review {
color: #dc7669;
}
.maturity-category-list {
display: grid;
width: 100%;
margin-top: 4px;
overflow: hidden;
}
.maturity-category-row {
display: grid;
grid-template-columns: minmax(180px, 1.55fr) repeat(3, minmax(100px, 1fr)) minmax(140px, 1.2fr);
gap: 12px;
align-items: center;
padding: 12px 0;
border-top: 1px solid color-mix(in oklab, rgb(var(--primary)) 11%, transparent);
font-size: 11px;
}
.maturity-category-row-header {
padding: 8px 0;
border-top: 0;
color: inherit;
font-size: 10px;
font-weight: 750;
letter-spacing: 0.04em;
opacity: 0.56;
text-transform: uppercase;
}
.maturity-category-area {
display: grid;
gap: 3px;
min-width: 0;
}
.maturity-category-title {
overflow-wrap: anywhere;
font-weight: 700;
}
.maturity-category-area > span:last-child {
font-size: 10px;
opacity: 0.62;
}
.maturity-category-docs {
min-width: 0;
overflow-wrap: anywhere;
line-height: 1.4;
}
.maturity-category-docs a {
text-decoration: none;
}
.maturity-category-docs a:hover {
text-decoration: underline;
text-underline-offset: 3px;
}
.maturity-level-list {
display: grid;
margin: 12px 0 28px;
border-top: 1px solid color-mix(in oklab, rgb(var(--primary)) 16%, transparent);
border-bottom: 1px solid color-mix(in oklab, rgb(var(--primary)) 16%, transparent);
}
.maturity-level-row {
display: grid;
grid-template-columns: minmax(130px, 0.32fr) minmax(0, 1fr);
gap: 4px 14px;
padding: 13px 0;
border-top: 1px solid color-mix(in oklab, rgb(var(--primary)) 11%, transparent);
}
.maturity-level-row:first-child {
border-top: 0;
}
.maturity-level-title {
grid-row: span 2;
font-size: 13px;
font-weight: 700;
}
.maturity-level-title .maturity-level-pill {
opacity: 1;
}
.maturity-level-row span,
.maturity-level-promotion {
font-size: 12px;
line-height: 1.45;
opacity: 0.68;
}
.maturity-surface-link {
display: grid;
gap: 3px;
margin: 0;
padding: 11px 0;
border-bottom: 1px solid color-mix(in oklab, currentColor 14%, transparent);
text-decoration: none;
}
.maturity-surface-link:hover {
color: rgb(var(--primary));
}
.maturity-surface-link .maturity-surface-title {
font-size: 13px;
font-weight: 700;
}
.maturity-surface-link > .maturity-surface-meta {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 6px;
}
.maturity-surface-link > .maturity-surface-meta > span:not(.maturity-level-pill) {
font-size: 11px;
opacity: 0.68;
}
.maturity-surface-link > .maturity-surface-meta .maturity-level-pill {
opacity: 1;
}
.maturity-surface-rollup {
display: flex;
flex-wrap: wrap;
gap: 5px 14px;
margin: 0 0 14px;
padding: 9px 0;
border-top: 1px solid color-mix(in oklab, rgb(var(--primary)) 13%, transparent);
border-bottom: 1px solid color-mix(in oklab, rgb(var(--primary)) 13%, transparent);
}
.maturity-surface-rollup > span {
color: inherit;
font-size: 11px;
font-weight: 700;
}
#content table .maturity-score,
#content table .maturity-lts {
white-space: nowrap;
}
@media (max-width: 960px) {
.maturity-summary-grid,
.maturity-evidence-grid {
grid-template-columns: 1fr;
}
.maturity-summary-item,
.maturity-summary-item + .maturity-summary-item,
.maturity-evidence-card,
.maturity-evidence-card + .maturity-evidence-card {
padding: 14px 0;
border-left: 0;
}
.maturity-summary-item + .maturity-summary-item,
.maturity-evidence-card + .maturity-evidence-card {
border-top: 1px solid color-mix(in oklab, rgb(var(--primary)) 12%, transparent);
}
.maturity-surface-row {
grid-template-columns: minmax(160px, 1.35fr) repeat(3, minmax(98px, 1fr)) minmax(70px, 0.5fr);
gap: 8px;
}
.maturity-category-row {
grid-template-columns: minmax(160px, 1.35fr) repeat(3, minmax(86px, 1fr)) minmax(110px, 1fr);
gap: 8px;
}
}
@media (max-width: 640px) {
.maturity-hero {
padding-left: 14px;
}
.maturity-surface-row-header {
display: none;
}
.maturity-surface-row {
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
gap: 9px 12px;
}
.maturity-surface-metric {
display: grid;
grid-template-columns: auto 1fr;
align-items: center;
gap: 6px;
}
.maturity-surface-metric-label {
display: block;
}
.maturity-surface-support {
justify-self: end;
}
.maturity-readiness-row,
.maturity-category-row {
grid-template-columns: 1fr;
gap: 5px;
}
.maturity-readiness-row-header,
.maturity-category-row-header {
display: none;
}
.maturity-category-docs {
padding-top: 3px;
}
.maturity-band-list {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.maturity-band + .maturity-band {
padding-left: 0;
border-left: 0;
}
.maturity-level-row {
grid-template-columns: 1fr;
}
.maturity-level-title {
grid-row: auto;
}
}

View File

@@ -22,8 +22,7 @@ Working directory for the command.
</ParamField>
<ParamField path="env" type="object">
Key/value environment overrides. Per-agent configured values are applied after
these model-supplied values.
Key/value environment overrides merged on top of the inherited environment.
</ParamField>
<ParamField path="yieldMs" type="number" default="10000">
@@ -90,7 +89,6 @@ Notes:
`$OPENCLAW_STATE_DIR/cache/shell-snapshots/`, then sources that snapshot before each exec command.
Secret-looking variables are excluded; sandbox and node exec do not use this snapshot. Set
`OPENCLAW_EXEC_SHELL_SNAPSHOT=0` in the Gateway process environment to disable this snapshot path.
Per-agent `tools.exec.inheritHostEnv: false` also disables it.
- Host execution (`gateway`/`node`) rejects `env.PATH` and loader overrides (`LD_*`/`DYLD_*`) to
prevent binary hijacking or injected code.
- OpenClaw sets `OPENCLAW_SHELL=exec` in the spawned command environment (including PTY and sandbox execution) so shell/profile rules can detect exec-tool context.
@@ -115,8 +113,6 @@ Notes:
- `tools.exec.notifyOnExit` (default: true): when true, backgrounded exec sessions enqueue a system event and request a heartbeat on exit.
- `tools.exec.approvalRunningNoticeMs` (default: 10000): emit a single "running" notice when an approval-gated exec runs longer than this (0 disables).
- `tools.exec.timeoutSec` (default: 1800): default per-command exec timeout in seconds. Per-call `timeout` overrides it; per-call `timeout: 0` disables the exec process timeout.
- `agents.list[].tools.exec.env`: credential-oriented environment values injected only into that agent's gateway/sandbox exec children. Values support SecretRefs; node-host exec rejects this map.
- `agents.list[].tools.exec.inheritHostEnv` (default: true): set false to omit the Gateway process environment and shell-startup snapshot from Gateway-hosted exec. This is rejected for `host=node`; sandbox exec is already minimal.
- `tools.exec.host` (default: `auto`; resolves to `sandbox` when sandbox runtime is active, `gateway` otherwise)
- `tools.exec.security` (default: `deny` for sandbox, `full` for gateway + node when unset)
- `tools.exec.ask` (default: `off`)
@@ -145,9 +141,7 @@ Example:
### PATH handling
- `host=gateway`: normally merges your login-shell `PATH` into the exec environment. With
`agents.list[].tools.exec.inheritHostEnv: false`, this merge is skipped; use an absolute command or
`tools.exec.pathPrepend`. `env.PATH` overrides are
- `host=gateway`: merges your login-shell `PATH` into the exec environment. `env.PATH` overrides are
rejected for host execution. The daemon itself still runs with a minimal `PATH`:
- macOS: `/opt/homebrew/bin`, `/usr/local/bin`, `/usr/bin`, `/bin`
- Linux: `/usr/local/bin`, `/usr/bin`, `/bin`

View File

@@ -88,9 +88,8 @@ still returns one synthesized answer with citations rather than an N-result
list.
`freshness` accepts `day`, `week`, `month`, `year`, and the shared shortcuts
`pd`, `pw`, `pm`, and `py`. `day`/`pd` adds a recency instruction to the Gemini
query instead of a hard 24-hour range. `week`, `month`, `year`, and explicit
`date_after`/`date_before` ranges set Gemini Google Search grounding's
`pd`, `pw`, `pm`, and `py`. OpenClaw converts these values, or an explicit
`date_after`/`date_before` range, into Gemini Google Search grounding's
`timeRangeFilter`. `country`, `language`, and `domain_filter` are not supported.
## Model selection

View File

@@ -152,8 +152,8 @@ publish and sync.
| Update all workspace skills | `openclaw skills update --all` |
| Update a shared managed skill | `openclaw skills update @owner/<slug> --global` |
| Update all shared managed skills | `openclaw skills update --all --global` |
| Verify a skill's trust envelope | `openclaw skills verify @owner/<slug>` |
| Print the generated Skill Card | `openclaw skills verify @owner/<slug> --card` |
| Verify a skill's trust envelope | `openclaw skills verify <slug>` |
| Print the generated Skill Card | `openclaw skills verify <slug> --card` |
| Publish / sync via ClawHub CLI | `clawhub sync --all` |
<AccordionGroup>
@@ -171,11 +171,9 @@ publish and sync.
</Accordion>
<Accordion title="Verification and security scanning">
`openclaw skills verify @owner/<slug>` asks ClawHub for the skill's
`openclaw skills verify <slug>` asks ClawHub for the skill's
`clawhub.skill.verify.v1` trust envelope. Installed ClawHub skills verify
against the version and registry recorded in `.clawhub/origin.json`.
Bare slugs remain accepted for existing installed or unambiguous skills, but
owner-qualified refs avoid publisher ambiguity.
ClawHub skill pages expose the latest security scan state before install,
with detail pages for VirusTotal, ClawScan, and static analysis. The

View File

@@ -240,7 +240,7 @@ plugins.
| `/tasks` | List active/recent background tasks for the current session |
| `/context [list\|detail\|map\|json]` | Explain how context is assembled |
| `/whoami` | Show your sender id. Alias: `/id` |
| `/usage off\|tokens\|full\|reset\|cost` | Control the per-response usage footer (`reset`/`inherit`/`clear`/`default` clears the session override to re-inherit the configured default) or print a local cost summary |
| `/usage off\|tokens\|full\|cost` | Control the per-response usage footer or print a local cost summary |
</Accordion>
<Accordion title="Skills, allowlists, approvals">

View File

@@ -389,8 +389,8 @@ show the `x_search` prompt.
freshness ranges require both start and end dates.
Gemini, Grok, and Kimi return one synthesized answer with citations. They
accept `count` for shared-tool compatibility, but it does not change the
grounded answer shape. Gemini treats `day` freshness as a recency hint; wider
freshness values and explicit dates set Google Search grounding time ranges.
grounded answer shape. Gemini supports `freshness`, `date_after`, and
`date_before` by converting them to Google Search grounding time ranges.
Perplexity behaves the same way when you use the Sonar/OpenRouter
compatibility path (`plugins.entries.perplexity.config.webSearch.baseUrl` /
`model` or `OPENROUTER_API_KEY`).

View File

@@ -126,7 +126,7 @@ Session controls:
- `/verbose <on|full|off>`
- `/trace <on|off>`
- `/reasoning <on|off|stream>`
- `/usage <off|tokens|full|reset>` (`reset`/`inherit`/`clear`/`default` clears the session override)
- `/usage <off|tokens|full>`
- `/goal [status] | /goal start <objective> | /goal pause|resume|complete|block|clear`
- `/elevated <on|off|ask|full>` (alias: `/elev`)
- `/activation <mention|always>`

View File

@@ -1,16 +1,16 @@
{
"name": "@openclaw/acpx",
"version": "2026.6.10",
"version": "2026.6.9",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@openclaw/acpx",
"version": "2026.6.10",
"version": "2026.6.9",
"dependencies": {
"@agentclientprotocol/claude-agent-acp": "0.39.0",
"@zed-industries/codex-acp": "0.15.0",
"acpx": "0.11.2",
"acpx": "0.10.0",
"zod": "4.4.3"
}
},
@@ -196,9 +196,9 @@
}
},
"node_modules/@clack/core": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/@clack/core/-/core-1.3.1.tgz",
"integrity": "sha512-fT1qHVGAag4IEkrupZ6lRRbNCs1vS9P01KB/sG8zKgvUztbYtFBtQpjSITNwooDZ83tpsPzP0mRNs1/KVszCRA==",
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/@clack/core/-/core-1.4.1.tgz",
"integrity": "sha512-FILJa1gGKEFTGZAJE9RpVhrjKz3c3h4ar60dSv6cGuDqufQ84YEIS3GAGvZiN+H6yaLbbvTFNejjCC4tXpZEuw==",
"license": "MIT",
"dependencies": {
"fast-wrap-ansi": "^0.2.0",
@@ -209,12 +209,12 @@
}
},
"node_modules/@clack/prompts": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/@clack/prompts/-/prompts-1.4.0.tgz",
"integrity": "sha512-S0My7XPGIgpRWMDG8uRqalbgT+a6FmCUdOW+HaIOVVpUPHOb7RrpvjTjiODadKp06fsrVDJZlIzc6yCTp4AnxA==",
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/@clack/prompts/-/prompts-1.5.1.tgz",
"integrity": "sha512-zccHj2z2oCCO4yrDiRSlFOxWerGqRiysP7a5jPK6uoI9URKAquwY42Dd/iUP8JWHxEzdRe4TlbvZCo8z1/mhrw==",
"license": "MIT",
"dependencies": {
"@clack/core": "1.3.1",
"@clack/core": "1.4.1",
"fast-string-width": "^3.0.2",
"fast-wrap-ansi": "^0.2.0",
"sisteransi": "^1.0.5"
@@ -701,7 +701,6 @@
"version": "0.15.0",
"resolved": "https://registry.npmjs.org/@zed-industries/codex-acp/-/codex-acp-0.15.0.tgz",
"integrity": "sha512-eAv7sGBeiYrYkOulF729nrM51szS7WIhBtugRj5wWq6csRKZUhAZfoUZlF8xUWdHPtOIzd/eT6MNG6gMHu6z0w==",
"deprecated": "This package has been replaced by @agentclientprotocol/codex-acp. Please migrate to continue receiving updates.",
"license": "Apache-2.0",
"bin": {
"codex-acp": "bin/codex-acp.js"
@@ -722,7 +721,6 @@
"cpu": [
"arm64"
],
"deprecated": "This package has been replaced by @agentclientprotocol/codex-acp. Please migrate to continue receiving updates.",
"license": "Apache-2.0",
"optional": true,
"os": [
@@ -739,7 +737,6 @@
"cpu": [
"x64"
],
"deprecated": "This package has been replaced by @agentclientprotocol/codex-acp. Please migrate to continue receiving updates.",
"license": "Apache-2.0",
"optional": true,
"os": [
@@ -756,7 +753,6 @@
"cpu": [
"arm64"
],
"deprecated": "This package has been replaced by @agentclientprotocol/codex-acp. Please migrate to continue receiving updates.",
"license": "Apache-2.0",
"optional": true,
"os": [
@@ -773,7 +769,6 @@
"cpu": [
"x64"
],
"deprecated": "This package has been replaced by @agentclientprotocol/codex-acp. Please migrate to continue receiving updates.",
"license": "Apache-2.0",
"optional": true,
"os": [
@@ -790,7 +785,6 @@
"cpu": [
"arm64"
],
"deprecated": "This package has been replaced by @agentclientprotocol/codex-acp. Please migrate to continue receiving updates.",
"license": "Apache-2.0",
"optional": true,
"os": [
@@ -807,7 +801,6 @@
"cpu": [
"x64"
],
"deprecated": "This package has been replaced by @agentclientprotocol/codex-acp. Please migrate to continue receiving updates.",
"license": "Apache-2.0",
"optional": true,
"os": [
@@ -831,15 +824,15 @@
}
},
"node_modules/acpx": {
"version": "0.11.2",
"resolved": "https://registry.npmjs.org/acpx/-/acpx-0.11.2.tgz",
"integrity": "sha512-ksTmfJDVqUAJJXsNDamEno03AMZ/aAZzXk/h5nt61VsLc/jcpoDMfCVpErzuYNJjwCd0V6Zm5o6F8OoqxsjQWA==",
"version": "0.10.0",
"resolved": "https://registry.npmjs.org/acpx/-/acpx-0.10.0.tgz",
"integrity": "sha512-hd48XV03gG3sd409T1lDrOKJTTz1ap4g0wrndXjxQ590tN85pBYlvfNLyerybvGRrtUGsZjNdt99r1jpIt6ukA==",
"license": "MIT",
"dependencies": {
"@agentclientprotocol/sdk": "^0.28.1",
"commander": "^15.0.0",
"skillflag": "^0.2.0",
"tsx": "^4.22.4",
"@agentclientprotocol/sdk": "^0.22.1",
"commander": "^14.0.3",
"skillflag": "^0.1.4",
"tsx": "^4.22.0",
"zod": "^4.4.3"
},
"bin": {
@@ -849,15 +842,6 @@
"node": ">=22.13.0"
}
},
"node_modules/acpx/node_modules/@agentclientprotocol/sdk": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@agentclientprotocol/sdk/-/sdk-0.28.1.tgz",
"integrity": "sha512-Z2Frs6YtPhnZZ+XwFXyQkRDXY0fn8FjCalEs0W4yUhQnY4TztmNq0/RnfzWdFN3vqT3h0jTz5klzYbZHGxCDyQ==",
"license": "Apache-2.0",
"peerDependencies": {
"zod": "^3.25.0 || ^4.0.0"
}
},
"node_modules/ajv": {
"version": "8.20.0",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz",
@@ -1059,12 +1043,12 @@
}
},
"node_modules/commander": {
"version": "15.0.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-15.0.0.tgz",
"integrity": "sha512-z67u4ZhzCL/Tydu1lJARtEZYWbWaN7oYLHbsuzocr6y4N6WZAagG3RQ4FW61V1/0+jImpj293XfrcYnd1qxtPg==",
"version": "14.0.3",
"resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz",
"integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==",
"license": "MIT",
"engines": {
"node": ">=22.12.0"
"node": ">=20"
}
},
"node_modules/content-disposition": {
@@ -2061,9 +2045,9 @@
"license": "MIT"
},
"node_modules/skillflag": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/skillflag/-/skillflag-0.2.0.tgz",
"integrity": "sha512-7ZmEpBeEoPLc+hqZ/StAnCO/hulgEPANzPyZgOM/CZ5zc3b0ApSp3URavY5POM/OKyi5d9+UC/Q21OoiYC2kJw==",
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/skillflag/-/skillflag-0.1.4.tgz",
"integrity": "sha512-egFg+XCF5sloOWdtzxZivTX7n4UDj5pxQoY33wbT8h+YSDjMQJ76MZUg2rXQIBXmIDtlZhLgirS1g/3R5/qaHA==",
"license": "MIT",
"dependencies": {
"@clack/prompts": "^1.0.1",

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/acpx",
"version": "2026.6.10",
"version": "2026.6.9",
"description": "OpenClaw ACP runtime backend with plugin-owned session and transport management.",
"repository": {
"type": "git",
@@ -10,7 +10,7 @@
"dependencies": {
"@agentclientprotocol/claude-agent-acp": "0.39.0",
"@zed-industries/codex-acp": "0.15.0",
"acpx": "0.11.2",
"acpx": "0.10.0",
"zod": "4.4.3"
},
"devDependencies": {
@@ -26,10 +26,10 @@
"minHostVersion": ">=2026.4.25"
},
"compat": {
"pluginApi": ">=2026.6.10"
"pluginApi": ">=2026.6.9"
},
"build": {
"openclawVersion": "2026.6.10",
"openclawVersion": "2026.6.9",
"staticAssets": [
{
"source": "./src/runtime-internals/mcp-proxy.mjs",

View File

@@ -251,15 +251,6 @@ describe("prepareAcpxCodexAuthConfig", () => {
expect(wrapper).not.toMatch(
/forceKillTimer = setTimeout\(\(\) => killChildTree\("SIGKILL"\), 1_500\);\s*forceKillTimer\.unref\?\.\(\);\s*process\.exit\(1\);/s,
);
// Orphan detection must trigger on any PPID change, not only when the new
// PPID is init (1). Systemd user services and container init reparent
// orphaned processes to a session manager or container init (PID != 1),
// and the older `process.ppid !== 1` guard would silently leak the codex
// adapter tree there.
expect(wrapper).not.toContain("process.ppid !== 1");
expect(wrapper).toMatch(
/setInterval\(\(\) => \{[\s\S]*?if \(process\.ppid === originalParentPid\) \{\s*return;\s*\}/,
);
});
it("uses the bundled Claude ACP dependency by default when it is installed", async () => {

View File

@@ -475,13 +475,7 @@ const parentWatcher =
process.platform === "win32"
? undefined
: setInterval(() => {
// Orphan detection: parent PID changed means our original parent died.
// The new parent could be PID 1 (init) on bare-metal hosts, OR a
// systemd user-session manager, OR a container init, OR a session
// leader — depending on environment. Previously this only triggered
// on PPID == 1, which missed all systemd-managed deployments and
// leaked codex-acp adapter trees on every gateway restart.
if (process.ppid === originalParentPid) {
if (process.ppid === originalParentPid || process.ppid !== 1) {
return;
}
if (orphanCleanupStarted) {

View File

@@ -2,7 +2,6 @@
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { RequestedModelUnsupportedError } from "acpx/runtime";
import { beforeEach, describe, expect, it, vi } from "vitest";
import {
AcpRuntimeError,
@@ -709,100 +708,6 @@ describe("AcpxRuntime fresh reset wrapper", () => {
});
});
it("retries without a model when ACPX reports missing model capability", async () => {
const baseStore: TestSessionStore = {
load: vi.fn(async () => undefined),
save: vi.fn(async () => {}),
};
const { runtime, delegate } = makeRuntime(baseStore, {
agentRegistry: {
resolve: (agentName: string) => (agentName === "opencode" ? "opencode acp" : agentName),
list: () => ["opencode"],
},
});
const ensure = vi
.spyOn(delegate, "ensureSession")
.mockRejectedValueOnce(
new RequestedModelUnsupportedError(
"Cannot apply --model: the ACP agent did not advertise model support",
"missing-capability",
),
)
.mockResolvedValueOnce({
sessionKey: "agent:opencode:acp:test",
backend: "acpx",
runtimeSessionName: "opencode",
});
await runtime.ensureSession({
sessionKey: "agent:opencode:acp:test",
agent: "opencode",
mode: "persistent",
model: "openrouter/owl-alpha",
});
expect(ensure).toHaveBeenCalledTimes(2);
expect(readFirstEnsureSessionInput(ensure)).toMatchObject({
model: "openrouter/owl-alpha",
sessionOptions: { model: "openrouter/owl-alpha" },
});
const [, secondCall] = ensure.mock.calls;
expect(secondCall?.[0]).not.toHaveProperty("sessionOptions");
expect((secondCall?.[0] as { model?: string } | undefined)?.model).toBeUndefined();
});
it("does not retry when ACPX rejects an explicitly unsupported model id", async () => {
const baseStore: TestSessionStore = {
load: vi.fn(async () => undefined),
save: vi.fn(async () => {}),
};
const { runtime, delegate } = makeRuntime(baseStore, {
agentRegistry: {
resolve: (agentName: string) => (agentName === "opencode" ? "opencode acp" : agentName),
list: () => ["opencode"],
},
});
const ensure = vi
.spyOn(delegate, "ensureSession")
.mockRejectedValueOnce(
new RequestedModelUnsupportedError(
"Cannot apply --model: the ACP agent did not advertise that model",
"unadvertised-model",
),
);
await expect(
runtime.ensureSession({
sessionKey: "agent:opencode:acp:test",
agent: "opencode",
mode: "persistent",
model: "unknown/model",
}),
).rejects.toThrow("did not advertise that model");
expect(ensure).toHaveBeenCalledTimes(1);
});
it("does not retry an unrelated error with similar wording", async () => {
const baseStore: TestSessionStore = {
load: vi.fn(async () => undefined),
save: vi.fn(async () => {}),
};
const { runtime, delegate } = makeRuntime(baseStore);
const ensure = vi
.spyOn(delegate, "ensureSession")
.mockRejectedValueOnce(new Error("the ACP agent did not advertise model support"));
await expect(
runtime.ensureSession({
sessionKey: "agent:main:acp:test",
agent: "main",
mode: "persistent",
model: "openrouter/owl-alpha",
}),
).rejects.toThrow("did not advertise model support");
expect(ensure).toHaveBeenCalledTimes(1);
});
it("injects Codex ACP startup config into the scoped registry", () => {
expect(testing.isCodexAcpCommand(CODEX_ACP_COMMAND)).toBe(true);
expect(testing.isCodexAcpCommand(CODEX_ACP_WRAPPER_COMMAND)).toBe(true);

View File

@@ -13,7 +13,6 @@ import {
createFileSessionStore,
decodeAcpxRuntimeHandleState,
encodeAcpxRuntimeHandleState,
isRequestedModelUnsupportedError,
type AcpAgentRegistry,
type AcpRuntimeDoctorReport,
type AcpRuntimeEvent,
@@ -587,26 +586,6 @@ function withAcpxSessionOptions(input: OpenClawRuntimeEnsureInput): AcpxDelegate
} as AcpxDelegateEnsureInput;
}
function isAcpModelCapabilityMissingError(error: unknown): boolean {
return isRequestedModelUnsupportedError(error) && error.reason === "missing-capability";
}
// ACPX owns the distinction between missing model capability and an invalid model id.
// Retry only the former so explicit model mistakes remain visible to the caller.
async function ensureDelegateSessionWithModelFallback(
delegate: BaseAcpxRuntime,
input: OpenClawRuntimeEnsureInput,
): Promise<AcpRuntimeHandle> {
try {
return await delegate.ensureSession(withAcpxSessionOptions(input));
} catch (error) {
if (!input.model || !isAcpModelCapabilityMissingError(error)) {
throw error;
}
return await delegate.ensureSession(withAcpxSessionOptions({ ...input, model: undefined }));
}
}
function quoteShellArg(value: string): string {
if (/^[A-Za-z0-9_./:=@+-]+$/.test(value)) {
return value;
@@ -1010,7 +989,7 @@ export class AcpxRuntime implements AcpRuntime {
this.withCodexWrapperDiagnostics({
command: stableLaunchCommand,
fallbackCode: "ACP_SESSION_INIT_FAILED",
run: () => ensureDelegateSessionWithModelFallback(delegate, ensureInput),
run: () => delegate.ensureSession(withAcpxSessionOptions(ensureInput)),
}),
});
}

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/admin-http-rpc",
"version": "2026.6.10",
"version": "2026.6.9",
"private": true,
"description": "OpenClaw admin HTTP RPC endpoint",
"type": "module",

View File

@@ -1,6 +1,6 @@
{
"id": "alibaba",
"icon": "https://cdn.simpleicons.org/alibabacloud",
"icon": "https://cdn.simpleicons.org/alibabacloud/111111",
"activation": {
"onStartup": false
},

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/alibaba-provider",
"version": "2026.6.10",
"version": "2026.6.9",
"private": true,
"description": "OpenClaw Alibaba Model Studio video provider plugin",
"type": "module",

View File

@@ -1,12 +1,12 @@
{
"name": "@openclaw/amazon-bedrock-mantle-provider",
"version": "2026.6.10",
"version": "2026.6.9",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@openclaw/amazon-bedrock-mantle-provider",
"version": "2026.6.10",
"version": "2026.6.9",
"dependencies": {
"@anthropic-ai/sdk": "0.100.1",
"@aws/bedrock-token-generator": "1.1.0"

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/amazon-bedrock-mantle-provider",
"version": "2026.6.10",
"version": "2026.6.9",
"description": "OpenClaw Amazon Bedrock Mantle provider plugin for OpenAI-compatible model routing.",
"repository": {
"type": "git",
@@ -24,10 +24,10 @@
"minHostVersion": ">=2026.5.12-beta.1"
},
"compat": {
"pluginApi": ">=2026.6.10"
"pluginApi": ">=2026.6.9"
},
"build": {
"openclawVersion": "2026.6.10",
"openclawVersion": "2026.6.9",
"bundledDist": false
},
"release": {

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