Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions packages/agent/src/dkg-agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5953,7 +5953,7 @@
}

get peerId(): string {
return this.node.peerId;

Check failure on line 5956 in packages/agent/src/dkg-agent.ts

View workflow job for this annotation

GitHub Actions / Tornado: agent [3/10]

test/e2e-flows.test.ts > Query safety (SPARQL guard) > allows queries with PREFIX declarations

Error: DKGNode not started ❯ DKGNode.requireNode ../core/src/node.ts:501:27 ❯ DKGNode.get peerId [as peerId] ../core/src/node.ts:479:17 ❯ DKGAgent.get peerId [as peerId] src/dkg-agent.ts:5956:22 ❯ DKGAgent.query src/dkg-agent.ts:2623:27 ❯ test/e2e-flows.test.ts:459:28

Check failure on line 5956 in packages/agent/src/dkg-agent.ts

View workflow job for this annotation

GitHub Actions / Tornado: agent [3/10]

test/e2e-flows.test.ts > Query safety (SPARQL guard) > allows DESCRIBE queries

Error: DKGNode not started ❯ DKGNode.requireNode ../core/src/node.ts:501:27 ❯ DKGNode.get peerId [as peerId] ../core/src/node.ts:479:17 ❯ DKGAgent.get peerId [as peerId] src/dkg-agent.ts:5956:22 ❯ DKGAgent.query src/dkg-agent.ts:2623:27 ❯ test/e2e-flows.test.ts:446:28

Check failure on line 5956 in packages/agent/src/dkg-agent.ts

View workflow job for this annotation

GitHub Actions / Tornado: agent [3/10]

test/e2e-flows.test.ts > Query safety (SPARQL guard) > allows ASK queries

Error: DKGNode not started ❯ DKGNode.requireNode ../core/src/node.ts:501:27 ❯ DKGNode.get peerId [as peerId] ../core/src/node.ts:479:17 ❯ DKGAgent.get peerId [as peerId] src/dkg-agent.ts:5956:22 ❯ DKGAgent.query src/dkg-agent.ts:2623:27 ❯ test/e2e-flows.test.ts:433:28

Check failure on line 5956 in packages/agent/src/dkg-agent.ts

View workflow job for this annotation

GitHub Actions / Tornado: agent [3/10]

test/e2e-flows.test.ts > Query safety (SPARQL guard) > allows CONSTRUCT queries

Error: DKGNode not started ❯ DKGNode.requireNode ../core/src/node.ts:501:27 ❯ DKGNode.get peerId [as peerId] ../core/src/node.ts:479:17 ❯ DKGAgent.get peerId [as peerId] src/dkg-agent.ts:5956:22 ❯ DKGAgent.query src/dkg-agent.ts:2623:27 ❯ test/e2e-flows.test.ts:420:28

Check failure on line 5956 in packages/agent/src/dkg-agent.ts

View workflow job for this annotation

GitHub Actions / Tornado: agent [3/10]

test/e2e-flows.test.ts > Query safety (SPARQL guard) > allows SELECT queries

Error: DKGNode not started ❯ DKGNode.requireNode ../core/src/node.ts:501:27 ❯ DKGNode.get peerId [as peerId] ../core/src/node.ts:479:17 ❯ DKGAgent.get peerId [as peerId] src/dkg-agent.ts:5956:22 ❯ DKGAgent.query src/dkg-agent.ts:2623:27 ❯ test/e2e-flows.test.ts:407:28
}

get nodeName(): string {
Expand Down Expand Up @@ -6013,6 +6013,23 @@
}
}

/**
* Public check for whether a CG is curated (private) vs open.
*
* Curated CGs restrict VM publish to the registered curator (mirrors
* the on-chain `publishPolicy = EVM_PUBLISH_CURATED` configured by
* `registerContextGraph` when local access policy is "private" or an
* allowlist exists). Open CGs accept publish attempts from any
* collaborator and let the chain adapter's `isAuthorizedPublisher`
* surface arbitrate.
*
* HTTP routes use this to decide whether an owner-only preflight is
* appropriate before handing off to the publisher.
*/
async isContextGraphCurated(contextGraphId: string): Promise<boolean> {
return this.isPrivateContextGraph(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: isPrivateContextGraph() is only local metadata state, not the live on-chain publishPolicy this helper claims to mirror. If a CG's publish policy is updated on chain, or the node has stale discovery data, /api/shared-memory/publish will take the wrong auth branch and either 403 a valid publish or fall back to the masked 200/tentative failure this PR is trying to prevent. Please read/sync the actual on-chain publish policy instead of inferring it from local access metadata.

}

/**
* Public owner-check used by HTTP routes that need to gate curator-only
* actions (manifest publish, SWM template rewrites, etc.). Throws a
Expand Down
12 changes: 12 additions & 0 deletions packages/agent/test/agent.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1358,6 +1358,18 @@
expect(chain.createOnChainContextGraphCalls[2]?.publishAuthority).toBe(ethers.getAddress(chain.signerAddress));
expect(chain.createOnChainContextGraphCalls[2]?.participantAgents).toEqual([]);

// `isContextGraphCurated` must mirror the EVM publish-policy
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Issue: This only pins the helper mapping, not the HTTP regression the PR is fixing. memory.ts can still regress on request auth/status handling and this test will stay green. Add a daemon/API test for /api/shared-memory/publish that proves curated CGs fail with 403/400 before tentative metadata is written, while open CGs still allow non-owner publishes.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fair — the agent-side test pins the helper but doesn't exercise the HTTP surface. I didn't add a daemon-level integration test here because the existing pattern (daemon-http-behavior-extra.test.ts) requires a real Hardhat node + registered curated CG + real EVMChainAdapter, which is a ~60s-per-test setup and more scope than this minimal route change. The devnet pass against scripts/devnet-test.sh §4e is the integration signal I'm leaning on here — it'll turn red if the handler regresses on request-auth handling.

Happy to add a unit-level daemon test using a stubbed DKGAgent (override isContextGraphCurated + assertContextGraphOwner, fire handleMemoryRoutes directly against a mock IncomingMessage/ServerResponse) if that's the bar — it wouldn't prove end-to-end but would lock in the HTTP branching logic. Let me know and I'll push it in a follow-up commit.

// mapping above — HTTP routes (see
// packages/cli/src/daemon/routes/memory.ts) rely on it to decide
// whether to preflight a curator-only owner gate before
// `publishFromSharedMemory`. If this ever drifts from the policy
// the chain adapter actually enforces, non-curator publishes to
// open CGs will be rejected with 403 even though the contract
// accepts them (Codex PR#299 review finding).
expect(await agent.isContextGraphCurated('register-open-policy')).toBe(false);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Issue: these assertions only pin the helper mapping; they don't cover the HTTP behavior that changed in this PR. Please add a daemon-level regression test for /api/shared-memory/publish covering one open CG and one curated CG so future refactors can't silently reintroduce the 200/tentative path or start 403'ing legitimate open publishes.

expect(await agent.isContextGraphCurated('register-curated-policy')).toBe(true);
expect(await agent.isContextGraphCurated('register-agent-allowlist-policy')).toBe(true);

await agent.stop().catch(() => {});
});

Expand Down Expand Up @@ -1998,7 +2010,7 @@

await agent.syncFromPeer('12D3KooWPerContextGraphDeadline111111111111111111111111', ['cg-a', 'cg-b']);

expect(deadlines).toHaveLength(4);

Check failure on line 2013 in packages/agent/test/agent.test.ts

View workflow job for this annotation

GitHub Actions / Tornado: agent [8/10]

test/agent.test.ts > DKGAgent config — syncContextGraphs and queryAccess warning > allocates a fresh sync deadline per context graph

AssertionError: expected [ 1060000, 1060000 ] to have a length of 4 but got 2 - Expected + Received - 4 + 2 ❯ test/agent.test.ts:2013:25
expect(deadlines[0]).toBe(1_060_000);
expect(deadlines[1]).toBe(1_060_000);
expect(deadlines[2]).toBe(1_120_000);
Expand Down
47 changes: 47 additions & 0 deletions packages/cli/src/daemon/routes/memory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -445,6 +445,53 @@ export async function handleMemoryRoutes(ctx: RequestContext): Promise<void> {
'"subGraphName" and "publishContextGraphId" cannot be used together',
});
}

// Policy-aware preflight (spec §2.2):
//
// - Curated CGs (on-chain `publishPolicy = EVM_PUBLISH_CURATED`,
// which `registerContextGraph` sets for private CGs or any CG
// with an allowlist): only the registered curator may VM-publish.
// Without this gate, non-curator callers hit the on-chain
// `isAuthorizedPublisher` revert deep in the stack and the HTTP
// surface returns 200 with `status=tentative` — masking the
// authorization failure and leaving phantom tentative metadata
// on disk.
//
// - Open CGs (on-chain `publishPolicy = EVM_PUBLISH_OPEN`): any
// non-zero collaborator may publish per the contract; we must
// NOT gate on curator identity here or we'd reject legitimate
// participant publishes with 403.
//
// Preflight applies only to the curated branch; open CGs fall
// through to the publisher and the chain adapter decides.
//
// Known scope gap (Codex PR#299 review, tracked separately):
// `assertContextGraphOwner` compares against the locally stored
// `dkg:curator` wallet DID. The on-chain
// `ContextGraphs.isAuthorizedPublisher` is richer — for PCA
// curators it live-resolves the NFT owner and any
// `agentToAccountId`-registered agents. This preflight therefore
// over-rejects PCA-delegated agents and post-transfer NFT holders
// whose wallets don't match the stale local curator metadata. The
// same shape of check is already used by share, invite, rename,
// and manifest-publish routes — migrating them all to a
// chain-authoritative preflight is a separate follow-up. Until
// then, those callers can work around this by publishing from the
// wallet recorded as the CG's local curator.
if (await agent.isContextGraphCurated(paranetId)) {
try {
await agent.assertContextGraphOwner(
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: assertContextGraphOwner is stricter than the contract's publish authorization. Curated CGs are authorized on-chain via isAuthorizedPublisher, which also covers Safe/PCA ownership and registered agents, and follows CG NFT transfers; this local check only compares against the stored dkg:curator/creator metadata. That means a valid publisher can be rejected with 403 here even though the publish would succeed on-chain. Preflight should use on-chain publish-policy/authorization (or only run when local owner metadata is known to match chain authority).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Valid concern, acknowledged as a known scope gap rather than a fresh regression:

  • assertContextGraphOwner is the same helper already used by /api/paranet/:id/share, invite-peer, rename, and the manifest-publish route. They all compare against the locally stored dkg:curator wallet DID, so this PR doesn't introduce a new mismatch with on-chain authorization — it propagates the existing stance to publish.
  • The preflight fires only on the curated branch. Open CGs fall through to the chain adapter unchanged.
  • For curated CGs this over-rejects two narrow scenarios: (a) PCA-delegated agents whose wallets aren't the stored curator but are registered via agentToAccountId, and (b) post-NFT-transfer owners whose wallets don't match the stale local curator metadata. In both cases the chain would accept; the HTTP gate returns 403.

Migrating all of these routes to a chain-authoritative preflight (ContextGraphs.isAuthorizedPublisher via the adapter, with the caller wallet resolved through the same path the publisher uses) is the right long-term fix but is a cross-cutting change that touches every owner-gated route plus the multi-wallet resolution path. That's bigger than this PR's intended scope ("ship the curator gate, stop there") so I've filed it as a follow-up and added a code comment in memory.ts documenting the gap.

In the meantime, affected callers can work around it by publishing from the wallet recorded as the local curator.

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 preflight uses assertContextGraphOwner(), which only compares the caller against the locally stored dkg:curator. The contract's curated publish auth is broader than that (ownerOf(accountId) and agentToAccountId(...) in PCA mode, plus ownership changes after NFT transfer), so this branch will now reject publishes that ContextGraphs.isAuthorizedPublisher would accept. The preflight needs to be chain-authoritative, not a local owner-DID check.

paranetId,
requestAgentAddress,
"publish shared memory to Verified Memory",
);
} catch (authErr: unknown) {
const msg = authErr instanceof Error ? authErr.message : String(authErr);
const code = /has no registered owner/.test(msg) ? 400 : 403;
return jsonResponse(res, code, { error: msg });
}
}

const ctx = createOperationContext("publishFromSWM");
tracker.start(ctx, {
contextGraphId: paranetId,
Expand Down
Loading