Skip to content

Context Graph memory model — edge agents can create curated CGs#595

Open
branarakic wants to merge 10 commits into
mainfrom
feat/cg-memory-model
Open

Context Graph memory model — edge agents can create curated CGs#595
branarakic wants to merge 10 commits into
mainfrom
feat/cg-memory-model

Conversation

@branarakic
Copy link
Copy Markdown
Contributor

Summary

Per the new SPEC_CG_MEMORY_MODEL.md RFC, 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 parameter parametersStorage.minimumRequiredSignatures(). This unblocks edge-node agents (no on-chain identityId) from registering invite-only / curators-only CGs — previously DKGAgent.registerContextGraph() fell back to ensureIdentity() which returned 0n on 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:

  • LU-1 c2f4fa60 feat(evm-module,chain) — drop per-CG hosting committees and quorum overrides from contracts, ABIs, and the chain adapter; struct repacked tight to 2 slots
  • LU-2 1a67adad feat(agent,cli,publisher) — drop participantIdentityIds + ensureIdentity() fallback from the agent SDK, daemon HTTP routes, CLI; rewire verify() quorum to the system minimum
  • LU-3 6c9c2ca7 feat(mcp-dkg,node-ui) — add AI-dev-facing sharing + contribution dials to dkg_context_graph_create MCP tool and CreateProjectModal; defaults are safe (invite-only + curators-only)
  • LU-4 bbf37133 docs — RFC + CHANGELOG + cross-link from SPEC_V10_IDENTITY_AND_ACCESS; legacy "edge-owned CG" terminology retired

Mental model (from the RFC §0)

A Context Graph is paired with an Agent Network. The pair has three orthogonal access dials:

  • Sharing (open / invite-only) — who can see the memory → on-chain accessPolicy
  • Contribution (open / curators-only) — who can publish to Verified Memory → on-chain publishPolicy
  • Per-fact privacy — per-publish share-content vs share-only-hash → privateQuads

…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

Package Result
evm-module (contracts) 625 passing + 46 pending
chain 368/368
agent 721/722 — 1 pre-existing swm-snapshot-sync failure, confirmed on main baseline
publisher 952/953 + 1 skipped
random-sampling 47/47
mcp-dkg 204/204
cli (relevant subset) 169/169
node-ui typecheck clean

The full cli suite 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):

curl -X POST .../api/context-graph/create \
  -d '{"id":"edge-curated-smoke","name":"Edge Curated Smoke",
       "accessPolicy":1,"publishPolicy":0}'
→ {"created":"edge-curated-smoke","uri":"did:dkg:context-graph:edge-curated-smoke"}

curl -X POST .../api/context-graph/register \
  -d '{"id":"edge-curated-smoke","accessPolicy":1,"publishPolicy":0}'
→ {"registered":"edge-curated-smoke","onChainId":"3",...}

On-chain verification of CG 3: active=true, publishPolicy=0 (curators-only), publishAuthority=0xD1A061… (edge curator's wallet).

Pre-LU-2 the /register call would have thrown at ensureIdentity() returning 0n. Now it just goes through.

Test plan

  • Per-package test suites green across all 8 modified packages (modulo one pre-existing baseline failure that reproduces on main)
  • Devnet smoke: edge agent on node 5 creates + registers curated CG end-to-end; on-chain state verified
  • All 4 commits cleanly stacked on latest ghorigin/main
  • Reviewer: confirm the wire-format break expectation matches the rc.10 release cadence — no compat shim is intended
  • Reviewer: spot-check the RFC's framing (CG + Agent Network pair, three dials, three durable tiers) — does the terminology land for you?

Notes for reviewers

  • scripts/devnet-test-publish.sh was 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 _meta graph never gets populated and the CLI's pre-publish check trips. Fixing that script properly (route through POST /api/context-graph/create instead of poking the contract directly) is a separate small refactor.
  • The daemon's POST /api/context-graph/create and register routes still accept participantIdentityIds / requiredSignatures in the request body and silently strip them with a one-line deprecation warn — keeps older clients working through the transition.
  • Stash 'devnet-state' artifacts (packages/evm-module/deployments/localhost_contracts.json) are intentionally not in this PR.

Made with Cursor

Branimir Rakic and others added 4 commits May 22, 2026 13:11
…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
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 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.

Comment thread scripts/devnet-test.sh Outdated
\"accessPolicy\":0,
\"publishPolicy\":1
}")
CG_ID=$(json_get "$CG" contextGraphId)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 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>
@branarakic
Copy link
Copy Markdown
Contributor Author

Addressed both Codex inline bugs in 05ba0d11:

Bug 1 — UI label honesty (CreateProjectModal.tsx)
Changed the "Open" contribution radio label from "any collaborator may publish" to "any wallet may publish to Verified Memory (not just members)". Also added an inline note that appears specifically when the operator combines invite-only sharing + open contribution — pointing them to RFC §2.5, which is the canonical place the rare-corner-case combo is documented:

Note: an invite-only sharing graph with open contribution still lets any wallet publish to Verified Memory — the on-chain publish check is not gated by the allowlist.

Bug 2 — scripts/devnet-test.sh response shape
The daemon's POST /api/context-graph/create returns { created, uri }, not the legacy { contextGraphId, success } shape. Switched the section-7 assertion to read created and require it to be non-empty. $CG_ID is only used inside the assertion's own echo so no downstream wiring is affected.

CI flake — the one failing check (Tornado: agent [1/10]) is e2e-publish-protocol.test.ts > publishes KC via publishDirect and registers it to the CG atomically blowing up in DKGAgent.start → autoRegisterDefaultAgent → mintCustodialWorkspaceEncryptionKey with Error: workspaceEncryptionKey must be 32 bytes. That entire code path is untouched by this PR; locally the file passes 13/13 in 56s. Looks like a flake in the workspace-encryption-key bootstrap path — most likely a fixture race during agent auto-register. Re-running CI should clear it; if it reproduces a second time we should treat it as a separate bug (filed against the workspace-encryption keystore, not this PR).

} catch (err: any) {
return jsonResponse(res, 500, { error: err.message });
}
if (parsed.participantIdentityIds !== undefined || parsed.requiredSignatures !== undefined) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 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)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 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>
@branarakic
Copy link
Copy Markdown
Contributor Author

Round-2 Codex review — one real bug confirmed and fixed in 420233a0, one false alarm with the analysis below.


Bug A — packages/cli/src/daemon/routes/context-graph.ts:438 (real, fixed)

Codex was right: pre-LU2 the route had a dedicated if (Array.isArray(parsed.participantIdentityIds) && !isLocalCreate) branch that bypassed the id/name requirement and called agent.registerContextGraphOnChain() directly. My LU-2 edit deleted that branch and only deleted the dead fields, so a legacy multisig-only caller (one that omits id + name) would silently fall through to a generic "Missing id or name" 400 — hiding the fact that the whole flow was retired in SPEC_CG_MEMORY_MODEL.

The fix returns an explicit 400 with code: "DEPRECATED_MULTISIG_CREATE_FLOW" when the body has participantIdentityIds/requiredSignatures but no id/name, pointing at the new { id, name, ... } + optional /api/context-graph/register flow. Modern clients that happen to still send the deprecated fields alongside a valid { id, name } body keep working (warned + stripped) as before.

Pinned by two HTTP-level tests in daemon-http-behavior-extra.test.ts:

  • strips deprecated participantIdentityIds/requiredSignatures from a modern body and still creates the CG
  • rejects legacy multisig-only POST /api/context-graph/create (no id/name) with an explicit deprecation 400

Both green locally.


Bug B — packages/evm-module/contracts/libraries/KnowledgeAssetsLib.sol:69 (false alarm — no fix needed)

ContextGraphStorage is not behind an upgradeable proxy. It's a regular contract ContextGraphStorage is INamed, IVersioned, Guardian, ERC721Enumerable {} — no Initializable, no UUPS, no transparent proxy. The deploy path is evm-module/deploy/active/050_deploy_context_graph_storage.tshre.helpers.deploy(...) → vanilla hre.deployments.deploy (regular CREATE), then Hub.setAssetStorageInHub(nameInHub, newAddress) to swap the registry pointer.

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 nameInHub → address map points at whichever is current. There's no storage-layout-preservation requirement between releases because no proxy ever decodes new code against old slots.

The contract file itself states this explicitly at line 55: // Storage layout — fresh design (no prior deployments to preserve). V10 is a brand-new contract suite — the only deployment artifact for it is base_sepolia_v10_contracts.json (testnet). If we ever migrate this contract into a proxy pattern, we'll need to seed reserved storage slots at that point; until then, the removed fields don't represent an upgrade hazard.

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`/
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 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.

Comment thread packages/agent/src/dkg-agent.ts Outdated
`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`);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 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.',
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 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>
@branarakic
Copy link
Copy Markdown
Contributor Author

Round-3 Codex review — two of three findings led to real fixes (8f75c6d4); the third is a contradiction with round-2's demand.


Bug A — dkg-agent.ts:9723 (registerContextGraphOnChain) — partial fix landed

Codex's concern: pre-LU2 CGs have persisted participantIdentityId + requiredSignatures triples in _meta; after upgrade those get silently dropped on registerContextGraphOnChain, weakening the security model.

What I actually found in the codebase:

  • DKG_PARTICIPANT_IDENTITY_ID triples are NOT dead — getVerifyParticipantIdentityIds() (dkg-agent.ts:11859, used at line 11749) still consumes them as an off-chain advisory allowlist during verify. So those identity IDs continue to gate signer eligibility, just off-chain instead of on-chain. No warning needed.
  • ${DKG_CONTEXT_GRAPH}RequiredSignatures triples ARE dead data — the new contract has no slot for them and no code path reads them post-LU2.

Fix: registerContextGraphOnChain now probes for the stale RequiredSignatures triple and logs a loud [migration] warning telling the operator that the system-wide quorum (parametersStorage.minimumRequiredSignatures()) applies instead. The current system-min value is included in the warning for reference. Not throwing because (a) the on-chain feature literally doesn't exist anymore — there's no chain-side path that could honour the old value, and (b) refusing to register would strand existing CGs.


Bug B — dkg-agent.ts:11683 (verify ACK quorum fallback) — real fail-open, fixed

Codex was right: chain.verify() only calls registerKnowledgeCollection() — the collected signatures are never submitted on-chain. The agent's local resolvedSignatures.length >= requiredSignatures check is the only enforcement gate. The previous code silently downgraded quorum to 1 on three failure paths:

  • getMinimumRequiredSignatures() throws (RPC outage / adapter bug)
  • getMinimumRequiredSignatures() returns a garbage value (non-int or < 1)
  • adapter doesn't implement the optional method at all

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 opts.requiredSignatures (the explicit advisory override). 73/73 relevant tests pass:

✓ swm-ack-quorum            (31 tests)
✓ swm-ack-quorum-integration (16 tests)
✓ e2e-publish-protocol       (13 tests)
✓ e2e-context-graph          (...)
✓ v10-ack-provider           (...)

Bug C — context-graph.ts:451 (deprecation 400) — false alarm

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 code: DEPRECATED_MULTISIG_CREATE_FLOW and a message naming the new flow. Modern clients that send the deprecated fields alongside a valid {id, name} body still work (warned + stripped).

Holding the line on this one.


CI on the prior push (420233a0) was fully green; pushing 8f75c6d4 now.

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
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 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.

Comment thread packages/agent/src/dkg-agent.ts Outdated
@@ -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);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 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) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 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(
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 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>
@branarakic
Copy link
Copy Markdown
Contributor Author

Round-4 Codex review — three real findings fixed in 269e8a28, one false alarm (same as round-2).


Bug D — daemon/routes/context-graph.ts (deprecated fields) — fixed by switching to hard-reject

The round-2 fix tried to preserve backward compatibility by stripping participantIdentityIds / requiredSignatures from request bodies and continuing. Round-4 (and the project owner) made clear: silent stripping lets callers believe they created a roster-constrained CG when those constraints were actually discarded.

New behaviour: any request body that contains either deprecated field returns 400 with code: "DEPRECATED_CONTEXT_GRAPH_FIELDS" and a structured deprecatedFields: string[] array naming the offending keys. No backward-compat strip path. Modern callers send the new shape; legacy callers get a clear error.

Tests now cover every combination: {participantIdentityIds, requiredSignatures}, {participantIdentityIds} only, {requiredSignatures} only — with and without valid id/name.


Bug C — publisher/src/verify-collector.ts (fail-open quorum default) — fixed

Same fail-open class as the agent-side verify() rewire from round-3 (which I'd already fixed there but missed the publisher's copy). VerifyCollector.collect defaulted requiredSignatures to 1 when:

  • caller omitted it AND probe is missing, OR
  • caller omitted it AND probe throws, OR
  • caller omitted it AND probe returns a non-positive integer

Since chain.verify() never submits collected signatures on-chain, the local count is the only enforcement → proposer self-approval passes quorum. All three paths now throw with actionable errors. Pinned by 4 new tests covering each failure mode + a positive-path test that proves the system minimum is honoured when wired correctly.


Bug B — dkg-agent.ts:11773 (no sharding-table eligibility check) — fixed

Codex was right: getParticipantPeers returned every connected peer, resolveVerifyApprovalIdentityId resolved to any registered identity, and nothing checked sharding-table membership — even though SPEC_CG_MEMORY_MODEL §4.3 explicitly promises "sharding-table members ACK the publish." Pre-LU2 the per-CG hosting-committee allowlist did this gating implicitly; LU-2 removed the allowlist without replacing it.

Fix:

  • Added optional isShardingTableMember(identityId: bigint): Promise<boolean> to ChainAdapter.
  • EVMChainAdapter: maps to ShardingTableStorage.nodeExists(uint72).
  • MockChainAdapter: returns true for any non-zero identityId (mocks don't model the sharding table; tests needing non-membership behaviour spy on the method).
  • DKGAgent.verify(): after resolveVerifyApprovalIdentityId resolves a signer identityId, probe isShardingTableMember; drop the approval (per-signer fail-closed) if it returns false or throws. Includes the proposer's own signature. Results cached per batch to avoid RPC hammering.
  • Mock-adapter parity test extended with the new method + a direct positive/negative assertion.

All 73 agent verify-path tests still pass.


Bug A — KnowledgeAssetsLib.sol:71 (packed-struct layout) — false alarm, same as round-2 Bug B

Codex flagged the same concern again: removing requiredSignatures from the struct changes byte offsets for publishPolicy / active / accessPolicy, breaking on-chain decode of existing entries.

This is a false alarm for the same reason as round-2:

  • ContextGraphStorage is not behind an upgradeable proxy (no Initializable, no UUPS, no transparent proxy).
  • Deploy path: hre.helpers.deploy(...) → vanilla hre.deployments.deploy (regular CREATE) → Hub.setAssetStorageInHub(name, newAddress). The Hub swaps the registry pointer; old contract keeps its address and storage; new contract lives at a new address with new layout.
  • File line 55: // Storage layout — fresh design (no prior deployments to preserve).
  • V10 is brand-new on-chain; only deployment artifact is base_sepolia_v10_contracts.json (testnet).

No storage-slot reservation needed. If/when this contract is migrated to a UUPS proxy, layout preservation becomes a concern at that point.


Pushing 269e8a28 now; CI on the previous push (8f75c6d4) was fully green.

} catch (err: any) {
return jsonResponse(res, 500, { error: err.message });
}
if (parsed.participantIdentityIds !== undefined || parsed.requiredSignatures !== undefined) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 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.

Comment thread packages/agent/src/dkg-agent.ts Outdated
// 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
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 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.

Comment thread packages/agent/src/dkg-agent.ts Outdated
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;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 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) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 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.

Comment thread packages/chain/src/evm-adapter.ts Outdated
params.participantAgents ?? [],
params.requiredSignatures,
params.metadataBatchId ?? 0n,
params.accessPolicy ?? 0,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 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();
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 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.

Comment thread packages/agent/src/dkg-agent.ts Outdated
@@ -11785,8 +11733,10 @@ export class DKGAgent {
},
getParticipantPeers: (cgId?: string) => {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 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.

Comment thread packages/agent/src/dkg-agent.ts Outdated
if (
this.identityId > 0n
&& isEligibleParticipant(this.identityId)
&& await probeShardingTableMembership(this.identityId)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 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>
Copy link
Copy Markdown

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Codex review skipped: filtered diff is 5184 lines (cap: 5,000). Please consider splitting this into smaller PRs for reviewability.

@branarakic
Copy link
Copy Markdown
Contributor Author

Round-5 — addressed in 22d5435

Eight 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 6a58ae2d.

Fixed

Finding 5 — {} silently creates public + open CG. CreateOnChainContextGraphParams now requires accessPolicy and publishPolicy at the type level and enforces them at runtime in both EVMChainAdapter and MockChainAdapter. Removes the privilege-widening footgun: a migration that drops the policy args can no longer accidentally publish a curated payload to a fully open CG. 19 test call sites updated to be explicit; new mock-adapter-parity test pins the rejection of {} / single-policy args.

Finding 6 — EVMChainAdapter.getMinimumRequiredSignatures() hardcoded 3 fallback. Now throws when ParametersStorage isn't resolvable. The agent + publisher fail-closed guards already threw on missing/garbage probe values, but the adapter was silently swallowing the lookup failure and returning a synthetic 3 — verify could enforce the wrong quorum without anyone noticing. New mock-adapter-parity test pins the throw.

Finding 8 — proposer-eligibility breaks requiredSignatures - 1 math. VerifyCollector.collect() previously hardcoded remoteRequired = requiredSignatures - 1 assuming the proposer always self-counts. After round-4's sharding-table filter, an edge-node proposer (identityId=0) gets dropped on minimumRequiredSignatures=1 → 0 sigs → spurious no_quorum. New proposerCountsTowardQuorum?: boolean param (default true) lets the agent flag ineligible proposers; collector then demands the FULL quorum from remote peers. DKGAgent.verify() computes proposerEligible BEFORE collect() and threads it through. 4 new tests pin the math (proposer counts, doesn't count, 1-of-1 self-sign still works, async peer enumeration).

Finding 7 — verify-proposal metadata leak. The collector was fanning verify proposals (payload: contextGraphId + verifiedMemoryId + batchId + root entities) to every connected peer, leaking curated-CG metadata to unrelated peers before the signer filter downstream. DKGAgent now resolves getParticipantPeers through the existing CGMemberEnumerator (allowlist for curated, topic-subscribers for public, empty for legacy unknown). VerifyCollector.getParticipantPeers signature widened to allow Promise<string[]> for the async enumerator path. Tested via the new awaits a Promise-returning getParticipantPeers case.

Finding 2 — legacy DKG_PARTICIPANT_IDENTITY_ID as hard verify filter. getVerifyParticipantIdentityIds enforced pre-LU2 _meta triples as a hard allowlist, which stranded already-registered CGs after upgrade when the new sharding table doesn't intersect the old roster (and registerContextGraph early-returns when onChainId exists). 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. resolveVerifyApprovalIdentityId simplified — the legacy candidate-set probe is gone; modern responders stamp identityId in the approval payload.

Held the line

Findings 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 participantIdentityIds / requiredSignatures request shape (these only ever existed on internal rc.x clients, no external SDK consumers shipped with them). The current 400 + DEPRECATED_CONTEXT_GRAPH_FIELDS code is correct — old callers fail loudly rather than silently writing CGs with broken assumptions about a quorum / roster the contract no longer carries.

Stale

Finding 3 — sharding-table fail-open was filed against commit 269e8a28. Already fixed in 6a58ae2d: the if (typeof this.chain.isShardingTableMember !== 'function') no-method branch throws fail-closed instead of returning true.

Suites green locally

  • chain/test/mock-adapter-parity — 15/15 (adds tests for finding 5 + 6)
  • publisher/test/verify-collector — 14/14 (adds 4 tests for finding 8 + async-peers test for finding 7)
  • publisher/test/workspace + signature-collection — 93/93
  • agent/test/e2e-publish-protocol — 13/13
  • agent/test/agent — 115/115
  • agent/test/swm-ack-quorum — 31/31
  • cli/test/daemon-http-behavior-extra — 50/50

The pre-existing swm-snapshot-sync flake (1 test) remains unchanged on ghorigin/main — unrelated to this PR.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant