chainlesschain 0.37.10 → 0.37.12
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/README.md +166 -10
- package/package.json +1 -1
- package/src/commands/a2a.js +374 -0
- package/src/commands/bi.js +240 -0
- package/src/commands/cowork.js +317 -0
- package/src/commands/economy.js +375 -0
- package/src/commands/evolution.js +398 -0
- package/src/commands/hmemory.js +273 -0
- package/src/commands/hook.js +260 -0
- package/src/commands/init.js +184 -0
- package/src/commands/lowcode.js +320 -0
- package/src/commands/plugin.js +55 -2
- package/src/commands/sandbox.js +366 -0
- package/src/commands/skill.js +254 -201
- package/src/commands/workflow.js +359 -0
- package/src/commands/zkp.js +277 -0
- package/src/index.js +44 -0
- package/src/lib/a2a-protocol.js +371 -0
- package/src/lib/agent-coordinator.js +273 -0
- package/src/lib/agent-economy.js +369 -0
- package/src/lib/app-builder.js +377 -0
- package/src/lib/bi-engine.js +299 -0
- package/src/lib/cowork/ab-comparator-cli.js +180 -0
- package/src/lib/cowork/code-knowledge-graph-cli.js +232 -0
- package/src/lib/cowork/debate-review-cli.js +144 -0
- package/src/lib/cowork/decision-kb-cli.js +153 -0
- package/src/lib/cowork/project-style-analyzer-cli.js +168 -0
- package/src/lib/cowork-adapter.js +106 -0
- package/src/lib/evolution-system.js +508 -0
- package/src/lib/hierarchical-memory.js +471 -0
- package/src/lib/hook-manager.js +387 -0
- package/src/lib/plugin-manager.js +118 -0
- package/src/lib/project-detector.js +53 -0
- package/src/lib/sandbox-v2.js +503 -0
- package/src/lib/service-container.js +183 -0
- package/src/lib/skill-loader.js +274 -0
- package/src/lib/workflow-engine.js +503 -0
- package/src/lib/zkp-engine.js +241 -0
- package/src/repl/agent-repl.js +117 -112
|
@@ -0,0 +1,503 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sandbox v2 — Security sandbox with permissions, quotas, and behavior monitoring.
|
|
3
|
+
* Provides isolated execution environments for agent code with fine-grained access control.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import crypto from "crypto";
|
|
7
|
+
|
|
8
|
+
// ─── Constants ────────────────────────────────────────────────
|
|
9
|
+
|
|
10
|
+
export const DEFAULT_QUOTA = {
|
|
11
|
+
cpu: 100,
|
|
12
|
+
memory: 256 * 1024 * 1024,
|
|
13
|
+
storage: 100 * 1024 * 1024,
|
|
14
|
+
network: 1000,
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export const DEFAULT_PERMISSIONS = {
|
|
18
|
+
fileSystem: {
|
|
19
|
+
read: ["/tmp"],
|
|
20
|
+
write: ["/tmp"],
|
|
21
|
+
denied: ["/etc", "/usr", "/sys"],
|
|
22
|
+
},
|
|
23
|
+
network: {
|
|
24
|
+
allowed: ["localhost"],
|
|
25
|
+
denied: [],
|
|
26
|
+
maxConnections: 10,
|
|
27
|
+
},
|
|
28
|
+
systemCalls: {
|
|
29
|
+
allowed: ["read", "write", "open", "close", "stat"],
|
|
30
|
+
denied: ["exec", "fork", "kill", "mount"],
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
// ─── In-memory stores ─────────────────────────────────────────
|
|
35
|
+
|
|
36
|
+
const activeSandboxes = new Map();
|
|
37
|
+
const auditLog = [];
|
|
38
|
+
|
|
39
|
+
// ─── Database helpers ─────────────────────────────────────────
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Create sandbox-related tables.
|
|
43
|
+
*/
|
|
44
|
+
export function ensureSandboxTables(db) {
|
|
45
|
+
db.exec(`
|
|
46
|
+
CREATE TABLE IF NOT EXISTS sandbox_instances (
|
|
47
|
+
id TEXT PRIMARY KEY,
|
|
48
|
+
agent_id TEXT NOT NULL,
|
|
49
|
+
status TEXT DEFAULT 'active',
|
|
50
|
+
permissions TEXT,
|
|
51
|
+
quota TEXT,
|
|
52
|
+
resource_usage TEXT,
|
|
53
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
54
|
+
updated_at TEXT DEFAULT (datetime('now'))
|
|
55
|
+
)
|
|
56
|
+
`);
|
|
57
|
+
db.exec(`
|
|
58
|
+
CREATE TABLE IF NOT EXISTS sandbox_audit (
|
|
59
|
+
id TEXT PRIMARY KEY,
|
|
60
|
+
sandbox_id TEXT NOT NULL,
|
|
61
|
+
action TEXT NOT NULL,
|
|
62
|
+
details TEXT,
|
|
63
|
+
result TEXT,
|
|
64
|
+
timestamp TEXT DEFAULT (datetime('now'))
|
|
65
|
+
)
|
|
66
|
+
`);
|
|
67
|
+
db.exec(`
|
|
68
|
+
CREATE TABLE IF NOT EXISTS sandbox_behavior (
|
|
69
|
+
id TEXT PRIMARY KEY,
|
|
70
|
+
sandbox_id TEXT NOT NULL,
|
|
71
|
+
event_type TEXT NOT NULL,
|
|
72
|
+
event_data TEXT,
|
|
73
|
+
timestamp TEXT DEFAULT (datetime('now'))
|
|
74
|
+
)
|
|
75
|
+
`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ─── Permission checking ──────────────────────────────────────
|
|
79
|
+
|
|
80
|
+
function checkFilePermission(permissions, filePath, mode) {
|
|
81
|
+
const fs = permissions.fileSystem || {};
|
|
82
|
+
const denied = fs.denied || [];
|
|
83
|
+
for (const d of denied) {
|
|
84
|
+
if (filePath.startsWith(d)) return false;
|
|
85
|
+
}
|
|
86
|
+
if (mode === "read") {
|
|
87
|
+
const allowed = fs.read || [];
|
|
88
|
+
return allowed.some((p) => filePath.startsWith(p));
|
|
89
|
+
}
|
|
90
|
+
if (mode === "write") {
|
|
91
|
+
const allowed = fs.write || [];
|
|
92
|
+
return allowed.some((p) => filePath.startsWith(p));
|
|
93
|
+
}
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function checkNetworkPermission(permissions, host) {
|
|
98
|
+
const net = permissions.network || {};
|
|
99
|
+
const denied = net.denied || [];
|
|
100
|
+
if (denied.includes(host)) return false;
|
|
101
|
+
const allowed = net.allowed || [];
|
|
102
|
+
return allowed.includes(host) || allowed.includes("*");
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function checkSystemCallPermission(permissions, call) {
|
|
106
|
+
const sys = permissions.systemCalls || {};
|
|
107
|
+
const denied = sys.denied || [];
|
|
108
|
+
if (denied.includes(call)) return false;
|
|
109
|
+
const allowed = sys.allowed || [];
|
|
110
|
+
return allowed.includes(call) || allowed.includes("*");
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// ─── Core functions ───────────────────────────────────────────
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Create a new sandbox for an agent.
|
|
117
|
+
*/
|
|
118
|
+
export function createSandbox(db, agentId, options = {}) {
|
|
119
|
+
ensureSandboxTables(db);
|
|
120
|
+
|
|
121
|
+
const id = crypto.randomUUID();
|
|
122
|
+
const permissions = options.permissions || { ...DEFAULT_PERMISSIONS };
|
|
123
|
+
const quota = options.quota || { ...DEFAULT_QUOTA };
|
|
124
|
+
const resourceUsage = { cpu: 0, memory: 0, storage: 0, network: 0 };
|
|
125
|
+
|
|
126
|
+
const sandbox = {
|
|
127
|
+
id,
|
|
128
|
+
agentId,
|
|
129
|
+
status: "active",
|
|
130
|
+
permissions,
|
|
131
|
+
quota,
|
|
132
|
+
resourceUsage,
|
|
133
|
+
createdAt: new Date().toISOString(),
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
db.prepare(
|
|
137
|
+
`INSERT INTO sandbox_instances (id, agent_id, status, permissions, quota, resource_usage)
|
|
138
|
+
VALUES (?, ?, ?, ?, ?, ?)`,
|
|
139
|
+
).run(
|
|
140
|
+
id,
|
|
141
|
+
agentId,
|
|
142
|
+
"active",
|
|
143
|
+
JSON.stringify(permissions),
|
|
144
|
+
JSON.stringify(quota),
|
|
145
|
+
JSON.stringify(resourceUsage),
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
activeSandboxes.set(id, sandbox);
|
|
149
|
+
|
|
150
|
+
logAudit(db, id, "create", { agentId, permissions, quota });
|
|
151
|
+
|
|
152
|
+
return { id, status: "active", permissions, quota };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Execute code within a sandbox.
|
|
157
|
+
*/
|
|
158
|
+
export function executeSandbox(db, sandboxId, code, options = {}) {
|
|
159
|
+
ensureSandboxTables(db);
|
|
160
|
+
|
|
161
|
+
const sandbox = activeSandboxes.get(sandboxId);
|
|
162
|
+
if (!sandbox || sandbox.status !== "active") {
|
|
163
|
+
throw new Error(`Sandbox ${sandboxId} not found or not active`);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Check file permissions if requested
|
|
167
|
+
if (options.filePath) {
|
|
168
|
+
const mode = options.fileMode || "read";
|
|
169
|
+
if (!checkFilePermission(sandbox.permissions, options.filePath, mode)) {
|
|
170
|
+
logAudit(db, sandboxId, "permission-denied", {
|
|
171
|
+
type: "fileSystem",
|
|
172
|
+
path: options.filePath,
|
|
173
|
+
mode,
|
|
174
|
+
});
|
|
175
|
+
logBehavior(db, sandboxId, "denied-access", {
|
|
176
|
+
type: "fileSystem",
|
|
177
|
+
path: options.filePath,
|
|
178
|
+
});
|
|
179
|
+
throw new Error(
|
|
180
|
+
`Permission denied: ${mode} access to ${options.filePath}`,
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Check network permissions if requested
|
|
186
|
+
if (options.host) {
|
|
187
|
+
if (!checkNetworkPermission(sandbox.permissions, options.host)) {
|
|
188
|
+
logAudit(db, sandboxId, "permission-denied", {
|
|
189
|
+
type: "network",
|
|
190
|
+
host: options.host,
|
|
191
|
+
});
|
|
192
|
+
logBehavior(db, sandboxId, "denied-access", {
|
|
193
|
+
type: "network",
|
|
194
|
+
host: options.host,
|
|
195
|
+
});
|
|
196
|
+
throw new Error(`Permission denied: network access to ${options.host}`);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Check system call permissions if requested
|
|
201
|
+
if (options.systemCall) {
|
|
202
|
+
if (!checkSystemCallPermission(sandbox.permissions, options.systemCall)) {
|
|
203
|
+
logAudit(db, sandboxId, "permission-denied", {
|
|
204
|
+
type: "systemCall",
|
|
205
|
+
call: options.systemCall,
|
|
206
|
+
});
|
|
207
|
+
logBehavior(db, sandboxId, "denied-access", {
|
|
208
|
+
type: "systemCall",
|
|
209
|
+
call: options.systemCall,
|
|
210
|
+
});
|
|
211
|
+
throw new Error(`Permission denied: system call ${options.systemCall}`);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Check quota
|
|
216
|
+
const usage = sandbox.resourceUsage;
|
|
217
|
+
if (usage.cpu >= sandbox.quota.cpu) {
|
|
218
|
+
logAudit(db, sandboxId, "quota-exceeded", { resource: "cpu" });
|
|
219
|
+
throw new Error("Quota exceeded: CPU limit reached");
|
|
220
|
+
}
|
|
221
|
+
if (usage.memory >= sandbox.quota.memory) {
|
|
222
|
+
logAudit(db, sandboxId, "quota-exceeded", { resource: "memory" });
|
|
223
|
+
throw new Error("Quota exceeded: memory limit reached");
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Simulate execution
|
|
227
|
+
const startTime = Date.now();
|
|
228
|
+
let output = "";
|
|
229
|
+
let exitCode = 0;
|
|
230
|
+
|
|
231
|
+
try {
|
|
232
|
+
// Simulate code evaluation (safe — just record)
|
|
233
|
+
output = `Executed ${code.length} bytes of code`;
|
|
234
|
+
exitCode = 0;
|
|
235
|
+
} catch (err) {
|
|
236
|
+
output = err.message;
|
|
237
|
+
exitCode = 1;
|
|
238
|
+
logBehavior(db, sandboxId, "error", { error: err.message });
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const duration = Date.now() - startTime;
|
|
242
|
+
|
|
243
|
+
// Update resource usage
|
|
244
|
+
usage.cpu += 1;
|
|
245
|
+
usage.memory += code.length;
|
|
246
|
+
sandbox.resourceUsage = usage;
|
|
247
|
+
|
|
248
|
+
db.prepare(
|
|
249
|
+
`UPDATE sandbox_instances SET resource_usage = ?, updated_at = datetime('now') WHERE id = ?`,
|
|
250
|
+
).run(JSON.stringify(usage), sandboxId);
|
|
251
|
+
|
|
252
|
+
logAudit(db, sandboxId, "execute", {
|
|
253
|
+
codeLength: code.length,
|
|
254
|
+
exitCode,
|
|
255
|
+
duration,
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
return {
|
|
259
|
+
output,
|
|
260
|
+
exitCode,
|
|
261
|
+
duration,
|
|
262
|
+
resourceUsage: { ...usage },
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Destroy a sandbox.
|
|
268
|
+
*/
|
|
269
|
+
export function destroySandbox(db, sandboxId) {
|
|
270
|
+
ensureSandboxTables(db);
|
|
271
|
+
|
|
272
|
+
const sandbox = activeSandboxes.get(sandboxId);
|
|
273
|
+
if (!sandbox) {
|
|
274
|
+
throw new Error(`Sandbox ${sandboxId} not found`);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
sandbox.status = "destroyed";
|
|
278
|
+
activeSandboxes.delete(sandboxId);
|
|
279
|
+
|
|
280
|
+
db.prepare(
|
|
281
|
+
`UPDATE sandbox_instances SET status = 'destroyed', updated_at = datetime('now') WHERE id = ?`,
|
|
282
|
+
).run(sandboxId);
|
|
283
|
+
|
|
284
|
+
logAudit(db, sandboxId, "destroy", {});
|
|
285
|
+
|
|
286
|
+
return { id: sandboxId, status: "destroyed" };
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Update permissions for a sandbox.
|
|
291
|
+
*/
|
|
292
|
+
export function setPermissions(db, sandboxId, permissions) {
|
|
293
|
+
ensureSandboxTables(db);
|
|
294
|
+
|
|
295
|
+
const sandbox = activeSandboxes.get(sandboxId);
|
|
296
|
+
if (!sandbox) {
|
|
297
|
+
throw new Error(`Sandbox ${sandboxId} not found`);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
sandbox.permissions = permissions;
|
|
301
|
+
|
|
302
|
+
db.prepare(
|
|
303
|
+
`UPDATE sandbox_instances SET permissions = ?, updated_at = datetime('now') WHERE id = ?`,
|
|
304
|
+
).run(JSON.stringify(permissions), sandboxId);
|
|
305
|
+
|
|
306
|
+
logAudit(db, sandboxId, "set-permissions", { permissions });
|
|
307
|
+
|
|
308
|
+
return { id: sandboxId, permissions };
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Update quota for a sandbox.
|
|
313
|
+
*/
|
|
314
|
+
export function setQuota(db, sandboxId, quota) {
|
|
315
|
+
ensureSandboxTables(db);
|
|
316
|
+
|
|
317
|
+
const sandbox = activeSandboxes.get(sandboxId);
|
|
318
|
+
if (!sandbox) {
|
|
319
|
+
throw new Error(`Sandbox ${sandboxId} not found`);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
sandbox.quota = quota;
|
|
323
|
+
|
|
324
|
+
db.prepare(
|
|
325
|
+
`UPDATE sandbox_instances SET quota = ?, updated_at = datetime('now') WHERE id = ?`,
|
|
326
|
+
).run(JSON.stringify(quota), sandboxId);
|
|
327
|
+
|
|
328
|
+
logAudit(db, sandboxId, "set-quota", { quota });
|
|
329
|
+
|
|
330
|
+
return { id: sandboxId, quota };
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Get sandbox information.
|
|
335
|
+
*/
|
|
336
|
+
export function getSandbox(db, sandboxId) {
|
|
337
|
+
ensureSandboxTables(db);
|
|
338
|
+
|
|
339
|
+
const sandbox = activeSandboxes.get(sandboxId);
|
|
340
|
+
if (sandbox) {
|
|
341
|
+
return {
|
|
342
|
+
id: sandbox.id,
|
|
343
|
+
agentId: sandbox.agentId,
|
|
344
|
+
status: sandbox.status,
|
|
345
|
+
permissions: sandbox.permissions,
|
|
346
|
+
quota: sandbox.quota,
|
|
347
|
+
resourceUsage: sandbox.resourceUsage,
|
|
348
|
+
createdAt: sandbox.createdAt,
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Fallback to DB
|
|
353
|
+
const row = db
|
|
354
|
+
.prepare(`SELECT * FROM sandbox_instances WHERE id = ?`)
|
|
355
|
+
.get(sandboxId);
|
|
356
|
+
if (!row) return null;
|
|
357
|
+
|
|
358
|
+
return {
|
|
359
|
+
id: row.id,
|
|
360
|
+
agentId: row.agent_id,
|
|
361
|
+
status: row.status,
|
|
362
|
+
permissions: JSON.parse(row.permissions || "{}"),
|
|
363
|
+
quota: JSON.parse(row.quota || "{}"),
|
|
364
|
+
resourceUsage: JSON.parse(row.resource_usage || "{}"),
|
|
365
|
+
createdAt: row.created_at,
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* List all active sandboxes.
|
|
371
|
+
*/
|
|
372
|
+
export function listSandboxes(db) {
|
|
373
|
+
ensureSandboxTables(db);
|
|
374
|
+
|
|
375
|
+
const results = [];
|
|
376
|
+
for (const [, sandbox] of activeSandboxes) {
|
|
377
|
+
if (sandbox.status === "active") {
|
|
378
|
+
results.push({
|
|
379
|
+
id: sandbox.id,
|
|
380
|
+
agentId: sandbox.agentId,
|
|
381
|
+
status: sandbox.status,
|
|
382
|
+
quota: sandbox.quota,
|
|
383
|
+
resourceUsage: sandbox.resourceUsage,
|
|
384
|
+
createdAt: sandbox.createdAt,
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
return results;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Get audit log entries.
|
|
393
|
+
*/
|
|
394
|
+
export function getAuditLog(db, sandboxId, options = {}) {
|
|
395
|
+
ensureSandboxTables(db);
|
|
396
|
+
|
|
397
|
+
let entries = [...auditLog];
|
|
398
|
+
|
|
399
|
+
if (sandboxId) {
|
|
400
|
+
entries = entries.filter((e) => e.sandboxId === sandboxId);
|
|
401
|
+
}
|
|
402
|
+
if (options.action) {
|
|
403
|
+
entries = entries.filter((e) => e.action === options.action);
|
|
404
|
+
}
|
|
405
|
+
if (options.limit) {
|
|
406
|
+
entries = entries.slice(-options.limit);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
return entries;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* Monitor sandbox behavior and detect suspicious patterns.
|
|
414
|
+
*/
|
|
415
|
+
export function monitorBehavior(db, sandboxId) {
|
|
416
|
+
ensureSandboxTables(db);
|
|
417
|
+
|
|
418
|
+
const entries = auditLog.filter((e) => e.sandboxId === sandboxId);
|
|
419
|
+
const patterns = [];
|
|
420
|
+
let riskScore = 0;
|
|
421
|
+
|
|
422
|
+
// Check for excessive denied access attempts
|
|
423
|
+
const deniedCount = entries.filter(
|
|
424
|
+
(e) => e.action === "permission-denied",
|
|
425
|
+
).length;
|
|
426
|
+
if (deniedCount > 10) {
|
|
427
|
+
patterns.push({
|
|
428
|
+
type: "excessive-denied-access",
|
|
429
|
+
count: deniedCount,
|
|
430
|
+
severity: "high",
|
|
431
|
+
});
|
|
432
|
+
riskScore += 40;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// Check for frequent errors
|
|
436
|
+
const errorEntries = auditLog.filter(
|
|
437
|
+
(e) => e.sandboxId === sandboxId && e.action === "execute",
|
|
438
|
+
);
|
|
439
|
+
const errorCount = errorEntries.filter(
|
|
440
|
+
(e) => e.details && e.details.exitCode !== 0,
|
|
441
|
+
).length;
|
|
442
|
+
if (errorCount > 5) {
|
|
443
|
+
patterns.push({
|
|
444
|
+
type: "frequent-errors",
|
|
445
|
+
count: errorCount,
|
|
446
|
+
severity: "medium",
|
|
447
|
+
});
|
|
448
|
+
riskScore += 25;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// Check for quota exceeded attempts
|
|
452
|
+
const quotaExceeded = entries.filter(
|
|
453
|
+
(e) => e.action === "quota-exceeded",
|
|
454
|
+
).length;
|
|
455
|
+
if (quotaExceeded > 0) {
|
|
456
|
+
patterns.push({
|
|
457
|
+
type: "quota-exceeded-attempts",
|
|
458
|
+
count: quotaExceeded,
|
|
459
|
+
severity: "medium",
|
|
460
|
+
});
|
|
461
|
+
riskScore += 15;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
return {
|
|
465
|
+
patterns,
|
|
466
|
+
totalEvents: entries.length,
|
|
467
|
+
riskScore: Math.min(100, riskScore),
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// ─── Internal helpers ─────────────────────────────────────────
|
|
472
|
+
|
|
473
|
+
function logAudit(db, sandboxId, action, details) {
|
|
474
|
+
const entry = {
|
|
475
|
+
id: crypto.randomUUID(),
|
|
476
|
+
sandboxId,
|
|
477
|
+
action,
|
|
478
|
+
details,
|
|
479
|
+
timestamp: new Date().toISOString(),
|
|
480
|
+
};
|
|
481
|
+
auditLog.push(entry);
|
|
482
|
+
|
|
483
|
+
db.prepare(
|
|
484
|
+
`INSERT INTO sandbox_audit (id, sandbox_id, action, details, timestamp)
|
|
485
|
+
VALUES (?, ?, ?, ?, ?)`,
|
|
486
|
+
).run(entry.id, sandboxId, action, JSON.stringify(details), entry.timestamp);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
function logBehavior(db, sandboxId, eventType, eventData) {
|
|
490
|
+
const id = crypto.randomUUID();
|
|
491
|
+
db.prepare(
|
|
492
|
+
`INSERT INTO sandbox_behavior (id, sandbox_id, event_type, event_data)
|
|
493
|
+
VALUES (?, ?, ?, ?)`,
|
|
494
|
+
).run(id, sandboxId, eventType, JSON.stringify(eventData));
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
/**
|
|
498
|
+
* Clear in-memory state (for testing).
|
|
499
|
+
*/
|
|
500
|
+
export function _resetState() {
|
|
501
|
+
activeSandboxes.clear();
|
|
502
|
+
auditLog.length = 0;
|
|
503
|
+
}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Service Container — Dependency Injection container for CLI services.
|
|
3
|
+
*
|
|
4
|
+
* Supports singleton/transient lifetimes, lazy resolution, dependency
|
|
5
|
+
* injection with circular dependency detection, tag-based lookup, and
|
|
6
|
+
* disposal of resolved instances.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* DI Container class.
|
|
11
|
+
*/
|
|
12
|
+
export class ServiceContainer {
|
|
13
|
+
constructor() {
|
|
14
|
+
/** @type {Map<string, { factory: Function, options: object }>} */
|
|
15
|
+
this._services = new Map();
|
|
16
|
+
|
|
17
|
+
/** @type {Map<string, any>} */
|
|
18
|
+
this._instances = new Map();
|
|
19
|
+
|
|
20
|
+
/** @type {Set<string>} */
|
|
21
|
+
this._initializing = new Set();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Register a service factory.
|
|
26
|
+
*
|
|
27
|
+
* @param {string} name - Service name
|
|
28
|
+
* @param {Function} factory - Factory function `(container) => instance`
|
|
29
|
+
* @param {object} [options]
|
|
30
|
+
* @param {boolean} [options.singleton=true] - Cache the instance
|
|
31
|
+
* @param {boolean} [options.lazy=true] - Resolve on first use
|
|
32
|
+
* @param {string[]} [options.dependencies=[]] - Dependency names (for documentation / health)
|
|
33
|
+
* @param {string[]} [options.tags=[]] - Tags for grouping
|
|
34
|
+
*/
|
|
35
|
+
register(name, factory, options = {}) {
|
|
36
|
+
if (!name || typeof factory !== "function") {
|
|
37
|
+
throw new Error("register() requires a name and factory function");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const opts = {
|
|
41
|
+
singleton: options.singleton !== undefined ? options.singleton : true,
|
|
42
|
+
lazy: options.lazy !== undefined ? options.lazy : true,
|
|
43
|
+
dependencies: options.dependencies || [],
|
|
44
|
+
tags: options.tags || [],
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
this._services.set(name, { factory, options: opts });
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Resolve a service by name.
|
|
52
|
+
*
|
|
53
|
+
* @param {string} name
|
|
54
|
+
* @returns {Promise<any>}
|
|
55
|
+
*/
|
|
56
|
+
async resolve(name) {
|
|
57
|
+
if (!this._services.has(name)) {
|
|
58
|
+
throw new Error(`Service "${name}" is not registered`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const entry = this._services.get(name);
|
|
62
|
+
|
|
63
|
+
// Return cached singleton
|
|
64
|
+
if (entry.options.singleton && this._instances.has(name)) {
|
|
65
|
+
return this._instances.get(name);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Circular dependency detection
|
|
69
|
+
if (this._initializing.has(name)) {
|
|
70
|
+
throw new Error(`Circular dependency detected: "${name}"`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
this._initializing.add(name);
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
const instance = await entry.factory(this);
|
|
77
|
+
|
|
78
|
+
if (entry.options.singleton) {
|
|
79
|
+
this._instances.set(name, instance);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return instance;
|
|
83
|
+
} finally {
|
|
84
|
+
this._initializing.delete(name);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Check whether a service is registered.
|
|
90
|
+
*
|
|
91
|
+
* @param {string} name
|
|
92
|
+
* @returns {boolean}
|
|
93
|
+
*/
|
|
94
|
+
has(name) {
|
|
95
|
+
return this._services.has(name);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Check whether a service has been resolved (instantiated).
|
|
100
|
+
*
|
|
101
|
+
* @param {string} name
|
|
102
|
+
* @returns {boolean}
|
|
103
|
+
*/
|
|
104
|
+
isResolved(name) {
|
|
105
|
+
return this._instances.has(name);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Find all services that have a given tag.
|
|
110
|
+
*
|
|
111
|
+
* @param {string} tag
|
|
112
|
+
* @returns {string[]} - Service names
|
|
113
|
+
*/
|
|
114
|
+
getByTag(tag) {
|
|
115
|
+
const result = [];
|
|
116
|
+
for (const [name, entry] of this._services) {
|
|
117
|
+
if (entry.options.tags.includes(tag)) {
|
|
118
|
+
result.push(name);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return result;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Dispose all resolved instances. Calls `dispose()` or `destroy()` on each.
|
|
126
|
+
*/
|
|
127
|
+
async disposeAll() {
|
|
128
|
+
for (const [name, instance] of this._instances) {
|
|
129
|
+
try {
|
|
130
|
+
if (typeof instance.dispose === "function") {
|
|
131
|
+
await instance.dispose();
|
|
132
|
+
} else if (typeof instance.destroy === "function") {
|
|
133
|
+
await instance.destroy();
|
|
134
|
+
}
|
|
135
|
+
} catch (_err) {
|
|
136
|
+
// Intentionally ignore disposal errors — best-effort cleanup
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
this._instances.clear();
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Return health / metadata for every registered service.
|
|
144
|
+
*
|
|
145
|
+
* @returns {Record<string, object>}
|
|
146
|
+
*/
|
|
147
|
+
getHealth() {
|
|
148
|
+
const health = {};
|
|
149
|
+
for (const [name, entry] of this._services) {
|
|
150
|
+
health[name] = {
|
|
151
|
+
registered: true,
|
|
152
|
+
resolved: this._instances.has(name),
|
|
153
|
+
singleton: entry.options.singleton,
|
|
154
|
+
lazy: entry.options.lazy,
|
|
155
|
+
dependencies: entry.options.dependencies,
|
|
156
|
+
tags: entry.options.tags,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
return health;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Return aggregate stats.
|
|
164
|
+
*
|
|
165
|
+
* @returns {{ totalServices: number, resolvedServices: number, initializingServices: number }}
|
|
166
|
+
*/
|
|
167
|
+
getStats() {
|
|
168
|
+
return {
|
|
169
|
+
totalServices: this._services.size,
|
|
170
|
+
resolvedServices: this._instances.size,
|
|
171
|
+
initializingServices: this._initializing.size,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Factory function to create a new ServiceContainer.
|
|
178
|
+
*
|
|
179
|
+
* @returns {ServiceContainer}
|
|
180
|
+
*/
|
|
181
|
+
export function createServiceContainer() {
|
|
182
|
+
return new ServiceContainer();
|
|
183
|
+
}
|