network-ai 5.10.2 → 5.11.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/INTEGRATION_GUIDE.md +2 -2
- package/README.md +5 -3
- package/SKILL.md +3 -3
- package/dist/esm/adapters/a2a-adapter.js +235 -0
- package/dist/esm/adapters/a2a-adapter.js.map +1 -0
- package/dist/esm/adapters/adapter-registry.js +613 -0
- package/dist/esm/adapters/adapter-registry.js.map +1 -0
- package/dist/esm/adapters/agno-adapter.js +140 -0
- package/dist/esm/adapters/agno-adapter.js.map +1 -0
- package/dist/esm/adapters/anthropic-computer-use-adapter.js +180 -0
- package/dist/esm/adapters/anthropic-computer-use-adapter.js.map +1 -0
- package/dist/esm/adapters/aps-adapter.js +289 -0
- package/dist/esm/adapters/aps-adapter.js.map +1 -0
- package/dist/esm/adapters/autogen-adapter.js +141 -0
- package/dist/esm/adapters/autogen-adapter.js.map +1 -0
- package/dist/esm/adapters/base-adapter.js +104 -0
- package/dist/esm/adapters/base-adapter.js.map +1 -0
- package/dist/esm/adapters/browser-agent-adapter.js +219 -0
- package/dist/esm/adapters/browser-agent-adapter.js.map +1 -0
- package/dist/esm/adapters/codex-adapter.js +318 -0
- package/dist/esm/adapters/codex-adapter.js.map +1 -0
- package/dist/esm/adapters/copilot-adapter.js +132 -0
- package/dist/esm/adapters/copilot-adapter.js.map +1 -0
- package/dist/esm/adapters/crewai-adapter.js +148 -0
- package/dist/esm/adapters/crewai-adapter.js.map +1 -0
- package/dist/esm/adapters/custom-adapter.js +142 -0
- package/dist/esm/adapters/custom-adapter.js.map +1 -0
- package/dist/esm/adapters/custom-streaming-adapter.js +181 -0
- package/dist/esm/adapters/custom-streaming-adapter.js.map +1 -0
- package/dist/esm/adapters/dspy-adapter.js +127 -0
- package/dist/esm/adapters/dspy-adapter.js.map +1 -0
- package/dist/esm/adapters/haystack-adapter.js +149 -0
- package/dist/esm/adapters/haystack-adapter.js.map +1 -0
- package/dist/esm/adapters/hermes-adapter.js +217 -0
- package/dist/esm/adapters/hermes-adapter.js.map +1 -0
- package/dist/esm/adapters/index.js +109 -0
- package/dist/esm/adapters/index.js.map +1 -0
- package/dist/esm/adapters/langchain-adapter.js +134 -0
- package/dist/esm/adapters/langchain-adapter.js.map +1 -0
- package/dist/esm/adapters/langchain-streaming-adapter.js +161 -0
- package/dist/esm/adapters/langchain-streaming-adapter.js.map +1 -0
- package/dist/esm/adapters/langgraph-adapter.js +119 -0
- package/dist/esm/adapters/langgraph-adapter.js.map +1 -0
- package/dist/esm/adapters/llamaindex-adapter.js +135 -0
- package/dist/esm/adapters/llamaindex-adapter.js.map +1 -0
- package/dist/esm/adapters/mcp-adapter.js +200 -0
- package/dist/esm/adapters/mcp-adapter.js.map +1 -0
- package/dist/esm/adapters/minimax-adapter.js +233 -0
- package/dist/esm/adapters/minimax-adapter.js.map +1 -0
- package/dist/esm/adapters/nemoclaw-adapter.js +465 -0
- package/dist/esm/adapters/nemoclaw-adapter.js.map +1 -0
- package/dist/esm/adapters/openai-agents-adapter.js +118 -0
- package/dist/esm/adapters/openai-agents-adapter.js.map +1 -0
- package/dist/esm/adapters/openai-assistants-adapter.js +130 -0
- package/dist/esm/adapters/openai-assistants-adapter.js.map +1 -0
- package/dist/esm/adapters/openclaw-adapter.js +107 -0
- package/dist/esm/adapters/openclaw-adapter.js.map +1 -0
- package/dist/esm/adapters/orchestrator-adapter.js +218 -0
- package/dist/esm/adapters/orchestrator-adapter.js.map +1 -0
- package/dist/esm/adapters/pydantic-ai-adapter.js +163 -0
- package/dist/esm/adapters/pydantic-ai-adapter.js.map +1 -0
- package/dist/esm/adapters/rlm-adapter.js +167 -0
- package/dist/esm/adapters/rlm-adapter.js.map +1 -0
- package/dist/esm/adapters/semantic-kernel-adapter.js +123 -0
- package/dist/esm/adapters/semantic-kernel-adapter.js.map +1 -0
- package/dist/esm/adapters/streaming-base-adapter.js +74 -0
- package/dist/esm/adapters/streaming-base-adapter.js.map +1 -0
- package/dist/esm/adapters/vertex-ai-adapter.js +166 -0
- package/dist/esm/adapters/vertex-ai-adapter.js.map +1 -0
- package/dist/esm/demo-control-plane.js +147 -0
- package/dist/esm/demo-control-plane.js.map +1 -0
- package/dist/esm/demo-worktree-dashboard.js +131 -0
- package/dist/esm/demo-worktree-dashboard.js.map +1 -0
- package/dist/esm/examples/01-hello-swarm.js +165 -0
- package/dist/esm/examples/01-hello-swarm.js.map +1 -0
- package/dist/esm/examples/02-fsm-pipeline.js +189 -0
- package/dist/esm/examples/02-fsm-pipeline.js.map +1 -0
- package/dist/esm/examples/03-parallel-agents.js +192 -0
- package/dist/esm/examples/03-parallel-agents.js.map +1 -0
- package/dist/esm/examples/05-code-review-swarm.js +1177 -0
- package/dist/esm/examples/05-code-review-swarm.js.map +1 -0
- package/dist/esm/examples/06-ai-pipeline-demo.js +263 -0
- package/dist/esm/examples/06-ai-pipeline-demo.js.map +1 -0
- package/dist/esm/examples/07-full-showcase.js +946 -0
- package/dist/esm/examples/07-full-showcase.js.map +1 -0
- package/dist/esm/examples/08-control-plane-stress-demo.js +186 -0
- package/dist/esm/examples/08-control-plane-stress-demo.js.map +1 -0
- package/dist/esm/examples/09-real-langchain.js +231 -0
- package/dist/esm/examples/09-real-langchain.js.map +1 -0
- package/dist/esm/examples/10-nemoclaw-sandbox-swarm.js +270 -0
- package/dist/esm/examples/10-nemoclaw-sandbox-swarm.js.map +1 -0
- package/dist/esm/examples/demo-runner.js +119 -0
- package/dist/esm/examples/demo-runner.js.map +1 -0
- package/dist/esm/index.js +1352 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/lib/adapter-hooks.js +216 -0
- package/dist/esm/lib/adapter-hooks.js.map +1 -0
- package/dist/esm/lib/adapter-test-harness.js +118 -0
- package/dist/esm/lib/adapter-test-harness.js.map +1 -0
- package/dist/esm/lib/agent-conversation.js +155 -0
- package/dist/esm/lib/agent-conversation.js.map +1 -0
- package/dist/esm/lib/agent-debate.js +146 -0
- package/dist/esm/lib/agent-debate.js.map +1 -0
- package/dist/esm/lib/agent-memory.js +336 -0
- package/dist/esm/lib/agent-memory.js.map +1 -0
- package/dist/esm/lib/agent-runtime.js +818 -0
- package/dist/esm/lib/agent-runtime.js.map +1 -0
- package/dist/esm/lib/agent-vcr.js +218 -0
- package/dist/esm/lib/agent-vcr.js.map +1 -0
- package/dist/esm/lib/anomaly-detector.js +178 -0
- package/dist/esm/lib/anomaly-detector.js.map +1 -0
- package/dist/esm/lib/approval-inbox.js +385 -0
- package/dist/esm/lib/approval-inbox.js.map +1 -0
- package/dist/esm/lib/auth-guardian.js +692 -0
- package/dist/esm/lib/auth-guardian.js.map +1 -0
- package/dist/esm/lib/auth-validator.js +32 -0
- package/dist/esm/lib/auth-validator.js.map +1 -0
- package/dist/esm/lib/blackboard-backend-crdt.js +251 -0
- package/dist/esm/lib/blackboard-backend-crdt.js.map +1 -0
- package/dist/esm/lib/blackboard-backend-redis.js +244 -0
- package/dist/esm/lib/blackboard-backend-redis.js.map +1 -0
- package/dist/esm/lib/blackboard-backend.js +141 -0
- package/dist/esm/lib/blackboard-backend.js.map +1 -0
- package/dist/esm/lib/blackboard-validator.js +985 -0
- package/dist/esm/lib/blackboard-validator.js.map +1 -0
- package/dist/esm/lib/circuit-breaker.js +164 -0
- package/dist/esm/lib/circuit-breaker.js.map +1 -0
- package/dist/esm/lib/claim-verifier.js +173 -0
- package/dist/esm/lib/claim-verifier.js.map +1 -0
- package/dist/esm/lib/comparison-runner.js +138 -0
- package/dist/esm/lib/comparison-runner.js.map +1 -0
- package/dist/esm/lib/compliance-monitor.js +261 -0
- package/dist/esm/lib/compliance-monitor.js.map +1 -0
- package/dist/esm/lib/confidence-filter.js +210 -0
- package/dist/esm/lib/confidence-filter.js.map +1 -0
- package/dist/esm/lib/config-watcher.js +215 -0
- package/dist/esm/lib/config-watcher.js.map +1 -0
- package/dist/esm/lib/consistency.js +274 -0
- package/dist/esm/lib/consistency.js.map +1 -0
- package/dist/esm/lib/console-ui.js +276 -0
- package/dist/esm/lib/console-ui.js.map +1 -0
- package/dist/esm/lib/context-throttler.js +171 -0
- package/dist/esm/lib/context-throttler.js.map +1 -0
- package/dist/esm/lib/control-plane.js +527 -0
- package/dist/esm/lib/control-plane.js.map +1 -0
- package/dist/esm/lib/cost-governor.js +128 -0
- package/dist/esm/lib/cost-governor.js.map +1 -0
- package/dist/esm/lib/cost-heatmap.js +161 -0
- package/dist/esm/lib/cost-heatmap.js.map +1 -0
- package/dist/esm/lib/coverage-gate.js +213 -0
- package/dist/esm/lib/coverage-gate.js.map +1 -0
- package/dist/esm/lib/coverage-reporter.js +177 -0
- package/dist/esm/lib/coverage-reporter.js.map +1 -0
- package/dist/esm/lib/crdt.js +141 -0
- package/dist/esm/lib/crdt.js.map +1 -0
- package/dist/esm/lib/dashboard-server.js +403 -0
- package/dist/esm/lib/dashboard-server.js.map +1 -0
- package/dist/esm/lib/dry-run.js +130 -0
- package/dist/esm/lib/dry-run.js.map +1 -0
- package/dist/esm/lib/env-manager.js +518 -0
- package/dist/esm/lib/env-manager.js.map +1 -0
- package/dist/esm/lib/errors.js +201 -0
- package/dist/esm/lib/errors.js.map +1 -0
- package/dist/esm/lib/event-bus.js +229 -0
- package/dist/esm/lib/event-bus.js.map +1 -0
- package/dist/esm/lib/explainability.js +102 -0
- package/dist/esm/lib/explainability.js.map +1 -0
- package/dist/esm/lib/fan-out.js +237 -0
- package/dist/esm/lib/fan-out.js.map +1 -0
- package/dist/esm/lib/federated-budget.js +322 -0
- package/dist/esm/lib/federated-budget.js.map +1 -0
- package/dist/esm/lib/fsm-journey.js +478 -0
- package/dist/esm/lib/fsm-journey.js.map +1 -0
- package/dist/esm/lib/goal-decomposer.js +698 -0
- package/dist/esm/lib/goal-decomposer.js.map +1 -0
- package/dist/esm/lib/goal-dsl.js +391 -0
- package/dist/esm/lib/goal-dsl.js.map +1 -0
- package/dist/esm/lib/job-queue.js +310 -0
- package/dist/esm/lib/job-queue.js.map +1 -0
- package/dist/esm/lib/landscape-agent.js +134 -0
- package/dist/esm/lib/landscape-agent.js.map +1 -0
- package/dist/esm/lib/learning-loop.js +181 -0
- package/dist/esm/lib/learning-loop.js.map +1 -0
- package/dist/esm/lib/lifecycle-hooks.js +148 -0
- package/dist/esm/lib/lifecycle-hooks.js.map +1 -0
- package/dist/esm/lib/locked-blackboard.js +1295 -0
- package/dist/esm/lib/locked-blackboard.js.map +1 -0
- package/dist/esm/lib/logger.js +150 -0
- package/dist/esm/lib/logger.js.map +1 -0
- package/dist/esm/lib/mcp-blackboard-tools.js +298 -0
- package/dist/esm/lib/mcp-blackboard-tools.js.map +1 -0
- package/dist/esm/lib/mcp-bridge.js +357 -0
- package/dist/esm/lib/mcp-bridge.js.map +1 -0
- package/dist/esm/lib/mcp-tool-consumer.js +287 -0
- package/dist/esm/lib/mcp-tool-consumer.js.map +1 -0
- package/dist/esm/lib/mcp-tools-control.js +392 -0
- package/dist/esm/lib/mcp-tools-control.js.map +1 -0
- package/dist/esm/lib/mcp-tools-extended.js +371 -0
- package/dist/esm/lib/mcp-tools-extended.js.map +1 -0
- package/dist/esm/lib/mcp-transport-http.js +528 -0
- package/dist/esm/lib/mcp-transport-http.js.map +1 -0
- package/dist/esm/lib/mcp-transport-sse.js +503 -0
- package/dist/esm/lib/mcp-transport-sse.js.map +1 -0
- package/dist/esm/lib/metrics.js +284 -0
- package/dist/esm/lib/metrics.js.map +1 -0
- package/dist/esm/lib/orchestrator-types.js +66 -0
- package/dist/esm/lib/orchestrator-types.js.map +1 -0
- package/dist/esm/lib/otel-bridge.js +167 -0
- package/dist/esm/lib/otel-bridge.js.map +1 -0
- package/dist/esm/lib/partition-planner.js +246 -0
- package/dist/esm/lib/partition-planner.js.map +1 -0
- package/dist/esm/lib/phase-pipeline.js +367 -0
- package/dist/esm/lib/phase-pipeline.js.map +1 -0
- package/dist/esm/lib/playground.js +224 -0
- package/dist/esm/lib/playground.js.map +1 -0
- package/dist/esm/lib/qa-orchestrator.js +296 -0
- package/dist/esm/lib/qa-orchestrator.js.map +1 -0
- package/dist/esm/lib/quadtree.js +259 -0
- package/dist/esm/lib/quadtree.js.map +1 -0
- package/dist/esm/lib/route-classifier.js +217 -0
- package/dist/esm/lib/route-classifier.js.map +1 -0
- package/dist/esm/lib/semantic-search.js +235 -0
- package/dist/esm/lib/semantic-search.js.map +1 -0
- package/dist/esm/lib/shared-blackboard.js +249 -0
- package/dist/esm/lib/shared-blackboard.js.map +1 -0
- package/dist/esm/lib/skill-composer.js +190 -0
- package/dist/esm/lib/skill-composer.js.map +1 -0
- package/dist/esm/lib/speculative-executor.js +107 -0
- package/dist/esm/lib/speculative-executor.js.map +1 -0
- package/dist/esm/lib/strategy-agent.js +626 -0
- package/dist/esm/lib/strategy-agent.js.map +1 -0
- package/dist/esm/lib/swarm-transport.js +307 -0
- package/dist/esm/lib/swarm-transport.js.map +1 -0
- package/dist/esm/lib/swarm-utils.js +510 -0
- package/dist/esm/lib/swarm-utils.js.map +1 -0
- package/dist/esm/lib/task-decomposer.js +272 -0
- package/dist/esm/lib/task-decomposer.js.map +1 -0
- package/dist/esm/lib/telemetry-provider.js +207 -0
- package/dist/esm/lib/telemetry-provider.js.map +1 -0
- package/dist/esm/lib/timeline-scrubber.js +173 -0
- package/dist/esm/lib/timeline-scrubber.js.map +1 -0
- package/dist/esm/lib/topology.js +591 -0
- package/dist/esm/lib/topology.js.map +1 -0
- package/dist/esm/lib/transport-agent.js +366 -0
- package/dist/esm/lib/transport-agent.js.map +1 -0
- package/dist/esm/lib/work-tree-dashboard.js +583 -0
- package/dist/esm/lib/work-tree-dashboard.js.map +1 -0
- package/dist/esm/lib/work-tree-ui.js +333 -0
- package/dist/esm/lib/work-tree-ui.js.map +1 -0
- package/dist/esm/lib/work-tree.js +480 -0
- package/dist/esm/lib/work-tree.js.map +1 -0
- package/dist/esm/run.js +144 -0
- package/dist/esm/run.js.map +1 -0
- package/dist/esm/security.js +1122 -0
- package/dist/esm/security.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -1
- package/dist/index.js.map +1 -1
- package/dist/lib/mcp-transport-http.d.ts +203 -0
- package/dist/lib/mcp-transport-http.d.ts.map +1 -0
- package/dist/lib/mcp-transport-http.js +528 -0
- package/dist/lib/mcp-transport-http.js.map +1 -0
- package/dist/lib/phase-pipeline.d.ts +31 -0
- package/dist/lib/phase-pipeline.d.ts.map +1 -1
- package/dist/lib/phase-pipeline.js +93 -1
- package/dist/lib/phase-pipeline.js.map +1 -1
- package/dist/lib/semantic-search.d.ts +42 -6
- package/dist/lib/semantic-search.d.ts.map +1 -1
- package/dist/lib/semantic-search.js +87 -6
- package/dist/lib/semantic-search.js.map +1 -1
- package/package.json +24 -4
|
@@ -0,0 +1,818 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Agent Runtime — Sandboxed execution environment for AI agents
|
|
4
|
+
*
|
|
5
|
+
* Provides controlled access to shell commands, file system, and system
|
|
6
|
+
* resources with policy enforcement, approval gates, and audit logging.
|
|
7
|
+
*
|
|
8
|
+
* Components:
|
|
9
|
+
* - SandboxPolicy — defines what agents are allowed to do
|
|
10
|
+
* - ShellExecutor — spawns child processes within policy constraints
|
|
11
|
+
* - FileAccessor — scoped file read/write with traversal protection
|
|
12
|
+
* - ApprovalGate — human-in-the-loop approval for sensitive operations
|
|
13
|
+
* - AgentRuntime — unified facade combining all components
|
|
14
|
+
*
|
|
15
|
+
* @module AgentRuntime
|
|
16
|
+
* @version 1.0.0
|
|
17
|
+
*/
|
|
18
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
19
|
+
exports.RuntimeExecutionError = exports.RuntimeApprovalError = exports.RuntimePolicyError = exports.AgentRuntime = exports.ApprovalGate = exports.FileAccessor = exports.ShellExecutor = exports.SandboxPolicy = exports.SourceProtectionError = void 0;
|
|
20
|
+
const child_process_1 = require("child_process");
|
|
21
|
+
const crypto_1 = require("crypto");
|
|
22
|
+
const promises_1 = require("fs/promises");
|
|
23
|
+
const path_1 = require("path");
|
|
24
|
+
const events_1 = require("events");
|
|
25
|
+
const security_1 = require("../security");
|
|
26
|
+
// ============================================================================
|
|
27
|
+
// ERRORS
|
|
28
|
+
// ============================================================================
|
|
29
|
+
/**
|
|
30
|
+
* Thrown when an agent attempts to access source code files or directories
|
|
31
|
+
* while `sourceProtection` is enabled on the sandbox policy.
|
|
32
|
+
*/
|
|
33
|
+
class SourceProtectionError extends Error {
|
|
34
|
+
/** The path that was blocked */
|
|
35
|
+
blockedPath;
|
|
36
|
+
/** The agent that attempted access */
|
|
37
|
+
agentId;
|
|
38
|
+
constructor(blockedPath, agentId) {
|
|
39
|
+
super(`Source protection: access to '${blockedPath}' is blocked for agent '${agentId}'`);
|
|
40
|
+
this.name = 'SourceProtectionError';
|
|
41
|
+
this.blockedPath = blockedPath;
|
|
42
|
+
this.agentId = agentId;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
exports.SourceProtectionError = SourceProtectionError;
|
|
46
|
+
// ============================================================================
|
|
47
|
+
// COMMAND PARSING
|
|
48
|
+
// ============================================================================
|
|
49
|
+
/**
|
|
50
|
+
* Shell metacharacters that enable command chaining, substitution, or
|
|
51
|
+
* redirection. Any of these appearing OUTSIDE of quotes turns a scoped
|
|
52
|
+
* allowlist entry (e.g. `git *`) into arbitrary execution and must be
|
|
53
|
+
* rejected (GHSA-qw6v-5fcf-5666).
|
|
54
|
+
*/
|
|
55
|
+
const SHELL_METACHARACTERS = new Set([
|
|
56
|
+
';', '&', '|', '$', '`', '(', ')', '<', '>', '{', '}', '\n', '\r',
|
|
57
|
+
]);
|
|
58
|
+
/**
|
|
59
|
+
* Tokenize a command string into an argv array, honoring single and double
|
|
60
|
+
* quotes, and report whether any shell metacharacter appears OUTSIDE of quotes.
|
|
61
|
+
*
|
|
62
|
+
* Commands are executed with `shell: false`, so no shell ever interprets the
|
|
63
|
+
* result — quoted metacharacters are therefore safe, literal data. Unquoted
|
|
64
|
+
* metacharacters indicate an attempt to chain, substitute, or redirect commands
|
|
65
|
+
* and cause the command to be rejected before it can run.
|
|
66
|
+
*
|
|
67
|
+
* @param command - Raw command string to parse.
|
|
68
|
+
* @returns Parsed argv plus flags for unquoted metacharacters and malformed quoting.
|
|
69
|
+
*/
|
|
70
|
+
function parseCommandLine(command) {
|
|
71
|
+
const argv = [];
|
|
72
|
+
let current = '';
|
|
73
|
+
let tokenOpen = false;
|
|
74
|
+
let quote = null;
|
|
75
|
+
let hasUnquotedMetacharacter = false;
|
|
76
|
+
for (let i = 0; i < command.length; i++) {
|
|
77
|
+
const ch = command[i];
|
|
78
|
+
if (quote) {
|
|
79
|
+
if (ch === quote) {
|
|
80
|
+
quote = null;
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
current += ch;
|
|
84
|
+
}
|
|
85
|
+
tokenOpen = true;
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
if (ch === '"' || ch === "'") {
|
|
89
|
+
quote = ch;
|
|
90
|
+
tokenOpen = true;
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
if (ch === ' ' || ch === '\t') {
|
|
94
|
+
if (tokenOpen) {
|
|
95
|
+
argv.push(current);
|
|
96
|
+
current = '';
|
|
97
|
+
tokenOpen = false;
|
|
98
|
+
}
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
if (SHELL_METACHARACTERS.has(ch)) {
|
|
102
|
+
// Unquoted metacharacter — reject the whole command. Do not include it
|
|
103
|
+
// in argv so the result can never be silently executed.
|
|
104
|
+
hasUnquotedMetacharacter = true;
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
current += ch;
|
|
108
|
+
tokenOpen = true;
|
|
109
|
+
}
|
|
110
|
+
if (tokenOpen)
|
|
111
|
+
argv.push(current);
|
|
112
|
+
return { argv, hasUnquotedMetacharacter, unterminatedQuote: quote !== null };
|
|
113
|
+
}
|
|
114
|
+
// ============================================================================
|
|
115
|
+
// SANDBOX POLICY
|
|
116
|
+
// ============================================================================
|
|
117
|
+
const DEFAULT_POLICY = {
|
|
118
|
+
allowedCommands: [],
|
|
119
|
+
blockedCommands: [
|
|
120
|
+
'rm -rf /',
|
|
121
|
+
'rm -rf /*',
|
|
122
|
+
'rmdir /s /q C:\\',
|
|
123
|
+
'format *',
|
|
124
|
+
'mkfs*',
|
|
125
|
+
'dd if=*',
|
|
126
|
+
':(){:|:&};:', // fork bomb
|
|
127
|
+
'shutdown*',
|
|
128
|
+
'reboot*',
|
|
129
|
+
'halt*',
|
|
130
|
+
'init 0',
|
|
131
|
+
'init 6',
|
|
132
|
+
'del /f /s /q C:\\*',
|
|
133
|
+
'reg delete*',
|
|
134
|
+
'net user*',
|
|
135
|
+
'net localgroup*',
|
|
136
|
+
],
|
|
137
|
+
allowedPaths: ['.'],
|
|
138
|
+
blockedPaths: [],
|
|
139
|
+
maxConcurrentProcesses: 5,
|
|
140
|
+
defaultTimeoutMs: 30_000,
|
|
141
|
+
defaultMaxOutputBytes: 1_048_576,
|
|
142
|
+
approvalRequired: [
|
|
143
|
+
'rm *',
|
|
144
|
+
'del *',
|
|
145
|
+
'rmdir *',
|
|
146
|
+
'git push*',
|
|
147
|
+
'git reset --hard*',
|
|
148
|
+
'npm publish*',
|
|
149
|
+
'docker *',
|
|
150
|
+
'kubectl *',
|
|
151
|
+
],
|
|
152
|
+
autoApproveReads: true,
|
|
153
|
+
};
|
|
154
|
+
/**
|
|
155
|
+
* Sandbox policy engine — determines what agents are allowed to do.
|
|
156
|
+
*
|
|
157
|
+
* @example
|
|
158
|
+
* ```typescript
|
|
159
|
+
* const policy = new SandboxPolicy({
|
|
160
|
+
* basePath: '/project',
|
|
161
|
+
* allowedCommands: ['npm *', 'node *', 'git status'],
|
|
162
|
+
* });
|
|
163
|
+
* policy.isCommandAllowed('npm test'); // true
|
|
164
|
+
* policy.isCommandAllowed('rm -rf /'); // false
|
|
165
|
+
* ```
|
|
166
|
+
*/
|
|
167
|
+
class SandboxPolicy {
|
|
168
|
+
config;
|
|
169
|
+
constructor(config) {
|
|
170
|
+
this.config = { ...DEFAULT_POLICY, ...config };
|
|
171
|
+
this.config.basePath = (0, path_1.resolve)(this.config.basePath);
|
|
172
|
+
}
|
|
173
|
+
/** Check if a command matches the policy's allowed list and isn't blocked */
|
|
174
|
+
isCommandAllowed(command) {
|
|
175
|
+
const trimmed = command.trim();
|
|
176
|
+
if (!trimmed)
|
|
177
|
+
return false;
|
|
178
|
+
// Reject shell metacharacters and malformed quoting BEFORE pattern matching.
|
|
179
|
+
// Commands run with `shell: false`, but a scoped allowlist entry such as
|
|
180
|
+
// `git *` must never be escapable into arbitrary execution via `;`, `|`,
|
|
181
|
+
// `&&`, `$(...)`, backticks, redirection, or newlines (GHSA-qw6v-5fcf-5666).
|
|
182
|
+
const parsed = parseCommandLine(trimmed);
|
|
183
|
+
if (parsed.hasUnquotedMetacharacter || parsed.unterminatedQuote || parsed.argv.length === 0) {
|
|
184
|
+
return false;
|
|
185
|
+
}
|
|
186
|
+
// Check blocked first (always wins)
|
|
187
|
+
if (this.matchesAny(trimmed, this.config.blockedCommands))
|
|
188
|
+
return false;
|
|
189
|
+
// If no allowedCommands, deny all
|
|
190
|
+
if (this.config.allowedCommands.length === 0)
|
|
191
|
+
return false;
|
|
192
|
+
// Check allowed
|
|
193
|
+
return this.matchesAny(trimmed, this.config.allowedCommands);
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Tokenize a command into an argv array suitable for `shell: false`
|
|
197
|
+
* execution. Returns `null` for any command that contains unquoted shell
|
|
198
|
+
* metacharacters or malformed quoting — i.e. exactly the inputs that
|
|
199
|
+
* {@link isCommandAllowed} rejects. Quoted metacharacters are preserved as
|
|
200
|
+
* literal data within their token.
|
|
201
|
+
*
|
|
202
|
+
* @param command - Raw command string to tokenize.
|
|
203
|
+
* @returns The argv array, or `null` if the command is unsafe to execute.
|
|
204
|
+
*/
|
|
205
|
+
tokenizeCommand(command) {
|
|
206
|
+
const parsed = parseCommandLine(command.trim());
|
|
207
|
+
if (parsed.hasUnquotedMetacharacter || parsed.unterminatedQuote || parsed.argv.length === 0) {
|
|
208
|
+
return null;
|
|
209
|
+
}
|
|
210
|
+
return parsed.argv;
|
|
211
|
+
}
|
|
212
|
+
/** Check if a command requires human approval */
|
|
213
|
+
requiresApproval(command) {
|
|
214
|
+
return this.matchesAny(command.trim(), this.config.approvalRequired);
|
|
215
|
+
}
|
|
216
|
+
/** Assess risk level of a command */
|
|
217
|
+
assessRisk(command) {
|
|
218
|
+
const trimmed = command.trim().toLowerCase();
|
|
219
|
+
const highRisk = ['rm ', 'del ', 'format', 'drop ', 'delete ', 'truncate ', 'git push', 'git reset', 'docker', 'kubectl'];
|
|
220
|
+
const medRisk = ['git ', 'npm ', 'pip ', 'mv ', 'move ', 'cp ', 'copy ', 'chmod ', 'chown '];
|
|
221
|
+
if (highRisk.some(p => trimmed.startsWith(p) || trimmed.includes(' ' + p)))
|
|
222
|
+
return 'high';
|
|
223
|
+
if (medRisk.some(p => trimmed.startsWith(p) || trimmed.includes(' ' + p)))
|
|
224
|
+
return 'medium';
|
|
225
|
+
return 'low';
|
|
226
|
+
}
|
|
227
|
+
/** Validate that a file path is within allowed scope */
|
|
228
|
+
isPathAllowed(filePath) {
|
|
229
|
+
const normalized = this.resolvePath(filePath);
|
|
230
|
+
if (!normalized)
|
|
231
|
+
return false;
|
|
232
|
+
// Check blocked paths
|
|
233
|
+
for (const blocked of this.config.blockedPaths) {
|
|
234
|
+
const blockedAbs = (0, path_1.resolve)(this.config.basePath, blocked);
|
|
235
|
+
if (normalized.startsWith(blockedAbs))
|
|
236
|
+
return false;
|
|
237
|
+
}
|
|
238
|
+
// Check allowed paths
|
|
239
|
+
for (const allowed of this.config.allowedPaths) {
|
|
240
|
+
const allowedAbs = (0, path_1.resolve)(this.config.basePath, allowed);
|
|
241
|
+
if (normalized.startsWith(allowedAbs))
|
|
242
|
+
return true;
|
|
243
|
+
}
|
|
244
|
+
return false;
|
|
245
|
+
}
|
|
246
|
+
/** Resolve and validate a path against basePath (returns null if traversal detected) */
|
|
247
|
+
resolvePath(filePath) {
|
|
248
|
+
const normalized = (0, path_1.normalize)(filePath);
|
|
249
|
+
const absolute = (0, path_1.isAbsolute)(normalized)
|
|
250
|
+
? normalized
|
|
251
|
+
: (0, path_1.join)(this.config.basePath, normalized);
|
|
252
|
+
const resolved = (0, path_1.resolve)(absolute);
|
|
253
|
+
// Traversal check
|
|
254
|
+
if (!resolved.startsWith(this.config.basePath))
|
|
255
|
+
return null;
|
|
256
|
+
return resolved;
|
|
257
|
+
}
|
|
258
|
+
/** Get a copy of the current policy config */
|
|
259
|
+
getConfig() {
|
|
260
|
+
return { ...this.config };
|
|
261
|
+
}
|
|
262
|
+
/** Update allowed commands */
|
|
263
|
+
allowCommand(pattern) {
|
|
264
|
+
if (!this.config.allowedCommands.includes(pattern)) {
|
|
265
|
+
this.config.allowedCommands.push(pattern);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
/** Remove an allowed command pattern */
|
|
269
|
+
disallowCommand(pattern) {
|
|
270
|
+
this.config.allowedCommands = this.config.allowedCommands.filter(c => c !== pattern);
|
|
271
|
+
}
|
|
272
|
+
/** Update allowed paths */
|
|
273
|
+
allowPath(path) {
|
|
274
|
+
if (!this.config.allowedPaths.includes(path)) {
|
|
275
|
+
this.config.allowedPaths.push(path);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
/** Block a path */
|
|
279
|
+
blockPath(path) {
|
|
280
|
+
if (!this.config.blockedPaths.includes(path)) {
|
|
281
|
+
this.config.blockedPaths.push(path);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
get basePath() { return this.config.basePath; }
|
|
285
|
+
get maxConcurrentProcesses() { return this.config.maxConcurrentProcesses; }
|
|
286
|
+
get defaultTimeoutMs() { return this.config.defaultTimeoutMs; }
|
|
287
|
+
get defaultMaxOutputBytes() { return this.config.defaultMaxOutputBytes; }
|
|
288
|
+
get autoApproveReads() { return this.config.autoApproveReads; }
|
|
289
|
+
/** Simple glob matching: supports * as wildcard */
|
|
290
|
+
matchesAny(value, patterns) {
|
|
291
|
+
return patterns.some(pattern => this.globMatch(pattern, value));
|
|
292
|
+
}
|
|
293
|
+
/** Match a simple glob pattern against a string */
|
|
294
|
+
globMatch(pattern, value) {
|
|
295
|
+
// Escape regex special chars except *
|
|
296
|
+
const regexStr = pattern
|
|
297
|
+
.replace(/[.+^${}()|[\]\\]/g, '\\$&')
|
|
298
|
+
.replace(/\*/g, '.*');
|
|
299
|
+
return new RegExp(`^${regexStr}$`, 'i').test(value);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
exports.SandboxPolicy = SandboxPolicy;
|
|
303
|
+
// ============================================================================
|
|
304
|
+
// SHELL EXECUTOR
|
|
305
|
+
// ============================================================================
|
|
306
|
+
/**
|
|
307
|
+
* Sandboxed shell command executor with timeout, output limits, and
|
|
308
|
+
* concurrent process tracking.
|
|
309
|
+
*
|
|
310
|
+
* @example
|
|
311
|
+
* ```typescript
|
|
312
|
+
* const executor = new ShellExecutor(policy);
|
|
313
|
+
* const result = await executor.execute('npm test', { agentId: 'tester' });
|
|
314
|
+
* console.log(result.exitCode, result.stdout);
|
|
315
|
+
* ```
|
|
316
|
+
*/
|
|
317
|
+
class ShellExecutor {
|
|
318
|
+
policy;
|
|
319
|
+
activeProcesses = 0;
|
|
320
|
+
constructor(policy) {
|
|
321
|
+
this.policy = policy;
|
|
322
|
+
}
|
|
323
|
+
/** Execute a shell command within policy constraints */
|
|
324
|
+
async execute(command, opts = {}) {
|
|
325
|
+
// Policy check
|
|
326
|
+
if (!this.policy.isCommandAllowed(command)) {
|
|
327
|
+
throw new RuntimePolicyError(`Command blocked by policy: ${command}`);
|
|
328
|
+
}
|
|
329
|
+
// Concurrency check
|
|
330
|
+
if (this.activeProcesses >= this.policy.maxConcurrentProcesses) {
|
|
331
|
+
throw new RuntimePolicyError(`Concurrency limit reached (${this.policy.maxConcurrentProcesses} max)`);
|
|
332
|
+
}
|
|
333
|
+
const cwd = opts.cwd
|
|
334
|
+
? (this.policy.resolvePath(opts.cwd) ?? this.policy.basePath)
|
|
335
|
+
: this.policy.basePath;
|
|
336
|
+
const timeoutMs = opts.timeoutMs ?? this.policy.defaultTimeoutMs;
|
|
337
|
+
const maxBytes = opts.maxOutputBytes ?? this.policy.defaultMaxOutputBytes;
|
|
338
|
+
this.activeProcesses++;
|
|
339
|
+
try {
|
|
340
|
+
return await this.spawnCommand(command, cwd, timeoutMs, maxBytes, opts.env);
|
|
341
|
+
}
|
|
342
|
+
finally {
|
|
343
|
+
this.activeProcesses--;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
/** Get the number of currently running processes */
|
|
347
|
+
get running() { return this.activeProcesses; }
|
|
348
|
+
spawnCommand(command, cwd, timeoutMs, maxBytes, env) {
|
|
349
|
+
return new Promise((resolvePromise, reject) => {
|
|
350
|
+
// Execute with `shell: false` using a parsed argv. The command was already
|
|
351
|
+
// validated by `isCommandAllowed`, but we re-tokenize here as the single
|
|
352
|
+
// source of truth for what actually runs — no string is ever handed to a
|
|
353
|
+
// shell, so metacharacters cannot be interpreted (GHSA-qw6v-5fcf-5666).
|
|
354
|
+
const argv = this.policy.tokenizeCommand(command);
|
|
355
|
+
if (!argv || argv.length === 0) {
|
|
356
|
+
reject(new RuntimePolicyError(`Command rejected — shell metacharacters are not permitted: ${command}`));
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
const file = argv[0];
|
|
360
|
+
const args = argv.slice(1);
|
|
361
|
+
const child = (0, child_process_1.spawn)(file, args, {
|
|
362
|
+
cwd,
|
|
363
|
+
env: env ? { ...process.env, ...env } : process.env,
|
|
364
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
365
|
+
windowsHide: true,
|
|
366
|
+
shell: false,
|
|
367
|
+
});
|
|
368
|
+
let stdout = '';
|
|
369
|
+
let stderr = '';
|
|
370
|
+
let totalBytes = 0;
|
|
371
|
+
let timedOut = false;
|
|
372
|
+
let truncated = false;
|
|
373
|
+
const timer = setTimeout(() => {
|
|
374
|
+
timedOut = true;
|
|
375
|
+
child.kill('SIGTERM');
|
|
376
|
+
setTimeout(() => { if (!child.killed)
|
|
377
|
+
child.kill('SIGKILL'); }, 2000);
|
|
378
|
+
}, timeoutMs);
|
|
379
|
+
child.stdout?.on('data', (chunk) => {
|
|
380
|
+
totalBytes += chunk.length;
|
|
381
|
+
if (totalBytes <= maxBytes) {
|
|
382
|
+
stdout += chunk.toString();
|
|
383
|
+
}
|
|
384
|
+
else if (!truncated) {
|
|
385
|
+
truncated = true;
|
|
386
|
+
stdout += '\n[OUTPUT TRUNCATED — exceeded limit]';
|
|
387
|
+
child.kill('SIGTERM');
|
|
388
|
+
}
|
|
389
|
+
});
|
|
390
|
+
child.stderr?.on('data', (chunk) => {
|
|
391
|
+
totalBytes += chunk.length;
|
|
392
|
+
if (totalBytes <= maxBytes) {
|
|
393
|
+
stderr += chunk.toString();
|
|
394
|
+
}
|
|
395
|
+
});
|
|
396
|
+
child.on('close', (code) => {
|
|
397
|
+
clearTimeout(timer);
|
|
398
|
+
resolvePromise({
|
|
399
|
+
exitCode: code ?? (timedOut ? 124 : 1),
|
|
400
|
+
stdout,
|
|
401
|
+
stderr,
|
|
402
|
+
durationMs: Date.now() - startTime,
|
|
403
|
+
timedOut,
|
|
404
|
+
truncated,
|
|
405
|
+
});
|
|
406
|
+
});
|
|
407
|
+
child.on('error', (err) => {
|
|
408
|
+
clearTimeout(timer);
|
|
409
|
+
reject(new RuntimeExecutionError(`Failed to spawn: ${err.message}`));
|
|
410
|
+
});
|
|
411
|
+
const startTime = Date.now();
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
exports.ShellExecutor = ShellExecutor;
|
|
416
|
+
// ============================================================================
|
|
417
|
+
// FILE ACCESSOR
|
|
418
|
+
// ============================================================================
|
|
419
|
+
/**
|
|
420
|
+
* Policy-scoped file system accessor. All paths are validated against
|
|
421
|
+
* the sandbox policy before any I/O.
|
|
422
|
+
*
|
|
423
|
+
* **Error contract:** All public methods (`read`, `write`, `list`) return a
|
|
424
|
+
* `{success: boolean, ...}` result object — they never throw. All access-denied
|
|
425
|
+
* paths (path traversal, out-of-scope under `sourceProtection`, policy-blocked
|
|
426
|
+
* reads/writes, and `SourceProtectionError`) are caught at the method boundary
|
|
427
|
+
* and converted to `{success: false, error: <message>}`. The `error` field
|
|
428
|
+
* contains a short description without leaking internal path details.
|
|
429
|
+
*
|
|
430
|
+
* @example
|
|
431
|
+
* ```typescript
|
|
432
|
+
* const files = new FileAccessor(policy);
|
|
433
|
+
* const result = await files.read('src/index.ts', 'reader-agent');
|
|
434
|
+
* ```
|
|
435
|
+
*/
|
|
436
|
+
class FileAccessor {
|
|
437
|
+
policy;
|
|
438
|
+
constructor(policy) {
|
|
439
|
+
this.policy = policy;
|
|
440
|
+
}
|
|
441
|
+
/**
|
|
442
|
+
* Checks whether the resolved absolute path is blocked by source protection.
|
|
443
|
+
* Throws SourceProtectionError if access is denied.
|
|
444
|
+
*/
|
|
445
|
+
checkSourceProtection(resolvedPath, agentId) {
|
|
446
|
+
const cfg = this.policy.getConfig();
|
|
447
|
+
if (!cfg.sourceProtection)
|
|
448
|
+
return;
|
|
449
|
+
const base = cfg.basePath;
|
|
450
|
+
const env = cfg.env ?? '';
|
|
451
|
+
// The only permitted subtree under source protection is data/<env>/ (when env
|
|
452
|
+
// is set) or data/ (legacy). All source code paths are blocked.
|
|
453
|
+
const dataEnvDir = env
|
|
454
|
+
? (0, path_1.resolve)(base, 'data', env)
|
|
455
|
+
: (0, path_1.resolve)(base, 'data');
|
|
456
|
+
if (resolvedPath.startsWith(dataEnvDir + require('path').sep) || resolvedPath === dataEnvDir) {
|
|
457
|
+
return; // allowed
|
|
458
|
+
}
|
|
459
|
+
throw new SourceProtectionError(resolvedPath, agentId);
|
|
460
|
+
}
|
|
461
|
+
/** Read a file within the sandbox scope */
|
|
462
|
+
async read(filePath, agentId) {
|
|
463
|
+
const start = Date.now();
|
|
464
|
+
const resolved = this.policy.resolvePath(filePath);
|
|
465
|
+
if (!resolved) {
|
|
466
|
+
return { success: false, path: filePath, error: 'Path traversal blocked', durationMs: Date.now() - start };
|
|
467
|
+
}
|
|
468
|
+
if (!this.policy.isPathAllowed(filePath)) {
|
|
469
|
+
return { success: false, path: filePath, error: 'Path not in allowed scope', durationMs: Date.now() - start };
|
|
470
|
+
}
|
|
471
|
+
try {
|
|
472
|
+
this.checkSourceProtection(resolved, agentId);
|
|
473
|
+
}
|
|
474
|
+
catch (err) {
|
|
475
|
+
if (err instanceof SourceProtectionError) {
|
|
476
|
+
return { success: false, path: resolved, error: err.message, durationMs: Date.now() - start };
|
|
477
|
+
}
|
|
478
|
+
throw err;
|
|
479
|
+
}
|
|
480
|
+
try {
|
|
481
|
+
const content = await (0, promises_1.readFile)(resolved, 'utf-8');
|
|
482
|
+
return { success: true, path: resolved, content, durationMs: Date.now() - start };
|
|
483
|
+
}
|
|
484
|
+
catch (err) {
|
|
485
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
486
|
+
return { success: false, path: resolved, error: msg, durationMs: Date.now() - start };
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
/** Write a file within the sandbox scope */
|
|
490
|
+
async write(filePath, content, agentId) {
|
|
491
|
+
const start = Date.now();
|
|
492
|
+
const resolved = this.policy.resolvePath(filePath);
|
|
493
|
+
if (!resolved) {
|
|
494
|
+
return { success: false, path: filePath, error: 'Path traversal blocked', durationMs: Date.now() - start };
|
|
495
|
+
}
|
|
496
|
+
if (!this.policy.isPathAllowed(filePath)) {
|
|
497
|
+
return { success: false, path: filePath, error: 'Path not in allowed scope', durationMs: Date.now() - start };
|
|
498
|
+
}
|
|
499
|
+
try {
|
|
500
|
+
this.checkSourceProtection(resolved, agentId);
|
|
501
|
+
}
|
|
502
|
+
catch (err) {
|
|
503
|
+
if (err instanceof SourceProtectionError) {
|
|
504
|
+
return { success: false, path: resolved, error: err.message, durationMs: Date.now() - start };
|
|
505
|
+
}
|
|
506
|
+
throw err;
|
|
507
|
+
}
|
|
508
|
+
try {
|
|
509
|
+
await (0, promises_1.mkdir)((0, path_1.dirname)(resolved), { recursive: true });
|
|
510
|
+
await (0, promises_1.writeFile)(resolved, content, 'utf-8');
|
|
511
|
+
return { success: true, path: resolved, durationMs: Date.now() - start };
|
|
512
|
+
}
|
|
513
|
+
catch (err) {
|
|
514
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
515
|
+
return { success: false, path: resolved, error: msg, durationMs: Date.now() - start };
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
/** List directory contents within the sandbox scope */
|
|
519
|
+
async list(dirPath, agentId) {
|
|
520
|
+
const start = Date.now();
|
|
521
|
+
const resolved = this.policy.resolvePath(dirPath);
|
|
522
|
+
if (!resolved) {
|
|
523
|
+
return { success: false, path: dirPath, error: 'Path traversal blocked', durationMs: Date.now() - start };
|
|
524
|
+
}
|
|
525
|
+
if (!this.policy.isPathAllowed(dirPath)) {
|
|
526
|
+
return { success: false, path: dirPath, error: 'Path not in allowed scope', durationMs: Date.now() - start };
|
|
527
|
+
}
|
|
528
|
+
try {
|
|
529
|
+
this.checkSourceProtection(resolved, agentId);
|
|
530
|
+
}
|
|
531
|
+
catch (err) {
|
|
532
|
+
if (err instanceof SourceProtectionError) {
|
|
533
|
+
return { success: false, path: resolved, error: err.message, durationMs: Date.now() - start };
|
|
534
|
+
}
|
|
535
|
+
throw err;
|
|
536
|
+
}
|
|
537
|
+
try {
|
|
538
|
+
const items = await (0, promises_1.readdir)(resolved);
|
|
539
|
+
const entries = [];
|
|
540
|
+
for (const item of items) {
|
|
541
|
+
const itemPath = (0, path_1.join)(resolved, item);
|
|
542
|
+
const info = await (0, promises_1.stat)(itemPath);
|
|
543
|
+
entries.push(info.isDirectory() ? `${item}/` : item);
|
|
544
|
+
}
|
|
545
|
+
return { success: true, path: resolved, entries, durationMs: Date.now() - start };
|
|
546
|
+
}
|
|
547
|
+
catch (err) {
|
|
548
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
549
|
+
return { success: false, path: resolved, error: msg, durationMs: Date.now() - start };
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
exports.FileAccessor = FileAccessor;
|
|
554
|
+
// ============================================================================
|
|
555
|
+
// APPROVAL GATE
|
|
556
|
+
// ============================================================================
|
|
557
|
+
/**
|
|
558
|
+
* Human-in-the-loop approval gate. Queues requests and waits for
|
|
559
|
+
* a decision from the configured callback.
|
|
560
|
+
*
|
|
561
|
+
* @example
|
|
562
|
+
* ```typescript
|
|
563
|
+
* const gate = new ApprovalGate(async (req) => {
|
|
564
|
+
* return { approved: true, approvedBy: 'operator' };
|
|
565
|
+
* });
|
|
566
|
+
* const decision = await gate.request({ type: 'shell', target: 'npm publish', ... });
|
|
567
|
+
* ```
|
|
568
|
+
*/
|
|
569
|
+
class ApprovalGate extends events_1.EventEmitter {
|
|
570
|
+
callback;
|
|
571
|
+
autoApproveAll;
|
|
572
|
+
history = [];
|
|
573
|
+
constructor(callback, autoApproveAll = false) {
|
|
574
|
+
super();
|
|
575
|
+
this.callback = callback ?? null;
|
|
576
|
+
this.autoApproveAll = autoApproveAll;
|
|
577
|
+
}
|
|
578
|
+
/** Request approval for an operation */
|
|
579
|
+
async request(req) {
|
|
580
|
+
this.emit('requested', req);
|
|
581
|
+
if (this.autoApproveAll) {
|
|
582
|
+
const decision = { approved: true, approvedBy: 'auto-approve-all' };
|
|
583
|
+
this.history.push({ request: req, decision });
|
|
584
|
+
this.emit('decided', req, decision);
|
|
585
|
+
return decision;
|
|
586
|
+
}
|
|
587
|
+
if (!this.callback) {
|
|
588
|
+
const decision = { approved: false, reason: 'No approval callback configured' };
|
|
589
|
+
this.history.push({ request: req, decision });
|
|
590
|
+
this.emit('decided', req, decision);
|
|
591
|
+
return decision;
|
|
592
|
+
}
|
|
593
|
+
const decision = await this.callback(req);
|
|
594
|
+
this.history.push({ request: req, decision });
|
|
595
|
+
this.emit('decided', req, decision);
|
|
596
|
+
return decision;
|
|
597
|
+
}
|
|
598
|
+
/** Get approval history */
|
|
599
|
+
getHistory() {
|
|
600
|
+
return this.history;
|
|
601
|
+
}
|
|
602
|
+
/** Get count of approvals/denials */
|
|
603
|
+
getStats() {
|
|
604
|
+
const approved = this.history.filter(h => h.decision.approved).length;
|
|
605
|
+
return { total: this.history.length, approved, denied: this.history.length - approved };
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
exports.ApprovalGate = ApprovalGate;
|
|
609
|
+
// ============================================================================
|
|
610
|
+
// AGENT RUNTIME (Facade)
|
|
611
|
+
// ============================================================================
|
|
612
|
+
/**
|
|
613
|
+
* Unified agent execution runtime. Combines policy, shell, file access,
|
|
614
|
+
* and approval into a single interface for agent consumption.
|
|
615
|
+
*
|
|
616
|
+
* @example
|
|
617
|
+
* ```typescript
|
|
618
|
+
* const runtime = new AgentRuntime({
|
|
619
|
+
* policy: {
|
|
620
|
+
* basePath: '/project',
|
|
621
|
+
* allowedCommands: ['npm *', 'node *', 'git status', 'git diff'],
|
|
622
|
+
* },
|
|
623
|
+
* onApproval: async (req) => {
|
|
624
|
+
* console.log(`Approve? ${req.type}: ${req.target}`);
|
|
625
|
+
* return { approved: true, approvedBy: 'operator' };
|
|
626
|
+
* },
|
|
627
|
+
* });
|
|
628
|
+
*
|
|
629
|
+
* const result = await runtime.exec('npm test', 'tester-agent');
|
|
630
|
+
* ```
|
|
631
|
+
*/
|
|
632
|
+
class AgentRuntime extends events_1.EventEmitter {
|
|
633
|
+
policy;
|
|
634
|
+
shell;
|
|
635
|
+
files;
|
|
636
|
+
gate;
|
|
637
|
+
auditLog = [];
|
|
638
|
+
receiptManager = new security_1.SecureTokenManager();
|
|
639
|
+
constructor(opts) {
|
|
640
|
+
super();
|
|
641
|
+
this.policy = new SandboxPolicy(opts.policy);
|
|
642
|
+
this.shell = new ShellExecutor(this.policy);
|
|
643
|
+
this.files = new FileAccessor(this.policy);
|
|
644
|
+
this.gate = new ApprovalGate(opts.onApproval, opts.autoApproveAll);
|
|
645
|
+
// Wire approval events
|
|
646
|
+
this.gate.on('requested', (req) => {
|
|
647
|
+
this.emit('approval:requested', req);
|
|
648
|
+
this.audit({ action: 'approval_requested', agentId: req.agentId, target: req.target, result: 'success' });
|
|
649
|
+
});
|
|
650
|
+
this.gate.on('decided', (req, dec) => {
|
|
651
|
+
this.emit('approval:decided', req, dec);
|
|
652
|
+
this.audit({
|
|
653
|
+
action: dec.approved ? 'approval_granted' : 'approval_denied',
|
|
654
|
+
agentId: req.agentId,
|
|
655
|
+
target: req.target,
|
|
656
|
+
result: dec.approved ? 'success' : 'denied',
|
|
657
|
+
details: { approvedBy: dec.approvedBy, reason: dec.reason },
|
|
658
|
+
});
|
|
659
|
+
});
|
|
660
|
+
}
|
|
661
|
+
/**
|
|
662
|
+
* Execute a shell command with policy + approval checks.
|
|
663
|
+
* Returns ShellResult on success, throws on policy violation.
|
|
664
|
+
*/
|
|
665
|
+
async exec(command, agentId, opts = {}) {
|
|
666
|
+
// Policy check
|
|
667
|
+
if (!this.policy.isCommandAllowed(command)) {
|
|
668
|
+
this.emit('policy:violation', agentId, command, 'Command not allowed by policy');
|
|
669
|
+
this.audit({ action: 'shell_execute', agentId, target: command, result: 'blocked' });
|
|
670
|
+
throw new RuntimePolicyError(`Command blocked by policy: ${command}`);
|
|
671
|
+
}
|
|
672
|
+
// Approval check
|
|
673
|
+
const needsApproval = opts.requiresApproval ?? this.policy.requiresApproval(command);
|
|
674
|
+
if (needsApproval) {
|
|
675
|
+
const decision = await this.gate.request({
|
|
676
|
+
type: 'shell',
|
|
677
|
+
target: command,
|
|
678
|
+
agentId,
|
|
679
|
+
risk: this.policy.assessRisk(command),
|
|
680
|
+
timestamp: Date.now(),
|
|
681
|
+
});
|
|
682
|
+
if (!decision.approved) {
|
|
683
|
+
this.audit({ action: 'shell_execute', agentId, target: command, result: 'denied', details: { reason: decision.reason } });
|
|
684
|
+
throw new RuntimeApprovalError(`Command denied: ${decision.reason ?? 'no reason given'}`);
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
// Execute
|
|
688
|
+
this.emit('command:start', agentId, command);
|
|
689
|
+
const result = await this.shell.execute(command, { ...opts, agentId });
|
|
690
|
+
this.emit('command:complete', agentId, command, result);
|
|
691
|
+
// Issue outcome-bound receipt — runtime authority, not agent's word
|
|
692
|
+
const outputHash = (0, crypto_1.createHash)('sha256')
|
|
693
|
+
.update(result.stdout + result.stderr + String(result.exitCode))
|
|
694
|
+
.digest('hex');
|
|
695
|
+
result.receipt = this.receiptManager.generateReceipt(agentId, 'shell_execute', command, result.exitCode, outputHash);
|
|
696
|
+
this.audit({
|
|
697
|
+
action: 'shell_execute',
|
|
698
|
+
agentId,
|
|
699
|
+
target: command,
|
|
700
|
+
result: result.exitCode === 0 ? 'success' : (result.timedOut ? 'timeout' : 'error'),
|
|
701
|
+
durationMs: result.durationMs,
|
|
702
|
+
details: { exitCode: result.exitCode, timedOut: result.timedOut },
|
|
703
|
+
});
|
|
704
|
+
return result;
|
|
705
|
+
}
|
|
706
|
+
/** Read a file with policy + optional approval */
|
|
707
|
+
async readFile(filePath, agentId) {
|
|
708
|
+
if (!this.policy.isPathAllowed(filePath)) {
|
|
709
|
+
this.emit('policy:violation', agentId, filePath, 'Path not allowed');
|
|
710
|
+
this.audit({ action: 'file_read', agentId, target: filePath, result: 'blocked' });
|
|
711
|
+
return { success: false, path: filePath, error: 'Path not allowed by policy', durationMs: 0 };
|
|
712
|
+
}
|
|
713
|
+
// Reads can auto-approve
|
|
714
|
+
if (!this.policy.autoApproveReads) {
|
|
715
|
+
const decision = await this.gate.request({
|
|
716
|
+
type: 'file_read', target: filePath, agentId, risk: 'low', timestamp: Date.now(),
|
|
717
|
+
});
|
|
718
|
+
if (!decision.approved) {
|
|
719
|
+
this.audit({ action: 'file_read', agentId, target: filePath, result: 'denied' });
|
|
720
|
+
return { success: false, path: filePath, error: 'Read denied', durationMs: 0 };
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
this.emit('file:access', agentId, filePath, 'read');
|
|
724
|
+
const result = await this.files.read(filePath, agentId);
|
|
725
|
+
this.audit({
|
|
726
|
+
action: 'file_read', agentId, target: filePath,
|
|
727
|
+
result: result.success ? 'success' : 'error', durationMs: result.durationMs,
|
|
728
|
+
});
|
|
729
|
+
return result;
|
|
730
|
+
}
|
|
731
|
+
/** Write a file with policy + approval */
|
|
732
|
+
async writeFile(filePath, content, agentId) {
|
|
733
|
+
if (!this.policy.isPathAllowed(filePath)) {
|
|
734
|
+
this.emit('policy:violation', agentId, filePath, 'Path not allowed');
|
|
735
|
+
this.audit({ action: 'file_write', agentId, target: filePath, result: 'blocked' });
|
|
736
|
+
return { success: false, path: filePath, error: 'Path not allowed by policy', durationMs: 0 };
|
|
737
|
+
}
|
|
738
|
+
const decision = await this.gate.request({
|
|
739
|
+
type: 'file_write', target: filePath, agentId, risk: 'medium', timestamp: Date.now(),
|
|
740
|
+
});
|
|
741
|
+
if (!decision.approved) {
|
|
742
|
+
this.audit({ action: 'file_write', agentId, target: filePath, result: 'denied' });
|
|
743
|
+
return { success: false, path: filePath, error: `Write denied: ${decision.reason ?? 'no reason'}`, durationMs: 0 };
|
|
744
|
+
}
|
|
745
|
+
this.emit('file:access', agentId, filePath, 'write');
|
|
746
|
+
const result = await this.files.write(filePath, content, agentId);
|
|
747
|
+
// Issue outcome-bound receipt on successful write
|
|
748
|
+
if (result.success) {
|
|
749
|
+
const contentHash = (0, crypto_1.createHash)('sha256').update(content).digest('hex');
|
|
750
|
+
result.receipt = this.receiptManager.generateReceipt(agentId, 'file_write', result.path, 0, contentHash);
|
|
751
|
+
}
|
|
752
|
+
this.audit({
|
|
753
|
+
action: 'file_write', agentId, target: filePath,
|
|
754
|
+
result: result.success ? 'success' : 'error', durationMs: result.durationMs,
|
|
755
|
+
});
|
|
756
|
+
return result;
|
|
757
|
+
}
|
|
758
|
+
/** List a directory with policy check */
|
|
759
|
+
async listDir(dirPath, agentId) {
|
|
760
|
+
if (!this.policy.isPathAllowed(dirPath)) {
|
|
761
|
+
this.emit('policy:violation', agentId, dirPath, 'Path not allowed');
|
|
762
|
+
this.audit({ action: 'file_list', agentId, target: dirPath, result: 'blocked' });
|
|
763
|
+
return { success: false, path: dirPath, error: 'Path not allowed by policy', durationMs: 0 };
|
|
764
|
+
}
|
|
765
|
+
this.emit('file:access', agentId, dirPath, 'list');
|
|
766
|
+
const result = await this.files.list(dirPath, agentId);
|
|
767
|
+
this.audit({
|
|
768
|
+
action: 'file_list', agentId, target: dirPath,
|
|
769
|
+
result: result.success ? 'success' : 'error', durationMs: result.durationMs,
|
|
770
|
+
});
|
|
771
|
+
return result;
|
|
772
|
+
}
|
|
773
|
+
/** Get the internal audit log */
|
|
774
|
+
getAuditLog() {
|
|
775
|
+
return this.auditLog;
|
|
776
|
+
}
|
|
777
|
+
/** Clear the internal audit log */
|
|
778
|
+
clearAuditLog() {
|
|
779
|
+
this.auditLog = [];
|
|
780
|
+
}
|
|
781
|
+
audit(entry) {
|
|
782
|
+
const full = { ...entry, timestamp: new Date().toISOString() };
|
|
783
|
+
this.auditLog.push(full);
|
|
784
|
+
this.emit('audit', full);
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
exports.AgentRuntime = AgentRuntime;
|
|
788
|
+
// ============================================================================
|
|
789
|
+
// ERROR TYPES
|
|
790
|
+
// ============================================================================
|
|
791
|
+
/** Thrown when an operation violates the sandbox policy */
|
|
792
|
+
class RuntimePolicyError extends Error {
|
|
793
|
+
code = 'POLICY_VIOLATION';
|
|
794
|
+
constructor(message) {
|
|
795
|
+
super(message);
|
|
796
|
+
this.name = 'RuntimePolicyError';
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
exports.RuntimePolicyError = RuntimePolicyError;
|
|
800
|
+
/** Thrown when an operation is denied by the approval gate */
|
|
801
|
+
class RuntimeApprovalError extends Error {
|
|
802
|
+
code = 'APPROVAL_DENIED';
|
|
803
|
+
constructor(message) {
|
|
804
|
+
super(message);
|
|
805
|
+
this.name = 'RuntimeApprovalError';
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
exports.RuntimeApprovalError = RuntimeApprovalError;
|
|
809
|
+
/** Thrown when a command fails to spawn */
|
|
810
|
+
class RuntimeExecutionError extends Error {
|
|
811
|
+
code = 'EXECUTION_ERROR';
|
|
812
|
+
constructor(message) {
|
|
813
|
+
super(message);
|
|
814
|
+
this.name = 'RuntimeExecutionError';
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
exports.RuntimeExecutionError = RuntimeExecutionError;
|
|
818
|
+
//# sourceMappingURL=agent-runtime.js.map
|