runConfigUnset scoped its missing-path error to --dry-run, so the real
invocation printed "No change" and exited 0 while --dry-run on the same
input exited 1 with a specific message. Dry-run exists to predict the
real run, and a typo'd unset in a script reported success.
Restore the failure for both modes, keeping the strict-config assertion
introduced alongside the regression and the JSON5 byte preservation it
was added to guarantee.
`openclaw approvals allowlist add|remove` printed "Writing local approvals."
from the shared target-resolution helper, before the mutation decision was
made. Both idempotent paths ("Already allowlisted.", "Pattern not found.")
returned without saving, so the CLI announced a write that never happened.
`approvals set` had the same problem: it announced the write and then rejected
unparseable input.
Move the announcement from `loadWritableSnapshotTarget` into the local branch
of `saveSnapshotTargeted`, the function that owns the write. Every caller of
the shared seam is fixed at once and exit codes are unchanged: idempotent add
and remove still leave the requested end state satisfied and exit 0.
* fix(daemon): preserve unknown service load state
* test(daemon): update service load-state fixtures
* fix(onboard): preserve loaded service diagnostic
* fix(daemon): preserve unknown lifecycle state
* fix(status): preserve loaded JSON fields
* fix(daemon): fail restart on unknown service state
* fix(daemon): fence unknown service state
* fix(agents): finalize guided creation safely
Run channel post-write hooks only after config publication, defer portable auth copying until agent creation succeeds without overwriting newer credentials, and provision existing workspaces before publishing updates.
Keep JSON-only guided creation interactive while routing wizard output to stderr so stdout remains one machine-readable summary.
* fix(terminal): preserve note call signature
* fix(agents): pass committed config to setup hooks
* ci: split heavy codex changed-test shards
Cap non-isolated Codex extension processes at 20 files so 4-vCPU changed-target jobs do not starve real-time watches or hit the no-output watchdog.
* test(ci): align codex shard cap fixture
* docs(cli): clarify agents add JSON mode
* fix(config): secure CLI reads and mutations
Use exact plugin metadata for config get redaction, reject impossible SecretRef provider bindings in canonical validation, preserve include CAS provenance, and skip destructive authored no-op rewrites.
* fix(config): preserve inactive SecretRef startup
Keep normal Gateway validation scoped to active secret owners while strict config validation and write paths reject impossible provider/source bindings, including authored no-ops.
* fix(gateway): preserve config patch no-op
Keep unchanged Gateway config.patch requests on the existing no-op path; strict validation remains owned by actual config writes and the config CLI mutation surfaces.
Use an off-main-thread watchdog for the process-owning Gateway runner so a CPU-bound shutdown cannot outlive the managed restart deadline. Bound agent-harness disposal so listener teardown can continue.
Release note: Managed Gateway restarts now recover when shutdown work stalls the event loop while preserving graceful cleanup first.
Closes#125840
* feat(gateway): reach a Gateway behind an identity-aware proxy from the CLI
Operator CLI surfaces (tui, attach, call, probe, onboarding, and configure) can present configured gateway.remote.edgeAuth headers to an identity-aware proxy. Headers are origin-bound, WSS-only, never follow redirects, cannot set transport-owned headers, and are redacted. Identity-proxy upgrade rejections are classified and remediated instead of being reported as an unreachable gateway.
* test(gateway): cover config-aware probe calls
Keep status probe expectations aligned with the resolved configuration forwarded for origin-bound edge-auth resolution.
* fix(gateway): preserve edge auth across wizard saves and enforce wss before secret resolution
Preserve gateway.remote.edgeAuth only when the configured Gateway scope is unchanged, and reject non-WSS targets before resolving any edge-auth SecretInput.
With no servers configured, mcp probe emitted only its header line and exited
0, while mcp list and mcp doctor both name the condition and the command that
fixes it. Short-circuit the human path with the same guidance and skip building
a runtime for zero servers; JSON output keeps its existing empty envelope.
The gateway-routed path already mapped terminal run status to an exit code,
but the local embedded path did not, so a failed turn exited 0 while its own
JSON envelope reported status "error". Route both through the canonical
agent-run terminal outcome, and fail closed on an unrecognized status since
the gateway response carries an open string.
* fix(cli): render gateway transport failures as expected conditions
An unreachable gateway is an expected, recoverable operator condition, but
only the devices command family surfaced it through the root failure handler,
where it rendered as "The CLI command failed" with debug hints. Extract
GatewayTransportError into its own module so the failure renderer can classify
it without importing the transport stack, and treat it as an expected CLI
error alongside missing credentials.
* fix(gateway): import the transport error kind used by the JSON shape
* test(cli): extract the process-test gateway harness
The new devices coverage pushed gateway-backed-exit.process.test.ts past the
max-lines budget, and that file is not in the ratchet baseline. Move the mock
Gateway servers into a sibling test-helpers module instead of suppressing the
rule.
* fix(cli): keep gateway diagnostics read-only
Direct gateway calls and diagnostic probes now preserve shared SQLite ownership instead of creating or mutating state during read-only operations.
Related: #101290
Follow-up to #125102
* fix(cli): keep required status probes read-only
* fix(cli): keep gateway controls off plugin state
Use core-only config validation for gateway call and restart so schema-ahead recovery can reach the running Gateway without opening newer persisted plugin metadata.\n\nFixes #125115
* test(infra): isolate browser open SSH environment
* test(cli): cover gateway call validation policy
update-cli.test.ts used a fixed /tmp/openclaw-update-tests root with
deterministic case-dir names; some cases write real fixture files there
and cleanup rm -rf's them, so concurrent runs on one machine (CI shards,
sibling checkouts) deleted each other's live fixtures — observed as 34
spurious failures on PR #124997 run 31992421445. migrate.test.ts had the
same shape via a fixed mocked resolveStateDir root rm -rf'd in
beforeEach/afterEach.
Both suites now use per-run mkdtemp roots, realpath'd for the macOS
/var -> /private/var symlink, removed on suite exit.
* fix(update): keep a committed post-core result when stopping the child signals
The post-core wait settles from three places: a result-file poll, child "error",
and child "exit". The poll stopped the child before claiming the settle, and
stopping delivers a signal, so the still-unclaimed exit handler could reject with
"post-update process terminated by signal SIGTERM" for an update whose child had
already written a valid result. The outer catch then restored the tentative
plugin index, rolling back work that actually succeeded.
Claim the settle first, then stop. The exit handler sees settled and returns.
Net zero production lines; pure reordering plus the invariant comment.
This is the error that leaked across tests in the agentic-cli shard on
2026-08-17 (run 31997770370), where it surfaced inside an unrelated test and
cascaded into 34 failures in src/cli/update-cli.test.ts.
Regression test drives the exact ordering: the child commits its result, then the
parent's stop delivers SIGTERM. It fails on pre-fix code with
"promise rejected ... instead of resolving / post-update process terminated by
signal SIGTERM".
* test(update): satisfy strict indexed access in the post-core signal regression