feat(adapter-openclaw): add dkg_share tool for direct SWM writes (#382, #408)#413
Merged
feat(adapter-openclaw): add dkg_share tool for direct SWM writes (#382, #408)#413
Conversation
Mirrors the Hermes adapter's existing dkg_share contract. Exposes a free-text agent-facing wrapper over POST /api/shared-memory/write so OpenClaw operators don't need the heavier dkg_assertion_create → dkg_assertion_write flow for one-off team-visible facts. Schema (mirrors Hermes' free-form contract): - content (string, required, non-empty) - context_graph_id (string, required, non-empty) - sub_graph_name (string, optional) context_graph_id is required on OpenClaw (vs Hermes' optional-with- fallback) because OpenClaw has no _context_graph / current-project state on the plugin class. Mirrors handleSharedMemoryPublish. Canned-quad URN: urn:openclaw:<agent>:shared / urn:openclaw:sharedContent. Parallel namespace to Hermes' urn:hermes:* preserves cross-adapter authorship attribution. SWM-search consumers are predicate-agnostic (verified by grep — only Hermes' write site references the predicate), so cross-adapter SWM recall remains uniform. Subject identifier resolves via this.resolveDefaultAgentAddress() (returns nodeAgentAddress ?? nodePeerId ?? 'unknown'). Tests (6 new cases in test/plugin.test.ts plus 1 inventory line): - schema shape (required fields, types) - validation: missing/empty content - validation: missing/empty context_graph_id - happy path: snake_case → camelCase body, localOnly=false, canned-quad - sub_graph_name plumbing - daemon-offline error surfacing via daemonError Reuses existing helpers: this.json/error/daemonError, ensureNode- AgentAddress, resolveDefaultAgentAddress, and the unchanged DkgDaemonClient.share() SDK helper (its localOnly: false default landed in #401). SKILL.md cell updated to remove the Hermes-only / "tracked in #382" note now that both adapters expose the tool with parity contracts. Fixes #382, Fixes #408
…hare Two bugs surfaced by Codex review on PR #413, both rooted in how the publisher and storage layers handle the canned share quad. (1) Constant-subject upsert. dkg-publisher.ts:422-429 deletes-then- inserts by root entity for shared-memory writes. Subject was a constant `urn:openclaw:<addr>:shared` per agent, so each successive dkg_share replaced the prior note instead of adding a new fact. Mint a unique shareId suffix (timestamp + random base36) so each call produces its own root entity. (2) Unquoted content treated as IRI. The storage layer's formatTerm (oxigraph.ts:233-244) wraps any object value not starting with `"` in angle brackets, so `content: "hello"` would be serialized as `<hello>` and either fail validation or store as an invalid IRI. Wrap content in N-Triples literal quoting (escapes for backslash, quote, newline, carriage return) before handing it to client.share(). Tests: - happy-path test now asserts subject ends with `:shared:<shareId>` and object is `"hello"` (quoted) - new test asserts two consecutive shares produce different subjects - new test asserts special chars in content (\n, ", \) are escaped Hermes' dkg_share has the same two bugs (same code path); follow-up issue will be filed separately to keep this PR scoped to OpenClaw. Round 1 fixes for PR #413 review feedback.
4 tasks
Round 2 fix for PR #413. The hand-rolled escaping in handleShare only covered backslash, quote, newline, and carriage-return — leaving tab, form-feed, and backspace unescaped. Inputs containing those control characters would produce an invalid N-Triples literal that the storage layer's parser would reject. Replace with `escapeDkgRdfLiteral` from `@origintrail-official/dkg-core` (packages/core/src/publisher-extension.ts:252) — it covers the full set: \, ", \n, \r, \t, \f, \b. The helper is already re-exported from this adapter's public surface (src/index.ts:10), so this is the canonical site to consume it. Test updated to exercise all seven escapes in a single string.
…HAR controls Round 3 fixes for PR #413 review. (1) Non-ECHAR control bytes. The canonical `escapeDkgRdfLiteral` only covers the ECHAR set (\, ", \n, \r, \t, \f, \b). Other ASCII control bytes (NUL, VT, DEL, etc.) pass through raw and would produce an invalid N-Triples literal. Defensive post-pass in handleShare UCHAR-encodes (`\uXXXX`) any remaining 0x00-0x1F or 0x7F bytes. The canonical helper has the same gap and should ideally be fixed at source; that's tracked separately so this PR stays scoped. Regex pattern is built from `String.fromCharCode` so the source code is plain ASCII (no embedded control bytes, no `\u` escape sequences that intermediate tooling can mangle). (2) Hermes/OpenClaw contract divergence. Codex flagged that the SKILL.md update implied parity, but Hermes had `required: ["content"]` while OpenClaw had `required: ["content", "context_graph_id"]`. Aligned both adapters to the OpenClaw contract: context_graph_id is now required on Hermes too, dropping the implicit `self._context_graph` fallback. Rationale: matches every other OpenClaw handler that takes a context_graph_id (explicit > implicit), and keeps portable agent code working unchanged across both surfaces. No existing test relied on the fallback. Tests: - new OpenClaw test asserts NUL/VT/DEL get UCHAR-encoded - existing OpenClaw escape test still passes (ECHAR coverage) - Hermes schema test now asserts `required: ["content", "context_graph_id"]` - new Hermes test asserts the missing-context_graph_id error path - SKILL.md cell updated to reflect the now-actually-identical contracts
6 tasks
…b_graph_name in Hermes schema Round 4 fixes for PR #413 review. (1) OpenClaw handleShare was trimming `args.content` and using the trimmed value for the literal payload. Agents sharing code blocks, exact transcripts, or any content with deliberate leading/trailing whitespace would silently lose it before the write. Switched to use the raw content for serialization; validation still rejects whitespace-only payloads as "empty" via `content.trim()` check. (2) Hermes' DKG_SHARE_SCHEMA was missing `sub_graph_name` even though `_handle_share` forwards the parameter through to client.share(). MCP clients discover allowed args from the schema, so sub-graph-scoped shares were not portable on Hermes despite the runtime accepting them. Added the property; the contract now matches OpenClaw exactly. Tests: - new OpenClaw test asserts whitespace and trailing newlines round-trip byte-for-byte, plus that whitespace-only content still rejects - Hermes schema assertion extended to require sub_graph_name in the properties set
…undary Round 5 fix for PR #413 review. The previous implementation used `String(args.content ?? '')` and equivalent for context_graph_id and sub_graph_name, which silently coerced malformed MCP payloads into persisted data — `{}` would land as `[object Object]`, `false` as `"false"`. Validate as actual strings before coercion so bad calls fail fast with a clear error rather than polluting SWM. Tests added for object/bool content rejection, object context_graph_id rejection, and number sub_graph_name rejection — all four paths verify no fetch is dispatched.
Round 6 fix for PR #413 review. handleShare only awaited ensureNodeAgentAddress() but never ensureNodePeerId(), so during daemon startup both probes can be unfired and resolveDefaultAgentAddress() returns undefined. The previous `?? 'unknown'` fallback would have written every share under `urn:openclaw:unknown:shared:<id>` for nodes still booting — polluting SWM with content that no peer can attribute back to its writer. Probe both the agent ETH address and the libp2p peer ID in parallel before resolving. If neither resolves, surface a clear error telling the caller the node identity isn't available yet, instead of writing under a placeholder. Test additions: - new test asserts that with no injected peer ID the handler errors with "node agent address and peer ID" in the message and never fetches - existing dkg_share tests now inject a placeholder peer ID via the setupPluginWithFetch helper (default-on, opt-out via skipNodeIdInjection) so they continue to exercise the fetch path
…SKILL.md parity wording Round 7 fixes for PR #413 review. (1) The handleShare response previously surfaced only the daemon's opaque shareOperationId. Callers wanting to target THIS specific share in a later dkg_shared_memory_publish({ root_entities: [...] }) had no handle — the shareOperationId is not usable as a root-entity selector. Return the minted subject (and rootEntities: [subject]) alongside the daemon response. (2) SKILL.md previously claimed Hermes/OpenClaw "identical contracts", but the implementations diverge: OpenClaw rejects whitespace-only content, requires resolved node identity, mints unique per-share subjects, and N-Triples-quotes content; Hermes is currently looser on each. Schema contracts are identical (same required/optional fields), but behavioral parity isn't there yet. Soften the wording to say MCP-discovered call signatures are portable, with the parallel Hermes hardening tracked in #414. Test added asserts subject and rootEntities are present in the response and the subject matches the unique-per-share regex.
… module is disabled Round 8 fix for PR #413 review. The round-6 fix made handleShare await ensureNodeAgentAddress and ensureNodePeerId before resolving the canned-quad subject. Both methods early-return when memoryResolverApi is null — which happens when the operator runs with `memory.enabled: false`. dkg_share then errored on every call with "node identity is unresolved" even though the daemon was healthy and could answer the identity probe directly. dkg_share writes to /api/shared-memory/write, which has no dependency on the memory module being enabled. The tool shouldn't go dark when memory is off. Added a direct fallback: when the gated probes can't resolve identity, hit /api/agent/identity through the daemon client (which has no memory-module dependency) and use the response. The endpoint returns both agentAddress and peerId, so a single call covers both cached fields. Tests: - new test exercises the memory-disabled scenario: a fetch mock that responds to /api/agent/identity, no nodePeerId injection. Handler must complete the share with the probed agentAddress in the subject - existing "unresolved identity" test updated: the direct probe is now allowed to fire, but the share itself must still be skipped
Contributor
Author
PR-driver review-loop status — 10 rounds completeClosing the bot-driven review loop here per the github-pr-driver skill cap. Further automated rounds beyond this should escalate to human review. Commits across all rounds
Push-backs (won't-fix with evidence)
Follow-up issues filed
Final status
Ready for human review. |
…root_entities response Round 11 fixes for PR #413 review. (1) handleShare previously hard-failed when node identity could not be resolved across both the gated probes and the direct /api/agent/identity fallback. /api/shared-memory/write itself doesn't require identity preflight, so refusing to write over-couples the tool to a startup race. Reconciled with the round-7/round-10 concerns about polluting SWM under a constant subject by minting a unique-per-call subject under `urn:openclaw:anon:shared:<shareId>` when identity is unresolved — attribution degrades to anonymous, but the upsert-collision problem that round 1 fixed is preserved (subject is still unique per call). (2) Renamed `rootEntities` → `root_entities` in the tool response so agents chaining `dkg_share` → `dkg_shared_memory_publish({ root_entities: ... })` can pass the value through without case translation. The `subject` field is unchanged (single noun, no convention conflict). Tests updated: - subject-and-rootEntities response test now asserts snake_case `root_entities` and that camelCase `rootEntities` is absent - former "errors on unresolved identity" test rewritten as the anon-fallback test: confirms the share write still happens and the subject lands under `urn:openclaw:anon:shared:<shareId>`
3 tasks
Jurij89
pushed a commit
that referenced
this pull request
May 5, 2026
…e responses Round 1 fixes for PR #418 review. (1) Type validation at the runtime boundary. handleShare passed `args.content` straight into `_quote_literal()` which calls .replace() on it. A malformed MCP call like `content: {}` would raise AttributeError out of handle_tool_call instead of returning a structured tool_error. Add explicit isinstance(str) guards for content / context_graph_id / sub_graph_name (mirrors the OpenClaw boundary checks from PR #413 round 7). (2) Don't mask failures with success-only fields. The Hermes Python client returns {success: False, error: ...} on daemon errors (it doesn't throw — see client.py:213). Previous code merged subject / root_entities into that failure response, making it look like a successful write and luring callers chaining into dkg_shared_memory_publish to publish a root entity that was never written. Now: pass failure responses through untouched, only attach subject / root_entities when the write succeeded. Tests added: type-validation rejection paths (object content, bool content, non-string context_graph_id, non-string sub_graph_name — none reach the daemon), and a FailingClient that returns {success: False} so the response shape can be asserted to NOT contain subject / root_entities.
This was referenced May 5, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
dkg_shareagent-facing tool to the OpenClaw adapter, providing parity with the existing Hermes contract. Closes the residual asymmetry left after PRs feat(adapter-openclaw): private-by-default CGs + align dkg_share localOnly with Hermes #401 / feat(adapter-hermes): private-by-default context graphs (parity with OpenClaw) #402 — OpenClaw operators previously had to build raw quads throughdkg_publishor use the heavierdkg_assertion_create→dkg_assertion_writeflow for the same intent.content(required, non-empty),context_graph_id(required, non-empty),sub_graph_name(optional). Dispatches to the existingDkgDaemonClient.share()SDK helper, which already POSTs/api/shared-memory/writewith the post-feat(adapter-openclaw): private-by-default CGs + align dkg_share localOnly with Hermes #401localOnly: falsedefault — no SDK changes needed.urn:openclaw:<agent>:sharedand predicateurn:openclaw:sharedContent, resolving the agent identifier via the existingthis.resolveDefaultAgentAddress()helper (ETH address with peer-ID fallback). Updates SKILL.md (line 132) to remove the "Hermes-only / tracked in Add OpenClaw dkg_share tool wrapper for direct SWM writes #382" note.Two design decisions (called out for review)
context_graph_idis REQUIRED on OpenClaw (vs Hermes' optional-with-fallback). Hermes falls back toself._context_graph(the current project); OpenClaw has no equivalent state on the plugin class — every other handler that takes acontext_graph_idvalidates it as required (seehandleSharedMemoryPublishfor the pattern). Adding a current-project concept just to mirror Hermes' optionality is significant scope creep. Issue Add OpenClaw dkg_share tool wrapper for direct SWM writes #382 also specifies it as required.Canned-quad URN uses
urn:openclaw:*(vs mirroringurn:hermes:*). The parallel-namespace approach preserves cross-adapter authorship attribution. Verified that no SWM-search consumer hardcodes the Hermes predicate — only the Hermes write site references it, so cross-adapter SWM recall (memory_search, SPARQL?s ?p ?o) remains uniform.Related
dkg_sharewrapper for direct SWM writes.localOnly: falseSDK default) and feat(adapter-hermes): private-by-default context graphs (parity with OpenClaw) #402 (Hermes privacy-by-default).Files changed
packages/adapter-openclaw/src/DkgNodePlugin.tsdkg_shareschema registration (afterdkg_shared_memory_publish) andhandleShareprivate method (afterhandleSharedMemoryPublish).packages/adapter-openclaw/test/plugin.test.tspackages/cli/skills/dkg-node/SKILL.mdTest plan
pnpm --filter @origintrail-official/dkg-adapter-openclaw exec vitest run— 985 tests, 984 passed, 1 pre-existing skip, including 6 newdkg_sharecases + 1 added inventory assertion.pnpm --filter @origintrail-official/dkg-adapter-openclaw build— clean TypeScript compile.urn:hermes:sharedContent— only Hermes write site uses the predicate; no consumer breakage.Migration impact
Pure additive change. No existing tools, schemas, wire formats, or daemon routes change. Existing call sites continue to work; the new tool is opt-in.
🤖 Generated with Claude Code