## What Problem This Solves
Fixes an issue where paired dashboard operators could not save or import a Nostr profile when their browser held only its device credential. Save & Publish and Import from Relays each returned HTTP 401, then displayed `e?.trim is not a function`.
The dashboard handoff in `docs/web/control-ui/connect-and-pair.md`, paired-operator Control UI read authorization, and profile editor in `docs/channels/nostr.md` establish this contract. The defect was reproduced on pinned main `3b88500581` through the real browser and Gateway.
Related: #138975 preserves recovery when a second credential exists; this fixes the device-only case. #105781 concerns import cancellation and drafts and remains separate. Related #78712 discusses shared-secret trusted-operator defaults; this repair preserves that shipped policy. Found by the adversarial round; no external reporter.
## Why This Change Was Made
The shared HTTP authorization owner now verifies current paired-operator credentials for plugin routes and passes their verified scopes to the existing runtime scope owner. Nostr keeps its admin requirement. Request headers cannot expand a device grant; ordinary plugin routes retain their write ceiling.
The existing plugin request scope also carries a Gateway-owned `revalidate` callback. Nostr, Admin HTTP RPC, Beam, and Geolocation call it after input reading/validation and before starting effects; Nostr checks after the publish queue wait and again before saving a published or imported profile. It verifies the original device grant through the same credential checker, writes the standard Gateway 401 on rejection, and stops the handler. There is no new registration option, config key, or contract version. This checkpoint does not cancel external work already in progress.
The shared Control UI JSON response boundary checks parsed objects, decodes Gateway error envelopes once, and supplies message plus HTTP status. The import adapter narrows profile text fields while preserving null values and extra fields. Channels and Browser Download use that result. Nostr-local error decoding and string-typed error assumptions are removed; profile validation details remain visible.
The decision "whether a shared or paired-device credential authenticates an operator HTTP request" is made by exactly one mechanism at `src/gateway/http-auth-utils.ts:241` (`checkHttpOperatorCredentials`).
The decision "whether an admitted device grant remains valid before handler effects" is made by exactly one mechanism at `src/gateway/http-auth-utils.ts:241` (`checkHttpOperatorCredentials`), shared by admission and the Gateway-owned request-scope callback.
The decision "how a Gateway JSON error becomes UI text with HTTP status" is made by exactly one mechanism at `ui/src/app/control-ui-auth.ts:6` (`readControlUiJsonResponse`).
## User Impact
Paired operators can publish and import their Nostr profile with their existing device session. Rejected credentials show `HTTP 401: Unauthorized`. Configured-token recovery remains available. No schema, migration implementation, config key, dependency, or protocol version changes are included.
## Evidence
| Real entry point | Pinned main | Candidate |
| --- | --- | --- |
| Startup-token dashboard, device-only browser: Save & Publish | One 401; trim exception | 200, persisted and published |
| Same paired browser: Import from Relays | One 401; trim exception | 200, saved and rendered |
| Auth disabled, direct dashboard: Save and Import | 200 / 200 | 200 / 200, no Authorization |
| Configured shared token after rejected device credential | Existing recovery contract | Each action: real 401 then 200 |
| Wrong credential | Object error envelope | Visible HTTP 401 and Unauthorized |
| Browser panel Download, rejected media credential | Decoder regression covered by rendered test | Real 401 rendered as HTTP 401: Unauthorized; recovery 200 |
Independent browser acceptance passed the bounded profile clauses on packaged candidate `669b3903`, before the additional revalidation correction. Its UI owner and callers are unchanged in the current candidate. Sanitized before/after screenshots and pass-through HTTP captures are retained with the review evidence. Synthetic profiles and a local relay were used; no external profile was published.
The earlier `eac0c373` package was produced with normal package preparation and declarations enabled in an isolated container. The canonical tarball checker passed. The actual v2026.9.4 CLI updated from the verified initial candidate tarball, exited 0, and finished its output. The UI correction was rebuilt and checked separately. After the first revalidation correction, that package ran Save and Import against a fresh copy of the preserved old state with the exact original v2026.9.4 bearer. Both returned 200; no pairing approval or token rotation occurred. All retained source-fixture hashes remained unchanged. Device identity, token bytes, scopes, and Gateway auth configuration remained unchanged. Browser automation resumed from the saved authentic old browser state; uninterrupted same-window recovery is not claimed.
On the preserved old-pairing fixture, both actions returned 200, but Import returned older local text in its merged payload; the UI displayed that value. That lane proves older-pairing authority and returned-data rendering, while the startup, configured-token, and auth-disabled lanes prove the new-text round trip. Package metadata has a null commit field; retained clean-source and tarball hashes bind the tested artifact to this candidate.
The `eac0c373` packaged Gateway also passed the delayed-revocation regression and Beam sibling. Each request was admitted while its body remained incomplete. Public device revocation completed before body completion. Nostr PUT and Beam POST then returned exactly `{"error":{"message":"Unauthorized","type":"unauthorized"}}` with HTTP 401. Nostr relay count and saved profile stayed unchanged; Beam's entire stored row, including retention metadata, stayed unchanged. Valid controls returned 200 and persisted. Fresh requests using the revoked tokens returned 401. Both isolated Gateways exited 0 and their ports closed.
The auth-disabled dashboard handoff separately closed with code 1000 before the editor loaded. Direct dashboard access and a backend socket worked. That handoff observation is retained separately and is not counted as a passing control.
The later CI correction `2d7774c3` only reuses structurally identical request/profile types and the existing test promise helper, and renames a shadowed test variable. Fresh native review verified unchanged authorization statements and API shapes; the exact eac0 package and runtime evidence remains bound to its original artifact. All 26 affected core tests and focused lint pass after this correction. CI owns the full type checks. No package or browser rerun is claimed for the type-only correction.
The final source correction is `7285ad188c241ed9a51f9646967d1a2ba9070539`. Its fresh isolated package build includes SDK declarations; all 153 SDK export checks and the canonical tarball checker passed. The build container was removed and a fresh absence check passed. Package SHA-256: `0011e2b13708d8af48bee07c8588f736a95577b89de2f33bfc85a4d7b19420aa`. Metadata commit is null; the clean source tree and tarball digest bind this artifact. The prior browser and actual old-updater receipts retain their original tested identities.
Final `7285ad18` packaged proof passed all three affected runs, once each. With the first relay acknowledgement held, a second Save was admitted and queued, then publicly revoked; after releasing the first publication, the second returned the standard HTTP 401 envelope with no second relay event and no config mutation. In a separate Save, the relay had already received the event before revocation; releasing its acknowledgement produced HTTP 401 and left config unchanged. That publication remains a completed partial outcome. Pending-body Nostr PUT and Beam POST also returned the same 401 with no publication/profile or Beam-row change; valid controls returned 200. The exact preserved v2026.9.4 bearer performed Save (200/persisted) and Import (200/saved) on the final package without pairing or rotation. The known local-over-imported text precedence remains; fresh imported text round-trip is not claimed in that fixture. All three isolated Gateways exited 0 and their ports closed.
## Consumers
| Consumer | Disposition |
| --- | --- |
| `extensions/nostr/src/nostr-profile.ts:84,133` | Retain kind-0 event creation/signing and the per-relay publication boundary. `successes`, `failures`, `eventId`, and `createdAt` describe the publication operation already started. A later config-save rejection does not undo a relay event. |
| `extensions/nostr/src/nostr-bus.ts:676,694` | Retain the publication owner’s previous-timestamp read, relay-result mapping, and publication-state write. Recording the actual publication remains valid when the later independent profile-config save is rejected. |
| `extensions/nostr/src/nostr-state-store.ts:103,114` | Retain account-keyed publication-state reads/writes and the existing stored format. Publication bookkeeping is separate from the saved profile configuration. |
| `extensions/nostr/src/nostr-profile-import.ts:86,238` | Retain relay subscription, profile validation, and local-first merge. The handler checks before import starts and again before saving its result. The older-state fixture’s imported fresh text and older merged text are distinct observed fields; no merge-precedence repair is claimed. |
| `extensions/beam/src/store.ts:15,23` | Retain the sessions namespace, retention policy, and forwarding of the upload update callback to the existing keyed store. |
| `src/plugin-state/plugin-state-store.ts:167,176,229` | Retain the asynchronous interface around the synchronous keyed update. After the handler’s authority check, this facade starts the existing synchronous update without another awaited queue. |
| `src/plugin-state/plugin-state-store.sqlite.ts:541` | Retain the transaction that selects the current Beam row, computes its replacement, writes value/expiry, and applies limits. Final revoked-request proof compares the whole row, including retention metadata, rather than only its payload. |
| `src/plugins/runtime/runtime-config.ts:8; src/config/mutate.ts:1405; extensions/nostr/index.ts:58` | Retain the lazy adapter and existing config-mutation owner used by Nostr’s registered `updateConfigProfile` callback with `afterWrite: { mode: "auto" }`. Save and Import pass a fresh authority check before this independent operation starts and await its result before reporting persistence. Existing config locking, commit, and observer behavior remain unchanged; cancellation inside an already-started operation is not claimed. |
The original BASE query used BASE production with candidate tests and is retained only as historical mixed-tree evidence. The corrected declaration census uses the immutable complete BASE source/test tree at `3b88500581`: all 16 original positions resolved in explicit root/test programs, yielding 248 raw references and 124 unique records. It recovered the omitted BASE `nostr-profile-ops.test.ts:128,136,148` calls; their exact helper-result assertions were retired in favor of rendered Channels and real-browser outcomes. All immutable BASE blobs matched before and after. The 136 unselected configurations remain explicit independent-resolution gaps. Unaffected candidate and integration queries retain their original bindings; they are not relabeled as current queries.
- `src/gateway/http-auth-utils.ts`: shared/device verification and request-local authority; removed `verifyControlUiDeviceReadToken` has no retained alternate implementation.
- `src/gateway/http-utils.ts`: type/function re-exports and session/profile authorization remain compatible.
- `src/gateway/http-endpoint-helpers.ts`, `models-http.ts`, `openresponses-http.ts`, and `sessions-history-http.ts`: generic HTTP callers retain their existing credential and owner contracts.
- `src/gateway/control-ui.ts`, `control-ui-plugin-assets.ts`, `plugin-icon-http.ts`, and `user-profiles-http.ts`: existing Control UI read/media/avatar callers use the shared credential mechanism; their route guards remain.
- `src/gateway/server-http.ts`, `server-http-plugin-auth.ts`, and `server-runtime-state.ts`: carry verified request context into dispatch.
- `src/gateway/server/plugins-http.ts` and `server/plugin-route-runtime-scopes.ts`: propagate verified scopes to the registered plugin client; trusted routes keep method-specific guards, ordinary routes keep their write ceiling.
- `src/gateway/server-http-upgrades.ts`: still uses generic HTTP authorization and gains no device-token admission.
- `extensions/nostr/index.ts`, `extensions/admin-http-rpc/index.ts`, and `extensions/diagnostics-prometheus/index.ts`: dynamic trusted-operator consumers retain their admin/method/read checks. QA trusted-operator registrations also consume this dispatcher. Ordinary Gateway-authenticated routes in Beam, Team Reports, and Geolocation retain their existing route guards and write ceiling.
- `ui/src/app/control-ui-auth.ts`: owns JSON error decoding; existing credential ordering and retry conditions remain.
- `ui/src/pages/channels/nostr-profile-ops.ts` and `channels-page.ts`: Save/Import consume decoded text; validation data and current-operation checks remain. `NostrProfileHttpResult` is removed without an alias. Form callback registration reaches the actual Save/Import buttons through `view.detail.ts`.
- `ui/src/components/browser/browser-panel-download.ts` and `browser-panel-render.ts`: failed media fetches reach the visible alert through the shared decoder; successful blobs and native downloads retain their existing handling.
- `ui/src/i18n/locales/en.ts`: removes unused `updateFailedStatus` and `importFailedStatus`; no source reader remains. Generated translations retain their existing translation-workflow ownership.
- Returned test readers: `src/gateway/server/plugin-route-runtime-scopes.test.ts`, `src/gateway/server/plugins-http.runtime-scopes.test.ts`, `ui/src/pages/channels/nostr-profile-ops.test.ts`, and `ui/src/components/browser/{browser-panel-download,browser-panel-native}.test.ts`. Additional field-query readers are `src/gateway/http-auth-utils.test.ts`, `http-endpoint-helpers.test.ts`, `http-utils.request-context.test.ts`, and `server/plugins-http.suspension-admission.test.ts`. These fixtures/assertions retain route ceilings, request context, request lifecycle, download, and native behavior coverage. Added rendered tests are listed below.
- `src/gateway/plugin-icon-http.test.ts:26`: retain the lexical-only read-auth mock; icon tests preserve the existing call/return contract and do not prove credential verification.
- `src/gateway/user-profiles-http.test.ts:14,28,117`: retain the mock and avatar authorization assertions, including required `users.list`; `{}`/null results remain valid route fixtures, without a paired-token proof claim.
- `src/gateway/control-ui-assistant-media-policy.test.ts:24,70`: retain the mocked `authMethod`/`operatorScopes` result; this suite tests media policy, not credential selection, and its result shape is unchanged.
- `ui/src/pages/channels/view.nostr-profile-form.ts:181,269`: final form displays page-state error/success text and binds Save/Import callbacks; it neither parses nor rebuilds the HTTP error.
- `extensions/nostr/src/nostr-profile-http.ts`: retain admin, loopback, and origin checks for PUT/Import and await the profile writer before reporting persisted/saved; the callback consumes Gateway authority after queue waiting and before independent effects and adds no local device verifier.
- `extensions/qa-lab/test-fixtures/current-requester-subagent-plugin/index.js:51`: retain the trusted-operator Gateway-auth registration as a dynamic runtime consumer; no fixture or live-execution claim changes.
- `extensions/qa-lab/test-fixtures/codex-hook-context-proof-plugin/index.js:38`: retain the existing dispatcher contract, with no private auth import or live-execution claim.
- `extensions/qa-lab/test-fixtures/self-yield-followup-subagent-plugin/index.js:56,130,183`: retain all three trusted-operator registrations and their existing dispatcher contract; no new exemption or execution claim.
- `extensions/admin-http-rpc/src/handler.ts`, `extensions/beam/src/http.ts`, and `extensions/geolocation/src/lookup-route.ts`: consume the same request-scope revalidation before Gateway dispatch, state update, and lazy database loading respectively. Prometheus and Team Reports handlers read only; Geolocation's shared cache maintenance remains service-owned once started. Unchanged plugin-auth webhooks do not enter the new device-credential path.
The earlier prepush query has its own retained coverage inventory; it is not relabeled as the final census. In that inventory, source-root membership shows 132 of the 133 unselected configurations add no files outside the loaded root; their independent module resolution remains unqueried. The remaining scripts config adds 753 roots, checked lexically; its sole match is an unchanged test benchmark path. The loaded root contains the affected repository TypeScript, including plugin and test sources. Native code, strings, generated output, and external plugins require the manual contract trace; no named SDK export or transport field is added. The existing request-scope type gains the documented optional revalidation callback. Admin HTTP RPC, Prometheus, native apps, and QA fixtures were traced but are not claimed as live-tested.
| Shipped consumer | Support floor / artifact | Operation | Result |
| --- | --- | --- | --- |
| v2026.9.4 browser/device state | Released CLI → b7ff package; preserved bearer → final 7285 package | Historical actual update, then current Save/Import authority | PASS; old merged-text behavior retained |
| Current paired browser | 669b package/UI; UI unchanged in final 7285 | Device-only Save/Import | PASS on 669b; final 7285 packaged HTTP controls also pass |
The unchanged response boundary has a supplemental typed census from UI correction `669b3903`, the prior candidate, and the pinned baseline: 486 reference records, each accounted for. All 57 applicable root/test/UI positions resolved in that bound correction tree. The 38 core/Nostr exclusions remain explicit because those programs exclude private UI files. Imported and merged profiles have one private payload guard; all returned readers remain in the four UI owner/caller modules listed above.
The final comparison base is `93a5d82275`. All authored source/test baseline blobs match the original reproduction base; two inherited assertion-baseline deletions are separate. The current contribution spans 21 authored files, while the original red and typed-baseline evidence remains pinned to `3b88500581`.
Revalidation reference census: candidate `eac0c373` and original BASE `3b885005`, with root, test, core, Nostr, Admin HTTP RPC and Geolocation projects. Candidate returned 1,860 records (773 exact reader groups); BASE returned 1,670 (700 groups). Both exited 0, with no ambiguous queries or tool failures. All returned files are named below.
- **Effect handlers and registration exports:** The four changed bundled effect handlers retain their existing registration owners. Nostr PUT checks after body validation and queue waiting before publish, then again before config save; Import checks before relay I/O and again before config save. Nostr GET remains read-only. Beam checks before keyed-store update; Admin before Gateway dispatch; Geolocation before lazy database/cache work. Revalidation rejection stops effects; Nostr inner and outer catches preserve an ended 401. Tests remain direct handler consumers. Nostr index.ts resolves api.ts by a string export, so typed references alone miss that registration edge; the source-reviewed lazy registration is recorded separately. Files: `extensions/admin-http-rpc/index.ts`, `extensions/admin-http-rpc/src/handler.test.ts`, `extensions/admin-http-rpc/src/handler.ts`, `extensions/beam/index.ts`, `extensions/beam/src/beam.test.ts`, `extensions/beam/src/http.ts`, `extensions/beam/src/mirror-retry.test.ts`, `extensions/geolocation/index.ts`, `extensions/geolocation/src/lookup-route.test.ts`, `extensions/geolocation/src/lookup-route.ts`, `extensions/nostr/api.ts`, `extensions/nostr/src/nostr-profile-http.test.ts`, `extensions/nostr/src/nostr-profile-http.ts`.
- **Revalidation capability producer, transport, and readers:** The auth owner creates one optional callback only for admitted device-token requests. The route scope copies it only for gateway-auth routes. Its seven production call sites serve five effect branches in four handlers; Save and Import each check again before their later independent config save. The paired-device regression fixture calls the same scope capability. There is no callback for shared credentials or plugin-auth routes. Files first listed in this group: `src/gateway/http-auth-utils.paired-device.test.ts`, `src/gateway/http-auth-utils.ts`, `src/gateway/server/plugins-http.ts`, `src/plugins/runtime/gateway-request-scope.ts`.
- **Credential and original-scope checks:** The existing HTTP credential owner handles initial admission and revalidation. The closure captures the bearer and admitted scope copy, passes original scopes to the current device verifier, and requires a successful device-token result. Current token scopes and original scopes both reach the pairing verifier. A current shared-secret match cannot revive a revoked device grant. Initial admission retains rate accounting; revalidation deliberately omits the limiter. BASE uses verifyControlUiDeviceReadToken and lacks the central checker/new fields, recorded as explicit declaration absences rather than zero readers. The auth-result declaration query also resolves its owning result-property references, all retained in the raw accounting. Files first listed in this group: `src/gateway/server/plugin-route-runtime-scopes.ts`.
- **HTTP auth types, server factory, and current-auth routing:** Core aliases, endpoint signatures, upgrade types, and test fixtures keep their prior required fields. The optional callback does not change core endpoint admission. Only the plugin HTTP authorizer attaches it. createGatewayHttpServer forwards its existing current-auth getter; server-runtime-state remains the serving owner supplying that getter. Both plugin route scope construction paths use the same factory. Static-auth test factories keep their existing fixed-auth contract. Files first listed in this group: `src/gateway/http-endpoint-helpers.ts`, `src/gateway/http-utils.ts`, `src/gateway/models-http.ts`, `src/gateway/openresponses-http.ts`, `src/gateway/provider-browser-auth/persistence.integration.test.ts`, `src/gateway/server-http-plugin-auth.ts`, `src/gateway/server-http-upgrades.ts`, `src/gateway/server-http.canvas.test.ts`, `src/gateway/server-http.node-workspace-transfer.test.ts`, `src/gateway/server-http.probe.test.ts`, `src/gateway/server-http.rejection-transport.test.ts`, `src/gateway/server-http.request-trace.test.ts`, `src/gateway/server-http.test-harness.ts`, `src/gateway/server-http.ts`, `src/gateway/server-http.upgrade-claim.test.ts`, `src/gateway/server-runtime-state.ts`, `src/gateway/server.plugin-node-capability-auth.test.ts`, `src/gateway/server.preauth-hardening.test.ts`, `src/gateway/server.public-worker-ingress.test.ts`, `src/gateway/server/plugins-http.runtime-scopes.test.ts`, `src/gateway/sessions-history-http.ts`, `src/gateway/worker-environments/node-workspace-transfer.test-support.ts`.
- **Standard unauthorized response owner:** sendUnauthorized remains the unchanged standard JSON 401 writer. The new closure invokes it before throwing; existing auth-failure and watch-node consumers retain their prior response behavior. No alternate error encoder or response owner was added. Files first listed in this group: `src/gateway/http-common.test.ts`, `src/gateway/http-common.ts`, `src/gateway/watch-node-http.ts`.
- **Existing request-scope type, getter, runner, and consumers:** The request-scope storage, getter and runner implementations are unchanged from original BASE. Existing registry, plugin identity, client, context, resolver and node-authority readers retain those fields and behavior; the callback is additive and optional. Existing spread-based scope transport preserves the closure, whose ended/destroyed-response guards prevent retained authority from being used after response expiry. Registry/context projection helpers retain their established inheritance rules. The only new behavior consumers are the four effect handlers and the paired-device fixture listed above. Public SDK barrels remain aliases to the same getter. The full file lists include every import, export, type, test fixture and execution reader returned by either side; no reader is discarded as merely a test. Files first listed in this group: `extensions/browser/src/browser/extension-relay/gateway-relay-route.ts`, `extensions/diagnostics-prometheus/src/service.ts`, `extensions/nostr/runtime-api.ts`, `extensions/nostr/src/nostr-profile-http-runtime.ts`, `extensions/team-reports/src/http.ts`, `packages/plugin-sdk/src/plugin-runtime.ts`, `src/acp/control-plane/spawn.test.ts`, `src/agents/embedded-agent-runner/compact.foreground-resources.test.ts`, `src/agents/embedded-agent-runner/run.plugin-runtime-refresh.integration.test.ts`, `src/agents/harness/host-capability.node-authority.test.ts`, `src/agents/harness/host-capability.test.ts`, `src/agents/harness/host-capability.ts`, `src/agents/harness/node-execution-authority.ts`, `src/agents/harness/selection.test.ts`, `src/agents/harness/session-deletion.ts`, `src/agents/isolated-completion.resources.test.ts`, `src/agents/isolated-completion.test.ts`, `src/agents/main-session-recovery/main-session-restart-recovery-marking.test.ts`, `src/agents/mcp-auth-profile.integration.test-support.ts`, `src/agents/mcp-connection-resolver.ts`, `src/agents/openclaw-plugin-tools.ts`, `src/agents/runtime-plugins.test.ts`, `src/agents/runtime-plugins.ts`, `src/agents/session-maintenance/run.ts`, `src/agents/subagents/announce/subagent-announce.requester-settle-dispatch.test.ts`, `src/agents/subagents/registry/subagent-registry.test.ts`, `src/agents/subagents/spawn/acp-spawn.authority.test.ts`, `src/agents/subagents/spawn/subagent-spawn-cleanup.test.ts`, `src/agents/subagents/spawn/subagent-spawn-cleanup.ts`, `src/agents/subagents/spawn/subagent-spawn-gateway.test.ts`, `src/agents/subagents/spawn/subagent-spawn-gateway.ts`, `src/agents/subagents/spawn/subagent-spawn.authority.test.ts`, `src/agents/subagents/spawn/subagent-spawn.in-process-gateway.test.ts`, `src/agents/subagents/spawn/subagent-spawn.preparation-authority.test.ts`, `src/agents/subagents/spawn/subagent-spawn.production-boundary.test.ts`, `src/agents/tools/dashboard-tool.test.ts`, `src/agents/tools/gateway.runtime-identity.test.ts`, `src/agents/tools/in-process-gateway.ts`, `src/auto-reply/reply/agent-runner-execution.ts`, `src/auto-reply/reply/agent-runner-run.ts`, `src/auto-reply/reply/agent-runner.misc.runreplyagent.test.ts`, `src/auto-reply/reply/commands-plugins.install-clawhub-spec.test.ts`, `src/auto-reply/reply/commands-plugins.ts`, `src/auto-reply/reply/commands-system-agent.ts`, `src/auto-reply/reply/dispatch-from-config.lifecycle.ts`, `src/auto-reply/reply/dispatch-from-config.reply-dispatch.test.ts`, `src/auto-reply/reply/followup-runner.test.ts`, `src/auto-reply/reply/followup-runner.ts`, `src/auto-reply/reply/reply-turn-admission.ts`, `src/canvas/widget-tool.ts`, `src/channels/plugins/registry-loader.ts`, `src/channels/plugins/registry.ts`, `src/cli/plugin-invocation-resources.owner.test.ts`, `src/cli/run-main.cleanup.test.ts`, `src/cron/trigger-script.preparation.test.ts`, `src/gateway/board-host-tools.ts`, `src/gateway/local-request-context.session-tools.test.ts`, `src/gateway/local-request-context.test.ts`, `src/gateway/local-request-context.ts`, `src/gateway/node-agent-cli-runtime.ts`, `src/gateway/node-claude-skill-runtime.test.ts`, `src/gateway/node-claude-skill-runtime.ts`, `src/gateway/node-invoke-plugin-policy.session-full.test.ts`, `src/gateway/node-invoke-plugin-policy.ts`, `src/gateway/operator-approval-placement-grants.test.ts`, `src/gateway/server-cron.test.ts`, `src/gateway/server-in-process-execution-lifetime.test.ts`, `src/gateway/server-methods.ts`, `src/gateway/server-methods/board.runtime-boundaries.test.ts`, `src/gateway/server-methods/hooks-status.ts`, `src/gateway/server-methods/plugins.runtime-owner.test.ts`, `src/gateway/server-methods/session-catalog-privacy.test.ts`, `src/gateway/server-methods/session-catalog-provider-access.test.ts`, `src/gateway/server-methods/session-catalog-provider-access.ts`, `src/gateway/server-methods/session-catalog.ts`, `src/gateway/server-methods/sessions-rewind.test.ts`, `src/gateway/server-methods/web.ts`, `src/gateway/server-plugin-in-process-dispatch.authorization.test.ts`, `src/gateway/server-plugin-in-process-dispatch.ts`, `src/gateway/server-plugin-reload.memory.test-support.ts`, `src/gateway/server-plugin-subagent-runtime.test.ts`, `src/gateway/server-plugin-subagent-runtime.ts`, `src/gateway/server-plugins-node-runtime.ts`, `src/gateway/server-plugins.lifecycle.channels.test.ts`, `src/gateway/server-plugins.subagent-ended-hook.test.ts`, `src/gateway/server-plugins.test.ts`, `src/gateway/server-plugins.ts`, `src/gateway/server-runtime-services.test.ts`, `src/gateway/server-runtime-state.tailscale.test.ts`, `src/gateway/server-startup-plugins.test.ts`, `src/gateway/server-startup-post-attach.test.ts`, `src/gateway/server.cron.test.ts`, `src/gateway/server.plugin-frame-auth.test.ts`, `src/gateway/server.plugin-http-auth.test.ts`, `src/gateway/server.plugin-http-role-scopes.test.ts`, `src/gateway/server/hooks.agent-trust.test.ts`, `src/gateway/server/plugins-http.ownership.test.ts`, `src/gateway/server/plugins-http.test.ts`, `src/gateway/session-worker-placement-context.ts`, `src/gateway/tool-resolution.terminal.test.ts`, `src/gateway/tools-invoke-http.test.ts`, `src/gateway/worker-environments/worker-turn-launcher-computer.test.ts`, `src/gateway/worker-environments/workspace-result-finalize.ts`, `src/infra/outbound/channel-bootstrap.runtime.ts`, `src/infra/outbound/channel-resolution.ts`, `src/infra/outbound/deliver-channel.ts`, `src/infra/outbound/runtime-visible-channels.ts`, `src/node-host/plugin-node-host.test.ts`, `src/plugin-sdk/agent-harness-task-runtime.test.ts`, `src/plugin-sdk/facade-activation-check.runtime.ts`, `src/plugin-sdk/gateway-method-runtime.test.ts`, `src/plugin-sdk/gateway-method-runtime.ts`, `src/plugin-sdk/plugin-runtime.ts`, `src/plugin-sdk/provider-catalog-runtime.test.ts`, `src/plugin-sdk/webhook-ingress.test.ts`, `src/plugin-sdk/webhook-ingress.ts`, `src/plugins/capability-provider-runtime.ts`, `src/plugins/cli-gateway-nodes-runtime.ts`, `src/plugins/compaction-provider.test.ts`, `src/plugins/current-plugin-metadata-snapshot.test.ts`, `src/plugins/hook-runner-global-state.ts`, `src/plugins/legacy-sdk-resource-host.ts`, `src/plugins/memory-runtime.owners.test.ts`, `src/plugins/memory-runtime.test.ts`, `src/plugins/migration-provider-runtime.test.ts`, `src/plugins/plugin-command-registry.ts`, `src/plugins/plugin-instance-callbacks.test.ts`, `src/plugins/plugin-instance.test.ts`, `src/plugins/plugin-module-loader-cache.ts`, `src/plugins/provider-registry-selection.ts`, `src/plugins/providers.runtime-core.ts`, `src/plugins/public-surface-generation.test.ts`, `src/plugins/registry-lifecycle.test.ts`, `src/plugins/registry-runtime.hooks.test.ts`, `src/plugins/registry.runtime-config.test.ts`, `src/plugins/registry.runtime-session-ownership.test.ts`, `src/plugins/runtime-context.test.ts`, `src/plugins/runtime-context.ts`, `src/plugins/runtime/gateway-request-scope.test-fixtures.ts`, `src/plugins/runtime/gateway-request-scope.test.ts`, `src/plugins/runtime/runtime-agent.ts`, `src/plugins/runtime/runtime-embedded-agent.runtime.ts`, `src/plugins/runtime/runtime-llm.runtime.ts`, `src/plugins/runtime/runtime-web-channel-plugin.test.ts`, `src/plugins/services.return-contract.test.ts`, `src/plugins/tools.optional.test.ts`, `src/plugins/widget-presenters.ts`, `src/sessions/session-initialization.ts`, `src/sessions/session-lifecycle-admission.ts`, `src/system-agent/setup-inference-turn.test.ts`, `src/transcripts/status.test.ts`.
- **Registration/docs supplement:** `extensions/nostr/index.ts` uses `loadBundledEntryExportSync` with the `createNostrProfileHttpHandler` export in `api.ts`; its gateway-auth route is not a typed call edge. Existing registrations in `extensions/diagnostics-prometheus/index.ts` and `extensions/team-reports/index.ts` remain read-only. `docs/plugins/sdk-channel-plugins.md` records the callback, response-first 401, response expiry, and later independent-mutation rule.
- **Resolution limits:** Candidate resolved 89 of 180 project-position queries; BASE resolved 53 of 108. The 91/55 unresolved pairs are named in evidence: isolated plugin configs do not load core declarations, core does not load plugin entry declarations, and unrelated plugin configs do not load one another. Root and test resolve every position on each side. There are 132 independently unqueried configs per side; config parsing accounts for each by name and exact source-root membership. All returned reader files belong to selected roots. Only `tsconfig.scripts.json` has roots outside selected projects (754 candidate / 753 BASE); these received lexical search, not compiler reference queries. `extensions/tsconfig.package-boundary.base.json` is a no-input template and reports that parse diagnostic. Beam has no independent tsconfig. No zero-consumer claim is made for unresolved or unqueried project resolutions.
- **Evidence boundary:** Every tracked source/symlink matches its named Git tree during and after the census. The exhaustive raw records, declaration absences, projects, file groups and hashes are retained in private `revalidate-census-*` evidence. That historical census prepared 64 positions across 15 files and six projects for integration; its completed integration and the final supplements retain their separate identities below.
The retained eac0 integration census ran against merge `bb8a61bda6a50f41a3c3869f90e0344ca06f61d6`, tree `e67790db8eccc4315a16a0a08ae1504ff4e29dfc`, after pushed head `eac0c373fe17aeeda4f835810cbf2c71ebded124` was verified. The full union used 64 positions across 15 unchanged declaration files and six explicit projects. It returned 2,390 records / 1,005 exact reader groups across 211 files, with exit 0 and no ambiguous queries or tool failures. Root and test each resolved all 64 positions.
- All 194 files in the preceding correction census remain covered. The full union also names these 17 already-known auth/UI consumers: `src/gateway/control-ui-plugin-assets.ts`, `src/gateway/control-ui.ts`, `src/gateway/http-auth-utils.test.ts`, `src/gateway/http-endpoint-helpers.test.ts`, `src/gateway/http-utils.request-context.test.ts`, `src/gateway/plugin-icon-http.ts`, `src/gateway/server/plugin-route-runtime-scopes.test.ts`, `src/gateway/server/plugins-http.suspension-admission.test.ts`, `src/gateway/user-profiles-http.ts`, `ui/src/app/control-ui-auth.ts`, `ui/src/components/browser/browser-panel-download.test.ts`, `ui/src/components/browser/browser-panel-download.ts`, `ui/src/components/browser/browser-panel-native.test.ts`, `ui/src/components/browser/browser-panel-render.ts`, `ui/src/pages/channels/channels-page.ts`, `ui/src/pages/channels/nostr-profile-ops.test.ts`, `ui/src/pages/channels/nostr-profile-ops.ts`. The read-admission helpers preserve the Control UI/icon/assets/profile callers; the UI decoder and Nostr adapter retain their status/error, Save/Import and Browser callback flows. Complete per-file dispositions remain in the final evidence.
- Every structural reader delta was reviewed: 37 added and 37 removed identities are source-line moves. Of these, 36 come from earlier correction files; the merge-specific move is `src/plugins/plugin-module-loader-cache.ts:652→662`, whose active-request registry check is unchanged. The comparison explicitly uses 30 current-head query sets, 19 lint-correction sets and 15 earlier integration sets; it is not a direct full-union candidate query. No new behavior consumer was found in this final union.
- Core resolved 35/64; Nostr 4/64; Admin and Geolocation 1/64 each. The remaining 215 project-position pairs are explicit source/project-boundary gaps. All 132 skipped configs are separately named and parsed for membership. All returned reader files belong to selected roots; only `tsconfig.scripts.json` adds outside roots (754), with lexical rather than typed coverage. The package-boundary template retains its no-input parse diagnostic. Config files and lockfile are unchanged. These limits do not imply zero consumers.
- All 20 authored source/test/docs files are byte-identical between pushed head and merge. The assertion baseline separately incorporates an upstream `sdk-alias` allowance reduction from 3 to 2. Stable head and merge archives matched all 41,548 / 41,550 tracked entries before and after queries. This is source-reference evidence for eac0, not a replacement for runtime tests or CI.
CI type-repair census is bound to candidate `2d7774c389cdf2327391b5d1713e10e08a704575`, tree `fec9b073599495339d5b83b7d5696f3dc6757a75`. Ten declarations cover the four aliases, four changed public signatures and two directly referenced producers. Root/test/core all resolved: 30 candidate queries returned 310 reference records / 123 groups. Original BASE `3b88500581` received 27 new queries returning 280 records; three exact earlier plugin-authorizer queries (six records) were retained, for 115 BASE groups. No unresolved query, ambiguity or tool failure occurred.
- The existing 211-file consumer union remains required. The supplement names 13 further files: `src/commands/configure.gateway.test.ts`, `src/gateway/auth.test.ts`, `src/gateway/auth.ts`, `src/gateway/github-user-identity.cache.test.ts`, `src/gateway/http-auth-user-profile.ts`, `src/gateway/http-utils.authorize-request.test.ts`, `src/gateway/managed-image-attachments.ts`, `src/gateway/server-http-probes.ts`, `src/gateway/server/plugin-node-capability-auth.ts`, `src/gateway/server/ws-connection/auth-context.ts`, `src/gateway/session-kill-http.ts`, `src/gateway/tools-invoke-http.ts`, `src/gateway/user-profiles-http.auth.test.ts`. Complete combined coverage lists all 224 files in evidence. General HTTP/scoped/check callers keep their existing authorization policy; producer readers keep the same profile and connection contracts; new alias reads are type-only.
- Every original-BASE structural delta (35 added / 27 removed identities) has a source disposition. This comparison includes the earlier functional repair; it does not imply runtime changes in this CI patch. Native review verified exact signature equivalence and unchanged executable production behavior; earlier runtime proof retains its own eac0 identity.
- All 135 skipped configs per side remain named and membership-accounted. Every supplemental reader belongs to the selected source roots. Unqueried project resolution and native/dynamic consumers are not certified; exact-name apps/docs/scripts searches returned no matches. The final merge supplement below completes integration of these changed types.
Queue/persistence correction is source-reviewed and census-bound to `7285ad188c241ed9a51f9646967d1a2ba9070539`, tree `297a066691af763caf620a5d058e8b529cf73420`. The prior post-body check was too early: queued publication could start after revocation, and a later config save could follow revocation during relay I/O. PUT now revalidates inside its queued callback before publication and again before later config persistence; the response guard preserves HTTP 401. The new packaged red evidence reproduces both gaps through actual registration, public device revocation and held relay acknowledgements; its original package identity remains separate from final green evidence.
- Eight candidate declarations were queried in root/test/Nostr: 24 queries, 23 resolved, 93 references / 39 exact groups. Root/test resolve every declaration. The explicit unresolved cell is the core request-scope property in standalone Nostr; all seven Nostr declarations resolve there. Original BASE has 15 fresh plus six exact reused queries, all resolved, 73 references / 29 groups. No ambiguity or tool failure occurred.
- Every group and structural delta (15 added / five removed / 24 matched) has a source disposition. All 135 skipped configs per side remain named and membership-accounted. Historical evidence retains its original identities. The retained union now lists 228 typed/manual files: previous 224 plus `extensions/nostr/src/channel.ts`, `extensions/nostr/index.ts`, `src/plugin-sdk/keyed-async-queue.ts`, and `docs/plugins/sdk-channel-plugins.md`. The string-loaded registration is a manual/runtime contract, not a claim of typed resolution.
- Full candidate/BASE snapshots match before and after queries. Seven production callback sites serve four handlers/five effect branches. The check admits work about to start; an already-published relay event remains a fact when later config persistence is rejected. Final packaged green is bound to `7285ad18`; final postpush integration retains its own evidence and identity.
Final source integration is bound to actual merge `7ef62a14187cac6e4b2d731b5aa27459ddc300db`, tree `c96aedcf4f18b17d2fec7d79f9b14ef60498c3dd`, for pushed candidate `7285ad188c241ed9a51f9646967d1a2ba9070539`. Canonical census passed on this merge with comparator `93a5d82275`.
- The 18 changed-type/queue declarations received 72 actual-merge queries in root/test/core/Nostr: 54 resolved, 405 references / 162 exact groups across 34 files. Root/test resolve all 18 each; core resolves 11 and Nostr seven. The 18 unresolved cells are explicit cross-project source-boundary gaps, with no ambiguity or tool failure. The root comparison has zero added / zero removed / 162 matched identities, retaining ten prior CI query identities plus eight exact current queue identities.
- All 228 retained typed/manual paths have current merge bindings and dispositions. Two contain inherited source changes: the loader exactly matches the previously reviewed `bb8a61` blob and preserves its registry guard at line 662; isolated-completion tests add three expected callback arguments, while their existing dynamic registry read shifts 654→657 with the same meaning. No further affected query needed expansion. Historical full-census records retain their original identities; this is an actual-merge supplement, not a new full 64-position query.
- All 20 authored source/test/docs files are identical from pushed candidate to merge. The assertion baseline separately inherits `session-cost-usage-reporting.ts` 8→5 and `sdk-alias.ts` 3→2. Full candidate/merge snapshots match before and after queries: 41,548 / 41,561 tracked files. All 134 skipped configs are named and membership-accounted; dynamic registration still uses manual and packaged proof. Final package/runtime results retain candidate `7285ad1` and the explicit fresh-marker Import limitation from their own report.
## Invalidation
Each HTTP request rereads pairing state and verifies the current token, scopes, revocation, and issuer generation. The verifier checks the entire observed scope grant, so narrowing between lookup and verification cannot authorize the broader snapshot. Each effect checkpoint retains the original bearer and required scopes, reads current Gateway auth, and requires fresh device verification; a different successful auth method cannot replace the admitted device grant. The capability expires when its HTTP response closes. No credential cache is introduced. External operations already started are not cancelled by this checkpoint.
Real requests confirmed rotation and revocation take effect, forged admin headers fail, auth-generation changes reject old tokens, rate locks expire, and cross-origin/site mutations remain denied. The exact lookup-to-verification race is source-reviewed rather than forced by a runtime failpoint.
Each UI operation captures its connection/form/tab ownership and resolves current credentials. Existing cancellation, deadline, and generation checks suppress stale completion. The shared decoder has no cache; body decoding preserves aborts. Named-role policy and auth-bypass cache lifecycle are unchanged.
## Contention
Existing per-client credential serialization groups shared/device verification and failure accounting into one terminal attempt. No new queue or transaction is introduced. Eight concurrent invalid plugin requests under a three-failure limit yielded three 401 and five 429 responses. A valid device worked while only the shared-secret bucket was locked, was denied during the device lock, and worked after expiry. No limiter bypass or deadline reset was added. The new awaited checkpoint was exercised with body-pending revocation in real Nostr and Beam requests; neither blocked the independent public revoke command, and both rejected before their effect. Save now revalidates inside the existing queued callback, after the prior publication settles, and again before its later config write. The check cannot undo a relay publication already sent. Final packaged evidence confirms queued revocation prevents a second publication/config mutation and revocation during relay acknowledgement prevents the later config write. Public device revocation completed while requests waited; normal first publication completed successfully. No queue, relay timeout, or locking policy changed.
## Tests
- `src/gateway/http-auth-utils.paired-device.test.ts:23`: registered `createGatewayHttpServer` plugin PUT/POST; current admin works, reader cannot forge admin, revoked/wrong credentials receive the Gateway 401 envelope. Regression fails on base and passes on candidate.
- `http-utils.authorize-request.test.ts`: removed the impossible generic-auth device-success mock. Generic HTTP admission is unchanged; registered plugin PUT/POST tests retain device authority and forged-header denial, including revocation with a pending body. Existing generic role/profile cases remain.
- `channels-page.test.ts`: registered `openclaw-channels-page` Save & Publish and Import; structured 401/403, non-JSON 503, two-credential recovery, and visible HTTP 400 validation details. Final Channels suite: 35 passed; focused Channels/Nostr/Browser set: 96 passed.
- `browser-panel-toolbar.test.ts`: registered `openclaw-browser-panel` Download renders HTTP 401 and Unauthorized; sibling download/native suites retain their existing coverage.
- `nostr-profile-ops.test.ts`: retained deadlines and stale/cancellation coverage. Deleted "preserves successful JSON responses for PUT and import" and "preserves the response when an error body is not JSON": internal return-shape assertions retired; registered rendered Channels success and non-JSON failure cases at `ui/src/pages/channels/channels-page.test.ts:235` replace them. One obsolete assertion allowance was removed; none added.
- Revalidation correction: 26 core tests across three files and 87 plugin/sibling tests across five files passed. The registered regression fails with the prior auth owner and passes with this candidate.
- Queue correction: 32 existing Nostr HTTP tests and 14 Beam sibling tests passed, plus focused lint/format. The first command chose a suite excluding Nostr and an incorrect Beam path, so no tests ran; the corrected suite/path passed. No test was edited or deleted in this correction. Its new regression uses the actual packaged Gateway registration.
- Targeted HTTP and UI suites, formatting, line limits, assertion ratchet, localization verification, and diff whitespace checks passed. Full type-aware lint, unused exports, and integration lanes remain CI-owned.
Production code grew to consolidate credential verification, transport verified authority, and decode errors at the shared boundary. The four-file/120-line guide was exceeded: 21 files, 529 written lines including tests, 242 removed lines.
Co-authored-by: Ayaan Zaidi <hi@obviy.us>