Skip to content

Commit 9e813ea

Browse files
committed
Fix OpenClaw ingest schema rejecting content_class
## Summary Fix the OpenClaw plugin's `memory_ingest` tool schema, which silently rejected every verbatim ingest because it never declared the `contentClass` parameter, and add a CI check that verifies every plugin surface can satisfy the content-class contract. ## Changes - Add a `contentClass` enum (`summary`, `redacted`, `raw`) to OpenClaw's `memory_ingest` schema, forwarded to MCP as a top-level argument rather than nested under `metadata`. - Add regression tests confirming the schema declares `contentClass` and that it is forwarded correctly on a live call. - Add `scripts/check-ingest-contract-conformance.mjs`, a CI check that verifies every plugin in the release family (MCP server, OpenClaw, Hermes, Claude Code, Codex, Cursor) declares, stamps, or delegates `content_class` handling, with structural checks that reject commented-out or handler-only mentions. - Wire the new conformance check and its unit tests into CI. - Export `families` from `scripts/version-families.mjs` so the conformance check can enumerate plugin family members instead of duplicating the list. - Fix llmwiki provider tests to use per-test temp directories instead of a shared `/tmp/x` path, removing a source of cross-process test flakiness. - Bump plugin package and manifest versions to 0.2.1. ## Why Core defaults to `RAW_CONTENT_POLICY=reject`, so an unstamped verbatim ingest is refused with a 422. Because OpenClaw's tool schema sets `additionalProperties: false`, a call without a declared `contentClass` property was rejected before it ever reached MCP — making verbatim ingest impossible from OpenClaw even though its docs and skill instructions already told the agent to send it. The conformance check closes the gap that let this ship: it enumerates every plugin in the release family and fails if a new or existing surface can't demonstrably satisfy the contract. ## Validation - Added unit tests for the OpenClaw schema and tool-call forwarding behavior. - Added unit tests for the new conformance check covering declaring, stamping, and delegating surfaces, missing files, and unmapped family members. - Wired both test suites into the CI workflow.
1 parent 2b9860f commit 9e813ea

22 files changed

Lines changed: 552 additions & 23 deletions

File tree

.claude-plugin/marketplace.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
"name": "claude-code",
1313
"source": "./plugins/claude-code",
1414
"description": "Persistent semantic memory for Claude Code - user preferences, project context, prior decisions, and codebase facts that survive across sessions.",
15-
"version": "0.2.0",
15+
"version": "0.2.1",
1616
"category": "productivity",
1717
"homepage": "https://docs.atomicstrata.ai/integrations/coding-agents/claude-code",
1818
"license": "Apache-2.0"

.github/workflows/ci.yml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,15 @@ jobs:
7878
run: node scripts/ci/release-policy.mjs
7979
- name: Run release-policy unit tests
8080
run: node --test scripts/ci/__tests__/release-policy.test.mjs
81+
# Every surface that can write a verbatim memory must be able to stamp
82+
# content_class. The 0.2.0 release fixed Hermes for Core's raw-content
83+
# policy and missed OpenClaw, whose published plugin could not perform a
84+
# verbatim ingest at all. Tests run first: a conformance check whose own
85+
# logic is unverified is not evidence of anything.
86+
- name: Run ingest contract conformance tests
87+
run: node --test scripts/ci/__tests__/ingest-contract-conformance.test.mjs
88+
- name: Check ingest contract conformance
89+
run: node scripts/check-ingest-contract-conformance.mjs
8190
- name: Run guard unit tests
8291
run: node --test scripts/guards/__tests__/*.test.mjs
8392

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,8 @@
4242
"ci:code-health": "node scripts/ci/code-health.mjs --verify && (turbo run code-health --affected || turbo run code-health)",
4343
"ci:pack-dry-run": "turbo run build && node scripts/ci/pack-dry-run.mjs",
4444
"ci:docs-contract": "turbo run docs-contract",
45-
"ci:public-smoke": "turbo run public-integration-smoke"
45+
"ci:public-smoke": "turbo run public-integration-smoke",
46+
"ci:contract-conformance": "node scripts/check-ingest-contract-conformance.mjs"
4647
},
4748
"devDependencies": {
4849
"@typescript-eslint/parser": "^8.59.3",

packages/llmwiki/src/__tests__/live-provider.test.ts

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,26 @@
66
import { describe, it } from "node:test";
77
import assert from "node:assert/strict";
88
import { mkdtemp } from "node:fs/promises";
9+
import { mkdtempSync } from "node:fs";
910
import { tmpdir } from "node:os";
1011
import path from "node:path";
1112
import { LiveLLMWikiProvider } from "../live/provider.ts";
1213

14+
// A real, private directory. These constructor tests previously hardcoded
15+
// TMP_ROOT, which is a shared path any process on the host can occupy. When it
16+
// exists as a FILE, createWiki() fails with "root exists but is not a
17+
// directory" and the positive cases fail, while the negative cases still pass
18+
// because scope validation throws before touching the filesystem. That made the
19+
// suite dependent on ambient host state rather than hermetic.
20+
const TMP_ROOT = mkdtempSync(path.join(tmpdir(), "llmwiki-provider-"));
21+
1322
const scope = { user: "u1" };
1423
const mk = (root: string) => new LiveLLMWikiProvider({ root, scope, projectId: "proj-1" });
1524

1625
describe("LiveLLMWikiProvider", () => {
1726
it("requires projectId", () => {
1827
assert.throws(
19-
() => new (LiveLLMWikiProvider as any)({ root: "/tmp/x", scope }),
28+
() => new (LiveLLMWikiProvider as any)({ root: TMP_ROOT, scope }),
2029
(e: any) => e?.code === "E_LLMWIKI_PROJECT_ID_REQUIRED",
2130
);
2231
});
@@ -64,7 +73,7 @@ describe("LiveLLMWikiProvider", () => {
6473
});
6574

6675
it("capabilities advertises text/messages/verbatim + package", () => {
67-
const c = mk("/tmp/whatever").capabilities();
76+
const c = mk(TMP_ROOT).capabilities();
6877
assert.deepEqual(c.ingestModes, ["text", "messages", "verbatim"]);
6978
assert.equal(c.extensions.package, true);
7079
});
@@ -260,28 +269,28 @@ describe("LiveLLMWikiProvider scope is copied at the boundary (no reference leak
260269
describe("LiveLLMWikiProvider construction scope validation (FIX G)", () => {
261270
it("throws E_LLMWIKI_PROVIDER_SCOPE_MISMATCH when scope is empty {}", () => {
262271
assert.throws(
263-
() => new LiveLLMWikiProvider({ root: "/tmp/x", scope: {}, projectId: "proj-1" }),
272+
() => new LiveLLMWikiProvider({ root: TMP_ROOT, scope: {}, projectId: "proj-1" }),
264273
(e: any) => e?.code === "E_LLMWIKI_PROVIDER_SCOPE_MISMATCH",
265274
);
266275
});
267276

268277
it("throws E_LLMWIKI_PROVIDER_SCOPE_MISMATCH when required field 'user' is missing", () => {
269278
assert.throws(
270-
() => new LiveLLMWikiProvider({ root: "/tmp/x", scope: { namespace: "ns" }, projectId: "proj-1" }),
279+
() => new LiveLLMWikiProvider({ root: TMP_ROOT, scope: { namespace: "ns" }, projectId: "proj-1" }),
271280
(e: any) => e?.code === "E_LLMWIKI_PROVIDER_SCOPE_MISMATCH",
272281
);
273282
});
274283

275284
it("throws E_LLMWIKI_PROVIDER_SCOPE_MISMATCH when 'user' is empty string", () => {
276285
assert.throws(
277-
() => new LiveLLMWikiProvider({ root: "/tmp/x", scope: { user: "" }, projectId: "proj-1" }),
286+
() => new LiveLLMWikiProvider({ root: TMP_ROOT, scope: { user: "" }, projectId: "proj-1" }),
278287
(e: any) => e?.code === "E_LLMWIKI_PROVIDER_SCOPE_MISMATCH",
279288
);
280289
});
281290

282291
it("valid scope { user: 'u1' } constructs without throwing", () => {
283292
assert.doesNotThrow(
284-
() => new LiveLLMWikiProvider({ root: "/tmp/x", scope: { user: "u1" }, projectId: "proj-1" }),
293+
() => new LiveLLMWikiProvider({ root: TMP_ROOT, scope: { user: "u1" }, projectId: "proj-1" }),
285294
);
286295
});
287296
});

packages/llmwiki/src/__tests__/live-registration.test.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,17 @@
66
*/
77
import { describe, it } from "node:test";
88
import assert from "node:assert/strict";
9+
import { mkdtempSync } from "node:fs";
10+
import { tmpdir } from "node:os";
11+
import path from "node:path";
912
import { liveLlmwikiProviderFactory, LiveLLMWikiProvider } from "../live.ts";
1013

14+
// Private directory rather than the shared TMP_ROOT: see live-provider.test.ts.
15+
const TMP_ROOT = mkdtempSync(path.join(tmpdir(), "llmwiki-registration-"));
16+
1117
describe("live registration + barrel", () => {
1218
it("factory returns a LiveLLMWikiProvider", () => {
13-
const { provider } = liveLlmwikiProviderFactory({ root: "/tmp/x", scope: { user: "u1" }, projectId: "proj-1" });
19+
const { provider } = liveLlmwikiProviderFactory({ root: TMP_ROOT, scope: { user: "u1" }, projectId: "proj-1" });
1420
assert.ok(provider instanceof LiveLLMWikiProvider);
1521
});
1622
});

plugins/claude-code/.claude-plugin/plugin.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "atomicmemory",
3-
"version": "0.2.0",
3+
"version": "0.2.1",
44
"description": "Persistent semantic memory for Claude Code — user preferences, project context, prior decisions, and codebase facts that survive across sessions.",
55
"author": {
66
"name": "AtomicMemory",

plugins/claude-code/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@atomicmemory/claude-code-plugin",
3-
"version": "0.2.0",
3+
"version": "0.2.1",
44
"description": "AtomicMemory plugin for Claude Code — persistent semantic memory across sessions.",
55
"private": false,
66
"license": "Apache-2.0",

plugins/codex/.codex-plugin/plugin.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "atomicmemory",
3-
"version": "0.2.0",
3+
"version": "0.2.1",
44
"description": "AtomicMemory memory layer for Codex. Pluggable semantic memory — swap backends through the SDK's MemoryProvider model by config, not code change.",
55
"author": {
66
"name": "AtomicMemory",

plugins/codex/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@atomicmemory/codex-plugin",
3-
"version": "0.2.0",
3+
"version": "0.2.1",
44
"description": "AtomicMemory plugin for OpenAI Codex — plugin manifest, MCP server config, and memory protocol skill.",
55
"private": true,
66
"license": "Apache-2.0",

plugins/codex/skills/atomicmemory/SKILL.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ description: >
1010
license: Apache-2.0
1111
metadata:
1212
author: AtomicMemory
13-
version: "0.2.0"
13+
version: "0.2.1"
1414
category: ai-memory
1515
tags: "memory, semantic-search, codex, pluggable"
1616
---

0 commit comments

Comments
 (0)