fix: three pre-mainnet blockers — write-route 500 crashes (#306/#787) + public-CG host-mode ingest (#1124)#1239
Conversation
…ssip-signer agentAddress (#306, #787) #306 — POST /api/knowledge-assets/:name/wm/write and #787 — POST /api/shared-memory/write (+ /shared-memory/conditional-write) only checked `Array.isArray(quads)`, so a string-shaped quad ("<s> <p> <o> .") slipped through and crashed the agent write path with a TypeError → HTTP 500. Added isWritableQuad (graph optional, unlike the publish path's isPublishQuad) and validate every quad at the route boundary, returning an actionable 400. #787 root cause — getWorkspaceGossipSigningAgent() called `record.agentAddress.toLowerCase()` unconditionally; a node-level key record (privateKey, no agentAddress) crashed it on every SWM write via that token. Guarded with optional chaining so such records fall through to the fallback signer. Verified: new test issue-306-787-write-quad-validation.test.ts — string quads → 4xx on both routes AND well-formed object quads still → 200 (no regression). cli write-path regression suites green (191 tests), agent gossip-signer test green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…est (#1124) Host-mode cores dropped a PUBLIC CG's plaintext SWM share at two gates in ingestSwmHostModeEnvelope — the isCiphertext sniff and the curated-agent authority check (which rejects 'no agent allowlist', i.e. exactly the public case). With no host holding the data, a public CG's storage-ACK quorum was unreachable (NO_DATA_IN_SWM). Private/curated CGs were unaffected (ciphertext + allowlist), which is why they worked while public CGs never published to VM. Fix opens BOTH gates, but ONLY for a CG positively confirmed public via the new isConfirmedPublicForHostMode helper: - Gate 1: a non-ciphertext envelope is dropped unless the CG is confirmed public (plaintext is the legitimate carrier for open CGs). - Gate 2: a 'no agent allowlist' verdict is accepted only for a confirmed public CG — the envelope is already verified SIGNED (the unsigned-envelope check runs first), and public host-mode storage stays bounded by the per-CG byte cap + registration economics (same safety net as the pre-reg fail-open). SECURITY: isConfirmedPublicForHostMode is biased so curated AND unknown both return false. A curated CG whose on-chain policy hasn't loaded yet (chain-event race — also surfaces as 'no agent allowlist') is therefore NEVER misclassified as public; it stays a drop and heals via member catchup. Curated + unknown behaviour is byte-for-byte unchanged. Verified: host-mode-public-ingest-1124.test.ts pins the classifier safety bias (confirmed-public→true; curated marker / private / unknown / throw → false). Full agent suite green (1668 passed); existing host-mode tests green (23). NOTE: end-to-end public-CG publish-to-VM quorum needs a host-mode sharded topology (non-member storage cores) to observe — see PR description for the testnet verification plan. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…/Kye_D 🟡) 🔴 Kye-4 — isConfirmedPublicForHostMode resolved the access policy via a direct cleartext `subscribedContextGraphs` lookup, which MISSES for a host-only core whose subscription is keyed by the wire HASH (the exact #1124 sharded topology); the public envelope would still be dropped. Now delegates to the shared `getContextGraphOnChainPolicy` resolver (cache + local _meta + chain RPC, key-independent). Only accessPolicy===0 is public; curated(1)/unknown(undefined) → false (safe; heals via catchup). 🟡 Kye_C — the public-CG exception keyed off the free-form `verdict.reason` string 'no agent allowlist on context graph', coupling a log message to a behavioral branch. verifyHostModeEnvelopeAuthority now returns a structured `reasonCode` (HostModeRejectionCode enum); the ingest path keys off `reasonCode === 'NO_AGENT_ALLOWLIST'`. Also hardened the public path to verify the envelope signature self-consistency (recovers to the claimed signer) — a public CG short-circuits the authority check BEFORE the sig verify, so a forged/garbage signature is now rejected (unsigned was already dropped). 🟡 Kye_D — added an ingest-level test (host-mode-public-ingest-1124.test.ts) that drives a real SIGNED plaintext WorkspacePublishRequest through ingestSwmHostModeEnvelope: confirmed-public → STORED; curated/unknown → DROPPED; tampered signature → DROPPED. Classifier tests now pin the getContextGraphOnChainPolicy contract directly. Verified: 8/8 in the #1124 suite; agent host-mode/gossip/lu11 regression (42) + publisher workspace-handler authority (19) green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ared verifier (#1239 round-2) Addresses otReviewAgent round-2 on the #1124 public-CG path: 🔴 KzQNo — the public exception used a bare ethers.verifyMessage that skipped the shared verifier's timestamp-freshness window, so a previously-signed public envelope could be replayed indefinitely and re-appended (evicting newer entries from the bounded host-mode store). Moved the public acceptance INTO SharedMemoryHandler.verifyHostModeEnvelopeAuthority behind a new `allowSelfSignedForPublicCg` option: it now runs the SAME verifyAgentEnvelope as curated traffic (signature + 5-min freshness), with the claimed signer as its own one-entry allowlist (self-consistency). Only the allowlist decision diverges. 🔴 KzQNk — cross-CG injection: the public path stored an envelope without checking the inner WorkspacePublishRequest.contextGraphId, so a valid envelope for public CG-A carrying a payload for CG-B was stored under A and could be applied to B by catchup. verifyHostModeEnvelopeAuthority now rejects (CG_MISMATCH) when the inner request's contextGraphId differs from the envelope CG. 🟡 KzQNt — the positive ingest test stubbed isConfirmedPublicForHostMode, bypassing the resolver the fix depends on. Tests now stub getContextGraphOnChainPolicy (the real dependency) so the actual classifier + both gates run, and add a cross-CG case asserting CG_MISMATCH drops. The agent ingest path is simplified accordingly (compute confirmedPublic once; pass the flag; no bespoke crypto in the agent). Verified: #1124 suite 9/9; agent host-mode/gossip/lu11 (42) + publisher workspace-handler authority (19) green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…; skip keyless signers (#1239 round-3) 🔴 Kzii9 — isConfirmedPublicForHostMode gated the self-signed public host-mode ingest path on accessPolicy===0 (READ visibility) alone. But read visibility and write authority are separate: a public-READABLE CG can still have publishPolicy===0 (curated / PCA publishing). For such a CG (no resolved agent allowlist), the self-signed path would let ANY key store plaintext SWM on host-mode cores and bypass the on-chain publisher authorization. Now requires BOTH accessPolicy===0 AND publishPolicy===1 (open read AND open write); curated/unknown on either axis → false (getContextGraphOnChainPolicy already returns both, with a chain-RPC fallback). Verified live earlier on a CG created publishPolicy:1, so the #1124 behaviour is unchanged for genuinely-open CGs. 🟡 KzijE — getWorkspaceGossipSigningAgent now SKIPS a record with no valid agentAddress entirely, rather than letting it become the fallback signer (which would emit an envelope with a missing agentAddress that downstream rejects). Avoids the original #787 crash AND picks a usable signer. 🟡 KzijJ — the #306/#787 daemon test exits at the HTTP quad-shape boundary before the signer is selected, so it wouldn't catch a revert. Added gossip-signer-selection-787.test.ts: a keyless record placed ahead of a valid signer is skipped (no crash; valid signer chosen; null when only keyless records). Verified: #1124 suite 10/10 + #787 signer 3/3; agent host-mode/gossip/lu11 (33), cli #306/#787 daemon (4), publisher workspace-handler authority (12) green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…uest + sender peer (#1239 round-4) 🔴 K0FKl — a public self-signed host-mode entry is later applied via host catchup with `trustedReplay`, which SKIPS the apply-time `publisherPeerId === fromPeerId` transport binding. The round-2/3 public path only checked the inner CG "when a request decoded", so: - a ciphertext/garbage payload (request undefined) fell through to accept, and - an honestly-signed envelope whose inner publisherPeerId named ANOTHER peer was stored, and catchup then applied the write under that spoofed publisher identity. verifyHostModeEnvelopeAuthority's public branch now, before accepting: 1. REQUIRES a decoded WorkspacePublishRequest (reject if none), 2. binds request.contextGraphId to the envelope CG (cross-CG injection guard), and 3. binds request.publisherPeerId to the actual sender fromPeerId (publisher-spoof guard) — mirroring the apply-time binding that trustedReplay skips. New reasonCode PUBLISHER_PEER_MISMATCH. Test: host-mode-public-ingest-1124.test.ts adds a publisher-spoof case (inner publisherPeerId != sender → dropped). #1124 suite 11/11; publisher workspace-handler authority/trusted-replay (19) + agent host-mode/lu11/signer (30) green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Review notes (read at HEAD
|
…strate quorum reached (Branimir review) Addresses the two sufficiency/scope calls in Branimir's review of PR #1239. 1. Close the ≤60s stale-publishPolicy admission window. `publishPolicy` is mutable on-chain (PublishPolicyUpdated) and the resolver serves it from a ≤60s-TTL cache, but `isConfirmedPublicForHostMode` is the first security-positive consumer — a stale `publishPolicy=1` would admit a self-signed plaintext write for up to the TTL after an owner downgrades open→curated. Add `getContextGraphOnChainPolicy(cg, { forcePublishPolicyChainRead })` which treats the publishPolicy cache as always-stale and re-reads from chain (fail-closed on RPC error). The host-mode gate now passes the flag; other callers are unaffected (param is optional). accessPolicy stays un-TTL'd — it is immutable on-chain. 2. Demonstrate the end-state (quorum *reached*), not just the gate drop. `host-mode-quorum-bridge-1124.test.ts` wires the real ACKCollector to real StorageACKHandlers over real stores and reaches a public-CG quorum purely from non-member host-mode cores holding only the host-mode-ingested plaintext (valid EIP-191 ACKs); negative control shows the pre-fix empty-SWM state returns NO_DATA_IN_SWM (quorum-blocking). The load-bearing fact: StorageACKHandlerConfig has no membership input, so a non-member host's ACK is consensus-identical to a member's. Resolver- and gate-level tests pin the force-fresh read. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Thanks for the careful trace, Branimir — both points were fair. Addressed all three in 1. Stale-
|
Re-review of
|
… non-member host is ACK-capable (otReviewAgent 🔴)
The host-mode gate fix admitted a confirmed-public self-signed plaintext
envelope but only appended the RAW envelope to SwmHostModeStore (catchup
retention) — it never applied the quads to `<cg>/_shared_memory`, which is the
graph the StorageACKHandler reads (loadSWMQuads / sharedMemoryReadBothFilter).
So a non-member host retained the share but still DECLINEd NO_DATA_IN_SWM when a
publisher dialed it for an ACK → public-CG quorum stayed unreachable via
non-member storage cores. The prior quorum test masked this by seeding the
triple store directly instead of driving the real ingest.
Fix: in ingestSwmHostModeEnvelope, for a CONFIRMED-PUBLIC CG only, also apply
the plaintext via the member apply path — `handler.handle(data, fromPeerId,
undefined, { trustedReplay: true })` — on the same already-authority-verified
bytes (mirrors the LU-6 catchup-replay). For a public CG handle() skips no
crypto; trustedReplay skips only the transport re-checks
verifyHostModeEnvelopeAuthority already performed. The opaque append is kept for
member host-catchup serving.
SECURITY: the `if (confirmedPublic)` wrapper is the SOLE authority gate and is
load-bearing — on a host-only core handle() CANNOT distinguish curated from
public (both resolve to agentGateAddresses===null && hasPrivateAccessPolicy
===false), so confirmedPublic (accessPolicy===0 && forced-fresh publishPolicy
===1) is what guarantees public. Documented in-code so a refactor can't hoist
the apply out.
Tests:
- host-mode-public-ingest-1124.test.ts: drives the REAL ingest end-to-end into a
REAL StorageACKHandler over the same store — confirmed-public ingest →
_shared_memory populated → signed quorum-eligible ACK (goes RED against the
pre-fix code). Negative controls: curated (accessPolicy=1) AND public-read/
restricted-publish (accessPolicy=0, publishPolicy=0) → _shared_memory empty →
NO_DATA_IN_SWM.
- host-mode-quorum-bridge-1124.test.ts: scope narrowed — it isolates the
collector-quorum link and points to the agent test as the real-ingest guard
(no longer asserts the unverified "this is what ingest writes" claim).
- issue-306-787-write-quad-validation.test.ts: reuse the shared live-daemon
helper instead of a duplicated startup harness (🔵 nit).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…(Branimir review follow-on) The force-fresh publishPolicy read (the strict choice from the #1 fix) runs on EVERY host-mode envelope via isConfirmedPublicForHostMode at the top of ingestSwmHostModeEnvelope — but `confirmedPublic` only gates anything when !isCiphertext, and verifyHostModeEnvelopeAuthority ignores allowSelfSignedForPublicCg whenever an allowlist exists. So the dominant ciphertext/curated path was paying a synchronous eth_call to compute a value it discards, and the public-plaintext path had no rate cap (spammed gossip could amplify into per-message chain RPCs) — undercutting the feature meant to scale public CGs. Two changes, keeping the strictness: 1. Lazy-compute: `const confirmedPublic = !isCiphertext && await isConfirmedPublicForHostMode(...)` — skips the policy resolution entirely for ciphertext (security-preserving: a ciphertext-on-public envelope just stays on the curated path / opaque append and heals via catchup). 2. Short cache window instead of force-every-time: replace getContextGraphOnChainPolicy's `forcePublishPolicyChainRead` with `publishPolicyMaxCacheAgeMs`; the admission gate passes 5s (HOST_MODE_PUBLISH_POLICY_MAX_CACHE_AGE_MS). This bounds open→curated downgrade staleness to seconds AND rate-caps the chain RPC to ~1 per window per CG (the resolver writes through to the same cache). Still fail-closed on RPC error; accessPolicy stays immutable/un-TTL'd. Tests: on-chain-policy reworked to prove within-window→cached/no-RPC, beyond-window→re-verify, and the 6s entry still fresh under the 60s default. host-mode-public-ingest: the gate passes a short window (≤10s), and a real CIPHERTEXT envelope triggers ZERO getContextGraphOnChainPolicy calls. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Good catch — fixed both parts in 1. Lazy const confirmedPublic = !isCiphertext && await this.isConfirmedPublicForHostMode(storageCgId);So the dominant ciphertext/curated path resolves 2. Kept the strictness, dropped the per-message RPC on the public-plaintext path too. Took your "short dedicated freshness window" over force-every-time. Replaced Net: the gate is unchanged in strictness, ciphertext traffic pays nothing, and the public-plaintext path is rate-capped. 163 agent SWM/policy/signer tests green. |
…list + enforce policy in the verifier (otReviewAgent) Two coupled findings on verifyHostModeEnvelopeAuthority: 🔴 The self-signed public exception only fired when getContextGraphAgentGateAddresses() returned null. A public-readable CG flipped on-chain to publishPolicy=1 (open publish) WITHOUT clearing its old participantAgents has a non-null (stale) allowlist, so the verifier fell into the curated branch and dropped valid open publishers — host-mode ACK quorum stayed unreachable for that valid open state, even though the contract's isAuthorizedPublisher ignores participants for open publish. 🟡 The exception was a trusted boolean (allowSelfSignedForPublicCg) decoupled from the security precondition (the forced-fresh both-axes check lived in the agent, enforced only by comments) — any caller could set it and get self-signed acceptance. Fix both: replace the boolean with `resolveOpenPublishPolicy` — the caller injects the on-chain policy resolver and the VERIFIER itself enforces accessPolicy===0 && publishPolicy===1 (fail-closed on undefined/throw), then takes the self-signed path INDEPENDENTLY of agentGateAddresses. The agent passes the same forced-fresh, ~5s-window resolver, and only for non-ciphertext (the lazy curated path still pays no chain read; the resolver shares the publishPolicy cache window so it's a warm hit, never a 2nd RPC). Tests (workspace-handler-host-mode-authority): open-publish + stale allowlist → accepted; same non-allowlisted signer with publishPolicy!=1 → curated reject (SIG_VERIFY_FAILED); thrown resolver → exception NOT granted (fail-closed). 163 agent + 77 publisher tests green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ntity gate in quorum test (otReviewAgent) 🔴 Public-CG host-catchup forgery. Now that public plaintext is host-mode-stored and served via catchup, the catchup-apply path (handle with trustedReplay) reached a member from an UNTRUSTED relaying host with the publisherPeerId===fromPeerId transport bind skipped — and for a public CG (no agent gate) handle() applied the plaintext with ZERO signature verification. A malicious host could fabricate brand-new public bytes (never seen by any member's live ingest gate) and have members apply them. Fix (workspace-handler.ts handle): add a public-CG self-signed authority gate that fires ONLY on trustedReplay — require a valid envelope.agentAddress and verifyAgentEnvelope against the claimed signer as its own one-entry allowlist (skipTimestampFreshness, since catchup replays aged envelopes; the signature + verifyAgentEnvelope's envelope.contextGraphId===contextGraphId bind still hold). Scoped to trustedReplay so the LIVE public path is unchanged — it still accepts the legacy unsigned-public producer, bound by the live publisherPeerId=== fromPeerId transport check. (Reviewed design caught that an unconditional gate would silently fail-close legitimate unsigned-public live writes + break an existing test.) Residual (documented, inherent to open-publish): publisherPeerId OWNERSHIP attribution is not cryptographically authenticatable on catchup for an open-publish CG (libp2p peerId vs EVM agentAddress, no cross-binding). Bounded: attacker needs a valid agent key; open publish lets anyone write anyway; on-chain finalization is the system of record. 🟡 Quorum test wired the production identity gate. host-mode-quorum-bridge: the collector previously ran with identity verification OFF, so it reached quorum on mere signature shape. Now verifyIdentity is wired against the generated (identityId→signer) registration, proving the ACKs are on-chain-submittable; a load-bearing negative shows unregistered signers are rejected. Tests: catchup signed→applied, unsigned→rejected, claimed!=recovered→rejected, LIVE unsigned→still applies (regression guard). 82 publisher + 136 agent green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…gate through the real collector (otReviewAgent) 🟡 The HostModeRejectionCode / HostModeEnvelopeAuthorityVerdict types were inserted between SharedMemoryApplyOutcome's JSDoc and the type, orphaning the contract doc. Moved them below SharedMemoryApplyOutcome so each exported type keeps its own documentation. 🟡 The quorum identity-gate negative control defined a local verifyIdentity and called it directly, so it didn't actually exercise ACKCollector's gate (would pass even if collect() stopped calling verifyIdentity). Rewrote it to drive the REAL ACKCollector wired with verifyIdentity rejecting every signer, asserting collect() fails to reach quorum. Identity rejection is non-retryable, so it fails fast (87ms, no ~31s transient-decline budget). 66 publisher tests green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
otReviewAgent
left a comment
There was a problem hiding this comment.
Operational Notice: Review Agent could not complete this review.
Synthesizer produced only invalid comment anchors.
Summary
Fixes three pre-mainnet-blocking issues surfaced by the #1129 issue-liveness suite. All three are live-confirmed bugs; each fix is scoped to be non-breaking (positive regression controls + full agent suite green).
#787 / #306 — write routes 500-crash on malformed quads
POST /api/knowledge-assets/:name/wm/write(#306) andPOST /api/shared-memory/write/…/conditional-write(#787) only checkedArray.isArray(quads), so a string-shaped N-Quad ("<s> <p> <o> .") slipped through and crashed the agent write path with aTypeError→ HTTP 500 instead of an actionable 4xx.isWritableQuad(daemonhttp-utils.ts) validates each quad at the route boundary.graphis optional here (unlike the publish path'sisPublishQuad) so valid{subject,predicate,object}writes are unaffected.getWorkspaceGossipSigningAgentcalledrecord.agentAddress.toLowerCase()unguarded — a node-level key record (privateKey, no agentAddress) crashed it on every SWM write via that token. Now skips records without a validethers.isAddressagentAddress → falls through to the fallback signer.#1124 — public CGs couldn't reach storage-ACK quorum
Host-mode cores dropped a public CG's plaintext SWM share at two gates in
ingestSwmHostModeEnvelope— theisCiphertextsniff and the curated-agent authority check (verifyHostModeEnvelopeAuthority, which rejects "no agent allowlist", i.e. exactly the public case). With no host holding the data, a public CG's storage-ACK quorum was unreachable (NO_DATA_IN_SWM). Private/curated CGs were unaffected (ciphertext + allowlist), which is why they worked while public CGs never published to VM.The fix opens both gates only for a CG positively confirmed public, and re-uses the same verification curated traffic gets — it does not weaken authentication:
isConfirmedPublicForHostModeadmits a self-signed plaintext envelope only whenaccessPolicy === 0(open read) andpublishPolicy === 1(open publish). Curated and unknown both →false, so a curated CG mid chain-event-race (which also surfaces as "no agent allowlist") is never misclassified as public — it stays a drop and heals via member catchup.publishPolicyis re-verified on a short window for this decision.publishPolicyis mutable on-chain (ContextGraphs.updatePublishPolicy→PublishPolicyUpdated) and the resolver otherwise serves it from a ≤60s TTL cache. Because this is the first security-positive consumer of that value, the admission path passesgetContextGraphOnChainPolicy(cg, { publishPolicyMaxCacheAgeMs: 5_000 })— it accepts the cached value only if ≤5s old, else re-reads from chain (fail-closed: an RPC error/timeout leavespublishPolicyundefined → not public → drop). This bounds the window in which a host could admit a self-signed write for a CG an owner just downgraded open→curated to ~5s (vs 60s), while rate-capping the chain RPC to ~1 per window per CG so public-plaintext gossip can't amplify into a per-messageeth_call. The resolution is also lazy —const confirmedPublic = !isCiphertext && await isConfirmedPublicForHostMode(...)— so the dominant ciphertext/curated path pays no chain read at all. (accessPolicyis immutable on-chain — no setter/event — so its un-TTL'd cache read can never be stale-permissive.)verifyHostModeEnvelopeAuthorityrunsverifyAgentEnvelope(EIP-191 signature + 5-min timestamp freshness) — identical to the curated path — then additionally requires a decodable innerWorkspacePublishRequest, bindsrequest.contextGraphId === contextGraphId(no cross-CG replay) andrequest.publisherPeerId === fromPeerId(no transport spoof). The last binding matters because host catchup applies viatrustedReplay, which skips the publisher↔sender transport check, so it must be enforced at ingest. Rejections return a structuredHostModeRejectionCode(DECODE_FAILED,UNSIGNED,NO_AGENT_ALLOWLIST,PEER_NOT_IN_ALLOWLIST,SIG_VERIFY_FAILED,CG_MISMATCH,PUBLISHER_PEER_MISMATCH) instead of free-text matching, so the ingest gate keys its transient-race-vs-warn logging on the code.SwmHostModeStore(member-catchup retention) is NOT what theStorageACKHandlera publisher dials reads — that reads<cg>/_shared_memoryfrom the triple store. So for a confirmed-public CG,ingestSwmHostModeEnvelopeALSO applies the plaintext via the member apply path (handler.handle(data, fromPeerId, undefined, { trustedReplay: true }), mirroring the LU-6 catchup-replay) into that exact graph — making a non-member host sign a quorum-eligible ACK instead of decliningNO_DATA_IN_SWM. Theif (confirmedPublic)wrapper is the sole authority gate for this apply and is load-bearing: on a host-only corehandle()can't itself distinguish curated from public (both resolve toagentGateAddresses === null && hasPrivateAccessPolicy === false), so the forced-fresh both-axes classifier is what keeps a curated CG's plaintext out of_shared_memory. For a public CGhandle({ trustedReplay })skips no cryptography — only the transport re-checks already performed byverifyHostModeEnvelopeAuthorityon the same bytes.No path admits an unauthenticated plaintext envelope into curated storage.
Verification
POST /api/assertion/:name/writereturns 500 + TypeError when given N-Quad strings instead of quad objects #306/SWM write via node-level API token crashes with TypeError (toLowerCase on undefined) #787:packages/cli/test/issue-306-787-write-quad-validation.test.ts— string quads → 4xx on both routes and well-formed object quads still → 200 (positive controls).packages/agent/test/gossip-signer-selection-787.test.ts— keyless signer records are skipped.packages/agent/test/swm/host-mode-public-ingest-1124.test.tspins the both-axes safety bias (public→true; curated / private / unknown / throw → false), the structured-reasonCodeingest behaviour, and that the admission gate forces a fresh publishPolicy chain read.packages/agent/test/dkg-agent-on-chain-policy.test.tsprovesforcePublishPolicyChainReadbypasses a fresh cache entry and re-verifies on-chain (same state → cached1without the flag, fresh-from-chain0with it).packages/agent/test/swm/host-mode-public-ingest-1124.test.tsdrives the realingestSwmHostModeEnvelopeinto a realStorageACKHandlerover the same agent store: a confirmed-public ingest populates<cg>/_shared_memoryand the host signs a quorum-eligible ACK (this test goes red against the pre-fix code, which only appended the opaque envelope). Hard negative controls: a curated CG (accessPolicy=1) and a public-read/restricted-publish CG (accessPolicy=0, publishPolicy=0) both leave_shared_memoryempty →NO_DATA_IN_SWM.packages/publisher/test/host-mode-quorum-bridge-1124.test.tswires the realACKCollectorto realStorageACKHandlers over real stores and shows a public-CG quorum reached purely from non-member host-mode cores — every ACK a valid EIP-191 signature over the canonical V10 digest. The load-bearing fact:StorageACKHandlerConfighas no membership input, so a non-member host's ACK is consensus-identical to a member's.Live verification (local rc.18 devnet, build
ece492d2f)POST /api/assertion/:name/writereturns 500 + TypeError when given N-Quad strings instead of quad objects #306/SWM write via node-level API token crashes with TypeError (toLowerCase on undefined) #787: both write routes return 4xx on string quads and 200 on valid object quads on a running node.registered: false) stored a public CG's plaintext SWM share via host-mode ingest — pre-fix Gate 1 drops all plaintext, so this is only possible via the fix.onChainId=4) published end-to-end → 3/3 storage-ACK quorum reached → confirmed on-chain (KA minted, block 2667). On this small all-staked devnet every core is a member of every registered CG (all ACKs taggedsource=member), which is why the non-member sub-scenario is demonstrated by the reproducible test above rather than live here — reproducing "quorum carried by non-member storage cores" live needs a large stake-weighted sharded topology.Notes
main's KC→KA contract rename hard-renamed the chain adapter, and Base Sepolia still has the pre-rename contracts, so an rc.18 build can't run against that testnet yet.🤖 Generated with Claude Code