predicate-claw 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (136) hide show
  1. package/.github/workflows/release.yml +76 -0
  2. package/.github/workflows/tests.yml +34 -0
  3. package/.markdownlint.yaml +5 -0
  4. package/.pre-commit-config.yaml +100 -0
  5. package/README.md +405 -0
  6. package/dist/src/adapter.d.ts +17 -0
  7. package/dist/src/adapter.js +36 -0
  8. package/dist/src/authority-client.d.ts +21 -0
  9. package/dist/src/authority-client.js +22 -0
  10. package/dist/src/circuit-breaker.d.ts +86 -0
  11. package/dist/src/circuit-breaker.js +174 -0
  12. package/dist/src/config.d.ts +8 -0
  13. package/dist/src/config.js +7 -0
  14. package/dist/src/control-plane-sync.d.ts +57 -0
  15. package/dist/src/control-plane-sync.js +99 -0
  16. package/dist/src/errors.d.ts +6 -0
  17. package/dist/src/errors.js +6 -0
  18. package/dist/src/index.d.ts +12 -0
  19. package/dist/src/index.js +12 -0
  20. package/dist/src/non-web-evidence.d.ts +46 -0
  21. package/dist/src/non-web-evidence.js +54 -0
  22. package/dist/src/openclaw-hooks.d.ts +27 -0
  23. package/dist/src/openclaw-hooks.js +54 -0
  24. package/dist/src/openclaw-plugin-api.d.ts +18 -0
  25. package/dist/src/openclaw-plugin-api.js +17 -0
  26. package/dist/src/provider.d.ts +48 -0
  27. package/dist/src/provider.js +154 -0
  28. package/dist/src/runtime-integration.d.ts +20 -0
  29. package/dist/src/runtime-integration.js +43 -0
  30. package/dist/src/web-evidence.d.ts +48 -0
  31. package/dist/src/web-evidence.js +49 -0
  32. package/dist/tests/adapter.test.d.ts +1 -0
  33. package/dist/tests/adapter.test.js +63 -0
  34. package/dist/tests/audit-event-e2e.test.d.ts +1 -0
  35. package/dist/tests/audit-event-e2e.test.js +209 -0
  36. package/dist/tests/authority-client.test.d.ts +1 -0
  37. package/dist/tests/authority-client.test.js +46 -0
  38. package/dist/tests/circuit-breaker.test.d.ts +1 -0
  39. package/dist/tests/circuit-breaker.test.js +200 -0
  40. package/dist/tests/control-plane-sync.test.d.ts +1 -0
  41. package/dist/tests/control-plane-sync.test.js +90 -0
  42. package/dist/tests/hack-vs-fix-demo.test.d.ts +1 -0
  43. package/dist/tests/hack-vs-fix-demo.test.js +36 -0
  44. package/dist/tests/jwks-rotation.test.d.ts +1 -0
  45. package/dist/tests/jwks-rotation.test.js +232 -0
  46. package/dist/tests/load-latency.test.d.ts +1 -0
  47. package/dist/tests/load-latency.test.js +175 -0
  48. package/dist/tests/multi-tenant-isolation.test.d.ts +1 -0
  49. package/dist/tests/multi-tenant-isolation.test.js +146 -0
  50. package/dist/tests/non-web-evidence.test.d.ts +1 -0
  51. package/dist/tests/non-web-evidence.test.js +139 -0
  52. package/dist/tests/openclaw-hooks.test.d.ts +1 -0
  53. package/dist/tests/openclaw-hooks.test.js +38 -0
  54. package/dist/tests/openclaw-plugin-api.test.d.ts +1 -0
  55. package/dist/tests/openclaw-plugin-api.test.js +40 -0
  56. package/dist/tests/provider.test.d.ts +1 -0
  57. package/dist/tests/provider.test.js +190 -0
  58. package/dist/tests/runtime-integration.test.d.ts +1 -0
  59. package/dist/tests/runtime-integration.test.js +57 -0
  60. package/dist/tests/web-evidence.test.d.ts +1 -0
  61. package/dist/tests/web-evidence.test.js +89 -0
  62. package/docs/MIGRATION_GUIDE.md +405 -0
  63. package/docs/OPERATIONAL_RUNBOOK.md +389 -0
  64. package/docs/PRODUCTION_READINESS.md +134 -0
  65. package/docs/SLO_THRESHOLDS.md +193 -0
  66. package/examples/README.md +171 -0
  67. package/examples/docker/Dockerfile.test +16 -0
  68. package/examples/docker/README.md +48 -0
  69. package/examples/docker/docker-compose.test.yml +16 -0
  70. package/examples/non-web-evidence-demo.ts +184 -0
  71. package/examples/openclaw-plugin-smoke/index.ts +30 -0
  72. package/examples/openclaw-plugin-smoke/openclaw.plugin.json +11 -0
  73. package/examples/openclaw-plugin-smoke/package.json +9 -0
  74. package/examples/openclaw_integration_example.py +41 -0
  75. package/examples/policy/README.md +165 -0
  76. package/examples/policy/approved-hosts.yaml +137 -0
  77. package/examples/policy/dev-workflow.yaml +206 -0
  78. package/examples/policy/policy.example.yaml +17 -0
  79. package/examples/policy/production-strict.yaml +97 -0
  80. package/examples/policy/sensitive-paths.yaml +114 -0
  81. package/examples/policy/source-trust.yaml +129 -0
  82. package/examples/policy/workspace-isolation.yaml +51 -0
  83. package/examples/runtime_registry_example.py +75 -0
  84. package/package.json +27 -0
  85. package/pyproject.toml +41 -0
  86. package/src/adapter.ts +45 -0
  87. package/src/authority-client.ts +50 -0
  88. package/src/circuit-breaker.ts +245 -0
  89. package/src/config.ts +15 -0
  90. package/src/control-plane-sync.ts +159 -0
  91. package/src/errors.ts +5 -0
  92. package/src/index.ts +12 -0
  93. package/src/non-web-evidence.ts +116 -0
  94. package/src/openclaw-hooks.ts +76 -0
  95. package/src/openclaw-plugin-api.ts +51 -0
  96. package/src/openclaw_predicate_provider/__init__.py +16 -0
  97. package/src/openclaw_predicate_provider/__main__.py +5 -0
  98. package/src/openclaw_predicate_provider/adapter.py +84 -0
  99. package/src/openclaw_predicate_provider/agentidentity_backend.py +78 -0
  100. package/src/openclaw_predicate_provider/cli.py +160 -0
  101. package/src/openclaw_predicate_provider/config.py +42 -0
  102. package/src/openclaw_predicate_provider/errors.py +13 -0
  103. package/src/openclaw_predicate_provider/integrations/__init__.py +5 -0
  104. package/src/openclaw_predicate_provider/integrations/openclaw_runtime.py +74 -0
  105. package/src/openclaw_predicate_provider/models.py +19 -0
  106. package/src/openclaw_predicate_provider/openclaw_hooks.py +75 -0
  107. package/src/openclaw_predicate_provider/provider.py +69 -0
  108. package/src/openclaw_predicate_provider/py.typed +1 -0
  109. package/src/openclaw_predicate_provider/sidecar.py +59 -0
  110. package/src/provider.ts +220 -0
  111. package/src/runtime-integration.ts +68 -0
  112. package/src/web-evidence.ts +95 -0
  113. package/tests/adapter.test.ts +76 -0
  114. package/tests/audit-event-e2e.test.ts +258 -0
  115. package/tests/authority-client.test.ts +52 -0
  116. package/tests/circuit-breaker.test.ts +266 -0
  117. package/tests/conftest.py +9 -0
  118. package/tests/control-plane-sync.test.ts +114 -0
  119. package/tests/hack-vs-fix-demo.test.ts +44 -0
  120. package/tests/jwks-rotation.test.ts +274 -0
  121. package/tests/load-latency.test.ts +214 -0
  122. package/tests/multi-tenant-isolation.test.ts +183 -0
  123. package/tests/non-web-evidence.test.ts +168 -0
  124. package/tests/openclaw-hooks.test.ts +46 -0
  125. package/tests/openclaw-plugin-api.test.ts +50 -0
  126. package/tests/provider.test.ts +227 -0
  127. package/tests/runtime-integration.test.ts +70 -0
  128. package/tests/test_adapter.py +46 -0
  129. package/tests/test_cli.py +26 -0
  130. package/tests/test_openclaw_hooks.py +53 -0
  131. package/tests/test_provider.py +59 -0
  132. package/tests/test_runtime_integration.py +77 -0
  133. package/tests/test_sidecar_client.py +198 -0
  134. package/tests/web-evidence.test.ts +113 -0
  135. package/tsconfig.json +14 -0
  136. package/vitest.config.ts +7 -0
@@ -0,0 +1,190 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { ActionDeniedError, GuardedProvider, SidecarUnavailableError, } from "../src/provider.js";
3
+ describe("GuardedProvider", () => {
4
+ it("builds deterministic intent hash", () => {
5
+ const first = GuardedProvider.intentHash({ cmd: "ls", flags: ["-la"] });
6
+ const second = GuardedProvider.intentHash({ flags: ["-la"], cmd: "ls" });
7
+ expect(first).toBe(second);
8
+ });
9
+ it("fails closed when sidecar is unavailable", async () => {
10
+ const provider = new GuardedProvider({
11
+ principal: "p1",
12
+ config: { failClosed: true },
13
+ authorityClient: {
14
+ authorize: async () => {
15
+ throw new SidecarUnavailableError("down");
16
+ },
17
+ },
18
+ });
19
+ await expect(provider.guardOrThrow({
20
+ action: "shell.execute",
21
+ resource: "echo hi",
22
+ args: { command: "echo hi" },
23
+ })).rejects.toBeInstanceOf(SidecarUnavailableError);
24
+ });
25
+ it("returns null in fail-open mode when sidecar is unavailable", async () => {
26
+ const provider = new GuardedProvider({
27
+ principal: "p1",
28
+ config: { failClosed: false },
29
+ authorityClient: {
30
+ authorize: async () => {
31
+ throw new SidecarUnavailableError("down");
32
+ },
33
+ },
34
+ });
35
+ await expect(provider.guardOrThrow({
36
+ action: "shell.execute",
37
+ resource: "echo hi",
38
+ args: { command: "echo hi" },
39
+ })).resolves.toBeNull();
40
+ });
41
+ it("emits decision telemetry for allow and deny", async () => {
42
+ const events = [];
43
+ const provider = new GuardedProvider({
44
+ principal: "p1",
45
+ telemetry: {
46
+ onDecision: (event) => {
47
+ events.push({ outcome: event.outcome, reason: event.reason });
48
+ },
49
+ },
50
+ authorityClient: {
51
+ authorize: async (request) => ({
52
+ allow: request.action !== "fs.read",
53
+ reason: request.action === "fs.read" ? "explicit_deny" : "allowed",
54
+ mandateId: request.action === "fs.read" ? undefined : "mnd_ok",
55
+ }),
56
+ },
57
+ });
58
+ await expect(provider.guardOrThrow({
59
+ action: "shell.execute",
60
+ resource: "echo hi",
61
+ args: { command: "echo hi" },
62
+ context: { source: "trusted_ui" },
63
+ })).resolves.toBe("mnd_ok");
64
+ await expect(provider.guardOrThrow({
65
+ action: "fs.read",
66
+ resource: "/etc/passwd",
67
+ args: { path: "/etc/passwd" },
68
+ context: { source: "untrusted_dm" },
69
+ })).rejects.toBeInstanceOf(ActionDeniedError);
70
+ expect(events).toEqual([
71
+ { outcome: "allow", reason: "allowed" },
72
+ { outcome: "deny", reason: "explicit_deny" },
73
+ ]);
74
+ });
75
+ it("sends canonical wire request fields to authority client", async () => {
76
+ let captured;
77
+ const provider = new GuardedProvider({
78
+ principal: "agent:openclaw-local",
79
+ authorityClient: {
80
+ authorize: async (request) => {
81
+ captured = request;
82
+ return { allow: true, reason: "allowed", mandateId: "mnd_ok" };
83
+ },
84
+ },
85
+ });
86
+ await provider.guardOrThrow({
87
+ action: "shell.execute",
88
+ resource: "echo hi",
89
+ args: { command: "echo hi", flags: ["-n"] },
90
+ context: { source: "trusted_ui", session_id: "s1" },
91
+ });
92
+ expect(captured).toBeDefined();
93
+ expect(captured?.principal).toBe("agent:openclaw-local");
94
+ expect(captured?.action).toBe("shell.execute");
95
+ expect(captured?.resource).toBe("echo hi");
96
+ expect(captured?.intent_hash).toEqual(expect.any(String));
97
+ expect(captured?.labels).toEqual(["source:trusted_ui"]);
98
+ });
99
+ it("exports decision audit events with correlation context", async () => {
100
+ const exported = [];
101
+ const provider = new GuardedProvider({
102
+ principal: "agent:openclaw-local",
103
+ auditExporter: {
104
+ exportDecision: async (event) => {
105
+ exported.push(event);
106
+ },
107
+ },
108
+ authorityClient: {
109
+ authorize: async () => ({
110
+ allow: true,
111
+ reason: "allowed",
112
+ mandateId: "mnd_123",
113
+ }),
114
+ },
115
+ });
116
+ await provider.guardOrThrow({
117
+ action: "shell.execute",
118
+ resource: "echo hi",
119
+ args: { command: "echo hi" },
120
+ context: {
121
+ source: "trusted_ui",
122
+ session_id: "session-1",
123
+ tenant_id: "tenant-1",
124
+ trace_id: "trace-1",
125
+ },
126
+ });
127
+ expect(exported).toHaveLength(1);
128
+ expect(exported[0]).toMatchObject({
129
+ principal: "agent:openclaw-local",
130
+ action: "shell.execute",
131
+ outcome: "allow",
132
+ mandateId: "mnd_123",
133
+ sessionId: "session-1",
134
+ tenantId: "tenant-1",
135
+ traceId: "trace-1",
136
+ });
137
+ });
138
+ it("does not block authorization if audit export fails", async () => {
139
+ const provider = new GuardedProvider({
140
+ principal: "agent:openclaw-local",
141
+ auditExporter: {
142
+ exportDecision: async () => {
143
+ throw new Error("audit export unavailable");
144
+ },
145
+ },
146
+ authorityClient: {
147
+ authorize: async () => ({
148
+ allow: true,
149
+ reason: "allowed",
150
+ mandateId: "mnd_456",
151
+ }),
152
+ },
153
+ });
154
+ await expect(provider.guardOrThrow({
155
+ action: "shell.execute",
156
+ resource: "echo ok",
157
+ args: { command: "echo ok" },
158
+ context: { source: "trusted_ui" },
159
+ })).resolves.toBe("mnd_456");
160
+ });
161
+ it("preserves deny reason and redacts sensitive resource in audit export", async () => {
162
+ const exported = [];
163
+ const provider = new GuardedProvider({
164
+ principal: "agent:openclaw-local",
165
+ auditExporter: {
166
+ exportDecision: async (event) => {
167
+ exported.push(event);
168
+ },
169
+ },
170
+ authorityClient: {
171
+ authorize: async () => ({
172
+ allow: false,
173
+ reason: "deny_sensitive_read_from_untrusted_context",
174
+ }),
175
+ },
176
+ });
177
+ await expect(provider.guardOrThrow({
178
+ action: "fs.read",
179
+ resource: "/Users/demo/.ssh/id_rsa",
180
+ args: { path: "/Users/demo/.ssh/id_rsa" },
181
+ context: { source: "untrusted_dm" },
182
+ })).rejects.toBeInstanceOf(ActionDeniedError);
183
+ expect(exported).toHaveLength(1);
184
+ expect(exported[0]).toMatchObject({
185
+ outcome: "deny",
186
+ reason: "deny_sensitive_read_from_untrusted_context",
187
+ resource: "[REDACTED]",
188
+ });
189
+ });
190
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,57 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { HookEnvelope } from "../src/openclaw-hooks.js";
3
+ import { OpenClawRuntimeIntegrator } from "../src/runtime-integration.js";
4
+ class Registry {
5
+ handlers = {
6
+ "cmd.run": async (args) => ({ tool: "cmd.run", args }),
7
+ "fs.readFile": async (args) => ({ tool: "fs.readFile", args }),
8
+ "http.request": async (args) => ({ tool: "http.request", args }),
9
+ };
10
+ get(toolName) {
11
+ return this.handlers[toolName];
12
+ }
13
+ set(toolName, handler) {
14
+ this.handlers[toolName] = handler;
15
+ }
16
+ invoke(toolName, args) {
17
+ return this.handlers[toolName](args);
18
+ }
19
+ }
20
+ describe("OpenClawRuntimeIntegrator", () => {
21
+ it("wraps sensitive handlers and routes through hooks", async () => {
22
+ const seen = [];
23
+ const registry = new Registry();
24
+ const hooks = {
25
+ onCmdRun: async (envelope, execute) => {
26
+ seen.push(`cmd:${envelope.toolName}`);
27
+ return execute(envelope.args);
28
+ },
29
+ onFsRead: async (envelope, execute) => {
30
+ seen.push(`fs:${envelope.toolName}`);
31
+ return execute(envelope.args);
32
+ },
33
+ onHttpRequest: async (envelope, execute) => {
34
+ seen.push(`http:${envelope.toolName}`);
35
+ return execute(envelope.args);
36
+ },
37
+ };
38
+ const integrator = new OpenClawRuntimeIntegrator({
39
+ hooks,
40
+ contextBuilder: (toolName, args) => new HookEnvelope({
41
+ toolName,
42
+ args,
43
+ sessionId: "s1",
44
+ source: "trusted_ui",
45
+ tenantId: "t1",
46
+ }),
47
+ });
48
+ integrator.register(registry);
49
+ const cmd = await registry.invoke("cmd.run", { command: "echo hi" });
50
+ const fs = await registry.invoke("fs.readFile", { path: "/tmp/demo" });
51
+ const http = await registry.invoke("http.request", { url: "https://example.com" });
52
+ expect(cmd).toMatchObject({ tool: "cmd.run" });
53
+ expect(fs).toMatchObject({ tool: "fs.readFile" });
54
+ expect(http).toMatchObject({ tool: "http.request" });
55
+ expect(seen).toEqual(["cmd:cmd.run", "fs:fs.readFile", "http:http.request"]);
56
+ });
57
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,89 @@
1
+ import crypto from "node:crypto";
2
+ import { describe, expect, it } from "vitest";
3
+ import { buildWebEvidenceFromProvider, buildWebEvidenceFromRuntimeSnapshot, OpenClawWebEvidenceProvider, } from "../src/web-evidence.js";
4
+ function sha256(input) {
5
+ return crypto.createHash("sha256").update(input).digest("hex");
6
+ }
7
+ describe("web evidence providers", () => {
8
+ it("builds web state evidence from OpenClaw runtime context", async () => {
9
+ const mockContext = {
10
+ url: "https://example.com/dashboard",
11
+ title: "Dashboard - Example App",
12
+ domHtml: "<html><body><h1>Dashboard</h1></body></html>",
13
+ visibleText: "Dashboard",
14
+ eventId: "evt-123",
15
+ observedAt: "2026-02-20T12:00:00Z",
16
+ dominantGroupKey: "main-content",
17
+ confidence: 0.95,
18
+ confidenceReasons: ["stable_dom", "no_pending_requests"],
19
+ };
20
+ const provider = new OpenClawWebEvidenceProvider(() => mockContext);
21
+ const evidence = await buildWebEvidenceFromProvider(provider);
22
+ expect(evidence.source).toBe("browser");
23
+ expect(evidence.schema_version).toBe("v1");
24
+ expect(evidence.state_hash).toBeDefined();
25
+ expect(typeof evidence.state_hash).toBe("string");
26
+ expect(evidence.confidence).toBe(0.95);
27
+ });
28
+ it("computes dom_hash when domHtml provided without domHash", async () => {
29
+ const domHtml = "<html><body>Test</body></html>";
30
+ const expectedHash = sha256(domHtml);
31
+ const provider = new OpenClawWebEvidenceProvider(() => ({
32
+ url: "https://example.com",
33
+ domHtml,
34
+ }));
35
+ const snapshot = await provider.captureWebSnapshot();
36
+ expect(snapshot.dom_hash).toBe(expectedHash);
37
+ });
38
+ it("computes visible_text_hash when visibleText provided without hash", async () => {
39
+ const visibleText = "Hello World";
40
+ const expectedHash = sha256(visibleText);
41
+ const provider = new OpenClawWebEvidenceProvider(() => ({
42
+ url: "https://example.com",
43
+ visibleText,
44
+ }));
45
+ const snapshot = await provider.captureWebSnapshot();
46
+ expect(snapshot.visible_text_hash).toBe(expectedHash);
47
+ });
48
+ it("uses provided hashes when available", async () => {
49
+ const precomputedDomHash = "abc123";
50
+ const precomputedTextHash = "def456";
51
+ const provider = new OpenClawWebEvidenceProvider(() => ({
52
+ url: "https://example.com",
53
+ domHash: precomputedDomHash,
54
+ visibleTextHash: precomputedTextHash,
55
+ }));
56
+ const snapshot = await provider.captureWebSnapshot();
57
+ expect(snapshot.dom_hash).toBe(precomputedDomHash);
58
+ expect(snapshot.visible_text_hash).toBe(precomputedTextHash);
59
+ });
60
+ it("builds evidence from predicate-runtime snapshot format", () => {
61
+ const runtimeSnapshot = {
62
+ url: "https://example.com/page",
63
+ timestamp: "2026-02-20T12:00:00Z",
64
+ dominant_group_key: "content-area",
65
+ diagnostics: {
66
+ confidence: 0.88,
67
+ reasons: ["dom_stable"],
68
+ },
69
+ };
70
+ const evidence = buildWebEvidenceFromRuntimeSnapshot(runtimeSnapshot);
71
+ expect(evidence.source).toBe("browser");
72
+ expect(evidence.schema_version).toBe("v1");
73
+ expect(evidence.confidence).toBe(0.88);
74
+ });
75
+ it("handles async capture functions", async () => {
76
+ const asyncCapture = async () => {
77
+ await new Promise((resolve) => setTimeout(resolve, 1));
78
+ return {
79
+ url: "https://async.example.com",
80
+ title: "Async Page",
81
+ };
82
+ };
83
+ const provider = new OpenClawWebEvidenceProvider(asyncCapture);
84
+ const snapshot = await provider.captureWebSnapshot();
85
+ expect(snapshot.url).toBe("https://async.example.com");
86
+ expect(snapshot.title).toBe("Async Page");
87
+ expect(snapshot.observed_at).toBeDefined();
88
+ });
89
+ });