Skip to content

Commit 2731c2f

Browse files
committed
Add opt-in native subprocess sandbox
1 parent 944f850 commit 2731c2f

29 files changed

Lines changed: 1870 additions & 102 deletions

src/autoresearch/state.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ export interface AutoresearchRun {
2121
parsed_metrics: Record<string, number>;
2222
parsed_primary: number | null;
2323
asi: JsonObject;
24+
sandbox?: JsonObject;
2425
completed_at: string;
2526
}
2627

@@ -34,6 +35,7 @@ export interface HarnessValidation {
3435
output_resource_uri?: string;
3536
timed_out?: boolean;
3637
output_truncated?: boolean;
38+
sandbox?: JsonObject;
3739
message: string;
3840
validated_at: string;
3941
}
@@ -436,6 +438,7 @@ function cloneHarnessValidation(status: HarnessValidation): HarnessValidation {
436438
return {
437439
...status,
438440
parsed_metrics: { ...status.parsed_metrics },
441+
sandbox: status.sandbox ? { ...status.sandbox } : undefined,
439442
};
440443
}
441444

@@ -460,6 +463,7 @@ function parseRun(value: unknown): AutoresearchRun | undefined {
460463
parsed_metrics: numericRecord(data.parsed_metrics),
461464
parsed_primary: typeof data.parsed_primary === "number" && Number.isFinite(data.parsed_primary) ? data.parsed_primary : null,
462465
asi: objectRecord(data.asi),
466+
sandbox: objectRecordOrUndefined(data.sandbox),
463467
completed_at: typeof data.completed_at === "string" ? data.completed_at : "",
464468
};
465469
}
@@ -484,6 +488,7 @@ function parseHarnessValidation(value: unknown): HarnessValidation | undefined {
484488
output_resource_uri: typeof data.output_resource_uri === "string" ? data.output_resource_uri : undefined,
485489
timed_out: data.timed_out === true ? true : undefined,
486490
output_truncated: data.output_truncated === true ? true : undefined,
491+
sandbox: objectRecordOrUndefined(data.sandbox),
487492
message,
488493
validated_at: typeof data.validated_at === "string" ? data.validated_at : "",
489494
};
@@ -603,6 +608,10 @@ function objectRecord(value: unknown): JsonObject {
603608
return value && typeof value === "object" && !Array.isArray(value) ? (value as JsonObject) : {};
604609
}
605610

611+
function objectRecordOrUndefined(value: unknown): JsonObject | undefined {
612+
return value && typeof value === "object" && !Array.isArray(value) ? (value as JsonObject) : undefined;
613+
}
614+
606615
function positiveIntOrUndefined(value: unknown): number | undefined {
607616
return typeof value === "number" && Number.isInteger(value) && value > 0 ? value : undefined;
608617
}

src/code-intelligence/codegraph-engine.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,7 @@ export class CodeGraphContextEngine {
155155
constructor(
156156
private readonly config: ContextEngineConfig,
157157
private readonly workspace: WorkspaceIdentity,
158+
private readonly agentConfig?: VllmAgentConfig,
158159
) {
159160
this.statusSnapshot = this.enabled()
160161
? { provider: "codegraph", state: "idle", watcher: "inactive" }
@@ -247,7 +248,7 @@ export class CodeGraphContextEngine {
247248
private async runIndexLifecycle(reason: string, options: { force?: boolean; signal?: AbortSignal }): Promise<ContextEngineStatus> {
248249
this.update({ provider: "codegraph", state: "indexing", phase: "initializing", current: undefined, total: undefined, error: undefined, reason });
249250
try {
250-
await ensureCodeGraphIgnored(this.workspace.root);
251+
await ensureCodeGraphIgnored(this.workspace, this.agentConfig);
251252
const { CodeGraph } = loadCodeGraphModule();
252253
const initialized = CodeGraph.isInitialized(this.workspace.root);
253254
const cg = initialized ? await CodeGraph.open(this.workspace.root) : await CodeGraph.init(this.workspace.root, { index: false });
@@ -772,8 +773,12 @@ function languagesFromStats(stats: CodeGraphStats): string[] | undefined {
772773
.sort();
773774
}
774775

775-
async function ensureCodeGraphIgnored(workspaceRoot: string): Promise<void> {
776-
const result = await runSmallCommand("git", ["rev-parse", "--git-dir"], workspaceRoot, 2000);
776+
async function ensureCodeGraphIgnored(workspace: WorkspaceIdentity, config?: VllmAgentConfig): Promise<void> {
777+
if (config?.sandbox.mode && config.sandbox.mode !== "off") {
778+
return;
779+
}
780+
const workspaceRoot = workspace.root;
781+
const result = await runSmallCommand("git", ["rev-parse", "--git-dir"], workspaceRoot, 2000, config ? { config, workspace } : undefined);
777782
if (result.code !== 0 || !result.stdout.trim()) {
778783
return;
779784
}

src/code-intelligence/hub.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ export class CodeIntelligenceHub {
2727
private readonly config: VllmAgentConfig,
2828
private readonly workspace: WorkspaceIdentity,
2929
) {
30-
this.codegraph = new CodeGraphContextEngine(normalizeContextEngineConfig(config), workspace);
30+
this.codegraph = new CodeGraphContextEngine(normalizeContextEngineConfig(config), workspace, config);
3131
}
3232

3333
dispose(): void {

src/config/config.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -222,7 +222,7 @@ function stringValue(value: unknown): string | undefined {
222222
}
223223

224224
function pruneConfig(config: VllmAgentConfig): void {
225-
pruneKeys(config, ["workspace", "model_setup", "model_retry", "omni", "permissions", "context", "skills", "web_search", "rtk", "daemon"]);
225+
pruneKeys(config, ["workspace", "model_setup", "model_retry", "omni", "permissions", "sandbox", "context", "skills", "web_search", "rtk", "daemon"]);
226226
pruneKeys(config.workspace, ["root"]);
227227
pruneKeys(config.model_setup, ["mode", "provider", "provider_id", "profile", "router", "base_url", "model", "api_key_ref", "api_key", "headers", "context_window"]);
228228
pruneKeys(config.model_retry, [
@@ -251,6 +251,7 @@ function pruneConfig(config: VllmAgentConfig): void {
251251
for (const policy of Object.values(config.permissions?.workspaces ?? {})) {
252252
pruneKeys(policy, ["mode", "custom"]);
253253
}
254+
pruneKeys(config.sandbox, ["mode", "backend", "network", "fail_if_unavailable", "extra_writable_roots", "env_passthrough"]);
254255
pruneKeys(config.context, ["compression_threshold", "context_window", "protected_recent_loops", "force_compression", "engine"]);
255256
pruneKeys(config.context?.engine, ["provider", "startup", "require_ready_before_chat", "watch"]);
256257
pruneKeys(config.skills, ["enabled", "managed_installs"]);

src/config/defaults.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,14 @@ export const DEFAULT_CONFIG: VllmAgentConfig = {
2121
permissions: {
2222
mode: "full_access",
2323
},
24+
sandbox: {
25+
mode: "off",
26+
backend: "auto",
27+
network: "restricted",
28+
fail_if_unavailable: true,
29+
extra_writable_roots: [],
30+
env_passthrough: [],
31+
},
2432
context: {
2533
compression_threshold: 0.8,
2634
context_window: 32_768,

src/rtk/command.ts

Lines changed: 26 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
import { spawn } from "node:child_process";
22
import path from "node:path";
33
import { ensureDir } from "../util/fs.js";
4-
import type { VllmAgentConfig } from "../types.js";
4+
import type { VllmAgentConfig, WorkspaceIdentity } from "../types.js";
55
import type { SessionStore } from "../session/store.js";
66
import { resolveRtkStatus, rtkDbPath, rtkEnv, type RtkRuntime } from "./manager.js";
77
import { readRtkCommandStats, type RtkCommandStats } from "./stats.js";
8+
import { runSandboxedProcess } from "../sandbox/runner.js";
9+
import type { SandboxExecutionInfo } from "../sandbox/types.js";
810

911
export interface RtkShellCommandOptions {
1012
config: VllmAgentConfig;
@@ -15,6 +17,7 @@ export interface RtkShellCommandOptions {
1517
tool_name: string;
1618
command: string;
1719
cwd: string;
20+
workspace: WorkspaceIdentity;
1821
env?: NodeJS.ProcessEnv;
1922
timeout_ms: number;
2023
}
@@ -27,6 +30,7 @@ export interface RtkShellCommandResult {
2730
command: string;
2831
rewritten_command?: string;
2932
rtk?: RtkCommandStats;
33+
sandbox?: SandboxExecutionInfo;
3034
}
3135

3236
interface PreparedRtkCommand {
@@ -40,7 +44,7 @@ interface PreparedRtkCommand {
4044

4145
export async function runRtkAwareShellCommand(options: RtkShellCommandOptions): Promise<RtkShellCommandResult> {
4246
const prepared = await prepareCommand(options);
43-
const result = await runShell(prepared.command, options.cwd, prepared.env, options.timeout_ms);
47+
const result = await runShell(prepared.command, options.cwd, prepared.env, options.timeout_ms, options.config, options.workspace, prepared.original_command, prepared.rewritten_command);
4448
const rtk = await recordRtkSavings(options, prepared);
4549
return {
4650
...result,
@@ -146,39 +150,26 @@ async function recordRtkSavings(options: RtkShellCommandOptions, prepared: Prepa
146150
return stats;
147151
}
148152

149-
function runShell(command: string, cwd: string, env: NodeJS.ProcessEnv, timeoutMs: number): Promise<Omit<RtkShellCommandResult, "command">> {
150-
return new Promise((resolve) => {
151-
const child = spawn(command, {
152-
cwd,
153-
env,
154-
shell: true,
155-
});
156-
let stdout = "";
157-
let stderr = "";
158-
let timedOut = false;
159-
const timeout = setTimeout(() => {
160-
timedOut = true;
161-
child.kill("SIGTERM");
162-
setTimeout(() => {
163-
if (!child.killed) {
164-
child.kill("SIGKILL");
165-
}
166-
}, 2000).unref();
167-
}, timeoutMs);
168-
child.stdout.on("data", (chunk) => {
169-
stdout += String(chunk);
170-
});
171-
child.stderr.on("data", (chunk) => {
172-
stderr += String(chunk);
173-
});
174-
child.on("close", (code) => {
175-
clearTimeout(timeout);
176-
resolve({ code, stdout, stderr, timed_out: timedOut });
177-
});
178-
child.on("error", (error) => {
179-
clearTimeout(timeout);
180-
resolve({ code: 127, stdout, stderr: error.message, timed_out: timedOut });
181-
});
153+
async function runShell(
154+
command: string,
155+
cwd: string,
156+
env: NodeJS.ProcessEnv,
157+
timeoutMs: number,
158+
config: VllmAgentConfig,
159+
workspace: WorkspaceIdentity,
160+
originalCommand: string,
161+
rewrittenCommand?: string,
162+
): Promise<Omit<RtkShellCommandResult, "command">> {
163+
return await runSandboxedProcess({
164+
config,
165+
workspace,
166+
command,
167+
shell: true,
168+
cwd,
169+
env,
170+
timeoutMs,
171+
originalCommand,
172+
rewrittenCommand,
182173
});
183174
}
184175

0 commit comments

Comments
 (0)