* test(control-ui): add --operator-scopes flag to the mock dev server
* feat(control-ui): simplified settings experience for non-admin operators
Non-admin browsers previously saw every settings page, many of which
dead-ended or rendered enabled controls whose RPCs fail with
'missing scope: operator.admin'.
- config.schema drops from operator.admin to operator.read: the schema is a
static document describing options whose values are already readable via
read-scoped config.get; admin-only schema only broke read-only settings
rendering (Automation/Infrastructure/AI Agents/Communications showed
'Schema unavailable. Use Raw.').
- Settings sidebar and settings search hide admin-only routes (custodian,
labs, updates, automation, infrastructure, mcp, security, secrets,
cloud-workers, communications, ai-agents, model-setup) for non-admin
viewers; legacy gateways without advertised scopes keep the full UI.
- Channels, Devices, Worktrees, Memory Import, Profile gate their mutation
controls on actual scopes with 'Browsing only…' notices instead of
enabled-but-failing buttons; Devices no longer fires device.pair.list /
exec.approvals.get without the scopes to call them (kills the two red
error callouts on page load).
- Scope-upgrade banner: dismissing it in the guidance phase (no in-app
upgrade path) now hides it fully instead of leaving a permanent chip.
- Config write coordinator surfaces scope refusals as a visible
admin-required error instead of silently resolving false.
* test(control-ui): advertise config.schema in the mock dev gateway
ensureSchemaLoaded now checks method advertisement + scope before loading
the schema; the mock harness must advertise config.schema like a real
gateway does or schema-driven settings pages render empty in the mock.
* fix(control-ui): close the worktree create draft on scope downgrade
* perf(doctor): isolate memory health artifact
Doctor lint loaded the broad Memory Core API barrel only to register health checks and read isolated check IDs. That synchronously pulled the full memory public graph into the first lint run, consuming most of the 120-second test budget.
Load a dedicated doctor-health public artifact instead and verify it is packaged. The bisect boundary was 9de3ca5fc9 (#125571); because that commit only adds upgrade-test assets, it exposed a pre-existing runner-sensitive cost rather than introducing the expensive import path.
* test(control-ui): restore device lifecycle test boundary
* perf(control-ui): lazy-load settings sidebar
* fix(ui): recheck access after confirmations
* fix(control-ui): gate presence-driven device reloads on pairing access
The presence connectivity-change path still called device.pair.list without
operator.pairing, the same invariant the pair-event and poller paths already
guard; a limited browser got a doomed RPC on every connectivity change.
* fix(control-ui): fail open on schema loads for legacy scope-less gateways
canCallGatewayMethod hardened to strict advertisement+scope checks (#125478),
which made the new ensureSchemaLoaded gate silently skip config.schema for
legacy hellos without advertised scopes or a method list. Schema loads now
skip only on a definitive denial (method advertised absent, or advertised
scopes without operator.read), reusing the fail-open hasOperatorReadAccess
semantics the rest of the non-admin UI uses; regression test pins the
legacy snapshot path.
* test(control-ui): split schema-access coverage into its own file
runtime-config-capability.test.ts crossed the max-lines cap; the legacy
fail-open regression and its denial counterpart move to a colocated
schema-access test file.
* fix(scripts): keep mapped Vitest lanes at their measured no-output floor
The codex extension shard legitimately works in silence beyond 300s under
the default reporter (measured 61s import + 293s testing at ~95% CPU); the
CI-wide OPENCLAW_VITEST_NO_OUTPUT_TIMEOUT_MS=300000 env override shrank the
lane below that and the watchdog killed healthy runs, flipping with
incidental flake output (#125825). Per-config entries in
VITEST_CONFIG_NO_OUTPUT_TIMEOUT_MS now act as measured silence floors: a
global env value may widen a mapped lane's window but no longer shrinks it;
unmapped configs and the explicit '0' disable keep env verbatim. Adds the
codex extension lane to the map at the extra-long tier (same class as the
discord entry from #123025).
* fix(ci): stop codex lane cold-graph hangs
The side-question domain-policy test loaded the complete agent-harness tool graph inside a one-second readiness race, making the serial non-isolated Codex shard fail or stay silent under cold imports. Build the test's web_search marker and real web_fetch tool from the narrow implementation, then synchronize on turn startup before issuing the tool call. Cap each Codex test process at 12 files so CI gets bounded time-to-first-output as defense in depth.\n\nRefs #125839
* fix(test): keep codex web fetch fixture on sdk boundary
Load the real web_fetch factory on demand through the existing local-only plugin test runtime. This preserves the narrow cold-graph fix without letting a bundled plugin test reach into core internals.
* 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
The lock auto-releases only while the pre-side-effect validation marker is active; review_init marked side effects before the read-only metadata fetch, so a transient GitHub failure retained the lock and forced a lock-recover loop. The read-only fetch now runs before the marker.
gh exit status was treated as proof that stdout held a PR object. The Octopool cache shim reports upstream 5xx responses as exit 0 with empty stdout, so reads now use a bounded-retry validation helper. Convert 9 of 28 gh pr view sites where an empty payload could produce a wrong decision.
One-time maintainer-authorized bootstrap landing for the exact reviewed head. This direct merge replaces the broken self-hosted verifier so subsequent pull requests can return to the native review, prepare, and merge workflow.
Preserve attempt-local recovery state so successful mutation retries emit a redacted terminal receipt without stale failure warnings.
Co-authored-by: Ayaan Zaidi <hi@obviy.us>
* fix(node-host): recover MCP catalogs and sessions
Share placement-neutral MCP lifecycle and result projection while keeping Gateway session ownership and node process ownership separate. Refresh node catalogs live, recover closed or expired transports without replay, and preserve MCP application errors across node.invoke.\n\nFixes #125044
* test: register node MCP CI inventory
* fix(node-host): stop MCP recovery after abort
* fix(gateway): bind auth limits to ingress attribution
* fix(gateway): close remaining ingress auth gaps
* fix(gateway): carry attribution into new ingress paths
* fix(gateway): close ingress ownership gaps
* fix(gateway): complete proxy ingress hardening
* fix(gateway): stabilize managed Tailscale ingress
* fix(gateway): make Tailscale cleanup ownership-safe
Refuse reset-on-exit publication until Tailscale exposes an atomic owner-bound cleanup operation, and migrate legacy configs with Doctor.
* fix(gateway): finish ingress ownership repair
* fix(gateway): own managed Tailscale route lifetime
Run managed Serve and Funnel routes as foreground claims tied to the Gateway lifecycle. Retire named Service config through Doctor because Tailscale Services cannot run in foreground mode.
Co-authored-by: Pavan Kumar Gondhi <pavangondhi@gmail.com>
* fix(gateway): align Tailscale consumers and build guards
Remove the retired named-service config from Telegram Mini App URL resolution and register the lifecycle worker as an explicit production entry.
Co-authored-by: Pavan Kumar Gondhi <pavangondhi@gmail.com>
* fix(gateway): preserve retired Tailscale inputs
Keep Funnel enabled when removing an ignored named-Service setting and accept the legacy positive reset flag as a no-op now that managed routes always follow Gateway lifetime.
Co-authored-by: Pavan Kumar Gondhi <pavangondhi@gmail.com>
* fix(gateway): preserve Tailscale route diagnostics
Prefer the actionable foreground CLI failure captured during timeout cleanup, and cover the original delayed-failure ordering.
Co-authored-by: Pavan Kumar Gondhi <pavangondhi@gmail.com>
* fix(gateway): reconcile Tailscale ingress with main
Preserve current ingress ownership contracts after the rebase, retire the obsolete device-auth migration check, validate route-owner IPC, and move Tailscale auth coverage onto the managed listener.
Co-authored-by: Pavan Kumar Gondhi <pavangondhi@gmail.com>
* fix(gateway): finish ingress rebase coverage
Unify the rebased net imports and let module-reset WebSocket tests prepare attribution through the same fresh module instance as the handler.
Co-authored-by: Pavan Kumar Gondhi <pavangondhi@gmail.com>
* test(gateway): align run-loop server fixture
---------
Co-authored-by: joshavant <830519+joshavant@users.noreply.github.com>
Blacksmith's image tracks an older runner-images snapshot. Measured on a leased
box 2026-08-16, its toolcache holds Node 20.20.0, 22.22.0 and 24.13.0 while this
repo's engines floor is >=22.22.3 and >=24.15.0 -- short by three and two
patches. Every candidate is rejected, so all 306 of 306 sampled jobs fell
through to a nodejs.org download. GitHub-hosted runners carry 24.19.0 and
resolve from the toolcache in about a second, which is why only Blacksmith pays.
Normally that download is 2.6s (p99 3.3s), but ~46 jobs fetch the same 50 MB
simultaneously and the mirror throttles: three of 53 sampled runs had setup-node
medians of 44-93s with maxes to 139s, and because every job pays at once it
lands whole on the wall -- those runs went ~210s to 325s.
Keep the payload in the Actions cache, which Blacksmith serves from its
colocated backend. Measured on Blacksmith: cold 1605ms, warm 77ms.
Restores are prefix-keyed and the save carries the resolved patch. An exact key
would be worse than nothing: cache entries are immutable and an exact hit
suppresses the post-job save, so a floating `24.x` key would pin the first Node
it ever saw and, once the floor advanced past it, every job would restore the
rejected payload and re-download forever. Keying the save on the installed
version lets a newer resolve publish a new entry that later prefix restores
pick up.
A rejected payload is pruned before the replacement installs, because the entry
is saved wholesale and a leftover would ride along in every future save.
Windows keeps its existing path. Proven on Blacksmith across cold, warm, stale
and truncated-binary cases; both guards are mutation-checked.
This stays useful even if Blacksmith refreshes their image: the floor moves
independently of the snapshot, so the gap recurs. The image refresh is still
the better fix and is worth asking them for.