Conversation
…s (F3) Gateway #2831: align the /api/jobs/:id read family with #353's predicate through one shared helper, not copies. Before, any API key read any job's record, status, evidence, drift alerts and settlement; only /execution checked the caller. readmodels/job-read-gate.ts gateJobRead reads the job row, applies TENANT_ENFORCE, and authorizes with authorizeJobRead: an admin (X-Admin-Key), the kernel operator, or the recorded buyer. refuseJobRead answers 401 for an anonymous caller, 503 when the record cannot be read, and otherwise the ROUTE'S OWN 404 for a missing job, so 'not yours' and 'no such job' are byte-identical. Gated: GET /api/jobs/:jobId, /execution (now through the gate), /status, /settlement, /evidence and /drift-alerts; GET /api/settlement/:jobId (via the legacy settlement loader); and the job-id form of GET /api/evidence/:jobId. The hash form stays content-addressed for the oracle. agent: pcc-readmodels (c255d7dc)
readmodels/job-read-gate.test.ts (42) covers all 8 reads: anonymous 401; a stranger gets the byte-identical answer for a missing job; the kernel operator (case-insensitive), the recorded buyer and an admin read; a wrong admin key is a stranger; another tenant's job is 404 under TENANT_ENFORCE; a failed row read is 503; the evidence-by-hash form stays content-addressed. Existing route tests that were not about authorization now read as a party through helpers/job-read-party.ts (the seeded kernel operator, a stand-in for the API gate). The facade-mocked compliance wiring test mocks the gate open. Gateway 3107 passed / 6 skipped / 0 failed. Mutation check: 8/8 killed. agent: pcc-readmodels (c255d7dc)
CLAUDE.md and docs/AGENT_INTEGRATION.md state the rule for the job read family: an admin, the kernel operator or the recorded buyer; a 404 identical to a missing job for anyone else; 401 without auth. agent: pcc-readmodels (c255d7dc)
…#403 #353's review-r3 fixes change the predicate this family's gate is built on, so the merge adapts the gate. The merged tree does not build without it. - gateJobRead: identity first (precheckJobRead). No credential is 401 and a credential without a PROVEN wallet (WP-A's req.provenWallet, SIWE) is 403 identity_unverified, both before the job row is read. Then the job, TENANT_ENFORCE, and authorizeJobRead(job, provenWallet): kernel operator or recorded buyer, compared as addresses. An operatorId or email is never trusted as an identity. - refuseJobRead and the two legacy settlement routes answer 401 and 403 with the shared JOB_READ_REFUSAL bodies, the same for every job id. - Conflict in routes/jobs.ts: the execution route keeps #403's gate call. - Tests: the stand-in gate sets a proven wallet for a wallet principal (helpers/job-read-party provenWalletFor), the F3 buyer is a wallet, and every route in the family pins the new 403 for a key that claims the operator's or buyer's id without proof, identical for a missing job. Effect: until WP-A (#326) merges, no caller has a proven wallet, so the whole job read family is admin-only (fail closed). After it, email-provisioned and legacy keys cannot read job records. That is the operator's call (decision posted with the #353 triage). Tests: gateway 3131/6/0, dashboard 258; tsc clean. Gate mutation check: 5/5 killed. agent: pcc-readmodels (c255d7dc)
LamaSu
added a commit
that referenced
this pull request
Sep 29, 2026
…ntity rule) into #441 agent: pcc-readmodels (c255d7dc)
LamaSu
added a commit
that referenced
this pull request
Sep 29, 2026
…d id is 403 After #403 took #353's review-r3 identity rule, the job read gate requires a proven wallet (WP-A's req.provenWallet). The test's stand-in gate sets one for a wallet principal. A key that only claims the operator's id, without proof, is pinned as 403 identity_unverified. Tests: gateway 3144/6/0, dashboard 258; tsc clean. agent: pcc-readmodels (c255d7dc)
LamaSu
added a commit
that referenced
this pull request
Sep 30, 2026
…ir read models (N68) Astra r1 on #448 (610607c), CRITICAL; reproduced first. POST /api/query answered its kernel_health, network_status, operator_stats and find_capability intents with the stored shop_kernels and capabilities rows. Any self-provisioned key read every kernel's exact coordinates and street address. The four new tests failed at 610607c with the canary's exact latitude in each answer. Those intents now read through the kernel and capability facades, whose populators project a site's location: coarse unless its operator opted in, and no street address. The answer text is filled from the same rows. The job intents are unchanged here; they belong to the job-read gate (F3, #403). agent: pcc-readmodels (c255d7dc)
LamaSu
added a commit
that referenced
this pull request
Sep 30, 2026
…eath it (N79 round 2) astra's pack 116b on #462 @38cf4116: DO-NOT-SHIP, with three HIGH findings and one MEDIUM. Each was reproduced first with a failing test at 38cf411 (n79-refund-vs-settlement.test.ts): - F1 HIGH: a repeated failure write while /complete is in flight refunded ("expected 'refunded' to be 'skipped'"); - F2 HIGH: cancelling one job refunded an escrow another job shares; - F3 HIGH: a refund landed while SettlementService.releaseMilestone awaited the chain ("expected 'refund_pending' to be 'skipped'"); - F4 MEDIUM: a completion that failed after a failure write left the failed job's escrow funded. Two more were reproduced the same way: - the keeper, which sweeps a findAll() snapshot across awaits, drove an escrow given back mid-sweep (astra's Q2 note); - the raw chain release route let a refund land while it awaited the chain (the F3 class, found by extending the audit). The fix is one mechanism: DURABLE SETTLEMENT OWNERSHIP on the escrow row. - A settlement takes its escrow with a compare-and-set from funded/active to `completing` (already in @pcc/spec's vocabulary), synchronously at its claim: /complete (same stretch as the job claim), resume-settlement, SettlementService.releaseMilestone (before the chain await) and the raw release route (by contract address). - A refund never lands on a `completing` escrow, however often the job's mutable status is rewritten (F1, F3, raw route). - A settlement that gives up BEFORE recording evidence hands the escrow back (prior status). If the job ended meanwhile, the escrow goes to the payer then, in one transaction (F4). After evidence exists, the settlement keeps it, since resume-settlement continues it. - A release that went through records its milestone `released`, and the escrow `completed` once every milestone is. - The refund gives back a whole escrow only if no other job references it; a shared escrow is skipped as `escrow_shared` (F2). - The keeper re-reads the row right before each drive. Tests: the six reproductions, plus the gaps astra named: - a failure on the LAST write rolls back every milestone and the status; - a failure after evidence keeps ownership; - resume takes a legacy row (the write repeated, as in F1); - a partial release hands the escrow back; - failed releases (service and raw route) hand it back. All 40 mutants (both rounds) are killed. The gateway suite passes: 3053 passed, 6 skipped (six threads; the Spark is at load 37, where two /complete tests time out at 5 s and pass alone). tsc is clean. It merges cleanly with #326, #385, #403, #424, #441, #445, #450, #451 and #459.
LamaSu
added a commit
that referenced
this pull request
Sep 30, 2026
…party (F1) The previous commit gates both legacy settlement routes, so the older route tests in paid-job-flow.test.ts and settlement.test.ts, which read them with no identity, got 401: 9 failed in the full suite at cec7fce. They now read as the seeded kernel-nyc operator's proven wallet, through the same stand-in gate helper #403 uses (helpers/job-read-party.ts, byte-identical), so the stack merges cleanly. agent: pcc-readmodels (c255d7dc)
…job-read-family-auth #382's round-1 fixes come forward: - milestones[].forThisJob from the attributed milestone, plus association; - the corrected route comments; - the job-party test helper. It was already byte-identical here. Conflicts resolved to this branch's family gate: gateJobRead and its unauthenticated, identity_unverified, not_found and unavailable kinds replace #382's inline precheck and authorize. Both implement #353's rule, and the gate is the shared one. The legacy-settlement integration tests keep #382's explicit stand-in gate (identity only from headers), so its 401 and 403 tests hold. Their reads name the kernel-nyc operator explicitly. agent: pcc-readmodels (c255d7dc)
…read (F3) Astra round 1 on #403, CRITICAL 1. Reproduced first: 18 tests fail at 74225e1. GET /api/jobs listed every job to any caller, because the gate covered single-job reads only. - jobsReadableBy (readmodels/job-execution.ts) gives the jobs a proven wallet may read. The rule is authorizeJobRead's: the wallet operates the job's kernel, or is the buyer recorded by the job's single negotiation session. It uses one read of the kernels and one of the sessions. - jobReadScopeOf (job-read-gate.ts): - refuses as the gate does (401, 403, 503); - an admin reads every job, within its tenant under TENANT_ENFORCE; - a proven wallet reads jobsReadableBy's jobs; - a tenant-less job matches only a tenant-less caller, as in the gate. - jobRecordFilterOf serves routes that mix records of many jobs. A record naming a job is kept only when that job is readable. A record naming no job is kept. - gateJobRecordRead gates a record found by its own id (an evidence bundle) on its job, checking identity first. - refuseJobRead also takes a scope's refusal. - GET /api/jobs filters by the scope before counting and paging (JobFilters.jobIds). - Timing (MEDIUM): a missing job, another tenant's job and a stranger's job now make the same kernel and session reads. The existing /api/jobs list test reads as the seeded operator's proven wallet. agent: pcc-readmodels (c255d7dc)
…n the job read gate (F3) Astra round 1 on #403, CRITICAL 1: the gate did not cover the whole job-read surface. Reproduced first. - /api/telemetry/pipeline/:jobId: the gate. A job the caller may not read gets the empty timeline a missing job gets. - /api/telemetry/active and /jobs: the scope. - /api/telemetry/logs: - with a jobId, the gate (no lines for an unreadable job); - without one, the record filter; - the limit applies after filtering. - /api/telemetry/logs/stream: the history and the live fan-out go through the caller's record filter. The filter is fixed at connect time, so it fails closed. - /api/batches/by-job/:jobId: the gate. An unreadable job gets the empty list a missing job gets. - /api/sensors/readings/:channel: the gate with a jobId, otherwise the record filter. - /api/compliance/evidence/:bundleId and /tier-compliance: gateJobRecordRead on the bundle's job. An unreadable one gets the missing-bundle 404. - /api/query: the job_status and job_history intents answer only readable jobs. - /api/print-and-mail/:jobId: the identity precheck, then admin only. Its :jobId is a courier job in the job-offers store, whose poster and driver ids are self-declared, so no party rule can be proven yet. This goes to gateway with carrier/Lob (CRITICAL 3). Existing tests: the compliance-routes gate mock gains gateJobRecordRead, and print-and-mail reads as an admin. agent: pcc-readmodels (c255d7dc)
…WE session proves its wallet (F3) Astra round 1 on #403, CRITICAL 2. Reproduced first. /sse/stream/job/:jobId had its own ownership check, off by default, which trusted an API key's self-declared operatorId. - After SSE auth, the route runs gateJobRead: - 401 and 403 as the gate refuses; - a job the caller may not read gets the missing-job 404. - resolveSSEAuth sets req.provenWallet from a SIWE session (cookie, bearer or ?token=), lowercased, as WP-A does for HTTP. An API key sets none. - The unused isJobOwnershipCheckEnabled/checkJobOwnership path is removed. agent: pcc-readmodels (c255d7dc)
…a missing job (F3) f3-job-read-surface.test.ts has 21 tests. The 18 written first all fail at 74225e1. They cover: - no credential (401) and no proven wallet (403) on 8 routes; - a stranger reads exactly what a missing job gives, on each gated route; - the compliance bundle routes, and /api/query's job intents; - SSE: anonymous gets 401 and a stranger 404, before any event; - timing: a stranger's job and a missing job make the same reads. Three were added during the fix: - TENANT_ENFORCE: a job moved to another tenant leaves the party's list, and its /execution returns 404; - the live log stream, over a real socket: a stranger and an anonymous caller get no line of another job, and the party gets its own; - the party of a job still receives that job's events on the per-job SSE stream. The print-and-mail case follows the admin-only design: a stranger and a party both get the missing-job 404, and an admin gets 200. agent: pcc-readmodels (c255d7dc)
LamaSu
added a commit
that referenced
this pull request
Oct 1, 2026
#403 answered its round-1 review in 4988a19. Among its changes, the compliance bundle routes now run gateJobRecordRead on the bundle's job: identity first, then the job read rule. That closes this PR's r1b CRITICAL: GET /api/compliance/evidence/:bundleId returned eventCount for another tenant's bundle with no object authorization. #441 stacks on #403. The merge is clean. agent: pcc-readmodels (c255d7dc)
LamaSu
added a commit
that referenced
this pull request
Oct 1, 2026
…Count (#441 r1b CRITICAL) Cross-family review r1b of #441, CRITICAL. GET /api/compliance/evidence/:bundleId returned the bundle's eventCount (this PR loads the events for it) with no object authorization. The merge of #403 (b6b688f) gates the route with gateJobRecordRead, on the bundle's job, identity first. The surface test now also asserts that the job's party reads eventCount and a stranger's answer does not contain it. agent: pcc-readmodels (c255d7dc)
…ches and nested bindings (F3 r3) Cross-family review r2 of #403 (rm-f3-403-r2-4988a191, DO-NOT-SHIP) found four bypasses. f3-r3-bypasses.test.ts reproduced all four at 4988a19 (11 failed, 2 passed). Each is fixed here: - CRITICAL: the kernel, device and batch SSE streams only called resolveSSEAuth, which passes anonymous callers when SSE_AUTH_REQUIRED is unset. They carry job-bound sensor readings. They now take the kernel's read rule (gateKernelRead): an admin, or the kernel's operator with a proven wallet. No credential gets 401, an unproven one 403, and anyone else the 404 an unknown kernel, device or batch gets, before any subscription. - CRITICAL: GET /api/batches and /api/batches/:batchId returned every batch, and its slots name each sample's job and buyer. The list is now the operated kernels' batches (all of them for an admin). The detail goes to an admin or the operator; anyone else gets the missing batch's answer. By job, a buyer sees only its own job's slots of a shared batch. - HIGH: jobRecordFilterOf read only a top-level jobId. recordBindingsOf now reads job and kernel bindings at any depth, in each key spelling. A malformed binding fails closed, and a record that names neither is an admin's, since its text can name any job. The live log stream uses the same rule. Anonymous callers get 401 on the mixed-record routes. - MEDIUM: the authorization reads were keyed by the requested job. jobReaderOf now reads only by the caller's wallet: the kernels it operates, and the jobs whose single session names it. A refusal costs the same reads whether the job exists or not. Only the requested row's own lookup is keyed by the request. Also (astra r2 on #382, MEDIUM): both legacy settlement routes now have a test pinning the generic 503 when the job read or the authorization read throws, for a party, a stranger and a missing job alike. agent: pcc-readmodels (c255d7dc)
…nd batch streams (F3 r3) CLAUDE.md's read-access paragraph, the SSE tables (in CLAUDE.md and docs/AGENT_INTEGRATION.md) and AGENT-SKILLS.md's rows now say who may read a kernel's batches and its streams, and how log lines and sensor readings are filtered per record. agent: pcc-readmodels (c255d7dc)
…the caller may read (F3 r3) Found while fixing round 3. Reproduced at b170cfa: anonymous got 200 from GET /api/kernels/kernel-nyc/jobs, and the public kernel detail embedded job-001 (f3-r3-kernels-repro-b170cfaf.log). - GET /api/kernels/:kernelId/jobs now checks identity first, before the jobs are read (jobReadScopeOf): no credential is 401 and an unproven one 403. A proven wallet gets the kernel's jobs it may read; an admin gets all of them. - GET /api/kernels/:kernelId stays public. Its recentJobs hold only the jobs the caller may read. recentJobsScope ("all", "readable_by_caller" or "unavailable") says what the list covers, so an empty list never reads as "no jobs". - The other getById callers (lob, carrier, compose) read only operatorAddress. - The API tables in CLAUDE.md and docs/AGENT_INTEGRATION.md say so. agent: pcc-readmodels (c255d7dc)
LamaSu
added a commit
that referenced
this pull request
Oct 3, 2026
… read only evidence the caller may read (PX-7 r3) Cross-family review r2 of #441 (rm-px7-441-r2-e411a9b3, DO-NOT-SHIP), CRITICAL: the capability compliance report named each recent bundle's job and hash to anyone, and GET /api/evidence/:hash served the whole canonical envelope (its events' sources and payloads) to anyone who had the hash. px7-r3-evidence-bypass.test.ts reproduced it at d2ffa1d (#441 @e411a9b3 plus the #403 r3 merge): 5 failed of 5. - GET /api/capabilities/:capabilityId/compliance checks identity first: 401 without a credential, 403 without a proven wallet. Then the facade computes the whole report only from evidence the caller may read (ComplianceReportAccess): all of the kernel's for an admin without a tenant, or the kernel's operator; anyone else gets the bundles of its own jobs. evidenceScope and bundlesConsidered say which. Events, capture verdicts, drift and the evidence list all come from those bundles only. - GET /api/evidence/:hash serves the envelope only to: - the settlement oracle's verifier read key (X-Verifier-Key, compared constant-time with PCC_VERIFIER_READ_KEY; at least 32 characters; unset grants nothing; it is not an identity elsewhere); - an admin; - a party to the bundle's job (gateJobRecordRead, identity first, then the bundle lookup). Anyone else falls through to the job-id form, so it gets exactly what an unknown hash gets. - compliance-routes.test.ts mocks the gate open; its mock gains the scope helpers, and the delegation test expects the access rule. - The dashboard's ComplianceReportDTO mirror gets the two fields. The docs (CLAUDE.md, docs/AGENT_INTEGRATION.md) give the rule and the new env var. Until the oracle sends X-Verifier-Key and the gateway has PCC_VERIFIER_READ_KEY, the oracle's API-key fetch gets 403 and fails closed. Provisioning that key is the operator's decision. agent: pcc-readmodels (c255d7dc)
… batches show only the opportunity, filters judge records as sent (F3 r4) Cross-family review r3 of #403 (rm-f3-403-r3-f004b709, DO-NOT-SHIP). f3-r4-bypasses.test.ts at f004b70: 7 of 11 failed. The 3 positive cases and MEDIUM 6 passed. - CRITICAL 1 (kernel records ignored TENANT_ENFORCE): each job-bound part of a kernel's record now follows the tenant-aware job rule (jobReadScopeOf): - jobPartScopeOf projects batch slots in the list, the detail and by-job; - streamEventFilterOf drops any job, kernel, device or batch stream event, and any batch-detail event, that names a job the caller may not read. An admin without a tenant keeps everything. - CRITICAL 2 (shared batches returned every claim publicly): anyone now gets sharedBatchFace, the opportunity with claimedSlotCount and no claims. Only an admin without a tenant, or the kernel's operator, sees the claims, on /open, /:batchId and the claimedBy list of /availability. - HIGH 3 (?jobId= reads skipped the nested filter): logs, sensor readings and the pipeline timeline now apply the gate and the record filter together. The pipeline timeline is the same class of path; it reproduced against f004b70's telemetry.ts. - HIGH 4 (the filter saw the live object, not what was sent): asSent and keepAsSent make every filter judge the record's JSON text parsed back, and every route sends that form. This covers REST logs and sensor readings, the log stream's history and live fan-out, all four SSE topics, and batch-detail events. A toJSON or a getter can no longer make the inspected and the sent records differ. - MEDIUM 5 (record-by-id timing): gateJobRecordRead now refuses a non-party from the record's own job and kernel, using the caller's wallet-keyed reader, before any job row is read. Parties and admins then go through gateJobRead. A stranger's refusal makes the same reads whether the bundle exists or not. - MEDIUM 6 (unmatched SSE paths leak slots): NOT REPRODUCED, so no code change. topicSSE is registered encapsulated (server.ts:844), so its onRequest hook runs only for its own matched routes. A pin test shows 20 unmatched requests followed by a valid stream: 200. agent: pcc-readmodels (c255d7dc)
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.
Summary
This is readmodels F3. The gateway asked for it in #2831: align the
/api/jobs/:idread family with #353's predicate through one shared helper, not copies.Before: any API key could read any job's record, status, evidence, drift alerts and settlement. Only
GET /api/jobs/:jobId/executionchecked the caller.Now: one gate,
readmodels/job-read-gate.ts, runs first on every read of a job's records. It reads the job row, appliesTENANT_ENFORCE, and authorizes the caller withauthorizeJobReadfrom #353.X-Admin-Key), the job's kernel operator (case-insensitive) or its recorded buyer reads the job.GET /api/jobs/:jobIdJOB_NOT_FOUND404)GET /api/jobs/:jobId/executionGET /api/jobs/:jobId/statusGET /api/jobs/:jobId/settlementGET /api/jobs/:jobId/evidenceGET /api/jobs/:jobId/drift-alertsGET /api/settlement/:jobIdSETTLEMENT_NOT_FOUND404)GET /api/evidence/:jobIdStacked on #382 (
fix/legacy-settlement-truth), which is stacked on #353. The diff is F3 alone. The CI suites run only on PRs with basemaster, so the counts below are my local runs.Tests
readmodels/job-read-gate.test.ts(42 tests) runs every route in the family on a real store:TENANT_ENFORCE, another tenant's job is 404;__tests__/helpers/job-read-party.ts, the seeded kernel operator standing in for the API gate. The facade-mocked compliance wiring test mocks the gate open./api/jobs/:id, drift-alerts, evidence-by-job and the legacy loader left ungated.Found while doing this (gateway's routes, reported on the bus)
PATCH /api/jobs/:jobId/statusauthorizes the caller bykernel.operatorIdandjob.submittedBy, and neither field exists:operatorAddress;submittedBy.So every authenticated PATCH is 403, and a request with no caller (possible only where the API gate is not in front) skips the check entirely.
🤖 Generated with Claude Code
https://claude.ai/code/session_01A2ZvsqsAb7jC7AC8Viisqn