Commit Graph

2296 Commits

Author SHA1 Message Date
Vito Cappello b8d2ac735a fix(compaction): scope the transcript byte fuse to the latest compaction window (#130487)
* fix(compaction): scope the transcript byte fuse to the latest compaction window

readVisibleTranscriptStats only closed its window at a reset boundary, so once a session compacted, maxActiveTranscriptBytes measured the whole active path and every following turn re-compacted. The preflight fuse now measures the window the model actually sees (since the latest compaction or reset, plus its kept tail); history readers keep their reset-only scope.

* test(compaction): cover retained transcript accounting

## Worked on by
- @VACInc

Co-authored-by: VACInc <3279061+VACInc@users.noreply.github.com>

---------

Co-authored-by: VACInc <3279061+VACInc@users.noreply.github.com>
Co-authored-by: roboclaw-bot <309084314+roboclaw-bot@users.noreply.github.com>
2026-08-26 21:40:28 -04:00
Peter Steinberger 0b60846fa4 fix(workers): re-arm admission-deadline launches, terminalize dead-worker turns, defer debris cleanup (#130446)
* fix(workers): re-arm admission-deadline launches, terminalize dead-worker turns, defer debris cleanup

Three lifecycle repairs from the 50-worker campaign (#129979): the gateway
launch adapter re-arms a launch up to five times (exponential backoff with
jitter, caller-deadline capped, deterministic derived launch/turn ids so
adapter replays find the same journal rows) when the node journal proves the
child died on the admission deadline — credential/build rejections and
post-start failures stay terminal; stuck-turn recovery records a terminal
failure once a journal-terminal worker has no claim past a 30s cleanup grace
instead of skipping keep_lane forever; and startup recovery defers orphan
git cleanup for terminal placements into the first tracked background sweep
(50-placement fixture: 50 startup git scans to zero, ~556ms to ~5ms) while
pending-result recovery and ownership fences stay ahead of readiness.

* fix(workers): use the diagnosed admission-deadline message after #130435 rebase

WorkerConnection now formats the redacted last-failure diagnosis into the
deadline error itself, so the runtime's re-format (and its
lastConnectionFailure tracking) is redundant; errorText uses error.message.

* fix(workers): cap admission re-arms within the minted credential lifetime

Re-arms reused the credential minted before the first admission attempt,
so with five 120s windows plus backoff the final advertised retry could
start after the credential expiry and fail closed as credential-expired.
The launcher now threads credential.expiresAtMs through the tunnel launch
request, and the adapter re-arms only while the retried child still gets a
full admission window (WORKER_ADMISSION_DEADLINE_MS, now shared from the
connection contract) on the original credential; otherwise it returns the
terminal admission result. Expiry-boundary tests walk both sides.
2026-08-26 17:28:04 -07:00
Peter Steinberger 95161c2335 fix(workers): protect in-flight workspace manifests from concurrent retention (#130449)
At high completion concurrency, 12/50 turns failed with 'Cloud worker
finished, but its workspace result could not be reconciled' (#129979):
between a worker's manifest upload and its verification, a sibling
completion could trigger retention with a snapshot holding only the old
base manifest — the terminal worker no longer protected the workspace, so
cleanup deleted the just-uploaded manifest and verification died on ENOENT.
The node workspace runtime now pins the latest transferred manifest per
generation and includes it in every cleanup's reachable set (cleared with
its generation), and gateway retention snapshots treat unsettled results —
an active claim or a pending-result fence — as protection, which also
preserves it across restarts when node-local pins are gone. Capture errors
retain bounded redacted stderr so this failure class can never be
faceless again. No deadlines, protocol, or completion-semantics changes;
deterministic 1-turn and 50-turn reproductions fail pre-fix.
2026-08-26 16:22:52 -07:00
Peter Steinberger 25a16dd96c fix(portals): keep worker previews reachable and owned by the active environment (#130434)
* fix(portals): preserve worker routes and portal lifetime ownership

* fix(worker): keep diagnostics off the JSON result stream

* test(portals): model worker stream closure across Node versions

* test(agents): refresh compaction runtime mocks
2026-08-26 16:16:21 -07:00
Peter Steinberger dcb20658fc fix(openshell): preserve file writes during concurrent mirror commands (#130447)
Lease complete mirror file operations through the workspace owner instead of locking only remote transport. Preserve successful writes across exec publication and share serialization across backend handles.

Proven with a real Docker red/green reproduction, 128-workflow mirror/remote stress matrix, exact inventories, failure recovery, 170 focused tests, and updated operator docs. Related: #127441; follow-up to #130031.
2026-08-26 15:59:31 -07:00
Peter Steinberger a482a50cda perf(ui): bootstrap session rosters in one request (#130294)
* perf(ui): collapse session roster bootstrap RPCs

Amp-Thread-ID: https://ampcode.com/threads/T-01a037b3-be95-76ea-8fb3-75979929acf1

* fix(gateway): isolate and bound session roster bootstrap

Scope cached projections to the authenticated viewer, apply startup admission
consistently, and preserve the independent owner and shared preview budgets.
Regenerate the Swift client and document the bootstrap and cursor contract.
Publish Gateway test configuration atomically to remove the deferred-reader race.

Co-authored-by: Amp <amp@ampcode.com>

* test(gateway): extract session roster cache fixtures

---------

Co-authored-by: Amp <amp@ampcode.com>
2026-08-26 14:34:48 -07:00
Marvinthebored ce4a680544 feat(models): add configurable model selection scopes (#127813)
* feat: make model persistence scope explicit

* fix: keep unauthorized model directives as plain text

An unauthorized sender's inline directives are cleared to plain text before
they are acted on, but the persistent write target was derived from the
directives as they arrived. An unauthorized `/model <provider/model> -a` or
`-g` therefore reached the owner-authority error return instead of the
plain-text path every other directive takes.

Derive the target, the authority flag, and the persist flag after the
clearing. Authorized senders skip the clearing, so their behavior is
unchanged; the only closure that captures the target is invoked well after
the new declaration site.

Covers both persistent flags with a regression test that fails on the prior
head with "Agent and global model defaults require owner authority or
operator.admin scope." and passes here.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat(models): make model selection scope configurable

Preserve current model-selection defaults when the optional scope preference is unset. Add explicit session, agent, and global overrides without broadening config-write authority.

Co-authored-by: Marvinthebored <peter@lindsey.jp>

* test(models): simplify scope expectation fixtures

---------

Co-authored-by: Marvinthebored <peter@lindsey.jp>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-08-26 14:31:56 -07:00
Peter Steinberger c3ea9775ca feat(portals): expose portals to sessions on node-backed cloud workers (#130105)
* feat(portals): expose portals to sessions on node-backed cloud workers

Worker-turn sessions placed on cloud workers (bundled Crabbox) can now open
portals. The portal tool joins the closed worker session-tool catalog and
executes on the Gateway, which mints a single-use ticket per proxied
connection; the enrolled node redeems it over a TLS-pinned WebSocket via the
new private worker.portal.stream.v1 command and splices to the worker's
loopback port. The desktop stream broker is generalized with discriminated
desktop/portal tickets, and shared node stream transport is extracted from
the desktop stream command. Portal availability is negotiated via a new
gateway hello capability plus the node's reconnect-scoped
workerHost.portalStream declaration, so old node bundles simply do not
receive the tool. Environment teardown and owner-epoch replacement close a
worker's portals; node disconnects destroy live streams while the listener
keeps serving the waiting page.

Live-proven on a Crabbox AWS worker: agent opened a portal over the worker
protocol, python3 -m http.server on the box served through the Gateway
portal URL with token auth intact.

* fix(portals): break portal proxy/service type cycle; regenerate Swift protocol models

* fix(portals): drop dead PortalTarget re-export; fix test import

* fix(portals): only tear down turn-created portals on post-open authority loss

ClawSweeper finding: a revoked turn's duplicate open could close the live
portal a still-authorized predecessor established, because service.open
returns the existing same-environment portal and the executor's error path
closed whatever id it received. portal.open now reports created-vs-reused;
the executor tears down only portals it created and closes its redundant
carrier handle whenever an existing portal is reused.

* feat(portals): fence portal reuse behind the caller's live-authority assert

Rank-up move from review: portal.open revalidates the caller inside the
serialized operation before a reuse can mutate a live portal's metadata,
mirroring close(assertCurrent). Regression test proves a lapsed caller
cannot rewrite a reused portal's title.

* test(portals): expect assertCurrent in worker portal open call

* refactor(gateway): extract worker request dispatch into its own module

worker-connection.ts crossed the 700-line ratchet after rebasing onto
current main; dispatchWorkerRequest and its rejection helper move to
worker-connection-dispatch.ts unchanged (434 + 329 lines).
2026-08-26 12:51:48 -07:00
Peter Steinberger 8a101ed5a5 docs(gateway): add Cloud Sessions overview page (#130301)
* docs(gateway): add Cloud Sessions overview page

Routes readers across paired devices, Crabbox cloud workers, automatic
device load balancing, and idle suspension with warm wakes; slots into
Scaling and operations ahead of Cloud Workers.

* docs(gateway): scope replacement and ranking claims by destination
2026-08-26 12:10:25 -07:00
Peter Steinberger d2d055b8d4 feat(gateway): auto-suspend idle cloud workers with suspendAfter (#130242)
* feat(gateway): auto-suspend idle cloud workers

* feat(crabbox): enable bounded warm images by default

* docs(gateway): explain idle worker suspension and warm images

* Revert "feat(crabbox): enable bounded warm images by default"

This reverts commit 546466eaa9.

* docs(gateway): keep warm images opt-in pending a proven capture boundary

Review finding: default-on capture would persist setup-created state outside
the scrubbed worker root into provider images for profiles that never opted
in. suspendAfter pairs with warmImage: true for warm wakes.
2026-08-26 11:30:25 -07:00
Peter Steinberger 84a0c6ea14 docs(gateway): document the Daytona cloud-worker provider (#130282)
Crabbox already ships a Daytona backend; add the profile example and
provider-specific notes (no crabbox login step, default snapshot ships
Node, default machine class) confirmed by a live lease-and-exec proof.
2026-08-26 11:24:40 -07:00
Peter Steinberger 52715a440a fix(sandbox): isolate role-required sandboxes per guest, not per agent (#130229)
* fix(sandbox): isolate role-required sandboxes per guest, not per agent

Role-forced sandboxing (#129926) contained a guest against the Gateway host
but not against other guests. Sandbox identity stayed agent-scoped, so every
session on a shared agent converged on one scope key, container and
workspace; configured workspaceAccess "rw" additionally pinned the workspace
to the shared agent directory regardless of scope, and configured scope
"shared" used a literal shared container name.

Measured before the fix, two guests on one shared agent:
  scope=agent(default) rw -> same scope key, same workspace
  scope=agent(default) ro -> same scope key, same workspace
  scope=session        rw -> distinct keys but SAME workspace
  scope=session        ro -> isolated

So only one of four combinations isolated guests, and it was not the default.
The authorization layer promised "edit only your own sessions" while the
container layer handed every guest the same writable workspace.

Role-required sessions now derive their sandbox identity from the session
creator: the scope key is principal-qualified, so different guests never
share a container or workspace while one guest's own sessions still do (per
guest, not per session). Configured "rw" resolves to "ro" with an
operator-visible warning, configured "shared" resolves to an effective agent
scope, and a required session with no creator provenance fails closed before
provisioning. Sessions without the requirement keep today's behavior.

* chore: drop accidentally committed sandbox scope repro scratch file
2026-08-26 10:58:33 -07:00
Josh Avant e6ed7e30cc feat(audit): record session action decisions (#129093)
* feat(audit): record session action decisions

* fix(protocol): preserve session sharing client compatibility
2026-08-26 08:21:37 -07:00
Peter Steinberger ef95d8f55e feat(secrets): agent-requested credentials the model never sees (#129670)
* feat(secrets): agent-requested credentials the model never sees

The new main-session secrets tool lets the agent request a credential by
name: the human enters the value in a masked question card (Control UI,
/ask/<id> deep link, iOS/macOS/Android), and the gateway diverts the
answer straight into the shared secret store at question.resolve. The
record, broadcast, waitAnswer, tool result, transcript, and model context
only ever carry a synthetic stored marker.

- protocol: additive secretStore binding, secretStoreExisting replacement
  metadata, and resolve-time secretStoreAllowedHosts (since 2026.8)
- gateway: store-bound question validation, admin-gated minting (blocks
  questions-scope self-answer escalation past secrets.store.set), shared
  redaction-first store write service reused by secrets.store.set
- tool: secrets request/list/delete; write-only by design, delete carries
  verified agent runtime identity; channel delivery is link-only so chat
  text is never captured as a secret
- Control UI: masked composer card with requester identity, store banner,
  editable allowed hosts, replacement warning, retry-on-validation-error,
  a standalone /ask/<id> page, and a named startup-JS baseline bump
- mobile: SecureField / password transformation for isSecret questions,
  no answer echo in terminal summaries; new native string registered in
  the locale-refresh inventory (generated artifacts stay workflow-owned)
- regression: claimed harness secret input stays out of session transcripts

Live-proven on an isolated dev gateway: real model turn, masked entry via
Playwright, value present only in secret_store_entries, absent from every
transcript, log, and the DOM.

* chore(protocol): regenerate protocol models and tool display

* fix(cli): read image string options through a typed helper

PR #129463 added four commander option narrowings in image.ts without
SAFETY coverage, leaving the assertion-safety ratchet red (21 > 17) for
every branch on current main. Replace the casts with a typeof-checked
read so the assertions are removed rather than annotated; each value is
still validated by its normalizer. SAFETY comments cannot work in this
file: the ratchet's raw scanner never rescans template tokens, so
comments after the first substitution template are unreadable to it.

* chore(protocol): refresh Swift models against current main

* chore(i18n): re-baseline the native inventory on current main

* docs(secrets): state the default-on tool policy and how to disable it

* fix(secrets): tell the model what the store actually does

The shipped tool description named the three actions and nothing else,
and no parameter carried a description. The model could not tell that
request blocks a human, that reason is shown to that human, what secret
and env select, or - the silent-failure case - that a secret stored with
no allowedHosts can never be substituted, so a successful request could
produce a permanently unusable credential. Move the description to the
presets module beside ask_user and document every parameter.

* refactor(agents): share one blocking-question lifecycle between tools

ask_user and secrets each carried their own registration, wait, and
cancel logic, and they had diverged: ask_user recovers an answer that
lands between its wait timeout and the cancel, while secrets discarded
it and reported no_answer even though the Gateway had already stored the
credential. One shared canceller and answer reader fixes that race for
both, folds the two divergent gateway-call types into one, and drops two
type assertions in favour of the canonical record guard (ask_user's
assertion baseline shrinks 11 -> 8).

Net +49 production lines: the shared module costs more than the
duplication it removes, and buys the correctness fix plus a single owner
for question lifecycle.

* fix(ui): keep the allowed-hosts field readable as an input

Main's composer restructure moved the free-text input styling into the
option-row context, so the store-request hosts field - which sits outside
a row - lost its border and read as static text. It is the one field the
operator is meant to review and edit before releasing a credential, so
give it its own border and focus ring.

* fix(secrets): close two credential-boundary holes in agent requests

Requests are now protected-secret only. list renders env values, so an
agent could request kind=env, watch a human type it into a masked box
under a no-visibility promise, then read it straight back; the tool text
even claimed values are never returned. Environment values stay operator
-set in Settings or the CLI, where they are agent-readable by design.

Store-bound questions are also bound to the run that requested them. The
resolve path authorized only the answering client, so a terminated or
replaced agent run could still have a credential written on its behalf -
the recorded runId was provenance, not closure-bound authority. Minting
now requires a runId and resolution revalidates that exact live run
immediately before the store write, with no await in between, failing
closed as QUESTION_REQUESTER_INACTIVE.

Both reported by ClawSweeper as P1 credential-boundary findings.
2026-08-26 08:10:16 -07:00
y$un_ 4ae1ce4c28 docs: declared local browser profiles must set cdpPort or cdpUrl (#129923)
The schema already rejects a hand-declared openclaw or clawd profile
that sets neither. Stop telling operators those fields auto-assign.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-26 06:27:30 -07:00
Peter Steinberger a82042f204 feat(crabbox): opt-in profile warm images for cloud workers (#130087)
* docs(plan): add worker hibernation design note

Evaluates dormant leases, checkpoint-based warm start, and serverless
backends for a worker-environment hibernation tier; proposes a plugin-only
phase 1 (Crabbox checkpoint fork warm images) with SDK/store phases gated
on maintainer acceptance.

* feat(crabbox): add opt-in profile warm images

* fix(crabbox): preserve warm image machine class

* refactor(crabbox): tighten warm-image seams after review

* fix(crabbox): budget warm-image capture for real snapshot round trips

The 60s command bound starved the scrub and checkpoint-create phases under
coordinator latency, silently degrading every teardown to cold-only.
Live-measured on AWS.

* docs(plan): record worker-hibernation acceptance and live proof

Phase 1 live-proven: local-container full loop (cold 101s vs warm fork 40s,
scrub boundary verified) and AWS cold provisioning with graceful degrade at
the coordinator admin gate; named upstream follow-ups.

* fix(crabbox): make warm-image lifecycle recovery-safe

Delete least-recently-used provider snapshots before image index admission.

Recover stale empty capture reservations without breaking single-flight.

Persist effective machine classes across restarts and revert heartbeat coupling.

Align the hibernation plan with the exact-class warm-image key policy.
2026-08-26 05:58:00 -07:00
Peter Steinberger 093166c7d9 feat(gateway): add renewable cooperative suspension draining (#130003)
* feat(gateway): add renewable suspension draining

* fix(gateway): satisfy drain ownership checks

* fix(gateway): retain pending question drain owner
2026-08-26 03:46:16 -07:00
Peter Steinberger c69abc2327 fix(sandbox): restore reliable OpenShell execution and workspace integrity (#130031)
* fix(sandbox): harden OpenShell lifecycle and workspace integrity

Fixes #127441, #127438, #98446, #119270, and #89606.

Co-authored-by: harjoth <harjoth.khara@gmail.com>

Co-authored-by: qingminlong <qing.minlong@xydigit.com>

* fix(agents): reject unresolved memory flush path aliases

* docs: keep OpenShell release notes in pull request

---------

Co-authored-by: qingminlong <qing.minlong@xydigit.com>
2026-08-26 03:10:07 -07:00
Peter Steinberger 03f33346c0 fix(gateway): fan out batched gmail hook pushes and bound gmail hook bodies (#130002)
* fix(gateway): fan out batched gmail hook pushes and bound gmail hook bodies

The built-in gmail hook preset rendered only messages[0] of a Pub/Sub push
that batches up to 100 messages, silently dropping the rest (#120277), and
the shared 256KiB hook body cap 413-wedged gog's history-rewind retry loop
on large batches (#120278).

Hook mappings gain an explicit forEach fan-out contract: one action per
payload array element, per-item replay identity derived from dispatch scope
so producer redeliveries converge instead of duplicating, and batch
responses bounded under gog's 10s hook client timeout. gmail-path mappings
inherit a body bound derived from the provisioned producer contract
(hooks.gmail.maxBytes x gog's 100-message batch).

* fix(gateway): guard null transform action before fan-out merge

* fix(gateway): admit fan-out hook items in the background

Live proof against the real gog producer exposed a livelock: eight
concurrent cold isolated-run workspace preps take ~15s, exceeding the 15s
admission deadline, so every fan-out item was canceled, nothing reached the
replay cache, and each Pub/Sub redelivery repeated the same cold burst
forever. Fan-out items now admit without the start deadline; the producer's
redelivery plus per-item replay identity own retry semantics, and direct
/hooks/agent dispatches keep the bounded 15s admission contract.

* test(gateway): adapt reload-handler hook assertions to actions[] shape

* test(gateway): split hook fan-out mapping tests into their own file

hooks-mapping.test.ts crossed the max-lines lint cap after the fan-out
coverage landed; the fan-out describe moves to hooks-mapping.fanout.test.ts.

* fix(gateway): harden fan-out identity, mixed action kinds, and body ceiling

Addresses the ClawSweeper P1 findings on #130002: within-batch identical
rendered items now get occurrence-numbered replay identities (both run,
redeliveries replay both); per-item transforms emitting mixed wake/agent
kinds dispatch every action through one unified loop instead of silently
dropping the non-first kind; and the gmail-derived body allowance is
capped at 32 MiB so an operator-set hooks.gmail.maxBytes cannot amplify
into an unbounded in-memory request buffer.
2026-08-26 03:00:41 -07:00
Peter Steinberger 8bf8e80e1d feat(cron): link chat notifications to Control UI run inspection (#130049)
* feat(cron): link chat notifications to Control UI run inspection

When gateway.publicOrigin is configured and the Control UI is enabled,
channel-delivered automation and task notifications append an
'Inspect: <url>' line so operators can click straight to the run record:

- cron failure alerts and command/script completion announcements link to
  /automations?job=<id>&run=cron:<id>:<startedAt> (appended after summary
  redaction so the redactor cannot strip the link)
- isolated agent run completions link to the run's exact session route
  (appended only after silent/NO_REPLY suppression, immutably, so a
  suppressed run cannot become a visible announcement)
- direct-channel task terminal notifications link to the task's child
  session; session-queued events stay link-free
- the automations page adopts ?job=&run= deep links (loader search pass-
  through, one-shot adoption after jobs load, History tab + run highlight)

No new config surface: the URL derives from the existing
gateway.publicOrigin + gateway.controlUi owner in control-ui-link-base.

* fix(cron): match linked runs by execution id and dedupe delivery runtime type

Live verification caught that ledger run ids are public run ids (receipt
UUIDs, manual:<...>) while notifications embed the execution id
(cron:<jobId>:<startedAtMs>), so the linked-run highlight never matched:
the automations page now resolves the execution id against the entry's
recorded runAtMs, with regression tests for both id spaces.

Also: move the isolated-delivery inspection-link append into the payload
normalization owner (keeps delivery-dispatch under the max-lines limit),
and replace the duplicate TaskRegistryDeliveryRuntime declaration in
task-registry.test-support with a type-only import of the canonical type
(the duplicate hid the new optional resolver from the tsgo test shard).
2026-08-26 02:59:51 -07:00
Peter Steinberger 68f17b01cc fix(gateway): permission modes work on rootless sessions via workspace default root (#129994)
* fix(gateway): permission modes work on rootless sessions via workspace default root

Session permission modes hard-required a recorded sessionRoot, which only the
sessions.create RPC ever records. Channel sessions, pre-#124909 rows, and
exec-node sessions are rootless, so runs failed closed with 'session permission
mode requires a recorded session root' (rows poisoned before the #128149 guards
were permanently bricked) and operators could not set a mode on channel
sessions at all.

The mode semantics are root-free; every containment consumer already falls
back to the workspace, and the remote worker launcher already derives its
containment root from placement.remoteWorkspaceDir. Make that the canonical
rule: a recorded sessionRoot pins worktree/explicit-cwd boundaries, and a
rootless session's boundary is the agent's canonical workspace resolved where
the run is prepared.

- embedded runner + compaction: default policy root to realpath(workspace)
  instead of throwing/dropping the mode; bricked rows self-heal
- gateway: delete the patch/create/reset rejection guards and
  session-permission-policy.ts entirely
- codex plugin: helpers take a required defaultRoot (agent workspace fact,
  never a requested thread cwd); delete the four root throws; sandbox context
  for conversation bindings now derives from the agent workspace
- ui: drop the rootless-rejection surface from the permission picker
- docs: permission-modes boundary contract updated

* fix(agents): split workspace path identity out of the kysely state store

Heals main: #116650 made memory-host-sdk/dreaming.ts import
resolveWorkspaceStateIdentity from workspace-state-store, statically pulling
kysely into the memory-core doctor-contract closure and failing the
doctor-contract-closure-guard lane on origin/main. The identity helpers are
pure path/crypto code; move them to workspace-state-identity.ts and repoint
all importers (dreaming, event-store, onboarding recommendations, workspace
migrations, legacy state).

* fix(ui): drop the unused permission picker sessionRoot prop and dead i18n keys

check-prod-types caught the New Session dialog still passing the removed
sessionRoot picker prop; the prop and the permissionControls.rootLabel /
sessionRoot strings have had no consumers since #124301.

* fix(codex): resolve rootless permission boundaries from agent config, not bind cwd

ClawSweeper P1: /codex bind --cwd stores the requested path as the binding
workspaceDir, which the previous commit then trusted as defaultRoot — letting
a bound thread's requested cwd become (and widen) the rootless permission
boundary. resolveConversationAppServerRuntime now owns the fact: it resolves
the selected agent's canonical workspace from config (falling back to the
plugin's configured default workspace), and the caller-supplied
agentWorkspaceDir parameter chain is deleted so no call site can pass a
tainted value. Recovery of recorded roots is unchanged (pinned roots stay
pinned); the moved-history test now proves the bind-cwd clamp lands on the
agent workspace.
2026-08-26 02:25:57 -07:00
Peter Steinberger 4708c0b607 feat(secrets): opt-in traffic allowlist for the secret egress proxy (#129880)
* feat(secrets): add opt-in traffic allowlist to the secret egress proxy

Add secrets.egressProxy.allowedHosts: when present, the egress proxy
refuses non-sentinel requests and CONNECT tunnels to hosts outside the
effective allowed set (configured list, hosts bound to the run's
registered secrets, and bypassHosts) with a typed host-not-allowed
refusal naming the remediation. Empty array is lockdown; omitting the
key keeps the previous unrestricted behavior. Sentinel substitution,
per-secret destination binding, proxy auth, and bypass tunnels are
unchanged; the allowlist is defense in depth for cooperating traffic
since bypass-surviving sentinels remain the primary defense.

* fix(secrets): validate egress proxy allowlist hostnames at the config boundary

Extract the exact-host contract from the secret store into a canonical
normalizeExactAllowedHost helper and validate secrets.egressProxy
allowedHosts and bypassHosts entries through it. Schemes, ports,
wildcards, and malformed hostnames are now rejected when config is
accepted instead of throwing during egress-proxy startup, which the
runtime normalizer did for both keys before this change.

* fix(doctor): repair unusable secret egress proxy host entries

Tightening the egress-proxy host schema can invalidate an existing
config: a disabled proxy with a malformed bypassHosts entry loaded fine
before and now fails validation, which exits the Gateway with code 78.

Add the matching doctor --fix migration. Invalid entries never
functioned (the proxy's own hostname normalizer threw on them at
startup), so they are dropped rather than canonicalized; enabled and
valid entries are preserved untouched.
2026-08-26 01:36:09 -07:00
Peter Steinberger 29966f4f70 fix(sandbox): stop exposing environment values in process listings (#129781)
* fix(sandbox): keep environment values out of process arguments

Route Docker and Podman environment values through private env files, and stage SSH and OpenShell environments separately from command argv. Container env-file values are now explicitly single-line.

* fix(sandbox): always clean staged SSH environment

Remove staged SSH and OpenShell exec scripts during finalization even when the local SSH process exits normally with a non-shell error code.

* test(openshell): align remote seed and staged exec fixtures

* fix(sandbox): validate container environment transport early

Reject Docker and Podman environment entries that secure env files cannot represent during config validation, with manual remediation guidance while preserving SSH and OpenShell multiline values.

* fix(config): keep sandbox env validation cold

Share the dependency-light scope and environment merge contract between runtime resolution and Docker/Podman config validation so isolated config and Doctor flows do not load runtime state.
2026-08-26 00:45:50 -07:00
Peter Steinberger 6a6635abb3 feat(daemon): support Bun 1.4 managed services (#129593)
* feat(daemon): support Bun 1.4 managed runtimes

Amp-Thread-ID: https://ampcode.com/threads/T-01a037b7-66db-71f0-91e7-1578b383afb2

* fix(daemon): enforce Bun SQLite safety

Amp-Thread-ID: https://ampcode.com/threads/T-01a037b7-66db-71f0-91e7-1578b383afb2

* fix(daemon): preserve lightweight Bun startup

Amp-Thread-ID: https://ampcode.com/threads/T-01a037b7-66db-71f0-91e7-1578b383afb2

* docs: show Bun runtime opt-in commands

Amp-Thread-ID: https://ampcode.com/threads/T-01a037b7-66db-71f0-91e7-1578b383afb2

---------

Co-authored-by: Amp <amp@ampcode.com>
2026-08-26 00:45:43 -07:00
Peter Steinberger 7f7448e96d feat(gateway): identity-conditional forced sandboxing for operator roles (#129926)
One shared agent could not give different people different execution
isolation: sandbox selection saw only the agent's mode and the session key,
never who created the session. Teams that share an agent had to choose
between sandboxing everyone or no one.

Add a closed, optional role axis sandbox?: "inherit" | "required"
(default "inherit", so existing deployments are unchanged). The Gateway
resolves it from the authenticated session creator and stamps it write-once
onto genuinely new session entries; the agents layer reads that immutable
fact and forces a sandbox even when the agent config says mode: "off".

Anchoring on the creator keeps a session's isolation stable for its lifetime:
a maintainer replying inside a guest session cannot un-sandbox it, and a
caller whose role requires a sandbox is refused inside an unstamped session
rather than silently inheriting host execution. Reset, rewind, rollover and
fast-path all carry the requirement forward so a session cannot be reset out
of its sandbox.

Because exec injects the agent's managed GitHub identity only for gateway-host
execution and blanks GH_TOKEN/GITHUB_TOKEN otherwise, a forced-sandbox session
also loses access to the shared agent's credentials — the property that makes
one shared agent safe for untrusted guests.

Session entries are JSON; no SQLite schema, schema-version, or protocol change.
Docs also correct the sandbox backend list, which omitted the first-class
podman backend.
2026-08-26 00:18:50 -07:00
Peter Steinberger 6d1c1c9f57 refactor(cron): unify heartbeat automations under cron ownership (#129862)
* refactor(cron): unify heartbeat automations under cron ownership

* test(cron): align heartbeat automation fixtures and snapshots

* test(heartbeat): preserve rebased manual-wake coverage

* test(ui): scope mobile session actions to active chat pane
2026-08-26 00:08:18 -07:00
Peter Steinberger 2f17d11e90 fix(backup): preserve configured agent state roots (#129773)
* fix(backup): honor configured agent state roots

Inventory configured agent directories as durable backup owners regardless of layout. Reuse the owner inventory for archive planning, SQLite snapshotting, verification, restore safety, and Git/SQLite backup selection.

Add manifest-first plugin backup resource declarations so regenerable runtime roots stay plugin-owned without provider IDs in core.

Closes #129730

* fix(backup): preserve canonical ownership checks
2026-08-25 21:13:37 -07:00
Peter Steinberger 9ab6df9f19 fix(openshell): remote workspace is never seeded when the gateway restarts between sandbox create and first exec (#129809)
The remote-mode seed obligation lived only in process memory
(remoteSeedPending), so a gateway restart between sandbox create and the
first exec adopted the sandbox via 'sandbox get' and silently skipped the
one-time seed, leaving an empty remote-canonical workspace with no visible
failure.

Adopted remote sandboxes now probe the managed roots once per process at
the ensure boundary and arm the seed only when every root is missing or
empty, so recovery can never wipe operator content in an already-seeded
workspace (the seed guard shipped in v2026.7.1, so legacy seeded sandboxes
without any marker exist). The probe also heals a create that timed out
client-side after the sandbox actually came up.

Regression test simulates create -> restart -> first exec and fails
pre-fix; a sibling test locks the never-reseed-non-empty-roots invariant.
2026-08-25 20:32:40 -07:00
Peter Steinberger 422d6818e4 refactor(workers): unify cloud bootstrap lifecycle (#129719)
* refactor(workers): unify cloud bootstrap lifecycle

* fix(workers): preserve cloud setup and repair qa fixtures
2026-08-25 20:11:33 -07:00
Peter Steinberger 3d0df74b0f fix(openshell): sandbox cleanup, workspace sync, and gateway setup failures (#129641)
* fix(openshell): harden sandbox lifecycle and workspace sync

* fix(openshell): preserve shipped workspace root layouts

* test(gateway): reset shared agent harness state between cases

* test(qa): measure channel health startup grace directly

* chore(release): leave changelog updates to release automation
2026-08-25 18:08:55 -07:00
Peter Steinberger 91cc37b788 fix(gateway): stop fabricating scope-upgrade requests on trusted-proxy reconnects (#129563)
* fix(gateway): stop fabricating scope-upgrade requests on trusted-proxy reconnects

Same-key trusted-proxy reconnects whose auto-approvable scopes are already
covered by the paired grant now narrow the session in place instead of
running the pairing request/approve machinery: no pending-store writes and
no false 'device access upgrade requested' / 'auto-approved' security-audit
warns on every Control UI page load. The upgrade audit warn moved into
requirePairing so it fires only when a pairing request is materialized.
Durable widening (broadened deviceAutoApprove.scopes) and key-mismatch
repairs keep the existing lanes.

* fix(gateway): revalidate the live paired grant in the reconnect fast path

Address ClawSweeper P1: the narrowing decision now rereads the
authoritative paired row as the last await before returning, so a
concurrent revoke, key replacement, or grant reduction fails closed into
the pairing lane instead of riding the pre-plan snapshot. Extract the
pairing approval-plan resolver into connect-pairing-approval-plan.ts to
keep the authorization owner under the 700-line lint boundary.
2026-08-25 17:59:51 -07:00
Josh Avant ede9775941 fix(gateway): isolate portal cookies by lifetime (#129503) 2026-08-25 16:11:30 -07:00
Peter Steinberger 6a1dedb00c refactor(agents): remove redundant heartbeat system prompts (#129642)
* refactor(agents): remove redundant heartbeat system prompts

* test(agents): align heartbeat cleanup checks and prompt snapshots
2026-08-25 16:10:22 -07:00
Peter Steinberger 3626a8e18e fix(agents): enforce session permissions for shared terminal input (#129604)
* fix(agents): enforce session permissions for shared terminal input

* chore: keep terminal release notes in pull request

* fix(agents): fence full terminal input to active run authority
2026-08-25 15:59:28 -07:00
Peter Steinberger 9b7c95a0f0 fix(anthropic): keep automated heartbeats on subscription usage (#129513)
* fix(anthropic): keep automated heartbeats on subscription usage

* test(agents): align retired heartbeat template expectations

* fix(anthropic): avoid heavyweight policy bootstrap imports

* chore(config): prune retired template assertion baseline

* test(vitest): cover Codex startup test family

* refactor(anthropic): remove obsolete thinking-profile re-export
2026-08-25 14:11:38 -07:00
Sasan e9620fba9d fix(gateway): usage.status no longer waits on provider HTTP (#121799)
* fix(gateway): refresh provider usage asynchronously

* fix(ui): report a stalled provider-usage refresh on Model Providers

The page observed the incomplete-usage marker but discarded the exhausted
outcome, so once the retry budget was spent it rendered ordinary provider
cards with no usage and no explanation — indistinguishable from providers
that report no usage at all. Keep the outcome and render the warning the
Usage page already owns, reusing usage.providerUsage.stalled rather than
minting a Model Providers key so no locale baseline churns.

A user-initiated refresh now restarts the retry budget. The notice tells the
operator to refresh, so the button has to hand back attempts to spend; only
the forced path resets it, or the budget could never exhaust.

Also fixes tsgo:core:test on the current head: createStore's inferred literal
had no usageStats, so the run-bookkeeping case could not stamp it, and
view.test.ts needed the new prop.

Closes the ClawSweeper P2 at model-providers-page.ts:169-175.

* fix(ui): keep the stalled usage notice when usage.status starts rejecting

loadModelProvidersData turned a rejected usage.status into providerUsage:
null, which the page read as a completed load. observe(false) then reset the
retry budget and cleared the stalled callout, so a permanently broken usage
endpoint rendered as ordinary cards with no usage and no explanation — the
same silent failure the callout was added to prevent. The reset also fired
mid-cycle: one incomplete response followed by one rejection restarted the
budget, so the notice could be deferred indefinitely.

Record the failure at its producer instead of inferring it downstream. A null
providerUsage also means "not loaded yet", and no caller can tell the two
apart, so load.ts now reports providerUsageFailed explicitly and the page
treats a failed read as unresolved rather than resolved-empty.

Found by a Codex review of 417d43b65d.

* revert(gateway): drop the opportunistic model-catalog fast path

It broke two chat.history tests on main — both assert the cold catalog loader
runs exactly once, and reading the prepared snapshot first means it never does.
checks-node-compact-small-10 was red for that reason.

The change was a separate-surface latency fix that this PR picked up in passing,
and the body already offered to split it. Dropping it is the honest resolution:
rewriting main's assertions to accommodate a drive-by optimization would trade
one concern's proof for another's convenience. optional-model-catalog.ts,
server-model-catalog-auth.ts and their test return to the merge-base.

This PR is now only the usage.status non-blocking contract and its clients.

* fix(usage): preserve incomplete retry state

* perf(ui): keep usage capability startup-neutral

* fix(ui): restore provider usage retry convergence

* fix(usage): restore retry and cache invariants

* fix(usage): stabilize provider convergence

* test(ui): exercise provider recovery path

* test(ui): remove stale usage route fixture field

* fix(macos): show provider usage errors

* fix(macos): bound usage retries per menu open

* fix(macos): end usage retries on menu close

---------

Co-authored-by: Josh Lehman <550978+jalehman@users.noreply.github.com>
2026-08-25 13:20:10 -07:00
Ayaan Zaidi 750a64e7cd fix(anthropic): keep Claude CLI authentication native (#129052)
Stop OpenClaw from copying or refreshing Claude CLI OAuth tokens.
Claude CLI now owns native login and refresh state; Doctor removes retired copies while preserving CLI routing.

Co-authored-by: Ayaan Zaidi <hi@obviy.us>
2026-08-25 17:18:06 +05:30
Peter Steinberger fda20a6ed2 fix(systemd): honor effective drop-in configuration (#129170)
* fix(systemd): honor effective drop-in configuration

Read one coherent manager-loaded service snapshot while retaining the managed base definition for repairs. Track drop-in ownership, surface pending daemon reloads, and keep update and doctor rewrites from adopting operator-owned values.

Closes #128929

* fix(systemd): expose reload status in service snapshot

* test(systemd): cover Linux doctor ownership
2026-08-25 02:30:34 -07:00
Peter Steinberger 24898ba894 fix(workers): repair cloud session lifecycle and recovery (#121564)
* fix(workers): repair cloud session lifecycle and recovery

* test(workers): preserve dual-mode provider dispatch coverage

* refactor(workers): centralize inherited profile authorization
2026-08-25 01:48:57 -07:00
Peter Steinberger 6885eb69b0 fix(telemetry): suppress reporting from automated environments (#129155)
CI jobs are not installs; unchecked they outnumber operators by orders of
magnitude and make version and platform counts meaningless. A configured
telemetry endpoint still reports, so update-path E2E lanes keep working.
2026-08-25 01:37:48 -07:00
RoboClaw 0415c403a7 improve: use agent identity as the sole assistant identity (#129067)
* refactor(config): remove duplicate assistant identity

* test(config): isolate assistant migration coverage

---------

Co-authored-by: roboclaw-bot <309084314+roboclaw-bot@users.noreply.github.com>
2026-08-25 00:21:13 -07:00
Peter Steinberger 8a1ff3966b feat(workers): run OpenClaw and Codex on the same cloud profile (#127752)
* feat(workers): run both harnesses on one cloud node profile

* docs(workers): explain shared OpenClaw and Codex cloud profiles

* fix(workers): preserve trusted Codex cloud enrollment

* docs(codex): clarify bundled cloud plugin eligibility

* fix(workers): honor Crabbox bootstrap retries

* fix(workers): allow cloud profile setup to finish

* fix(workers): reject unsupported cloud execution modes

* refactor(workers): split cloud placement modules

* fix(workers): preserve completed container results
2026-08-25 00:13:29 -07:00
Vincent Koc df83ff516f fix(agents): apply tool policy to Anthropic native calls (#128805)
* fix(agents): enforce native policy in Anthropic SDK

* fix(agents): enforce canonical native tool policies safely

Co-authored-by: Vincent Koc <25068+vincentkoc@users.noreply.github.com>

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-08-24 23:58:23 -07:00
Peter Steinberger e9017714c2 fix(hooks): avoid false admission timeouts during runtime reload (#128975) 2026-08-24 22:12:54 -07:00
Peter Steinberger 2d7b1d21d3 fix: keep verbose health available when channel probes stall (#128915)
* fix(gateway): bound live health aggregation

Keep verbose health within a Gateway-owned deadline, preserve healthy sibling results when channel probes stall, and document that the CLI timeout controls the Gateway connection.

Fixes #128890

* fix(gateway): retain health probe permits

Keep process-wide health capacity occupied until timed-out plugin work actually settles, and prove repeated refreshes cannot replace five stalled probes.
2026-08-24 19:44:18 -07:00
Josh Avant 2e9c8f8d3f Audit exact-bound owner-native lifecycle receipts (#126082)
* feat(audit): add owner-native lifecycle receipts

* fix(audit): store lifecycle bindings additively

* test(cron): isolate execution binding fixtures

* fix(audit): reject stale lifecycle owners

* fix(acp): gate parent flow binding on retained task

* fix(audit): reject pruned owner cursors
2026-08-24 17:49:25 -07:00
Peter Steinberger 33b10fe4aa fix(macos): user accent from Control UI is clobbered by config snapshots and never live-updates the chat window (#128703)
* fix(macos): honor user accent precedence in config snapshot and live-update chat window

The Control UI user accent (ui.prefs.accent) landed in #128432/#128577 with
precedence user accent -> operator ui.seamColor -> theme default, and the
gateway's talk.config payload already applies it. The macOS app had two gaps:

- ChannelsStore.applyUIConfig read raw ui.seamColor from the config.get
  snapshot and clobbered the user accent set from talk.config depending on
  arrival order. It now resolves ui.prefs.accent ?? ui.seamColor via a
  testable helper mirroring the gateway precedence.
- The native chat window read AppStateStore.seamColorHex once at window
  construction, so accent changes never live-updated. MacChatSurface now
  reads the @Observable store in body, deleting the one-shot userAccent
  plumbing.

Docs: configuration-reference.md documents the precedence for native-app
chrome. Regression test fails pre-fix (snapshot returned the operator seam
color instead of the user accent).

* fix(macos): refresh config from gateway config.changed events

Addresses the review finding that no macOS consumer turned the gateway's
hash-only config.changed broadcast into refreshed shared state, so a
Control UI accent change never reached an open native chat window while
the app ran. ChannelsStore now subscribes to gateway pushes and re-fetches
config.get on config.changed, reconnect snapshots, and sequence gaps.

The refresh applies non-force so an in-progress local settings draft wins
(the gateway rejects stale-hash writes anyway). The in-flight reload queue
gains a closed pending level (none/refresh/force) so a refresh arriving
during a load is coalesced instead of dropped, and a requeued refresh
cannot clobber a dirty draft the way the old boolean force-pending did.
2026-08-24 05:37:59 -07:00
Peter Steinberger 4c48c13ab6 fix: surface cloud worker enrollment rejection evidence (#128632) 2026-08-24 02:32:24 -07:00
Peter Steinberger 12d0fd2ef8 refactor(anthropic): replace handwritten Claude sessions with Agent SDK (#128131)
* refactor(anthropic): explore official Claude Agent SDK runtime

* refactor(anthropic): replace handwritten Claude sessions with SDK

* refactor(anthropic): collapse SDK live-session ownership

* refactor(anthropic): simplify SDK ownership and preserve live skills

* fix(anthropic): fence cancelled SDK runs before process startup

* fix(anthropic): harden SDK approvals, lifecycle, and packaging

* refactor(anthropic): own SDK process trees and streamline runtime

* fix(anthropic): repair rebased packaging and legacy test fixtures
2026-08-24 01:59:16 -07:00
Peter Steinberger 53dcaaedec feat(gateway): generic operator roles for non-maintainer access (#128548)
* feat(gateway): generic operator roles for non-maintainer access

Adds gateway.roles: named role bundles over a closed capability set —
sessions.others (none/view/suggest/write), an agents allowlist, and an
operator-scope ceiling. Roles are person-level (additive user_profiles.role
column, SQLite stays at v9); users.setRole (admin-only) assigns them. With
no gateway.roles config, behavior is unchanged for solo deployments.

Enforcement is deny-by-default from a host-minted actor identity
(system vs operator+profileId on server-only client.internal, never
accepted from the wire) and covers every entry point: WS RPCs, OpenAI-compat
and Responses HTTP, tools invocation, cron, questions, usage, task
suggestions, session catalog/sharing/reads. The agents allowlist gates both
session creation and run-start on existing sessions. Subagent completion
announce and descendant wake mint explicit system authority so role
boundaries never silently drop parent notifications.

The enforcement surface is expressed through a narrow policy vocabulary
(operatorSessionCap, hasOperatorBoundary, authorizeSessionSharing) rather
than per-handler policy internals.

* fix(gateway): heal PR CI after rebase onto main

- Break import cycles: extract GatewayOperatorRoleActor leaf contract; merge
  session-group-mutation-targets into session-sharing-target-input.
- Split sessions-suggestions.test.ts (max-lines) into a visibility suite.
- Add users.setRole to the 2026.8 train registry test and regenerate the
  Kotlin protocol client.
- Startup UNAVAILABLE gating now precedes session authorization: session
  stores are not loaded during startup, so authorization reads would deny
  with a misleading non-retryable error.
- sessions.assignOwner keeps its documented visibility-authorized contract
  when no operator role caps the caller; view/suggest-capped roles still
  cannot reassign foreign session ownership.
- Test stubs updated for main's socket readyState guard (#128144) and the
  system-authority arg on channel-native resets.

* test(gateway): chat.send pending-profile dispatch carries its required session target

chat.send requires a non-empty sessionKey at the protocol level; the mutation
pipeline now rejects targetless frames before profile-dependent dispatch, so
the pending-profile test must send a realistic frame.
2026-08-24 01:20:03 -07:00