OT-RFC-38 LU-5 — edge curator can publish curated CGs to Verified Memory#608
OT-RFC-38 LU-5 — edge curator can publish curated CGs to Verified Memory#608branarakic wants to merge 1 commit into
Conversation
…lish to VM
Implements the minimum unblocker for OT-RFC-38 §1.1: an edge agent that
has no on-chain Profile can now create a curated CG and publish it to
Verified Memory in no-attribution mode, with cores attesting to opaque
ciphertext they cannot decrypt.
Three concerns combined here because they share a single API surface and
their semantics only make sense together:
1. Inline encrypted publish payload (the rescoped minimal LU-5 from
the build plan). Adds `isEncryptedPayload` to PublishIntent so cores
branch on opaque ciphertext, AES-256-GCM chain-key AEAD helper in
dkg-core (`v10-publish-payload.ts`), publisher-side `encryptInlinePayload`
hook, agent-side `_resolveEncryptInlinePayload` wired into both
`publish` and `publishFromSharedMemory`. Byte-size accounting uses
ciphertext length (`effectiveByteSize`) when the payload is encrypted
so ACK signing and on-chain pricing agree.
2. Drop the "identity=0 → skip on-chain" gate in DKGPublisher. Edge
agents that won't (and shouldn't) register a Profile must still
publish in attribution-id=0 mode, which `KnowledgeAssetsV10` already
accepts as no-attribution. The early `willAttemptOnChainPublish`
check and the late identity gate both collapse to "do we have a CG
id + a V10-ready adapter + a signer". Pre-fix, even after LU-2
dropped the register-side gate, an edge agent's publish silently
fell back to tentative and the UI optimistically reported success;
now it goes on-chain or hard-fails with a clear error.
3. UI honesty about VM publish status. The SWM→VM panel and per-entity
publish card now require status=confirmed AND a real txHash to
render success; anything else is a red "NOT published to Verified
Memory" card with the actual status and a diagnostic. The full TX
hash is shown (was truncated to ~16 chars before — too short to
paste into a block explorer). Vite proxy honours `DEVNET_NODE` so
the UI can target an edge node (`UI_NODE_ID=5 ./scripts/devnet.sh
ui start`) — required to test the §1.1 flow at all.
Tests:
- `v10-publish-payload.test.ts` (new, 8): round-trip, wire layout, domain
separation, error handling.
- `storage-ack-handler.test.ts` (+5): encrypted-payload branch — byte-size
verification, missing-claim error, empty stagingQuads error.
- `publisher-no-random-wallet.test.ts` + `phase-sequences.test.ts`: two
tests updated whose premise was the old short-circuit; new behaviour
pinned with explanatory comments. Total publisher: 965/965 pass.
Devnet validation (`scripts/devnet-test-rfc38-lu5.sh` and `-lu5-public.sh`):
- Curated CG from edge node 5 (identity=0, no Profile) → KC #3 confirmed
on-chain (TX 0xa137cc7d…) via attributionId=0 publish, 1 core ACK
collected, KCS read-back: merkleRoots=1, byteSize=512 [ciphertext].
- Public CG regression on the same node → KC #4 confirmed on-chain (TX
0xa24f3bc1…), no chain-key AEAD wrap fires (correctly skipped for
public CGs), byteSize=131 [plaintext].
Specs:
- `SPEC_CG_HOSTING_MEMBERSHIP.md` (new) — OT-RFC-38 full text.
- `SPEC_CG_MEMORY_MODEL.md` — cross-reference to the new doc.
Co-authored-by: Cursor <cursoragent@cursor.com>
| // mismatches; outsider attestation tokens (LU-9) let third parties | ||
| // verify after the fact. Cores DO verify `stagingQuads.length` matches | ||
| // `publicByteSize` so a misreported size can't slip past pricing. | ||
| if (intent.isEncryptedPayload === true) { |
There was a problem hiding this comment.
🔴 Bug: This trusts a sender-controlled isEncryptedPayload bit without checking that the target CG is actually curated. A malicious publisher can set the flag on a public CG and bypass every root/KA/merkle verification path, because the handler will sign whatever merkleRoot, kaCount, and merkleLeafCount it was told. Gate this branch on the graph's real access policy (or another trusted curated-only signal) before ACKing opaque payloads.
| object: ciphertextLiteral, | ||
| graph: stagingGraphUri, | ||
| }]); | ||
| setTimeout(async () => { |
There was a problem hiding this comment.
🔴 Bug: The core signs an availability ACK and then unconditionally deletes the ciphertext 10 minutes later. That breaks the new persist-before-sign contract under slow/failing chain submits and guarantees later catch-up requests cannot be served from an ACKed core. Cleanup needs to be tied to publish finalization/retention bookkeeping, not a fixed timer started before the publish outcome is known.
| ?? this.peerId; | ||
| const stateKey = swmSenderStateKey(contextGraphId, subGraphName, senderAddress); | ||
| const state = this.swmSenderKeySendStates.get(stateKey); | ||
| if (!state) { |
There was a problem hiding this comment.
🔴 Bug: On a fresh curated CG with no sender state yet, this silently returns undefined, which makes publish() fall back to the old plaintext-inline path. That leaks private data to cores exactly in the case this change is meant to protect. For curated graphs, fail closed here (or bootstrap the sender state first) instead of downgrading to plaintext.
| // agent membership with hosting membership and broke verify | ||
| // entirely for curated CGs (cores aren't agents in the | ||
| // allowlist) — revert kept here as Codex round-5 follow-up. | ||
| getParticipantPeers: () => { |
There was a problem hiding this comment.
🔴 Bug: VerifyCollector's contract explicitly says the peer set must be filtered to recipients that are allowed to see the proposal payload, but this now broadcasts rootEntities for every verify request to all connected peers. On invite-only CGs that's a real privacy regression, not just extra traffic. Please resolve the sharding-table/core roster first, or strip sensitive fields from the proposal until that mapping exists.
| // publish path requires an on-chain CG id). The modal already | ||
| // tells the user "Registering context graph on the network…", | ||
| // so the combined create+register flow matches the UX promise. | ||
| register: true, |
There was a problem hiding this comment.
🟡 Issue: This removes the UI's ability to create a local-only context graph. The daemon route still supports create-now/register-later, but unfunded or offline users will now get a hard create failure from the primary UI path instead of a usable local project. Consider making registration opt-in or falling back to local creation when the register leg fails.
| // CG that looks fine in the UI but can never publish to VM | ||
| // (publish-to-VM requires an on-chain CG id). Fail loud with | ||
| // an actionable hint pointing at the retry endpoint. | ||
| if (result.registered === false) { |
There was a problem hiding this comment.
🔴 Bug: Throwing here turns a partial success into an apparent create failure even though the CG was already created locally. The next retry will usually hit 409 already exists, and the user never gets routed to the existing project to retry registration from Settings. Surface this as a warning on the created project instead of aborting the flow.
Summary
Closes the §1.1 bug in OT-RFC-38 /
SPEC_CG_HOSTING_MEMBERSHIP.md: an edge agent with no on-chain Profile can now create a curated CG and publish it to Verified Memory in no-attribution mode, with cores attesting to opaque ciphertext they cannot decrypt.Stacked on top of #595 (SPEC_CG_MEMORY_MODEL LU-1..LU-4). #595 was a necessary precondition because the chain-side
participantIdentityIdsargument was incompatible with edge agents'identityId=0n.What this commit does
Three concerns combined because they share a single API surface and only make sense together:
Inline encrypted publish payload (rescoped minimal LU-5). Adds
isEncryptedPayloadfield (proto field 14) toPublishIntent; cores branch on opaque ciphertext, verifyingchunkDigest+byteSizeagainst persisted bytes without ever N-Quad-parsing. AES-256-GCM chain-key AEAD helper indkg-core(v10-publish-payload.ts), publisher-sideencryptInlinePayloadhook, agent-side_resolveEncryptInlinePayloadwired into bothpublishandpublishFromSharedMemory. Byte-size accounting uses ciphertext length (effectiveByteSize) when the payload is encrypted so ACK signing and on-chain pricing agree.Drop the
identity=0 → skip on-chaingate in DKGPublisher. Edge agents that won't (and shouldn't) register a Profile must still publish inattributionId=0mode, whichKnowledgeAssetsV10already accepts as no-attribution. The earlywillAttemptOnChainPublishcheck and the late identity gate both collapse to "do we have a CG id + a V10-ready adapter + a signer". Pre-fix, even after Context Graph memory model — edge agents can create curated CGs #595's LU-2 dropped the register-side gate, an edge agent's publish silently fell back to tentative and the UI optimistically reported success; now it goes on-chain or hard-fails with a clear error.UI honesty about VM publish status. The SWM→VM panel and per-entity publish card now require
status=confirmedAND a realtxHashto render success; anything else is a red "NOT published to Verified Memory" card with the actual status and a diagnostic. The full TX hash is shown (was truncated to ~16 chars before — too short to paste into a block explorer). Vite proxy honoursDEVNET_UI_NODEso the UI can target an edge node (DEVNET_UI_NODE=5 ./scripts/devnet.sh ui start) — required to test the §1.1 flow at all.Test plan
Unit:
v10-publish-payload.test.ts(new, 8): round-trip, wire layout, domain separation, error handling.storage-ack-handler.test.ts(+5): encrypted-payload branch — byte-size verification, missing-claim error, empty stagingQuads error.publisher-no-random-wallet.test.ts+phase-sequences.test.ts: two tests updated whose premise was the old short-circuit; new behaviour pinned with explanatory comments. Total publisher: 965/965 pass.Devnet (
scripts/devnet-test-rfc38-lu5.sh+lu5-public.sh):merkleRoots=1,byteSize=N[ciphertext].byteSize=N[plaintext].Specs
docs/specs/SPEC_CG_HOSTING_MEMBERSHIP.md(new) — OT-RFC-38 full text.docs/specs/SPEC_CG_MEMORY_MODEL.md— cross-reference to the new doc.Follow-ups
LU-7/8/9/10 + integration harness land in the stacked follow-up PR. LU-6 (substrate hosting on cores) is deferred — gap documented in
SPEC_CG_HOSTING_MEMBERSHIP.md§7.1.1.Made with Cursor