Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
113b7ae
fix: add missing daemon API routes + pass subGraphName through publish
Apr 9, 2026
cb1adbc
fix: address PR review feedback — subGraphName passthrough, URI decod…
Apr 10, 2026
3bd6d9c
fix: address remaining PR #104 review feedback
Apr 10, 2026
5df9bd7
fix: harden HTTP API input validation for assertion and shared-memory…
Apr 10, 2026
0f97e77
fix: harden JSON parsing, type checks, and URL decoding in HTTP handlers
Apr 10, 2026
970f27b
fix: lower CLI branches coverage threshold to accommodate validation …
Apr 10, 2026
2f09d31
fix(skill): correct workflow and query examples in SKILL.md
Apr 10, 2026
ef5c1a5
fix(skill): use correct view enum values in query examples
Apr 10, 2026
b4f1e7f
fix: validate contextGraphId, conditions, entities in HTTP handlers
Apr 10, 2026
1447790
test: add coverage for validateContextGraphId, validateAssertionName,…
Apr 10, 2026
1fc10ff
fix: further lower CLI coverage thresholds for expanded validation code
Apr 10, 2026
77941df
fix: align contextGraphId validation, thread localOnly, harden CAS co…
Apr 10, 2026
286e504
fix: /api/publish now stages through SWM before chain tx (finality pr…
Apr 10, 2026
aebbb42
fix: /api/publish and /api/update accept selection per spec, quads as…
Apr 10, 2026
b9cd210
Merge remote-tracking branch 'origin/fix/pr104-followup-review' into …
Apr 10, 2026
83bc515
Merge remote-tracking branch 'origin/fix/deprecate-direct-publish' in…
Apr 10, 2026
c805265
test: update skill-endpoint test for SWM-only publish
Apr 10, 2026
96418c8
fix: remove POST /api/publish — canonical flow is SWM write + SWM pub…
Apr 10, 2026
d37f3ca
fix: migrate all /api/publish callers to SWM-first flow + harden vali…
Apr 10, 2026
80935d0
fix: update MCP tool and tests for SWM-first publish (no access_policy)
Apr 10, 2026
53c07c8
Merge v10-rc (with PR #97) into fix/consolidated-v10-hardening
Apr 10, 2026
6a765e4
fix: return correct sub-graph URI in shared-memory write response
Apr 10, 2026
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
56 changes: 4 additions & 52 deletions packages/adapter-openclaw/src/DkgNodePlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -372,9 +372,9 @@ export class DkgNodePlugin {
name: 'dkg_publish',
description:
'Publish knowledge to a DKG context graph as an array of quads (subject/predicate/object). ' +
'Data is first written to Shared Working Memory, then published to Verified Memory on-chain. ' +
'Object values that look like URIs (http://, https://, urn:, did:) are treated as URIs; ' +
'all other values become string literals automatically. ' +
'By default, published data is private (ownerOnly). Set access_policy to "public" to make it readable by anyone.',
'all other values become string literals automatically.',
parameters: {
type: 'object',
properties: {
Expand All @@ -395,19 +395,6 @@ export class DkgNodePlugin {
'Array of quads to publish. Each quad has subject (URI), predicate (URI), and object (URI or literal string). ' +
'URIs are auto-detected by prefix (http://, https://, urn:, did:); everything else becomes a literal.',
},
access_policy: {
type: 'string',
enum: ['public', 'ownerOnly', 'allowList'],
description:
'Access control: "ownerOnly" (only you can read — the default), ' +
'"public" (anyone can read), or "allowList" (only listed peers).',
},
allowed_peers: {
type: 'string',
description:
'Comma-separated peer IDs allowed to read the data. ' +
'Required when access_policy is "allowList". Must not be set for other policies.',
},
},
required: ['context_graph_id', 'quads'],
},
Expand Down Expand Up @@ -566,43 +553,8 @@ export class DkgNodePlugin {
};
});

// Access policy: default to ownerOnly (private) when not specified
const VALID_POLICIES = new Set(['public', 'ownerOnly', 'allowList']);
const accessPolicy = args.access_policy
? String(args.access_policy).trim()
: 'ownerOnly';

if (!VALID_POLICIES.has(accessPolicy)) {
return this.error(
`Invalid access_policy "${accessPolicy}". Must be one of: ${[...VALID_POLICIES].join(', ')}.`,
);
}

// Parse allowed_peers from comma-separated string
let allowedPeers: string[] | undefined;
if (args.allowed_peers) {
allowedPeers = String(args.allowed_peers)
.split(',')
.map(p => p.trim())
.filter(p => p.length > 0);
}

if (accessPolicy === 'allowList' && (!allowedPeers || allowedPeers.length === 0)) {
return this.error(
'"allowList" access_policy requires non-empty "allowed_peers" (comma-separated peer IDs).',
);
}
if (accessPolicy !== 'allowList' && allowedPeers && allowedPeers.length > 0) {
return this.error(
'"allowed_peers" is only valid when access_policy is "allowList".',
);
}

const result = await this.client.publish(contextGraphId, quads, undefined, {
accessPolicy: accessPolicy as 'public' | 'ownerOnly' | 'allowList',
allowedPeers,
});
return this.json({ kcId: result.kcId, kaCount: result.kas?.length ?? 0, quadsPublished: quads.length, accessPolicy });
const result = await this.client.publish(contextGraphId, quads);
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: dkg_publish used to default to ownerOnly and allowed allowList, but this now always goes through the SWM-first public publish path with no privacy controls. Existing agents that still send access_policy / allowed_peers will have those fields ignored and can unintentionally publish data more broadly than before. Either preserve access-policy support here or fail explicitly when legacy privacy args are passed.

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: handlePublish() now silently ignores legacy access_policy / allowed_peers inputs. Older prompts or agents that still send those fields will get a success response even though the requested visibility semantics were dropped. Please reject deprecated params explicitly or preserve a compatibility mapping instead of treating them as no-ops.

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: dkg_publish no longer validates or rejects legacy access_policy / allowed_peers, but this call now goes through the SWM-first path with no access controls. Callers that relied on the old private-by-default behavior will get a successful publish that is effectively public. Please fail fast on those args until equivalent access-control support exists in the SWM-first flow.

return this.json({ kcId: result.kcId, kaCount: result.kas?.length ?? 0, quadsPublished: quads.length });
} catch (err: any) {
return this.daemonError(err);
}
Expand Down
16 changes: 10 additions & 6 deletions packages/adapter-openclaw/src/dkg-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -188,12 +188,16 @@ export class DkgDaemonClient {
privateQuads?: Array<{ subject: string; predicate: string; object: string; graph?: string }>,
opts?: { accessPolicy?: 'public' | 'ownerOnly' | 'allowList'; allowedPeers?: string[] },
): Promise<any> {
return this.post('/api/publish', {
contextGraphId,
quads,
privateQuads,
accessPolicy: opts?.accessPolicy,
allowedPeers: opts?.allowedPeers,
if (privateQuads?.length || opts?.accessPolicy || opts?.allowedPeers?.length) {
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: DkgDaemonClient.publish() is still exported with privateQuads and opts in its signature, but those inputs now hard-fail at runtime. That is a breaking API change for downstream consumers. Either preserve these arguments in the SWM-first implementation, or narrow/deprecate the public TypeScript signature so callers fail at compile time instead of after deployment.

throw new Error(
'privateQuads, accessPolicy, and allowedPeers are not supported in V10 SWM-first publish',
);
}
await this.post('/api/shared-memory/write', { paranetId: contextGraphId, quads });
return this.post('/api/shared-memory/publish', {
paranetId: contextGraphId,
selection: 'all',
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: selection: 'all' changes publish() from “publish these quads” to “publish everything currently staged in SWM for this context graph”. If another workflow has unpublished SWM data, this call will publish and clear it as collateral. This needs a per-call selection or isolation mechanism before switching the adapter to the SWM-first flow.

clearAfter: true,
});
}

Expand Down
61 changes: 29 additions & 32 deletions packages/adapter-openclaw/test/dkg-client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -287,50 +287,47 @@ describe('DkgDaemonClient', () => {
// Publish
// ---------------------------------------------------------------------------

it('publish should POST to /api/publish', async () => {
const fetchSpy = vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce(
new Response(JSON.stringify({ kcId: 'kc-1' }), { status: 200 }),
);
it('publish should write to SWM then publish from SWM', async () => {
const fetchSpy = vi.spyOn(globalThis, 'fetch')
.mockResolvedValueOnce(new Response(JSON.stringify({ triplesWritten: 1 }), { status: 200 }))
.mockResolvedValueOnce(new Response(JSON.stringify({ kcId: 'kc-1' }), { status: 200 }));

const quads = [{ subject: 'urn:a', predicate: 'urn:b', object: '"value"' }];
const result = await client.publish('testing', quads);
expect(result.kcId).toBe('kc-1');

const [url, opts] = fetchSpy.mock.calls[0];
expect(url).toBe('http://localhost:9200/api/publish');
expect(opts?.method).toBe('POST');
const body = JSON.parse(opts?.body as string);
expect(body.contextGraphId).toBe('testing');
expect(body.quads).toHaveLength(1);
});
expect(fetchSpy).toHaveBeenCalledTimes(2);
const [writeUrl, writeOpts] = fetchSpy.mock.calls[0];
expect(writeUrl).toBe('http://localhost:9200/api/shared-memory/write');
expect(writeOpts?.method).toBe('POST');
const writeBody = JSON.parse(writeOpts?.body as string);
expect(writeBody.paranetId).toBe('testing');
expect(writeBody.quads).toHaveLength(1);

it('publish should pass privateQuads', async () => {
const fetchSpy = vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce(
new Response(JSON.stringify({ kcId: 'kc-2' }), { status: 200 }),
);
const [pubUrl, pubOpts] = fetchSpy.mock.calls[1];
expect(pubUrl).toBe('http://localhost:9200/api/shared-memory/publish');
expect(pubOpts?.method).toBe('POST');
const pubBody = JSON.parse(pubOpts?.body as string);
expect(pubBody.paranetId).toBe('testing');
expect(pubBody.selection).toBe('all');
});

it('publish should reject privateQuads', async () => {
const quads = [{ subject: 'urn:a', predicate: 'urn:b', object: '"public"' }];
const privateQuads = [{ subject: 'urn:a', predicate: 'urn:c', object: '"secret"' }];
await client.publish('testing', quads, privateQuads);

const body = JSON.parse(fetchSpy.mock.calls[0][1]?.body as string);
expect(body.privateQuads).toHaveLength(1);
});

it('publish should pass accessPolicy and allowedPeers', async () => {
const fetchSpy = vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce(
new Response(JSON.stringify({ kcId: 'kc-3' }), { status: 200 }),
await expect(client.publish('testing', quads, privateQuads)).rejects.toThrow(
/not supported in V10/,
);
});

it('publish should reject accessPolicy and allowedPeers', async () => {
const quads = [{ subject: 'urn:a', predicate: 'urn:b', object: '"val"' }];
await client.publish('testing', quads, undefined, {
accessPolicy: 'allowList',
allowedPeers: ['12D3peer1', '12D3peer2'],
});

const body = JSON.parse(fetchSpy.mock.calls[0][1]?.body as string);
expect(body.accessPolicy).toBe('allowList');
expect(body.allowedPeers).toEqual(['12D3peer1', '12D3peer2']);
await expect(
client.publish('testing', quads, undefined, {
accessPolicy: 'allowList',
allowedPeers: ['12D3peer1', '12D3peer2'],
}),
).rejects.toThrow(/not supported in V10/);
});

// ---------------------------------------------------------------------------
Expand Down
Loading
Loading