pi-harness-runtime 1.1.429 → 1.1.431
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/package.json +1 -1
- package/packages/a2a-adapter/package.json +1 -1
- package/packages/agent-policy/package.json +1 -1
- package/packages/architecture-generator/package.json +1 -1
- package/packages/auth/package.json +1 -1
- package/packages/autonomous-refactor/package.json +1 -1
- package/packages/autonomous-runtime/package.json +1 -1
- package/packages/cache-strategy/package.json +1 -1
- package/packages/capability-registry/package.json +1 -1
- package/packages/checkpoint/package.json +1 -1
- package/packages/cli-plugin-sdk/package.json +1 -1
- package/packages/clipboard/package.json +1 -1
- package/packages/clipboard-plugin/package.json +1 -1
- package/packages/code-generation/package.json +1 -1
- package/packages/code-review/package.json +1 -1
- package/packages/codex-adapter/package.json +1 -1
- package/packages/codex-watcher/package.json +1 -1
- package/packages/config-capture/package.json +1 -1
- package/packages/connection-runtime/package.json +1 -1
- package/packages/context-compiler/package.json +1 -1
- package/packages/context-discovery/package.json +1 -1
- package/packages/context-manager/package.json +1 -1
- package/packages/contracts/package.json +1 -1
- package/packages/cookie-sanitizer/package.json +1 -1
- package/packages/cost-optimizer/package.json +1 -1
- package/packages/dependency-analyzer/package.json +1 -1
- package/packages/django-plugin/package.json +1 -1
- package/packages/doc-generator/package.json +1 -1
- package/packages/durable-review/package.json +1 -1
- package/packages/e2e-tester/package.json +1 -1
- package/packages/evaluation-engine/package.json +1 -1
- package/packages/evaluation-runner/package.json +1 -1
- package/packages/event-bus/package.json +1 -1
- package/packages/event-sink/package.json +1 -1
- package/packages/event-store/package.json +1 -1
- package/packages/experience-replay/package.json +1 -1
- package/packages/feedback-collector/package.json +1 -1
- package/packages/file-copy-helper/package.json +1 -1
- package/packages/framework-detector/package.json +1 -1
- package/packages/framework-plugin-sdk/package.json +1 -1
- package/packages/frappe-plugin/package.json +1 -1
- package/packages/generic-web-plugin/package.json +1 -1
- package/packages/health-monitor/package.json +1 -1
- package/packages/intent-analyzer/package.json +1 -1
- package/packages/jev-judge/package.json +1 -1
- package/packages/knowledge-graph/package.json +1 -1
- package/packages/knowledge-retrieval/package.json +1 -1
- package/packages/laravel-plugin/package.json +1 -1
- package/packages/learning-engine/package.json +1 -1
- package/packages/logging/package.json +1 -1
- package/packages/mcp-adapter/package.json +1 -1
- package/packages/memory-engine/package.json +1 -1
- package/packages/milestone-manager/package.json +1 -1
- package/packages/model-registry/package.json +1 -1
- package/packages/nextjs-plugin/package.json +1 -1
- package/packages/notification/package.json +1 -1
- package/packages/observability/package.json +1 -1
- package/packages/okf-indexer/package.json +1 -1
- package/packages/paperclip-integration/package.json +1 -1
- package/packages/performance-optimizer/package.json +1 -1
- package/packages/permission-mode/package.json +1 -1
- package/packages/privilege-broker/package.json +1 -1
- package/packages/project-analyzer/package.json +1 -1
- package/packages/project-bootstrap/package.json +1 -1
- package/packages/projection-engine/package.json +1 -1
- package/packages/prompt-compiler/package.json +1 -1
- package/packages/prompt-versioning/package.json +1 -1
- package/packages/provider-adapter-sdk/package.json +1 -1
- package/packages/provider-router/package.json +1 -1
- package/packages/provider-selector/package.json +1 -1
- package/packages/providers/package.json +1 -1
- package/packages/qdrant-skills/package.json +1 -1
- package/packages/quota-manager/package.json +1 -1
- package/packages/rate-limiter/package.json +1 -1
- package/packages/react-vite-plugin/package.json +1 -1
- package/packages/relay/package.json +1 -1
- package/packages/release-manager/package.json +1 -1
- package/packages/requirement-compiler/package.json +1 -1
- package/packages/runtime/dist/runtime-api.d.ts +61 -6
- package/packages/runtime/dist/runtime-api.d.ts.map +1 -1
- package/packages/runtime/dist/runtime-api.js +83 -30
- package/packages/runtime/dist/runtime-api.js.map +1 -1
- package/packages/runtime/package.json +1 -1
- package/packages/runtime/src/runtime-api.ts +168 -52
- package/packages/runtime/test/runtime-api.test.ts +169 -0
- package/packages/scheduler/package.json +1 -1
- package/packages/scheduler-adapter/package.json +1 -1
- package/packages/session/package.json +1 -1
- package/packages/session-api/package.json +1 -1
- package/packages/session-export/package.json +1 -1
- package/packages/shared-context/package.json +1 -1
- package/packages/skill-mcp-client/package.json +1 -1
- package/packages/skill-registry/package.json +1 -1
- package/packages/skills/package.json +1 -1
- package/packages/sprint-planner/package.json +1 -1
- package/packages/subscription-engine/package.json +1 -1
- package/packages/task-compiler/package.json +1 -1
- package/packages/tencentdb-memory/package.json +1 -1
- package/packages/tencentdb-sync/package.json +1 -1
- package/packages/test-data-generator/package.json +1 -1
- package/packages/test-generator/package.json +1 -1
- package/packages/todo-bd-sync/package.json +1 -1
- package/packages/token-estimation/package.json +1 -1
- package/packages/token-optimizer/package.json +1 -1
- package/packages/tooling-preference/package.json +1 -1
- package/packages/trajectory/package.json +1 -1
- package/packages/tui/package.json +1 -1
- package/packages/two-phase-review/package.json +1 -1
- package/packages/types/package.json +1 -1
- package/packages/voice-api/package.json +1 -1
- package/packages/webmcp-tester/package.json +1 -1
- package/packages/wiki/package.json +1 -1
- package/packages/workflow-events/package.json +1 -1
- package/packages/workspace-scanner/package.json +1 -1
- package/packages/worktree/package.json +1 -1
- package/packages/write-review/package.json +1 -1
- package/scripts/typecheck-packages.mjs +1 -1
- package/harness/task-graph.d.ts +0 -51
- package/packages/notification/adapters/email-adapter.ts +0 -126
- package/packages/notification/adapters/ntfy-adapter.ts +0 -100
- package/packages/notification/adapters/telegram-adapter.ts +0 -107
- package/packages/notification/adapters/webhook-adapter.ts +0 -75
- package/packages/notification/base-adapter.ts +0 -75
- package/packages/notification/index.ts +0 -51
- package/packages/notification/notification-center.ts +0 -286
- package/packages/notification/types.ts +0 -89
- package/packages/quota-manager/index.ts +0 -28
- package/packages/quota-manager/quota-manager.ts +0 -339
- package/packages/quota-manager/tui-usage-monitor.ts +0 -355
- package/packages/runtime/command-executor.ts +0 -523
- package/packages/runtime/index.ts +0 -50
- package/packages/runtime/local-runtime-agent.ts +0 -480
- package/packages/runtime/policy-engine.ts +0 -813
- package/packages/runtime/runtime-api.ts +0 -665
- package/packages/runtime/workspace-manager.ts +0 -591
- package/packages/types/src/ai-providers.d.ts +0 -36
- package/packages/types/src/ai-providers.js +0 -66
- package/packages/types/src/runtime-types.d.ts +0 -418
- package/packages/types/src/runtime-types.js +0 -3
|
@@ -1,813 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Policy Engine — RFC-0028
|
|
3
|
-
*
|
|
4
|
-
* Policy enforcement for command execution, network access, and file operations.
|
|
5
|
-
*
|
|
6
|
-
* Features:
|
|
7
|
-
* - Command allowlist/denylist
|
|
8
|
-
* - Network access control
|
|
9
|
-
* - File operation restrictions
|
|
10
|
-
* - Rate limiting
|
|
11
|
-
* - Audit logging
|
|
12
|
-
* - Policy inheritance and composition
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
import { EventEmitter } from "node:events";
|
|
16
|
-
|
|
17
|
-
export type PolicyEffect = "allow" | "deny" | "ask";
|
|
18
|
-
|
|
19
|
-
export interface PolicyResult {
|
|
20
|
-
effect: PolicyEffect;
|
|
21
|
-
reason?: string;
|
|
22
|
-
policy?: string;
|
|
23
|
-
score?: number;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export interface PolicyContext {
|
|
27
|
-
/** Who/what is requesting access */
|
|
28
|
-
subject: {
|
|
29
|
-
user?: string;
|
|
30
|
-
role?: string;
|
|
31
|
-
agent?: string;
|
|
32
|
-
ip?: string;
|
|
33
|
-
};
|
|
34
|
-
/** What resource is being accessed */
|
|
35
|
-
resource: {
|
|
36
|
-
type: "command" | "file" | "network" | "api" | "system";
|
|
37
|
-
path?: string;
|
|
38
|
-
url?: string;
|
|
39
|
-
method?: string;
|
|
40
|
-
operation?: string;
|
|
41
|
-
};
|
|
42
|
-
/** Environment context */
|
|
43
|
-
environment?: {
|
|
44
|
-
jobId?: string;
|
|
45
|
-
taskId?: string;
|
|
46
|
-
worktree?: string;
|
|
47
|
-
workspace?: string;
|
|
48
|
-
};
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
export interface Policy {
|
|
52
|
-
id: string;
|
|
53
|
-
name: string;
|
|
54
|
-
description?: string;
|
|
55
|
-
priority: number;
|
|
56
|
-
effect: PolicyEffect;
|
|
57
|
-
condition: PolicyCondition;
|
|
58
|
-
action?: string;
|
|
59
|
-
reason?: string;
|
|
60
|
-
audit?: boolean;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
export interface PolicyCondition {
|
|
64
|
-
/** Match subject */
|
|
65
|
-
subject?: {
|
|
66
|
-
user?: string | string[];
|
|
67
|
-
role?: string | string[];
|
|
68
|
-
agent?: string | string[];
|
|
69
|
-
ip?: string | string[];
|
|
70
|
-
};
|
|
71
|
-
/** Match resource */
|
|
72
|
-
resource?: {
|
|
73
|
-
type?: "command" | "file" | "network" | "api" | "system" | string;
|
|
74
|
-
path?: string | string[];
|
|
75
|
-
pathPattern?: string;
|
|
76
|
-
url?: string | string[];
|
|
77
|
-
urlPattern?: string;
|
|
78
|
-
method?: string | string[];
|
|
79
|
-
operation?: string | string[];
|
|
80
|
-
};
|
|
81
|
-
/** Match environment */
|
|
82
|
-
environment?: {
|
|
83
|
-
jobId?: string | string[];
|
|
84
|
-
taskId?: string | string[];
|
|
85
|
-
worktree?: string | string[];
|
|
86
|
-
workspace?: string | string[];
|
|
87
|
-
};
|
|
88
|
-
/** Time-based conditions */
|
|
89
|
-
time?: {
|
|
90
|
-
startHour?: number;
|
|
91
|
-
endHour?: number;
|
|
92
|
-
days?: number[];
|
|
93
|
-
};
|
|
94
|
-
/** Custom condition function */
|
|
95
|
-
custom?: (context: PolicyContext) => boolean;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
export interface PolicyRule {
|
|
99
|
-
pattern: RegExp;
|
|
100
|
-
effect: PolicyEffect;
|
|
101
|
-
reason?: string;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
export interface RateLimit {
|
|
105
|
-
maxRequests: number;
|
|
106
|
-
windowMs: number;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
export interface AuditEntry {
|
|
110
|
-
timestamp: string;
|
|
111
|
-
policy: string;
|
|
112
|
-
effect: PolicyEffect;
|
|
113
|
-
context: PolicyContext;
|
|
114
|
-
reason?: string;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
const DEFAULT_RATE_LIMIT: RateLimit = {
|
|
118
|
-
maxRequests: 100,
|
|
119
|
-
windowMs: 60000,
|
|
120
|
-
};
|
|
121
|
-
|
|
122
|
-
export class PolicyEngine extends EventEmitter {
|
|
123
|
-
private policies: Policy[] = [];
|
|
124
|
-
private commandRules: PolicyRule[] = [];
|
|
125
|
-
private fileRules: PolicyRule[] = [];
|
|
126
|
-
private networkRules: PolicyRule[] = [];
|
|
127
|
-
private rateLimits: Map<string, RateLimit> = new Map();
|
|
128
|
-
private requestCounts: Map<string, { count: number; windowStart: number }> = new Map();
|
|
129
|
-
private auditLog: AuditEntry[] = [];
|
|
130
|
-
private maxAuditEntries = 10000;
|
|
131
|
-
|
|
132
|
-
constructor() {
|
|
133
|
-
super();
|
|
134
|
-
this.initializeDefaultPolicies();
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
/**
|
|
138
|
-
* Initialize default security policies
|
|
139
|
-
*/
|
|
140
|
-
private initializeDefaultPolicies(): void {
|
|
141
|
-
// Deny dangerous commands
|
|
142
|
-
this.addPolicy({
|
|
143
|
-
id: "deny-rm-rf",
|
|
144
|
-
name: "Deny rm -rf /",
|
|
145
|
-
description: "Block rm -rf on root",
|
|
146
|
-
priority: 100,
|
|
147
|
-
effect: "deny",
|
|
148
|
-
condition: {
|
|
149
|
-
subject: {},
|
|
150
|
-
resource: {
|
|
151
|
-
type: "command",
|
|
152
|
-
pathPattern: "^\\s*rm\\s+-rf\\s+\\/",
|
|
153
|
-
},
|
|
154
|
-
},
|
|
155
|
-
reason: "Cannot delete root directory",
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
// Allow safe development commands
|
|
159
|
-
this.addPolicy({
|
|
160
|
-
id: "allow-git",
|
|
161
|
-
name: "Allow git commands",
|
|
162
|
-
description: "Allow git operations",
|
|
163
|
-
priority: 10,
|
|
164
|
-
effect: "allow",
|
|
165
|
-
condition: {
|
|
166
|
-
resource: {
|
|
167
|
-
type: "command",
|
|
168
|
-
pathPattern: "^\\s*git\\s+",
|
|
169
|
-
},
|
|
170
|
-
},
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
// Allow npm/npx commands
|
|
174
|
-
this.addPolicy({
|
|
175
|
-
id: "allow-npm",
|
|
176
|
-
name: "Allow npm/npx",
|
|
177
|
-
description: "Allow npm package management",
|
|
178
|
-
priority: 10,
|
|
179
|
-
effect: "allow",
|
|
180
|
-
condition: {
|
|
181
|
-
resource: {
|
|
182
|
-
type: "command",
|
|
183
|
-
pathPattern: "^(npm|npx|yarn|pnpm)\\s+",
|
|
184
|
-
},
|
|
185
|
-
},
|
|
186
|
-
});
|
|
187
|
-
|
|
188
|
-
// Deny network to private IPs
|
|
189
|
-
this.addPolicy({
|
|
190
|
-
id: "deny-private-network",
|
|
191
|
-
name: "Deny private network access",
|
|
192
|
-
description: "Block access to private IP ranges",
|
|
193
|
-
priority: 90,
|
|
194
|
-
effect: "deny",
|
|
195
|
-
condition: {
|
|
196
|
-
resource: {
|
|
197
|
-
type: "network",
|
|
198
|
-
urlPattern: "^(10\\.|172\\.(1[6-9]|2[0-9]|3[0-1])\\.|192\\.168\\.|127\\.|localhost)",
|
|
199
|
-
},
|
|
200
|
-
},
|
|
201
|
-
reason: "Private network access is restricted",
|
|
202
|
-
});
|
|
203
|
-
|
|
204
|
-
// Allow HTTPS only
|
|
205
|
-
this.addPolicy({
|
|
206
|
-
id: "allow-https",
|
|
207
|
-
name: "Allow HTTPS",
|
|
208
|
-
description: "Allow secure HTTPS connections",
|
|
209
|
-
priority: 20,
|
|
210
|
-
effect: "allow",
|
|
211
|
-
condition: {
|
|
212
|
-
resource: {
|
|
213
|
-
type: "network",
|
|
214
|
-
urlPattern: "^https://",
|
|
215
|
-
},
|
|
216
|
-
},
|
|
217
|
-
});
|
|
218
|
-
|
|
219
|
-
// Block file access outside workspace
|
|
220
|
-
this.addPolicy({
|
|
221
|
-
id: "deny-outside-workspace",
|
|
222
|
-
name: "Deny access outside workspace",
|
|
223
|
-
description: "Block file operations outside workspace",
|
|
224
|
-
priority: 80,
|
|
225
|
-
effect: "deny",
|
|
226
|
-
condition: {
|
|
227
|
-
resource: {
|
|
228
|
-
type: "file",
|
|
229
|
-
pathPattern: "^\\.\\.(/|$)",
|
|
230
|
-
},
|
|
231
|
-
},
|
|
232
|
-
reason: "Cannot access paths outside workspace",
|
|
233
|
-
});
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
/**
|
|
237
|
-
* Add a policy
|
|
238
|
-
*/
|
|
239
|
-
addPolicy(policy: Policy): void {
|
|
240
|
-
this.policies.push(policy);
|
|
241
|
-
this.policies.sort((a, b) => b.priority - a.priority);
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
/**
|
|
245
|
-
* Remove a policy
|
|
246
|
-
*/
|
|
247
|
-
removePolicy(id: string): boolean {
|
|
248
|
-
const index = this.policies.findIndex((p) => p.id === id);
|
|
249
|
-
if (index !== -1) {
|
|
250
|
-
this.policies.splice(index, 1);
|
|
251
|
-
return true;
|
|
252
|
-
}
|
|
253
|
-
return false;
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
/**
|
|
257
|
-
* Get all policies
|
|
258
|
-
*/
|
|
259
|
-
getPolicies(): Policy[] {
|
|
260
|
-
return [...this.policies];
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
/**
|
|
264
|
-
* Check if a request is allowed
|
|
265
|
-
*/
|
|
266
|
-
evaluate(context: PolicyContext): PolicyResult {
|
|
267
|
-
// Check rate limits first
|
|
268
|
-
const rateLimitResult = this.checkRateLimit(context);
|
|
269
|
-
if (rateLimitResult.effect === "deny") {
|
|
270
|
-
return rateLimitResult;
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
// Find matching policy
|
|
274
|
-
for (const policy of this.policies) {
|
|
275
|
-
if (this.matchesPolicy(context, policy)) {
|
|
276
|
-
// Audit if enabled
|
|
277
|
-
if (policy.audit) {
|
|
278
|
-
this.logAudit(policy, context);
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
return {
|
|
282
|
-
effect: policy.effect,
|
|
283
|
-
reason: policy.reason,
|
|
284
|
-
policy: policy.id,
|
|
285
|
-
};
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
// Default: ask for unknown resources
|
|
290
|
-
return {
|
|
291
|
-
effect: "ask",
|
|
292
|
-
reason: "No matching policy found",
|
|
293
|
-
};
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
/**
|
|
297
|
-
* Check if a command is allowed
|
|
298
|
-
*/
|
|
299
|
-
canExecuteCommand(command: string, context?: Partial<PolicyContext>): PolicyResult {
|
|
300
|
-
const fullContext: PolicyContext = {
|
|
301
|
-
subject: context?.subject ?? {},
|
|
302
|
-
resource: {
|
|
303
|
-
type: "command",
|
|
304
|
-
path: command,
|
|
305
|
-
},
|
|
306
|
-
environment: context?.environment,
|
|
307
|
-
};
|
|
308
|
-
|
|
309
|
-
// Check against command rules first
|
|
310
|
-
for (const rule of this.commandRules) {
|
|
311
|
-
if (rule.pattern.test(command)) {
|
|
312
|
-
this.logAudit(
|
|
313
|
-
{ id: "command-rule", name: "Command Rule", priority: 0, effect: rule.effect } as Policy,
|
|
314
|
-
fullContext,
|
|
315
|
-
);
|
|
316
|
-
|
|
317
|
-
return {
|
|
318
|
-
effect: rule.effect,
|
|
319
|
-
reason: rule.reason,
|
|
320
|
-
};
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
return this.evaluate(fullContext);
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
/**
|
|
328
|
-
* Check if a file operation is allowed
|
|
329
|
-
*/
|
|
330
|
-
canAccessFile(
|
|
331
|
-
path: string,
|
|
332
|
-
operation: "read" | "write" | "delete" | "execute",
|
|
333
|
-
context?: Partial<PolicyContext>,
|
|
334
|
-
): PolicyResult {
|
|
335
|
-
const fullContext: PolicyContext = {
|
|
336
|
-
subject: context?.subject ?? {},
|
|
337
|
-
resource: {
|
|
338
|
-
type: "file",
|
|
339
|
-
path,
|
|
340
|
-
operation,
|
|
341
|
-
},
|
|
342
|
-
environment: context?.environment,
|
|
343
|
-
};
|
|
344
|
-
|
|
345
|
-
// Check against file rules
|
|
346
|
-
for (const rule of this.fileRules) {
|
|
347
|
-
if (rule.pattern.test(path)) {
|
|
348
|
-
this.logAudit(
|
|
349
|
-
{ id: "file-rule", name: "File Rule", priority: 0, effect: rule.effect } as Policy,
|
|
350
|
-
fullContext,
|
|
351
|
-
);
|
|
352
|
-
|
|
353
|
-
return {
|
|
354
|
-
effect: rule.effect,
|
|
355
|
-
reason: rule.reason,
|
|
356
|
-
};
|
|
357
|
-
}
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
return this.evaluate(fullContext);
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
/**
|
|
364
|
-
* Check if a network request is allowed
|
|
365
|
-
*/
|
|
366
|
-
canMakeNetworkRequest(
|
|
367
|
-
url: string,
|
|
368
|
-
method?: string,
|
|
369
|
-
context?: Partial<PolicyContext>,
|
|
370
|
-
): PolicyResult {
|
|
371
|
-
const fullContext: PolicyContext = {
|
|
372
|
-
subject: context?.subject ?? {},
|
|
373
|
-
resource: {
|
|
374
|
-
type: "network",
|
|
375
|
-
url,
|
|
376
|
-
method,
|
|
377
|
-
},
|
|
378
|
-
environment: context?.environment,
|
|
379
|
-
};
|
|
380
|
-
|
|
381
|
-
// Check against network rules
|
|
382
|
-
for (const rule of this.networkRules) {
|
|
383
|
-
if (rule.pattern.test(url)) {
|
|
384
|
-
this.logAudit(
|
|
385
|
-
{ id: "network-rule", name: "Network Rule", priority: 0, effect: rule.effect } as Policy,
|
|
386
|
-
fullContext,
|
|
387
|
-
);
|
|
388
|
-
|
|
389
|
-
return {
|
|
390
|
-
effect: rule.effect,
|
|
391
|
-
reason: rule.reason,
|
|
392
|
-
};
|
|
393
|
-
}
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
return this.evaluate(fullContext);
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
/**
|
|
400
|
-
* Add a command rule
|
|
401
|
-
*/
|
|
402
|
-
addCommandRule(pattern: RegExp, effect: PolicyEffect, reason?: string): void {
|
|
403
|
-
this.commandRules.push({ pattern, effect, reason });
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
/**
|
|
407
|
-
* Add a file rule
|
|
408
|
-
*/
|
|
409
|
-
addFileRule(pattern: RegExp, effect: PolicyEffect, reason?: string): void {
|
|
410
|
-
this.fileRules.push({ pattern, effect, reason });
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
/**
|
|
414
|
-
* Add a network rule
|
|
415
|
-
*/
|
|
416
|
-
addNetworkRule(pattern: RegExp, effect: PolicyEffect, reason?: string): void {
|
|
417
|
-
this.networkRules.push({ pattern, effect, reason });
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
/**
|
|
421
|
-
* Set rate limit for a resource
|
|
422
|
-
*/
|
|
423
|
-
setRateLimit(resource: string, limit: RateLimit): void {
|
|
424
|
-
this.rateLimits.set(resource, limit);
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
/**
|
|
428
|
-
* Get rate limit for a resource
|
|
429
|
-
*/
|
|
430
|
-
getRateLimit(resource: string): RateLimit | undefined {
|
|
431
|
-
return this.rateLimits.get(resource);
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
/**
|
|
435
|
-
* Get audit log
|
|
436
|
-
*/
|
|
437
|
-
getAuditLog(filter?: {
|
|
438
|
-
policy?: string;
|
|
439
|
-
effect?: PolicyEffect;
|
|
440
|
-
since?: Date;
|
|
441
|
-
}): AuditEntry[] {
|
|
442
|
-
let entries = [...this.auditLog];
|
|
443
|
-
|
|
444
|
-
if (filter?.policy) {
|
|
445
|
-
entries = entries.filter((e) => e.policy === filter.policy);
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
if (filter?.effect) {
|
|
449
|
-
entries = entries.filter((e) => e.effect === filter.effect);
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
if (filter?.since) {
|
|
453
|
-
entries = entries.filter(
|
|
454
|
-
(e) => new Date(e.timestamp) >= filter.since!,
|
|
455
|
-
);
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
return entries;
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
/**
|
|
462
|
-
* Clear audit log
|
|
463
|
-
*/
|
|
464
|
-
clearAuditLog(): void {
|
|
465
|
-
this.auditLog = [];
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
/**
|
|
469
|
-
* Export policies as JSON
|
|
470
|
-
*/
|
|
471
|
-
exportPolicies(): string {
|
|
472
|
-
return JSON.stringify(
|
|
473
|
-
{
|
|
474
|
-
policies: this.policies,
|
|
475
|
-
commandRules: this.commandRules.map((r) => ({
|
|
476
|
-
pattern: r.pattern.source,
|
|
477
|
-
effect: r.effect,
|
|
478
|
-
reason: r.reason,
|
|
479
|
-
})),
|
|
480
|
-
fileRules: this.fileRules.map((r) => ({
|
|
481
|
-
pattern: r.pattern.source,
|
|
482
|
-
effect: r.effect,
|
|
483
|
-
reason: r.reason,
|
|
484
|
-
})),
|
|
485
|
-
networkRules: this.networkRules.map((r) => ({
|
|
486
|
-
pattern: r.pattern.source,
|
|
487
|
-
effect: r.effect,
|
|
488
|
-
reason: r.reason,
|
|
489
|
-
})),
|
|
490
|
-
rateLimits: Object.fromEntries(this.rateLimits),
|
|
491
|
-
},
|
|
492
|
-
null,
|
|
493
|
-
2,
|
|
494
|
-
);
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
/**
|
|
498
|
-
* Import policies from JSON
|
|
499
|
-
*/
|
|
500
|
-
importPolicies(json: string): boolean {
|
|
501
|
-
try {
|
|
502
|
-
const data = JSON.parse(json);
|
|
503
|
-
|
|
504
|
-
if (data.policies) {
|
|
505
|
-
this.policies = data.policies;
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
if (data.commandRules) {
|
|
509
|
-
this.commandRules = data.commandRules.map(
|
|
510
|
-
(r: { pattern: string; effect: PolicyEffect; reason?: string }) => ({
|
|
511
|
-
pattern: new RegExp(r.pattern),
|
|
512
|
-
effect: r.effect,
|
|
513
|
-
reason: r.reason,
|
|
514
|
-
}),
|
|
515
|
-
);
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
if (data.fileRules) {
|
|
519
|
-
this.fileRules = data.fileRules.map(
|
|
520
|
-
(r: { pattern: string; effect: PolicyEffect; reason?: string }) => ({
|
|
521
|
-
pattern: new RegExp(r.pattern),
|
|
522
|
-
effect: r.effect,
|
|
523
|
-
reason: r.reason,
|
|
524
|
-
}),
|
|
525
|
-
);
|
|
526
|
-
}
|
|
527
|
-
|
|
528
|
-
if (data.networkRules) {
|
|
529
|
-
this.networkRules = data.networkRules.map(
|
|
530
|
-
(r: { pattern: string; effect: PolicyEffect; reason?: string }) => ({
|
|
531
|
-
pattern: new RegExp(r.pattern),
|
|
532
|
-
effect: r.effect,
|
|
533
|
-
reason: r.reason,
|
|
534
|
-
}),
|
|
535
|
-
);
|
|
536
|
-
}
|
|
537
|
-
|
|
538
|
-
if (data.rateLimits) {
|
|
539
|
-
this.rateLimits = new Map(Object.entries(data.rateLimits));
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
return true;
|
|
543
|
-
} catch {
|
|
544
|
-
return false;
|
|
545
|
-
}
|
|
546
|
-
}
|
|
547
|
-
|
|
548
|
-
// ─── Private Methods ────────────────────────────────────────────────
|
|
549
|
-
|
|
550
|
-
private matchesPolicy(context: PolicyContext, policy: Policy): boolean {
|
|
551
|
-
const condition = policy.condition;
|
|
552
|
-
|
|
553
|
-
// Check subject
|
|
554
|
-
if (condition.subject && !this.matchesSubject(context, condition.subject)) {
|
|
555
|
-
return false;
|
|
556
|
-
}
|
|
557
|
-
|
|
558
|
-
// Check resource
|
|
559
|
-
if (condition.resource && !this.matchesResource(context, condition.resource)) {
|
|
560
|
-
return false;
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
// Check environment
|
|
564
|
-
if (condition.environment && !this.matchesEnvironment(context, condition.environment)) {
|
|
565
|
-
return false;
|
|
566
|
-
}
|
|
567
|
-
|
|
568
|
-
// Check time
|
|
569
|
-
if (condition.time && !this.matchesTime(condition.time)) {
|
|
570
|
-
return false;
|
|
571
|
-
}
|
|
572
|
-
|
|
573
|
-
// Check custom
|
|
574
|
-
if (condition.custom && !condition.custom(context)) {
|
|
575
|
-
return false;
|
|
576
|
-
}
|
|
577
|
-
|
|
578
|
-
return true;
|
|
579
|
-
}
|
|
580
|
-
|
|
581
|
-
private matchesSubject(
|
|
582
|
-
context: PolicyContext,
|
|
583
|
-
condition: NonNullable<PolicyCondition["subject"]>,
|
|
584
|
-
): boolean {
|
|
585
|
-
const { user, role, agent, ip } = context.subject;
|
|
586
|
-
|
|
587
|
-
if (user && condition.user) {
|
|
588
|
-
if (!this.matchesValue(user, condition.user)) return false;
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
if (role && condition.role) {
|
|
592
|
-
if (!this.matchesValue(role, condition.role)) return false;
|
|
593
|
-
}
|
|
594
|
-
|
|
595
|
-
if (agent && condition.agent) {
|
|
596
|
-
if (!this.matchesValue(agent, condition.agent)) return false;
|
|
597
|
-
}
|
|
598
|
-
|
|
599
|
-
if (ip && condition.ip) {
|
|
600
|
-
if (!this.matchesValue(ip, condition.ip)) return false;
|
|
601
|
-
}
|
|
602
|
-
|
|
603
|
-
return true;
|
|
604
|
-
}
|
|
605
|
-
|
|
606
|
-
private matchesResource(
|
|
607
|
-
context: PolicyContext,
|
|
608
|
-
condition: NonNullable<PolicyCondition["resource"]>,
|
|
609
|
-
): boolean {
|
|
610
|
-
const { type, path, url, method, operation } = context.resource;
|
|
611
|
-
|
|
612
|
-
if (type && condition.type) {
|
|
613
|
-
if (type !== condition.type && condition.type !== "*") return false;
|
|
614
|
-
}
|
|
615
|
-
|
|
616
|
-
if (path && (condition.path || condition.pathPattern)) {
|
|
617
|
-
if (condition.path && !this.matchesValue(path, condition.path)) return false;
|
|
618
|
-
if (condition.pathPattern && !new RegExp(condition.pathPattern).test(path)) {
|
|
619
|
-
return false;
|
|
620
|
-
}
|
|
621
|
-
}
|
|
622
|
-
|
|
623
|
-
if (url && (condition.url || condition.urlPattern)) {
|
|
624
|
-
if (condition.url && !this.matchesValue(url, condition.url)) return false;
|
|
625
|
-
if (condition.urlPattern && !new RegExp(condition.urlPattern).test(url)) {
|
|
626
|
-
return false;
|
|
627
|
-
}
|
|
628
|
-
}
|
|
629
|
-
|
|
630
|
-
if (method && condition.method) {
|
|
631
|
-
if (!this.matchesValue(method, condition.method)) return false;
|
|
632
|
-
}
|
|
633
|
-
|
|
634
|
-
if (operation && condition.operation) {
|
|
635
|
-
if (!this.matchesValue(operation, condition.operation)) return false;
|
|
636
|
-
}
|
|
637
|
-
|
|
638
|
-
return true;
|
|
639
|
-
}
|
|
640
|
-
|
|
641
|
-
private matchesEnvironment(
|
|
642
|
-
context: PolicyContext,
|
|
643
|
-
condition: NonNullable<PolicyCondition["environment"]>,
|
|
644
|
-
): boolean {
|
|
645
|
-
const env = context.environment ?? {};
|
|
646
|
-
|
|
647
|
-
if (env.jobId && condition.jobId) {
|
|
648
|
-
if (!this.matchesValue(env.jobId, condition.jobId)) return false;
|
|
649
|
-
}
|
|
650
|
-
|
|
651
|
-
if (env.taskId && condition.taskId) {
|
|
652
|
-
if (!this.matchesValue(env.taskId, condition.taskId)) return false;
|
|
653
|
-
}
|
|
654
|
-
|
|
655
|
-
if (env.worktree && condition.worktree) {
|
|
656
|
-
if (!this.matchesValue(env.worktree, condition.worktree)) return false;
|
|
657
|
-
}
|
|
658
|
-
|
|
659
|
-
if (env.workspace && condition.workspace) {
|
|
660
|
-
if (!this.matchesValue(env.workspace, condition.workspace)) return false;
|
|
661
|
-
}
|
|
662
|
-
|
|
663
|
-
return true;
|
|
664
|
-
}
|
|
665
|
-
|
|
666
|
-
private matchesTime(condition: NonNullable<PolicyCondition["time"]>): boolean {
|
|
667
|
-
const now = new Date();
|
|
668
|
-
|
|
669
|
-
if (condition.startHour !== undefined && condition.endHour !== undefined) {
|
|
670
|
-
const hour = now.getHours();
|
|
671
|
-
if (condition.startHour <= condition.endHour) {
|
|
672
|
-
if (hour < condition.startHour || hour > condition.endHour) return false;
|
|
673
|
-
} else {
|
|
674
|
-
// Handles overnight ranges like 22:00 - 06:00
|
|
675
|
-
if (hour < condition.startHour && hour > condition.endHour) return false;
|
|
676
|
-
}
|
|
677
|
-
}
|
|
678
|
-
|
|
679
|
-
if (condition.days && condition.days.length > 0) {
|
|
680
|
-
const day = now.getDay();
|
|
681
|
-
if (!condition.days.includes(day)) return false;
|
|
682
|
-
}
|
|
683
|
-
|
|
684
|
-
return true;
|
|
685
|
-
}
|
|
686
|
-
|
|
687
|
-
private matchesValue(value: string, pattern: string | string[]): boolean {
|
|
688
|
-
if (Array.isArray(pattern)) {
|
|
689
|
-
return pattern.includes(value);
|
|
690
|
-
}
|
|
691
|
-
|
|
692
|
-
// Check if pattern is a regex
|
|
693
|
-
if (pattern.startsWith("/") && pattern.endsWith("/")) {
|
|
694
|
-
try {
|
|
695
|
-
return new RegExp(pattern.slice(1, -1)).test(value);
|
|
696
|
-
} catch {
|
|
697
|
-
return false;
|
|
698
|
-
}
|
|
699
|
-
}
|
|
700
|
-
|
|
701
|
-
// Glob pattern
|
|
702
|
-
if (pattern.includes("*")) {
|
|
703
|
-
const regex = new RegExp(
|
|
704
|
-
"^" + pattern.replace(/\*/g, ".*").replace(/\?/g, ".") + "$",
|
|
705
|
-
);
|
|
706
|
-
return regex.test(value);
|
|
707
|
-
}
|
|
708
|
-
|
|
709
|
-
return value === pattern;
|
|
710
|
-
}
|
|
711
|
-
|
|
712
|
-
private checkRateLimit(context: PolicyContext): PolicyResult {
|
|
713
|
-
const key = this.getRateLimitKey(context);
|
|
714
|
-
const limit = this.rateLimits.get(key) ?? DEFAULT_RATE_LIMIT;
|
|
715
|
-
|
|
716
|
-
const now = Date.now();
|
|
717
|
-
let record = this.requestCounts.get(key);
|
|
718
|
-
|
|
719
|
-
if (!record || now - record.windowStart > limit.windowMs) {
|
|
720
|
-
// Start new window
|
|
721
|
-
record = { count: 1, windowStart: now };
|
|
722
|
-
this.requestCounts.set(key, record);
|
|
723
|
-
return { effect: "allow" };
|
|
724
|
-
}
|
|
725
|
-
|
|
726
|
-
record.count++;
|
|
727
|
-
this.requestCounts.set(key, record);
|
|
728
|
-
|
|
729
|
-
if (record.count > limit.maxRequests) {
|
|
730
|
-
return {
|
|
731
|
-
effect: "deny",
|
|
732
|
-
reason: `Rate limit exceeded: ${record.count}/${limit.maxRequests} requests in ${limit.windowMs}ms`,
|
|
733
|
-
};
|
|
734
|
-
}
|
|
735
|
-
|
|
736
|
-
return { effect: "allow" };
|
|
737
|
-
}
|
|
738
|
-
|
|
739
|
-
private getRateLimitKey(context: PolicyContext): string {
|
|
740
|
-
const parts: string[] = [context.resource.type];
|
|
741
|
-
|
|
742
|
-
if (context.subject.user) parts.push(context.subject.user);
|
|
743
|
-
if (context.subject.agent) parts.push(context.subject.agent);
|
|
744
|
-
if (context.resource.path) parts.push(context.resource.path);
|
|
745
|
-
if (context.resource.url) parts.push(context.resource.url);
|
|
746
|
-
|
|
747
|
-
return parts.join(":");
|
|
748
|
-
}
|
|
749
|
-
|
|
750
|
-
private logAudit(policy: Policy, context: PolicyContext): void {
|
|
751
|
-
const entry: AuditEntry = {
|
|
752
|
-
timestamp: new Date().toISOString(),
|
|
753
|
-
policy: policy.id,
|
|
754
|
-
effect: policy.effect,
|
|
755
|
-
context,
|
|
756
|
-
reason: policy.reason,
|
|
757
|
-
};
|
|
758
|
-
|
|
759
|
-
this.auditLog.push(entry);
|
|
760
|
-
|
|
761
|
-
// Trim log if too large
|
|
762
|
-
if (this.auditLog.length > this.maxAuditEntries) {
|
|
763
|
-
this.auditLog = this.auditLog.slice(-this.maxAuditEntries / 2);
|
|
764
|
-
}
|
|
765
|
-
|
|
766
|
-
this.emit("audit", entry);
|
|
767
|
-
}
|
|
768
|
-
}
|
|
769
|
-
|
|
770
|
-
/**
|
|
771
|
-
* Create a PolicyEngine with default harness policies
|
|
772
|
-
*/
|
|
773
|
-
export function createHarnessPolicyEngine(): PolicyEngine {
|
|
774
|
-
const engine = new PolicyEngine();
|
|
775
|
-
|
|
776
|
-
// Add additional harness-specific policies
|
|
777
|
-
engine.addPolicy({
|
|
778
|
-
id: "allow-harness-internal",
|
|
779
|
-
name: "Allow harness internal commands",
|
|
780
|
-
description: "Allow internal harness commands",
|
|
781
|
-
priority: 15,
|
|
782
|
-
effect: "allow",
|
|
783
|
-
condition: {
|
|
784
|
-
resource: {
|
|
785
|
-
type: "command",
|
|
786
|
-
pathPattern: "^(pi|harness)-",
|
|
787
|
-
},
|
|
788
|
-
},
|
|
789
|
-
});
|
|
790
|
-
|
|
791
|
-
// Default deny for system commands
|
|
792
|
-
engine.addPolicy({
|
|
793
|
-
id: "deny-system",
|
|
794
|
-
name: "Deny system commands",
|
|
795
|
-
description: "Block system-level commands by default",
|
|
796
|
-
priority: 70,
|
|
797
|
-
effect: "deny",
|
|
798
|
-
condition: {
|
|
799
|
-
resource: {
|
|
800
|
-
type: "command",
|
|
801
|
-
pathPattern: "^(sudo|su|chmod|chown|mount|umount|reboot|shutdown)\\s",
|
|
802
|
-
},
|
|
803
|
-
},
|
|
804
|
-
reason: "System commands are restricted",
|
|
805
|
-
});
|
|
806
|
-
|
|
807
|
-
// Set rate limits
|
|
808
|
-
engine.setRateLimit("command", { maxRequests: 60, windowMs: 60000 }); // 60 commands/minute
|
|
809
|
-
engine.setRateLimit("network", { maxRequests: 100, windowMs: 60000 }); // 100 requests/minute
|
|
810
|
-
engine.setRateLimit("file", { maxRequests: 200, windowMs: 60000 }); // 200 file ops/minute
|
|
811
|
-
|
|
812
|
-
return engine;
|
|
813
|
-
}
|