diff --git a/README.md b/README.md
index 482e0e2..6524967 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
-# openclaw-predicate-provider
+# predicate-claw
> **Stop prompt injection before it executes.**
@@ -17,8 +17,8 @@ Policy: DENY (sensitive_path + untrusted_source)
Result: ActionDeniedError — SSH key never read
```
-[](https://www.npmjs.com/package/openclaw-predicate-provider)
-[](https://github.com/PredicateSystems/openclaw-predicate-provider/actions)
+[](https://www.npmjs.com/package/predicate-claw)
+[](https://github.com/PredicateSystems/predicate-claw/actions)
[](LICENSE)
---
@@ -45,6 +45,8 @@ a document, or a webpage can hijack your agent.
Predicate Authority intercepts every tool call and authorizes it **before execution**.
+*Identity providers give your agent a passport. Predicate gives it a work visa.* We don't just know who the agent is; we cryptographically verify exactly what it is allowed to do, right when it tries to do it.
+
| Without Protection | With Predicate Authority |
|-------------------|-------------------------|
| Agent reads ~/.ssh/id_rsa | **BLOCKED** - sensitive path |
@@ -57,21 +59,37 @@ Predicate Authority intercepts every tool call and authorizes it **before execut
- 🔒 **Deterministic** — No probabilistic filtering, reproducible decisions
- 🚫 **Fail-closed** — Errors block execution, never allow
- 📋 **Auditable** — Every decision logged with full context
+- 🛡️ **Zero-egress** — Sidecar runs locally; no data leaves your infrastructure
---
## Quick Start
+### 0. Prerequisites
+
+This SDK requires the Predicate Authority sidecar to evaluate policies locally.
+
+```bash
+# macOS (Homebrew)
+brew install predicatesystems/tap/predicate-authorityd
+
+# Or via install script
+curl -sSL https://predicate.systems/install.sh | bash
+
+# Or Docker
+docker run -d -p 8787:8787 predicatesystems/authorityd:latest
+```
+
### 1. Install
```bash
-npm install openclaw-predicate-provider
+npm install predicate-claw
```
### 2. Protect your agent
```typescript
-import { GuardedProvider } from "openclaw-predicate-provider";
+import { GuardedProvider } from "predicate-claw";
const provider = new GuardedProvider({
principal: "agent:my-agent",
@@ -92,8 +110,8 @@ const content = await fs.readFile(path); // Only runs if authorized
### 3. See it in action
```bash
-git clone https://github.com/PredicateSystems/openclaw-predicate-provider
-cd openclaw-predicate-provider
+git clone https://github.com/PredicateSystems/predicate-claw
+cd predicate-claw
npm install
npm run test:demo
```
@@ -386,5 +404,5 @@ MIT OR Apache-2.0
Don't let prompt injection own your agent.
- npm install openclaw-predicate-provider
+ npm install predicate-claw
diff --git a/examples/README.md b/examples/README.md
index 18aafe8..ded175b 100644
--- a/examples/README.md
+++ b/examples/README.md
@@ -112,6 +112,56 @@ sidecar runs.
3. Show deny result and user-facing blocked message.
4. Show test command and green output as reproducible evidence.
+## Non-Web Evidence Provider Demo
+
+Demonstrates terminal and desktop accessibility evidence providers with canonical
+hashing for reproducible `state_hash` computation.
+
+### Run the Demo
+
+```bash
+npx tsx examples/non-web-evidence-demo.ts
+```
+
+### What It Shows
+
+1. **Terminal Evidence** - Captures command-line state with:
+ - Path normalization (`/workspace/./src/../src` → `/workspace/src`)
+ - Whitespace collapsing (`git status` → `git status`)
+ - ANSI code stripping (removes color codes)
+ - Timestamp normalization (`[12:34:56]` → `[TIMESTAMP]`)
+ - Secret redaction (environment variables like `AWS_SECRET_KEY`)
+
+2. **Desktop Evidence** - Captures accessibility tree state with:
+ - App name normalization
+ - UI tree text normalization
+ - Whitespace handling
+
+3. **Hash Stability** - Proves that minor variations produce identical hashes
+ when canonicalization is enabled.
+
+### API Usage
+
+```typescript
+import {
+ OpenClawTerminalEvidenceProvider,
+ buildTerminalEvidenceFromProvider,
+} from "predicate-claw";
+
+const provider = new OpenClawTerminalEvidenceProvider(() => ({
+ sessionId: "my-session",
+ cwd: process.cwd(),
+ command: "npm test",
+ transcript: "...",
+}));
+
+const evidence = await buildTerminalEvidenceFromProvider(provider, {
+ useCanonicalHash: true, // default
+});
+
+console.log(evidence.state_hash); // sha256:...
+```
+
## Other Examples
- `openclaw_integration_example.py` - Python integration example
diff --git a/examples/non-web-evidence-demo.ts b/examples/non-web-evidence-demo.ts
new file mode 100644
index 0000000..a38f31e
--- /dev/null
+++ b/examples/non-web-evidence-demo.ts
@@ -0,0 +1,184 @@
+/**
+ * Non-Web Evidence Provider Demo
+ *
+ * This example demonstrates how to use the terminal and desktop accessibility
+ * evidence providers with canonical hashing for reproducible state_hash computation.
+ *
+ * The canonicalization ensures that minor variations (ANSI codes, timestamps,
+ * whitespace) don't break hash verification.
+ *
+ * Run with: npx tsx examples/non-web-evidence-demo.ts
+ */
+
+import {
+ OpenClawTerminalEvidenceProvider,
+ OpenClawDesktopAccessibilityEvidenceProvider,
+ buildTerminalEvidenceFromProvider,
+ buildDesktopEvidenceFromProvider,
+ type TerminalRuntimeContext,
+ type DesktopRuntimeContext,
+} from "../src/index.js";
+
+// ============================================================================
+// Terminal Evidence Demo
+// ============================================================================
+
+async function demoTerminalEvidence(): Promise {
+ console.log("=".repeat(60));
+ console.log("Terminal Evidence Provider Demo");
+ console.log("=".repeat(60));
+
+ // Simulated terminal runtime context (in real usage, capture from actual terminal)
+ const terminalContext: TerminalRuntimeContext = {
+ sessionId: "demo-session-001",
+ terminalId: "term-1",
+ cwd: "/workspace/./src/../src", // Will be normalized to /workspace/src
+ command: "git status ", // Extra whitespace will be collapsed
+ // Transcript with ANSI codes and timestamps that will be normalized
+ transcript: `\x1b[32m[12:34:56]\x1b[0m Running git status...
+On branch main
+Your branch is up to date with 'origin/main'.
+
+\x1b[33mnothing to commit, working tree clean\x1b[0m`,
+ env: {
+ HOME: "/home/user",
+ PATH: "/usr/bin:/bin",
+ AWS_SECRET_KEY: "should-be-redacted", // Secrets are redacted
+ EDITOR: "vim",
+ },
+ };
+
+ // Create provider with capture function
+ const terminalProvider = new OpenClawTerminalEvidenceProvider(
+ () => terminalContext,
+ );
+
+ // Build evidence with canonical hashing (default)
+ const terminalEvidence = await buildTerminalEvidenceFromProvider(
+ terminalProvider,
+ { useCanonicalHash: true },
+ );
+
+ console.log("\nTerminal Evidence:");
+ console.log(JSON.stringify(terminalEvidence, null, 2));
+
+ // Demonstrate hash stability - same content with different formatting
+ const terminalContext2: TerminalRuntimeContext = {
+ ...terminalContext,
+ cwd: "/workspace/src", // Same path after normalization
+ command: "git status", // Same command after whitespace collapse
+ // Same content without ANSI codes and different timestamps
+ transcript: `[09:00:00] Running git status...
+On branch main
+Your branch is up to date with 'origin/main'.
+
+nothing to commit, working tree clean`,
+ };
+
+ const terminalProvider2 = new OpenClawTerminalEvidenceProvider(
+ () => terminalContext2,
+ );
+
+ const terminalEvidence2 = await buildTerminalEvidenceFromProvider(
+ terminalProvider2,
+ { useCanonicalHash: true },
+ );
+
+ console.log("\nTerminal Evidence (normalized variant):");
+ console.log(JSON.stringify(terminalEvidence2, null, 2));
+
+ const hashesMatch = terminalEvidence.state_hash === terminalEvidence2.state_hash;
+ console.log(`\nHashes match: ${hashesMatch ? "YES (canonicalization working)" : "NO"}`);
+}
+
+// ============================================================================
+// Desktop Accessibility Evidence Demo
+// ============================================================================
+
+async function demoDesktopEvidence(): Promise {
+ console.log("\n" + "=".repeat(60));
+ console.log("Desktop Accessibility Evidence Provider Demo");
+ console.log("=".repeat(60));
+
+ // Simulated desktop accessibility context
+ const desktopContext: DesktopRuntimeContext = {
+ appName: " Visual Studio Code ", // Will be trimmed and normalized
+ windowTitle: "main.ts - my-project",
+ focusedRole: "editor",
+ focusedName: "Text Editor",
+ // Simulated UI tree text (in real usage, capture from OS accessibility API)
+ uiTreeText: `
+ Window: Visual Studio Code
+ Toolbar:
+ Button: New File
+ Button: Save
+ Editor:
+ TextArea: main.ts content
+ StatusBar:
+ Label: Ln 42, Col 15
+ `,
+ confidence: 0.95,
+ };
+
+ // Create provider with capture function
+ const desktopProvider = new OpenClawDesktopAccessibilityEvidenceProvider(
+ () => desktopContext,
+ );
+
+ // Build evidence with canonical hashing
+ const desktopEvidence = await buildDesktopEvidenceFromProvider(
+ desktopProvider,
+ { useCanonicalHash: true },
+ );
+
+ console.log("\nDesktop Evidence:");
+ console.log(JSON.stringify(desktopEvidence, null, 2));
+
+ // Demonstrate hash stability with whitespace variations
+ const desktopContext2: DesktopRuntimeContext = {
+ ...desktopContext,
+ appName: "Visual Studio Code", // Same after normalization
+ // Same content with different whitespace
+ uiTreeText: `Window: Visual Studio Code
+Toolbar:
+Button: New File
+Button: Save
+Editor:
+TextArea: main.ts content
+StatusBar:
+Label: Ln 42, Col 15`,
+ };
+
+ const desktopProvider2 = new OpenClawDesktopAccessibilityEvidenceProvider(
+ () => desktopContext2,
+ );
+
+ const desktopEvidence2 = await buildDesktopEvidenceFromProvider(
+ desktopProvider2,
+ { useCanonicalHash: true },
+ );
+
+ console.log("\nDesktop Evidence (normalized variant):");
+ console.log(JSON.stringify(desktopEvidence2, null, 2));
+
+ const hashesMatch = desktopEvidence.state_hash === desktopEvidence2.state_hash;
+ console.log(`\nHashes match: ${hashesMatch ? "YES (canonicalization working)" : "NO"}`);
+}
+
+// ============================================================================
+// Main
+// ============================================================================
+
+async function main(): Promise {
+ console.log("OpenClaw Non-Web Evidence Provider Demo");
+ console.log("Demonstrates canonical hashing for terminal and desktop contexts\n");
+
+ await demoTerminalEvidence();
+ await demoDesktopEvidence();
+
+ console.log("\n" + "=".repeat(60));
+ console.log("Demo complete!");
+ console.log("=".repeat(60));
+}
+
+main().catch(console.error);
diff --git a/package-lock.json b/package-lock.json
index 7e68710..01478d2 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,15 +1,15 @@
{
- "name": "openclaw-predicate-provider",
+ "name": "predicate-claw",
"version": "0.1.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
- "name": "openclaw-predicate-provider",
+ "name": "predicate-claw",
"version": "0.1.0",
"license": "(MIT OR Apache-2.0)",
"dependencies": {
- "@predicatesystems/authority": "^0.3.2"
+ "@predicatesystems/authority": "^0.3.3"
},
"devDependencies": {
"@types/node": "^25.3.0",
@@ -3743,9 +3743,9 @@
}
},
"node_modules/@predicatesystems/authority": {
- "version": "0.3.2",
- "resolved": "https://registry.npmjs.org/@predicatesystems/authority/-/authority-0.3.2.tgz",
- "integrity": "sha512-+QAd3Bo4lgY17SGN1hU39se/cdAvlehyZ7E+YtABtcZsJTWxS4bu2b0FuH+7YFkpn3T+05ckV173UdtBSAAjVg==",
+ "version": "0.3.3",
+ "resolved": "https://registry.npmjs.org/@predicatesystems/authority/-/authority-0.3.3.tgz",
+ "integrity": "sha512-AGGfrzgnox7IG/9o3tAVLDd4eRkxvz+JTkNoQ+ypiQwxqRMchOX3gXyBP78pqg0TtkkBsCwtGMN8ml7XdE0otw==",
"license": "(MIT OR Apache-2.0)",
"engines": {
"node": ">=20.0.0"
diff --git a/package.json b/package.json
index 2aca266..0b328dd 100644
--- a/package.json
+++ b/package.json
@@ -1,5 +1,5 @@
{
- "name": "openclaw-predicate-provider",
+ "name": "predicate-claw",
"version": "0.1.0",
"description": "TypeScript OpenClaw security provider with Predicate Authority pre-execution checks.",
"main": "dist/src/index.js",
@@ -16,7 +16,7 @@
"license": "(MIT OR Apache-2.0)",
"type": "module",
"dependencies": {
- "@predicatesystems/authority": "^0.3.2"
+ "@predicatesystems/authority": "^0.3.3"
},
"devDependencies": {
"@types/node": "^25.3.0",
diff --git a/src/non-web-evidence.ts b/src/non-web-evidence.ts
index aa33588..e88a9e7 100644
--- a/src/non-web-evidence.ts
+++ b/src/non-web-evidence.ts
@@ -1,12 +1,11 @@
-import crypto from "node:crypto";
import {
- buildDesktopAccessibilityStateEvidence,
- buildTerminalStateEvidence,
type DesktopAccessibilityEvidenceProvider,
type DesktopAccessibilitySnapshot,
type StateEvidence,
type TerminalEvidenceProvider,
type TerminalSessionSnapshot,
+ buildDesktopAccessibilityStateEvidence,
+ buildTerminalStateEvidence,
} from "@predicatesystems/authority";
export interface TerminalRuntimeContext {
@@ -14,9 +13,12 @@ export interface TerminalRuntimeContext {
terminalId?: string;
cwd?: string;
command?: string;
+ /** Raw transcript text (will be canonicalized before hashing) */
transcript?: string;
observedAt?: string;
confidence?: number;
+ /** Environment variables (secrets will be redacted before hashing) */
+ env?: Record;
}
export interface DesktopRuntimeContext {
@@ -24,7 +26,9 @@ export interface DesktopRuntimeContext {
windowTitle?: string;
focusedRole?: string;
focusedName?: string;
+ /** Raw UI tree text (will be normalized before hashing) */
uiTreeText?: string;
+ /** Pre-computed UI tree hash (bypasses canonicalization) */
uiTreeHash?: string;
observedAt?: string;
confidence?: number;
@@ -46,7 +50,10 @@ export class OpenClawTerminalEvidenceProvider
terminal_id: runtime.terminalId,
cwd: runtime.cwd,
command: runtime.command,
- transcript_hash: sha256(runtime.transcript ?? ""),
+ // Pass raw transcript - canonicalization happens in buildTerminalStateEvidence
+ // when useCanonicalHash is enabled. The field is named transcript_hash for
+ // backward compatibility but carries raw text when canonical hashing is used.
+ transcript_hash: runtime.transcript ?? "",
observed_at: runtime.observedAt ?? new Date().toISOString(),
confidence: runtime.confidence,
};
@@ -64,32 +71,46 @@ export class OpenClawDesktopAccessibilityEvidenceProvider
async captureAccessibilitySnapshot(): Promise {
const runtime = await this.capture();
+ // Pass raw UI tree text - canonicalization happens in buildDesktopAccessibilityStateEvidence
+ // when useCanonicalHash is enabled. Use pre-computed hash if available for legacy mode.
+ // The field is named ui_tree_hash for backward compatibility but carries raw text
+ // when canonical hashing is used.
return {
app_name: runtime.appName,
window_title: runtime.windowTitle,
focused_role: runtime.focusedRole,
focused_name: runtime.focusedName,
- ui_tree_hash: runtime.uiTreeHash ?? sha256(runtime.uiTreeText ?? ""),
+ ui_tree_hash: runtime.uiTreeHash ?? runtime.uiTreeText ?? "",
observed_at: runtime.observedAt ?? new Date().toISOString(),
confidence: runtime.confidence,
};
}
}
+export interface BuildEvidenceOptions {
+ /**
+ * Use canonical hashing with proper normalization.
+ * When true, applies ANSI stripping, timestamp normalization, whitespace
+ * collapsing, and other canonicalization rules for reproducible hashes.
+ * @default true
+ */
+ useCanonicalHash?: boolean;
+}
+
export async function buildTerminalEvidenceFromProvider(
provider: TerminalEvidenceProvider,
+ options: BuildEvidenceOptions = {},
): Promise {
const snapshot = await provider.captureTerminalSnapshot();
- return buildTerminalStateEvidence({ snapshot });
+ const useCanonicalHash = options.useCanonicalHash ?? true;
+ return buildTerminalStateEvidence({ snapshot, useCanonicalHash });
}
export async function buildDesktopEvidenceFromProvider(
provider: DesktopAccessibilityEvidenceProvider,
+ options: BuildEvidenceOptions = {},
): Promise {
const snapshot = await provider.captureAccessibilitySnapshot();
- return buildDesktopAccessibilityStateEvidence({ snapshot });
-}
-
-function sha256(input: string): string {
- return crypto.createHash("sha256").update(input).digest("hex");
+ const useCanonicalHash = options.useCanonicalHash ?? true;
+ return buildDesktopAccessibilityStateEvidence({ snapshot, useCanonicalHash });
}
diff --git a/tests/non-web-evidence.test.ts b/tests/non-web-evidence.test.ts
index cd1a642..7bab9c8 100644
--- a/tests/non-web-evidence.test.ts
+++ b/tests/non-web-evidence.test.ts
@@ -7,50 +7,162 @@ import {
} from "../src/non-web-evidence.js";
describe("non-web evidence providers", () => {
- it("builds terminal session state evidence", async () => {
- const provider = new OpenClawTerminalEvidenceProvider(() => ({
- sessionId: "s-1",
- terminalId: "t-1",
- cwd: "/workspace",
- command: "cat secrets.txt",
- transcript: "line1\nline2\n",
- observedAt: "2026-02-20T08:00:00.000Z",
- confidence: 0.92,
- }));
-
- const snapshot = await provider.captureTerminalSnapshot();
- const evidence = await buildTerminalEvidenceFromProvider(provider);
-
- expect(snapshot.session_id).toBe("s-1");
- expect(snapshot.transcript_hash).toEqual(expect.any(String));
- expect(evidence).toMatchObject({
- source: "terminal",
- schema_version: "terminal-v1",
- confidence: 0.92,
+ describe("legacy mode (useCanonicalHash=false)", () => {
+ it("builds terminal session state evidence", async () => {
+ const provider = new OpenClawTerminalEvidenceProvider(() => ({
+ sessionId: "s-1",
+ terminalId: "t-1",
+ cwd: "/workspace",
+ command: "cat secrets.txt",
+ transcript: "line1\nline2\n",
+ observedAt: "2026-02-20T08:00:00.000Z",
+ confidence: 0.92,
+ }));
+
+ const snapshot = await provider.captureTerminalSnapshot();
+ const evidence = await buildTerminalEvidenceFromProvider(provider, {
+ useCanonicalHash: false,
+ });
+
+ expect(snapshot.session_id).toBe("s-1");
+ expect(snapshot.transcript_hash).toEqual(expect.any(String));
+ expect(evidence).toMatchObject({
+ source: "terminal",
+ schema_version: "terminal-v1",
+ confidence: 0.92,
+ });
+ expect(evidence.state_hash).toEqual(expect.any(String));
+ });
+
+ it("builds desktop accessibility state evidence", async () => {
+ const provider = new OpenClawDesktopAccessibilityEvidenceProvider(() => ({
+ appName: "Terminal",
+ windowTitle: "Deploy Prod",
+ focusedRole: "button",
+ focusedName: "Confirm",
+ uiTreeText: "root > dialog > button:Confirm",
+ observedAt: "2026-02-20T08:01:00.000Z",
+ confidence: 0.88,
+ }));
+
+ const snapshot = await provider.captureAccessibilitySnapshot();
+ const evidence = await buildDesktopEvidenceFromProvider(provider, {
+ useCanonicalHash: false,
+ });
+
+ expect(snapshot.ui_tree_hash).toEqual(expect.any(String));
+ expect(evidence).toMatchObject({
+ source: "desktop_accessibility",
+ schema_version: "desktop-a11y-v1",
+ confidence: 0.88,
+ });
+ expect(evidence.state_hash).toEqual(expect.any(String));
});
- expect(evidence.state_hash).toEqual(expect.any(String));
});
- it("builds desktop accessibility state evidence", async () => {
- const provider = new OpenClawDesktopAccessibilityEvidenceProvider(() => ({
- appName: "Terminal",
- windowTitle: "Deploy Prod",
- focusedRole: "button",
- focusedName: "Confirm",
- uiTreeText: "root > dialog > button:Confirm",
- observedAt: "2026-02-20T08:01:00.000Z",
- confidence: 0.88,
- }));
-
- const snapshot = await provider.captureAccessibilitySnapshot();
- const evidence = await buildDesktopEvidenceFromProvider(provider);
-
- expect(snapshot.ui_tree_hash).toEqual(expect.any(String));
- expect(evidence).toMatchObject({
- source: "desktop_accessibility",
- schema_version: "desktop-a11y-v1",
- confidence: 0.88,
+ describe("canonical mode (default)", () => {
+ it("builds terminal evidence with canonical schema version", async () => {
+ const provider = new OpenClawTerminalEvidenceProvider(() => ({
+ sessionId: "s-1",
+ terminalId: "t-1",
+ cwd: "/workspace",
+ command: "npm test",
+ transcript: "\x1b[32mPASS\x1b[0m all tests",
+ observedAt: "2026-02-20T08:00:00.000Z",
+ confidence: 0.95,
+ }));
+
+ const evidence = await buildTerminalEvidenceFromProvider(provider);
+
+ expect(evidence).toMatchObject({
+ source: "terminal",
+ schema_version: "terminal:v1.0",
+ confidence: 0.95,
+ });
+ expect(evidence.state_hash).toMatch(/^sha256:[a-f0-9]{64}$/);
+ });
+
+ it("builds desktop evidence with canonical schema version", async () => {
+ const provider = new OpenClawDesktopAccessibilityEvidenceProvider(() => ({
+ appName: "Firefox",
+ windowTitle: "GitHub",
+ focusedRole: "button",
+ focusedName: "Submit",
+ uiTreeText: "window > form > button:Submit",
+ observedAt: "2026-02-20T08:01:00.000Z",
+ confidence: 0.9,
+ }));
+
+ const evidence = await buildDesktopEvidenceFromProvider(provider);
+
+ expect(evidence).toMatchObject({
+ source: "desktop_accessibility",
+ schema_version: "desktop:v1.0",
+ confidence: 0.9,
+ });
+ expect(evidence.state_hash).toMatch(/^sha256:[a-f0-9]{64}$/);
+ });
+
+ it("produces stable hashes for equivalent terminal inputs", async () => {
+ // Same content with different whitespace/ANSI codes
+ const provider1 = new OpenClawTerminalEvidenceProvider(() => ({
+ sessionId: "s-1",
+ command: " npm test ",
+ transcript: "\x1b[32mOK\x1b[0m",
+ }));
+
+ const provider2 = new OpenClawTerminalEvidenceProvider(() => ({
+ sessionId: "s-1",
+ command: "npm test",
+ transcript: "OK",
+ }));
+
+ const evidence1 = await buildTerminalEvidenceFromProvider(provider1);
+ const evidence2 = await buildTerminalEvidenceFromProvider(provider2);
+
+ // Canonical hashing should produce identical hashes
+ expect(evidence1.state_hash).toBe(evidence2.state_hash);
+ });
+
+ it("produces stable hashes for equivalent desktop inputs", async () => {
+ // Same content with different whitespace
+ const provider1 = new OpenClawDesktopAccessibilityEvidenceProvider(() => ({
+ appName: " Firefox ",
+ windowTitle: " GitHub ",
+ focusedRole: "BUTTON",
+ focusedName: " Submit ",
+ }));
+
+ const provider2 = new OpenClawDesktopAccessibilityEvidenceProvider(() => ({
+ appName: "Firefox",
+ windowTitle: "GitHub",
+ focusedRole: "button",
+ focusedName: "Submit",
+ }));
+
+ const evidence1 = await buildDesktopEvidenceFromProvider(provider1);
+ const evidence2 = await buildDesktopEvidenceFromProvider(provider2);
+
+ // Canonical hashing should produce identical hashes
+ expect(evidence1.state_hash).toBe(evidence2.state_hash);
+ });
+
+ it("passes raw transcript for canonical hashing in SDK", async () => {
+ const provider = new OpenClawTerminalEvidenceProvider(() => ({
+ sessionId: "s-1",
+ transcript: "\x1b[31mERROR\x1b[0m: something went wrong",
+ }));
+
+ const snapshot = await provider.captureTerminalSnapshot();
+
+ // transcript_hash now contains raw text (hashing is done by SDK when useCanonicalHash=true)
+ expect(snapshot.transcript_hash).toEqual(expect.any(String));
+ // Raw transcript with ANSI codes is 36 chars
+ expect(snapshot.transcript_hash).toBe("\x1b[31mERROR\x1b[0m: something went wrong");
+
+ // Verify the final evidence hash is canonical (sha256-prefixed)
+ const evidence = await buildTerminalEvidenceFromProvider(provider);
+ expect(evidence.state_hash).toMatch(/^sha256:[a-f0-9]{64}$/);
});
- expect(evidence.state_hash).toEqual(expect.any(String));
});
});