Commit Graph

2092 Commits

Author SHA1 Message Date
Peter Steinberger 6e71e9b156 feat(gateway): expose public worker ingress (#122578)
* feat(gateway): expose public worker ingress

* docs(plan): link public worker ingress PR
2026-08-12 07:20:02 -07:00
Peter Steinberger 85230cf76c test(secrets): cover Code Mode exec store env and document the harness boundary (#122405)
* test(secrets): cover code-mode nested exec store env; document harness boundary

* docs(secrets): warn that store env does not reach external agent harnesses
2026-08-12 07:06:40 -07:00
Peter Steinberger df707a9670 feat: view this machine in the Desktop panel (#122545)
* feat(gateway): add gateway-host desktop source behind desktop.host lab

Introduce the host as a first-class desktop source so operators can view
the machine OpenClaw runs on, not just cloud-worker environments:

- protocol: desktop.observe / desktop.launch with a discriminated
  DesktopSource union (host | environment) plus an additive auth hint;
  EnvironmentSummary gains a top-level desktop flag
- config: desktop.host { enabled, port?, passwordFile? }, Labs-gated
- rfb-probe: pure RFB version/security-type parser used to detect an
  already-running loopback VNC server and classify its auth
- host-source: attaches to 127.0.0.1:<port>, refuses unauthenticated
  (None) and unsupported (VeNCrypt) servers, and refuses ARD with the
  supported alternative until the macOS milestone
- host-guidance: per-OS enablement text so no path dead-ends
- doctor + status report host desktop availability and auth type only

worker.desktop.observe/launch stay as delegating aliases with identical
behavior. Also drops the now-unused WorkerDesktopTunnels type export.

Live-verified against macOS Screen Sharing: probe reads RFB 003.889,
returns security types [30,33,36,35], classifies ard-account.

* test(gateway): probe RFB handshakes through the socket boundary

The probe's banner and security-offer parsers were exported solely so
unit tests could call them, which the dead-export gate rejects and which
tests internals rather than behavior. Keep them module-local and drive
the probe through a scripted loopback server instead.

The boundary tests also cover what pure-function vectors could not:
handshakes split across packets, legacy RFB 3.3 single-word security,
server-rejected handshakes, early hangups, and connect timeouts.

* feat(ui): let the Desktop panel view this machine, not just cloud workers

The Desktop panel was gated on a cloud-worker session placement, so an
operator running OpenClaw locally had no way to see the machine hosting
their main session even with a VNC server running on it.

Availability now follows the advertised desktop.observe method plus
operator.admin instead of session placement, and the picker lists every
environment whose summary reports a desktop, with the gateway row shown
as "This machine". Sources are passed to the generic desktop.observe /
desktop.launch RPCs; the app launcher stays worker-only. When a host
attach needs a password the gateway did not supply, the panel prompts and
keeps the value in memory for that connection only.

Adds the hostDesktop Labs toggle for desktop.host.enabled.

* fix(scripts): keep the env-var ratchet usable in shallow checkouts

The env-var budget check resolved its base ref, then hard-failed when
`git merge-base` found no shared ancestor. Shallow clones and grafted
agent checkouts resolve origin/main but truncate the history behind it,
so an advisory growth ratchet took down the whole check:changed gate
with "Could not resolve env-var count merge base for: origin/main".

Only the growth comparison needs a baseline, and the script already has
a no-baseline path. Treat git's exit 1 with empty output (no shared
ancestor) as that case and say so on stderr; a genuine failure still
exits 128 and still throws, and the absolute count-vs-budget check runs
either way.

* test(ui): measure the inline-code chip against its line box

The inline-code spacing test compared the chip's height to a prose text
rect, so it silently measured the monospace font's default line spacing.
That is ~17px on macOS and several px shorter on Linux, so the assertion
passed on CI and failed locally at 4.5 against a 3.75 bound -- after the
bound had already been widened once to chase browser font metrics.

Compare the chip to the paragraph's CSS line box instead, which is what
"the chip must not disrupt the line" actually means and is platform
independent. The horizontal gap stays as-is: it is em-derived padding
plus border, and it is the assertion that catches detached punctuation.

Verified both directions on macOS: the file is fully green, and
restoring the pre-fix 0.15em/0.35em padding still fails the gap
assertion at 5.41.

* feat(gateway): view macOS Screen Sharing from the Desktop panel

Modern macOS only offers ARD account authentication for Screen Sharing,
so the host desktop source refused every Mac. The Gateway now performs
the ARD handshake itself against the loopback server and hands the
browser a plain RFB 003.008 no-auth handshake, so the operator's macOS
account password authenticates the desktop without ever reaching the
browser, the observe result, a URL, or a log.

- rfb-preauth: ARD (type 30) Diffie-Hellman with MD5-derived AES-128-ECB
  credentials, and VncAuth (type 2) bit-reversed DES, both under a single
  10s negotiation deadline; Apple's RFB 003.889 maps to 3.8
- observe-bridge: runs pre-auth before splicing and starts the view-only
  filter at clientInit, since the browser handshake is consumed here;
  worker tokens keep the original version start phase
- host-source: attaches ARD, requiring per-observation credentials that
  live only in the one-shot observer token and are dropped after use
- doctor: offers an explicitly confirmed sudo launchctl repair when
  Screen Sharing is off, and prints the System Settings path otherwise

Live-verified against this Mac's Screen Sharing: the DH exchange and
credential framing are accepted and the server returns SecurityResult.
The VncAuth DES vector is confirmed against OpenSSL independently.
2026-08-12 06:58:30 -07:00
Josh Avant 562391b9af feat(audit): explain denied operator approvals (#119815)
* Audit: add durable decision receipts

* Audit: route generic decision facts through writer

* Audit: satisfy strict decision count typing

* Test: align decision writer type coverage

* Refactor: isolate decision receipt projection

* Fix: preserve ambiguous approval correlation

* Fix decision coverage across pages

* Fix approval coverage across corrupt pages

* Fix decision summaries across retention and paging

* Remove superseded decision fact count path

* Keep session-derived approval links conservative

* Test decision paging at owner boundary

* Audit: bind approval receipts to exact execution

* Test: keep decision receipt coverage focused

* docs(agents): preserve decision receipt ownership

* Docs: refresh Plugin SDK split baselines

* Docs: require opt-in for future decision facts

* Fix: bound decision receipt inspection
2026-08-12 05:23:29 -05:00
Peter Steinberger d44f70eb4b feat(pairing): one-paste device pairing via oc-pair setup links (#120768)
* feat(pairing): one-paste device pairing via oc-pair setup links

Implements milestone 3 from docs/plan/runners.md.

* fix(pairing): sign bootstrap handshake, keep URL candidates, wire pairing countdown

* test(gateway): update client callsite guard

* fix(pairing): preserve setup URL context paths

* fix(ui): keep pairing help aligned with setup mode

* fix(pairing): isolate bootstrap credentials

* perf(ui): keep one-paste pairing within bundle budget

* refactor(pairing): isolate native pair URL prefix parsing

* fix(pairing): preserve candidate lifecycle state

* fix(pairing): retire shared credentials after bootstrap

* fix(pairing): apply rotated manifest through client owner

* test(pairing): prove bootstrap retirement across reconnect

* fix(pairing): preserve native gateway context paths

* fix(pairing): carry native context paths through reconnect

* fix(ios): preserve encoded gateway context path

* chore(plugin-sdk): refresh pairing API baselines
2026-08-12 02:01:33 -07:00
Josh Avant 3b01ea7905 fix(audit): show authenticated users for Gateway runs (#122484)
* fix(audit): show authenticated users for Gateway runs

* fix(audit): keep profile labels out of sessions

* test(qa): verify session label retention at storage

* fix(sessions): preserve canonical profile ownership

* test(qa): require full identity inspection proof

* docs(agents): preserve execution identity ownership boundary
2026-08-12 08:50:37 +00:00
Peter Steinberger 8190c326ce feat(gateway): add /startupz startup probe and fix deployment template admission (#122477)
* feat(gateway): add /startupz startup probe with auth-gated version detail

Startup/traffic-admission probe that excludes downstream channel health:
200 started once startup work completes and the gateway is not draining,
503 starting/draining otherwise. Version and uptime are only included for
local-direct or authenticated callers, reusing the /readyz detail gate.

* fix(deploy): use /startupz for traffic admission in bundled templates

fly.toml gains its missing HTTP check; render.yaml stops using pure
liveness as admission; k8s pins an immutable image tag, seeds config
only when missing, and adds a startupProbe; stale Fly healthcheck-port
doc corrected (healthcheck follows the active gateway lock port since
bc4221a07e).

* docs(k8s): make persistent-file config ownership explicit with reseed path
2026-08-11 23:36:33 -07:00
Peter Steinberger 61ab6a8f9d feat(gateway): make suspend/resume operator-usable end to end (#122100)
* feat(gateway): make suspend/resume operator-usable end to end

A prepared Gateway now accepts authenticated WebSocket connects while
keeping every method except gateway.suspend.* fenced, so a fresh CLI or
controller process can resume a suspension instead of dead-ending on a
rejected upgrade until the two-minute lease expires. Restart drain,
worker ingress, and desktop-observe streams stay fully closed.

The gateway client surfaces non-101 upgrade responses (bounded body
read) as typed retryable errors instead of an opaque 1006 close, and
new openclaw gateway suspend / resume commands drive the whole
handshake, including bounded --wait polling with blocker output.

Live-verified on an isolated dev gateway: prepare, SIGSTOP/SIGCONT
freeze, resume, over-TTL expiry self-heal, conflict and mismatch paths.

* refactor(gateway-client): move wire-client contract types to protocol-client-contract

The connectError addition pushed protocol-client.ts over the 700-line
max-lines gate; split the adapter-facing contract types into their own
module instead of suppressing.

* refactor(gateway-client): keep contract-internal option types unexported

Knip deadcode gates reject exported types with no importer; the connect
and close decision shapes are only referenced inside the contract module.

* chore(plugin-sdk): refresh gateway-runtime API baseline after rebase

* fix(gateway-client): preserve hello type after rebase

* test(gateway): support websocket upgrade rejection events

* test(gateway): expect connection errors in close info

* fix(gateway): keep prepared-suspension connects control-only

Address ClawSweeper review: node and worker connects stay refused while
suspension is prepared (only operator control connects pass), and the
CLI never issues another suspend prepare after its --wait deadline.
2026-08-11 22:12:53 -07:00
Sarah Fortune 00fc1bd123 fix(slack): scope enterprise channel and user policies by workspace (#122346)
* fix(slack): scope channel policies by workspace

* fix(slack): scope user policies by workspace

* fix(slack): preserve workspace policies at ingress

* test(slack): use canonical workspace ids

* chore(slack): remove stale allowlist import

* fix(slack): require workspace-scoped grid policies

* test(slack): scope enterprise policy fixtures

* style(slack): format workspace policy changes

* fix(slack): preserve workspace policy wildcards

* fix(slack): retain workspace system-event policies

* fix(slack): scope bot policy identities

* fix(slack): retain workspace in group policy

* fix(slack): preserve workspace DM allowlists

---------

Co-authored-by: Sarah Fortune <sarah.fortune@gmail.com>
2026-08-11 22:09:46 -07:00
Peter Steinberger ae2158c0da fix(cloud-workers): start source bundles with vendored packages (#122400)
* test(qa): prove cloud worker mid-turn loss

* fix(cloud-workers): prune vendored workspace dependencies

* test(qa): keep SSH fixture type private
2026-08-11 20:32:54 -07:00
Peter Steinberger db9bdb4a11 fix(gateway): preserve session identity through archive transitions (#121169)
* fix(sessions): fence lifecycle mutations by identity

* fix(sessions): finalize lifecycle identity fences
2026-08-11 16:43:10 -07:00
Ayaan Zaidi 6a4a546593 feat(mcp): per-requester OAuth for MCP servers (#122166)
Per-requester OAuth for HTTP MCP servers: in shared channels each trusted sender connects their own account. New config: mcp.servers.<name>.oauth.identity ("shared" default, byte-identical behavior) and gateway.publicOrigin (HTTPS except loopback) for the new GET /oauth/mcp/callback served on the Gateway HTTP server. Requester tokens are isolated per (channel, account, sender) in mcp_oauth_stores rows (no schema bump); callbacks correlate through a durable state-keyed one-time index with a 10-minute TTL; per-requester servers are fail-closed out of static/scheduled runtimes; unauthenticated senders get a sign-in link with a portable URL button; MCP config mutations clear requester credentials at the canonical owner. mcp status --json keeps the legacy authStatus fields additively. Proven by a two-user self-hosted-Executor E2E through the real gateway callback. Part of #122034.

Co-authored-by: Ayaan Zaidi <hi@obviy.us>
2026-08-11 20:58:51 +00:00
Peter Steinberger a1846dbebc feat(ui): session-reset dividers + boundary markers for DB-backed sessions (#122222)
* feat(ui): show session-reset dividers and fix boundary markers on DB-backed sessions

/reset now leaves a durable 'Session reset' divider at the transcript
boundary in the Control UI. Root-cause fix underneath: the SQLite
transcript projection only selected message events, so compaction (and
now reset) markers never reached clients for DB-backed sessions; marker
synthesis now has one owner (session-transcript-message.ts) consumed by
both storage backends across full/recent/paged/by-id/anchor reads.
Additive __openclaw marker kind 'reset' documented in clients.md.

* fix(gateway): keep history readers out of the plugin SDK barrel and fix CI gates

Direct imports for the sqlite history readers (the session-accessor barrel
is SDK-reachable via session-transcript-lock-runtime); reset marker added
to the kept-tail chat.history expectation; lint naming fixes; marker tests
split into session-transcript-readers.markers.test.ts.
2026-08-11 12:59:11 -07:00
WhatsSkiLL d9766c3d57 fix(wizard): honor remote password auth in probes (#118134)
* fix(wizard): honor remote password auth in probes

* fix(wizard): preserve remote probe auth policy

* test(wizard): keep remote probe coverage focused

* fix(gateway): preserve remote probe secret refs

* fix(gateway): isolate remote probe credentials

* fix(wizard): authenticate remote health checks

* fix(gateway): unify remote probe auth precedence

* test(gateway): align remote probe auth coverage

* fix(gateway): preserve disabled exec-ref guard

* fix(gateway): block failed remote ref fallback

* test(gateway): follow consolidated probe coverage

* fix(wizard): stop health after unresolved remote auth

* fix(gateway): preserve configured sibling probe auth

* fix(wizard): bind remote health target

* docs(gateway): clarify remote probe auth precedence

---------

Co-authored-by: IWhatsskill <284122573+IWhatsskill@users.noreply.github.com>
Co-authored-by: Dallin Romney <dallinromney@gmail.com>
2026-08-12 00:46:25 +07:00
Peter Steinberger 1dcac5b15f fix: open terminals no longer delay gateway updates (#121601)
* fix(gateway): allow updates with open terminals

* fix(updater): support terminal policy upgrades

* chore(plugin-sdk): refresh API baseline
2026-08-11 09:37:58 -07:00
Ayaan Zaidi 2c8ed54ddb feat(heartbeat): default delivery to the configured owner, never groups (#121988)
Unset heartbeat.target now resolves "owner": elected heartbeat notifications deliver to the operator's DM resolved from commands.ownerAllowFrom or the channel allowFrom (first concrete entry; wildcards and channel-scoped wildcards excluded; configured owners exhausted across channels before any channel-local fallback). Delivery requires the channel's own classifier to positively prove a direct destination — every bundled messaging plugin now ships an inferTargetChatType contract — and unproven or group-shaped destinations fail closed to the visible no-route state. The first implicitly-routed delivery carries a one-line self-explanation naming the target: "none" opt-out. Explicit target "last" remains as the follow-the-conversation opt-in. Refines the unreleased #121892 default before it ships; refs #121880.

Co-authored-by: Ayaan Zaidi <hi@obviy.us>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-11 13:18:22 +00:00
Peter Steinberger bcaec0cf14 fix: resume main sessions after gateway restarts (#121969)
* fix(agents): always resume main sessions after restart

* fix(ui): render internal messages as system rows

* docs: update gateway restart recovery semantics

* refactor(sessions): simplify restart hook checkpoints

* refactor(agents): consolidate restart recovery claims

* test(agents): update delivery recovery revision guard

* fix(plugin-sdk): preserve restart hook state contract

* fix(ui): localize system row label

* test(agents): remove obsolete recovery import

* test(agents): align abort-code propagation with always-resume recovery

* fix(ui): share notice-aware turn boundaries

* test(agents): reconcile restart recovery ownership

* fix(ci): reconcile rebased recovery and UI checks
2026-08-11 04:01:34 -07:00
Peter Steinberger b067a4dce3 fix(secrets): reject empty secret values and classify oversized input as validation (#121947) 2026-08-11 02:30:04 -07:00
Ayaan Zaidi ba9e72c17f fix(heartbeat): deliver to the last conversation by default (#121892)
Unset heartbeat.target silently collapsed into the explicit "none" opt-out: heartbeats ran every 30m by default, elected notifications were dropped with only an in-memory event, and health read fine. Unset now resolves to "last" (the most recent conversation); explicit target: "none" keeps its internal-only contract. Polls skip pre-model with reason no-route while no route exists yet, and status/doctor surface the waiting-for-route state. Deliberate maintainer-owned default cutover: existing installs without a configured target start receiving heartbeat alerts in their last conversation after upgrade.

Fixes #121880

Co-authored-by: Ayaan Zaidi <hi@obviy.us>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-11 08:45:30 +00:00
Peter Steinberger ea06d72e85 feat(secrets): manage team secrets in Control UI (#121724)
* feat(secrets): add gateway store settings

* perf(control-ui): trim secrets startup copy

* perf(control-ui): reduce secrets startup payload

* fix(secrets): harden store mutation refresh

* perf(control-ui): meet secrets startup budget

* test(control-ui): update secrets navigation copy

* fix(ui): pluralize secret-detection count and drop duplicated dialog hint

* chore(protocol): regenerate gateway clients and SDK baseline after rebase

* fix(gateway): merge secrets store methods after project RPCs in advertised order

* chore: leave changelog to release generation

* test(gateway): retain desktop launch train coverage
2026-08-11 07:20:38 +00:00
Peter Steinberger f788af0238 docs(memory): explain automatic QMD migration (#121911) 2026-08-10 23:57:34 -07:00
Peter Steinberger 768f53d345 fix(secrets): resolve default aliases by source (#121630) 2026-08-10 21:05:26 -07:00
Peter Steinberger b5d5ec340f feat(cloud-workers): add desktop apps and browser autonomy (#121475)
* feat(cloud-workers): add desktop apps and browser autonomy

provider-attested Browser/Terminal launchers, shared visible loopback CDP Browser tool, no MCP/cookies/generic command.

* feat(ui): add cloud desktop app launcher

* docs(gateway): document cloud desktop apps and browser autonomy

* perf(ui): trim desktop launcher startup copy

* refactor(ui): simplify desktop launch feedback

* perf(ui): reuse desktop app labels

* fix(ui): keep desktop launch failures actionable

* fix(crabbox): allow browser bootstrap to finish

* fix(cloud-workers): honor provider provision budgets

* fix(cloud-workers): persist browser screenshot receipts

* fix(cloud-workers): bound browser screenshot lifecycle

* fix(cloud-workers): avoid replaying desktop launches

* test(cloud-workers): isolate browser runtime integration

* refactor(cloud-workers): tighten desktop runtime boundaries

* test(cloud-workers): keep browser runtime mock synchronous

* fix(cloud-workers): break gateway type import cycle

* fix(ci): settle admitted setup sessions in tests

* build(plugin-sdk): refresh desktop app contract

* ci: refresh merge-tree validation

* build(plugin-sdk): regenerate desktop app baseline

* style(gateway): format merged method order test
2026-08-10 20:31:07 -07:00
Marvinthebored bd897a6143 fix(exec): make timeout unit explicit (#119874)
* fix: exec timeout field does not state its unit, so callers pass milliseconds

`exec.timeout` is in seconds, but its sibling `yieldMs` is in milliseconds and
the `process` tool's identically named `timeout` is also in milliseconds. Those
two tools are used together in one workflow: exec backgrounds a long command,
then process polls it.

The unit exists only in the field description, and code mode deliberately defers
descriptions - the model sees `timeout?: number` and nothing else. A caller that
guesses milliseconds passes 900000 intending 15 minutes and gets ~10 days, so
the command never times out.

Add `timeoutSeconds` as the canonical field, matching the convention already
used by the config default (`tools.exec.timeoutSeconds`) and the sibling
(`yieldMs`). Keep `timeout` as a deprecated alias; `timeoutSeconds` wins when
both are supplied. Project the new field onto `nodeExecSchema`, which
hand-picks its properties.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XQojYRCUdXhcKFeu4QW5Gh

* fix(ci): satisfy test-type and dependency checks

Two CI failures from the test approach, not the change itself.

- check-test-types: TypeBox's TOptional does not surface `description` on its
  static type, so the schema assertions failed under tsgo. Read it through a
  narrow accessor instead.
- check-dependencies: knip flagged resolveExecTimeoutSeconds as an unused
  export because only the test imported it. Move it beside the schema it
  resolves, where the exec runtime imports it, so it is production-reachable.

Verified with the same commands CI runs: `run-tsgo.mjs -p
test/tsconfig/tsconfig.core.test.json` reports no errors in these files, and
knip --production reports no unused exports.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XQojYRCUdXhcKFeu4QW5Gh

* fix(ci): fit the exec description budget and satisfy oxlint

- `agent-tools.schema.test.ts` enforces a 550-char budget on the exec tool's
  model-facing descriptions; the two new ones pushed it to 640. Shortened both.
  The rename is what makes this possible: the unit now lives in the field name,
  so the description no longer has to carry it. Extended that budget test to
  pin `timeoutSeconds` alongside `timeout`.
- oxlint `no-base-to-string`: the test's description accessor typed the field as
  `unknown` and stringified it. Typed as `string | undefined` instead.

Verified: budget test passes (137 tests green across both suites), and all six
behavior tests still fail against origin/main.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XQojYRCUdXhcKFeu4QW5Gh

* fix: remove the unit-ambiguous exec timeout field instead of deprecating it

Owner decision on the review's "ambiguous model-visible alias" finding: drop
`timeout` entirely rather than keep it as a deprecated alias.

Keeping it visible defeated the purpose. Code mode renders property names and
types and defers descriptions, so a model would still be offered a bare
`timeout` sitting next to a millisecond-based `yieldMs` and could still pick it.
A deprecation marker in a description the model never sees changes nothing.

Backward compatibility is not required here: every tool call is constructed
fresh by the model from the current schema, and a repository search found no
internal caller passing `timeout` to the exec tool. `timeoutSeconds` is now the
only exec timeout field, on both the main and node-only surfaces.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XQojYRCUdXhcKFeu4QW5Gh

* fix: migrate the internal exec callers to timeoutSeconds

Removing `timeout` from the exec schema broke three real internal callers that
the earlier survey missed, plus two test files and the runtime's own reads.
`check-test-types` caught it; my local search had filtered `timeout:` matches to
lines mentioning "exec" and discarded these.

- src/auto-reply/reply/bash-command.ts
- src/auto-reply/reply/commands-diagnostics.ts
- src/auto-reply/reply/commands-export-trajectory.ts
  all passed `timeout: timeoutSec` and now pass `timeoutSeconds: timeoutSec`.
- bash-tools.exec-run.ts reads the field through the same typeof narrowing the
  third call site already used; `params` is loosely typed, so a raw read is
  `unknown`.
- Test fixtures updated. A vitest `waitFor({ timeout })` option in
  background-abort.test.ts is NOT an exec param and was left alone.

`run-tsgo.mjs -p test/tsconfig/tsconfig.core.test.json` now reports zero errors
in changed files; the 5 remaining `rejectSymlinks` errors are pre-existing on
main. 148 tests pass across the affected suites.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XQojYRCUdXhcKFeu4QW5Gh

* docs: synchronize the exec config table with timeoutSeconds

Addresses the P2 finding. The tools.exec.timeoutSeconds row still described the
per-call override as `timeout` and `timeout: 0`, which no longer exist. The
only remaining bare `timeout` reference in this page is the deliberate
contrast with the process tool's millisecond field.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XQojYRCUdXhcKFeu4QW5Gh

* fix: migrate the QA lifecycle caller and the shared ExecToolArgs contract

Two findings from the re-review of 6e8c3560e7, both real.

[P1] test/e2e/qa-lab/runtime/openclaw-exec-process-lifecycle.e2e.test.ts:174
still passed `timeout: 0.05`. After the removal that field is accepted and
ignored, so the call silently used the default timeout and could not produce
the asserted 50 ms `overall-timeout` result. Migrated.

[P2] ExecToolArgs in bash-tools.exec-request-preparation.ts:33 still declared
`timeout?: number` while the runtime reads only `timeoutSeconds`, offering
request-preparation users an accepted-but-inert argument. Renamed.

Why the typecheck missed both: ExecToolArgs is
`Record<string, unknown> & {...}`, and that index signature defeats excess
property checking, so a stale member produces no error at any call site. The
e2e file also sits outside the core test tsconfig I had been running locally.

Left alone deliberately: `processTool.execute({ action: "poll", timeout })` is
milliseconds and correct, and `createBashTool` in sessions/tools/bash.ts is a
separate tool surface with its own timeout.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XQojYRCUdXhcKFeu4QW5Gh

* fix(test): update the node exec tool property assertion to timeoutSeconds

src/gateway/tool-resolution.exclude.test.ts:413 asserts the exact property list
of the node-forced exec tool and still expected "timeout". Renamed to match
nodeExecSchema.

Found by checks-node-compact-large-5; my local core-test tsconfig does not cover
src/gateway, so it did not surface there.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XQojYRCUdXhcKFeu4QW5Gh

* fix: accept the legacy exec timeout as an undocumented alias

Removing the field from the schema does not reject it at runtime: the exec
schema accepts unknown properties, so a call built against an older schema was
accepted and then silently ignored, taking the default deadline instead of the
one it asked for.

Route every read through resolveExecTimeoutSeconds, which prefers the canonical
timeoutSeconds and falls back to a numeric timeout. The alias stays out of
execSchema, nodeExecSchema and ExecToolArgs, so no new caller can adopt it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XQojYRCUdXhcKFeu4QW5Gh

* docs: update the background-process guide to timeoutSeconds

The exec parameter table and the inheritance note still taught `timeout` and
`timeout: 0`, which kept the retired spelling publicly discoverable and
contradicted the exec guide.

`poll`'s own millisecond `timeout` is left alone; it is a different tool and a
different unit, which is the collision this rename removes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XQojYRCUdXhcKFeu4QW5Gh

* fix(exec): reject removed timeout field

* chore: regenerate plugin SDK API baseline

---------

Co-authored-by: Marvinthebored <262704729+Marvinthebored@users.noreply.github.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Patrick Erichsen <patrick.a.erichsen@gmail.com>
2026-08-10 20:02:53 -07:00
Peter Steinberger fada067277 feat(browser): add zero-click Chrome extension bootstrap (#121586)
* feat(browser): add zero-click extension bootstrap

Pre-register deterministic path-derived extension IDs and install a strict native messaging host.

Keep the popup and options UI minimal while removing the obsolete copilot and page-share flows.

* fix(browser): satisfy native bootstrap CI guards

* test(browser): isolate native bootstrap Chrome roots

* test(browser): flush native bootstrap profile before status

* test(browser): seed Linux native bootstrap identity

* fix(browser): preserve native bootstrap upgrade safety

Allow immutable root-owned package inputs while keeping mutable state, manifests, and launchers user-owned. Preserve all retired copilot keys whenever active or unrecognized recovery custody remains.

* fix(browser): preserve pending copilot custody

Retired cleanup now removes copilot state only when the durable registry is exactly empty. Any session, archive, malformed value, future shape, or read failure preserves every retired key.

* fix(browser): guard native bootstrap upgrades

Fail closed while retired copilot custody remains and make discard durable across partial failures.

Require exact launcher-embedded origins and repair full launcher drift without accepting mismatched registrations.

* fix(browser): remove stale layout export

* chore(release): leave changelog to release flow
2026-08-10 19:31:13 -07:00
Peter Steinberger 52e3149ed7 feat(secrets): inject store env into agent exec (#121773) 2026-08-10 18:59:32 -07:00
Peter Steinberger 96b0cf95ef fix(gateway): restore restart preflight compatibility (#121757)
* fix(gateway): restore restart preflight compatibility

* ci(protocol): preserve restored method vintages

* chore: defer compatibility note to release
2026-08-10 16:43:27 -07:00
Peter Steinberger 59492c51a5 feat(agents): rename spawn_task tool to suggest_task (#121694)
* feat(agents): rename spawn_task tool to suggest_task

Models across vendors read the name spawn_task as 'starts background
work' and refuse to call it when the user has not asked to start
anything (Claude Sonnet 5, verbatim: 'spawn_task (which actively starts
background work)... Since you didn't ask me to start work, I'm flagging
this as a follow-up instead'). The tool only records a suggestion card,
so the name defeated the feature.

Rename the model-facing tool to suggest_task everywhere (catalog,
descriptors, presets, display config, OpenClawKit tool-display, docs)
and migrate persisted operator state via doctor: config tool policies
and cron SQLite toolsAllow entries that reference spawn_task are
rewritten to suggest_task. No runtime alias remains; runtime exposes
only the canonical name.

Live clean-room matrix on a dev gateway (virgin session per cell):
suggested-task-card phrasing now yields exactly one card on
anthropic/claude-sonnet-5, claude-haiku-4-5, openai/gpt-5.6-sol,
gpt-5.6-luna, and google/gemini-3.1-pro; pre-rename the same bait
produced prose-only follow-ups.

* chore(doctor): keep legacy tool-name predicate module-local

isLegacyTaskSuggestionToolName has no external consumers; the exported
form tripped the knip unused-export gate (deadcode:exports).

* fix(doctor): keep tool-name migration out of plugin-owned config

The rename migration traversed the whole raw config, so any object
carrying a toolsAllow key was rewritten - including opaque
plugins.entries.*.config, which core must not mutate. Scope the apply
pass to the typed core roots (tools, agents, channels, gateway) and pin
plugin config preservation in the test. Plugin-owned tool lists migrate
via the owning plugin's doctor contract instead.

* fix(ci): heal main deadcode and stale Swift protocol mirror

Unrelated main breakage blocking this PR's merge gate, healed here per
landing policy:

- #121653 left dead exports in src/skills/workshop: make
  removeSkillCollectionDirectory and SkillCollectionRestoreResult
  module-local; register collection-review.ts under the documented knip
  ignoreIssues test-only-export convention (production runs it via the
  scheduled maintenance loop).
- #121673 added ApprovalResolveParams.reviewer without regenerating the
  Swift mirror; regenerate GatewayModels.swift (additive only).

* fix(ci): satisfy generic approval-runtime request signature in resolver test

Third main-heal carried by this PR: #121673 (9935ca3b30) left the
scoped-request mock's concrete inferred type unassignable to the generic
GatewayNativeApprovalRuntime request signature, breaking check-test-types
on main. Cast the mock at the runtime literal; assertions keep the Mock.

* chore(ci): drop approval-test cast superseded by main's typed mock rewrite

* chore(ci): drop duplicate knip entry superseded by main's heal
2026-08-10 16:41:39 -07:00
Peter Steinberger 87475758dc fix(cloud-workers): retry timed-out dispatches without reprovisioning (#121616)
* fix(cloud-workers): coalesce dispatch retries

Amp-Thread-ID: https://ampcode.com/threads/T-019feaaa-c7ed-769e-9f29-a3612bec72e7

* chore: lower environment variable count budget

Amp-Thread-ID: https://ampcode.com/threads/T-019feaaa-c7ed-769e-9f29-a3612bec72e7

---------

Co-authored-by: Amp <amp@ampcode.com>
2026-08-10 09:17:17 -07:00
Peter Steinberger a0ad38e71a feat(docs-ci): validate docs config examples against the schema (#121336)
* feat(docs-ci): validate config examples against schema

* docs: teach canonical agent roster configuration

* docs: nest inline environment variables under vars

* docs: update canonical media model keys

* docs: remove retired config keys from examples

* fix(docs-ci): keep audit internals private

* feat(docs-ci): validate plugin-owned config examples

* fix(docs-ci): validate indented config fences

* fix(ci): route manifest changes through docs audit

* chore(docs): refresh generated config baselines
2026-08-10 07:57:56 -07:00
Peter Steinberger f4bac99a81 feat(secrets): add SQLite-backed secret store (#121559) 2026-08-10 07:08:40 -07:00
Peter Steinberger c6edd3e80f feat(gateway): grant scopes by verified identity (#121531)
* feat(gateway): grant scopes by verified identity

* chore(plugin-sdk): refresh API baseline

* test(agents): isolate transcript repair mocks
2026-08-10 03:19:45 -07:00
Peter Steinberger bab4546b41 refactor(gateway): remove dead rpc surfaces (#121387) 2026-08-10 02:41:57 -07:00
Peter Steinberger 7954dde7cc fix(cloud-workers): support large Git workspaces (#121262)
* fix(cloud-workers): support large git workspaces

* test(cloud-workers): track workspace temp dirs

* refactor(cloud-workers): keep inventory helpers internal
2026-08-10 00:56:48 -07:00
Peter Steinberger f6298bf84d fix(workers): preserve disappeared-worker failures across restarts (#121122)
* fix(workers): persist placement terminal failures

* fix(workers): refresh placement protocol clients

* refactor(workers): isolate error formatting

* fix: integrate cloud terminal state with current main

* chore(plugin-sdk): refresh API baseline

* refactor(ui): inline one-use cloud terminal-reason banner helper

Keeps the Control UI startup JS bundle inside its 317 KiB gzip budget
(the helper + type-only import tipped it by 16 bytes).

* refactor(ui): trim terminal-reason lookup to type-erased optional access

Recovers the last gzip byte of the Control UI startup budget
(324609 B vs the 324608 B limit).
2026-08-10 00:36:52 -07:00
Sarah Fortune a968d15f63 feat(slack): enable Enterprise Grid identity, DMs, routing, actions, approvals, and presence (#121373)
* refactor(slack): detect Enterprise Grid installs at runtime

* fix(slack): preserve enterprise bot identity

* style(slack): format enterprise identity test

* fix(slack): scope enterprise config by workspace

* fix(slack): preserve qualified binding casing

* feat(slack): enable enterprise presence polling

* fix(slack): normalize enterprise binding accounts

* fix(slack): require workspace-scoped Grid targets

* docs(slack): document Enterprise policy target forms

* fix(slack): preserve Grid pairing workspace scope

* refactor(slack): simplify scoped target formatting

* fix(slack): repair Enterprise Grid CI checks

* chore: refresh generated Slack metadata

* refactor(slack): simplify optional workspace scope

* test(slack): infer installation state handle types

* chore: resolve plugin SDK baseline conflict

* fix(ci): regenerate plugin SDK API baseline on Linux

* fix(slack): allow standalone workspace sends

---------

Co-authored-by: Sarah Fortune <sarah.fortune@gmail.com>
2026-08-10 07:10:06 +00:00
Peter Steinberger 8ee945b907 refactor(channels): flatten channel-turn dispatch naming layers (#121308)
* refactor(channels): flatten channel turn dispatch naming

* docs(plugin-sdk): narrow inbound reply compat guidance

* docs(channels): point stale references at turn defining modules

* fix(channels): preserve dispatch contracts after flattening

* chore(plugin-sdk): ratchet surface budgets after flattening

* chore(channels): ratchet removed export collisions

* fix(plugin-sdk): restore inbound reply compat exports

Restore eight still-existing legacy callable re-exports from canonical SDK seams and cover the deprecated package subpath with a table-driven compatibility test.

Raise the public export, callable export, and deprecated export budgets by exactly eight; the three maintainer-authorized zero-consumer symbols remain removed.

* test(channels): split channel turn kernel coverage

Replace the oversized kernel test with independently mocked delivery, pipeline, and finalize suites, preserving all 51 tests while removing the max-lines suppression and stale ratchet entry.

* chore(plugin-sdk): refresh inbound reply API hash

* fix(ci): align channel turn review fixes

Restore the test-local DeliveryResult type removed during the split.

Ratchet the public export, callable export, and deprecated export budgets by exactly seven: six channel-inbound plus one channel-outbound legacy re-export.
2026-08-09 22:22:46 -07:00
Peter Steinberger 8ab7ac54e2 docs: align prompt-overlay, truncation, and pruning docs with shipped behavior (#121324)
* fix(openai): describe personality as the canonical GPT-5 style toggle

The shipped config-schema description told operators and models to prefer
agents.defaults.promptOverlays.gpt5.personality, a retired key that config
validation rejects and doctor deletes. plugins.entries.openai.config.personality
is the only live reader (src/agents/gpt5-prompt-overlay.ts).

* docs: align prompt-overlay, truncation-notice, and pruning docs with shipped behavior

- teach plugins.entries.openai.config.personality as canonical; retired
  agents.defaults.promptOverlays noted as rejected/migrated
- replace nonexistent agents.defaults.bootstrapPromptTruncationWarning with
  prose describing the built-in always-on notice
- reword session-pruning internal constants as built-in behavior, name the
  real contextPruning config surface
- delete stale/orphan pages (path3 harness for a never-committed script,
  superseded swarms plan, landed path3 artifact-family scoping note)
- fix dead paths in reference/test.md and concepts/typebox.md

* docs: describe the embedded truncation notice as compact

The embedded runtime injects buildBootstrapPromptWarningNotice, which
deliberately omits file names and sizes; per-file diagnostics stay in
/context, status, and logs. Addresses ClawSweeper P2 on #121324.

* docs: doctor migrates the retired personality key instead of removing it

Main landed #121346 mid-flight: doctor --fix now moves
agents.defaults.promptOverlays.gpt5.personality into
plugins.entries.openai.config.personality when unset.
2026-08-09 21:41:43 -07:00
Peter Steinberger 914f73ac99 docs: replace retired config keys with canonical schema keys (#121330) 2026-08-09 20:30:43 -07:00
Peter Steinberger ada95a20c1 docs(concepts): add session synchronization and attachment guide (#121091)
* docs(concepts): add session synchronization and attachment guide

* docs: mark session attachment page schema

* docs: distinguish gateway and local TUI sessions
2026-08-09 18:06:18 -07:00
Peter Steinberger 42283cb609 fix: completed tasks no longer block gateway suspension (#121267)
* fix(gateway): close task terminals at run end

* refactor(agents): keep terminal task ownership internal

* test(gateway): type task-owned terminal fixtures

* test(infra): split agent run task ownership coverage
2026-08-09 15:45:44 -07:00
Peter Steinberger 0ef798d28d fix(gateway): keep hello authorization aligned with RPC access (#120888)
* fix(gateway): separate socket and device token scopes

* chore(i18n): refresh native source baseline

* style(ios): keep gateway channel within lint limit

* refactor(gateway): simplify scope metadata decoding

* ci(ui): isolate real-gateway e2e suites

* docs(ci): align runner table formatting

* chore(plugin-sdk): refresh api baseline

* ci(ui): route real-gateway retries to hosted runners

* chore(plugin-sdk): repair generated api baseline

* test(ui): select Labs toggles by title

* fix(gateway): preserve stored scopes without wire metadata
2026-08-09 14:50:07 -07:00
Sarah Fortune 66dee798b5 Revert "fix: enable Slack native approvals for Enterprise Grid installs (#120…" (#121219)
This reverts commit 96798953b2.
2026-08-09 19:49:35 +00:00
Peter Steinberger d3c84674e8 feat(gateway): task-suggestion acceptance modes with compact split-button card (#121173)
* feat(gateway): task-suggestion acceptance modes with compact split-button card

- taskSuggestions.accept gains additive mode (worktree default | local | cloud | session) + cloudProfileId; operator.admin unchanged; worktree requests stay {taskId}-only for older gateways
- local reuses the plain-cwd create; cloud mirrors the Control UI create-empty -> sessions.dispatch -> send sequence with pre-claim profile validation and full rollback; session delivers the prompt into the source session (steer single active run, plain turn when idle, ambiguous refused) and can only cancel the chip on failure, never touch the source session
- Control UI card redesigned per owner reference: repo-basename eyebrow, clamped summary, instructions expander, split button with per-profile cloud items or a disabled "No cloud environment configured" hint
- TUI gains the same modes as selector rows with capability gating and double-Enter arming

Release-note context: operators now choose where an accepted follow-up runs — isolated worktree (default), the project checkout, a configured cloud worker, or as the next instruction in the current session — from a compact suggestion card.

* fix(gateway): negotiate task-suggestion acceptance modes and enforce steerable-run invariant

- Gate non-worktree task-suggestion actions on the negotiated Gateway capability.
- Require exactly one steerable run ID before delivering into an active source session.
2026-08-09 12:21:21 -07:00
Sarah Fortune 96798953b2 fix: enable Slack native approvals for Enterprise Grid installs (#120874)
* fix(slack): scope Grid native approvals by team

* fix(slack): type action middleware context

* fix(slack): secure Grid native approvals

Co-authored-by: Sarah Fortune <sarah.fortune@gmail.com>

---------

Co-authored-by: Sarah Fortune <sarah.fortune@gmail.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-08-09 12:09:59 -07:00
Peter Steinberger 05cd389cac fix(cloud-workers): make Crabbox provisioning idempotent (#121019)
* fix(cloud-workers): make Crabbox provisioning idempotent

* fix(crabbox): preserve desktop fixed-id warmups
2026-08-09 11:51:34 -07:00
Peter Steinberger 8fdf7570a1 feat(gateway): live Desktop observer for cloud workers (Labs) (#120727)
* feat(gateway): live desktop observer for cloud workers

Adds live observation for cloud worker desktops through the gateway and Crabbox plugin, including desktop provisioning, persisted desktop metadata, tunneled WebSocket proxying, and the worker.desktop.observe protocol method.

The gateway, Crabbox plugin, and gateway protocol surfaces remain off by default behind the cloudWorkers.desktop Labs flag.

* feat(ui): Desktop panel for cloud worker observation

* docs(gateway): document cloud worker desktop lab

* fix(ci): regenerate contract baselines after rebase

* fix(protocol): regenerate Android gateway methods

* fix(ci): align rebased SDK and lint baselines

* fix(gateway): enforce view-only RFB boundary and fence desktop teardown

* fix(gateway): tighten RFB filter surface

* fix(state): keep pre-desktop databases readable and harden view-only RFB

* fix(gateway): fence desktop observer upgrades behind work admission

* fix(gateway): bind desktop observer tokens to their owner epoch

* fix(ci): regenerate config and SDK baselines after rebase

* fix(ci): regenerate native protocol and SDK baselines

* fix(ci): regenerate contracts after main rebase

* fix(state): register desktop metadata as lazy additive

* fix(ci): regenerate SDK baseline after final direct-merge rebase
2026-08-09 09:37:01 -07:00
Peter Steinberger b4104e29b5 fix(sessions): stop active runs before archiving (#120892)
* fix(sessions): stop active runs before archiving

Archive now fences the exact session, stops and drains active work before commit, preserves main/global-main/unknown protections and Delete behavior, and keeps patchMany per-target ordering.

* test(gateway): provide archive lifecycle context

* test(ui): expect active sessions to remain archivable

* refactor(gateway): keep archive drains internal

* fix(gateway): keep abort lifecycle contract acyclic

* fix(sessions): fence sharing across archive drains

* test(gateway): type archive lifecycle responses

* fix(sessions): reclaim cloud workers before archive

* fix(sessions): align archive request integration

* test(gateway): omit default deferred type

* docs(sessions): clarify cloud archive retries
2026-08-09 09:03:20 -07:00
Peter Steinberger 9a96375e60 feat(gateway): session-catalog terminal start plans behind cliAgents gate (#121020)
* feat(gateway): add session-catalog terminal start plans

* refactor(gateway): split catalog terminal start handler

* fix(gateway): enforce catalog terminal start eligibility

* test(gateway): split session catalog snapshot coverage
2026-08-09 08:13:39 -07:00
Peter Steinberger 7dcb4fb760 feat(talk): broker realtime sideband control (#121054)
* feat(talk): broker OpenAI realtime sideband

* test(openai): prove audio-only sideband offer

* fix(talk): harden realtime sideband control

* fix(talk): restore sideband session update fence

* fix(talk): recognize GA function output events

* refactor(talk): unify realtime broker session leases

* fix(talk): use PCM audio for WebRTC sideband

* chore(plugin-sdk): refresh API baseline
2026-08-09 05:14:20 -07:00