Q117: commands run in an allowlisted environment, and records and approvals say host isolation is advisory

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ZacharyZhang-NYandClaude Opus 5.5 committed 2026-10-08 19:07:28 -04:00
1 parent d67996fd25
commit cb1a4be167
10 files changed
+103 -6

No files matched your search

+2 -2
View File
@@ -12,7 +12,7 @@ import { TaskMutations } from "./task-mutations";
import { normalizeAvatarColor, normalizeAvatarImage } from "./avatar-values";
import { approvalDecision, autonomy } from "./org-approvals";
import { admitAgent } from "./agent-admission";
import { TASK_APPROVAL_KINDS_SQL, TASK_SNAPSHOT_SQL } from "./task-approval";
import { HOST_ACCESS_NOTE, TASK_APPROVAL_KINDS_SQL, TASK_SNAPSHOT_SQL } from "./task-approval";
export class OrganizationService {
private readonly audit: AuditService;
@@ -302,7 +302,7 @@ export class OrganizationService {
id, org_id, kind, title, details, risk, requested_by_type, requested_by_id, source_entity_type, source_entity_id,
state, decided_by_employee_id, created_at, decided_at, subject_snapshot
) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`).run(approvalId, orgId, approvalKind, task.title, task.description, RISK_LEVEL.HIGH, "system", null, "task", taskId,
`).run(approvalId, orgId, approvalKind, task.title, task.requires_filesystem === 1 ? `${task.description}\n\n${HOST_ACCESS_NOTE}` : task.description, RISK_LEVEL.HIGH, "system", null, "task", taskId,
decision.state, decision.decidedBy, now, decision.decidedAt, task.snapshot);
this.settleApproval(orgId, approvalId, approvalKind, RISK_LEVEL.HIGH, autoApprove);
}
+3
View File
@@ -2,3 +2,6 @@
export const TASK_SNAPSHOT_SQL = "json_object('title', t.title, 'description', t.description, 'assignee', t.assignee_agent_id, 'risk', t.risk, 'workspace', t.workspace_id, 'filesystem', t.requires_filesystem)";
export const TASK_APPROVAL_KINDS_SQL = "('task_risk', 'task_execution')";
/** What approving file access really allows: commands are not confined to the workspace. */
export const HOST_ACCESS_NOTE = "Approving lets this task run commands on this computer with your user's file access: they can read and change files outside the workspace. Only their environment is cleaned of keys and your profile.";
@@ -113,11 +113,14 @@ export function createIsolationContext(
if (task.requires_filesystem === 1 && !workspace) throw new Error("Filesystem tasks require a workspace.");
const serviceIdentityId = requireServiceIdentity(db, orgId, agent.id);
const executionContextId = createId("ctx");
// The mount mode guides the tools; the host does not enforce it, and commands run with the user's file access in an allowlisted environment.
const workspaceMounts = workspace ? [{
...workspaceAccess(task, workspace)[0],
name: workspace.name,
localPath: workspace.local_path,
repoRef: workspace.repo_ref,
hostEnforcement: "advisory",
commandEnvironment: "allowlisted",
}] : [];
return {
executionContextId,
+20 -1
View File
@@ -1,8 +1,14 @@
import { spawn, spawnSync } from "node:child_process";
import { createHash } from "node:crypto";
import { mkdirSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { currentRunSignal } from "./run-control";
const COMMAND_TIMEOUT_MS = 120_000;
const KEEP_BYTES = 8_000;
/** What a command may inherit: enough for the OS and toolchains to run, nothing that carries the user's keys, tokens or profile. */
const INHERITED_ENV = ["PATH", "PATHEXT", "SystemRoot", "SystemDrive", "windir", "ComSpec", "NUMBER_OF_PROCESSORS", "PROCESSOR_ARCHITECTURE", "OS", "LANG", "LC_ALL", "TERM"];
/** Keeps the first and last KEEP_BYTES of a stream and counts what it drops. */
export class BoundedOutput {
@@ -47,7 +53,7 @@ export function runCommand(workspaceRoot: string, command: string, options: { ti
const timeoutMs = options.timeoutMs ?? COMMAND_TIMEOUT_MS;
return new Promise((resolvePromise) => {
// Its own process group on POSIX, so the whole tree can be killed at once.
const child = spawn(command, { shell: true, cwd: workspaceRoot, windowsHide: true, detached: process.platform !== "win32" });
const child = spawn(command, { shell: true, cwd: workspaceRoot, env: commandEnvironment(workspaceRoot), windowsHide: true, detached: process.platform !== "win32" });
const output = new BoundedOutput();
let settled = false;
const runSignal = currentRunSignal();
@@ -79,6 +85,19 @@ export function runCommand(workspaceRoot: string, command: string, options: { ti
});
}
/** A command's environment: the allowlisted variables plus a home and temp folder of the workspace's own, so ~ is not the user's profile. */
export function commandEnvironment(workspaceRoot: string): NodeJS.ProcessEnv {
const home = join(tmpdir(), `agentcompany-home-${createHash("sha256").update(workspaceRoot).digest("hex").slice(0, 12)}`);
const temp = join(home, "tmp");
mkdirSync(temp, { recursive: true });
const env: NodeJS.ProcessEnv = { HOME: home, USERPROFILE: home, APPDATA: join(home, "AppData", "Roaming"), LOCALAPPDATA: join(home, "AppData", "Local"), TMP: temp, TEMP: temp, TMPDIR: temp };
for (const name of INHERITED_ENV) {
const value = process.env[name];
if (value !== undefined) env[name] = value;
}
return env;
}
/** The tool result for a finished command; no exit code means it was killed, which is never success. */
export function formatCommandResult(code: number | null, signal: NodeJS.Signals | null, text: string): string {
const body = text.trim() || "(no output)";