diff --git a/docs/decisions/0001-openclaw-jws-response-verification-deferred.md b/docs/decisions/0001-openclaw-jws-response-verification-deferred.md new file mode 100644 index 0000000..7ed1ef7 --- /dev/null +++ b/docs/decisions/0001-openclaw-jws-response-verification-deferred.md @@ -0,0 +1,48 @@ +# 0001 — JWS Response Signature Verification deferred to @moltrust/openclaw-plugin v2.1 + +**Datum:** 2026-05-28 +**Status:** Accepted + +## Context + +Der §12-Review für `@moltrust/openclaw-plugin@2.0.0-alpha.0` (Run 2026-05-28 16:36 UTC, Output `~/moltstack/reviews/20260528_163640_openclaw-plugin-v2.0.0-alpha.0_review.md`) hat als Blocker #3 (Konsens von Gemini 3.1 Pro Preview + Perplexity Sonar Pro) markiert: + +> Keine JWS-Signatur-Verifikation der API-Antworten → MITM-anfällig. + +Die MolTrust-API liefert seit CAEP Profile v1 (LIVE 2026-05-09, `kid: moltrust-registry-2026-v1`) Ed25519-signierte Trust-Scores. Der OpenClaw-Plugin-Client (`moltrust-openclaw-v2/src/client.ts`) ruft die Endpoints heute ohne lokale Signatur-Verifikation auf — er vertraut HTTPS + JSON-Parsing. + +Risiko bei MITM-Szenarien (Corporate-Proxy mit gefälschten CA-Roots, Routing-Manipulation, kompromittierte Edge-Node): gefälschte ALLOW/DENY-Entscheidungen würden vom Plugin unbeanstandet ausgeführt. + +## Decision + +Die JWS-Verifikation wird **explizit deferred** auf `@moltrust/openclaw-plugin v2.1`, nicht in v2.0.0-alpha.1 implementiert. + +Begründung: + +- v2.0.0-alpha.x ist Public-Preview, kein Production-Release. Trust-Threshold ist `0` per Default (opt-in via `minTrustScore`). +- Saubere JWS-Verifikation braucht: (a) öffentlicher Key-Bootstrap-Mechanismus (JWKS-Fetch, Trust-on-First-Use, Pinning?), (b) Rotations-Policy (Plugin muss Key-Rotation der MolTrust-Registry handhaben), (c) Failure-Mode-Spec (Signatur-Mismatch vs Key-not-found vs Clock-Skew). Diese drei Punkte sind eine eigene Design-Spec, kein 1-Sprint-Fix. +- Der gefährlichste Fail-Open-Pfad (Plugin lässt Agents trotz API-Ausfall durch) wird **in v2.0.0-alpha.1** durch Blocker-Fix #1 (`failOpen: false` Default, opt-in) bereits geschlossen — damit ist der primäre MITM-Attack-Surface „API not reachable, fall through" mitigiert. Verbleibende MITM-Surface: aktive Man-in-the-Middle-Manipulation einer eigentlich erreichbaren API-Antwort. + +## Consequences + +**Positiv:** + +- v2.0.0-alpha.1 kann zeitnah re-reviewed und publik gemacht werden (Preview-Status, kein Production-Trust-Gating-Anspruch). +- v2.1-Spec bekommt einen eigenen Sprint mit Design-Review für JWS-Bootstrap, Key-Rotation und Failure-Modes. + +**Negativ:** + +- Bis v2.1 publiziert ist, müssen Operators in MITM-fähigen Netzwerken (z.B. Corporate-Proxy mit Custom-CA) das Plugin als „ungeeignet für Production-Trust-Gating in solchen Umgebungen" einstufen. +- README muss diesen Trade-off explizit kommunizieren (Section „Security Posture & Roadmap"). + +**Pflicht-Begleitmaßnahmen für v2.0.0-alpha.1:** + +- README-Note: „Response signature verification planned for v2.1 — see ADR 0001" +- ADR-Link aus README erreichbar (Discovery) +- v2.1-Spec als Item in `docs/BACKLOG.md` festhalten (separat von diesem PR) + +## Alternatives considered + +1. **JWS-Verify in v2.0.0-alpha.1 ad-hoc implementieren** — verworfen: ohne Bootstrap-/Rotations-Spec wird der Plugin selbst zur Angriffsfläche (z.B. Plugin akzeptiert jeden Key beim First-Use ohne Pinning, oder bricht stumm bei Routine-Key-Rotation der Registry). +2. **Public Release blocken bis v2.1 fertig** — verworfen: v2.0.0-alpha.x ist Preview. Realistische Anwender setzen es jetzt zum Testen ein, nicht für Production-Trust-Gating. Veröffentlichung mit klarer Roadmap-Note ist ehrlicher als Stillstand. +3. **Plugin als private (nicht-npm) Package halten bis v2.1** — verworfen: widerspricht dem v2-Ziel (öffentliche OpenClaw-Integration), und die Preview-Phase ist gerade dafür gedacht, dass Early-Adopters Feedback geben. diff --git a/moltrust-openclaw-v2/.gitignore b/moltrust-openclaw-v2/.gitignore new file mode 100644 index 0000000..69e3e50 --- /dev/null +++ b/moltrust-openclaw-v2/.gitignore @@ -0,0 +1,12 @@ +node_modules/ +dist/ +coverage/ +.DS_Store +*.log +npm-debug.log* +.env +.env.* +!.env.example +.vscode/ +.idea/ +*.tgz diff --git a/moltrust-openclaw-v2/.npmignore b/moltrust-openclaw-v2/.npmignore new file mode 100644 index 0000000..824015b --- /dev/null +++ b/moltrust-openclaw-v2/.npmignore @@ -0,0 +1,15 @@ +src/ +tests/ +node_modules/ +coverage/ +.github/ +.vscode/ +.idea/ +tsconfig.json +vitest.config.ts +.gitignore +.npmignore +.editorconfig +*.log +*.tgz +.DS_Store diff --git a/moltrust-openclaw-v2/README.md b/moltrust-openclaw-v2/README.md index a6b32db..09af68a 100644 --- a/moltrust-openclaw-v2/README.md +++ b/moltrust-openclaw-v2/README.md @@ -1,4 +1,4 @@ -# @moltrust/openclaw v2 +# @moltrust/openclaw-plugin v2 > W3C DID trust verification + lifecycle gating for [OpenClaw](https://openclaw.ai) @@ -7,10 +7,14 @@ v2 adds the four lifecycle hooks the OpenClaw core already exposes (per `before_tool_call`, `inbound_claim`, `gateway_start` — on top of the v1 agent tools / slash commands / gateway RPC / CLI surface. +> **Preview release.** v2.0.0-alpha.x is a public preview, not a Production +> Trust-Gating release. See *Security Posture & Roadmap* below for the +> v2.1 hardening list. + ## Install ```bash -openclaw plugins install @moltrust/openclaw +openclaw plugins install @moltrust/openclaw-plugin ``` Restart your gateway. @@ -47,7 +51,9 @@ New tool: `moltrust_endorse` — issue a SkillEndorsementCredential (W3C VC, "gateAllTools": false, "installAllowlist": [], "installBlocklist": [], - "cacheTtlMs": 300000 + "cacheTtlMs": 10000, + "failOpen": false, + "registerMoltrustTools": true } } } @@ -61,8 +67,8 @@ Get an API key at [api.moltrust.ch/auth/signup](https://api.moltrust.ch/auth/sig ``` src/ -├── openclaw-types.ts vendored OpenClaw plugin SDK types (subset) -├── client.ts MolTrustClient + LRU cache (5 min TTL) +├── openclaw-types.ts vendored OpenClaw plugin SDK types (subset, range 0.9.x–1.0.x) +├── client.ts MolTrustClient + LRU cache (10 s TTL default) ├── utils.ts extractDids / isLikelyDid ├── hooks/ │ ├── before-install.ts makeBeforeInstallHandler({cfg, logger}) @@ -79,17 +85,92 @@ unit-testable without an OpenClaw host. ```bash npm install -npm test # ≥15 vitest tests across hooks + client +npm test # vitest — hooks + client (>= 27 tests) npm run build # produces dist/*.js + *.d.ts ``` -## Fail-open on lookup errors +## Security Posture & Roadmap + +### Fail-closed by default (v2.0.0-alpha.1) + +When a MolTrust API lookup fails (network, rate-limit, 5xx), `before_tool_call` +and `inbound_claim` **block the call/inbound** with a clear `blockReason` +mentioning `failOpen=false`. This is the default — safe for Production +Trust-Gating. + +Opt-in fail-open is available via `failOpen: true` for fleets where +availability matters more than trust-gating (e.g. internal dev environments, +non-financial tools). Set it explicitly and monitor the warn-log. + +### Response signature verification — planned for v2.1 + +This release does **not** verify the Ed25519 JWS signatures that +`api.moltrust.ch` returns on trust-score and verify responses (kid +`moltrust-registry-2026-v1`). It trusts HTTPS + JSON parsing. + +In MITM-capable environments (Corporate-Proxy with custom CA, routing +manipulation, compromised edge node) an attacker could forge ALLOW/DENY +decisions. The fail-closed default mitigates the most common attack path +("API unreachable, fall through"), but does not stop active in-line +manipulation. + +JWS verification is on the v2.1 roadmap as a dedicated design sprint +(JWKS bootstrap, key rotation, failure-mode spec). See [ADR +0001](../docs/decisions/0001-openclaw-jws-response-verification-deferred.md) +in the parent MolTrust API repo for the full reasoning. + +### Cache TTL + +Default `cacheTtlMs: 10000` (10 seconds). Tunes revocation latency vs. +API-call volume. Lower it to 0 to disable caching entirely; raise it only +if your `minTrustScore` threshold is well above the worst-case score of any +agent you'd permit (i.e. cache cannot mask a decision flip). + +### OpenClaw version range + +The plugin vendors a subset of OpenClaw's plugin SDK types +(`src/openclaw-types.ts`) pinned to the upstream signature baseline at +commit `45146913007d` (tested range: 0.9.x – 1.0.x). On host versions +outside this range the hook contracts may diverge silently. Bump-and-test +when upstream cuts a breaking minor. + +## Privacy & Data Handling + +This plugin sends agent DIDs and (optionally) wallet addresses to +`api.moltrust.ch` for trust-score lookups. Specifically: + +- **`before_tool_call`** sends your `agentDid` plus any DIDs found in the + tool call's `params` (via `did:*` regex on string values). +- **`inbound_claim`** sends the sender DID extracted from + `event.metadata.did` or `event.senderId`. +- **`gateway_start`** sends `agentDid` only if `verifyOnStart: true`. +- The `moltrust_verify` / `moltrust_trust_score` / `moltrust_endorse` tools + send whichever DID/address the calling agent passes as the argument. + +**Endpoint:** `https://api.moltrust.ch` (configurable via `apiUrl` — +self-hosting documented separately). + +**Retention:** the MolTrust service stores trust-score lookups per the +operator's privacy policy at [moltrust.ch/privacy](https://moltrust.ch/privacy) +(MolTrust as data processor; you remain controller for your fleet's DIDs). + +**Disabling automatic outbound calls:** set `minTrustScore: 0` and +`verifyOnStart: false`. The lifecycle hooks then make no outbound calls. +However, the `moltrust_verify` / `moltrust_trust_score` / `moltrust_endorse` +agent tools remain **registered with the agent runtime** — an LLM +hallucination or unintended chain-of-thought could still trigger them. + +**True air-gap mode:** additionally set `registerMoltrustTools: false`. +The three `moltrust_*` agent tools are then **not exposed to the agent +runtime at all** — the LLM cannot invoke them. Slash commands +(`/trust`, `/trustscore`) and the gateway RPC methods remain available +for explicit operator/user invocations. -`before_tool_call` and `inbound_claim` log a warning and **do not block** when -a MolTrust API lookup fails (network down, rate limit, etc.). This is a -deliberate design choice: a transient trust-API outage shouldn't take an -agent fleet offline. Operators should monitor the warn-log for sustained -failures. +This is a trust-verification plugin — *intentional* use requires sending +DIDs to MolTrust. There is no way to gate agents on remote trust scores +without that round-trip. If you need air-gapped trust gating, set +`minTrustScore: 0` + `verifyOnStart: false` + `registerMoltrustTools: false` +and rely only on the (manual) slash commands for ad-hoc lookups. ## License diff --git a/moltrust-openclaw-v2/openclaw.plugin.json b/moltrust-openclaw-v2/openclaw.plugin.json index e39a3c4..a067ef3 100644 --- a/moltrust-openclaw-v2/openclaw.plugin.json +++ b/moltrust-openclaw-v2/openclaw.plugin.json @@ -58,9 +58,19 @@ }, "cacheTtlMs": { "type": "integer", - "default": 300000, + "default": 10000, "minimum": 0, - "description": "TTL for client-side cache of verify/score lookups (ms). 0 disables cache." + "description": "TTL for client-side cache of verify/score lookups (ms). 0 disables cache. Default 10s — Zero-Trust appropriate revocation latency." + }, + "failOpen": { + "type": "boolean", + "default": false, + "description": "When a MolTrust API lookup fails (network, rate limit, 5xx), what to do? false (default) = block the call/message (fail-closed, safe). true = warn-log and pass through (fail-open, opt-in for low-criticality fleets where availability beats trust-gating). See ADR 0001." + }, + "registerMoltrustTools": { + "type": "boolean", + "default": true, + "description": "Register the moltrust_verify / moltrust_trust_score / moltrust_endorse agent tools? Default true. Set false for true air-gapping: tools won't be exposed to the agent runtime, so LLM hallucinations cannot trigger outbound DID lookups. Slash commands and lifecycle hooks are unaffected." } } }, @@ -70,6 +80,8 @@ "minTrustScore": { "label": "Minimum Trust Score (0=off)", "placeholder": "50" }, "agentDid": { "label": "Your Agent DID", "placeholder": "did:moltrust:..." }, "verifyOnStart": { "label": "Self-verify on gateway start" }, - "gateAllTools": { "label": "Gate ALL tools (not just sensitive prefixes)" } + "gateAllTools": { "label": "Gate ALL tools (not just sensitive prefixes)" }, + "failOpen": { "label": "Fail-open on API lookup error (opt-in — fail-closed is safer)" }, + "registerMoltrustTools": { "label": "Register moltrust_* agent tools (set false for air-gap)" } } } diff --git a/moltrust-openclaw-v2/package-lock.json b/moltrust-openclaw-v2/package-lock.json index a043831..68c9c3c 100644 --- a/moltrust-openclaw-v2/package-lock.json +++ b/moltrust-openclaw-v2/package-lock.json @@ -1,17 +1,20 @@ { - "name": "@moltrust/openclaw", - "version": "2.0.0-alpha.0", + "name": "@moltrust/openclaw-plugin", + "version": "2.0.0-alpha.2", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "@moltrust/openclaw", - "version": "2.0.0-alpha.0", + "name": "@moltrust/openclaw-plugin", + "version": "2.0.0-alpha.2", "license": "MIT", "devDependencies": { "@types/node": "^20.0.0", "typescript": "^5.4.0", "vitest": "^1.6.0" + }, + "engines": { + "node": ">=20" } }, "node_modules/@esbuild/aix-ppc64": { diff --git a/moltrust-openclaw-v2/package.json b/moltrust-openclaw-v2/package.json index 05491ec..299cd95 100644 --- a/moltrust-openclaw-v2/package.json +++ b/moltrust-openclaw-v2/package.json @@ -1,7 +1,7 @@ { - "name": "@moltrust/openclaw", - "version": "2.0.0-alpha.0", - "description": "MolTrust v2 trust-provider plugin for OpenClaw — lifecycle hooks for install / tool-call / inbound / gateway-start gating, plus W3C DID identity verification, trust scoring, sybil detection.", + "name": "@moltrust/openclaw-plugin", + "version": "2.0.0-alpha.2", + "description": "MolTrust v2 trust-provider plugin for OpenClaw — lifecycle hooks for install / tool-call / inbound / gateway-start gating, plus W3C DID identity verification, trust scoring, sybil detection. Fail-closed by default.", "keywords": [ "openclaw", "moltrust", @@ -15,8 +15,12 @@ "homepage": "https://moltrust.ch", "repository": { "type": "git", - "url": "https://github.com/MoltyCel/moltrust-openclaw" + "url": "git+https://github.com/MoltyCel/moltrust-openclaw-plugin.git" }, + "bugs": { + "url": "https://github.com/MoltyCel/moltrust-openclaw-plugin/issues" + }, + "author": "Lars Kroehl (https://moltrust.ch)", "license": "MIT", "type": "module", "main": "dist/index.js", @@ -24,8 +28,15 @@ "files": [ "dist", "openclaw.plugin.json", - "LICENSE" + "LICENSE", + "README.md" ], + "engines": { + "node": ">=20" + }, + "publishConfig": { + "access": "public" + }, "openclaw": { "extensions": [ "./dist/index.js" @@ -35,7 +46,8 @@ "build": "tsc", "dev": "tsc --watch", "test": "vitest run", - "test:watch": "vitest" + "test:watch": "vitest", + "prepublishOnly": "npm run build && npm test" }, "devDependencies": { "@types/node": "^20.0.0", diff --git a/moltrust-openclaw-v2/src/hooks/before-tool-call.ts b/moltrust-openclaw-v2/src/hooks/before-tool-call.ts index d792983..9dc5f90 100644 --- a/moltrust-openclaw-v2/src/hooks/before-tool-call.ts +++ b/moltrust-openclaw-v2/src/hooks/before-tool-call.ts @@ -11,8 +11,12 @@ * - toolName not in any cfg.sensitivePrefixes AND cfg.gateAllTools is false * - cfg.minTrustScore <= 0 (opt-in design) * - * Failure mode: lookup errors fail-OPEN by design (warn-log, don't block). - * Returns {block: true, blockReason} on actual policy violation. + * Failure mode (lookup errors — network, rate-limit, 5xx): controlled by + * cfg.failOpen. Default cfg.failOpen=false → fail-CLOSED: block the call + * with a clear blockReason ("API unreachable"). Opt-in cfg.failOpen=true → + * warn-log and pass through (legacy v2.0.0-alpha.0 behavior, only for fleets + * where availability beats trust-gating). See ADR 0001 + README "Security + * Posture & Roadmap". */ import type { MolTrustClient } from "../client.js"; import type { @@ -68,30 +72,50 @@ export function makeBeforeToolCallHandler(deps: BeforeToolCallDeps) { return { block: true, blockReason: reason }; } } catch (err) { - logger.warn( - `[moltrust] own DID score lookup failed: ${(err as Error).message}`, - ); - // fail-open + const msg = (err as Error).message; + if (cfg.failOpen) { + logger.warn( + `[moltrust] own DID score lookup failed (failOpen=true, allowing): ${msg}`, + ); + } else { + const reason = `[moltrust] tool ${toolName} blocked: own DID ${cfg.agentDid} score lookup failed and failOpen=false: ${msg}`; + logger.warn(reason); + return { block: true, blockReason: reason }; + } } } - // Check 2: counterparty DIDs in params + // Check 2: counterparty DIDs in params. + // Lookups run in parallel via Promise.allSettled — sequential await per + // counterparty was O(N × API-latency) and could add 200ms+ to a 4-DID + // tool call. Block-priority is deterministic: first counterparty in + // array order whose result triggers a block wins. const counterparties = extractDids(event.params).filter( (d) => d !== cfg.agentDid, ); - for (const did of counterparties) { - try { - const score = await client.getTrustScore(did); - if (score.score < cfg.minTrustScore) { - const reason = `[moltrust] tool ${toolName} blocked: counterparty ${did} score ${score.score} < threshold ${cfg.minTrustScore}`; + const results = await Promise.allSettled( + counterparties.map((did) => client.getTrustScore(did)), + ); + for (let i = 0; i < counterparties.length; i++) { + const did = counterparties[i]; + const r = results[i]; + if (r.status === "fulfilled") { + if (r.value.score < cfg.minTrustScore) { + const reason = `[moltrust] tool ${toolName} blocked: counterparty ${did} score ${r.value.score} < threshold ${cfg.minTrustScore}`; + logger.warn(reason); + return { block: true, blockReason: reason }; + } + } else { + const msg = (r.reason as Error).message; + if (cfg.failOpen) { + logger.warn( + `[moltrust] counterparty ${did} lookup failed (failOpen=true, allowing): ${msg}`, + ); + } else { + const reason = `[moltrust] tool ${toolName} blocked: counterparty ${did} score lookup failed and failOpen=false: ${msg}`; logger.warn(reason); return { block: true, blockReason: reason }; } - } catch (err) { - logger.warn( - `[moltrust] counterparty ${did} lookup failed: ${(err as Error).message}`, - ); - // fail-open: skip this counterparty } } diff --git a/moltrust-openclaw-v2/src/hooks/inbound-claim.ts b/moltrust-openclaw-v2/src/hooks/inbound-claim.ts index ea42d33..a345bcf 100644 --- a/moltrust-openclaw-v2/src/hooks/inbound-claim.ts +++ b/moltrust-openclaw-v2/src/hooks/inbound-claim.ts @@ -10,8 +10,10 @@ * - if cfg.minTrustScore <= 0 → no-op * - fetch trust score; if below threshold → return handled+warn-reply * - * Failure mode: lookup errors fail-open (don't block message delivery on a - * transient API failure). + * Failure mode (lookup errors): controlled by cfg.failOpen. Default + * cfg.failOpen=false → fail-CLOSED: block the inbound claim with a warn-reply + * (handled: true). Opt-in cfg.failOpen=true → warn-log and pass through. + * See ADR 0001 + README "Security Posture & Roadmap". */ import type { MolTrustClient } from "../client.js"; import type { @@ -52,10 +54,17 @@ export function makeInboundClaimHandler(deps: InboundClaimDeps) { return { handled: true, reply: { content: `⚠️ ${reason}` } }; } } catch (err) { - logger.warn( - `[moltrust] inbound DID ${senderDid} lookup failed: ${(err as Error).message}`, - ); - // fail-open + const msg = (err as Error).message; + if (cfg.failOpen) { + logger.warn( + `[moltrust] inbound DID ${senderDid} lookup failed (failOpen=true, allowing): ${msg}`, + ); + // fall through to undefined (allow) + } else { + const reason = `Inbound message from ${senderDid} blocked: trust score lookup failed and failOpen=false: ${msg}`; + logger.warn(`[moltrust] ${reason}`); + return { handled: true, reply: { content: `⚠️ ${reason}` } }; + } } return undefined; }; diff --git a/moltrust-openclaw-v2/src/index.ts b/moltrust-openclaw-v2/src/index.ts index 84410ff..59616df 100644 --- a/moltrust-openclaw-v2/src/index.ts +++ b/moltrust-openclaw-v2/src/index.ts @@ -58,7 +58,16 @@ export default function register(api: OpenClawPluginApi): void { ); // ── v1 surface preserved: tools / commands / RPC / CLI ───────────────── - api.registerTool?.({ + // moltrust_* agent tools can be air-gapped via cfg.registerMoltrustTools=false + // (LLM-callable, so the riskier surface). Slash commands + RPC + CLI remain + // active regardless — those are explicit operator/user invocations. + if (!cfg.registerMoltrustTools) { + logger.info( + "[moltrust] registerMoltrustTools=false — moltrust_verify / moltrust_trust_score / moltrust_endorse NOT exposed as agent tools (air-gap mode). Slash commands + lifecycle hooks remain active.", + ); + } + + if (cfg.registerMoltrustTools) api.registerTool?.({ name: "moltrust_verify", description: "Verify an AI agent's W3C DID identity against MolTrust. Returns verified status, trust score, and Verifiable Credential details.", @@ -97,7 +106,7 @@ export default function register(api: OpenClawPluginApi): void { }, }); - api.registerTool?.({ + if (cfg.registerMoltrustTools) api.registerTool?.({ name: "moltrust_trust_score", description: "Get the MolTrust trust score (0-100) for an AI agent by DID or wallet address. Includes sybil detection and behavioral history.", @@ -123,7 +132,7 @@ export default function register(api: OpenClawPluginApi): void { }, }); - api.registerTool?.({ + if (cfg.registerMoltrustTools) api.registerTool?.({ name: "moltrust_endorse", description: "Endorse another agent's skill via MolTrust SkillEndorsementCredential (W3C VC, 90-day expiry). Requires apiKey in plugin config.", diff --git a/moltrust-openclaw-v2/src/openclaw-types.ts b/moltrust-openclaw-v2/src/openclaw-types.ts index 5d7438c..4e59528 100644 --- a/moltrust-openclaw-v2/src/openclaw-types.ts +++ b/moltrust-openclaw-v2/src/openclaw-types.ts @@ -3,6 +3,12 @@ * surface. Kept small and stable so the plugin doesn't break when upstream * tightens private fields. Track upstream in the close-comment of * openclaw/openclaw#49971 (commit 45146913007d). + * + * Tested OpenClaw version range: 0.9.x — 1.0.x (commit 45146913007d baseline). + * If you load this plugin into an OpenClaw build outside this range, the + * vendored hook signatures may diverge from the host's runtime expectations. + * On upstream-breaking changes, bump the plugin's minor version and update + * this anchor — do not silently expand the range. */ // ─── Logger ────────────────────────────────────────────────────────────────── @@ -155,6 +161,8 @@ export interface MolTrustConfig { installAllowlist?: string[]; installBlocklist?: string[]; cacheTtlMs?: number; + failOpen?: boolean; + registerMoltrustTools?: boolean; } export const DEFAULT_CONFIG: Required = { @@ -167,5 +175,7 @@ export const DEFAULT_CONFIG: Required = { gateAllTools: false, installAllowlist: [], installBlocklist: [], - cacheTtlMs: 300_000, + cacheTtlMs: 10_000, + failOpen: false, + registerMoltrustTools: true, }; diff --git a/moltrust-openclaw-v2/tests/before-tool-call.test.ts b/moltrust-openclaw-v2/tests/before-tool-call.test.ts index 5c4d314..a6592cf 100644 --- a/moltrust-openclaw-v2/tests/before-tool-call.test.ts +++ b/moltrust-openclaw-v2/tests/before-tool-call.test.ts @@ -141,7 +141,7 @@ describe("before_tool_call", () => { expect(r?.block).toBe(true); }); - it("fails open on lookup error (warns but does not block)", async () => { + it("fails CLOSED by default on lookup error (blocks the call)", async () => { const logger = stubLogger(); const h = makeBeforeToolCallHandler({ cfg: { @@ -152,10 +152,82 @@ describe("before_tool_call", () => { client: makeClient({}), logger, }); + const r = (await h({ toolName: "pay_send", params: {} }, {})) as + | Block + | undefined; + expect(r?.block).toBe(true); + expect(r?.blockReason).toContain("failOpen=false"); + expect(logger.warn).toHaveBeenCalledWith( + expect.stringContaining("lookup failed"), + ); + }); + + it("fails OPEN when failOpen=true is explicitly set (allows the call)", async () => { + const logger = stubLogger(); + const h = makeBeforeToolCallHandler({ + cfg: { + ...DEFAULT_CONFIG, + minTrustScore: 50, + agentDid: "did:moltrust:unknown", + failOpen: true, + }, + client: makeClient({}), + logger, + }); const r = await h({ toolName: "pay_send", params: {} }, {}); expect(r).toBeUndefined(); expect(logger.warn).toHaveBeenCalledWith( - expect.stringContaining("lookup failed"), + expect.stringContaining("failOpen=true"), + ); + }); + + it("fails CLOSED on counterparty lookup error (default)", async () => { + const logger = stubLogger(); + const h = makeBeforeToolCallHandler({ + cfg: { + ...DEFAULT_CONFIG, + minTrustScore: 50, + agentDid: "did:moltrust:self", + }, + client: makeClient({ "did:moltrust:self": 90 }), + logger, + }); + const r = (await h( + { + toolName: "pay_send", + params: { to: "did:moltrust:unknown" }, + }, + {}, + )) as Block | undefined; + expect(r?.block).toBe(true); + expect(r?.blockReason).toContain("counterparty"); + expect(r?.blockReason).toContain("failOpen=false"); + }); + + it("combinatorial: own DID OK + counterparty lookup fails + failOpen=true → ALLOW", async () => { + const logger = stubLogger(); + const h = makeBeforeToolCallHandler({ + cfg: { + ...DEFAULT_CONFIG, + minTrustScore: 50, + agentDid: "did:moltrust:self", + failOpen: true, + }, + client: makeClient({ "did:moltrust:self": 90 }), + logger, + }); + const r = await h( + { + toolName: "pay_send", + params: { to: "did:moltrust:unknown" }, + }, + {}, + ); + expect(r).toBeUndefined(); + expect(logger.warn).toHaveBeenCalledWith( + expect.stringContaining( + "counterparty did:moltrust:unknown lookup failed (failOpen=true", + ), ); }); }); diff --git a/moltrust-openclaw-v2/tests/inbound-claim.test.ts b/moltrust-openclaw-v2/tests/inbound-claim.test.ts index 1af5648..63f4113 100644 --- a/moltrust-openclaw-v2/tests/inbound-claim.test.ts +++ b/moltrust-openclaw-v2/tests/inbound-claim.test.ts @@ -61,4 +61,30 @@ describe("inbound_claim", () => { const r = await h({ senderId: "alice@example.com" }, {}); expect(r).toBeUndefined(); }); + + it("fails CLOSED by default on lookup error (blocks the inbound)", async () => { + const h = makeInboundClaimHandler({ + cfg: { ...DEFAULT_CONFIG, minTrustScore: 50 }, + client: makeClient({}), + logger: stubLogger(), + }); + const r = await h({ metadata: { did: "did:moltrust:unknown" } }, {}); + expect(r?.handled).toBe(true); + expect(r?.reply?.content).toContain("lookup failed"); + expect(r?.reply?.content).toContain("failOpen=false"); + }); + + it("fails OPEN when failOpen=true (passes the inbound)", async () => { + const logger = stubLogger(); + const h = makeInboundClaimHandler({ + cfg: { ...DEFAULT_CONFIG, minTrustScore: 50, failOpen: true }, + client: makeClient({}), + logger, + }); + const r = await h({ metadata: { did: "did:moltrust:unknown" } }, {}); + expect(r).toBeUndefined(); + expect(logger.warn).toHaveBeenCalledWith( + expect.stringContaining("failOpen=true"), + ); + }); });