@yuaone/core 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +663 -0
- package/README.md +15 -0
- package/dist/__tests__/context-manager.test.d.ts +6 -0
- package/dist/__tests__/context-manager.test.d.ts.map +1 -0
- package/dist/__tests__/context-manager.test.js +220 -0
- package/dist/__tests__/context-manager.test.js.map +1 -0
- package/dist/__tests__/governor.test.d.ts +6 -0
- package/dist/__tests__/governor.test.d.ts.map +1 -0
- package/dist/__tests__/governor.test.js +210 -0
- package/dist/__tests__/governor.test.js.map +1 -0
- package/dist/__tests__/model-router.test.d.ts +6 -0
- package/dist/__tests__/model-router.test.d.ts.map +1 -0
- package/dist/__tests__/model-router.test.js +329 -0
- package/dist/__tests__/model-router.test.js.map +1 -0
- package/dist/agent-logger.d.ts +384 -0
- package/dist/agent-logger.d.ts.map +1 -0
- package/dist/agent-logger.js +820 -0
- package/dist/agent-logger.js.map +1 -0
- package/dist/agent-loop.d.ts +163 -0
- package/dist/agent-loop.d.ts.map +1 -0
- package/dist/agent-loop.js +609 -0
- package/dist/agent-loop.js.map +1 -0
- package/dist/agent-modes.d.ts +85 -0
- package/dist/agent-modes.d.ts.map +1 -0
- package/dist/agent-modes.js +418 -0
- package/dist/agent-modes.js.map +1 -0
- package/dist/approval.d.ts +137 -0
- package/dist/approval.d.ts.map +1 -0
- package/dist/approval.js +299 -0
- package/dist/approval.js.map +1 -0
- package/dist/async-completion-queue.d.ts +56 -0
- package/dist/async-completion-queue.d.ts.map +1 -0
- package/dist/async-completion-queue.js +77 -0
- package/dist/async-completion-queue.js.map +1 -0
- package/dist/auto-fix.d.ts +174 -0
- package/dist/auto-fix.d.ts.map +1 -0
- package/dist/auto-fix.js +319 -0
- package/dist/auto-fix.js.map +1 -0
- package/dist/codebase-context.d.ts +396 -0
- package/dist/codebase-context.d.ts.map +1 -0
- package/dist/codebase-context.js +1260 -0
- package/dist/codebase-context.js.map +1 -0
- package/dist/conflict-resolver.d.ts +191 -0
- package/dist/conflict-resolver.d.ts.map +1 -0
- package/dist/conflict-resolver.js +524 -0
- package/dist/conflict-resolver.js.map +1 -0
- package/dist/constants.d.ts +52 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +141 -0
- package/dist/constants.js.map +1 -0
- package/dist/context-budget.d.ts +435 -0
- package/dist/context-budget.d.ts.map +1 -0
- package/dist/context-budget.js +903 -0
- package/dist/context-budget.js.map +1 -0
- package/dist/context-compressor.d.ts +143 -0
- package/dist/context-compressor.d.ts.map +1 -0
- package/dist/context-compressor.js +511 -0
- package/dist/context-compressor.js.map +1 -0
- package/dist/context-manager.d.ts +112 -0
- package/dist/context-manager.d.ts.map +1 -0
- package/dist/context-manager.js +247 -0
- package/dist/context-manager.js.map +1 -0
- package/dist/continuous-reflection.d.ts +267 -0
- package/dist/continuous-reflection.d.ts.map +1 -0
- package/dist/continuous-reflection.js +338 -0
- package/dist/continuous-reflection.js.map +1 -0
- package/dist/cross-file-refactor.d.ts +352 -0
- package/dist/cross-file-refactor.d.ts.map +1 -0
- package/dist/cross-file-refactor.js +1544 -0
- package/dist/cross-file-refactor.js.map +1 -0
- package/dist/dag-orchestrator.d.ts +138 -0
- package/dist/dag-orchestrator.d.ts.map +1 -0
- package/dist/dag-orchestrator.js +379 -0
- package/dist/dag-orchestrator.js.map +1 -0
- package/dist/debate-orchestrator.d.ts +301 -0
- package/dist/debate-orchestrator.d.ts.map +1 -0
- package/dist/debate-orchestrator.js +719 -0
- package/dist/debate-orchestrator.js.map +1 -0
- package/dist/dependency-analyzer.d.ts +113 -0
- package/dist/dependency-analyzer.d.ts.map +1 -0
- package/dist/dependency-analyzer.js +444 -0
- package/dist/dependency-analyzer.js.map +1 -0
- package/dist/design-loop.d.ts +59 -0
- package/dist/design-loop.d.ts.map +1 -0
- package/dist/design-loop.js +344 -0
- package/dist/design-loop.js.map +1 -0
- package/dist/doc-intelligence.d.ts +383 -0
- package/dist/doc-intelligence.d.ts.map +1 -0
- package/dist/doc-intelligence.js +1307 -0
- package/dist/doc-intelligence.js.map +1 -0
- package/dist/dynamic-role-generator.d.ts +76 -0
- package/dist/dynamic-role-generator.d.ts.map +1 -0
- package/dist/dynamic-role-generator.js +194 -0
- package/dist/dynamic-role-generator.js.map +1 -0
- package/dist/errors.d.ts +69 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +102 -0
- package/dist/errors.js.map +1 -0
- package/dist/event-bus.d.ts +159 -0
- package/dist/event-bus.d.ts.map +1 -0
- package/dist/event-bus.js +305 -0
- package/dist/event-bus.js.map +1 -0
- package/dist/execution-engine.d.ts +425 -0
- package/dist/execution-engine.d.ts.map +1 -0
- package/dist/execution-engine.js +1555 -0
- package/dist/execution-engine.js.map +1 -0
- package/dist/git-intelligence.d.ts +306 -0
- package/dist/git-intelligence.d.ts.map +1 -0
- package/dist/git-intelligence.js +1099 -0
- package/dist/git-intelligence.js.map +1 -0
- package/dist/governor.d.ts +77 -0
- package/dist/governor.d.ts.map +1 -0
- package/dist/governor.js +161 -0
- package/dist/governor.js.map +1 -0
- package/dist/hierarchical-planner.d.ts +313 -0
- package/dist/hierarchical-planner.d.ts.map +1 -0
- package/dist/hierarchical-planner.js +981 -0
- package/dist/hierarchical-planner.js.map +1 -0
- package/dist/index.d.ts +121 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +123 -0
- package/dist/index.js.map +1 -0
- package/dist/intent-inference.d.ts +103 -0
- package/dist/intent-inference.d.ts.map +1 -0
- package/dist/intent-inference.js +605 -0
- package/dist/intent-inference.js.map +1 -0
- package/dist/interrupt-manager.d.ts +143 -0
- package/dist/interrupt-manager.d.ts.map +1 -0
- package/dist/interrupt-manager.js +196 -0
- package/dist/interrupt-manager.js.map +1 -0
- package/dist/kernel.d.ts +564 -0
- package/dist/kernel.d.ts.map +1 -0
- package/dist/kernel.js +1419 -0
- package/dist/kernel.js.map +1 -0
- package/dist/language-support.d.ts +232 -0
- package/dist/language-support.d.ts.map +1 -0
- package/dist/language-support.js +1134 -0
- package/dist/language-support.js.map +1 -0
- package/dist/llm-client.d.ts +82 -0
- package/dist/llm-client.d.ts.map +1 -0
- package/dist/llm-client.js +475 -0
- package/dist/llm-client.js.map +1 -0
- package/dist/mcp-client.d.ts +232 -0
- package/dist/mcp-client.d.ts.map +1 -0
- package/dist/mcp-client.js +718 -0
- package/dist/mcp-client.js.map +1 -0
- package/dist/memory-manager.d.ts +200 -0
- package/dist/memory-manager.d.ts.map +1 -0
- package/dist/memory-manager.js +568 -0
- package/dist/memory-manager.js.map +1 -0
- package/dist/memory.d.ts +87 -0
- package/dist/memory.d.ts.map +1 -0
- package/dist/memory.js +341 -0
- package/dist/memory.js.map +1 -0
- package/dist/model-router.d.ts +245 -0
- package/dist/model-router.d.ts.map +1 -0
- package/dist/model-router.js +632 -0
- package/dist/model-router.js.map +1 -0
- package/dist/parallel-executor.d.ts +125 -0
- package/dist/parallel-executor.d.ts.map +1 -0
- package/dist/parallel-executor.js +201 -0
- package/dist/parallel-executor.js.map +1 -0
- package/dist/perf-optimizer.d.ts +212 -0
- package/dist/perf-optimizer.d.ts.map +1 -0
- package/dist/perf-optimizer.js +721 -0
- package/dist/perf-optimizer.js.map +1 -0
- package/dist/persona.d.ts +305 -0
- package/dist/persona.d.ts.map +1 -0
- package/dist/persona.js +887 -0
- package/dist/persona.js.map +1 -0
- package/dist/planner.d.ts +70 -0
- package/dist/planner.d.ts.map +1 -0
- package/dist/planner.js +264 -0
- package/dist/planner.js.map +1 -0
- package/dist/qa-pipeline.d.ts +365 -0
- package/dist/qa-pipeline.d.ts.map +1 -0
- package/dist/qa-pipeline.js +1352 -0
- package/dist/qa-pipeline.js.map +1 -0
- package/dist/reasoning-adapter.d.ts +116 -0
- package/dist/reasoning-adapter.d.ts.map +1 -0
- package/dist/reasoning-adapter.js +187 -0
- package/dist/reasoning-adapter.js.map +1 -0
- package/dist/role-registry.d.ts +55 -0
- package/dist/role-registry.d.ts.map +1 -0
- package/dist/role-registry.js +192 -0
- package/dist/role-registry.js.map +1 -0
- package/dist/sandbox-tiers.d.ts +327 -0
- package/dist/sandbox-tiers.d.ts.map +1 -0
- package/dist/sandbox-tiers.js +928 -0
- package/dist/sandbox-tiers.js.map +1 -0
- package/dist/security-scanner.d.ts +222 -0
- package/dist/security-scanner.d.ts.map +1 -0
- package/dist/security-scanner.js +1129 -0
- package/dist/security-scanner.js.map +1 -0
- package/dist/security.d.ts +93 -0
- package/dist/security.d.ts.map +1 -0
- package/dist/security.js +393 -0
- package/dist/security.js.map +1 -0
- package/dist/self-reflection.d.ts +397 -0
- package/dist/self-reflection.d.ts.map +1 -0
- package/dist/self-reflection.js +908 -0
- package/dist/self-reflection.js.map +1 -0
- package/dist/session-persistence.d.ts +191 -0
- package/dist/session-persistence.d.ts.map +1 -0
- package/dist/session-persistence.js +395 -0
- package/dist/session-persistence.js.map +1 -0
- package/dist/speculative-executor.d.ts +210 -0
- package/dist/speculative-executor.d.ts.map +1 -0
- package/dist/speculative-executor.js +618 -0
- package/dist/speculative-executor.js.map +1 -0
- package/dist/state-machine.d.ts +289 -0
- package/dist/state-machine.d.ts.map +1 -0
- package/dist/state-machine.js +695 -0
- package/dist/state-machine.js.map +1 -0
- package/dist/sub-agent.d.ts +177 -0
- package/dist/sub-agent.d.ts.map +1 -0
- package/dist/sub-agent.js +303 -0
- package/dist/sub-agent.js.map +1 -0
- package/dist/system-prompt.d.ts +26 -0
- package/dist/system-prompt.d.ts.map +1 -0
- package/dist/system-prompt.js +84 -0
- package/dist/system-prompt.js.map +1 -0
- package/dist/test-intelligence.d.ts +439 -0
- package/dist/test-intelligence.d.ts.map +1 -0
- package/dist/test-intelligence.js +1165 -0
- package/dist/test-intelligence.js.map +1 -0
- package/dist/types.d.ts +632 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +6 -0
- package/dist/types.js.map +1 -0
- package/dist/vector-index.d.ts +314 -0
- package/dist/vector-index.d.ts.map +1 -0
- package/dist/vector-index.js +618 -0
- package/dist/vector-index.js.map +1 -0
- package/package.json +41 -0
|
@@ -0,0 +1,928 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module sandbox-tiers
|
|
3
|
+
* @description YUAN Agent Sandbox Execution Tiers (T0–T4).
|
|
4
|
+
*
|
|
5
|
+
* 5 levels of isolation based on task risk level:
|
|
6
|
+
* - T0: Read-Only — file read, grep, glob only
|
|
7
|
+
* - T1: Write-Restricted — T0 + specific file writes, no network
|
|
8
|
+
* - T2: Project-Scoped — full project read/write, limited shell
|
|
9
|
+
* - T3: Build-Enabled — T2 + npm/pnpm, localhost network
|
|
10
|
+
* - T4: Full-Network — T3 + external network (allowlist)
|
|
11
|
+
*
|
|
12
|
+
* The SandboxManager auto-selects a tier based on requested tools,
|
|
13
|
+
* target files, and shell commands, then validates every action
|
|
14
|
+
* against the tier's policy before allowing execution.
|
|
15
|
+
*/
|
|
16
|
+
import { EventEmitter } from "node:events";
|
|
17
|
+
import path from "node:path";
|
|
18
|
+
// ══════════════════════════════════════════════════════════════════════
|
|
19
|
+
// Constants
|
|
20
|
+
// ══════════════════════════════════════════════════════════════════════
|
|
21
|
+
/** Maximum violation records kept in memory */
|
|
22
|
+
const MAX_VIOLATIONS = 500;
|
|
23
|
+
/** Maximum escalation history entries */
|
|
24
|
+
const MAX_ESCALATION_HISTORY = 100;
|
|
25
|
+
/** Tools that only read data */
|
|
26
|
+
const READ_ONLY_TOOLS = new Set([
|
|
27
|
+
"file_read",
|
|
28
|
+
"grep",
|
|
29
|
+
"glob",
|
|
30
|
+
"codebase_search",
|
|
31
|
+
"list_directory",
|
|
32
|
+
]);
|
|
33
|
+
/** Tools that write files */
|
|
34
|
+
const WRITE_TOOLS = new Set(["file_write", "file_edit", "file_create"]);
|
|
35
|
+
/** Tools that delete files */
|
|
36
|
+
const DELETE_TOOLS = new Set(["file_delete"]);
|
|
37
|
+
/** Tools that execute shell commands */
|
|
38
|
+
const SHELL_TOOLS = new Set(["shell_exec", "shell_command", "bash"]);
|
|
39
|
+
/** Build-related command patterns */
|
|
40
|
+
const BUILD_COMMANDS = new Set([
|
|
41
|
+
"npm",
|
|
42
|
+
"pnpm",
|
|
43
|
+
"yarn",
|
|
44
|
+
"npx",
|
|
45
|
+
"tsc",
|
|
46
|
+
"make",
|
|
47
|
+
"cmake",
|
|
48
|
+
"cargo",
|
|
49
|
+
"go",
|
|
50
|
+
"gradle",
|
|
51
|
+
"mvn",
|
|
52
|
+
"pip",
|
|
53
|
+
"poetry",
|
|
54
|
+
]);
|
|
55
|
+
/** Network-related command patterns */
|
|
56
|
+
const NETWORK_COMMANDS = new Set([
|
|
57
|
+
"curl",
|
|
58
|
+
"wget",
|
|
59
|
+
"fetch",
|
|
60
|
+
"http",
|
|
61
|
+
"ssh",
|
|
62
|
+
"scp",
|
|
63
|
+
"rsync",
|
|
64
|
+
"ftp",
|
|
65
|
+
]);
|
|
66
|
+
/** Package install patterns (regex) */
|
|
67
|
+
const PACKAGE_INSTALL_PATTERNS = [
|
|
68
|
+
/^npm\s+install/,
|
|
69
|
+
/^npm\s+i\b/,
|
|
70
|
+
/^pnpm\s+add/,
|
|
71
|
+
/^pnpm\s+install/,
|
|
72
|
+
/^yarn\s+add/,
|
|
73
|
+
/^pip\s+install/,
|
|
74
|
+
/^cargo\s+install/,
|
|
75
|
+
/^go\s+get/,
|
|
76
|
+
];
|
|
77
|
+
/** Default file size limit: 10 MB */
|
|
78
|
+
const DEFAULT_MAX_FILE_SIZE = 10 * 1024 * 1024;
|
|
79
|
+
// ══════════════════════════════════════════════════════════════════════
|
|
80
|
+
// SandboxManager
|
|
81
|
+
// ══════════════════════════════════════════════════════════════════════
|
|
82
|
+
/**
|
|
83
|
+
* SandboxManager — manages execution isolation tiers for the YUAN agent.
|
|
84
|
+
*
|
|
85
|
+
* Provides 5 tiers of isolation (T0–T4), auto-selects the appropriate tier
|
|
86
|
+
* based on requested tools and commands, and validates every action against
|
|
87
|
+
* the active tier's policy before allowing execution.
|
|
88
|
+
*
|
|
89
|
+
* @example
|
|
90
|
+
* ```ts
|
|
91
|
+
* const sandbox = new SandboxManager({ projectPath: "/my/project" });
|
|
92
|
+
*
|
|
93
|
+
* // Auto-select tier
|
|
94
|
+
* const decision = sandbox.selectTier(["file_read", "file_write"], ["src/app.ts"]);
|
|
95
|
+
* // => { tier: 1, reason: "file write required", ... }
|
|
96
|
+
*
|
|
97
|
+
* // Validate actions
|
|
98
|
+
* const { allowed } = sandbox.canWriteFile("src/app.ts");
|
|
99
|
+
* ```
|
|
100
|
+
*/
|
|
101
|
+
export class SandboxManager extends EventEmitter {
|
|
102
|
+
config;
|
|
103
|
+
tiers;
|
|
104
|
+
state;
|
|
105
|
+
constructor(config) {
|
|
106
|
+
super();
|
|
107
|
+
this.config = {
|
|
108
|
+
projectPath: path.resolve(config.projectPath),
|
|
109
|
+
defaultTier: config.defaultTier ?? 2,
|
|
110
|
+
maxTier: config.maxTier ?? 3,
|
|
111
|
+
enableAutoEscalation: config.enableAutoEscalation ?? false,
|
|
112
|
+
auditLog: config.auditLog ?? true,
|
|
113
|
+
};
|
|
114
|
+
this.tiers = this.buildDefaultTiers();
|
|
115
|
+
this.state = {
|
|
116
|
+
currentTier: this.config.defaultTier,
|
|
117
|
+
sessionId: "",
|
|
118
|
+
writeCount: 0,
|
|
119
|
+
shellCount: 0,
|
|
120
|
+
violations: [],
|
|
121
|
+
escalationHistory: [],
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
// ──────────────────────────────────────────────────────────────────
|
|
125
|
+
// Tier Selection
|
|
126
|
+
// ──────────────────────────────────────────────────────────────────
|
|
127
|
+
/**
|
|
128
|
+
* Auto-select the appropriate sandbox tier based on requested tools,
|
|
129
|
+
* target files, and shell commands.
|
|
130
|
+
*
|
|
131
|
+
* @param tools - List of tool names that will be used
|
|
132
|
+
* @param targetFiles - List of file paths that may be modified
|
|
133
|
+
* @param shellCommands - Optional list of shell commands to execute
|
|
134
|
+
* @returns Decision with selected tier, reason, and influencing factors
|
|
135
|
+
*/
|
|
136
|
+
selectTier(tools, targetFiles, shellCommands) {
|
|
137
|
+
let tier = 0;
|
|
138
|
+
const factors = [];
|
|
139
|
+
const toolSet = new Set(tools);
|
|
140
|
+
const commands = shellCommands ?? [];
|
|
141
|
+
// File write needed?
|
|
142
|
+
const hasWriteTools = tools.some((t) => WRITE_TOOLS.has(t));
|
|
143
|
+
if (hasWriteTools) {
|
|
144
|
+
tier = Math.max(tier, 1);
|
|
145
|
+
factors.push("file write required");
|
|
146
|
+
if (targetFiles.length > 5) {
|
|
147
|
+
tier = Math.max(tier, 2);
|
|
148
|
+
factors.push(`many files to modify (${targetFiles.length})`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
// File delete needed?
|
|
152
|
+
const hasDeleteTools = tools.some((t) => DELETE_TOOLS.has(t));
|
|
153
|
+
if (hasDeleteTools) {
|
|
154
|
+
tier = Math.max(tier, 2);
|
|
155
|
+
factors.push("file deletion required");
|
|
156
|
+
}
|
|
157
|
+
// Shell needed?
|
|
158
|
+
const hasShellTools = tools.some((t) => SHELL_TOOLS.has(t));
|
|
159
|
+
if (hasShellTools) {
|
|
160
|
+
tier = Math.max(tier, 2);
|
|
161
|
+
factors.push("shell execution required");
|
|
162
|
+
// Check for build commands
|
|
163
|
+
for (const cmd of commands) {
|
|
164
|
+
const executable = this.extractCommand(cmd);
|
|
165
|
+
if (this.isBuildCommand(executable)) {
|
|
166
|
+
tier = Math.max(tier, 3);
|
|
167
|
+
factors.push(`build command detected: ${executable}`);
|
|
168
|
+
break;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
// Check for network commands
|
|
172
|
+
for (const cmd of commands) {
|
|
173
|
+
const executable = this.extractCommand(cmd);
|
|
174
|
+
if (this.isNetworkCommand(executable)) {
|
|
175
|
+
tier = Math.max(tier, 4);
|
|
176
|
+
factors.push(`network command detected: ${executable}`);
|
|
177
|
+
break;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
// Package install?
|
|
182
|
+
if (hasShellTools) {
|
|
183
|
+
for (const cmd of commands) {
|
|
184
|
+
if (PACKAGE_INSTALL_PATTERNS.some((p) => p.test(cmd.trim()))) {
|
|
185
|
+
tier = Math.max(tier, 4);
|
|
186
|
+
factors.push("package installation requires network");
|
|
187
|
+
break;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
// Cap at maxTier
|
|
192
|
+
const uncapped = tier;
|
|
193
|
+
tier = Math.min(tier, this.config.maxTier);
|
|
194
|
+
if (tier < uncapped) {
|
|
195
|
+
factors.push(`capped from T${uncapped} to T${tier} by maxTier config`);
|
|
196
|
+
}
|
|
197
|
+
const policy = this.tiers.get(tier);
|
|
198
|
+
const reason = factors.length > 0
|
|
199
|
+
? `T${tier} (${policy.name}): ${factors[0]}`
|
|
200
|
+
: `T${tier} (${policy.name}): read-only access sufficient`;
|
|
201
|
+
// Set the tier
|
|
202
|
+
this.setTier(tier, reason);
|
|
203
|
+
return {
|
|
204
|
+
tier,
|
|
205
|
+
reason,
|
|
206
|
+
factors,
|
|
207
|
+
overrideable: tier < 4,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Manually set the sandbox tier.
|
|
212
|
+
*
|
|
213
|
+
* @param tier - Target tier level
|
|
214
|
+
* @param reason - Reason for the tier change
|
|
215
|
+
* @throws If tier exceeds maxTier
|
|
216
|
+
*/
|
|
217
|
+
setTier(tier, reason) {
|
|
218
|
+
if (tier > this.config.maxTier) {
|
|
219
|
+
this.recordViolation("setTier", `T${tier}`, `tier ${tier} exceeds maxTier ${this.config.maxTier}`, true);
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
const prev = this.state.currentTier;
|
|
223
|
+
if (prev !== tier) {
|
|
224
|
+
this.state.currentTier = tier;
|
|
225
|
+
this.emit("tier:changed", prev, tier, reason);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Escalate to the next higher tier.
|
|
230
|
+
*
|
|
231
|
+
* @param reason - Why escalation is needed
|
|
232
|
+
* @returns true if escalation succeeded, false if already at maxTier
|
|
233
|
+
*/
|
|
234
|
+
escalate(reason) {
|
|
235
|
+
if (!this.config.enableAutoEscalation) {
|
|
236
|
+
this.recordViolation("escalate", `T${this.state.currentTier}→T${(this.state.currentTier + 1)}`, "auto-escalation disabled", true);
|
|
237
|
+
return false;
|
|
238
|
+
}
|
|
239
|
+
const current = this.state.currentTier;
|
|
240
|
+
if (current >= this.config.maxTier) {
|
|
241
|
+
return false;
|
|
242
|
+
}
|
|
243
|
+
const next = (current + 1);
|
|
244
|
+
// Cap escalation history
|
|
245
|
+
if (this.state.escalationHistory.length >= MAX_ESCALATION_HISTORY) {
|
|
246
|
+
this.state.escalationHistory = this.state.escalationHistory.slice(-Math.floor(MAX_ESCALATION_HISTORY / 2));
|
|
247
|
+
}
|
|
248
|
+
this.state.escalationHistory.push({
|
|
249
|
+
from: current,
|
|
250
|
+
to: next,
|
|
251
|
+
reason,
|
|
252
|
+
timestamp: Date.now(),
|
|
253
|
+
});
|
|
254
|
+
this.state.currentTier = next;
|
|
255
|
+
this.emit("tier:changed", current, next, reason);
|
|
256
|
+
this.emit("escalation", current, next, reason);
|
|
257
|
+
return true;
|
|
258
|
+
}
|
|
259
|
+
/** Get the current active tier */
|
|
260
|
+
getCurrentTier() {
|
|
261
|
+
return this.state.currentTier;
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Get the policy for a specific tier, or the current tier if omitted.
|
|
265
|
+
*
|
|
266
|
+
* @param tier - Tier to get policy for (defaults to current)
|
|
267
|
+
*/
|
|
268
|
+
getTierPolicy(tier) {
|
|
269
|
+
const t = tier ?? this.state.currentTier;
|
|
270
|
+
const policy = this.tiers.get(t);
|
|
271
|
+
if (!policy) {
|
|
272
|
+
throw new Error(`Unknown sandbox tier: ${t}`);
|
|
273
|
+
}
|
|
274
|
+
return policy;
|
|
275
|
+
}
|
|
276
|
+
// ──────────────────────────────────────────────────────────────────
|
|
277
|
+
// Validation
|
|
278
|
+
// ──────────────────────────────────────────────────────────────────
|
|
279
|
+
/**
|
|
280
|
+
* Check if reading a file is allowed under the current tier.
|
|
281
|
+
*
|
|
282
|
+
* @param filePath - Absolute or relative file path
|
|
283
|
+
* @returns true if the read is allowed
|
|
284
|
+
*/
|
|
285
|
+
canReadFile(filePath) {
|
|
286
|
+
const policy = this.getTierPolicy();
|
|
287
|
+
if (!policy.fileRead) {
|
|
288
|
+
this.recordViolation("file_read", filePath, "file reads not allowed at this tier", true);
|
|
289
|
+
return false;
|
|
290
|
+
}
|
|
291
|
+
// All tiers that allow reads can read any file in the project
|
|
292
|
+
const normalized = this.normalizePath(filePath);
|
|
293
|
+
if (!normalized.startsWith(this.config.projectPath)) {
|
|
294
|
+
this.recordViolation("file_read", filePath, "path is outside project directory", true);
|
|
295
|
+
return false;
|
|
296
|
+
}
|
|
297
|
+
return true;
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* Check if writing a file is allowed under the current tier.
|
|
301
|
+
*
|
|
302
|
+
* @param filePath - Absolute or relative file path
|
|
303
|
+
* @returns Object with allowed status and optional reason
|
|
304
|
+
*/
|
|
305
|
+
canWriteFile(filePath) {
|
|
306
|
+
const policy = this.getTierPolicy();
|
|
307
|
+
if (!policy.fileWrite) {
|
|
308
|
+
const reason = "file writes not allowed at this tier";
|
|
309
|
+
this.recordViolation("file_write", filePath, reason, true);
|
|
310
|
+
return { allowed: false, reason };
|
|
311
|
+
}
|
|
312
|
+
// Check write count limit
|
|
313
|
+
if (this.state.writeCount >= policy.maxTotalWrites) {
|
|
314
|
+
const reason = `write limit reached (${policy.maxTotalWrites})`;
|
|
315
|
+
this.recordViolation("file_write", filePath, reason, true);
|
|
316
|
+
return { allowed: false, reason };
|
|
317
|
+
}
|
|
318
|
+
const normalized = this.normalizePath(filePath);
|
|
319
|
+
const relative = this.toRelative(normalized);
|
|
320
|
+
// Must be within project
|
|
321
|
+
if (!normalized.startsWith(this.config.projectPath)) {
|
|
322
|
+
const reason = "path is outside project directory";
|
|
323
|
+
this.recordViolation("file_write", filePath, reason, true);
|
|
324
|
+
return { allowed: false, reason };
|
|
325
|
+
}
|
|
326
|
+
// Check blocked paths
|
|
327
|
+
if (this.matchesPattern(relative, policy.blockedWritePaths)) {
|
|
328
|
+
const reason = `path matches blocked pattern`;
|
|
329
|
+
this.recordViolation("file_write", filePath, reason, true);
|
|
330
|
+
return { allowed: false, reason };
|
|
331
|
+
}
|
|
332
|
+
// Check allowed paths (if restricted)
|
|
333
|
+
if (policy.allowedWritePaths.length > 0 &&
|
|
334
|
+
!policy.allowedWritePaths.includes("**")) {
|
|
335
|
+
if (!this.matchesPattern(relative, policy.allowedWritePaths)) {
|
|
336
|
+
const reason = "path not in allowed write paths";
|
|
337
|
+
this.recordViolation("file_write", filePath, reason, true);
|
|
338
|
+
return { allowed: false, reason };
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
// Note: writeCount is incremented here. Callers should only call
|
|
342
|
+
// canWriteFile() when they intend to actually perform the write.
|
|
343
|
+
// For preview/validation, use validateToolCall() which does not
|
|
344
|
+
// increment counters directly.
|
|
345
|
+
this.state.writeCount++;
|
|
346
|
+
return { allowed: true };
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* Check if writing a file would be allowed WITHOUT incrementing counters.
|
|
350
|
+
* Use this for preview/validation — unlike canWriteFile, it has no side effects.
|
|
351
|
+
*/
|
|
352
|
+
checkWriteFile(filePath) {
|
|
353
|
+
const policy = this.getTierPolicy();
|
|
354
|
+
if (!policy.fileWrite)
|
|
355
|
+
return { allowed: false, reason: "file writes not allowed at this tier" };
|
|
356
|
+
if (this.state.writeCount >= policy.maxTotalWrites)
|
|
357
|
+
return { allowed: false, reason: `write limit reached (${policy.maxTotalWrites})` };
|
|
358
|
+
const normalized = this.normalizePath(filePath);
|
|
359
|
+
if (!normalized.startsWith(this.config.projectPath))
|
|
360
|
+
return { allowed: false, reason: "path is outside project directory" };
|
|
361
|
+
const relative = this.toRelative(normalized);
|
|
362
|
+
if (this.matchesPattern(relative, policy.blockedWritePaths))
|
|
363
|
+
return { allowed: false, reason: "path matches blocked pattern" };
|
|
364
|
+
if (policy.allowedWritePaths.length > 0 && !policy.allowedWritePaths.includes("**")) {
|
|
365
|
+
if (!this.matchesPattern(relative, policy.allowedWritePaths))
|
|
366
|
+
return { allowed: false, reason: "path not in allowed write paths" };
|
|
367
|
+
}
|
|
368
|
+
return { allowed: true };
|
|
369
|
+
}
|
|
370
|
+
/**
|
|
371
|
+
* Check if deleting a file is allowed under the current tier.
|
|
372
|
+
*
|
|
373
|
+
* @param filePath - Absolute or relative file path
|
|
374
|
+
* @returns Object with allowed status and optional reason
|
|
375
|
+
*/
|
|
376
|
+
canDeleteFile(filePath) {
|
|
377
|
+
const policy = this.getTierPolicy();
|
|
378
|
+
if (!policy.fileDelete) {
|
|
379
|
+
const reason = "file deletion not allowed at this tier";
|
|
380
|
+
this.recordViolation("file_delete", filePath, reason, true);
|
|
381
|
+
return { allowed: false, reason };
|
|
382
|
+
}
|
|
383
|
+
const normalized = this.normalizePath(filePath);
|
|
384
|
+
const relative = this.toRelative(normalized);
|
|
385
|
+
// Must be within project
|
|
386
|
+
if (!normalized.startsWith(this.config.projectPath)) {
|
|
387
|
+
const reason = "path is outside project directory";
|
|
388
|
+
this.recordViolation("file_delete", filePath, reason, true);
|
|
389
|
+
return { allowed: false, reason };
|
|
390
|
+
}
|
|
391
|
+
// Check blocked paths
|
|
392
|
+
if (this.matchesPattern(relative, policy.blockedWritePaths)) {
|
|
393
|
+
const reason = "path matches blocked pattern";
|
|
394
|
+
this.recordViolation("file_delete", filePath, reason, true);
|
|
395
|
+
return { allowed: false, reason };
|
|
396
|
+
}
|
|
397
|
+
return { allowed: true };
|
|
398
|
+
}
|
|
399
|
+
/**
|
|
400
|
+
* Check if a shell command is allowed under the current tier.
|
|
401
|
+
*
|
|
402
|
+
* @param command - The command string (e.g. "tsc --noEmit")
|
|
403
|
+
* @param args - Optional additional arguments
|
|
404
|
+
* @returns Object with allowed status and optional reason
|
|
405
|
+
*/
|
|
406
|
+
canExecuteShell(command, args) {
|
|
407
|
+
const policy = this.getTierPolicy();
|
|
408
|
+
if (!policy.shellExec) {
|
|
409
|
+
const reason = "shell execution not allowed at this tier";
|
|
410
|
+
this.recordViolation("shell_exec", command, reason, true);
|
|
411
|
+
return { allowed: false, reason };
|
|
412
|
+
}
|
|
413
|
+
// Check shell count limit
|
|
414
|
+
if (this.state.shellCount >= policy.maxShellCalls) {
|
|
415
|
+
const reason = `shell call limit reached (${policy.maxShellCalls})`;
|
|
416
|
+
this.recordViolation("shell_exec", command, reason, true);
|
|
417
|
+
return { allowed: false, reason };
|
|
418
|
+
}
|
|
419
|
+
const fullCommand = args ? `${command} ${args.join(" ")}` : command;
|
|
420
|
+
const executable = this.extractCommand(fullCommand);
|
|
421
|
+
// Check blocked commands — defense-in-depth:
|
|
422
|
+
// 1. Exact executable match (after path.basename stripping)
|
|
423
|
+
// 2. Detect shell wrappers (bash -c, sh -c, eval, etc.)
|
|
424
|
+
// 3. Check if blocked command appears as executable in piped/chained commands
|
|
425
|
+
const SHELL_WRAPPERS = new Set(["bash", "sh", "zsh", "dash", "csh", "ksh", "env"]);
|
|
426
|
+
const isShellWrapped = SHELL_WRAPPERS.has(executable) &&
|
|
427
|
+
(fullCommand.includes(" -c ") || fullCommand.includes(" -c\"") || fullCommand.includes(" -c'"));
|
|
428
|
+
for (const blocked of policy.blockedCommands) {
|
|
429
|
+
// Exact executable match
|
|
430
|
+
if (executable === blocked) {
|
|
431
|
+
const reason = `command "${blocked}" is blocked at this tier`;
|
|
432
|
+
this.recordViolation("shell_exec", fullCommand, reason, true);
|
|
433
|
+
return { allowed: false, reason };
|
|
434
|
+
}
|
|
435
|
+
// Shell wrapper detection — block "bash -c 'rm -rf /'" etc.
|
|
436
|
+
if (isShellWrapped && fullCommand.includes(blocked)) {
|
|
437
|
+
const reason = `command "${blocked}" detected inside shell wrapper`;
|
|
438
|
+
this.recordViolation("shell_exec", fullCommand, reason, true);
|
|
439
|
+
return { allowed: false, reason };
|
|
440
|
+
}
|
|
441
|
+
// Check piped/chained commands (|, &&, ;, ||)
|
|
442
|
+
const segments = fullCommand.split(/\s*(?:\|{1,2}|&&|;)\s*/);
|
|
443
|
+
for (const segment of segments) {
|
|
444
|
+
const segCmd = segment.trim().split(/\s+/)[0];
|
|
445
|
+
if (segCmd && path.basename(segCmd) === blocked) {
|
|
446
|
+
const reason = `command "${blocked}" detected in chained command`;
|
|
447
|
+
this.recordViolation("shell_exec", fullCommand, reason, true);
|
|
448
|
+
return { allowed: false, reason };
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
// Check allowed commands (if restricted)
|
|
453
|
+
if (policy.allowedCommands.length > 0 &&
|
|
454
|
+
!policy.allowedCommands.includes("*")) {
|
|
455
|
+
if (!policy.allowedCommands.includes(executable)) {
|
|
456
|
+
const reason = `command "${executable}" not in allowed list`;
|
|
457
|
+
this.recordViolation("shell_exec", fullCommand, reason, true);
|
|
458
|
+
return { allowed: false, reason };
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
// Increment shell count
|
|
462
|
+
this.state.shellCount++;
|
|
463
|
+
return { allowed: true };
|
|
464
|
+
}
|
|
465
|
+
/**
|
|
466
|
+
* Check if a network request to a specific host is allowed.
|
|
467
|
+
*
|
|
468
|
+
* @param host - The hostname to check
|
|
469
|
+
* @returns Object with allowed status and optional reason
|
|
470
|
+
*/
|
|
471
|
+
canAccessNetwork(host) {
|
|
472
|
+
const policy = this.getTierPolicy();
|
|
473
|
+
if (!policy.networkAccess) {
|
|
474
|
+
const reason = "network access not allowed at this tier";
|
|
475
|
+
this.recordViolation("network", host, reason, true);
|
|
476
|
+
return { allowed: false, reason };
|
|
477
|
+
}
|
|
478
|
+
// Strip port from host for comparison
|
|
479
|
+
const hostOnly = host.replace(/:\d+$/, "").toLowerCase();
|
|
480
|
+
// Check blocked hosts — includes subdomain matching
|
|
481
|
+
for (const blocked of policy.blockedHosts) {
|
|
482
|
+
const blockedLower = blocked.toLowerCase();
|
|
483
|
+
if (hostOnly === blockedLower ||
|
|
484
|
+
hostOnly.endsWith("." + blockedLower)) {
|
|
485
|
+
const reason = `host "${host}" is blocked (matches ${blocked})`;
|
|
486
|
+
this.recordViolation("network", host, reason, true);
|
|
487
|
+
return { allowed: false, reason };
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
// Block cloud metadata endpoints (AWS, GCP, Azure)
|
|
491
|
+
const METADATA_IPS = ["169.254.169.254", "metadata.google.internal", "100.100.100.200"];
|
|
492
|
+
if (METADATA_IPS.some((ip) => hostOnly === ip || hostOnly.endsWith("." + ip))) {
|
|
493
|
+
const reason = "cloud metadata endpoint blocked";
|
|
494
|
+
this.recordViolation("network", host, reason, true);
|
|
495
|
+
return { allowed: false, reason };
|
|
496
|
+
}
|
|
497
|
+
// Check allowed hosts (if restricted) — includes subdomain matching
|
|
498
|
+
if (policy.allowedHosts.length > 0 &&
|
|
499
|
+
!policy.allowedHosts.includes("*")) {
|
|
500
|
+
const isAllowed = policy.allowedHosts.some((allowed) => {
|
|
501
|
+
const allowedLower = allowed.toLowerCase();
|
|
502
|
+
return hostOnly === allowedLower || hostOnly.endsWith("." + allowedLower);
|
|
503
|
+
});
|
|
504
|
+
if (!isAllowed) {
|
|
505
|
+
const reason = `host "${host}" not in allowed list`;
|
|
506
|
+
this.recordViolation("network", host, reason, true);
|
|
507
|
+
return { allowed: false, reason };
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
return { allowed: true };
|
|
511
|
+
}
|
|
512
|
+
/**
|
|
513
|
+
* Validate a tool call against the current tier's policy.
|
|
514
|
+
*
|
|
515
|
+
* @param toolName - Name of the tool being called
|
|
516
|
+
* @param input - Tool input parameters
|
|
517
|
+
* @returns Object with allowed status and list of violations
|
|
518
|
+
*/
|
|
519
|
+
validateToolCall(toolName, input) {
|
|
520
|
+
const violations = [];
|
|
521
|
+
// Read tools
|
|
522
|
+
if (READ_ONLY_TOOLS.has(toolName)) {
|
|
523
|
+
const filePath = (input.path ?? input.file_path ?? input.pattern);
|
|
524
|
+
if (filePath && !this.canReadFile(filePath)) {
|
|
525
|
+
violations.push(`file read not allowed: ${filePath}`);
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
// Write tools
|
|
529
|
+
if (WRITE_TOOLS.has(toolName)) {
|
|
530
|
+
const filePath = (input.path ?? input.file_path);
|
|
531
|
+
if (filePath) {
|
|
532
|
+
const result = this.canWriteFile(filePath);
|
|
533
|
+
if (!result.allowed) {
|
|
534
|
+
violations.push(`file write blocked: ${result.reason}`);
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
// Check file size
|
|
538
|
+
const content = input.content;
|
|
539
|
+
if (content) {
|
|
540
|
+
const policy = this.getTierPolicy();
|
|
541
|
+
const size = Buffer.byteLength(content, "utf-8");
|
|
542
|
+
if (size > policy.maxFileSize) {
|
|
543
|
+
violations.push(`file size ${size} exceeds limit ${policy.maxFileSize}`);
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
// Delete tools
|
|
548
|
+
if (DELETE_TOOLS.has(toolName)) {
|
|
549
|
+
const filePath = (input.path ?? input.file_path);
|
|
550
|
+
if (filePath) {
|
|
551
|
+
const result = this.canDeleteFile(filePath);
|
|
552
|
+
if (!result.allowed) {
|
|
553
|
+
violations.push(`file delete blocked: ${result.reason}`);
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
// Shell tools
|
|
558
|
+
if (SHELL_TOOLS.has(toolName)) {
|
|
559
|
+
const command = (input.command ?? input.cmd);
|
|
560
|
+
if (command) {
|
|
561
|
+
const result = this.canExecuteShell(command);
|
|
562
|
+
if (!result.allowed) {
|
|
563
|
+
violations.push(`shell exec blocked: ${result.reason}`);
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
return {
|
|
568
|
+
allowed: violations.length === 0,
|
|
569
|
+
violations,
|
|
570
|
+
};
|
|
571
|
+
}
|
|
572
|
+
// ──────────────────────────────────────────────────────────────────
|
|
573
|
+
// Monitoring
|
|
574
|
+
// ──────────────────────────────────────────────────────────────────
|
|
575
|
+
/** Get all recorded violations */
|
|
576
|
+
getViolations() {
|
|
577
|
+
return [...this.state.violations];
|
|
578
|
+
}
|
|
579
|
+
/** Get current sandbox state (readonly snapshot) */
|
|
580
|
+
getState() {
|
|
581
|
+
return { ...this.state, violations: [...this.state.violations] };
|
|
582
|
+
}
|
|
583
|
+
/**
|
|
584
|
+
* Reset counters for a new session.
|
|
585
|
+
*
|
|
586
|
+
* @param sessionId - New session identifier
|
|
587
|
+
*/
|
|
588
|
+
reset(sessionId) {
|
|
589
|
+
this.state = {
|
|
590
|
+
currentTier: this.config.defaultTier,
|
|
591
|
+
sessionId,
|
|
592
|
+
writeCount: 0,
|
|
593
|
+
shellCount: 0,
|
|
594
|
+
violations: [],
|
|
595
|
+
escalationHistory: [],
|
|
596
|
+
};
|
|
597
|
+
}
|
|
598
|
+
// ──────────────────────────────────────────────────────────────────
|
|
599
|
+
// Private Helpers
|
|
600
|
+
// ──────────────────────────────────────────────────────────────────
|
|
601
|
+
/**
|
|
602
|
+
* Build the default tier policies (T0–T4).
|
|
603
|
+
*
|
|
604
|
+
* @returns Map of tier level to policy definition
|
|
605
|
+
*/
|
|
606
|
+
buildDefaultTiers() {
|
|
607
|
+
const tiers = new Map();
|
|
608
|
+
// ── T0: Read-Only ──
|
|
609
|
+
tiers.set(0, {
|
|
610
|
+
tier: 0,
|
|
611
|
+
name: "Read-Only",
|
|
612
|
+
description: "File read, grep, glob only. No writes, no shell, no network.",
|
|
613
|
+
fileRead: true,
|
|
614
|
+
fileWrite: false,
|
|
615
|
+
fileDelete: false,
|
|
616
|
+
allowedWritePaths: [],
|
|
617
|
+
blockedWritePaths: [],
|
|
618
|
+
shellExec: false,
|
|
619
|
+
allowedCommands: [],
|
|
620
|
+
blockedCommands: [],
|
|
621
|
+
maxExecTime: 0,
|
|
622
|
+
networkAccess: false,
|
|
623
|
+
allowedHosts: [],
|
|
624
|
+
blockedHosts: [],
|
|
625
|
+
maxFileSize: DEFAULT_MAX_FILE_SIZE,
|
|
626
|
+
maxTotalWrites: 0,
|
|
627
|
+
maxShellCalls: 0,
|
|
628
|
+
});
|
|
629
|
+
// ── T1: Write-Restricted ──
|
|
630
|
+
tiers.set(1, {
|
|
631
|
+
tier: 1,
|
|
632
|
+
name: "Write-Restricted",
|
|
633
|
+
description: "Read + specific file writes (src/test). No shell, no network.",
|
|
634
|
+
fileRead: true,
|
|
635
|
+
fileWrite: true,
|
|
636
|
+
fileDelete: false,
|
|
637
|
+
allowedWritePaths: ["src/**", "test/**", "tests/**"],
|
|
638
|
+
blockedWritePaths: [
|
|
639
|
+
"**/node_modules/**",
|
|
640
|
+
"**/.env*",
|
|
641
|
+
"**/package-lock.json",
|
|
642
|
+
"**/pnpm-lock.yaml",
|
|
643
|
+
],
|
|
644
|
+
shellExec: false,
|
|
645
|
+
allowedCommands: [],
|
|
646
|
+
blockedCommands: [],
|
|
647
|
+
maxExecTime: 0,
|
|
648
|
+
networkAccess: false,
|
|
649
|
+
allowedHosts: [],
|
|
650
|
+
blockedHosts: [],
|
|
651
|
+
maxFileSize: DEFAULT_MAX_FILE_SIZE,
|
|
652
|
+
maxTotalWrites: 10,
|
|
653
|
+
maxShellCalls: 0,
|
|
654
|
+
});
|
|
655
|
+
// ── T2: Project-Scoped ──
|
|
656
|
+
tiers.set(2, {
|
|
657
|
+
tier: 2,
|
|
658
|
+
name: "Project-Scoped",
|
|
659
|
+
description: "Full project read/write, limited shell (lint/format), no network.",
|
|
660
|
+
fileRead: true,
|
|
661
|
+
fileWrite: true,
|
|
662
|
+
fileDelete: true,
|
|
663
|
+
allowedWritePaths: ["**"],
|
|
664
|
+
blockedWritePaths: [
|
|
665
|
+
"**/node_modules/**",
|
|
666
|
+
"**/.env*",
|
|
667
|
+
"**/.git/**",
|
|
668
|
+
],
|
|
669
|
+
shellExec: true,
|
|
670
|
+
allowedCommands: [
|
|
671
|
+
"tsc",
|
|
672
|
+
"eslint",
|
|
673
|
+
"prettier",
|
|
674
|
+
"cat",
|
|
675
|
+
"ls",
|
|
676
|
+
"wc",
|
|
677
|
+
"grep",
|
|
678
|
+
"find",
|
|
679
|
+
],
|
|
680
|
+
blockedCommands: [
|
|
681
|
+
"rm -rf /",
|
|
682
|
+
"sudo",
|
|
683
|
+
"chmod",
|
|
684
|
+
"chown",
|
|
685
|
+
"kill",
|
|
686
|
+
"pkill",
|
|
687
|
+
"dd",
|
|
688
|
+
"mkfs",
|
|
689
|
+
],
|
|
690
|
+
maxExecTime: 30_000,
|
|
691
|
+
networkAccess: false,
|
|
692
|
+
allowedHosts: [],
|
|
693
|
+
blockedHosts: [],
|
|
694
|
+
maxFileSize: DEFAULT_MAX_FILE_SIZE,
|
|
695
|
+
maxTotalWrites: 50,
|
|
696
|
+
maxShellCalls: 20,
|
|
697
|
+
});
|
|
698
|
+
// ── T3: Build-Enabled ──
|
|
699
|
+
tiers.set(3, {
|
|
700
|
+
tier: 3,
|
|
701
|
+
name: "Build-Enabled",
|
|
702
|
+
description: "Full project access, all shell (except blocked), localhost + registry network.",
|
|
703
|
+
fileRead: true,
|
|
704
|
+
fileWrite: true,
|
|
705
|
+
fileDelete: true,
|
|
706
|
+
allowedWritePaths: ["**"],
|
|
707
|
+
blockedWritePaths: [
|
|
708
|
+
"**/node_modules/**",
|
|
709
|
+
"**/.env*",
|
|
710
|
+
"**/.git/**",
|
|
711
|
+
],
|
|
712
|
+
shellExec: true,
|
|
713
|
+
allowedCommands: ["*"],
|
|
714
|
+
blockedCommands: [
|
|
715
|
+
"sudo",
|
|
716
|
+
"chmod 777",
|
|
717
|
+
"rm -rf /",
|
|
718
|
+
"dd",
|
|
719
|
+
"mkfs",
|
|
720
|
+
"curl",
|
|
721
|
+
"wget",
|
|
722
|
+
],
|
|
723
|
+
maxExecTime: 120_000,
|
|
724
|
+
networkAccess: true,
|
|
725
|
+
allowedHosts: [
|
|
726
|
+
"localhost",
|
|
727
|
+
"127.0.0.1",
|
|
728
|
+
"registry.npmjs.org",
|
|
729
|
+
"registry.yarnpkg.com",
|
|
730
|
+
],
|
|
731
|
+
blockedHosts: [],
|
|
732
|
+
maxFileSize: DEFAULT_MAX_FILE_SIZE,
|
|
733
|
+
maxTotalWrites: 200,
|
|
734
|
+
maxShellCalls: 50,
|
|
735
|
+
});
|
|
736
|
+
// ── T4: Full-Network ──
|
|
737
|
+
tiers.set(4, {
|
|
738
|
+
tier: 4,
|
|
739
|
+
name: "Full-Network",
|
|
740
|
+
description: "Full access with external network. Cloud metadata endpoints blocked.",
|
|
741
|
+
fileRead: true,
|
|
742
|
+
fileWrite: true,
|
|
743
|
+
fileDelete: true,
|
|
744
|
+
allowedWritePaths: ["**"],
|
|
745
|
+
blockedWritePaths: [
|
|
746
|
+
"**/node_modules/**",
|
|
747
|
+
"**/.env*",
|
|
748
|
+
"**/.git/**",
|
|
749
|
+
],
|
|
750
|
+
shellExec: true,
|
|
751
|
+
allowedCommands: ["*"],
|
|
752
|
+
blockedCommands: ["sudo", "rm -rf /", "dd", "mkfs"],
|
|
753
|
+
maxExecTime: 300_000,
|
|
754
|
+
networkAccess: true,
|
|
755
|
+
allowedHosts: ["*"],
|
|
756
|
+
blockedHosts: [
|
|
757
|
+
"169.254.169.254",
|
|
758
|
+
"metadata.google.internal",
|
|
759
|
+
],
|
|
760
|
+
maxFileSize: DEFAULT_MAX_FILE_SIZE,
|
|
761
|
+
maxTotalWrites: 500,
|
|
762
|
+
maxShellCalls: 100,
|
|
763
|
+
});
|
|
764
|
+
return tiers;
|
|
765
|
+
}
|
|
766
|
+
/**
|
|
767
|
+
* Check if a relative path matches any of the given glob patterns.
|
|
768
|
+
* Uses a simplified glob matcher (supports `**`, `*`, and `?`).
|
|
769
|
+
*
|
|
770
|
+
* @param relativePath - Path relative to the project root
|
|
771
|
+
* @param patterns - Glob patterns to match against
|
|
772
|
+
* @returns true if the path matches any pattern
|
|
773
|
+
*/
|
|
774
|
+
matchesPattern(relativePath, patterns) {
|
|
775
|
+
for (const pattern of patterns) {
|
|
776
|
+
if (this.globMatch(relativePath, pattern)) {
|
|
777
|
+
return true;
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
return false;
|
|
781
|
+
}
|
|
782
|
+
/**
|
|
783
|
+
* Simple glob matcher supporting `**` (any path segments), `*` (any chars
|
|
784
|
+
* within a segment), and `?` (single char).
|
|
785
|
+
*
|
|
786
|
+
* @param str - String to test
|
|
787
|
+
* @param pattern - Glob pattern
|
|
788
|
+
* @returns true if the string matches the pattern
|
|
789
|
+
*/
|
|
790
|
+
globMatch(str, pattern) {
|
|
791
|
+
// Convert glob to regex
|
|
792
|
+
let regexStr = "^";
|
|
793
|
+
let i = 0;
|
|
794
|
+
while (i < pattern.length) {
|
|
795
|
+
const char = pattern[i];
|
|
796
|
+
if (char === "*" && pattern[i + 1] === "*") {
|
|
797
|
+
// `**` — match any path segments (including none)
|
|
798
|
+
regexStr += ".*";
|
|
799
|
+
i += 2;
|
|
800
|
+
// Skip trailing slash after **
|
|
801
|
+
if (pattern[i] === "/") {
|
|
802
|
+
i++;
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
else if (char === "*") {
|
|
806
|
+
// `*` — match any chars except `/`
|
|
807
|
+
regexStr += "[^/]*";
|
|
808
|
+
i++;
|
|
809
|
+
}
|
|
810
|
+
else if (char === "?") {
|
|
811
|
+
regexStr += "[^/]";
|
|
812
|
+
i++;
|
|
813
|
+
}
|
|
814
|
+
else if (".+()[]{}^$|\\".includes(char)) {
|
|
815
|
+
regexStr += "\\" + char;
|
|
816
|
+
i++;
|
|
817
|
+
}
|
|
818
|
+
else {
|
|
819
|
+
regexStr += char;
|
|
820
|
+
i++;
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
regexStr += "$";
|
|
824
|
+
try {
|
|
825
|
+
return new RegExp(regexStr).test(str);
|
|
826
|
+
}
|
|
827
|
+
catch {
|
|
828
|
+
return false;
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
/**
|
|
832
|
+
* Normalize and resolve a file path to an absolute path.
|
|
833
|
+
*
|
|
834
|
+
* @param filePath - The file path to normalize
|
|
835
|
+
* @returns Absolute resolved path
|
|
836
|
+
*/
|
|
837
|
+
normalizePath(filePath) {
|
|
838
|
+
const resolved = path.isAbsolute(filePath)
|
|
839
|
+
? path.resolve(filePath)
|
|
840
|
+
: path.resolve(this.config.projectPath, filePath);
|
|
841
|
+
// Defense against symlink traversal: use realpath-equivalent check.
|
|
842
|
+
// path.resolve normalizes ".." but cannot detect symlinks at this layer.
|
|
843
|
+
// The actual symlink resolution must happen at the filesystem layer (tools).
|
|
844
|
+
// Here we ensure the resolved path does not escape via ".." normalization.
|
|
845
|
+
const normalizedProject = path.resolve(this.config.projectPath);
|
|
846
|
+
if (!resolved.startsWith(normalizedProject + path.sep) && resolved !== normalizedProject) {
|
|
847
|
+
// Return a path that will definitely fail startsWith checks
|
|
848
|
+
return resolved;
|
|
849
|
+
}
|
|
850
|
+
return resolved;
|
|
851
|
+
}
|
|
852
|
+
/**
|
|
853
|
+
* Convert an absolute path to a project-relative path.
|
|
854
|
+
*
|
|
855
|
+
* @param absolutePath - Absolute file path
|
|
856
|
+
* @returns Path relative to the project root
|
|
857
|
+
*/
|
|
858
|
+
toRelative(absolutePath) {
|
|
859
|
+
return path.relative(this.config.projectPath, absolutePath);
|
|
860
|
+
}
|
|
861
|
+
/**
|
|
862
|
+
* Record a sandbox violation and emit the appropriate event.
|
|
863
|
+
*
|
|
864
|
+
* @param action - What action was attempted
|
|
865
|
+
* @param resource - The resource involved
|
|
866
|
+
* @param rule - Which rule was violated
|
|
867
|
+
* @param blocked - Whether the action was blocked
|
|
868
|
+
*/
|
|
869
|
+
recordViolation(action, resource, rule, blocked) {
|
|
870
|
+
const violation = {
|
|
871
|
+
tier: this.state.currentTier,
|
|
872
|
+
action,
|
|
873
|
+
resource,
|
|
874
|
+
rule,
|
|
875
|
+
timestamp: Date.now(),
|
|
876
|
+
blocked,
|
|
877
|
+
};
|
|
878
|
+
// Cap violations array to prevent unbounded memory growth
|
|
879
|
+
if (this.state.violations.length >= MAX_VIOLATIONS) {
|
|
880
|
+
// Keep the last half + new entry (preserve recent violations)
|
|
881
|
+
this.state.violations = this.state.violations.slice(-Math.floor(MAX_VIOLATIONS / 2));
|
|
882
|
+
}
|
|
883
|
+
this.state.violations.push(violation);
|
|
884
|
+
if (blocked) {
|
|
885
|
+
this.emit("violation:blocked", violation);
|
|
886
|
+
}
|
|
887
|
+
else {
|
|
888
|
+
this.emit("violation:warned", violation);
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
/**
|
|
892
|
+
* Extract the base command name from a full command string.
|
|
893
|
+
*
|
|
894
|
+
* @param command - Full command string (e.g. "pnpm install lodash")
|
|
895
|
+
* @returns The first token / executable name (e.g. "pnpm")
|
|
896
|
+
*/
|
|
897
|
+
extractCommand(command) {
|
|
898
|
+
const trimmed = command.trim();
|
|
899
|
+
// Handle env vars prefix (e.g. "NODE_ENV=prod tsc")
|
|
900
|
+
const parts = trimmed.split(/\s+/);
|
|
901
|
+
for (const part of parts) {
|
|
902
|
+
if (!part.includes("=")) {
|
|
903
|
+
// Strip path prefix (e.g. "/usr/bin/node" → "node")
|
|
904
|
+
return path.basename(part);
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
return parts[0] ?? "";
|
|
908
|
+
}
|
|
909
|
+
/**
|
|
910
|
+
* Check if a command is a build-related command.
|
|
911
|
+
*
|
|
912
|
+
* @param command - The extracted command name
|
|
913
|
+
* @returns true if it's a build command
|
|
914
|
+
*/
|
|
915
|
+
isBuildCommand(command) {
|
|
916
|
+
return BUILD_COMMANDS.has(command);
|
|
917
|
+
}
|
|
918
|
+
/**
|
|
919
|
+
* Check if a command is a network-related command.
|
|
920
|
+
*
|
|
921
|
+
* @param command - The extracted command name
|
|
922
|
+
* @returns true if it requires network access
|
|
923
|
+
*/
|
|
924
|
+
isNetworkCommand(command) {
|
|
925
|
+
return NETWORK_COMMANDS.has(command);
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
//# sourceMappingURL=sandbox-tiers.js.map
|