Context Graph memory model — edge agents can create curated CGs#595
Context Graph memory model — edge agents can create curated CGs#595branarakic wants to merge 10 commits into
Conversation
…rum overrides
Per SPEC_CG_MEMORY_MODEL, Context Graphs no longer declare per-CG hosting
nodes or per-CG ACK quorums. Hosting is handled by the network sharding
table at publish time; the ACK quorum is the system parameter
parametersStorage.minimumRequiredSignatures(). This unblocks edge-node
agents (no on-chain identityId) from registering invite-only / curators-
only CGs — previously the agent SDK threw at register time when
ensureIdentity() returned 0n trying to seed a hosting committee.
Wire-format break end-to-end; no compatibility shim — every package
upgrades together. Public publish path was already consulting
parametersStorage.minimumRequiredSignatures() for ACK quorum, so this
is a contract-surface cleanup, not a publish-flow semantics change.
Contracts (packages/evm-module/contracts/):
- Removed hostingNodes + requiredSignatures arguments from
ContextGraphs.createContextGraph + ContextGraphStorage.createContextGraph
- Removed setHostingNodes, updateQuorum, getContextGraphRequiredSignatures,
getHostingNodes, isHostingNode functions
- Removed HostingNodesSet, QuorumUpdated events
- Removed MAX_HOSTING_NODES constant and _validateHostingNodes helper
- Removed requiredSignatures field on KnowledgeAssetsLib.ContextGraph
(struct repacked tight to 2 slots)
- Removed matching fields on ContextGraphCreated event and
getContextGraph return tuple
- ABIs regenerated under packages/evm-module/abi/
Chain adapter (packages/chain/src/):
- Removed participantIdentityIds + requiredSignatures from
CreateOnChainContextGraphParams interface
- Removed getContextGraphParticipants method
- EVM + mock adapters mirror the new surface; mock uses system-wide
minimumRequiredSignatures for verify() quorum
- Pinned ABI digests + ContextGraphCreated event signature in
abi-pinning.test.ts and evm-event-decode.test.ts updated
Devnet scripts:
- scripts/devnet-test-publish.sh updated to the new createContextGraph
signature; dead hosting-node identity discovery block removed
- scripts/devnet.sh comment updated for the new entrypoint signature
RFC: docs/specs/SPEC_CG_MEMORY_MODEL.md (landed in a follow-up commit)
Co-authored-by: Cursor <cursoragent@cursor.com>
…eIdentity fallback; rewire verify quorum
Follow-up to LU-1 chain-side cleanup. Removes the participantIdentityIds
/ requiredSignatures concept from the agent SDK, daemon HTTP routes, and
CLI, and rewires the verify() path to consult the chain's
getMinimumRequiredSignatures() rather than a per-CG override.
This is the actual change that unblocks edge-node agents: pre-LU-2,
DKGAgent.registerContextGraph() would fall back to ensureIdentity() when
no participantIdentityIds were persisted, and edge nodes (which skip
on-chain profile creation) return identityId 0n — failing contract-side
onlyRegistered(identityId) validation.
Agent SDK (packages/agent/src/):
- dkg-agent-types.ts: dropped participantIdentityIds + requiredSignatures
from public CreateContextGraphOptions / RegisterContextGraphOptions
- dkg-agent.ts:
- createContextGraph no longer auto-adds creator's identityId to _meta
- registerContextGraph no longer falls back to ensureIdentity()
- verify() quorum reads from chain.getMinimumRequiredSignatures();
caller-provided override still honoured as advisory
- No longer forwards dead fields to the chain adapter
Daemon (packages/cli/src/daemon/routes/context-graph.ts):
POST /api/context-graph/create strips deprecated participantIdentityIds
/ requiredSignatures from request bodies with a one-line deprecation
warn so older clients keep working
CLI (packages/cli/src/):
- cli.ts: removed --participant-identity-id and --required-signatures
flags from context-graph create + register commands
- api-client.ts: dropped matching fields from createContextGraph opts
- skills/dkg-node/SKILL.md updated to the new model
Publisher (packages/publisher/src/verify-collector.ts):
requiredSignatures parameter now optional; defaults to the system-wide
minimum when omitted (mirrors agent.verify rewire)
Test surface (agent, publisher, random-sampling, cli):
Fixtures + call sites updated to the new signatures; quorum assertions
rewired to consult getMinimumRequiredSignatures()
scripts/devnet-test.sh body for §7 CG creation no longer passes the
deprecated fields (daemon would strip them with a warn anyway) and now
provides id+name as required by the post-LU-2 route.
Co-authored-by: Cursor <cursoragent@cursor.com>
…eation surfaces
Per SPEC_CG_MEMORY_MODEL, the MCP tool dkg_context_graph_create and the
node-ui CreateProjectModal expose the access model to AI devs and to
human operators using plain-language dials, decoupled from the on-chain
wire fields:
sharing → accessPolicy (0 = open, 1 = invite-only)
contribution → publishPolicy (0 = curators-only, 1 = open)
Defaults are safe (invite-only + curators-only) so an agent that "just
creates a CG" gets the most-locked-down state; opening either dial is
explicit.
MCP (packages/mcp-dkg/src/):
- tools/setup.ts dkg_context_graph_create gains `sharing` and
`contribution` enum fields with Zod schemas + descriptions; maps to
accessPolicy / publishPolicy before forwarding to the daemon
- client.ts createContextGraph forwards both bytes to the daemon
UI (packages/node-ui/src/ui/):
- CreateProjectModal promoted the Contribution radio out of
"(coming soon)" — Curators-only / Open radios now wired to
publishPolicy
- Labels renamed: "Access" → "Sharing", "Publish Policy" → "Contribution"
to match the AI-dev-facing model in the RFC
- api.ts createContextGraph accepts publishPolicy in opts
On-chain wire format unchanged — the dials are a translation layer.
Co-authored-by: Cursor <cursoragent@cursor.com>
…cross-link New AI-dev-facing RFC for the Context Graph access model. Frames a CG as two inseparable things (the Context Graph + the Agent Network), defines three orthogonal dials (sharing, contribution, per-fact privacy), and the three durable memory tiers (WM, SWM, VM) with the explicit guidance that WM is a legitimate final destination — there's no canonical linear promotion path. §4.0 adds a mermaid sequence diagram of the CG create + publish + ACK + fanout flow. §6 documents the four landing units this PR ships: - LU-1: contracts + chain adapter - LU-2: agent SDK + daemon + CLI + verify rewire - LU-3: MCP + UI dials - LU-4: docs (this commit) Glossary entry for "Edge-owned CG" retired (the pattern is now the only pattern, so the qualifier is meaningless). CHANGELOG.md gets an [Unreleased] entry titled "Context Graph memory model — edge agents can create curated CGs" with one paragraph per landing unit and the wire-format-break warning. SPEC_V10_IDENTITY_AND_ACCESS.md gets a forward pointer at the top — that spec continues to define the identity layer; this RFC sits above it and defines the access model exposed to agents. Co-authored-by: Cursor <cursoragent@cursor.com>
| <input type="radio" checked={publishPolicy === 'open'} readOnly disabled /> | ||
| Open — any collaborator can publish to Verified Memory | ||
| <input type="radio" checked={publishPolicy === 'open'} onChange={() => setPublishPolicy('open')} /> | ||
| Open — any collaborator may publish to Verified Memory |
There was a problem hiding this comment.
🔴 Bug: this label is stronger than what the contract enforces. publishPolicy = open authorizes any non-zero wallet to publish, not just collaborators/allowlisted agents. In an invite-only graph this copy implies a much narrower write surface than exists on-chain. Please either change the wording to reflect the real contract behavior or add an allowlisted-publisher mode before exposing this option.
| \"accessPolicy\":0, | ||
| \"publishPolicy\":1 | ||
| }") | ||
| CG_ID=$(json_get "$CG" contextGraphId) |
There was a problem hiding this comment.
🔴 Bug: POST /api/context-graph/create returns { created, uri, ... }, not { contextGraphId, success }. After this PR the script will parse empty values here and fail the section even when creation succeeded. If you only need to verify local creation, read created; if you need the numeric on-chain id, call /api/context-graph/register (or create with register: true) and read onChainId.
….sh response shape
Two Codex inline bugs from the LU-4 review:
UI (packages/node-ui/src/ui/components/Modals/CreateProjectModal.tsx):
The Contribution → Open label read "any collaborator may publish to VM",
which is stronger than the contract enforces. publishPolicy=1 actually
authorises any non-zero wallet to publish — not just members of the
Agent Network. Updated the label to "any wallet may publish to Verified
Memory (not just members)" and added a small inline note when the
combination "invite-only sharing + open contribution" is selected
pointing to RFC §2.5 (the explicit corner case for that combo).
scripts/devnet-test.sh:
POST /api/context-graph/create returns { created, uri }, not the
legacy { contextGraphId, success } shape my LU-2 edit assumed. The
section-7 assertion would have parsed empty values and failed even on
successful creation. Switched to reading `created` (the slug) and
asserting it's non-empty. $CG_ID is only consumed by the assertion's
own echo so no downstream wiring is affected.
Co-authored-by: Cursor <cursoragent@cursor.com>
|
Addressed both Codex inline bugs in Bug 1 — UI label honesty (
Bug 2 — CI flake — the one failing check ( |
| } catch (err: any) { | ||
| return jsonResponse(res, 500, { error: err.message }); | ||
| } | ||
| if (parsed.participantIdentityIds !== undefined || parsed.requiredSignatures !== undefined) { |
There was a problem hiding this comment.
🔴 Bug: This no longer preserves backward compatibility for the legacy POST /api/context-graph/create shape. A client that still sends only participantIdentityIds / requiredSignatures now falls through to the Missing "id" or "name" 400 below after we delete those fields, so the request does not "keep working" as the comment says. Either translate the old body into the new create/register flow, or fail explicitly with a deprecation error instead of silently dropping fields.
| @@ -64,7 +68,6 @@ library KnowledgeAssetsLib { | |||
| // Slot 0 (28 bytes of 32 used) | |||
| address publishAuthority; // Curator address (EOA, Safe multisig, or PCA owner) | |||
There was a problem hiding this comment.
🔴 Bug: Removing requiredSignatures from this persisted struct changes the storage layout of every existing _contextGraphs[id] entry. Unless ContextGraphStorage is guaranteed to be redeployed from scratch with no prior state, an upgrade will decode/write the old slots incorrectly. The removed _hostingNodes mapping in ContextGraphStorage has the same problem for subsequent state variables. Keep the deprecated fields as reserved storage slots, or migrate to a new storage contract/address explicitly.
…multisig-only /create body
Pre-LU2 the route had a dedicated branch that accepted
`{ participantIdentityIds, requiredSignatures }` (no id/name) and called
`agent.registerContextGraphOnChain()` directly. LU-2 deleted that branch
but only stripped the dead fields, so legacy multisig-only callers
silently fell through to "Missing id or name" — a misleading 400 that
hides the fact that the entire flow was retired in SPEC_CG_MEMORY_MODEL.
Now those callers get an explicit 400 with code
`DEPRECATED_MULTISIG_CREATE_FLOW` pointing at the new
`{ id, name, ... }` + optional `/api/context-graph/register` flow.
Modern clients that send the deprecated fields alongside a valid
`{ id, name }` body keep working (warned + stripped) as before.
Two HTTP-level tests pin both branches:
- modern body with deprecated extras still creates the CG
- legacy multisig-only body returns the explicit deprecation error
Co-authored-by: Cursor <cursoragent@cursor.com>
|
Round-2 Codex review — one real bug confirmed and fixed in Bug A — Codex was right: pre-LU2 the route had a dedicated The fix returns an explicit 400 with Pinned by two HTTP-level tests in
Both green locally. Bug B —
That pattern is deploy-fresh-and-rename-in-Hub, not in-place upgrade. The old contract (if any) keeps its address with its old code and old storage forever; the new release lives at a new address with new code and new layout; the Hub's The contract file itself states this explicitly at line 55: I'm leaving the code as-is and won't add reserved slots in this PR. Happy to revisit if the storage contract gets promoted to a proxy in a separate effort. |
| const effectiveRequiredSignatures = Number.isInteger(storedRequiredSignatures) && storedRequiredSignatures > 0 | ||
| ? storedRequiredSignatures | ||
| : 1; | ||
| // LU-2: edge-owned CG pattern — no `participantIdentityIds`/ |
There was a problem hiding this comment.
🔴 Bug: This silently drops DKG_PARTICIPANT_IDENTITY_ID / RequiredSignatures from pre-LU2 local context graphs when they are registered after upgrade. Those graphs will now mint on-chain with the global quorum and no participant roster, which materially weakens the security model compared to the metadata the user already created. If the legacy shape can no longer be translated, detect it here and fail with an explicit migration error instead of registering a different contract state.
| `Cannot determine requiredSignatures for context graph ${contextGraphIdOnChain}: ${err?.message ?? err}. ` + | ||
| `Pass opts.requiredSignatures explicitly or fix the chain adapter connection.`, | ||
| ); | ||
| this.log.warn(ctx, `getMinimumRequiredSignatures failed (${err?.message ?? err}); falling back to 1`); |
There was a problem hiding this comment.
🔴 Bug: Falling back to 1 here makes verify fail-open. chain.verify() only calls registerKnowledgeCollection() and does not send the collected signatures on-chain, so this local quorum check is the only enforcement. If getMinimumRequiredSignatures() is unavailable, we can mark a batch verified after collecting only the proposer signature. Keep the previous hard failure, or require an explicit override, instead of defaulting to 1.
| contextGraphId: String(result.contextGraphId), | ||
| success: true, | ||
| error: | ||
| 'The multisig-only POST /api/context-graph/create flow (participantIdentityIds without id/name) was removed in SPEC_CG_MEMORY_MODEL. Per-CG hosting committees and per-CG quorum overrides no longer exist. Send a regular `{ id, name, accessPolicy?, publishPolicy? }` body and (if you want chain registration) follow up with POST /api/context-graph/register.', |
There was a problem hiding this comment.
🔴 Bug: This hard-breaks the existing POST /api/context-graph/create multisig body for older clients by turning a previously successful flow into a 400. The rest of the PR still treats the deprecated fields as compatibility input, so removing the endpoint behavior here creates an unnecessary API regression. Either keep translating the legacy body server-side or gate the removal behind a versioned endpoint / explicit migration path.
…pre-LU2 quorum metadata
Two findings from Codex review round-3 on dkg-agent.ts:
1) verify() ACK-quorum fail-open (line ~11675)
`chain.verify()` only calls `registerKnowledgeCollection()` — it does
NOT submit the collected signatures on-chain. The local
`resolvedSignatures.length >= requiredSignatures` check inside the
agent is therefore the *only* enforcement gate for the M-of-N ACK
contract. The previous code silently downgraded `requiredSignatures`
to 1 on three failure modes:
- `getMinimumRequiredSignatures()` throws (RPC outage, adapter bug)
- `getMinimumRequiredSignatures()` returns garbage (non-int / < 1)
- adapter doesn't implement the optional method at all
That's fail-open: a batch could be marked verified after collecting
only the proposer signature.
Now all three paths throw with an actionable error pointing the
caller at the `opts.requiredSignatures` override knob. The caller-
supplied override still wins (advisory paths). 73/73 relevant tests
pass (swm-ack-quorum, swm-ack-quorum-integration, e2e-publish-
protocol, e2e-context-graph, v10-ack-provider) so no regression in
real adapter usage.
2) Pre-LU2 RequiredSignatures metadata warning (line ~9700)
CGs created on pre-LU2 rc.x builds may have a persisted
`dkg:contextGraphRequiredSignatures` triple in `_meta`. Post-LU2 the
on-chain contract has no slot for it, so the value silently becomes
advisory. registerContextGraphOnChain now probes for the stale
triple and logs a loud `[migration]` warning telling the operator
that the system-wide quorum applies instead — including the
currently configured `getMinimumRequiredSignatures()` value for
reference.
`DKG_PARTICIPANT_IDENTITY_ID` triples are NOT warned about: the
verify path still consumes them off-chain as an advisory allowlist
via `getVerifyParticipantIdentityIds`, so they're not dead data.
Co-authored-by: Cursor <cursoragent@cursor.com>
|
Round-3 Codex review — two of three findings led to real fixes ( Bug A — Codex's concern: pre-LU2 CGs have persisted What I actually found in the codebase:
Fix: Bug B — Codex was right:
That's fail-open: a batch could be marked verified with just the proposer signature. All three paths now throw with an actionable error pointing the caller at Bug C — Codex round-3 says: "removing the endpoint behavior here creates an unnecessary API regression. Either keep translating the legacy body server-side or gate the removal behind a versioned endpoint." This directly contradicts round-2's Bug A on the same line, which demanded: "If the legacy shape can no longer be translated, detect it here and fail with an explicit migration error instead of registering a different contract state." Both can't be right. The on-chain semantics the legacy fields requested (per-CG hosting committee + per-CG quorum) were removed from the contract in LU-1. There is no faithful server-side translation: silently routing to the new flow would silently change the security model the caller asked for. That's precisely what round-2 told us to avoid. The current behavior is the honest answer: fail 400 with Holding the line on this one. CI on the prior push ( |
| address publishAuthority; // Curator address (EOA, Safe multisig, or PCA owner) | ||
| uint40 createdAt; // Seconds since epoch; good until year ~36,835 | ||
| uint8 requiredSignatures; | ||
| uint8 publishPolicy; // 0 = curated (default), 1 = open |
There was a problem hiding this comment.
🔴 Bug: Removing requiredSignatures from this packed struct changes the byte offsets of publishPolicy/active/accessPolicy for every existing ContextGraph entry already stored on-chain. On an upgrade, previously-created CGs will be decoded with corrupted policy/active values. Keep a deprecated placeholder field to preserve layout, or ship this only with a fresh storage redeploy/migration.
| @@ -11785,8 +11771,10 @@ export class DKGAgent { | |||
| }, | |||
| getParticipantPeers: (cgId?: string) => { | |||
| const allPeers = this.node.libp2p.getPeers().map(p => p.toString()).filter(id => id !== this.peerId); | |||
There was a problem hiding this comment.
🔴 Bug: Returning every connected peer here weakens verify authorization. The downstream path only resolves signer identities; it does not prove sharding-table membership, and chain.verify() just registers the KC without re-checking signatures on-chain. That means any connected node with a valid identity can help satisfy quorum. Please filter peers/signers against the actual eligible host set before counting approvals.
| // Fall through to the 1-of-1 default below. | ||
| } | ||
| } | ||
| if (requiredSignatures <= 0) { |
There was a problem hiding this comment.
🔴 Bug: This is a fail-open downgrade. If a caller omits requiredSignatures and the system-min lookup is unavailable or transiently fails, verify falls back to quorum 1, so the proposer can self-approve. Because the on-chain verify path does not revalidate signatures, this can accept an unverified KC. Throw when the system minimum cannot be determined instead of defaulting to 1.
| } catch (err: any) { | ||
| return jsonResponse(res, 500, { error: err.message }); | ||
| } | ||
| console.warn( |
There was a problem hiding this comment.
🔴 Bug: Silently dropping participantIdentityIds/requiredSignatures but still returning success creates an API-contract break for existing clients: they can believe they created an M-of-N / roster-constrained graph when those constraints were actually discarded. Please reject any create request that still sends these deprecated fields, or return an explicit non-success response that forces the caller to migrate.
…VerifyCollector, reject deprecated fields
Three legitimate findings from round-4 review addressed in this commit.
(D) packages/cli/src/daemon/routes/context-graph.ts
The previous round-2 fix tried to preserve backward compatibility by
stripping `participantIdentityIds` / `requiredSignatures` from request
bodies and warning. Round-4 (and the project owner) made clear that
silent stripping lets callers believe they created a roster-constrained
CG when those constraints were actually discarded. We now reject
outright any request body that carries either deprecated field — code
`DEPRECATED_CONTEXT_GRAPH_FIELDS`, structured `deprecatedFields` array,
HTTP 400. No backward-compat strip path.
(C) packages/publisher/src/verify-collector.ts
`VerifyCollector.collect` used to silently default `requiredSignatures`
to 1 when the caller omitted it and the `getMinimumRequiredSignatures`
probe failed (missing probe / RPC outage / garbage value). Since
`chain.verify()` does NOT submit collected signatures on-chain, the
local quorum check is the only enforcement; defaulting to 1 lets the
proposer self-approve and pass. Now all three failure modes throw
with actionable errors pointing at the explicit `requiredSignatures`
override knob. 4 new unit tests pin the behaviour.
(B) packages/chain/src/{chain-adapter,evm-adapter,mock-adapter}.ts
+ packages/agent/src/dkg-agent.ts
SPEC_CG_MEMORY_MODEL §4.3 promises that only sharding-table members
can ACK a VM publish, but the post-LU2 verify path counted every
resolved signer regardless of membership. Added an optional
`isShardingTableMember(identityId)` to the ChainAdapter interface,
wired EVMChainAdapter to `ShardingTableStorage.nodeExists(uint72)`,
and made the mock return true for any non-zero identityId (mocks
don't model the sharding table). The agent verify path now probes
membership for every resolved signer (including the proposer) and
drops approvals from non-members with a fail-closed per-signer log.
Results are cached per batch to avoid hammering the RPC.
Tests:
- cli/test/daemon-http-behavior-extra.test.ts: replace round-2 strip-
tolerance tests with reject tests covering every deprecated-field
combination (with and without id/name).
- publisher/test/verify-collector.test.ts: four new tests covering
missing-probe / probe-throws / probe-returns-garbage / probe-honoured
paths.
- chain/test/mock-adapter-parity.test.ts: extend the parity manifest
+ add a direct positive/negative assertion for the new method.
All targeted suites pass locally:
✓ publisher/verify-collector (10/10)
✓ chain/mock-adapter-parity (13/13)
✓ chain/abi-pinning (13/13)
✓ cli/daemon-http-behavior-extra (5/5 in-scope subset)
✓ agent/{e2e-publish-protocol,
e2e-context-graph,
v10-ack-provider,
swm-ack-quorum,
swm-ack-quorum-integration} (73/73)
Holds the line on round-4 Bug A (`KnowledgeAssetsLib.sol:71` packed-
struct layout): ContextGraphStorage is not behind an upgradeable proxy;
the deploy pattern is "deploy fresh + register in Hub by name". File
itself states `// Storage layout — fresh design (no prior deployments
to preserve)`. V10 is brand-new on-chain (only testnet artifact:
base_sepolia_v10_contracts.json). No storage-slot reservation needed.
Co-authored-by: Cursor <cursoragent@cursor.com>
|
Round-4 Codex review — three real findings fixed in Bug D — The round-2 fix tried to preserve backward compatibility by stripping New behaviour: any request body that contains either deprecated field returns Tests now cover every combination: Bug C — Same fail-open class as the agent-side
Since Bug B — Codex was right: Fix:
All 73 agent verify-path tests still pass. Bug A — Codex flagged the same concern again: removing This is a false alarm for the same reason as round-2:
No storage-slot reservation needed. If/when this contract is migrated to a UUPS proxy, layout preservation becomes a concern at that point. Pushing |
| } catch (err: any) { | ||
| return jsonResponse(res, 500, { error: err.message }); | ||
| } | ||
| if (parsed.participantIdentityIds !== undefined || parsed.requiredSignatures !== undefined) { |
There was a problem hiding this comment.
🔴 Bug: This turns the existing /api/context-graph/create contract into a hard 400 for legacy callers that still send participantIdentityIds / requiredSignatures. The rest of this PR (including the updated skill doc) describes these fields as backwards-compatible no-ops, so older SDK/HTTP clients will now break instead of degrading gracefully. If you want to remove the semantics without breaking the endpoint, keep accepting the fields here and emit a deprecation warning, or move the hard rejection behind a versioned route.
| // LU-2: on-chain CGs no longer carry per-CG hosting committees, so | ||
| // there is no `chain.getContextGraphParticipants()` to consult. | ||
| // Any sharding-table member can ACK; participant gating is removed. | ||
| // We still return locally-cached participant identity IDs (legacy |
There was a problem hiding this comment.
🔴 Bug: Keeping legacy DKG_PARTICIPANT_IDENTITY_ID values as a hard verify filter can permanently strand already-registered context graphs on the old roster. registerContextGraph() now returns early once onChainId exists, so these graphs cannot actually be "re-registered" to clear the stale participant set. Any pre-LU2 CG whose stored roster is smaller than or different from the current sharding table can become unverifiable under the new system-wide quorum. Consider ignoring/migrating away the legacy roster for already-on-chain CGs, or adding an explicit metadata migration path instead of treating it as an ongoing allowlist.
| const shardingMembershipCache = new Map<string, boolean>(); | ||
| const probeShardingTableMembership = async (identityId: bigint): Promise<boolean> => { | ||
| if (identityId <= 0n) return false; | ||
| if (typeof this.chain.isShardingTableMember !== 'function') return true; |
There was a problem hiding this comment.
🔴 Bug: This is fail-open for any adapter that hasn't implemented isShardingTableMember() yet. Because chain.verify() only registers the KC and does not submit signatures on-chain, this local gate is the only place that enforces the new "ACKs must come from sharding-table members" rule. Returning true here silently disables that check for out-of-tree adapters. This should fail closed like getMinimumRequiredSignatures() does, with an explicit test-only escape hatch if needed.
…ath code Per project owner: backward compatibility for pre-LU2 / pre-V10 callers is not a requirement for this PR. Two complexity sources removed: 1) registerContextGraphOnChain — pre-LU2 RequiredSignatures migration warning The round-3 fix added a `[migration]` warning that probed the WM triple store for stale `dkg:contextGraphRequiredSignatures` triples on every register, then logged a notice that they were now advisory. The probe + log was ~35 lines of pure pre-LU2 affordance: no caller in this codebase ever wrote those triples post-LU2, and the rc.x users it would warn are an internal-only audience the project owner doesn't want code complexity for. Deleted. 2) verify() — `isShardingTableMember` "no method" graceful-degradation branch Round-4 added `if (typeof this.chain.isShardingTableMember !== 'function') return true;` to the membership probe — a fail-open fallback for adapters that hadn't yet implemented the method. Both in-tree adapters (real EVM + mock) implement it, so the branch is dead in-tree and invites the same fail-open class Codex flagged elsewhere for out-of-tree adapters. Now we throw with an actionable error before any signers are counted — refusing to enforce §4.3 silently is the only safe move. 73/73 agent verify/CG tests still green after both deletions. Co-authored-by: Cursor <cursoragent@cursor.com>
| } catch (err: any) { | ||
| return jsonResponse(res, 500, { error: err.message }); | ||
| } | ||
| if (parsed.participantIdentityIds !== undefined || parsed.requiredSignatures !== undefined) { |
There was a problem hiding this comment.
🔴 Bug: This turns a previously accepted request shape into a hard 400, so older CLI/SDK/MCP clients that still send participantIdentityIds / requiredSignatures can no longer create/register a CG at all. The rest of this PR describes those fields as a backwards-compatible deprecation. If compatibility matters, strip them with a warning; otherwise version the endpoint and callers together so this doesn't become an accidental API break.
| params.participantAgents ?? [], | ||
| params.requiredSignatures, | ||
| params.metadataBatchId ?? 0n, | ||
| params.accessPolicy ?? 0, |
There was a problem hiding this comment.
🔴 Bug: After making every field on CreateOnChainContextGraphParams optional, {} is now a valid call and this wrapper silently creates a public, open-contribution CG (accessPolicy=0, publishPolicy=1). That's a privilege widening relative to the safe defaults used by the MCP/UI surfaces, and it makes migrations from the old signature easy to get wrong. Require callers to pass both policies explicitly, or default here to invite-only/curators-only (1 / 0).
| throw new Error(`getContextGraphConfig returned invalid requiredSignatures: ${raw} (must be a positive integer)`); | ||
| } | ||
| requiredSignatures = parsed; | ||
| sysMin = await this.chain.getMinimumRequiredSignatures(); |
There was a problem hiding this comment.
🔴 Bug: This only fails closed if getMinimumRequiredSignatures() reflects a real chain read. EVMChainAdapter.getMinimumRequiredSignatures() still falls back to a hard-coded 3 when ParametersStorage cannot be resolved, so verify can silently use the wrong quorum instead of erroring. Please make the adapter throw/return undefined on lookup failure, or explicitly validate that the value came from the chain before treating it as the enforcement source.
| @@ -11785,8 +11733,10 @@ export class DKGAgent { | |||
| }, | |||
| getParticipantPeers: (cgId?: string) => { | |||
There was a problem hiding this comment.
🔴 Bug: collect() now sends verify proposals to every connected peer, not just peers that are allowed to see this CG and could legitimately ACK it. The proposal payload includes contextGraphId, verifiedMemoryId, batchId, and root entities, so invite-only/curated CG activity is leaked to unrelated peers before the later signature filter drops them. Please pre-filter recipients to the CG-visible / ACK-eligible set before calling sendReliable.
| if ( | ||
| this.identityId > 0n | ||
| && isEligibleParticipant(this.identityId) | ||
| && await probeShardingTableMembership(this.identityId) |
There was a problem hiding this comment.
🔴 Bug: Once the proposer is dropped unless it is a sharding-table member, VerifyCollector's requiredSignatures - 1 assumption is no longer valid. On an edge node with minimumRequiredSignatures() === 1, collect() returns in self-sign mode, this branch rejects the proposer, and verify exits no_quorum without ever asking a core node for the one required ACK. Compute remote quorum from proposer eligibility before collection, or pass an explicit selfCountsTowardQuorum flag into the collector.
Five real findings from Codex round-5 on PR #595, plus two findings held as Hold-the-Line (backwards-compat we explicitly don't owe) and one stale finding already fixed in 6a58ae2. Findings 5 + 6 — Adapter-level fail-open removed - chain/src/chain-adapter.ts: make `accessPolicy` and `publishPolicy` required fields on CreateOnChainContextGraphParams so `{}` can no longer silently create a public + open CG (privilege widening). - chain/src/evm-adapter.ts + mock-adapter.ts: enforce the required fields at runtime; refuse with an actionable error when omitted. - chain/src/evm-adapter.ts: getMinimumRequiredSignatures() now throws when ParametersStorage isn't resolvable instead of returning a hardcoded 3. The agent+publisher fail-closed guards already throw on "missing probe" / "garbage value", but the adapter was silently returning the wrong quorum from a null contract ref. Closed. - Added parity-test coverage for both behaviours. Finding 8 — Proposer-eligibility math in VerifyCollector - VerifyCollector.collect() previously hardcoded `remoteRequired = requiredSignatures - 1`, assuming the proposer always self-counts. After the round-4 sharding-table filter, an edge node proposer with identityId=0 gets dropped, leaving 0 sigs on a `minimumRequiredSignatures=1` chain → spurious no_quorum. - New `proposerCountsTowardQuorum?: boolean` (default true) lets the agent flag ineligible proposers so the collector demands the FULL quorum from remote peers. - DKGAgent.verify() now computes `proposerEligible` BEFORE calling collect() and passes it through. - Tests pin all three branches (proposer counts, proposer doesn't count, mixed) and the still-passing self-sign 1-of-1 path. Finding 7 — Verify-proposal metadata leak - The collector was fanning verify proposals (payload includes contextGraphId / verifiedMemoryId / batchId / root entities) to EVERY connected peer — for curated CGs that leaked CG metadata to unrelated peers before the downstream signer filter ran. - DKGAgent now resolves `getParticipantPeers` via the existing CGMemberEnumerator (allowlist for curated CGs, topic-subscribers for public, empty for legacy unknown). VerifyCollector accepts an async getParticipantPeers signature for this. Finding 2 — Drop legacy DKG_PARTICIPANT_IDENTITY_ID verify filter - `getVerifyParticipantIdentityIds` read pre-LU2 `_meta` triples and enforced them as a HARD allowlist in verify, which could strand already-registered CGs after upgrade when the current sharding table doesn't match the old roster (and `registerContextGraph` early-returns when onChainId is set, so it can't be re-fixed). - Deleted the method entirely; sharding-table membership is now the only authoritative ACK gate. Trust-degradation now keys off "any remote sharding-table-eligible ACK" instead of the legacy participant distinction. - Simplified `resolveVerifyApprovalIdentityId` to drop the legacy candidate-set probe; modern responders stamp identityId in the approval payload. Stale / held-the-line - Finding 3 (sharding-table fail-open) was on commit 269e8a2 and was already fixed by `6a58ae2d` (no-method branch throws). - Findings 1 + 4 (hard-400 on deprecated request fields breaks legacy callers) — user explicitly declined backwards compat for internal rc.x clients; we're keeping the explicit deprecation error so old callers fail loudly rather than silently writing CGs with broken assumptions. Test-callsite ripple - 19 test call sites updated to pass `accessPolicy`+`publishPolicy` explicitly. Existing semantics preserved (public+open where the test used to publish openly, curated+curators-only where it used the strict path). No new functional behaviour, just explicit args. Suites green locally: - packages/chain mock-adapter-parity 15/15 - packages/publisher verify-collector 14/14 - packages/publisher workspace + sigcoll 93/93 - packages/agent e2e-publish-protocol 13/13 - packages/agent agent.test 115/115 - packages/agent swm-ack-quorum 31/31 - packages/cli daemon-http-behavior-extra 50/50 - swm-snapshot-sync flake (pre-existing) unrelated. Co-authored-by: Cursor <cursoragent@cursor.com>
Round-5 — addressed in 22d5435Eight Codex findings landed across the two latest review rounds. Triaged into five real fixes, two held-the-line for explicit user direction (no backwards-compat tax), and one stale finding already fixed in FixedFinding 5 — Finding 6 — Finding 8 — proposer-eligibility breaks Finding 7 — verify-proposal metadata leak. The collector was fanning verify proposals (payload: Finding 2 — legacy Held the lineFindings 1 + 4 — hard-400 on deprecated request fields breaks legacy callers. This was the call from rounds 2 + 4. User explicitly declined backwards-compat for the deprecated StaleFinding 3 — sharding-table fail-open was filed against commit Suites green locally
The pre-existing |
Summary
Per the new
SPEC_CG_MEMORY_MODEL.mdRFC, removes per-CG hosting committees and per-CG ACK quorums from the Context Graph stack end-to-end. Hosting is handled by the network's sharding table at publish time; ACK quorum is the system parameterparametersStorage.minimumRequiredSignatures(). This unblocks edge-node agents (no on-chainidentityId) from registering invite-only / curators-only CGs — previouslyDKGAgent.registerContextGraph()fell back toensureIdentity()which returned0non edge nodes and threw.Wire-format break end-to-end; no compatibility shim — every package upgrades together. The publish path was already consulting
parametersStorage.minimumRequiredSignatures()for ACK quorum, so this is a contract-surface cleanup, not a publish-flow semantics change.Landed in four landing-unit commits:
c2f4fa60feat(evm-module,chain)— drop per-CG hosting committees and quorum overrides from contracts, ABIs, and the chain adapter; struct repacked tight to 2 slots1a67adadfeat(agent,cli,publisher)— dropparticipantIdentityIds+ensureIdentity()fallback from the agent SDK, daemon HTTP routes, CLI; rewireverify()quorum to the system minimum6c9c2ca7feat(mcp-dkg,node-ui)— add AI-dev-facingsharing+contributiondials todkg_context_graph_createMCP tool andCreateProjectModal; defaults are safe (invite-only + curators-only)bbf37133docs— RFC + CHANGELOG + cross-link fromSPEC_V10_IDENTITY_AND_ACCESS; legacy "edge-owned CG" terminology retiredMental model (from the RFC §0)
A Context Graph is paired with an Agent Network. The pair has three orthogonal access dials:
open/invite-only) — who can see the memory → on-chainaccessPolicyopen/curators-only) — who can publish to Verified Memory → on-chainpublishPolicyprivateQuads…and three durable memory tiers: WM (private to one agent), SWM (shared live among Agent Network members), VM (chain-anchored, network-replicated). WM is a legitimate final destination — no canonical linear promotion.
Edge nodes do everything except host VM data and ACK publishes (which require staking + sharding-table membership). End users (MCP / UI) only see the two dials; identity IDs, hosting committees, and quorums are never exposed.
Per-package test results
evm-module(contracts)chainagentswm-snapshot-syncfailure, confirmed onmainbaselinepublisherrandom-samplingmcp-dkgcli(relevant subset)node-uiThe full
clisuite has unrelated long-running e2e tests that hang (not in files we touched).Devnet acceptance — the original ask, confirmed end-to-end
Fresh 6-node devnet (4 core, 2 edge). On edge node 5 (
nodeRole=edge,identityId=0,hasIdentity=false):On-chain verification of CG 3:
active=true,publishPolicy=0(curators-only),publishAuthority=0xD1A061…(edge curator's wallet).Pre-LU-2 the
/registercall would have thrown atensureIdentity()returning0n. Now it just goes through.Test plan
main)ghorigin/mainNotes for reviewers
scripts/devnet-test-publish.shwas updated for the new contract signature but still fails on a pre-existing test gap unrelated to this PR: the script bypasses the daemon's create flow and goes straight to the chain, so the daemon's local_metagraph never gets populated and the CLI's pre-publish check trips. Fixing that script properly (route throughPOST /api/context-graph/createinstead of poking the contract directly) is a separate small refactor.POST /api/context-graph/createandregisterroutes still acceptparticipantIdentityIds/requiredSignaturesin the request body and silently strip them with a one-line deprecation warn — keeps older clients working through the transition.packages/evm-module/deployments/localhost_contracts.json) are intentionally not in this PR.Made with Cursor