Repository navigation
feat(gateway): server-side unmet-demand capture, flag default OFF (R44 D2, stacked on #365) - #387
Merged
Merged
Conversation
…URE_ENABLED (R44 D2) The gateway half of R44, stacked on the kit-demand signal (steward ruling #2634). With PCC_UNMET_CAPTURE_ENABLED=true (default OFF), the four first-party intent capture points (requests, negotiation, nl-query, A2A) record: - the authenticated principal as actorType "authenticated_operator", taken only from the server-side auth context: apiGate's req.operatorId, or the API key the A2A route resolves itself. Never from the request body. All capture-time identity reading lives in services/unmet-capture.ts (principalFromApiKey), with a TODO to move to WP-A's identity binding. - fulfillmentPath and unmet from a pure matcher over live supply (CapabilityFacade.listByType: instances, availability, kernel status, tiers). Live supply decides served versus unmet; the CSD registry only decides the key (URI if a CSD exists, else a kebab slug). The first design checked the registry first and misreported a type with live instances but no CSD (hplc on the seeded store) as unmet; a probe caught it and a regression test pins the fix. Unconditionally, POST /api/intents/ingest now drops caller-supplied fulfillmentPath and unmet (server-owned fields). "authenticated_operator" is added to AnalyticsEvent.actorType, and the event bus references the spec union instead of duplicating it. stripServerOnlyDemandFields' generic is loosened to accept zod-inferred envelopes. Flag OFF: the capture points emit exactly what they did before; a route test pins this. Tests: 17 unit + 10 route/seam, including the seam from route capture through UnmetDemandLens to toPublicOpportunityAggregate (4 authenticated operators stay private, 5 publish "5-9", a repeat operator adds volume not breadth, and proposed types are never public). Premises about the seed are asserted, not assumed. Full @pcc/gateway suite: 189 files, 3008 passed, 6 skipped, 0 failed; tsc clean. @pcc/spec 842/842 and @pcc/demand-intel 46/46, tsc clean. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JBSkBhKBWPA9AeCJ7J5G7a
LamaSu
added a commit
that referenced
this pull request
Sep 28, 2026
…ound-1 F1-F5) The round-1 cross-family review of #365 @e5fa22ba (pack 10) returned DO-NOT-SHIP as a public feed. This commit closes the pure-layer half of each finding. The publisher (no-parameter read surface, write-once release ledger, approved set) belongs to pcc-kits (#3405). F1 toPublicOpportunityAggregate has no policy parameter. One frozen PUBLIC_RELEASE_POLICY: k = 5, floor authenticated_order, 24 h grace. F2 Only canonical UTC calendar-month periods ("YYYY-MM"), released only after close + grace. Any other window, an open period, or a signal computed before its period closed throws. buildPublicRelease() returns one deterministic release with a sha256 digest. The lens gains computeForPeriod(). F3 UnmetCapabilitySchema requires the key form to match the reason: no_capability_type only as a slug, every other reason only as a CSD URI. resolveCapabilityKey checks the reason first and can take a registered set. The projection emits only exact members of the publisher's approved set that pass isPublishableCapabilityId (slug at most 40 chars, at most 3 hyphens, no run of 4+ digits). The release records the approved set's digest. F4 asOf is gone. The aggregate (now v1) carries the period label. F5 CallerDemandEnvelopeSchema (no fulfillmentPath, no unmet) for caller input. The ingest route switches to it in #387. Tests cover the verdict's adversarial cases: two k values, sliding windows at millisecond precision, the CSD-shaped no_capability_type counterexample, and one unverified late intent. Each guard was also mutation-checked locally: removing it fails the suite. spec 865/865, demand-intel 55/55. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ture off the response path (gateway review #2975) Gateway reviewed #387 @98e8e490 as SHIP_WITH_FIXES, with two items to close before anyone turns PCC_UNMET_CAPTURE_ENABLED on: 1. An API key's operatorId is self-asserted at provisioning, so a key holder is not a verified requester. Flag-ON capture now records two strengths: "authenticated_operator" only for a proven wallet (an /a2a SIWE session, or req.provenWallet once gateway's #326 follow-up binds it), and "authenticated_key" for a plain key holder. The lens counts only the first as verified breadth. Until a proven wallet exists on /api routes, breadth there stays 0, which fails closed. 2. Matching ran on the response path and was unbounded. It now runs off the response path (captureUnmetThenEmit, which never rejects), matches at most 16 distinct types per intent (recording unmetTruncated), and reads supply through a bounded per-type 30 s cache. Flag OFF still emits exactly the previous events, synchronously. gateway 3019 passed / 6 skipped (189 files). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…/kit-demand-capture
…t parses the caller schema (PX-13 round-1 F3, F5) Follows the merge of feat/kit-demand-signal @6377dd5d (the #365 round-1 fixes). F3 computeUnmet lists an entry only if it passes UnmetCapabilitySchema: a registered type only by its canonical CSD URI, a type with no CSD only as no_capability_type with a slug. An unmet type that cannot be keyed that way is counted as unkeyed, not listed: no capacity or no tier for an uncatalogued type, a registered URL that is not a canonical CSD URI, or a name that does not slugify. Such an intent is still marked "unfulfilled", never "auto". F5 POST /api/intents/ingest parses with CallerDemandEnvelopeSchema, which has no fulfillmentPath, unmet or unmetTruncated. The strip stays as a second guard. The lens header now describes D2's two actor strengths. The seam tests now run capture -> lens (computeForPeriod) -> buildPublicRelease. A type publishes only at 5 proven principals, for a closed period, and only when the publisher approved its CSD. Key holders and proposed types never publish. spec 865/865, demand-intel 55/55, gateway 3026 passed / 6 skipped. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
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
The gateway half of the kit-demand work, stacked on #365. With
PCC_UNMET_CAPTURE_ENABLED=true(default OFF), the first-party intent capture points record two server-owned facts.Who made the request. It comes only from the server-side auth context, never from the body, and is recorded at one of two strengths:
authenticated_operator: a proven wallet only. That is an /a2a SIWE session, orreq.provenWalletonce the gateway's identity binding sets it. Nothing setsreq.provenWalletyet, so/apiroutes record none.authenticated_key: a plain API-key holder. The key's operatorId is self-asserted at provisioning, so it counts as volume, never breadth.All identity reading for capture lives in
services/unmet-capture.ts.Which requested types no live supply can serve (
fulfillmentPathplusunmet), fromCapabilityFacade.listByType:UnmetCapabilitySchema: a registered type is keyed by its canonical CSD URI, and a type with no CSD appears only asno_capability_typewith a slug.unfulfilled, neverauto.unmetTruncated, and it reads supply through a bounded per-type 30 s cache.Separately and unconditionally,
POST /api/intents/ingestparses withCallerDemandEnvelopeSchema, which has nofulfillmentPath,unmetorunmetTruncated, and strips them again. A caller can never assert unmet demand.With the flag OFF, the capture points emit exactly what they emit today.
Changes
packages/gateway/src/services/unmet-capture.tsroutes/requests.ts,routes/negotiation.tsroutes/a2a-tasks.tsroutes/nl-query.tsunknown_synthetic, so there is no matching)routes/intent-ingest.tsCallerDemandEnvelopeSchema, plus the stripservices/event-bus.ts,packages/spec/src/types/analytics.tsauthenticated_operatorandauthenticated_keyadded to theactorTypeunionpackages/spec/src/types/demand.tsunmetTruncated; the strip helper covers itNot in this PR
price_exceeds_budgetandregion_unavailableare not computed: there is no reliable price or service-area supply data yet.fundedevidence is a follow-up./apiroutes waits for the gateway's identity binding. Until then, breadth there is 0. An /a2a SIWE wallet already counts as proven, following the gateway's definition. Such wallets cost nothing to mint, so whether they are enough for a public k is an open decision, and the public feed waits for it.Tests
All run locally at
991e635b. CI runs suites only on PRs based on master, so this stacked PR gets no suite run until it is retargeted.@pcc/gateway@pcc/spec@pcc/demand-intelunmet-capture.test.ts(31 tests) covers:unmet-capture-routes.test.ts(14 tests) runs against real routes and a seeded store. It covers:authenticated_key, and proven walletsauthenticated_operator(lower-cased);buildPublicRelease):5-9for the period;Review and merge
ebf7c70a.masterbefore merge, after the auth-hardening PR fix(gateway)!: WP-A scope hardening — wildcard is not money/admin authority, fail-closed scopes, identity binding (P0 MUST-CLOSE 6-10) #326.🤖 Generated with Claude Code