Repository navigation
Conversation
…s (board N23) KernelFacade.heartbeat serves POST /api/kernels/:kernelId/heartbeat and POST /api/operator/heartbeat. It auto-registers the capabilities a kernel announces, and it used to fill in missing terms with tiers [0, 1] and "USDC 0". Every reader then took the defaults as the provider's own offer: - the catalog showed a price nobody set; - the plan re-read (R10, #355) would sell tier 1 on a kernel that never offered it, because it re-reads the row as the provider's terms. A new announced capability is now registered only with declared, well-formed terms: - tiers: integers 0..3, non-empty, at most 16 entries, kept as a sorted set; - pricing: a currency, plus baseCost and minimum (the column's type needs both), with any variable components, all as plain decimal strings and at least one non-zero. Anything else is skipped and reported in an additive capabilitiesSkipped field. Existing rows are untouched: a heartbeat only refreshes their TTL. Skip rather than store an incomplete row: pricing is a NOT NULL column with a typed shape, so "no price" has no type-valid representation. Tests: - New real-state test over the real routes and facade, including pcc-node's actual {type, deviceId, protocol} announcement. - protocol-fixes and kernel-ttl-integration now declare their terms. - Full gateway suite: 2936 passed, 6 skipped. The capture suites fail to load without a built @pcc/verifier dist, as before. - Mutations: 12 of 12 killed. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This was referenced Sep 29, 2026
LamaSu
added a commit
that referenced
this pull request
Sep 29, 2026
…560) The gateway registers a capability announced by heartbeat only with declared, well-formed terms (#437). The node claimed assuranceTiers [0] for every capability, which is itself an invented default, and sent no pricing. Now the node announces only what the operator declares, per device, in its own config. - pcc_node/declared_terms.py (new): declared_tiers and declared_pricing mirror #437's declaredTiers and declaredPricing (same reason codes). validate_device_terms is stricter: an unknown pricing key (which the gateway would drop silently) and a half declaration are errors naming the device and the field. announceable_types inverts the executor's CAPABILITY_PROTOCOL_MAP, so a type is offered only if a job of that type can be routed to the device. announcement_plan refuses two devices offering one type under different terms (the gateway keeps one row per kernel and type). - config: load_config checks every declaration, so a malformed one stops the node. The kernel-wide pricing default {"base": 10, "per_minute": 0.15} is removed. merge_detected_devices keeps configured entries whole: `start` used to REPLACE them with the detected devices, which would have erased every declaration on the next start. - register: announce_capabilities sends only declared capabilities with their terms, signs exactly what it sends, logs and returns the gateway's capabilitiesSkipped, and sends nothing when nothing is declared. register_kernel no longer sends "devices" (raw dicts, credentials included) or "pricing", which the gateway never read. register_devices sends only public fields as adapterConfig, and never a URL with user info. - daemon, cli, ws_client: the periodic path announces the declared capabilities, or sends a plain liveness heartbeat when none are declared. The state file and `pcc-node status` show what was announced, what was not and why, and what the gateway refused. `start` stops with the file unchanged when the config does not load (it used to generate a new config and save it over the operator's). The config wizard asks for per-device terms with no defaults. Tests: test_declared_terms.py (99) mirrors #437's accept and skip cases one for one, plus the Python-only edges. It also checks that nothing declared means nothing sent (at announce and at the daemon's periodic path), the signature over the terms, the no-credential rules, and a byte-identical config after a failed start. test_cli adds a start that keeps the configured device and its terms. 21 of 21 mutants of the new rules are killed. pcc-node: 440 passed. The 2 failures predate this (test_cli start_flow and test_discovery start_with_discover_flag: the first-run diagnostics prompt reads EOF under CliRunner). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ailures AGENT_NAME: implementer-bravo PR #437 astra review (Q1 MEDIUM): a heartbeat's per-capability loop read `cap.type` via a bare type assertion outside any per-entry try/catch, so a `null` (or otherwise non-object) entry threw and aborted every later, otherwise-valid entry in the same announcement. Non-string `type`/ `capability_type` values were also accepted uncoerced into the row id and the insert. Separately, an insertion exception was swallowed by the existing non-fatal catch with no skip reason, so the response acknowledged a capability that was never persisted. Fix: validate each entry (a plain object with a non-empty string `type` or `capability_type`) inside the per-entry handling before touching any of its fields; a malformed entry is now skipped and reported (`invalid-entry`) and later valid entries still register. A storage exception on insert is now reported (`storage-failed`) instead of silently acknowledged. No default tier or price is invented anywhere; failure still means skip-and-report. Tests: null_announcement_does_not_abort_later_valid_entries, "a non-string type is skipped and reported...", failed_insert_is_reported_as_unregistered (spy on the capabilities repo's insert to force the storage failure). 12/12 passing (packages/gateway/src/__tests__/heartbeat-declared-terms.test.ts). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
AGENT_NAME: implementer-bravo PR #437 astra review (Q1 MEDIUM): the declared-pricing validator's decimal check (`[0-9]{1,30}` per side) accepted non-canonical spellings such as "00.10" and "01". R10 (packages/gateway/src/services/ plan-snapshot-revalidation.ts on the stacked branch, not present on this one) then rejects those at acceptance-time revalidation as malformed, so the capability could register here and never actually be sellable —an availability/interop defect, not a claim of incorrect payment. Fix: copy R10's canonical grammar verbatim (no leading zeros in the integer part unless it is exactly "0"; trailing fractional zeros allowed) plus its 100-char length cap, replacing the old per-side digit-count check. A non-canonical spelling is skipped and reported (`invalid-pricing`) — never rewritten to a canonical form. Also documents (no behavior change) the pre-existing alphanumeric currency restriction: it rejects dotted variants like "USDC.e" and symbols like "$"; no supplied contract requires those spellings today, so this is recorded as a deliberate restriction, not a demonstrated settlement defect (astra review, Q2). Test: heartbeat_rejects_noncanonical_leading_zero_price, plus unit-level grammar cases in the declaredPricing describe block. 14/14 passing (packages/gateway/src/__tests__/heartbeat-declared-terms.test.ts). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…overage AGENT_NAME: implementer-bravo PR #437 astra review (Q5 MEDIUM — sensitive boundary cases remain unpinned). Test-only, no code change: none of these expose a defect beyond what M1 (invalid-entry validation) already fixed. - capability_type alias follows the exact same validation as type: a valid alias registers; a non-string runtime type is skipped the same way `type` is (exercises the same 437-M1 code path, not a new defect). - Duplicate-order within one heartbeat: invalid-first + valid-second registers the second; valid-first + invalid-second leaves the first row unchanged (pre-existing, correct "first successfully inserted declaration wins" behavior — pinned, not changed). - An existing row's TTL advances and its declared terms are preserved on both a termless heartbeat for that type and an empty-list heartbeat (the bulk no-capabilities-body refresh path). - The pre-existing "an existing row is only refreshed" test relied on the previous test's "fdm" insertion; gave it its own setup under a distinct type so it no longer depends on execution order. 20/20 passing (packages/gateway/src/__tests__/heartbeat-declared-terms.test.ts). Also re-ran all kernel/heartbeat/capability suites in the package: 19 files, 235/235 passing, no regressions. Co-Authored-By: Claude Sonnet 5 <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.
Summary
Board row N23 (composition), the data-layer half. Steward ruling #3538.
KernelFacade.heartbeatservesPOST /api/kernels/:kernelId/heartbeatandPOST /api/operator/heartbeat, the path pcc-node's daemon calls. It auto-registers the capabilities a kernel announces. When an announcement carried no terms, it filled them in:assuranceTiers ?? [0, 1]andpricing ?? {currency: "USDC", baseCost: "0", minimum: "0"}(kernel.facade.ts531-533 at master). Every reader then took the defaults as the provider's own offer:revalidatePlanSnapshots, feat(gateway): live provider re-read for externally authored plans (R10) #355 on the stack) re-reads rows as the provider's terms. So a heartbeat with a real price and no tiers yields a row that R10 sells at tier 1. R10 itself refuses the zero price (non-positive-price).The change
A NEW announced capability is registered only when it declares both of these:
currency([A-Za-z0-9]{1,16}),baseCostandminimum(the column's type needs both), plus any ofperMinute,perGram,perCm3; all plain decimal strings, and at least one non-zeroAnything else is skipped, and the heartbeat response says why.
capabilitiesSkipped: [{type, reason}], with reason one ofno-declared-tiers,invalid-tiers,no-declared-pricing,invalid-pricingorzero-price.capabilitiesReceivedstill counts every announcement.Why skip rather than store an incomplete row.
pricingis a NOT NULL column typed{currency, baseCost, minimum, ...}, so "no price" has no type-valid representation. An off-type{}would reach every catalog reader. A typed "terms not declared" state belongs in the capability DTOs (PX-7).Know this before merging
daemon.py_build_capabilities_from_devicessends{type, deviceId, protocol}.operator_heartbeatschema also leaves the capability items untyped.USDC / "0" / "0", tiers[0, 1]). R10 refuses these on the zero price.[0, 1]. R10 sells these at tier 1 today. They cannot be told apart from a declared[0, 1]by the row alone.cap-<kernelId>-<type>, whose description is the heartbeat default (Auto-registered from heartbeat for kernel <kernelId>), and whose tiers are[0, 1]. A kernel that sent its own description is not caught this way.kernel.facade.ts; steward #3538).facades/capability.facade.ts:337:POST /api/capabilitiesdefaults pricing to "USDC 0" (N43's handler half, gateway).routes/onboard.ts:84,routes/wizard.ts:590,routes/setup.ts:655: zero-price defaults.routes/capabilities.ts:679: the TD output defaults tiers to[0, 1, 2, 3].services/agentic-decomposer.ts:238: the legacy decomposer's?? [0, 1]. Its proposals re-quote at R10.location ?? {lat: 0, lng: 0}, outside the ruled scope.Tests
heartbeat-declared-terms.test.tsprotocol-fixesandkernel-ttl-integration, whose announcements now declare termsTwo suites fail to load in a worktree without a built
@pcc/verifierdist:captureandcapture-3d. They are untouched here, and CI builds first.Review
🤖 Generated with Claude Code