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,159 @@
1
+ export interface ControlPlaneSyncConfig {
2
+ baseUrl: string;
3
+ tenantId: string;
4
+ timeoutMs?: number;
5
+ }
6
+
7
+ export interface PolicySyncSnapshot {
8
+ version: string;
9
+ cursor: string;
10
+ rules: unknown[];
11
+ }
12
+
13
+ export interface RevocationSyncSnapshot {
14
+ version: string;
15
+ cursor: string;
16
+ revoked: unknown[];
17
+ }
18
+
19
+ export interface ControlPlaneSyncStatus {
20
+ policyVersion: string;
21
+ revocationVersion: string;
22
+ pinnedPolicyVersion?: string;
23
+ policyVersionMismatch: boolean;
24
+ stale: boolean;
25
+ syncAgeMs: number;
26
+ lastSyncedAtMs: number;
27
+ }
28
+
29
+ export interface ControlPlaneSyncStatusTrackerOptions {
30
+ pinnedPolicyVersion?: string;
31
+ staleAfterMs?: number;
32
+ onStatus?: (status: ControlPlaneSyncStatus) => void;
33
+ }
34
+
35
+ export class ControlPlaneSyncClient {
36
+ private readonly baseUrl: string;
37
+ private readonly tenantId: string;
38
+ private readonly timeoutMs: number;
39
+
40
+ constructor(config: ControlPlaneSyncConfig) {
41
+ this.baseUrl = config.baseUrl.replace(/\/+$/, "");
42
+ this.tenantId = config.tenantId;
43
+ this.timeoutMs = config.timeoutMs ?? 3000;
44
+ }
45
+
46
+ async pullPolicySnapshot(
47
+ cursor?: string,
48
+ ): Promise<PolicySyncSnapshot> {
49
+ const url = new URL(`${this.baseUrl}/v1/policy/sync`);
50
+ url.searchParams.set("tenant_id", this.tenantId);
51
+ if (cursor) {
52
+ url.searchParams.set("cursor", cursor);
53
+ }
54
+ const response = await this.fetchWithTimeout(url.toString());
55
+ if (!response.ok) {
56
+ throw new Error(`policy sync failed: ${response.status}`);
57
+ }
58
+ return (await response.json()) as PolicySyncSnapshot;
59
+ }
60
+
61
+ async pullRevocationSnapshot(
62
+ cursor?: string,
63
+ ): Promise<RevocationSyncSnapshot> {
64
+ const url = new URL(`${this.baseUrl}/v1/revocations/sync`);
65
+ url.searchParams.set("tenant_id", this.tenantId);
66
+ if (cursor) {
67
+ url.searchParams.set("cursor", cursor);
68
+ }
69
+ const response = await this.fetchWithTimeout(url.toString());
70
+ if (!response.ok) {
71
+ throw new Error(`revocation sync failed: ${response.status}`);
72
+ }
73
+ return (await response.json()) as RevocationSyncSnapshot;
74
+ }
75
+
76
+ private async fetchWithTimeout(input: string): Promise<Response> {
77
+ const controller = new AbortController();
78
+ const timer = setTimeout(() => controller.abort(), this.timeoutMs);
79
+ try {
80
+ return await fetch(input, { signal: controller.signal });
81
+ } finally {
82
+ clearTimeout(timer);
83
+ }
84
+ }
85
+ }
86
+
87
+ export class ControlPlaneSyncStatusTracker {
88
+ private readonly pinnedPolicyVersion?: string;
89
+ private readonly staleAfterMs: number;
90
+ private readonly onStatus?: (status: ControlPlaneSyncStatus) => void;
91
+ private latest?: {
92
+ policyVersion: string;
93
+ revocationVersion: string;
94
+ lastSyncedAtMs: number;
95
+ };
96
+
97
+ constructor(options?: ControlPlaneSyncStatusTrackerOptions) {
98
+ this.pinnedPolicyVersion = options?.pinnedPolicyVersion;
99
+ this.staleAfterMs = options?.staleAfterMs ?? 300000;
100
+ this.onStatus = options?.onStatus;
101
+ }
102
+
103
+ recordSync(
104
+ snapshot: { policy: PolicySyncSnapshot; revocations: RevocationSyncSnapshot },
105
+ nowMs: number = Date.now(),
106
+ ): ControlPlaneSyncStatus {
107
+ this.latest = {
108
+ policyVersion: snapshot.policy.version,
109
+ revocationVersion: snapshot.revocations.version,
110
+ lastSyncedAtMs: nowMs,
111
+ };
112
+ const status = this.snapshot(nowMs);
113
+ this.onStatus?.(status);
114
+ return status;
115
+ }
116
+
117
+ snapshot(nowMs: number = Date.now()): ControlPlaneSyncStatus {
118
+ if (!this.latest) {
119
+ return {
120
+ policyVersion: "unknown",
121
+ revocationVersion: "unknown",
122
+ pinnedPolicyVersion: this.pinnedPolicyVersion,
123
+ policyVersionMismatch: false,
124
+ stale: true,
125
+ syncAgeMs: Number.POSITIVE_INFINITY,
126
+ lastSyncedAtMs: 0,
127
+ };
128
+ }
129
+ const syncAgeMs = Math.max(0, nowMs - this.latest.lastSyncedAtMs);
130
+ const policyVersionMismatch =
131
+ typeof this.pinnedPolicyVersion === "string" &&
132
+ this.pinnedPolicyVersion.length > 0
133
+ ? this.latest.policyVersion !== this.pinnedPolicyVersion
134
+ : false;
135
+ return {
136
+ policyVersion: this.latest.policyVersion,
137
+ revocationVersion: this.latest.revocationVersion,
138
+ pinnedPolicyVersion: this.pinnedPolicyVersion,
139
+ policyVersionMismatch,
140
+ stale: syncAgeMs > this.staleAfterMs,
141
+ syncAgeMs,
142
+ lastSyncedAtMs: this.latest.lastSyncedAtMs,
143
+ };
144
+ }
145
+ }
146
+
147
+ export async function syncControlPlaneState(
148
+ client: ControlPlaneSyncClient,
149
+ cursors?: { policyCursor?: string; revocationCursor?: string },
150
+ ): Promise<{
151
+ policy: PolicySyncSnapshot;
152
+ revocations: RevocationSyncSnapshot;
153
+ }> {
154
+ const [policy, revocations] = await Promise.all([
155
+ client.pullPolicySnapshot(cursors?.policyCursor),
156
+ client.pullRevocationSnapshot(cursors?.revocationCursor),
157
+ ]);
158
+ return { policy, revocations };
159
+ }
package/src/errors.ts ADDED
@@ -0,0 +1,5 @@
1
+ export class GuardError extends Error {}
2
+
3
+ export class ActionDeniedError extends GuardError {}
4
+
5
+ export class SidecarUnavailableError extends GuardError {}
package/src/index.ts ADDED
@@ -0,0 +1,12 @@
1
+ export * from "./adapter.js";
2
+ export * from "./authority-client.js";
3
+ export * from "./circuit-breaker.js";
4
+ export * from "./config.js";
5
+ export * from "./control-plane-sync.js";
6
+ export * from "./errors.js";
7
+ export * from "./non-web-evidence.js";
8
+ export * from "./web-evidence.js";
9
+ export * from "./openclaw-hooks.js";
10
+ export * from "./openclaw-plugin-api.js";
11
+ export * from "./provider.js";
12
+ export * from "./runtime-integration.js";
@@ -0,0 +1,116 @@
1
+ import {
2
+ type DesktopAccessibilityEvidenceProvider,
3
+ type DesktopAccessibilitySnapshot,
4
+ type StateEvidence,
5
+ type TerminalEvidenceProvider,
6
+ type TerminalSessionSnapshot,
7
+ buildDesktopAccessibilityStateEvidence,
8
+ buildTerminalStateEvidence,
9
+ } from "@predicatesystems/authority";
10
+
11
+ export interface TerminalRuntimeContext {
12
+ sessionId: string;
13
+ terminalId?: string;
14
+ cwd?: string;
15
+ command?: string;
16
+ /** Raw transcript text (will be canonicalized before hashing) */
17
+ transcript?: string;
18
+ observedAt?: string;
19
+ confidence?: number;
20
+ /** Environment variables (secrets will be redacted before hashing) */
21
+ env?: Record<string, string>;
22
+ }
23
+
24
+ export interface DesktopRuntimeContext {
25
+ appName?: string;
26
+ windowTitle?: string;
27
+ focusedRole?: string;
28
+ focusedName?: string;
29
+ /** Raw UI tree text (will be normalized before hashing) */
30
+ uiTreeText?: string;
31
+ /** Pre-computed UI tree hash (bypasses canonicalization) */
32
+ uiTreeHash?: string;
33
+ observedAt?: string;
34
+ confidence?: number;
35
+ }
36
+
37
+ export class OpenClawTerminalEvidenceProvider
38
+ implements TerminalEvidenceProvider
39
+ {
40
+ constructor(
41
+ private readonly capture: () =>
42
+ | Promise<TerminalRuntimeContext>
43
+ | TerminalRuntimeContext,
44
+ ) {}
45
+
46
+ async captureTerminalSnapshot(): Promise<TerminalSessionSnapshot> {
47
+ const runtime = await this.capture();
48
+ return {
49
+ session_id: runtime.sessionId,
50
+ terminal_id: runtime.terminalId,
51
+ cwd: runtime.cwd,
52
+ command: runtime.command,
53
+ // Pass raw transcript - canonicalization happens in buildTerminalStateEvidence
54
+ // when useCanonicalHash is enabled. The field is named transcript_hash for
55
+ // backward compatibility but carries raw text when canonical hashing is used.
56
+ transcript_hash: runtime.transcript ?? "",
57
+ observed_at: runtime.observedAt ?? new Date().toISOString(),
58
+ confidence: runtime.confidence,
59
+ };
60
+ }
61
+ }
62
+
63
+ export class OpenClawDesktopAccessibilityEvidenceProvider
64
+ implements DesktopAccessibilityEvidenceProvider
65
+ {
66
+ constructor(
67
+ private readonly capture: () =>
68
+ | Promise<DesktopRuntimeContext>
69
+ | DesktopRuntimeContext,
70
+ ) {}
71
+
72
+ async captureAccessibilitySnapshot(): Promise<DesktopAccessibilitySnapshot> {
73
+ const runtime = await this.capture();
74
+ // Pass raw UI tree text - canonicalization happens in buildDesktopAccessibilityStateEvidence
75
+ // when useCanonicalHash is enabled. Use pre-computed hash if available for legacy mode.
76
+ // The field is named ui_tree_hash for backward compatibility but carries raw text
77
+ // when canonical hashing is used.
78
+ return {
79
+ app_name: runtime.appName,
80
+ window_title: runtime.windowTitle,
81
+ focused_role: runtime.focusedRole,
82
+ focused_name: runtime.focusedName,
83
+ ui_tree_hash: runtime.uiTreeHash ?? runtime.uiTreeText ?? "",
84
+ observed_at: runtime.observedAt ?? new Date().toISOString(),
85
+ confidence: runtime.confidence,
86
+ };
87
+ }
88
+ }
89
+
90
+ export interface BuildEvidenceOptions {
91
+ /**
92
+ * Use canonical hashing with proper normalization.
93
+ * When true, applies ANSI stripping, timestamp normalization, whitespace
94
+ * collapsing, and other canonicalization rules for reproducible hashes.
95
+ * @default true
96
+ */
97
+ useCanonicalHash?: boolean;
98
+ }
99
+
100
+ export async function buildTerminalEvidenceFromProvider(
101
+ provider: TerminalEvidenceProvider,
102
+ options: BuildEvidenceOptions = {},
103
+ ): Promise<StateEvidence> {
104
+ const snapshot = await provider.captureTerminalSnapshot();
105
+ const useCanonicalHash = options.useCanonicalHash ?? true;
106
+ return buildTerminalStateEvidence({ snapshot, useCanonicalHash });
107
+ }
108
+
109
+ export async function buildDesktopEvidenceFromProvider(
110
+ provider: DesktopAccessibilityEvidenceProvider,
111
+ options: BuildEvidenceOptions = {},
112
+ ): Promise<StateEvidence> {
113
+ const snapshot = await provider.captureAccessibilitySnapshot();
114
+ const useCanonicalHash = options.useCanonicalHash ?? true;
115
+ return buildDesktopAccessibilityStateEvidence({ snapshot, useCanonicalHash });
116
+ }
@@ -0,0 +1,76 @@
1
+ import type { ToolAdapter } from "./adapter.js";
2
+
3
+ export class HookEnvelope {
4
+ readonly toolName: string;
5
+ readonly args: Record<string, unknown>;
6
+ readonly sessionId: string;
7
+ readonly source: string;
8
+ readonly tenantId?: string;
9
+ readonly userId?: string;
10
+ readonly traceId?: string;
11
+
12
+ constructor(params: {
13
+ toolName: string;
14
+ args: Record<string, unknown>;
15
+ sessionId: string;
16
+ source: string;
17
+ tenantId?: string;
18
+ userId?: string;
19
+ traceId?: string;
20
+ }) {
21
+ this.toolName = params.toolName;
22
+ this.args = params.args;
23
+ this.sessionId = params.sessionId;
24
+ this.source = params.source;
25
+ this.tenantId = params.tenantId;
26
+ this.userId = params.userId;
27
+ this.traceId = params.traceId;
28
+ }
29
+
30
+ context(): Record<string, unknown> {
31
+ return {
32
+ session_id: this.sessionId,
33
+ source: this.source,
34
+ tenant_id: this.tenantId,
35
+ user_id: this.userId,
36
+ trace_id: this.traceId,
37
+ };
38
+ }
39
+ }
40
+
41
+ export class OpenClawHooks {
42
+ constructor(private readonly adapter: Pick<ToolAdapter, "runShell" | "readFile" | "httpRequest">) {}
43
+
44
+ onCmdRun(
45
+ envelope: HookEnvelope,
46
+ execute: (args: Record<string, unknown>) => Promise<unknown>,
47
+ ) {
48
+ return this.adapter.runShell({
49
+ args: envelope.args,
50
+ context: envelope.context(),
51
+ execute,
52
+ });
53
+ }
54
+
55
+ onFsRead(
56
+ envelope: HookEnvelope,
57
+ execute: (args: Record<string, unknown>) => Promise<unknown>,
58
+ ) {
59
+ return this.adapter.readFile({
60
+ args: envelope.args,
61
+ context: envelope.context(),
62
+ execute,
63
+ });
64
+ }
65
+
66
+ onHttpRequest(
67
+ envelope: HookEnvelope,
68
+ execute: (args: Record<string, unknown>) => Promise<unknown>,
69
+ ) {
70
+ return this.adapter.httpRequest({
71
+ args: envelope.args,
72
+ context: envelope.context(),
73
+ execute,
74
+ });
75
+ }
76
+ }
@@ -0,0 +1,51 @@
1
+ interface RegisteredTool {
2
+ name: string;
3
+ description?: string;
4
+ parameters?: unknown;
5
+ execute: (id: string, params: Record<string, unknown>) => Promise<unknown>;
6
+ }
7
+
8
+ interface OpenClawPluginApi {
9
+ registerTool(tool: RegisteredTool, options?: { optional?: boolean }): void;
10
+ }
11
+
12
+ interface PredicateToolHandlers {
13
+ executeCmdRun(args: Record<string, unknown>): Promise<unknown>;
14
+ executeFsReadFile(args: Record<string, unknown>): Promise<unknown>;
15
+ executeHttpRequest(args: Record<string, unknown>): Promise<unknown>;
16
+ }
17
+
18
+ export function registerOpenClawPredicateTools(
19
+ api: OpenClawPluginApi,
20
+ handlers: PredicateToolHandlers,
21
+ ): void {
22
+ api.registerTool(
23
+ {
24
+ name: "predicate_cmd_run",
25
+ description: "Guarded shell execution routed through Predicate Authority.",
26
+ execute: async (_id: string, params: Record<string, unknown>) =>
27
+ handlers.executeCmdRun(params),
28
+ },
29
+ { optional: true },
30
+ );
31
+
32
+ api.registerTool(
33
+ {
34
+ name: "predicate_fs_read_file",
35
+ description: "Guarded file read routed through Predicate Authority.",
36
+ execute: async (_id: string, params: Record<string, unknown>) =>
37
+ handlers.executeFsReadFile(params),
38
+ },
39
+ { optional: true },
40
+ );
41
+
42
+ api.registerTool(
43
+ {
44
+ name: "predicate_http_request",
45
+ description: "Guarded HTTP request routed through Predicate Authority.",
46
+ execute: async (_id: string, params: Record<string, unknown>) =>
47
+ handlers.executeHttpRequest(params),
48
+ },
49
+ { optional: true },
50
+ );
51
+ }
@@ -0,0 +1,16 @@
1
+ """OpenClaw Predicate provider package."""
2
+
3
+ from .adapter import ToolAdapter
4
+ from .config import ProviderConfig
5
+ from .integrations import OpenClawRuntimeIntegrator
6
+ from .openclaw_hooks import HookEnvelope, OpenClawHooks
7
+ from .provider import GuardedProvider
8
+
9
+ __all__ = [
10
+ "ProviderConfig",
11
+ "GuardedProvider",
12
+ "ToolAdapter",
13
+ "OpenClawRuntimeIntegrator",
14
+ "HookEnvelope",
15
+ "OpenClawHooks",
16
+ ]
@@ -0,0 +1,5 @@
1
+ from .cli import main
2
+
3
+
4
+ if __name__ == "__main__":
5
+ raise SystemExit(main())
@@ -0,0 +1,84 @@
1
+ """OpenClaw adapter scaffolding for guarded tool execution."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any, Awaitable, Callable
6
+
7
+ from .provider import GuardedProvider
8
+
9
+ ToolCallable = Callable[[dict[str, Any]], Awaitable[Any]]
10
+
11
+
12
+ class ToolAdapter:
13
+ """Wrap tool handlers with Predicate guard checks."""
14
+
15
+ def __init__(self, guard: GuardedProvider):
16
+ self._guard = guard
17
+
18
+ async def run(
19
+ self,
20
+ *,
21
+ action: str,
22
+ resource: str,
23
+ args: dict[str, Any],
24
+ context: dict[str, Any],
25
+ execute: ToolCallable,
26
+ ) -> Any:
27
+ await self._guard.guard_or_raise(
28
+ action=action,
29
+ resource=resource,
30
+ args=args,
31
+ context=context,
32
+ )
33
+ return await execute(args)
34
+
35
+ async def run_shell(
36
+ self,
37
+ *,
38
+ args: dict[str, Any],
39
+ context: dict[str, Any],
40
+ execute: ToolCallable,
41
+ ) -> Any:
42
+ """Guard OpenClaw command execution."""
43
+ resource = str(args.get("command", ""))
44
+ return await self.run(
45
+ action="shell.execute",
46
+ resource=resource,
47
+ args=args,
48
+ context=context,
49
+ execute=execute,
50
+ )
51
+
52
+ async def read_file(
53
+ self,
54
+ *,
55
+ args: dict[str, Any],
56
+ context: dict[str, Any],
57
+ execute: ToolCallable,
58
+ ) -> Any:
59
+ """Guard OpenClaw file-read execution."""
60
+ resource = str(args.get("path", ""))
61
+ return await self.run(
62
+ action="fs.read",
63
+ resource=resource,
64
+ args=args,
65
+ context=context,
66
+ execute=execute,
67
+ )
68
+
69
+ async def http_request(
70
+ self,
71
+ *,
72
+ args: dict[str, Any],
73
+ context: dict[str, Any],
74
+ execute: ToolCallable,
75
+ ) -> Any:
76
+ """Guard OpenClaw outbound HTTP execution."""
77
+ resource = str(args.get("url", ""))
78
+ return await self.run(
79
+ action="net.http",
80
+ resource=resource,
81
+ args=args,
82
+ context=context,
83
+ execute=execute,
84
+ )
@@ -0,0 +1,78 @@
1
+ """Optional AgentIdentity SDK-backed authorization backend."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING, Any
6
+
7
+ from .config import ProviderConfig
8
+ from .errors import SidecarUnavailableError
9
+ from .models import AuthorizationDecision, AuthorizationRequest
10
+
11
+ if TYPE_CHECKING:
12
+ from predicate_authority.client import AuthorityClient
13
+
14
+
15
+ class AgentIdentityLocalClient:
16
+ """Adapter that uses AgentIdentity local SDK authorization primitives."""
17
+
18
+ def __init__(self, config: ProviderConfig):
19
+ self._client = self._build_client(config)
20
+
21
+ @staticmethod
22
+ def _build_client(config: ProviderConfig) -> "AuthorityClient":
23
+ try:
24
+ from predicate_authority.client import AuthorityClient
25
+ except ImportError as exc: # pragma: no cover - environment-dependent
26
+ raise SidecarUnavailableError(
27
+ "AgentIdentity SDK unavailable. Install predicate-authority package."
28
+ ) from exc
29
+
30
+ if config.agentidentity_policy_file and config.agentidentity_signing_key:
31
+ local_ctx = AuthorityClient.from_policy_file(
32
+ policy_file=config.agentidentity_policy_file,
33
+ secret_key=config.agentidentity_signing_key,
34
+ ttl_seconds=config.agentidentity_ttl_seconds,
35
+ )
36
+ return local_ctx.client
37
+ local_ctx = AuthorityClient.from_env()
38
+ return local_ctx.client
39
+
40
+ def authorize(self, request: AuthorizationRequest) -> AuthorizationDecision:
41
+ try:
42
+ from predicate_authority.integrations.sdk_python import (
43
+ SdkStepEvidence,
44
+ to_action_request,
45
+ )
46
+ except ImportError as exc: # pragma: no cover - environment-dependent
47
+ raise SidecarUnavailableError(
48
+ "AgentIdentity sdk_python integration module unavailable."
49
+ ) from exc
50
+
51
+ state_hash = str(request.context.get("state_hash", request.intent_hash))
52
+ step = SdkStepEvidence(
53
+ principal_id=request.principal,
54
+ action=request.action,
55
+ resource=request.resource,
56
+ intent=request.intent_hash,
57
+ state_hash=state_hash,
58
+ tenant_id=_string_or_none(request.context.get("tenant_id")),
59
+ session_id=_string_or_none(request.context.get("session_id")),
60
+ )
61
+ decision = self._client.authorize(to_action_request(step))
62
+ mandate_id = decision.mandate.claims.mandate_id if decision.mandate else None
63
+ reason = (
64
+ decision.reason.value
65
+ if hasattr(decision.reason, "value")
66
+ else str(decision.reason)
67
+ )
68
+ return AuthorizationDecision(
69
+ allow=decision.allowed,
70
+ reason=reason,
71
+ mandate_id=mandate_id,
72
+ )
73
+
74
+
75
+ def _string_or_none(value: Any) -> str | None:
76
+ if isinstance(value, str) and value.strip():
77
+ return value
78
+ return None