Skip to content

feat(adapter-openclaw): add dkg_share tool for direct SWM writes (#382, #408)#413

Merged
Jurij89 merged 10 commits intomainfrom
feat/openclaw-dkg-share
May 5, 2026
Merged

feat(adapter-openclaw): add dkg_share tool for direct SWM writes (#382, #408)#413
Jurij89 merged 10 commits intomainfrom
feat/openclaw-dkg-share

Conversation

@Jurij89
Copy link
Copy Markdown
Contributor

@Jurij89 Jurij89 commented May 5, 2026

Summary

Two design decisions (called out for review)

  1. context_graph_id is REQUIRED on OpenClaw (vs Hermes' optional-with-fallback). Hermes falls back to self._context_graph (the current project); OpenClaw has no equivalent state on the plugin class — every other handler that takes a context_graph_id validates it as required (see handleSharedMemoryPublish for 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.

  2. Canned-quad URN uses urn:openclaw:* (vs mirroring urn: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

Files changed

File What
packages/adapter-openclaw/src/DkgNodePlugin.ts New dkg_share schema registration (after dkg_shared_memory_publish) and handleShare private method (after handleSharedMemoryPublish).
packages/adapter-openclaw/test/plugin.test.ts Six new test cases covering schema shape, validation paths, snake→camel body shape, sub-graph plumbing, and daemon-offline error surfacing. One line added to the tool-inventory test.
packages/cli/skills/dkg-node/SKILL.md Line 132 cell updated to reflect both adapters now exposing the tool with parity contracts.

Test plan

  • pnpm --filter @origintrail-official/dkg-adapter-openclaw exec vitest run985 tests, 984 passed, 1 pre-existing skip, including 6 new dkg_share cases + 1 added inventory assertion.
  • pnpm --filter @origintrail-official/dkg-adapter-openclaw build — clean TypeScript compile.
  • Cross-package grep for urn:hermes:sharedContent — only Hermes write site uses the predicate; no consumer breakage.
  • Manual smoke (post-merge or pre-merge in dev):
    # In OpenClaw chat:
    #   "Use dkg_share to share the fact 'OpenClaw share smoke 2026-05-05' against share-smoke."
    curl -s -X POST "http://127.0.0.1:9200/api/query" \
      -H "Authorization: Bearer $TOK" \
      -H "Content-Type: application/json" \
      -d '{"sparql":"SELECT ?s ?p ?o WHERE { GRAPH ?g { ?s ?p ?o } FILTER(STRSTARTS(STR(?s), \"urn:openclaw:\") && CONTAINS(STR(?o), \"share smoke 2026-05-05\")) }"}' | jq
    # Expected: one binding with subject = urn:openclaw:<addr>:shared, predicate = urn:openclaw:sharedContent
  • Confirm CI green on this branch.

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

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
Comment thread packages/adapter-openclaw/src/DkgNodePlugin.ts Outdated
Comment thread packages/adapter-openclaw/src/DkgNodePlugin.ts Outdated
…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.
Comment thread packages/adapter-openclaw/src/DkgNodePlugin.ts Outdated
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.
Comment thread packages/adapter-openclaw/src/DkgNodePlugin.ts Outdated
Comment thread packages/cli/skills/dkg-node/SKILL.md Outdated
…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
Comment thread packages/adapter-openclaw/src/DkgNodePlugin.ts Outdated
Comment thread packages/adapter-hermes/hermes-plugin/__init__.py
…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
Comment thread packages/adapter-hermes/hermes-plugin/__init__.py
Comment thread packages/adapter-openclaw/src/DkgNodePlugin.ts
Comment thread packages/adapter-openclaw/src/DkgNodePlugin.ts Outdated
…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.
Comment thread packages/adapter-hermes/hermes-plugin/__init__.py
Comment thread packages/adapter-openclaw/src/DkgNodePlugin.ts Outdated
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
Comment thread packages/adapter-hermes/hermes-plugin/__init__.py
Comment thread packages/adapter-openclaw/src/DkgNodePlugin.ts Outdated
Comment thread packages/cli/skills/dkg-node/SKILL.md Outdated
…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.
Comment thread packages/adapter-hermes/hermes-plugin/__init__.py
Comment thread packages/adapter-openclaw/src/DkgNodePlugin.ts
… 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
@Jurij89
Copy link
Copy Markdown
Contributor Author

Jurij89 commented May 5, 2026

PR-driver review-loop status — 10 rounds complete

Closing 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

# Commit What
1 50a07323 Initial open: tool registration, handler, schema, 7 tests, SKILL.md update
2 61bb7ef1 Round 1: mint unique subject (publisher upserts by root entity) + N-Triples literal quoting (storage formatTerm IRI-coerces unquoted text)
3 07a58f9b Round 3: switched to canonical escapeDkgRdfLiteral from @origintrail-official/dkg-core
4 d2589f06 Round 4: defensive UCHAR post-pass for non-ECHAR control bytes (NUL, VT, DEL) + Hermes unification (context_graph_id required on both adapters)
5 4a9db722 Round 6: preserve content whitespace (validate trimmed, serialize raw) + add sub_graph_name to Hermes schema
6 77ef3c5a Round 7: type guards at the runtime boundary (reject non-string content / context_graph_id / sub_graph_name)
7 450fab33 Round 8: require resolved node identity before share (probe both ensureNodeAgentAddress and ensureNodePeerId, error if neither resolves)
8 b9f37ad6 Round 9: return minted subject and rootEntities in the tool response + tighten SKILL.md parity wording
9 e3e752b8 Round 10: direct /api/agent/identity fallback probe so dkg_share doesn't go dark when memory.enabled: false

Push-backs (won't-fix with evidence)

  • Hermes context_graph_id fallback removal as a "breaking change" — re-raised as 4 separate threads across rounds 7, 8, 9, 10 (PRRT_kwDORwbl8c5_t3Bo, _uABG, _uJD1, _uNnY). Resolved each time: the unification was intentional per operator directive after the prior cross-adapter divergence was flagged in round 4; v10-rc release semantics treat this as a clean cut rather than a deprecation surface; no internal Hermes test relied on the fallback.
  • N-Triples round-trip allegedly broken at storage (round 7, PRRT_kwDORwbl8c5_t3Bu) — empirical verification against the actual OxigraphStore showed the insert path uses oxigraph's W3C-compliant N-Quads parser (not the local parseTerm), and the round-trip works correctly: ECHAR escapes decode on insert, are re-escaped only at SPARQL output formatting via escapeNQuadsLiteral.

Follow-up issues filed

Final status

Ready for human review.

Comment thread packages/adapter-hermes/hermes-plugin/__init__.py
Comment thread packages/adapter-openclaw/src/DkgNodePlugin.ts Outdated
Comment thread packages/adapter-openclaw/src/DkgNodePlugin.ts Outdated
…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>`
Comment thread packages/adapter-hermes/hermes-plugin/__init__.py
@Jurij89 Jurij89 merged commit b34b643 into main May 5, 2026
32 checks passed
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.
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.

feat(adapter-openclaw): add dkg_share tool to match Hermes surface Add OpenClaw dkg_share tool wrapper for direct SWM writes

1 participant