* 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.
* 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>
* fix(android): apply the gateway accent color to the Claw design theme
The Android app fetched config.ui.seamColor into a StateFlow that no
Compose surface consumed, so gateway accents were silently dropped. The
accent now resolves with Control UI precedence (ui.prefs.accent over
ui.seamColor) and overrides the Claw accent/accentSoft/accentBorder
tokens in ShellScreen and OnboardingFlow; Material You system chrome is
untouched. With no gateway accent set the hardcoded ClawDark/ClawLight
palettes apply unchanged, replacing two mismatched hardcoded defaults.
* fix(android): invoke branding refresh on connect and match gateway accent precedence
ClawSweeper findings: refreshBrandingFromGateway had no call site (the
accent state stayed null), and a present non-string user accent must not
fall through to seamColor per the gateway's nullish selection. The
refresh now runs in the post-connection coroutine alongside wake-word
and exec-approval refreshes.
* 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.