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.
- package/.github/workflows/release.yml +76 -0
- package/.github/workflows/tests.yml +34 -0
- package/.markdownlint.yaml +5 -0
- package/.pre-commit-config.yaml +100 -0
- package/README.md +405 -0
- package/dist/src/adapter.d.ts +17 -0
- package/dist/src/adapter.js +36 -0
- package/dist/src/authority-client.d.ts +21 -0
- package/dist/src/authority-client.js +22 -0
- package/dist/src/circuit-breaker.d.ts +86 -0
- package/dist/src/circuit-breaker.js +174 -0
- package/dist/src/config.d.ts +8 -0
- package/dist/src/config.js +7 -0
- package/dist/src/control-plane-sync.d.ts +57 -0
- package/dist/src/control-plane-sync.js +99 -0
- package/dist/src/errors.d.ts +6 -0
- package/dist/src/errors.js +6 -0
- package/dist/src/index.d.ts +12 -0
- package/dist/src/index.js +12 -0
- package/dist/src/non-web-evidence.d.ts +46 -0
- package/dist/src/non-web-evidence.js +54 -0
- package/dist/src/openclaw-hooks.d.ts +27 -0
- package/dist/src/openclaw-hooks.js +54 -0
- package/dist/src/openclaw-plugin-api.d.ts +18 -0
- package/dist/src/openclaw-plugin-api.js +17 -0
- package/dist/src/provider.d.ts +48 -0
- package/dist/src/provider.js +154 -0
- package/dist/src/runtime-integration.d.ts +20 -0
- package/dist/src/runtime-integration.js +43 -0
- package/dist/src/web-evidence.d.ts +48 -0
- package/dist/src/web-evidence.js +49 -0
- package/dist/tests/adapter.test.d.ts +1 -0
- package/dist/tests/adapter.test.js +63 -0
- package/dist/tests/audit-event-e2e.test.d.ts +1 -0
- package/dist/tests/audit-event-e2e.test.js +209 -0
- package/dist/tests/authority-client.test.d.ts +1 -0
- package/dist/tests/authority-client.test.js +46 -0
- package/dist/tests/circuit-breaker.test.d.ts +1 -0
- package/dist/tests/circuit-breaker.test.js +200 -0
- package/dist/tests/control-plane-sync.test.d.ts +1 -0
- package/dist/tests/control-plane-sync.test.js +90 -0
- package/dist/tests/hack-vs-fix-demo.test.d.ts +1 -0
- package/dist/tests/hack-vs-fix-demo.test.js +36 -0
- package/dist/tests/jwks-rotation.test.d.ts +1 -0
- package/dist/tests/jwks-rotation.test.js +232 -0
- package/dist/tests/load-latency.test.d.ts +1 -0
- package/dist/tests/load-latency.test.js +175 -0
- package/dist/tests/multi-tenant-isolation.test.d.ts +1 -0
- package/dist/tests/multi-tenant-isolation.test.js +146 -0
- package/dist/tests/non-web-evidence.test.d.ts +1 -0
- package/dist/tests/non-web-evidence.test.js +139 -0
- package/dist/tests/openclaw-hooks.test.d.ts +1 -0
- package/dist/tests/openclaw-hooks.test.js +38 -0
- package/dist/tests/openclaw-plugin-api.test.d.ts +1 -0
- package/dist/tests/openclaw-plugin-api.test.js +40 -0
- package/dist/tests/provider.test.d.ts +1 -0
- package/dist/tests/provider.test.js +190 -0
- package/dist/tests/runtime-integration.test.d.ts +1 -0
- package/dist/tests/runtime-integration.test.js +57 -0
- package/dist/tests/web-evidence.test.d.ts +1 -0
- package/dist/tests/web-evidence.test.js +89 -0
- package/docs/MIGRATION_GUIDE.md +405 -0
- package/docs/OPERATIONAL_RUNBOOK.md +389 -0
- package/docs/PRODUCTION_READINESS.md +134 -0
- package/docs/SLO_THRESHOLDS.md +193 -0
- package/examples/README.md +171 -0
- package/examples/docker/Dockerfile.test +16 -0
- package/examples/docker/README.md +48 -0
- package/examples/docker/docker-compose.test.yml +16 -0
- package/examples/non-web-evidence-demo.ts +184 -0
- package/examples/openclaw-plugin-smoke/index.ts +30 -0
- package/examples/openclaw-plugin-smoke/openclaw.plugin.json +11 -0
- package/examples/openclaw-plugin-smoke/package.json +9 -0
- package/examples/openclaw_integration_example.py +41 -0
- package/examples/policy/README.md +165 -0
- package/examples/policy/approved-hosts.yaml +137 -0
- package/examples/policy/dev-workflow.yaml +206 -0
- package/examples/policy/policy.example.yaml +17 -0
- package/examples/policy/production-strict.yaml +97 -0
- package/examples/policy/sensitive-paths.yaml +114 -0
- package/examples/policy/source-trust.yaml +129 -0
- package/examples/policy/workspace-isolation.yaml +51 -0
- package/examples/runtime_registry_example.py +75 -0
- package/package.json +27 -0
- package/pyproject.toml +41 -0
- package/src/adapter.ts +45 -0
- package/src/authority-client.ts +50 -0
- package/src/circuit-breaker.ts +245 -0
- package/src/config.ts +15 -0
- package/src/control-plane-sync.ts +159 -0
- package/src/errors.ts +5 -0
- package/src/index.ts +12 -0
- package/src/non-web-evidence.ts +116 -0
- package/src/openclaw-hooks.ts +76 -0
- package/src/openclaw-plugin-api.ts +51 -0
- package/src/openclaw_predicate_provider/__init__.py +16 -0
- package/src/openclaw_predicate_provider/__main__.py +5 -0
- package/src/openclaw_predicate_provider/adapter.py +84 -0
- package/src/openclaw_predicate_provider/agentidentity_backend.py +78 -0
- package/src/openclaw_predicate_provider/cli.py +160 -0
- package/src/openclaw_predicate_provider/config.py +42 -0
- package/src/openclaw_predicate_provider/errors.py +13 -0
- package/src/openclaw_predicate_provider/integrations/__init__.py +5 -0
- package/src/openclaw_predicate_provider/integrations/openclaw_runtime.py +74 -0
- package/src/openclaw_predicate_provider/models.py +19 -0
- package/src/openclaw_predicate_provider/openclaw_hooks.py +75 -0
- package/src/openclaw_predicate_provider/provider.py +69 -0
- package/src/openclaw_predicate_provider/py.typed +1 -0
- package/src/openclaw_predicate_provider/sidecar.py +59 -0
- package/src/provider.ts +220 -0
- package/src/runtime-integration.ts +68 -0
- package/src/web-evidence.ts +95 -0
- package/tests/adapter.test.ts +76 -0
- package/tests/audit-event-e2e.test.ts +258 -0
- package/tests/authority-client.test.ts +52 -0
- package/tests/circuit-breaker.test.ts +266 -0
- package/tests/conftest.py +9 -0
- package/tests/control-plane-sync.test.ts +114 -0
- package/tests/hack-vs-fix-demo.test.ts +44 -0
- package/tests/jwks-rotation.test.ts +274 -0
- package/tests/load-latency.test.ts +214 -0
- package/tests/multi-tenant-isolation.test.ts +183 -0
- package/tests/non-web-evidence.test.ts +168 -0
- package/tests/openclaw-hooks.test.ts +46 -0
- package/tests/openclaw-plugin-api.test.ts +50 -0
- package/tests/provider.test.ts +227 -0
- package/tests/runtime-integration.test.ts +70 -0
- package/tests/test_adapter.py +46 -0
- package/tests/test_cli.py +26 -0
- package/tests/test_openclaw_hooks.py +53 -0
- package/tests/test_provider.py +59 -0
- package/tests/test_runtime_integration.py +77 -0
- package/tests/test_sidecar_client.py +198 -0
- package/tests/web-evidence.test.ts +113 -0
- package/tsconfig.json +14 -0
- 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
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,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
|