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,22 @@
|
|
|
1
|
+
import { AuthorityClient, } from "@predicatesystems/authority";
|
|
2
|
+
export function createAuthorityAdapter(client) {
|
|
3
|
+
return {
|
|
4
|
+
async authorize(request) {
|
|
5
|
+
const decision = await client.authorize(request);
|
|
6
|
+
return {
|
|
7
|
+
allow: decision.allowed,
|
|
8
|
+
reason: decision.reason,
|
|
9
|
+
mandateId: decision.mandate_id ?? undefined,
|
|
10
|
+
};
|
|
11
|
+
},
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
export function createDefaultAuthorityAdapter(config) {
|
|
15
|
+
const sdkClient = new AuthorityClient({
|
|
16
|
+
baseUrl: config.baseUrl,
|
|
17
|
+
timeoutMs: config.timeoutMs,
|
|
18
|
+
maxRetries: config.maxRetries,
|
|
19
|
+
backoffInitialMs: config.backoffInitialMs,
|
|
20
|
+
});
|
|
21
|
+
return createAuthorityAdapter(sdkClient);
|
|
22
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Circuit breaker for sidecar outage resilience.
|
|
3
|
+
*
|
|
4
|
+
* States:
|
|
5
|
+
* - CLOSED: Normal operation, requests pass through
|
|
6
|
+
* - OPEN: Too many failures, requests fail fast without calling sidecar
|
|
7
|
+
* - HALF_OPEN: Testing if sidecar has recovered
|
|
8
|
+
*/
|
|
9
|
+
export type CircuitState = "closed" | "open" | "half_open";
|
|
10
|
+
export interface CircuitBreakerConfig {
|
|
11
|
+
/** Number of failures before opening the circuit */
|
|
12
|
+
failureThreshold: number;
|
|
13
|
+
/** Time in ms before attempting recovery (half-open state) */
|
|
14
|
+
resetTimeoutMs: number;
|
|
15
|
+
/** Number of successful calls in half-open to close circuit */
|
|
16
|
+
successThreshold: number;
|
|
17
|
+
/** Optional callback when state changes */
|
|
18
|
+
onStateChange?: (from: CircuitState, to: CircuitState) => void;
|
|
19
|
+
}
|
|
20
|
+
export declare const defaultCircuitBreakerConfig: CircuitBreakerConfig;
|
|
21
|
+
export interface CircuitBreakerMetrics {
|
|
22
|
+
state: CircuitState;
|
|
23
|
+
failureCount: number;
|
|
24
|
+
successCount: number;
|
|
25
|
+
lastFailureTime: number | null;
|
|
26
|
+
totalFailures: number;
|
|
27
|
+
totalSuccesses: number;
|
|
28
|
+
totalRejections: number;
|
|
29
|
+
}
|
|
30
|
+
export declare class CircuitBreaker {
|
|
31
|
+
private readonly config;
|
|
32
|
+
private state;
|
|
33
|
+
private failureCount;
|
|
34
|
+
private successCount;
|
|
35
|
+
private lastFailureTime;
|
|
36
|
+
private totalFailures;
|
|
37
|
+
private totalSuccesses;
|
|
38
|
+
private totalRejections;
|
|
39
|
+
constructor(config: CircuitBreakerConfig);
|
|
40
|
+
getMetrics(): CircuitBreakerMetrics;
|
|
41
|
+
getState(): CircuitState;
|
|
42
|
+
/**
|
|
43
|
+
* Check if request should be allowed through.
|
|
44
|
+
* Returns true if allowed, false if circuit is open.
|
|
45
|
+
*/
|
|
46
|
+
allowRequest(): boolean;
|
|
47
|
+
/**
|
|
48
|
+
* Record a successful call.
|
|
49
|
+
*/
|
|
50
|
+
recordSuccess(): void;
|
|
51
|
+
/**
|
|
52
|
+
* Record a failed call.
|
|
53
|
+
*/
|
|
54
|
+
recordFailure(): void;
|
|
55
|
+
/**
|
|
56
|
+
* Force reset to closed state (e.g., for testing or manual recovery).
|
|
57
|
+
*/
|
|
58
|
+
reset(): void;
|
|
59
|
+
private transitionTo;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Exponential backoff calculator with jitter.
|
|
63
|
+
*/
|
|
64
|
+
export interface BackoffConfig {
|
|
65
|
+
initialMs: number;
|
|
66
|
+
maxMs: number;
|
|
67
|
+
multiplier: number;
|
|
68
|
+
jitterFactor: number;
|
|
69
|
+
}
|
|
70
|
+
export declare const defaultBackoffConfig: BackoffConfig;
|
|
71
|
+
export declare function calculateBackoff(attempt: number, config?: BackoffConfig): number;
|
|
72
|
+
/**
|
|
73
|
+
* Sleep for a given number of milliseconds.
|
|
74
|
+
*/
|
|
75
|
+
export declare function sleep(ms: number): Promise<void>;
|
|
76
|
+
/**
|
|
77
|
+
* Wrap an async function with circuit breaker and retry logic.
|
|
78
|
+
*/
|
|
79
|
+
export declare function withCircuitBreaker<T>(breaker: CircuitBreaker, fn: () => Promise<T>, options?: {
|
|
80
|
+
maxRetries?: number;
|
|
81
|
+
backoffConfig?: BackoffConfig;
|
|
82
|
+
isFailure?: (error: unknown) => boolean;
|
|
83
|
+
}): Promise<T>;
|
|
84
|
+
export declare class CircuitOpenError extends Error {
|
|
85
|
+
constructor(message: string);
|
|
86
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Circuit breaker for sidecar outage resilience.
|
|
3
|
+
*
|
|
4
|
+
* States:
|
|
5
|
+
* - CLOSED: Normal operation, requests pass through
|
|
6
|
+
* - OPEN: Too many failures, requests fail fast without calling sidecar
|
|
7
|
+
* - HALF_OPEN: Testing if sidecar has recovered
|
|
8
|
+
*/
|
|
9
|
+
export const defaultCircuitBreakerConfig = {
|
|
10
|
+
failureThreshold: 5,
|
|
11
|
+
resetTimeoutMs: 30_000,
|
|
12
|
+
successThreshold: 2,
|
|
13
|
+
};
|
|
14
|
+
export class CircuitBreaker {
|
|
15
|
+
config;
|
|
16
|
+
state = "closed";
|
|
17
|
+
failureCount = 0;
|
|
18
|
+
successCount = 0;
|
|
19
|
+
lastFailureTime = null;
|
|
20
|
+
totalFailures = 0;
|
|
21
|
+
totalSuccesses = 0;
|
|
22
|
+
totalRejections = 0;
|
|
23
|
+
constructor(config) {
|
|
24
|
+
this.config = config;
|
|
25
|
+
}
|
|
26
|
+
getMetrics() {
|
|
27
|
+
return {
|
|
28
|
+
state: this.state,
|
|
29
|
+
failureCount: this.failureCount,
|
|
30
|
+
successCount: this.successCount,
|
|
31
|
+
lastFailureTime: this.lastFailureTime,
|
|
32
|
+
totalFailures: this.totalFailures,
|
|
33
|
+
totalSuccesses: this.totalSuccesses,
|
|
34
|
+
totalRejections: this.totalRejections,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
getState() {
|
|
38
|
+
return this.state;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Check if request should be allowed through.
|
|
42
|
+
* Returns true if allowed, false if circuit is open.
|
|
43
|
+
*/
|
|
44
|
+
allowRequest() {
|
|
45
|
+
if (this.state === "closed") {
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
if (this.state === "open") {
|
|
49
|
+
const now = Date.now();
|
|
50
|
+
if (this.lastFailureTime !== null &&
|
|
51
|
+
now - this.lastFailureTime >= this.config.resetTimeoutMs) {
|
|
52
|
+
this.transitionTo("half_open");
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
this.totalRejections++;
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
// half_open: allow limited requests to test recovery
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Record a successful call.
|
|
63
|
+
*/
|
|
64
|
+
recordSuccess() {
|
|
65
|
+
this.totalSuccesses++;
|
|
66
|
+
if (this.state === "half_open") {
|
|
67
|
+
this.successCount++;
|
|
68
|
+
if (this.successCount >= this.config.successThreshold) {
|
|
69
|
+
this.transitionTo("closed");
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
else if (this.state === "closed") {
|
|
73
|
+
// Reset failure count on success
|
|
74
|
+
this.failureCount = 0;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Record a failed call.
|
|
79
|
+
*/
|
|
80
|
+
recordFailure() {
|
|
81
|
+
this.totalFailures++;
|
|
82
|
+
this.lastFailureTime = Date.now();
|
|
83
|
+
if (this.state === "half_open") {
|
|
84
|
+
// Any failure in half-open reopens the circuit
|
|
85
|
+
this.transitionTo("open");
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
if (this.state === "closed") {
|
|
89
|
+
this.failureCount++;
|
|
90
|
+
if (this.failureCount >= this.config.failureThreshold) {
|
|
91
|
+
this.transitionTo("open");
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Force reset to closed state (e.g., for testing or manual recovery).
|
|
97
|
+
*/
|
|
98
|
+
reset() {
|
|
99
|
+
this.transitionTo("closed");
|
|
100
|
+
}
|
|
101
|
+
transitionTo(newState) {
|
|
102
|
+
if (this.state === newState)
|
|
103
|
+
return;
|
|
104
|
+
const oldState = this.state;
|
|
105
|
+
this.state = newState;
|
|
106
|
+
// Reset counters on state change
|
|
107
|
+
if (newState === "closed") {
|
|
108
|
+
this.failureCount = 0;
|
|
109
|
+
this.successCount = 0;
|
|
110
|
+
}
|
|
111
|
+
else if (newState === "half_open") {
|
|
112
|
+
this.successCount = 0;
|
|
113
|
+
}
|
|
114
|
+
else if (newState === "open") {
|
|
115
|
+
this.successCount = 0;
|
|
116
|
+
}
|
|
117
|
+
this.config.onStateChange?.(oldState, newState);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
export const defaultBackoffConfig = {
|
|
121
|
+
initialMs: 100,
|
|
122
|
+
maxMs: 10_000,
|
|
123
|
+
multiplier: 2,
|
|
124
|
+
jitterFactor: 0.1,
|
|
125
|
+
};
|
|
126
|
+
export function calculateBackoff(attempt, config = defaultBackoffConfig) {
|
|
127
|
+
const base = Math.min(config.initialMs * Math.pow(config.multiplier, attempt), config.maxMs);
|
|
128
|
+
const jitter = base * config.jitterFactor * (Math.random() * 2 - 1);
|
|
129
|
+
return Math.max(0, Math.round(base + jitter));
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Sleep for a given number of milliseconds.
|
|
133
|
+
*/
|
|
134
|
+
export function sleep(ms) {
|
|
135
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Wrap an async function with circuit breaker and retry logic.
|
|
139
|
+
*/
|
|
140
|
+
export async function withCircuitBreaker(breaker, fn, options) {
|
|
141
|
+
const maxRetries = options?.maxRetries ?? 0;
|
|
142
|
+
const backoffConfig = options?.backoffConfig ?? defaultBackoffConfig;
|
|
143
|
+
const isFailure = options?.isFailure ?? (() => true);
|
|
144
|
+
if (!breaker.allowRequest()) {
|
|
145
|
+
throw new CircuitOpenError("Circuit breaker is open");
|
|
146
|
+
}
|
|
147
|
+
let lastError;
|
|
148
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
149
|
+
try {
|
|
150
|
+
const result = await fn();
|
|
151
|
+
breaker.recordSuccess();
|
|
152
|
+
return result;
|
|
153
|
+
}
|
|
154
|
+
catch (error) {
|
|
155
|
+
lastError = error;
|
|
156
|
+
if (!isFailure(error)) {
|
|
157
|
+
// Not a circuit-breaker-relevant failure (e.g., business logic error)
|
|
158
|
+
throw error;
|
|
159
|
+
}
|
|
160
|
+
breaker.recordFailure();
|
|
161
|
+
if (attempt < maxRetries && breaker.allowRequest()) {
|
|
162
|
+
const delay = calculateBackoff(attempt, backoffConfig);
|
|
163
|
+
await sleep(delay);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
throw lastError;
|
|
168
|
+
}
|
|
169
|
+
export class CircuitOpenError extends Error {
|
|
170
|
+
constructor(message) {
|
|
171
|
+
super(message);
|
|
172
|
+
this.name = "CircuitOpenError";
|
|
173
|
+
}
|
|
174
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
export interface ControlPlaneSyncConfig {
|
|
2
|
+
baseUrl: string;
|
|
3
|
+
tenantId: string;
|
|
4
|
+
timeoutMs?: number;
|
|
5
|
+
}
|
|
6
|
+
export interface PolicySyncSnapshot {
|
|
7
|
+
version: string;
|
|
8
|
+
cursor: string;
|
|
9
|
+
rules: unknown[];
|
|
10
|
+
}
|
|
11
|
+
export interface RevocationSyncSnapshot {
|
|
12
|
+
version: string;
|
|
13
|
+
cursor: string;
|
|
14
|
+
revoked: unknown[];
|
|
15
|
+
}
|
|
16
|
+
export interface ControlPlaneSyncStatus {
|
|
17
|
+
policyVersion: string;
|
|
18
|
+
revocationVersion: string;
|
|
19
|
+
pinnedPolicyVersion?: string;
|
|
20
|
+
policyVersionMismatch: boolean;
|
|
21
|
+
stale: boolean;
|
|
22
|
+
syncAgeMs: number;
|
|
23
|
+
lastSyncedAtMs: number;
|
|
24
|
+
}
|
|
25
|
+
export interface ControlPlaneSyncStatusTrackerOptions {
|
|
26
|
+
pinnedPolicyVersion?: string;
|
|
27
|
+
staleAfterMs?: number;
|
|
28
|
+
onStatus?: (status: ControlPlaneSyncStatus) => void;
|
|
29
|
+
}
|
|
30
|
+
export declare class ControlPlaneSyncClient {
|
|
31
|
+
private readonly baseUrl;
|
|
32
|
+
private readonly tenantId;
|
|
33
|
+
private readonly timeoutMs;
|
|
34
|
+
constructor(config: ControlPlaneSyncConfig);
|
|
35
|
+
pullPolicySnapshot(cursor?: string): Promise<PolicySyncSnapshot>;
|
|
36
|
+
pullRevocationSnapshot(cursor?: string): Promise<RevocationSyncSnapshot>;
|
|
37
|
+
private fetchWithTimeout;
|
|
38
|
+
}
|
|
39
|
+
export declare class ControlPlaneSyncStatusTracker {
|
|
40
|
+
private readonly pinnedPolicyVersion?;
|
|
41
|
+
private readonly staleAfterMs;
|
|
42
|
+
private readonly onStatus?;
|
|
43
|
+
private latest?;
|
|
44
|
+
constructor(options?: ControlPlaneSyncStatusTrackerOptions);
|
|
45
|
+
recordSync(snapshot: {
|
|
46
|
+
policy: PolicySyncSnapshot;
|
|
47
|
+
revocations: RevocationSyncSnapshot;
|
|
48
|
+
}, nowMs?: number): ControlPlaneSyncStatus;
|
|
49
|
+
snapshot(nowMs?: number): ControlPlaneSyncStatus;
|
|
50
|
+
}
|
|
51
|
+
export declare function syncControlPlaneState(client: ControlPlaneSyncClient, cursors?: {
|
|
52
|
+
policyCursor?: string;
|
|
53
|
+
revocationCursor?: string;
|
|
54
|
+
}): Promise<{
|
|
55
|
+
policy: PolicySyncSnapshot;
|
|
56
|
+
revocations: RevocationSyncSnapshot;
|
|
57
|
+
}>;
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
export class ControlPlaneSyncClient {
|
|
2
|
+
baseUrl;
|
|
3
|
+
tenantId;
|
|
4
|
+
timeoutMs;
|
|
5
|
+
constructor(config) {
|
|
6
|
+
this.baseUrl = config.baseUrl.replace(/\/+$/, "");
|
|
7
|
+
this.tenantId = config.tenantId;
|
|
8
|
+
this.timeoutMs = config.timeoutMs ?? 3000;
|
|
9
|
+
}
|
|
10
|
+
async pullPolicySnapshot(cursor) {
|
|
11
|
+
const url = new URL(`${this.baseUrl}/v1/policy/sync`);
|
|
12
|
+
url.searchParams.set("tenant_id", this.tenantId);
|
|
13
|
+
if (cursor) {
|
|
14
|
+
url.searchParams.set("cursor", cursor);
|
|
15
|
+
}
|
|
16
|
+
const response = await this.fetchWithTimeout(url.toString());
|
|
17
|
+
if (!response.ok) {
|
|
18
|
+
throw new Error(`policy sync failed: ${response.status}`);
|
|
19
|
+
}
|
|
20
|
+
return (await response.json());
|
|
21
|
+
}
|
|
22
|
+
async pullRevocationSnapshot(cursor) {
|
|
23
|
+
const url = new URL(`${this.baseUrl}/v1/revocations/sync`);
|
|
24
|
+
url.searchParams.set("tenant_id", this.tenantId);
|
|
25
|
+
if (cursor) {
|
|
26
|
+
url.searchParams.set("cursor", cursor);
|
|
27
|
+
}
|
|
28
|
+
const response = await this.fetchWithTimeout(url.toString());
|
|
29
|
+
if (!response.ok) {
|
|
30
|
+
throw new Error(`revocation sync failed: ${response.status}`);
|
|
31
|
+
}
|
|
32
|
+
return (await response.json());
|
|
33
|
+
}
|
|
34
|
+
async fetchWithTimeout(input) {
|
|
35
|
+
const controller = new AbortController();
|
|
36
|
+
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
37
|
+
try {
|
|
38
|
+
return await fetch(input, { signal: controller.signal });
|
|
39
|
+
}
|
|
40
|
+
finally {
|
|
41
|
+
clearTimeout(timer);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
export class ControlPlaneSyncStatusTracker {
|
|
46
|
+
pinnedPolicyVersion;
|
|
47
|
+
staleAfterMs;
|
|
48
|
+
onStatus;
|
|
49
|
+
latest;
|
|
50
|
+
constructor(options) {
|
|
51
|
+
this.pinnedPolicyVersion = options?.pinnedPolicyVersion;
|
|
52
|
+
this.staleAfterMs = options?.staleAfterMs ?? 300000;
|
|
53
|
+
this.onStatus = options?.onStatus;
|
|
54
|
+
}
|
|
55
|
+
recordSync(snapshot, nowMs = Date.now()) {
|
|
56
|
+
this.latest = {
|
|
57
|
+
policyVersion: snapshot.policy.version,
|
|
58
|
+
revocationVersion: snapshot.revocations.version,
|
|
59
|
+
lastSyncedAtMs: nowMs,
|
|
60
|
+
};
|
|
61
|
+
const status = this.snapshot(nowMs);
|
|
62
|
+
this.onStatus?.(status);
|
|
63
|
+
return status;
|
|
64
|
+
}
|
|
65
|
+
snapshot(nowMs = Date.now()) {
|
|
66
|
+
if (!this.latest) {
|
|
67
|
+
return {
|
|
68
|
+
policyVersion: "unknown",
|
|
69
|
+
revocationVersion: "unknown",
|
|
70
|
+
pinnedPolicyVersion: this.pinnedPolicyVersion,
|
|
71
|
+
policyVersionMismatch: false,
|
|
72
|
+
stale: true,
|
|
73
|
+
syncAgeMs: Number.POSITIVE_INFINITY,
|
|
74
|
+
lastSyncedAtMs: 0,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
const syncAgeMs = Math.max(0, nowMs - this.latest.lastSyncedAtMs);
|
|
78
|
+
const policyVersionMismatch = typeof this.pinnedPolicyVersion === "string" &&
|
|
79
|
+
this.pinnedPolicyVersion.length > 0
|
|
80
|
+
? this.latest.policyVersion !== this.pinnedPolicyVersion
|
|
81
|
+
: false;
|
|
82
|
+
return {
|
|
83
|
+
policyVersion: this.latest.policyVersion,
|
|
84
|
+
revocationVersion: this.latest.revocationVersion,
|
|
85
|
+
pinnedPolicyVersion: this.pinnedPolicyVersion,
|
|
86
|
+
policyVersionMismatch,
|
|
87
|
+
stale: syncAgeMs > this.staleAfterMs,
|
|
88
|
+
syncAgeMs,
|
|
89
|
+
lastSyncedAtMs: this.latest.lastSyncedAtMs,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
export async function syncControlPlaneState(client, cursors) {
|
|
94
|
+
const [policy, revocations] = await Promise.all([
|
|
95
|
+
client.pullPolicySnapshot(cursors?.policyCursor),
|
|
96
|
+
client.pullRevocationSnapshot(cursors?.revocationCursor),
|
|
97
|
+
]);
|
|
98
|
+
return { policy, revocations };
|
|
99
|
+
}
|
|
@@ -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,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,46 @@
|
|
|
1
|
+
import { type DesktopAccessibilityEvidenceProvider, type DesktopAccessibilitySnapshot, type StateEvidence, type TerminalEvidenceProvider, type TerminalSessionSnapshot } from "@predicatesystems/authority";
|
|
2
|
+
export interface TerminalRuntimeContext {
|
|
3
|
+
sessionId: string;
|
|
4
|
+
terminalId?: string;
|
|
5
|
+
cwd?: string;
|
|
6
|
+
command?: string;
|
|
7
|
+
/** Raw transcript text (will be canonicalized before hashing) */
|
|
8
|
+
transcript?: string;
|
|
9
|
+
observedAt?: string;
|
|
10
|
+
confidence?: number;
|
|
11
|
+
/** Environment variables (secrets will be redacted before hashing) */
|
|
12
|
+
env?: Record<string, string>;
|
|
13
|
+
}
|
|
14
|
+
export interface DesktopRuntimeContext {
|
|
15
|
+
appName?: string;
|
|
16
|
+
windowTitle?: string;
|
|
17
|
+
focusedRole?: string;
|
|
18
|
+
focusedName?: string;
|
|
19
|
+
/** Raw UI tree text (will be normalized before hashing) */
|
|
20
|
+
uiTreeText?: string;
|
|
21
|
+
/** Pre-computed UI tree hash (bypasses canonicalization) */
|
|
22
|
+
uiTreeHash?: string;
|
|
23
|
+
observedAt?: string;
|
|
24
|
+
confidence?: number;
|
|
25
|
+
}
|
|
26
|
+
export declare class OpenClawTerminalEvidenceProvider implements TerminalEvidenceProvider {
|
|
27
|
+
private readonly capture;
|
|
28
|
+
constructor(capture: () => Promise<TerminalRuntimeContext> | TerminalRuntimeContext);
|
|
29
|
+
captureTerminalSnapshot(): Promise<TerminalSessionSnapshot>;
|
|
30
|
+
}
|
|
31
|
+
export declare class OpenClawDesktopAccessibilityEvidenceProvider implements DesktopAccessibilityEvidenceProvider {
|
|
32
|
+
private readonly capture;
|
|
33
|
+
constructor(capture: () => Promise<DesktopRuntimeContext> | DesktopRuntimeContext);
|
|
34
|
+
captureAccessibilitySnapshot(): Promise<DesktopAccessibilitySnapshot>;
|
|
35
|
+
}
|
|
36
|
+
export interface BuildEvidenceOptions {
|
|
37
|
+
/**
|
|
38
|
+
* Use canonical hashing with proper normalization.
|
|
39
|
+
* When true, applies ANSI stripping, timestamp normalization, whitespace
|
|
40
|
+
* collapsing, and other canonicalization rules for reproducible hashes.
|
|
41
|
+
* @default true
|
|
42
|
+
*/
|
|
43
|
+
useCanonicalHash?: boolean;
|
|
44
|
+
}
|
|
45
|
+
export declare function buildTerminalEvidenceFromProvider(provider: TerminalEvidenceProvider, options?: BuildEvidenceOptions): Promise<StateEvidence>;
|
|
46
|
+
export declare function buildDesktopEvidenceFromProvider(provider: DesktopAccessibilityEvidenceProvider, options?: BuildEvidenceOptions): Promise<StateEvidence>;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { buildDesktopAccessibilityStateEvidence, buildTerminalStateEvidence, } from "@predicatesystems/authority";
|
|
2
|
+
export class OpenClawTerminalEvidenceProvider {
|
|
3
|
+
capture;
|
|
4
|
+
constructor(capture) {
|
|
5
|
+
this.capture = capture;
|
|
6
|
+
}
|
|
7
|
+
async captureTerminalSnapshot() {
|
|
8
|
+
const runtime = await this.capture();
|
|
9
|
+
return {
|
|
10
|
+
session_id: runtime.sessionId,
|
|
11
|
+
terminal_id: runtime.terminalId,
|
|
12
|
+
cwd: runtime.cwd,
|
|
13
|
+
command: runtime.command,
|
|
14
|
+
// Pass raw transcript - canonicalization happens in buildTerminalStateEvidence
|
|
15
|
+
// when useCanonicalHash is enabled. The field is named transcript_hash for
|
|
16
|
+
// backward compatibility but carries raw text when canonical hashing is used.
|
|
17
|
+
transcript_hash: runtime.transcript ?? "",
|
|
18
|
+
observed_at: runtime.observedAt ?? new Date().toISOString(),
|
|
19
|
+
confidence: runtime.confidence,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
export class OpenClawDesktopAccessibilityEvidenceProvider {
|
|
24
|
+
capture;
|
|
25
|
+
constructor(capture) {
|
|
26
|
+
this.capture = capture;
|
|
27
|
+
}
|
|
28
|
+
async captureAccessibilitySnapshot() {
|
|
29
|
+
const runtime = await this.capture();
|
|
30
|
+
// Pass raw UI tree text - canonicalization happens in buildDesktopAccessibilityStateEvidence
|
|
31
|
+
// when useCanonicalHash is enabled. Use pre-computed hash if available for legacy mode.
|
|
32
|
+
// The field is named ui_tree_hash for backward compatibility but carries raw text
|
|
33
|
+
// when canonical hashing is used.
|
|
34
|
+
return {
|
|
35
|
+
app_name: runtime.appName,
|
|
36
|
+
window_title: runtime.windowTitle,
|
|
37
|
+
focused_role: runtime.focusedRole,
|
|
38
|
+
focused_name: runtime.focusedName,
|
|
39
|
+
ui_tree_hash: runtime.uiTreeHash ?? runtime.uiTreeText ?? "",
|
|
40
|
+
observed_at: runtime.observedAt ?? new Date().toISOString(),
|
|
41
|
+
confidence: runtime.confidence,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
export async function buildTerminalEvidenceFromProvider(provider, options = {}) {
|
|
46
|
+
const snapshot = await provider.captureTerminalSnapshot();
|
|
47
|
+
const useCanonicalHash = options.useCanonicalHash ?? true;
|
|
48
|
+
return buildTerminalStateEvidence({ snapshot, useCanonicalHash });
|
|
49
|
+
}
|
|
50
|
+
export async function buildDesktopEvidenceFromProvider(provider, options = {}) {
|
|
51
|
+
const snapshot = await provider.captureAccessibilitySnapshot();
|
|
52
|
+
const useCanonicalHash = options.useCanonicalHash ?? true;
|
|
53
|
+
return buildDesktopAccessibilityStateEvidence({ snapshot, useCanonicalHash });
|
|
54
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { ToolAdapter } from "./adapter.js";
|
|
2
|
+
export declare class HookEnvelope {
|
|
3
|
+
readonly toolName: string;
|
|
4
|
+
readonly args: Record<string, unknown>;
|
|
5
|
+
readonly sessionId: string;
|
|
6
|
+
readonly source: string;
|
|
7
|
+
readonly tenantId?: string;
|
|
8
|
+
readonly userId?: string;
|
|
9
|
+
readonly traceId?: string;
|
|
10
|
+
constructor(params: {
|
|
11
|
+
toolName: string;
|
|
12
|
+
args: Record<string, unknown>;
|
|
13
|
+
sessionId: string;
|
|
14
|
+
source: string;
|
|
15
|
+
tenantId?: string;
|
|
16
|
+
userId?: string;
|
|
17
|
+
traceId?: string;
|
|
18
|
+
});
|
|
19
|
+
context(): Record<string, unknown>;
|
|
20
|
+
}
|
|
21
|
+
export declare class OpenClawHooks {
|
|
22
|
+
private readonly adapter;
|
|
23
|
+
constructor(adapter: Pick<ToolAdapter, "runShell" | "readFile" | "httpRequest">);
|
|
24
|
+
onCmdRun(envelope: HookEnvelope, execute: (args: Record<string, unknown>) => Promise<unknown>): Promise<unknown>;
|
|
25
|
+
onFsRead(envelope: HookEnvelope, execute: (args: Record<string, unknown>) => Promise<unknown>): Promise<unknown>;
|
|
26
|
+
onHttpRequest(envelope: HookEnvelope, execute: (args: Record<string, unknown>) => Promise<unknown>): Promise<unknown>;
|
|
27
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
export class HookEnvelope {
|
|
2
|
+
toolName;
|
|
3
|
+
args;
|
|
4
|
+
sessionId;
|
|
5
|
+
source;
|
|
6
|
+
tenantId;
|
|
7
|
+
userId;
|
|
8
|
+
traceId;
|
|
9
|
+
constructor(params) {
|
|
10
|
+
this.toolName = params.toolName;
|
|
11
|
+
this.args = params.args;
|
|
12
|
+
this.sessionId = params.sessionId;
|
|
13
|
+
this.source = params.source;
|
|
14
|
+
this.tenantId = params.tenantId;
|
|
15
|
+
this.userId = params.userId;
|
|
16
|
+
this.traceId = params.traceId;
|
|
17
|
+
}
|
|
18
|
+
context() {
|
|
19
|
+
return {
|
|
20
|
+
session_id: this.sessionId,
|
|
21
|
+
source: this.source,
|
|
22
|
+
tenant_id: this.tenantId,
|
|
23
|
+
user_id: this.userId,
|
|
24
|
+
trace_id: this.traceId,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
export class OpenClawHooks {
|
|
29
|
+
adapter;
|
|
30
|
+
constructor(adapter) {
|
|
31
|
+
this.adapter = adapter;
|
|
32
|
+
}
|
|
33
|
+
onCmdRun(envelope, execute) {
|
|
34
|
+
return this.adapter.runShell({
|
|
35
|
+
args: envelope.args,
|
|
36
|
+
context: envelope.context(),
|
|
37
|
+
execute,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
onFsRead(envelope, execute) {
|
|
41
|
+
return this.adapter.readFile({
|
|
42
|
+
args: envelope.args,
|
|
43
|
+
context: envelope.context(),
|
|
44
|
+
execute,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
onHttpRequest(envelope, execute) {
|
|
48
|
+
return this.adapter.httpRequest({
|
|
49
|
+
args: envelope.args,
|
|
50
|
+
context: envelope.context(),
|
|
51
|
+
execute,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
}
|