Conversation
…alhost:3200 (N50) Gate A, HIGH (steward #2640, product-steward #2602). - SetupWizardPage.tsx:53 fetched http://localhost:3200/api/capabilities with the signed-in user's Authorization header on every /setup load, in every build, then claimed "Gateway connected at localhost:3200". - SetupAgentPage defaulted gatewayUrl to http://localhost:3200 for its three key-bearing requests (scan-network, identify-machine, register-device), and its only caller never overrode it. In production both handed the user's key to whatever listens on port 3200 of their own machine. Fix: - lib/gateway-base.ts: the configured gateway. That is same-origin, or VITE_PCC_URL, the variable the auth store already uses. It is the only origin that gets the key. - The wizard's liveness check hits the public /api/health, with no key, and says "Gateway reachable". - SetupAgentPage defaults to the configured gateway. Guard: __tests__/no-hardcoded-gateway-origin.test.ts fails on any http://localhost or 127.0.0.1 literal in production code. Its shrink-only KNOWN list holds three literals that are not key leaks: two dev-only bases and a chain-RPC form default. Tests: - setup-key-origin.test.tsx renders both real pages, records every fetch, and fails if a request leaves the configured gateway or carries the key off it. - 4 of the 5 new tests fail against master's pages. - Dashboard 225/225. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VU6exGFC7uLukeDF2GBUpQ
… (N50 review) Cross-family review of #368 (sol, #2857) found that the key was attached without regard to where a request went. This fixes each finding: 1. Two credential destinations. There is now one gateway origin (lib/gateway-base.ts). fetchWithKey() is the one place a key goes onto a request: it resolves the final URL, refuses any other origin, and only then adds the header. lib/api.ts, api/gateway.ts (every react-query hook), the auth store's key check, SetupAgentPage and EarnFromYourWorkPage use it. EarnFromYourWorkPage sent its new key to http://localhost:3200 in dev builds, and in production builds to https://capability.network, even when served from staging. 2. VITE_PCC_URL was not validated. It must now be an absolute https: origin (http: only on loopback in a dev build). A bad value fails closed: no request carries a key, and the reason is logged. 3. The guard was literal-localhost only. A startup egress guard (main.tsx) rejects any fetch that carries a PCC key or the stored key, in a header, the URL or a string body, to another origin. no-direct-auth-headers.test.ts ratchets the remaining hand-built headers (31 modules, each with an owning lane) out of the code. SetupAgentPage no longer takes a gateway argument (product-steward, #2849). Tests: dashboard 264/264 (+39). tsc and the build are clean. Negative controls: removing the origin check fails 6 tests, accepting any scheme fails 6, a pass-through guard fails 3, no guard at startup fails 1, and restoring master's EarnFromYourWorkPage, lib/api.ts or api/gateway.ts fails 3, 2 and 2. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VU6exGFC7uLukeDF2GBUpQ
LamaSu
added a commit
that referenced
this pull request
Sep 24, 2026
…ay client FeedbackButton built its own Authorization header. It now calls api.submitFeedback, so its key is attached the same way as every other request (and, with #368, only for the configured gateway). Once #368 lands, a hand-built header is a ratchet failure. Behaviour is unchanged: a rejected post still shows "Failed -- retry?". Tests: FeedbackButton.test.tsx (2), which characterise the refactor rather than a fix, so they also pass on the old code. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VU6exGFC7uLukeDF2GBUpQ
This was referenced Sep 24, 2026
LamaSu
added a commit
that referenced
this pull request
Sep 25, 2026
… ratchet list, settle fix) into implementer-echo
…Page.tsx, SettlementPage.tsx send the key only through authorizedFetch Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…estratorPage.tsx, OrchestratorDetailPage.tsx send the key only through authorizedFetch Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…SponsorTelemetryPage.tsx, EvidenceExplorerPage.tsx send the key only through authorizedFetch Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…artPage.tsx, routes/onboard/chat/index.tsx send the key only through authorizedFetch Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…st.ts KNOWN list drops the 12 files migrated to authorizedFetch The ratchet's "KNOWN only shrinks" test failed after the n50-mig-a migration because these 12 files no longer build an Authorization header directly: WalletPage.tsx, TelemetryPage.tsx, SettlementPage.tsx, ProtocolRunPage.tsx, OrchestratorPage.tsx, OrchestratorDetailPage.tsx, SystemDashboardPage.tsx, SponsorTelemetryPage.tsx, EvidenceExplorerPage.tsx, BatchTrackingPage.tsx, StartPage.tsx, routes/onboard/chat/index.tsx. Removing their entries is exactly what the test asks for; no other KNOWN entries were touched. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…torDashboardPage, AgentPackagePage, OperatorA2APage, ProtocolLibraryPage send the key only through authorizedFetch Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…BoardPage, SensorDashboardPage, NegotiationSessionPage send the key only through authorizedFetch Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…Form, DiscoverabilityPanel, DisputeModal send the key only through authorizedFetch Also drops these 4 files plus the pages/* migrated in the two prior commits from the no-direct-auth-headers ratchet's KNOWN allowlist: the test asserts KNOWN only shrinks, and after this migration none of the 13 files assigned to this lane build an Authorization header by hand any more. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
astra's round-2 review of #368 (DO-NOT-SHIP on 74f758b) found that legacy callers could still read the key, while a request scanner that could be bypassed served as the boundary. The boundary is now structural: - stores/auth-store.ts keeps the key out of zustand state, so reading it from the store is a type error, and getAuthHeaders() is gone. The key's one accessor, readApiKeyForAuthorizedFetch(), is read only by lib/authorized-fetch.ts. - fetchWithKey() (lib/gateway-base.ts) is the only code that puts a key on a request. It resolves the target against the one validated gateway origin, refuses any other origin, and refuses redirects (redirect: "error"). - The page-origin fallback is validated like VITE_PCC_URL: https, or http only on a loopback host. Anything else fails closed. - The egress guard is defence in depth. It reads URLSearchParams, FormData, Blob, binary and Request bodies, percent-decodes them, checks the stored key's base64 forms, and refuses any body it can't read: a stream, or an object it doesn't recognise, such as one from another realm. - The special callers: - Analytics fetches only literal /api/analytics paths, enforced by type. - Orchestrator chat accepts only a gateway-relative api_base and encodes session ids. - CdpFundedKeyOnramp, agent-client, AgentChatPage and the passkey flow send through authorizedFetch. - The ratchet (no-direct-auth-headers.test.ts) has no file exemptions: - getAuthHeaders doesn't exist; - the accessor is referenced only in authorized-fetch; - the storage slot is touched only in the store, whether by name or by sweeping localStorage; - Authorization and Bearer are built only in gateway-base, apart from exact reviewed display lines, each with the number of times it occurs. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…words (N50) lib/telemetry.ts turns on PostHog session recording and asked for request and response headers and bodies through session_recording. networkPayloadCapture. The client SDK never reads that option; it is a remote project setting. So whether PostHog recorded headers and bodies was up to the PostHog project's settings. Requests carry the API key, and the response from /api/contributors/quickstart carries a newly issued key and, from the demo wallet adapter, a recovery phrase. The client now sets recordHeaders and recordBody to false. The project settings can't override that: in posthog-js 1.363.1 the recorder enables header capture only if the client's recordHeaders isn't false and the remote config turns it on, and body capture the same way. PostHog also records DOM text by default; it masks only inputs. EarnFromYourWorkPage rendered the new key and the twelve recovery words as text. Both now sit inside .ph-no-capture, PostHog's default blockClass, which session recording and autocapture skip. Sentry Replay 8.55 masks all text and records no network details by default, and a test pins that initTelemetry() loosens neither. Found while attacking the round-2 fix; astra's pack did not include telemetry.ts. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
LamaSu
added a commit
that referenced
this pull request
Sep 29, 2026
…rod-mock This brings #368's round-3 boundary into #408: the key held outside the store, authorizedFetch as its one reader, and the 25 migrated call sites. Conflicts resolved: - SponsorTelemetryPage, SystemDashboardPage, TelemetryPage and WalletPage: kept #408's rewrites. They read through apiGet (lib/api.ts, which sends with authorizedFetch) and supersede #368's mechanical migration of the same fetches. - __tests__/no-direct-auth-headers.test.ts: took #368's round-3 ratchet, which has no exemption list, so #408's edits to the old list no longer apply. tsc is clean and the dashboard passes 477/477. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
LamaSu
added a commit
that referenced
this pull request
Sep 29, 2026
…t-held-actions This brings #368's round-3 boundary into #413: the key held outside the store, authorizedFetch as its one reader, and the migrated call sites. Conflicts resolved: - agent/agent-client.ts and pages/AgentChatPage.tsx: kept #413's deletion. The held-actions chat replaces them, so #368's migration of their fetches no longer applies. - routing.test.tsx and agent-chat-held-actions.test.tsx set the key with adoptApiKey(), because the store's state no longer holds one. tsc is clean and the dashboard passes 320/320. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
LamaSu
added a commit
that referenced
this pull request
Sep 29, 2026
…ssion recording (N50) A credential minted by a confirmed held action is shown once, in the reply's revealed-secret panel. The panel rendered the credential as DOM text, and PostHog session recording captures DOM text: it masks only inputs. The panel now carries ph-no-capture, PostHog's default block class, which session recording and autocapture both skip. #368 did the same for the Earn page's new key and recovery words. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
LamaSu
added a commit
that referenced
this pull request
Sep 29, 2026
…hat-held-actions This brings #354's round-3 fixes (astra pack 19) into #413. The spatial and agent workspaces now own only their own address, and every sign-in and sign-out empties the query cache. It also brings #352's round-3 honesty fixes and #313, which #354 now stacks on. Conflict resolved: #354's new user-switch test in routing.test.tsx set the key with useAuthStore.setState({ apiKey }). #368's store, which #413 carries, keeps the key out of state, so the test uses adoptApiKey(). That still flips isAuthenticated, which is what App.tsx's cache-clearing subscription watches. tsc is clean and the dashboard passes 387/387. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This is the review loop's verify-before-fix step for astra's round-3 verdict on #368 (A03c, DO-NOT-SHIP at cfe3ce9). Each test fails at cfe3ce9, which reproduces its finding: - F1-A: an export hands out the key. readApiKeyForAuthorizedFetch() returns the signed-in key to any module, including one that reaches it by computed access. - F1-B: sendBeacon is outside the egress guard. A key-carrying beacon to another origin goes out. - F2: an operator-bound passkey registration without the authorized fetch does not fail closed. It makes an anonymous challenge request. Also reproduced by trace at cfe3ce9: a production module holding both of astra's bypass snippets type-checks, and no-direct-auth-headers passes 7/7. The snippets are computed access to the accessor, and localStorage.getItem("pcc-" + "api-key"), each sent with sendBeacon. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ithout the authorized fetch astra A03c F2 (MEDIUM): runPasskeyRegistration took authorizedFetchFn as optional. With an operatorId and no authorized fetch, the challenge went out through the plain fetch, without the key, so the passkey would register unbound to the operator. It now throws before any request. Reproduced at cfe3ce9 by 3b6d07d ("an operator binding without the authorized fetch fails closed, before any request"), which now passes. The 401 test binds an operator, so it now supplies the authorized fetch and checks the plain fetch is never called. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…s in as a string literal
The two Node scripts AgentLinkPage (/go) shows for the user to copy or
download were template literals in the page. They build the Authorization
header the user's agent sends, so the key-boundary ratchet had to exempt
those lines (DISPLAY_ONLY), and astra A03c F1 found that exemption
context-blind: the same line moved into running code would still pass.
- The scripts are now text assets (pages/agent-link/*.js.txt, imported with
?raw), byte-identical to what the page showed before for a plain ?q= and
for none. The dashboard shows them and never runs them, and no dashboard
module holds the header line, so DISPLAY_ONLY is empty. The next commit
removes it from the ratchet.
- Found while moving them: ?q= was pasted into the script's last line as
chat("${capability}"). A crafted link such as
/go?q=x"); require("child_process").execSync("..."); ("
made the script the user downloads and runs with node run code of the
link's choosing. Reproduced by agent-link-quickstart.test.tsx against the
old page (chat got "x", and the injected statement ran). ?q= now goes in as
a JSON string literal, through a replacer function so $& and $1 in ?q=
stay literal.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… and the ratchet has no exemptions astra A03c F1 (HIGH): at cfe3ce9 another production module could read the signed-in key and send it off-origin without failing the ratchet or passing the fetch guard. Reproduced by 3b6d07d (key-boundary-r4.test.ts), and by probe modules holding astra's two snippets: both type-checked, and both passed the old ratchet. - One holder, no reader. lib/authorized-fetch.ts now owns the key: a module-private variable, and its localStorage slot. It exports setStoredApiKey, hasStoredApiKey, authorizedFetch and installGatewayKeyGuard, and none returns the key. The store's readApiKeyForAuthorizedFetch export is gone, and the store only knows whether a key is held. - sendBeacon is under the egress guard. installKeyEgressGuard now wraps navigator.sendBeacon too, and inspects it synchronously: a string, URLSearchParams, FormData of strings, or binary body, and the URL. A key-carrying beacon to another origin is refused (returns false). A body it can't read synchronously (a Blob or a file) is refused, except to origins registered for such beacons: main.tsx registers the PostHog host, whose SDK sends Blob beacons on unload. A readable body is inspected whatever the origin. - The ratchet (__tests__/no-direct-auth-headers.test.ts) is rewritten as rules over every production module, with no exemption for any page or line. Only the key's owner and the boundary do what the rules forbid elsewhere: - the slot name appears only in the owner, and other storage calls name their slot with one plain string literal; - there are no computed globals, eval or new Function; - the owner and the store are never imported whole or by a computed path; - only gateway-base builds Authorization or Bearer, or uses sendBeacon, XMLHttpRequest or WebSocket. The owner's and the store's export lists are pinned at run time. Self-tests run astra's two snippets, and the quickstart header line moved into code, through the rules, and all three are caught. Negative controls on the real tree: - the snippets as probe modules still type-check and now fail the ratchet; - re-adding an exported reader fails two tests; - unwrapping sendBeacon fails the beacon tests. Still open, and parked with the operator: the key is persisted in JavaScript-readable localStorage, so hostile script on this origin can read it, or can replace a built-in the key passes through. Only an HttpOnly gateway session takes the key out of JavaScript's reach. The ratchet is a lint for our own modules, not a sandbox, and its header says so. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Brings in master's merges since cfe3ce9's base, including #353's job-execution read model and its onIdentityChange (review r3 of #353): the query cache is cleared when the signed-in identity changes. Two conflicts, both from N50 taking the key out of the auth store: - api/gateway.ts: keeps this PR's authorizedFetch, adds master's JobExecutionDTO import, and drops getAuthHeaders, which N50 removed. Master's getJobExecution goes through fetchAPI, which already sends with authorizedFetch. - stores/auth-store.ts: master's onIdentityChange compared s.apiKey, which this store no longer holds. The store now has keyEpoch, a counter bumped by every adoptApiKey() and logout(). onIdentityChange compares keyEpoch, address and sessionToken. adoptApiKey counts every call as an identity change, even with the same key: telling "same key" from "another key" would make it an equality test on the stored key. logout() is now one set(), so a sign-out is reported once, as master's test expects. Tests that follow: - stores/__tests__/auth-store-identity.test.ts (master's) now signs in with adoptApiKey instead of writing apiKey into the store, and checks that no key string appears in the state. - __tests__/no-direct-auth-headers.test.ts pins the store's exports to adoptApiKey, onIdentityChange and useAuthStore. Checked: tsc clean; dashboard 439/439 (24 files). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…iterals Four N50 test files held fake keys as pcc_test_ literals: - no-direct-auth-headers; - gateway-base; - key-boundary-r4; - setup-key-origin. The secret scanners flag that shape: the review-pack scan, and the gate the shell lane runs before every push. That would block #408's push, which merges this branch. The tests now build the same values at run time, for example ["pcc", "test", "r4boundary0123456789abcdef"].join("_"). Behaviour is unchanged. Checked: tsc clean; dashboard 439/439. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
LamaSu
added a commit
that referenced
this pull request
Oct 3, 2026
#408 lands after #352 and #368. This brings #368's round-4 key boundary: - the key owner exports no reader; - the sendBeacon guard; - the exemption-free lint; - the keyEpoch onIdentityChange. #368's merge of master 753feb4 comes with it, including #353's job-execution page and #393's AmountDisplay. One conflict, in use-pcc-data.ts. The imports keep this PR's authorizedFetch, which useKernel's N51 fix uses, and add master's job-execution imports. useJobs keeps the rowsOrThrow validation. Adapted to what the merge brings: - lib/demo-mode.ts names its sessionStorage slot with one literal. #368's round-4 lint allows no other form: a const name is a computed name. - no-production-mock's KNOWN_OFFENDERS drops JobDetailPage. #353 made it clean, and the ratchet requires the list to shrink. This is the change product-steward's #3985 asked for once #353 landed. - Comma-tolerant money asserts in the DePIN, SWF and Wallet honesty tests: #393 now groups thousands ("$12,450.00"). RC2 #4101 and design #4451 found these: a positive assert failed, and a negative one passed without checking anything. Checked: tsc clean; dashboard 688/688 (49 files). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
LamaSu
added a commit
that referenced
this pull request
Oct 3, 2026
Brings #368's test-only change: fake API keys are built at run time, not held as key-shaped literals, so the secret scanners stay quiet. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
LamaSu
added a commit
that referenced
this pull request
Oct 3, 2026
…-actions #413 carried #368's round-3 auth (cfe3ce9). This brings round 4: - no export returns the key; - the sendBeacon guard; - the exemption-free lint; - the keyEpoch onIdentityChange; - master 753feb4, through #368's own merge. Conflicts: - App.tsx: master's onIdentityChange replaces the boolean-keyed cache clear. - use-pcc-data.ts: useJobs keeps rowsOrThrow. An intermediate step: routing.test.tsx still signs in with useAuthStore.setState({ apiKey }), which #368's store no longer has (RC2 #4101 item 1), so 12 of its tests fail here. The next merge (#354 @d8ff4eb2) brings their current versions, which the next commit adapts to #368's key API. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
LamaSu
added a commit
that referenced
this pull request
Oct 3, 2026
…d-actions Brings #354's rounds 4 and 5 of account isolation (astra 19c and 19d), with #352 @2311bf36: - every account-scoped store resets, and its actions are bound to the account epoch; - the wallet and SIWE session end before the next account's shell mounts, failing closed; - the SSE hook cancels its reconnect. This is the first branch where #354's account boundary meets #368's key boundary (RC2 #4101 item 1). Conflicts: - stores/auth-store.ts: #354's onAccountChange compared s.apiKey, which #368's store doesn't hold. It now follows keyEpoch, which every adoptApiKey() and logout() bumps. #368 removed getAuthHeaders, and it stays out. - App.tsx: takes #354's account boundary whole. It sits on #368's keyEpoch through onAccountChange and onIdentityChange. Tests follow #368's key API: - account-isolation-r5 signs A in with adoptApiKey("pcc_test_key"), not useAuthStore.setState({ apiKey }). routing.test.tsx already used adoptApiKey here. - #368's ratchet pins the store's exports to adoptApiKey, onAccountChange, onIdentityChange and useAuthStore. Checked: tsc clean; dashboard 591/591 (33 files). That includes #354's isolation tests and #368's key-boundary ratchet over #354's new code. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…entity signal and the round-4 probe past the lint (fail at 564a22b) - N1: setStoredApiKey() called directly changes the key without an identity change; a key cleared or set that way leaves isAuthenticated stale. - F1: astra's probe (the global aliased, Reflect.get by a concatenated name, the slot by a concatenated name, sent by an Image) and fetch replaced to watch authorizedFetch: no rule catches any line. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…int reads code, not lines (astra A03d N1, F1) - N1 (HIGH): setStoredApiKey() reports every change to onStoredKeyChange's listeners, without the key. The auth store's one listener moves keyEpoch, follows isAuthenticated, and on sign-out clears the wallet fields in the same set(), so a module that sets the key directly can no longer replace it behind onIdentityChange. adoptApiKey and logout go through it. - F1 (HIGH): the lint adds syntax rules read from the parsed file, so strings, comments and JSX text are never taken for code and a string built from literals is judged by what it spells. They catch every line of astra's round-4 probe (the global aliased; Reflect.get; "local" + "Storage"; "pcc-" + "api-key"; new Image()) and fetch replaced to watch authorizedFetch, plus window handles, Function and .constructor(), string timers, import.meta.glob, and prototype or navigator/document writes. Four casts of window became named reads (HandTracker, usePasskey, passkey-registration). The header now says what the lint is: a check on our own modules, not a sandbox; the JS-readable key stays the operator's HttpOnly-session decision (#4459). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ported once (pins the store's key listener) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…tation-API writes past the lint (fail at dc70242) - N1: a listener registered before the auth store throws; setStoredApiKey changes the key but keyEpoch stays. - F1: Object.defineProperty / defineProperties / assign / setPrototypeOf on Headers.prototype, Request.prototype or navigator: no rule catches them. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…e, and the lint sees mutation-API writes (astra A03e N1, F1)
- N1 (HIGH): setStoredApiKey tells every listener every change, each in its
own try; the first error is rethrown once all have heard, so the auth
store's identity change can't be skipped by an observer that failed
first. A listener that changes the key again doesn't recurse: its change
goes to everyone after the current round, up to 32 rounds, then an error.
- F1 (HIGH): global-write also catches Object.defineProperty /
defineProperties / assign / setPrototypeOf, and __defineGetter__ /
__defineSetter__, when they change a global (window, navigator, document,
anything reached from one), a built-in a request or the key passes
through (Headers, Request, Response, Storage, …) or any prototype.
Object.assign({}, …) and defineProperty on an app object pass.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…essor definers Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…f Object, helper libraries, and any call handed a built-in's prototype (fail at 7476c23) Self-found while writing pack A03f: the round-5 rule matched Object.<api> only, so O.defineProperty(Headers.prototype, …) with O = Object, _.assign, $.extend, merge(…) and patch(Headers.prototype, …) all passed. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ceiver, and any call handed a built-in's prototype (self-found, A03e F1's family) - defineProperty, defineProperties, setPrototypeOf, assign, extend, merge, mixin and defaults are judged by the object they change, whatever they are called on (Object, an alias of it, a helper library). - A call handed a built-in's prototype (Headers.prototype, …) is caught whatever it does with it. - location.assign, merging app state and Array.prototype.slice.call pass. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… target that is no prototype (navigator) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
LamaSu
added a commit
that referenced
this pull request
Oct 3, 2026
…cap and a protected target through a local alias (fail at 3c2aede) - N1: with the store registered first and a listener alternating the key, the cap clears a change already committed to the key and storage: the store stays signed in while no key is held, and keyEpoch is one short of the committed changes. - F1: const p = Headers.prototype (or const nav = navigator), then Object.defineProperty(p, …): no rule catches it. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…lands, and the lint follows local aliases of protected targets (astra A03f N1, F1)
- N1 (HIGH): a change made in the last of 32 delivery rounds is refused
before it touches the key or storage, so the key never holds a value its
listeners weren't told of: the store and the key agree, one epoch per
committed change. No committed change is dropped any more.
- F1 (HIGH): global-write follows local aliases of protected targets, to a
fixed point: const p = Headers.prototype, const nav = navigator,
const { prototype } = Headers, an alias of an alias, a plain reassignment,
through parentheses, casts, ?:, || and ??. Such a name counts as protected
as a mutation target and as an assignment root, and a prototype alias also
when handed to any call. rootOf sees through casts. Aliasing document to
read from it, and reassigning app state, pass.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… with an out-of-order chain and a renamed binding Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… changes it inside (fails at 695a57d) Self-found while writing pack A03g, in A03f F1's family: patch(navigator), an alias of navigator handed to a call, and wrap(Headers) all passed, since only a built-in's prototype counted when handed to a call. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…y or through a local name holding it (self-found, A03f F1's family) navigator, document and the built-ins (Headers, Request, …) can be changed inside whatever they're handed to, so handing one to a call is caught, as a built-in's prototype already was. Only names holding the object itself count, not values read from one (const w = window.innerWidth stays free). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…bject property, class field, this.x, a return, an array, getPrototypeOf, __proto__ (6 of 7 fail at 00ca50c) Self-found while writing pack A03g, in A03f F1's family. Following aliases name by name keeps missing a form; astra's alternative is to reject the aliasing itself. The window.navigator alias is already caught and stays as a guard. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ead of chasing aliases (astra A03f F1, the whole family) Following aliases name by name kept missing a form (an object property, a class field, this.x, a return, an array). astra's alternative closes them all: navigator, document, the built-ins, any of those reached from window, and any built-in's prototype may appear only where they are read: a member read, a call or construction, typeof, the right of instanceof or in. Held in a variable, passed, returned, stored, spread or destructured, they are caught, so no name can hold one to change it later. getPrototypeOf, __proto__, .constructor and the legacy accessor definers are caught too. The alias resolution and the mutation-API branch it needed are gone; global-write keeps its assignment checks. No production module breaks it. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…gnment check judges by root only
A { navigator } shorthand passes the object itself, and is caught. The
separate X.prototype.y = regex is gone: a built-in's prototype is already
caught by its built-in root, and app classes' own prototypes carry no key.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…element access or by a read that returns it, and the constructor overbreadth (fail at c57a067) At c57a067: - F1 (HIGH): both of astra's bypasses pass the lint: const p = Headers["prototype"] and const nav = navigator.valueOf(), each then handed to Object.defineProperty. - F1's family, self-found: nine more pass. They are the same guarded names by element access (["__proto__"], ["getPrototypeOf"], ["defaultView"], ["constructor"] run as code), other reads that return a protected object (Headers.valueOf(), Array.prototype.reverse()), and the document reached by a walk (documentElement.parentNode, getRootNode(), ownerDocument). A tenth (Storage["proto" + "type"]) is already caught, and stays as a pin. - F2 (MEDIUM): ordinary.constructor.name, a constructor comparison and typeof x.constructor are all rejected. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…hand back a protected object, and checks every change's target (astra A03g) F1 (HIGH): the read-only rule assumed a read can't create an alias. Two reads did: Headers["prototype"] (element access wasn't recognized) and navigator.valueOf() (it returns its receiver). Three layers now: - A literal element read counts as the member it names, in every syntax rule. So x["prototype"], x["__proto__"], x["getPrototypeOf"], x["defaultView"] and x["constructor"](...) are judged like their dotted forms. - No read hands back a protected object without naming it: valueOf on one, a method called directly on a built-in's prototype (Array.prototype.reverse() returns it), or a walk to the document (parentNode, getRootNode, ownerDocument; production uses none). - The mutation-target check astra asked to restore: a mutation API's target, a written or deleted property's object, is followed through the module's own names, member reads and calls that return their receiver. If it may be navigator, document, a built-in or the global, it fails. A value a call builds (createElement) or a name the module never binds is not followed, so ordinary DOM and object code passes. F2 (MEDIUM): x.constructor read for its name, compared or asked its type no longer fails. Production passes every rule unchanged. New cases pin each layer on its own. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…on-target check now covers, and pin window.<object> on its own The assignment root check (WRITE_ROOTS, BUILTINS via rootOf) flagged a property written on navigator, document, a global or a built-in. The mutation-target check covers the same roots, and follows aliases too, so the root check no longer decided any case; its control stopped failing. It is removed. fetch replaced by name stays its own check. A one-layer case pins the read-only rule's window.<object> branch: const nav = window.navigator; alone, with no later write. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…he held rule already decides fetch = spy puts fetch in a write position, which the read-only rule flags as a held built-in; the separate name check never decided a case, and its control didn't fail. Removed, so every remaining check has a control that fails. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Gate A, HIGH: N50 (steward #2640; product-steward #2602). Dashboard only. Base: master. Head 74f758b answers coord-watch's DO-NOT-SHIP (#2857); see "Review round 1".
The leak
SetupWizardPage.tsx:53fetchedhttp://localhost:3200/api/capabilitieswith the signed-in user'sAuthorization: Bearer <key>on every/setupload, in every build. It then claimed "Gateway connected at localhost:3200".SetupAgentPagedefaultedgatewayUrltohttp://localhost:3200for its three key-bearing requests (/api/setup/scan-network,/api/ai/identify-machine,/api/setup/register-device), and its only caller (line 901) never overrode it.In production, both handed the user's key to whatever process listens on port 3200 of their own machine.
The fix
lib/gateway-base.tsholds the configured gateway: same-origin in production, orVITE_PCC_URL, which the auth store already uses to validate the key. It is the only origin that may receive the key./api/healthwith no key, and the page says "Gateway reachable".Review round 1: coord-watch DO-NOT-SHIP (#2857), fixed at 74f758b
Sol found that the setup leak was fixed but the key was still attached regardless of destination. Each finding and its fix:
/apicallers went to the dashboard origin, and gateway-base callers toVITE_PCC_URL. Fix: there is one gateway origin.fetchWithKey()(lib/gateway-base.ts) is the only code that puts a key on a request. It resolves the final URL (including//hostand/..//host), refuses any other origin before making a request, and only then adds the header. It is reached throughauthorizedFetch()(lib/authorized-fetch.ts). These now use it:lib/api.ts;api/gateway.ts(every react-query hook);http://localhost:3200in dev builds and tohttps://capability.networkin production builds, even from staging.VITE_PCC_URLwas not validated. Fix: it must be an absolutehttps:origin;http:is accepted only on loopback in a dev build. A bad value fails closed: no request carries a key, and the reason is logged once.installKeyEgressGuard()runs first inmain.tsx. It rejects anyfetchthat carries apcc_live_/pcc_test_key, or the stored key, to another origin, whether in a header, the URL or a string body. The refusal message never includes the query string.__tests__/no-direct-auth-headers.test.tsenforces "key construction only inside the wrapper" as a shrink-only ratchet. It lists 31 legacy modules with their owning lanes, and fix(dashboard): no production mock — pages show live data, say they aren't live, or say they couldn't load (Wave 0, D8) #408 removes 5 of them.Also: SetupAgentPage's
gatewayUrlparameter is gone (product-steward, #2849).Tests
gateway-base.test.ts32, ratchet 4, setup/earn page tests 3).tscand the build are clean.lib/api.tsorapi/gateway.tsfails 3, 2 and 2.returns/pcc-shell-work/e2e-n50-20260924/). It ran a real gateway (production mode) and a sink server on another origin:/setup's liveness check carries no key, and nothing goes to:3200;VITE_PCC_URL=http://…sent no key-bearing request at all.🤖 Generated with Claude Code
https://claude.ai/code/session_01VU6exGFC7uLukeDF2GBUpQ