Commit Graph

11645 Commits

Author SHA1 Message Date
Dallin Romney 1baebbfd39 Merge main into feat/talk-open-session 2026-08-26 12:30:35 -07:00
Peter Steinberger 87b56458a1 feat(plugins): inbound IMAP email trigger (#130230)
* feat(plugin-sdk): add in-process hook agent dispatch seam

* feat(plugins): add imap inbound email trigger plugin

* fix(imap): coalesce sweep wakeups and drop test-only gate exports

* docs(imap): fix agents bindings command invocation

* fix(gateway): guard optional bound context in plugin hook dispatch

Post-rebase drift made resolveBoundGatewayContext possibly-undefined; the
plugin hook dispatch path invoked it unconditionally. Use the optional-call
guard consistent with sibling runtime accessors.

* fix(imap): bump mailparser to 3.9.16 off vulnerable deepmerge-ts

mailparser 3.9.15 pinned html-to-text@10.0.0 -> deepmerge-ts@7.1.6 (GHSA
HIGH, stack exhaustion, <8.0.0). 3.9.16 pins html-to-text@10.0.1 ->
deepmerge-ts@8.0.2, clearing the production audit gate.

* refactor(gateway): extract plugin hooks runtime to keep server-plugins under max-lines

The dispatchHookAgentTurn seam pushed server-plugins.ts to 702 effective lines
(cap 700). Extract createGatewayHooksRuntime into the sibling runtime-helpers
module, mirroring createGatewayNodesRuntime/createGatewaySubagentRuntime; no
suppression added.

* docs(secrets): register imap plugin password in secretref credential matrix

The bundled imap plugin declares a secretInput (accounts.*.password); regenerate
the user-supplied credentials matrix and surface list so the target-registry docs
sync test stays green.
2026-08-26 12:24:02 -07:00
Peter Steinberger 99a02bf115 feat(approvals): typed approval scope summaries on channel cards (#130116)
* feat(approvals): typed approval scope summaries on channel cards

Approval owners can attach a closed ApprovalScope union (message-send,
payment, external-post) describing an action's blast radius. The gateway
sanitizes it once at the producer boundary, the core view model renders a
Scope metadata row so Slack/Discord/Google Chat cards show it unchanged,
shared text builders cover Telegram/WhatsApp/Signal/iMessage/Matrix, and
the durable presentation carries it additively for operator surfaces.
Scope is display-only, never authorization; missing scope keeps today's
cards.

* fix(approvals): emit native ApprovalScope union and clamp recipient previews

Name the three scope variants as registered protocol schemas so the Swift
generator emits the ApprovalScope discriminated union the presentation
structs reference, and commit the regenerated GatewayModels.swift. Clamp
recipient previews to the declared recipientCount at the sanitize boundary
so a count of 1 with 2 previews can no longer render inconsistently.
Addresses both ClawSweeper findings on #130116.

* refactor(approvals): extract text sanitizer to break the exec-approvals import cycle

check:architecture flagged approval-scope joining the exec-approvals SCC
through exec-approval-command-display. Move the self-contained display
sanitizer into a leaf module (exec-approval-text-sanitize) with no
exec-approvals imports and migrate all sanitize importers; command-display
keeps only the payload-typed command/preview resolver.

* chore(plugin-sdk): ratchet public surface budgets down after sanitizer extraction

The approval display sanitizers left the publicly reachable SDK graph when
they moved to the exec-approval-text-sanitize leaf: exports 4343 -> 4338,
callable exports 2582 -> 2578. Shrink-only budget pin.
2026-08-26 12:13:49 -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 6287056d26 docs(secrets): make the credential-request tool discoverable and warn about empty hosts (#130299)
The tool page shipped with the feature but nothing pointed at it: the
tools index still listed only ask_user under human input, and neither the
secrets CLI page nor ask-user mentioned it. Add those three references.

The page also missed the trap the card invites: the allowed-hosts field
is editable, and clearing it stores a secret that can never be
substituted at egress, so the request looks successful and the credential
is unusable. Warn where the operator edits it, say what a skipped or
expired prompt means for the agent, and drop wording left over from when
requests could ask for an env value.
2026-08-26 11:56:12 -07:00
Peter Steinberger 08a42a561c feat(control-ui): add the Tide, Beacon, and Phosphor themes (#130232)
* chore(control-ui): vendor JetBrains Mono and Atkinson Hyperlegible Next

Adds the woff2 faces for the Beacon and Phosphor themes plus the stylesheets
that declare them. Both families are SIL Open Font License 1.1 and ship
unmodified with their upstream license text, as the license requires.

Atkinson Hyperlegible Next was drawn by the Braille Institute for low vision:
its letterforms disambiguate the pairs that blur first at low acuity (I/l/1,
O/0, rn/m), which is why it belongs to the accessibility theme specifically.

base.css has always named JetBrains Mono first in --mono, but nothing shipped
it, so code blocks rendered in it only for people who happened to have it
installed locally. Phosphor makes that deterministic.

Latin and latin-ext subsets only, 195 KB. Served from the gateway rather than a
font CDN so font-src 'self' holds, no third-party request happens on load, and
an offline or LAN-only gateway still renders correctly. The url() references are
relative to their stylesheet so they follow a configured Control UI mount.

* feat(control-ui): add the Tide, Beacon, and Phosphor themes

Three built-in themes, each filling a gap the existing four share rather than
adding a fifth variation on them. Every shipped accent today is warm — coral,
crimson, chocolate, clay — and the only cool token anywhere is Claw's teal
secondary.

Tide is the cool one: steel cyan on deep slate, deep teal-blue on cool paper.
It deliberately ships no webfonts, so it costs nothing beyond its tokens.

Beacon is the accessibility one. It targets WCAG AAA (7:1) rather than the AA
4.5:1 floor the other themes hold, for low vision, direct sunlight, projectors,
and poor panels, and sets everything in Atkinson Hyperlegible Next. Reaching AAA
forced one deliberate break from the status-token convention: elsewhere each
--x-subtle is --x at 8%, but a label on a tint of its own hue caps achievable
contrast because the tint pulls the background toward the text. Beacon tints
neutrally and lets the label carry the contrast. Its focus ring is opaque and
3px for the same reason — a translucent ring is the first thing to vanish on a
bad panel. The contrast guardrail now enforces 7:1 for Beacon specifically, so a
later palette edit cannot quietly demote it to an ordinary dark theme.

Phosphor is the terminal one: phosphor green on green-cast black with the whole
surface, chat prose included, in JetBrains Mono. Monospace prose is a character
choice for an operator console, not a readability claim, which is why it is
opt-in.

Every palette was solved against the real guardrail before any CSS was written.
Worst text pairing per theme: Tide 6.12:1 dark / 4.63:1 light, Beacon 10.29:1 /
9.72:1, Phosphor 6.69:1 / 4.76:1. Worst status label on its own tint: Beacon
7.05:1 / 7.02:1, the others above 4.6:1.

Budgets: the startup CSS ceiling moves 45 -> 47 KiB and the JS ratchet baseline
moves 523 B. Every built-in theme's tokens ship in the startup sheet, so each
new theme costs ~0.5 KiB gzip whether or not anyone selects it. Moving per-theme
palettes to lazily linked stylesheets (as theme webfonts already are) would take
this back under the old ceiling, but it needs a first-paint story first: a late
palette flashes default colors where a late font only swaps.

* fix(control-ui): use Beacon's destructive foreground in chat confirmations

The chat confirmation button paints from --danger under --media-foreground, a
theme-invariant white, and each dark palette opts out of that pairing through a
selector list in chat/grouped.css. Beacon defines a deliberately light --danger
(#ffabab, chosen so status labels clear AAA) but was missing from that list, so
its confirm button rendered white on pale pink at 1.80:1 — on the theme whose
entire promise is contrast.

Adds Beacon to the list, which routes it to --destructive over
--destructive-foreground at 11.67:1.

Also adds a guardrail for the bug class, since the selector list has to be
extended by hand for every new dark theme and I have now missed it once. The
test reads membership back out of grouped.css rather than restating it, resolves
each theme's effective (background, color) pair, and holds it to that theme's
floor. It reproduces this defect at 1.80:1 against the pre-fix stylesheet.

Reported by ClawSweeper review on #130232.
2026-08-26 11:47:01 -07:00
Peter Steinberger 8e39f58365 docs(memory): add memory provenance and deletion concepts page (#130278)
* docs(memory): add memory provenance and deletion concepts page

New concepts/memory-provenance page tells the operator policy story end to end: recorded session lineage through consolidation, the admission policy and its pipeline-only boundary, memory forget guarantees (whole-entry purge, derived-artifact sweep, forgotten tombstones) and reported boundaries (transcripts, curated writes, paraphrased prose), plus the person/source purge workflow. Cross-linked from memory architecture, dreaming, builtin, overview, CLI, and config reference; registered in docs navigation.

* docs(i18n): add glossary terms for the memory provenance page

* docs(memory): distinguish curated entries from missing lineage in the provenance page
2026-08-26 11:36:39 -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
Josh Lehman 2c9782bad4 fix(skills): patch skills above the read budget (#128871)
* fix(skills): allow bounded patches above read budget

* fix(skills): close bounded patch review gaps
2026-08-26 18:28:39 +00: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
Peter Steinberger 9d9dc31275 feat(memory): session provenance, admission policy, and openclaw memory forget (#130151)
* feat(state): add memory entry origins and session tombstone tables with session-aware write provenance

Additive same-version per-agent tables (memory_entry_origins, memory_session_tombstones) declared canonically with lazy idempotent ensure; read-only agent DB access exposed through the SDK; memory-write observers and the session-memory hook record the authoring session; archived narrative transcripts classified by authoritative run identity.

* feat(memory): admission policy, entry provenance, and openclaw memory forget

memoryPolicy.excludeSessions keeps configured sources out of the dreaming pipeline with recorded exclusions; promotion writes are marker-addressable and carry per-session origins through consolidation; memory forget purges whole entries and every derived artifact (corpus lines, diary quotes, index/FTS/vec/cache, short-term state, backups), reports curated agent writes from transcripts, and tombstones purged sessions so ingestion, backfill, and transcript indexing never re-admit them.

* docs(memory): document admission policy, memory forget, and provenance boundaries

* fix(memory): open the vec probe through openNodeSqliteDatabase

Kysely guardrail forbids direct node:sqlite constructors in production; the in-memory extension probe now uses the canonical opener.

* fix(memory): resolve archived and explicit sessions in memory forget

Explicit --session selectors union live session windows, transcript archives, and exact unresolved ids so an operator-named session always purges and tombstones instead of silently no-oping after lifecycle archival; reports carry per-session resolution sources. ClawSweeper P1 on #130151.

* fix(memory): reconcile consolidation origins for every workspace agent

Shared workspaces record entry origins per source agent; consolidation now re-keys and prunes origins in each participating agent's database (membership from the authoritative dreaming workspace mapping), so a non-owner agent's later memory forget still finds the live consolidated entry. ClawSweeper re-review P1 on #130151.
2026-08-26 10:57:04 -07:00
shaoohh db4d42ead2 fix(browser): warn when Chrome extension version drifts after upgrades (#119641)
* fix(browser): detect Chrome extension version drift

* fix(browser): diagnose paired Chrome extension version drift

Co-authored-by: shaoohh <150606856+shaoohh@users.noreply.github.com>

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-08-26 10:42:57 -07:00
Harjoth Khara 3be7baa1a3 fix(macos): timestamp hash-pinned Developer ID signatures (#118989)
Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-08-26 10:39:41 -07:00
Josh Avant 424521a3d3 fix(file-transfer): keep remembered approvals exact (#129762)
* fix(file-transfer): bind standing approvals exactly

* fix(file-transfer): keep validator error lint-safe

* fix(file-transfer): bind directory listing target before I/O

* fix(file-transfer): keep dir list preflight type stable

* test(file-transfer): split directory listing policy coverage

* fix(file-transfer): bind final node effects

* fix(file-transfer): close migration and archive gaps

* fix(file-transfer): bind standing grants before preflight

* fix(file-transfer): bind directory listings to checked target

* fix(file-transfer): bind final effects to filesystem identity

* test(file-transfer): prove exact approval transport

* fix(file-transfer): keep reapproval prompts path-scoped

* chore(config): refresh generated baselines
2026-08-26 09:38:50 -07:00
Vyctor H. Brzezowski 60ef0d169f fix(ui): keep progress in main chat when side chat opens (#130109)
* fix(ui): keep progress in main chat when side chat opens

* fix(ui): align progress placement with current chat
2026-08-26 12:55:14 -03: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
Peter Steinberger ad1e946c7a feat(a2a): add A2A v1.0 agent-interop channel plugin (#130008)
* feat(a2a): add A2A v1.0 agent-interop channel plugin

Adds `a2a` as a bundled, default-off channel plugin so A2A-compliant agents
(Hermes Agent, LangChain, CrewAI, Google ADK) can discover an OpenClaw gateway
and send it tasks, and so OpenClaw can address configured peers.

Inbound HTTP (`/.well-known/agent-card.json`, `/a2a/v1`) runs through the normal
channel ingress path, so peer messages inherit allowlist admission, provenance
framing, and session routing. The agent reply returns through the channel
delivery callback and resolves the A2A task with its artifact, correlated per
(peer, contextId) FIFO so concurrent sends cannot cross-talk.

Wire format follows the canonical v1.0.0 spec sources (a2aproject/A2A
specification/a2a.proto): PascalCase SendMessage/GetTask/CancelTask,
supportedInterfaces[] rather than the 0.3-era top-level url/protocolVersion,
member-presence Part oneof, TASK_STATE_*/ROLE_* enums, no Task.kind. The 0.3
dotted method names are accepted as explicit compat aliases because shipped
Hermes-generation peers still send them.

Security: per-peer bearer tokens compared in constant time, per-peer sliding
window rate limit, 1 MiB body cap, 64 KiB inbound text cap, tasks scoped to
their owning peer, outbound redirect: "error", and no inbound-controlled target
URLs. With no peers configured the channel stays unconfigured and registers no
routes.

Live-verified against a real gateway on an isolated state dir with a mock
provider: 31/31 proofs, including the agent reply round-tripping into the task
artifact. That run caught two defects fixed here:

- The agent card read `cfg.agents.list` directly, so gateways configured with
  the canonical `agents.entries` roster advertised zero skills. Now uses
  listAgentIds/resolveAgentConfig, which read either roster shape.
- `returnImmediately` dispatches inherited the HTTP request's released work-
  admission root and every async task failed as GatewayDrainingError. Now
  reserves an independent root via runDetachedWebhookWork, matching sms/line/zalo.

* fix(a2a): route outbound sends through the SSRF guard and land setup metadata

CI on the first push surfaced four real defects that local `check:changed` did
not cover (it runs no oxlint or typecheck lane):

- Outbound peer sends used raw `fetch()`, tripping the channel/plugin
  no-raw-fetch boundary. They now go through `fetchWithSsrFGuard` with
  `maxRedirects: 0`, so A2A egress carries the same SSRF policy as every other
  plugin call and a redirect cannot re-target a delivered task.
- The channel declared an empty setup contract while its package manifest
  declared none, so the bundled-channel shape contract failed. The wizard now
  collects `advertisedUrl` plus a first peer name/token pair, which is the
  minimum that leaves A2A actually configured (it stays unconfigured until one
  peer credential exists), and the package manifest mirrors that metadata.
- Six lint errors in the test files: base-to-string on `BodyInit` bodies, an
  unused mock, and a shadowed `options` parameter. Request bodies now go through
  a typed reader that asserts the serialized-string contract before parsing.
- A card assertion referenced `capabilities` without declaring it on the local
  response type, failing test typecheck.

Outbound redirect handling moves from `redirect: "error"` to the guard's
`manual` inspection; the adapter tests assert the new shape.

* fix(a2a): drop route replaceExisting, accept configWrites, register runtime api

CI after the interop run surfaced four more gates:

- `registerPluginHttpRoute` used `replaceExisting: true`, tripping the
  GHSA-RQP8-Q22P-5J9Q rule: a duplicate path can silently replace another
  account's handler. A2A owns fixed global paths on a single account, so a
  duplicate registration means a stale or conflicting owner. It now fails loudly
  through `throwOnFailure` instead.
- The channel schema rejected `channels.a2a.configWrites`, which every bundled
  channel must accept; added to the zod schema, the manifest schema, and the
  config type, then regenerated the channel metadata.
- The new `input` assertion in the setup adapter needed a `// SAFETY:` line for
  the assertion ratchet.
- `extensions/a2a/runtime-api.ts` needed registering in the runtime-api
  classification list, like every other bundled channel barrel.

Also repairs a pre-existing incomplete `vi.mock` factory for `../infra/fs-safe.js`
in the agent-delete suites. Both pass in isolation, but the shared-worker lane
lets a sibling importer reach `readLocalFileSafely`, which the partial factory
never exported; adding this channel changed shard composition and surfaced it.
The factory now spreads the real module and overrides only `movePathToTrash`.

* chore(a2a): refresh config baseline for the configWrites surface

* chore(tests): adopt main's fs-safe mock repair for the agent-delete suites

Main landed the same importOriginal spread for the shared-worker mock-defeat
while this branch carried an equivalent fix; take main's version verbatim so the
two do not conflict.

* fix(a2a): isolate peer sessions and stop faking task cancellation

Addresses both ClawSweeper P1 findings.

Peer sessions: inbound routing never passed `dmScope`, so it fell back to
`session.dmScope ?? "main"` and every authenticated peer converged on
`agent:main:main` under default config. Untrusted remote content was joining the
operator's own session, and peers shared conversation history. A2A now pins
`per-account-channel-peer`; the peer id already embeds the A2A contextId, so each
peer+context pair gets its own session regardless of global session config.

Cancellation: `CancelTask` marked the task `TASK_STATE_CANCELED` and discarded
the late reply, but the dispatched agent run kept going and could still use
tools. The peer was told the work stopped when it had not. There is no
plugin-facing seam to abort a live run, so cancellation is refused with -32004
instead of acknowledged, and the store's cancel path plus its canceled-delivery
tombstone are deleted rather than left as a lying terminal state. Documented as
an explicit limitation.

Live-verified on a real gateway: 34/34 proofs, including three distinct
`agent:main:a2a:default:direct:<peer>:<context>` sessions with zero traffic in
`agent:main:main`, and both cancel spellings refused while the task stays
WORKING. Re-ran the official a2a-sdk 1.1.2 Docker interop afterwards: 9/9.
2026-08-26 08:04:53 -07:00
Jesse Merhi f8550bc11d fix(ui): support slash commands and skills inside normal drafts (#122425)
* feat(ui): support slash commands inside normal drafts

* fix(ui): preserve inline skill colon completion

* refactor(ui): extract slash menu DOM ids

* fix(ui): account for Absolutely theme startup CSS

* fix(ui): sync typing after inline command dispatch

* test(commands): preserve fs-safe exports in agent delete mocks
2026-08-26 14:27:32 +00:00
Peter Steinberger bb66c4c288 fix(ios): surface Voice Wake transcript delivery failures (#130139) 2026-08-26 06:59:55 -07:00
Peter Steinberger 42f2aa6a15 feat: offer to turn a repeated job into a scheduled automation (#130120)
When someone asks for substantially the same job a third time, the agent now
does the work and then offers to promote it into a recurring automation,
instead of waiting for the user to think of scheduling it themselves.

Promotion inherits a run the user has already read: the agent restates the
schedule and task in plain words, creates the job with delivery defaulting to
the requesting channel/thread, and immediately force-runs it once as a visible
test so real output lands before the first scheduled occurrence. A failed test
is reported and the job removed.

The guidance lives in the system prompt gated on the automations tool, not in
the tool description: the repeat is noticed during ordinary work, not while the
model is reading that tool's schema, so guidance buried in the schema would
never fire. Gating keeps it out of contexts where the tool is unavailable.

Jobs are created enabled rather than disabled-pending-approval. The scheduler
already supervises enabled jobs through failure alerts and auto-disable, while
nothing watches a disabled one; a job left disabled awaiting a confirmation
that never arrives is hidden from the default list and fails silently.

No new detection engine and no new storage: repetition comes from the
conversation and duplicates are checked with the existing automations list.
2026-08-26 06:31:55 -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
Ayaan Zaidi 6a8278d822 feat(cron): schedule the skill collection review as a system-owned job (#130030)
* feat(skills): expose the collection review as a single cron-driven run

The weekly Skill Workshop collection review lived in a bespoke gateway
maintenance loop with its own interval, due-state check, and gateway admission
wrapper. Replace it with `runSkillCollectionReviewForAgent`, one run that keeps
the workspace claim, identity check, and status recording and reports
`{ status, summary }` to its caller. The cron timer that will own the schedule
already runs under gateway root work admission, so the extra wrapper and its
drain test go away.

* feat(cron): schedule the skill collection review as a system-owned job

Fold the weekly Skill Workshop review into cron the same way the heartbeat
monitor is: a `skillCollectionReview` payload kind, one system-owned job per
workspace (first agent of each canonical workspace) reconciled at gateway
startup, enabled only when the workshop runs in `auto` mode, on a 7-day
`every` schedule with the heartbeat phase spread. The timer invokes the
review runner directly; cron clients cannot create, patch, or remove
system-owned payloads, and the job shows up in `openclaw cron list`.

* fix(cron): warn when skill reviews are disabled

* fix(cron): preserve skill review failure details

* fix(cron): narrow serialized payload variants

* test(cron): tighten skill review coverage

* fix(gateway): reconcile skill reviews on config reload

* fix(cron): fence revoked skill reviews

* fix(cron): satisfy system payload type coverage

* fix(ui): protect system-owned cron jobs

* test(gateway): prove review revocation fence

* fix(gateway): revoke skill reviews before config publish
2026-08-26 18:43:54 +05:30
Peter Steinberger 2e017baa7b docs(plan): record CUA production vertical, node-channel fix, and channel schema repair (#130117) 2026-08-26 06:13:15 -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
RoboClaw 44d60a29e6 fix: preserve unread reminder for open sessions (#129386)
* fix: preserve manual unread markers in open sessions

* fix: distinguish explicit session reads

* fix(ui): gate unread contract on gateway capability

* perf(ui): keep server capabilities out of startup bundle

* test(gateway): keep agent fixtures roster-consistent

* fix(sessions): preserve legacy read compatibility

* test(gateway): type agent fixture configs

* fix(ui): remove unread gateway fallback

Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>

* test(infra): avoid fixed SSH tunnel port

* fix(ui): acknowledge unread after history commit

* docs: clarify unread upgrade boundary

* test(ui): drive mobile session menu by tap

* fix(ios): remove stale read reconciliation call

---------

Co-authored-by: roboclaw-bot <309084314+roboclaw-bot@users.noreply.github.com>
Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
2026-08-26 05:04:39 -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 49d916e8e9 fix(memory): preserve session recall when upgrading from QMD (#130016)
* fix(memory): preserve QMD session search across builtin migration

* fix(memory): migrate agent defaults to canonical memory owner
2026-08-26 03:18:42 -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 ea78c2488b feat(msteams): native Adaptive Card approve/deny for exec and plugin approvals (#129997)
* feat(msteams): deliver native Adaptive Card approvals

Exec and gateway plugin approvals now render as Adaptive Cards in Microsoft
Teams with token-bound approve/deny actions, mirroring the Google Chat card
pattern. Card submits are intercepted before message-text serialization,
authorized against channels.msteams.allowFrom/defaultTo AAD object IDs via
the existing approval auth, claimed once, resolved over the gateway, and the
card is updated in place to its terminal state. Native delivery gates on the
top-level approvals.exec/approvals.plugin forwarding config; the /approve
text fallback remains.

* chore(msteams): shrink assertion-safety baseline after send.ts cast removal

* chore(msteams): record approval-native adapter seam in chained-assertion ledger

* fix(msteams): surface a text approval fallback when card delivery fails

When the native route suppressed the local text prompt, a failed Adaptive
Card send only logged, leaving the pending approval invisible. On delivery
error, send a plain-text /approve prompt to the planned target so the
operator always has a visible approval path. Addresses the ClawSweeper P1
on #129997 channel-locally; #130040 tracks the shared-boundary fix.
2026-08-26 02:56:22 -07:00
Peter Steinberger 22e892a4ea fix(secrets): preserve canonical configuration path identity (#128318)
* fix(secrets): scope manifest-owned credentials

* fix(secrets): isolate plugin owner domains

* fix(secrets): preserve manifest target path identity

* fix(config): preserve typed secret target paths

* fix(secrets): preserve dotted header target paths

* test(secrets): expect canonical auth profile paths

* test(secrets): prove plugin owner authority chain

* fix(secrets): preserve canonical configuration path identity

* docs(plugins): clarify canonical secret paths and route ownership

* fix(cli): keep configuration path parsing out of bootstrap dependencies
2026-08-26 02:52:38 -07:00
Vincent Koc ce54cae86d fix(ci): stop source-only PR fallback requiring generated locales (#120973)
* fix(ci): scope native i18n parity for release gates

Punchcard-Session: amber-workshop-workshop-36

* fix(ci): preserve native locale scope for PR release gates

Co-authored-by: Vincent Koc <vincentkoc@ieee.org>

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-08-26 02:50:48 -07:00
Ayaan Zaidi a901d33184 fix(skills): share the foreground prompt prefix with the experience review (#130013)
The detached Skill Workshop experience review rebuilt its system prompt and tool catalog from a different context than the foreground turn, so every review missed the prompt cache. Native harnesses (embedded, Codex, Copilot) now hand the review the same foreground prompt context via buildEmbeddedForegroundPromptContext; the review reuses the foreground prefix and gates execution to skill_workshop while keeping the catalog identical. Reviews without a foreground prompt (CLI hook contexts) are skipped.
2026-08-26 09:39:54 +00:00
sunlit-deng 87b93b5303 fix(browser): bound batch action files (#115882)
* fix(browser): bound batch action files

* fix(browser): preserve legacy file input while bounding batches

Co-authored-by: sunlit-deng <yang.jiajun1@xydigit.com>

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-08-26 02:37:42 -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 1605dbd3ef chore(deps): refresh dependencies after seven-day cooldown (#129941)
* chore(deps): refresh dependencies after cooldown

* chore(deps): refresh sherpa-onnx runtime to v1.13.6

* test(ios): refresh Fastlane pin expectations
2026-08-26 01:37:37 -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 9f9ad63246 fix: honor session policy for widget approvals (#129970) 2026-08-26 01:34:24 -07:00
Harjoth Khara 7c6040550a ci(macos): budget macos-swift by runner so fork PRs finish (#119017)
* ci(macos): budget macos-swift by runner so fork PRs finish

runs-on falls back to hosted macos-26 for several cases: workflow_dispatch,
run_attempt > 1, fork pull requests, and (as of the runner-backend circuit
breaker landed on main the same day this PR was authored) breaker-routed
GitHub-hosted fallback. timeout-minutes did not cover the fork-PR case, so
fork PRs ran on the slow hosted runner with the Blacksmith-tuned 20-minute
budget.

Measured on PR #118989 (a fork PR): macos-swift was cancelled after 20m25s,
killed mid-compile at step 1365/1416. No test executed, and the log has no
swift compile error - only 'The operation was canceled.' ci-gate then fails
because it lists macos-swift as required, so the PR reads red for a reason
unrelated to its diff, and a contributor cannot rerun it.

Make the budget follow the runner instead of the trigger: every hosted path
gets 30 (folded into the same budget the circuit breaker's own hosted-fallback
timeout extension already established for this job), so this doesn't
reintroduce a second, competing hosted-timeout value. Blacksmith paths keep
20 unchanged. Replaces the single pinned-string guard with a table-driven
test covering runs-on and timeout-minutes together across every trigger
context that can route to a hosted runner.

* chore: refresh PR head (keep open for maintainer review)

* ci(macos): track main's author-association runner routing

Main now routes macos-swift by pull_request author_association rather than
fork-ness, so the timeout predicate and its guard scenarios follow it.

* ci(macos): tighten hosted runner budget coverage

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

* docs(ci): document hosted macOS budgets without merge conflicts

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

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-08-26 01:12:07 -07:00
Peter Steinberger 80ecc7c8b5 feat(control-ui): add the Absolutely theme with self-hosted typography (#129885)
* chore(control-ui): vendor Space Grotesk and Lora webfont subsets

Adds the woff2 faces the Absolutely theme paints in, plus the stylesheet that
declares them. Both families are SIL Open Font License 1.1 and ship unmodified,
with their upstream license text beside the files as the license requires.

Latin and latin-ext subsets only, 184 KB total. Serving these from the gateway
rather than a font CDN keeps font-src 'self' intact, avoids a third-party
request on every load, and works on an offline or LAN-only gateway.

The stylesheet is a static asset rather than a bundled import: it is linked at
runtime only while a theme that declares faces is active, so it stays out of
the startup CSS budget and out of every other theme's load.

* feat(control-ui): add the Absolutely theme with self-hosted typography

Adds a fourth built-in Control UI theme: terracotta clay on warm graphite in
dark mode, burnt clay on ivory in light mode, with its own typography — Space
Grotesk for chrome and Lora for chat prose.

Typography is theme-owned rather than global. A new --font-chat token defaults
to --font-body, so every other theme is byte-for-byte unchanged, and the face
declarations live in a static stylesheet that the theme applier links only
while a theme that declares fonts is active. Themes without declared faces
issue no font request at all.

The faces themselves are vendored in the preceding commit.

Also fixes two defects found while building it:
- Theme cards overflowed their border on long labels: the grid's 1fr label
  track floors at min-content, so a long name pushed the trailing column past
  the card edge. The track is now minmax(0, 1fr) with an ellipsis, which also
  covers imported tweakcn labels. The selected-state checkmark that made the
  overflow visible is gone; the accent border, tint, and ring already carry
  selection, and aria-pressed owns the semantics.
- index.html's first-paint bootstrap only knew claw/knot/dash, so any new theme
  flashed the default palette before the app corrected it. It now resolves the
  new theme and has a matching pre-paint background, with mount-fallback
  coverage for both modes.

Startup JS grows 198 B for the theme wiring, appearance option, locale
strings, and font loader, which fits the existing ratchet tolerance, so no
budget baseline moves. Startup CSS stays within its 45 KiB ceiling, which is
why the faces are a linked static asset rather than a bundled import.

* style(control-ui): format the vendored font stylesheet

The generated @font-face block kept its upstream single-line unicode-range
declarations, which oxfmt wraps. Repo-wide `pnpm format:check` is clean.

* test(control-ui): type the chat runId in the typography e2e

Reuses the chat-flow requireRecord/requireString helpers instead of asserting
the params shape, so runId is a real string rather than an unknown coerced by
String() (oxlint no-base-to-string).

* fix(control-ui): resolve theme fonts against the configured UI mount

The theme font stylesheet and its faces used root-absolute URLs, so a gateway
served under a configured Control UI base path (gateway.controlUi.basePath,
for example /openclaw) fetched them from the wrong origin path. Both requests
404ed there and Absolutely silently fell back to system fonts while its palette
still applied, which reads as "the theme looks a bit off" rather than as a
broken asset.

The link href now goes through inferControlUiPublicAssetPath, the same helper
plugin art and provider icons already use, and the stylesheet's own url()
references are relative to it, so both levels follow the mount without the
loader knowing the base path. Adds an e2e case under /openclaw asserting the
resolved href and that the browser fetches below the mount; it fails on the
previous code with the root-absolute path.

Reported by ClawSweeper review on #129885.

* test(slack): stop racing the aborted read request in the Web API bound test

`bounds dedicated reads without timing out shared clients` asserted the mock
server received exactly two requests, but the read client aborts at 20ms and
the server records on arrival, so whether the aborted request lands first is a
race. It failed on a loaded CI runner with 1 received.

The bound is still asserted directly (read rejects, shared resolves); the count
now only requires the shared client's arrival, and checks its token so the
dedicated client is still proven not to have carried the call.

Unrelated to this PR's theme work; fixed here rather than left red.
2026-08-26 01:06:41 -07:00
Peter Steinberger 4bd8859126 fix(skills): record skill usage again and retire dead curator tables (#129769)
* fix(scripts): see SAFETY comments after template substitutions

The assertion ratchet scanned each file with a raw ts.createScanner, which
reads the `}` closing a template-literal substitution as a block close. The
scanner desynced there, so every `// SAFETY:` comment after a file's first
`${...}` was invisible and its annotated assertions were counted as bare.

Track substitution brace depth and rescan the closing brace the way the parser
does. The fix uncovers already-annotated assertions in eight files, so the
baseline shrinks accordingly.

* fix(skills): record skill usage again and retire dead curator tables

Skill lifecycle curation shipped in 2026.7.1 with two producers: a trusted
`skill.used` consumer writing `skill_usage`, and a daily sweep aging skills
into `skill_lifecycle`. The weekly collection review (#121653) replaced that
policy and deleted both producers, but left every reader in place. Since then
`skill_usage` has had no writer, so curator status reported `lastUsedAtMs:
null` and `useCount: 0` forever, and `skill_lifecycle` had no writer either, so
pin/unpin/restore either threw "not found" on fresh installs or, worse,
"succeeded" on upgraded ones while gating nothing at all.
`skill_workshop_proposal_origin_runs` was never read in any revision; proposal
provenance is authoritative in `record_json`.

Restore the usage producer at its owner and retire what has no owner:

- `skill.used` events populate `skill_usage` again, registered with the
  collection-review maintenance it belongs beside. Curator status derives
  curated skills from applied Workshop create proposals and reports real usage.
- Weekly review receives bounded `useCount` / `lastUsedDaysAgo` evidence, with
  prompt text stating usage supports keeping a procedure and never alone
  justifies a drop.
- State schema 10 drops `skill_lifecycle` and
  `skill_workshop_proposal_origin_runs`. Previously archived skills return to
  the active collection, where review judges them by content; the migration
  logs how many. Reconcile now clears usage only for skills it actually drops.
- `skills.curator.pin`/`unpin`/`restore` stay registered for existing clients
  but fail with an explicit retirement message instead of silently doing
  nothing.

Retirement code moves to openclaw-state-db-table-retirements.ts to keep the
schema-repair module under max-lines; that split is a pure relocation.

Production delta is +23 raw: about -46 for the change itself, +44 for the file
split, +25 for the ratchet fix in the previous commit.
2026-08-26 01:06:06 -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 0dfd87ebe7 fix(ollama): skip local checks for hosted web search (#129833)
* fix(ollama): skip local checks for hosted web search

* fix(ollama): explain hosted search API key requirements

* docs(ollama): separate hosted and local search setup
2026-08-26 00:37:35 -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 8912f7e288 perf(computer): omit duplicate consecutive screenshots from model context (#129924)
Tight computer-use loops (click, screenshot, click, screenshot) carried a
full ~1200px image every step even when the screen never changed. Only
age-based pruning existed, so those duplicate bytes sat in model context
for three completed turns each.

When a new capture's sanitized image identity matches the frame identity
already tracked for the same node and screen in the current context epoch,
the tool now returns its normal text metadata plus an explicit "screen
unchanged since previous frame (frameId ...)" note and omits the image
block. The result is append-only; no prior transcript message is rewritten.

Coordinate-frame ownership is preserved: a dedup hit deliberately leaves
contextEpoch.frameToolCallId/frameImageIdentity pointing at the original
image-bearing tool result, so invalidateComputerFrameIfMissing keeps
guarding the pixels the model actually holds. Once history pruning drops
that image, the epoch bumps, coordinates expire as before, and the next
screenshot delivers full pixels again. Only the node-issued displayFrameId
advances on a hit. Dedup is therefore gated on context-epoch tracking:
without it, an unchanged-screen note could reference an already-pruned
image, so those screenshots always deliver pixels.

Screenshots now stay bound until replacement pixels are verified, so
capture failures, act rejections, and failed follow-up screenshots demote
the frame explicitly instead of relying on an unconditional pre-capture
reset.

Provider observation images (window/browser observations) and the browser
plugin's screenshot tool are intentionally excluded: neither has
context-presence tracking.
2026-08-25 23:51:16 -07:00