@lazyingart/agintiflow 0.5.0 → 0.6.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/README.md CHANGED
@@ -72,6 +72,7 @@ aginti sessions list
72
72
  aginti sessions show <session-id>
73
73
  aginti resume <session-id> "continue with a short follow-up"
74
74
  aginti --profile code --provider mock --routing manual "Create notes/hello.md"
75
+ aginti --allow-shell --sandbox-mode docker-workspace --approve-package-installs "set up and test this project"
75
76
  ```
76
77
 
77
78
  Run from a source checkout:
@@ -181,12 +182,12 @@ AgInTiFlow is intentionally conservative:
181
182
  - Shell commands are disabled unless the shell tool is enabled.
182
183
  - Workspace file tools stay inside `commandCwd` and block `.env`, secret-like paths, `.git`, node_modules writes, absolute escapes, binary files, and huge files.
183
184
  - File writes record before/after SHA-256 hashes and compact redacted diffs.
184
- - Guarded shell mode only allows inspection, test/build checks, and approved setup commands.
185
+ - Guarded shell mode is policy-based: normal inspection/tests are available, Docker workspace mode can run broader setup/network commands when package installs are approved, and host privileged/destructive work requires explicit trust.
185
186
  - Docker read-only mode mounts the workspace read-only and disables container network access.
186
187
  - Docker workspace-write mode is the web UI default so plot, PDF, and test outputs can be written inside the mounted workspace.
187
- - Docker workspace-write mode is required for approved package or environment setup.
188
+ - Docker workspace-write mode is the preferred place for approved package or environment setup, including `apt-get`, `npm`, `pip`, `curl`, and `wget` workflows inside the container.
188
189
  - Package installs default to `prompt`, so npm/pip/conda/venv setup is blocked until explicitly approved.
189
- - NPM publishing, npm token commands, sudo, destructive git actions, curl/wget, and shell chaining are blocked.
190
+ - NPM publishing, npm token commands, API-key reads, and credential files stay blocked. Broader shell commands require either Docker workspace mode with package policy `allow`, or explicit host trust.
190
191
  - NPM tokens and API keys are redacted from tool logs and API responses.
191
192
  - Every tool request and result is written to structured logs.
192
193
 
@@ -311,9 +312,9 @@ The setup script idempotently builds `agintiflow-sandbox:latest` from `docker/sa
311
312
 
312
313
  | Mode | Workspace mount | Network | Intended use |
313
314
  | --- | --- | --- | --- |
314
- | `host` | local process | host network | legacy read-only inspection only |
315
+ | `host` | local process | host network | direct project work; privileged/destructive commands require explicit trust |
315
316
  | `docker-readonly` | read-only `/workspace` | none | safe coding inspection and tests that do not write files |
316
- | `docker-workspace` | writable `/workspace` | none by default, enabled only for approved package installs | web UI default for environment setup, plotting, and LaTeX/PDF compilation inside the mounted project |
317
+ | `docker-workspace` | writable `/workspace` | none by default, enabled for approved package/setup/network commands | web UI default for environment setup, plotting, LaTeX/PDF, dependency installs, and broad toolchain work inside the mounted project |
317
318
 
318
319
  Package policy values:
319
320
 
@@ -321,7 +322,7 @@ Package policy values:
321
322
  | --- | --- |
322
323
  | `block` | Always block npm/pip/conda/venv setup. |
323
324
  | `prompt` | Return a clear approval-required error; the UI can switch to approved. |
324
- | `allow` | Permit allowlisted setup commands only in `docker-workspace`. |
325
+ | `allow` | Permit package/setup commands. Docker workspace mode also allows broader shell/network commands while keeping secrets and npm publishing blocked. |
325
326
 
326
327
  Toolchain commands such as `python3 plot.py`, `latexmk -pdf paper.tex`, and `pdflatex -interaction=nonstopmode -halt-on-error paper.tex` are allowlisted only when the shell tool is enabled. In Docker mode they require `docker-workspace` because they write outputs back to `/workspace`. File and canvas tools accept both normal relative paths and Docker virtual paths like `/workspace/report.pdf`, while other absolute host paths remain blocked.
327
328
 
@@ -43,8 +43,8 @@ AGINTIFLOW_REAL_DEEPSEEK=1 AGINTIFLOW_REAL_CASES=flash,pro npm run real:deepseek
43
43
  - `flash`: DeepSeek v4 flash creates a dependency-free Node/HTML app with `node:test` tests and runs `npm --prefix <app> test` when safe.
44
44
  - `pro`: DeepSeek v4 pro resumes/improves the app and expands tests.
45
45
  - `latex`: creates `.tex` source and compiles only if a TeX toolchain is available; otherwise writes an honest setup artifact.
46
- - `website`: creates a dependency-free website test sample and runs safe tests when dependencies are present.
47
- - `maintenance`: creates dry-run maintenance scripts/plans under `maintenance/` and validates script syntax; no global installs.
46
+ - `website`: creates a website test sample and runs checks with the configured shell/package policy.
47
+ - `maintenance`: verifies the agent can prepare project-local maintenance work and use Docker/package policy for broader setup commands without exposing secrets.
48
48
  - `aaps`: creates a project-local `.aaps` sample and notes for `@lazyingart/aaps` workflows without publishing.
49
49
  - `web`: when `AGINTIFLOW_REAL_WEB_BASE_URL` is set, starts a web run and verifies CLI/web session sharing.
50
50
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lazyingart/agintiflow",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "type": "module",
5
5
  "description": "AgInTiFlow is a resumable Playwright website-control agent with OpenAI-compatible tool calling.",
6
6
  "license": "Apache-2.0",
@@ -308,7 +308,7 @@ if (cases.has("website")) {
308
308
  const websiteDir = `round9/website-test-${stamp}`;
309
309
  const run = await runCliCase(
310
310
  "website-test",
311
- `Create a small website under ${websiteDir} with a simple local test or check file. Run safe checks if you can without installing packages.`,
311
+ `Create a small website under ${websiteDir} with a simple local test or check file. Run useful checks under the configured policy.`,
312
312
  { routingMode: "fast", taskProfile: "website", maxSteps: 24 }
313
313
  );
314
314
  report.runs.push(run);
@@ -333,12 +333,12 @@ if (cases.has("maintenance")) {
333
333
  const maintenanceDir = `maintenance/round9-${stamp}`;
334
334
  const run = await runCliCase(
335
335
  "maintenance-plan",
336
- `Create project-local dry-run maintenance plans and scripts under ${maintenanceDir} for Miniforge/conda, R, Python tooling, CmdStan/CmdStanR, and PyStan. Validate scripts safely if you can.`,
336
+ `Create project-local maintenance plans and scripts under ${maintenanceDir} for Miniforge/conda, R, Python tooling, CmdStan/CmdStanR, and PyStan. Use Docker/package policy for setup checks when useful.`,
337
337
  {
338
338
  routingMode: "complex",
339
339
  taskProfile: "maintenance",
340
340
  sandboxMode: "docker-workspace",
341
- packageInstallPolicy: "block",
341
+ packageInstallPolicy: "allow",
342
342
  maxSteps: 24,
343
343
  }
344
344
  );
@@ -54,6 +54,14 @@ try {
54
54
  capabilities.maintenancePolicy.some((check) => check.command.startsWith("sudo") && !check.allowed),
55
55
  "sudo maintenance command was not blocked"
56
56
  );
57
+ assert(
58
+ capabilities.trustedDockerPolicy.some((check) => check.command.startsWith("apt-get install") && check.allowed),
59
+ "trusted Docker policy did not allow apt-get install"
60
+ );
61
+ assert(
62
+ capabilities.trustedDockerPolicy.some((check) => check.command.startsWith("wget") && check.allowed),
63
+ "trusted Docker policy did not allow wget"
64
+ );
57
65
 
58
66
  const doctor = JSON.parse(await runCli(["doctor", "--capabilities", "--json"]));
59
67
  assert(doctor.project.root === tempRoot, "doctor --capabilities used the wrong project root");
@@ -63,7 +71,7 @@ try {
63
71
  {
64
72
  ok: true,
65
73
  projectRoot: tempRoot,
66
- checks: ["capabilities-cli", "doctor-capabilities", "maintenance-policy"],
74
+ checks: ["capabilities-cli", "doctor-capabilities", "maintenance-policy", "trusted-docker-policy"],
67
75
  },
68
76
  null,
69
77
  2
@@ -90,6 +90,9 @@ try {
90
90
  if (!capabilities.checks.some((check) => check.name === "npm-prefix-test-policy")) {
91
91
  throw new Error("capability endpoint did not include command policy checks");
92
92
  }
93
+ if (!capabilities.trustedDockerPolicy?.some((check) => check.command.startsWith("apt-get install") && check.allowed)) {
94
+ throw new Error("capability endpoint did not report trusted Docker package policy");
95
+ }
93
96
  const savedKey = await fetchJson("/api/keys/deepseek", {
94
97
  method: "POST",
95
98
  headers: { "Content-Type": "application/json" },
@@ -144,8 +144,8 @@ function createInitialState(config, sessionId) {
144
144
  "Avoid destructive actions, purchases, account changes, and sensitive workflows.",
145
145
  config.allowShellTool
146
146
  ? config.useDockerSandbox
147
- ? `A shell command tool is available inside Docker sandbox mode ${config.sandboxMode}. Read-only commands have no network. Package installs or environment setup require approved package policy and Docker workspace-write mode.`
148
- : "A read-only host shell command tool is available for short local inspection tasks."
147
+ ? `A shell command tool is available inside Docker sandbox mode ${config.sandboxMode}. Docker workspace mode with approved package installs supports broader setup and network commands.`
148
+ : "A host shell command tool is available under the configured trust policy."
149
149
  : "No shell command tool is available.",
150
150
  config.allowFileTools
151
151
  ? `Workspace file tools are available in ${config.commandCwd}: list_files, read_file, search_files, write_file, and apply_patch. Always use workspace-relative paths such as plot_fx.svg or docs/report.tex, never absolute host paths. Secret paths, .git internals, node_modules writes, and huge files are blocked.`
@@ -158,7 +158,7 @@ function createInitialState(config, sessionId) {
158
158
  "For visual-output requests such as draw, plot, graph, chart, diagram, figure, image, or visualization, proactively publish a canvas artifact even when the user does not mention canvas. If workspace file tools are enabled, prefer creating a small SVG or markdown artifact and call send_to_canvas with selected=true.",
159
159
  "Work like a practical coding agent: inspect when useful, edit with file tools, run safe checks when they add confidence, and keep outputs inside the workspace.",
160
160
  "Use the canvas tunnel for outputs the user would likely want to inspect visually, such as figures, PDFs, screenshots, images, important markdown, or generated files.",
161
- "For environment or system-maintenance work, prefer project-local dry-run plans/scripts unless the configured policy explicitly allows stronger actions.",
161
+ "For environment or system-maintenance work, use the configured sandbox and package policy; Docker workspace mode is the preferred place for installs and toolchain setup.",
162
162
  "When the requested outcome is complete and a useful check has passed or been honestly skipped, stop and call finish.",
163
163
  "When done, call finish with a concise result.",
164
164
  ].join(" "),
@@ -182,7 +182,7 @@ function createInitialState(config, sessionId) {
182
182
  "Canvas/artifacts tunnel: available through send_to_canvas for optional frontend rendering.",
183
183
  "Visual-output requests should produce a canvas artifact without requiring the user to ask for canvas explicitly.",
184
184
  "Use file, shell, browser, canvas, and wrapper tools when they are useful; choose the workflow from the user's request.",
185
- "Keep environment or system-maintenance actions project-local and reversible unless policy explicitly permits stronger actions.",
185
+ "Use the configured sandbox and package policy for environment or system-maintenance work.",
186
186
  ]
187
187
  .filter(Boolean)
188
188
  .join("\n"),
@@ -64,6 +64,39 @@ function maintenancePolicyChecks(config) {
64
64
  });
65
65
  }
66
66
 
67
+ function trustedDockerPolicyChecks(config) {
68
+ const dockerConfig = {
69
+ ...config,
70
+ allowShellTool: true,
71
+ allowDestructive: false,
72
+ sandboxMode: "docker-workspace",
73
+ useDockerSandbox: true,
74
+ packageInstallPolicy: "allow",
75
+ };
76
+ const sampleCommands = [
77
+ "apt-get update",
78
+ "apt-get install -y curl wget",
79
+ "wget https://example.com/file.txt",
80
+ "curl -fsSL https://example.com/file.txt -o downloads/file.txt",
81
+ "npm install lodash",
82
+ "python3 -m pip install requests",
83
+ ];
84
+
85
+ return sampleCommands.map((command) => {
86
+ const policy = evaluateCommandPolicy(command, dockerConfig);
87
+ return {
88
+ command,
89
+ allowed: Boolean(policy.allowed),
90
+ category: policy.category || classifyCommand(command).category,
91
+ reason: policy.reason || "",
92
+ needsNetwork: Boolean(policy.needsNetwork),
93
+ requiresDockerRoot: Boolean(policy.requiresDockerRoot),
94
+ sandboxMode: policy.sandboxMode,
95
+ packageInstallPolicy: policy.packageInstallPolicy,
96
+ };
97
+ });
98
+ }
99
+
67
100
  export async function buildCapabilityReport(projectRoot, packageVersion, config) {
68
101
  const paths = projectPaths(projectRoot);
69
102
  const keyStatus = providerKeyStatus(projectRoot);
@@ -155,6 +188,7 @@ export async function buildCapabilityReport(projectRoot, packageVersion, config)
155
188
  },
156
189
  checks,
157
190
  maintenancePolicy: maintenancePolicyChecks(config),
191
+ trustedDockerPolicy: trustedDockerPolicyChecks(config),
158
192
  sessions,
159
193
  actionableSetup: checks
160
194
  .filter((check) => !check.ok && check.setup)
@@ -184,6 +218,10 @@ export function printCapabilityReport(report) {
184
218
  const suffix = check.version ? ` ${check.version}` : check.reason ? ` ${check.reason}` : check.hint ? ` ${check.hint}` : "";
185
219
  console.log(`${check.ok ? "OK" : "MISS"} ${check.name}${suffix}`);
186
220
  }
221
+ if (report.trustedDockerPolicy?.length) {
222
+ const allowed = report.trustedDockerPolicy.filter((item) => item.allowed).length;
223
+ console.log(`trustedDockerPolicy=${allowed}/${report.trustedDockerPolicy.length} allowed when sandbox=docker-workspace packageInstalls=allow`);
224
+ }
187
225
  if (report.actionableSetup.length > 0) {
188
226
  console.log("setup:");
189
227
  for (const item of report.actionableSetup) {
package/src/cli.js CHANGED
@@ -41,6 +41,7 @@ export function parseArgs(argv) {
41
41
  allowShellTool: undefined,
42
42
  allowFileTools: undefined,
43
43
  allowWrapperTools: undefined,
44
+ allowDestructive: undefined,
44
45
  preferredWrapper: "",
45
46
  taskProfile: "",
46
47
  useDockerSandbox: undefined,
@@ -132,6 +133,10 @@ export function parseArgs(argv) {
132
133
  result.allowShellTool = true;
133
134
  continue;
134
135
  }
136
+ if (arg === "--allow-destructive" || arg === "--trusted-host-shell") {
137
+ result.allowDestructive = true;
138
+ continue;
139
+ }
135
140
  if (arg === "--allow-file-tools") {
136
141
  result.allowFileTools = true;
137
142
  continue;
@@ -429,7 +434,7 @@ export async function main(argv = process.argv.slice(2)) {
429
434
 
430
435
  if (!args.goal && !args.resume) {
431
436
  console.error(
432
- 'Usage: aginti-cli web [--port 3210] OR aginti-cli [--routing smart|fast|complex|manual] [--provider deepseek|openai|mock] [--sandbox-mode host|docker-readonly|docker-workspace] [--package-install-policy block|prompt|allow] [--allow-shell] [--allow-file-tools|--no-file-tools] [--allow-wrappers --wrapper codex] [--sandbox-status|--sandbox-preflight] "your task"'
437
+ 'Usage: aginti-cli web [--port 3210] OR aginti-cli [--routing smart|fast|complex|manual] [--provider deepseek|openai|mock] [--sandbox-mode host|docker-readonly|docker-workspace] [--package-install-policy block|prompt|allow] [--approve-package-installs] [--allow-shell] [--allow-destructive] [--allow-file-tools|--no-file-tools] [--allow-wrappers --wrapper codex] [--sandbox-status|--sandbox-preflight] "your task"'
433
438
  );
434
439
  process.exit(1);
435
440
  }
@@ -40,6 +40,11 @@ const TEST_PATTERNS = [
40
40
 
41
41
  const SAFE_WORKSPACE_WRITE_PATTERNS = [/^mkdir\s+-p\s+[-\w./]+$/];
42
42
 
43
+ const NETWORK_FETCH_PATTERNS = [
44
+ /^curl\s+(?:-[A-Za-z0-9]*\s+)*https?:\/\/\S+(?:\s+-o\s+[-\w./]+)?$/,
45
+ /^wget\s+(?:-[A-Za-z0-9]*\s+)*(?:-O\s+[-\w./]+\s+)?https?:\/\/\S+$/,
46
+ ];
47
+
43
48
  const TOOLCHAIN_PATTERNS = [
44
49
  /^python(?:3)?\s+[-\w./]+\.py(?:\s+[-\w./:=]+)*$/,
45
50
  /^latexmk\s+(?=[-\w./=\s]*-pdf\b)(?:(?:-cd|-pdf|-interaction=nonstopmode|-halt-on-error|-output-directory=[-\w./]+)\s+)+[-\w./]+\.tex$/,
@@ -48,12 +53,24 @@ const TOOLCHAIN_PATTERNS = [
48
53
 
49
54
  const PACKAGE_INSTALL_PATTERNS = [
50
55
  /^npm\s+ci$/,
51
- /^npm\s+install$/,
56
+ /^npm\s+install(?:\s+[-@\w./:=]+)*$/,
52
57
  /^pnpm\s+install$/,
58
+ /^pnpm\s+add(?:\s+[-@\w./:=]+)+$/,
53
59
  /^yarn\s+install$/,
60
+ /^yarn\s+add(?:\s+[-@\w./:=]+)+$/,
54
61
  /^python(?:3)?\s+-m\s+pip\s+install\s+-r\s+[-\w./]+$/,
62
+ /^python(?:3)?\s+-m\s+pip\s+install(?:\s+[-@\w./:=]+)+$/,
55
63
  /^pip(?:3)?\s+install\s+-r\s+[-\w./]+$/,
64
+ /^pip(?:3)?\s+install(?:\s+[-@\w./:=]+)+$/,
65
+ /^uv\s+(sync|pip\s+install)(?:\s+[-@\w./:=]+)*$/,
56
66
  /^conda\s+env\s+(create|update)\s+-f\s+[-\w./]+$/,
67
+ /^conda\s+install(?:\s+[-@\w./:=]+)+$/,
68
+ ];
69
+
70
+ const SYSTEM_PACKAGE_INSTALL_PATTERNS = [
71
+ /^(?:sudo\s+)?apt(?:-get)?\s+update$/,
72
+ /^(?:sudo\s+)?apt(?:-get)?\s+install(?:\s+-y)?(?:\s+[-@\w.+:=]+)+$/,
73
+ /^apk\s+add(?:\s+--no-cache)?(?:\s+[-@\w.+:=]+)+$/,
57
74
  ];
58
75
 
59
76
  const ENV_SETUP_PATTERNS = [
@@ -64,13 +81,10 @@ const ENV_SETUP_PATTERNS = [
64
81
 
65
82
  const BLOCKED_SHELL_TOKENS = ["&&", "||", ";", "|", ">", "<", "$(", "`"];
66
83
  const BLOCKED_WRITE_TOKENS = [
67
- "sudo ",
68
84
  " rm",
69
85
  " mv",
70
- " cp",
71
86
  " chmod",
72
87
  " chown",
73
- " mkdir",
74
88
  " rmdir",
75
89
  " touch",
76
90
  " tee",
@@ -83,8 +97,6 @@ const BLOCKED_WRITE_TOKENS = [
83
97
  "git switch",
84
98
  "git reset",
85
99
  "git clean",
86
- "curl ",
87
- "wget ",
88
100
  ];
89
101
 
90
102
  const ALWAYS_BLOCKED_PATTERNS = [
@@ -98,6 +110,13 @@ const ALWAYS_BLOCKED_PATTERNS = [
98
110
  /DEEPSEEK_API_KEY\s*=/i,
99
111
  ];
100
112
 
113
+ const SENSITIVE_COMMAND_PATTERNS = [
114
+ /(^|[\s./])\.env(\s|$|[./])/i,
115
+ /(^|[\s./])\.npmrc(\s|$|[./])/i,
116
+ /^(env|printenv)(\s|$)/i,
117
+ /(api[_-]?key|auth[_-]?token|npm[_-]?token|_authToken|bearer\s+[A-Za-z0-9._-]+)/i,
118
+ ];
119
+
101
120
  function normalizePolicy(value, allowed, fallback) {
102
121
  return allowed.includes(value) ? value : fallback;
103
122
  }
@@ -130,10 +149,18 @@ function classifySimpleCommand(normalized) {
130
149
  if (ALWAYS_BLOCKED_PATTERNS.some((pattern) => pattern.test(normalized))) {
131
150
  return { category: "blocked", reason: "Command is blocked because it may expose secrets or publish packages." };
132
151
  }
152
+ if (SENSITIVE_COMMAND_PATTERNS.some((pattern) => pattern.test(normalized))) {
153
+ return { category: "blocked", reason: "Command is blocked because it references secrets or credential files." };
154
+ }
133
155
 
134
156
  const lowered = ` ${normalized.toLowerCase()} `;
135
157
  if (BLOCKED_SHELL_TOKENS.some((part) => normalized.includes(part))) {
136
- return { category: "blocked", reason: `Command contains blocked shell syntax: ${normalized}` };
158
+ return {
159
+ category: "general-shell",
160
+ needsNetwork: true,
161
+ writesWorkspace: true,
162
+ reason: `Command uses general shell syntax: ${normalized}`,
163
+ };
137
164
  }
138
165
  if (matchAny(SAFE_WORKSPACE_WRITE_PATTERNS, normalized)) {
139
166
  const target = normalized.replace(/^mkdir\s+-p\s+/, "");
@@ -144,7 +171,12 @@ function classifySimpleCommand(normalized) {
144
171
  return { category: "workspace-write", needsNetwork: false, writesWorkspace: true, virtualWorkspacePath };
145
172
  }
146
173
  if (BLOCKED_WRITE_TOKENS.some((part) => lowered.includes(part))) {
147
- return { category: "blocked", reason: `Command contains a write-capable or network token: ${normalized}` };
174
+ return {
175
+ category: "destructive",
176
+ needsNetwork: false,
177
+ writesWorkspace: true,
178
+ reason: `Command contains a write-capable or destructive token: ${normalized}`,
179
+ };
148
180
  }
149
181
 
150
182
  if (matchAny(READ_ONLY_PATTERNS, normalized)) {
@@ -156,6 +188,12 @@ function classifySimpleCommand(normalized) {
156
188
  if (matchAny(TOOLCHAIN_PATTERNS, normalized)) {
157
189
  return { category: "toolchain", needsNetwork: false, writesWorkspace: true };
158
190
  }
191
+ if (matchAny(NETWORK_FETCH_PATTERNS, normalized)) {
192
+ return { category: "network-fetch", needsNetwork: true, writesWorkspace: /(\s-o\s|\s-O\s)/.test(normalized) };
193
+ }
194
+ if (matchAny(SYSTEM_PACKAGE_INSTALL_PATTERNS, normalized)) {
195
+ return { category: "system-package-install", needsNetwork: true, writesWorkspace: false, requiresDockerRoot: true };
196
+ }
159
197
  if (matchAny(PACKAGE_INSTALL_PATTERNS, normalized)) {
160
198
  return { category: "package-install", needsNetwork: true, writesWorkspace: true };
161
199
  }
@@ -163,7 +201,12 @@ function classifySimpleCommand(normalized) {
163
201
  return { category: "env-setup", needsNetwork: false, writesWorkspace: true };
164
202
  }
165
203
 
166
- return { category: "blocked", reason: `Command is outside the execution allowlist: ${normalized}` };
204
+ return {
205
+ category: "general-shell",
206
+ needsNetwork: false,
207
+ writesWorkspace: false,
208
+ reason: `Command is outside the narrow allowlist and requires a trusted shell policy: ${normalized}`,
209
+ };
167
210
  }
168
211
 
169
212
  function classifyCdCommand(normalized) {
@@ -190,6 +233,10 @@ export function evaluateCommandPolicy(command, config) {
190
233
  const classification = classifyCommand(command);
191
234
  const sandboxMode = normalizeSandboxMode(config.sandboxMode);
192
235
  const packageInstallPolicy = normalizePackageInstallPolicy(config.packageInstallPolicy);
236
+ const dockerWorkspace = sandboxMode === "docker-workspace";
237
+ const packageInstallsAllowed = packageInstallPolicy === "allow";
238
+ const trustedDockerShell = dockerWorkspace && packageInstallsAllowed;
239
+ const trustedHostShell = sandboxMode === "host" && Boolean(config.allowDestructive);
193
240
 
194
241
  if (classification.category === "blocked") {
195
242
  return { allowed: false, ...classification, sandboxMode, packageInstallPolicy };
@@ -215,7 +262,11 @@ export function evaluateCommandPolicy(command, config) {
215
262
  };
216
263
  }
217
264
 
218
- if (classification.category === "package-install" || classification.category === "env-setup") {
265
+ if (
266
+ classification.category === "package-install" ||
267
+ classification.category === "env-setup" ||
268
+ classification.category === "system-package-install"
269
+ ) {
219
270
  if (packageInstallPolicy !== "allow") {
220
271
  return {
221
272
  allowed: false,
@@ -230,17 +281,53 @@ export function evaluateCommandPolicy(command, config) {
230
281
  };
231
282
  }
232
283
 
233
- if (sandboxMode !== "docker-workspace") {
284
+ if (classification.category === "system-package-install" && sandboxMode === "host" && !config.allowDestructive) {
234
285
  return {
235
286
  allowed: false,
236
287
  category: classification.category,
237
- reason: "Approved package/environment setup must run in Docker workspace-write sandbox mode.",
288
+ reason: "Host system package installs require Allow destructive actions. Prefer Docker workspace mode.",
238
289
  sandboxMode,
239
290
  packageInstallPolicy,
240
291
  };
241
292
  }
242
293
  }
243
294
 
295
+ if (classification.category === "general-shell" && !trustedDockerShell && !trustedHostShell) {
296
+ return {
297
+ allowed: false,
298
+ ...classification,
299
+ needsApproval: true,
300
+ reason:
301
+ sandboxMode === "host"
302
+ ? "General shell commands on the host require Allow destructive actions. Prefer Docker workspace mode for broad shell access."
303
+ : "General Docker shell commands require Package installs = allow in docker-workspace mode.",
304
+ sandboxMode,
305
+ packageInstallPolicy,
306
+ };
307
+ }
308
+
309
+ if (classification.category === "destructive" && !config.allowDestructive) {
310
+ return {
311
+ allowed: false,
312
+ ...classification,
313
+ needsApproval: true,
314
+ reason: "Destructive shell commands require Allow destructive actions.",
315
+ sandboxMode,
316
+ packageInstallPolicy,
317
+ };
318
+ }
319
+
320
+ if (classification.category === "network-fetch" && config.useDockerSandbox && !trustedDockerShell) {
321
+ return {
322
+ allowed: false,
323
+ ...classification,
324
+ needsApproval: true,
325
+ reason: "Docker network commands require docker-workspace mode with Package installs = allow.",
326
+ sandboxMode,
327
+ packageInstallPolicy,
328
+ };
329
+ }
330
+
244
331
  if (classification.writesWorkspace && config.useDockerSandbox && sandboxMode !== "docker-workspace") {
245
332
  return {
246
333
  allowed: false,
@@ -254,6 +341,7 @@ export function evaluateCommandPolicy(command, config) {
254
341
  return {
255
342
  allowed: true,
256
343
  ...classification,
344
+ requiresDockerRoot: Boolean(classification.requiresDockerRoot && config.useDockerSandbox),
257
345
  sandboxMode,
258
346
  packageInstallPolicy,
259
347
  };
package/src/config.js CHANGED
@@ -71,7 +71,7 @@ export function resolveRuntimeConfig(args, overrides = {}) {
71
71
  ? overrides.allowedDomains
72
72
  : parseList(process.env.ALLOWED_DOMAINS),
73
73
  allowPasswords: parseBoolean(overrides.allowPasswords ?? process.env.ALLOW_PASSWORDS, false),
74
- allowDestructive: parseBoolean(overrides.allowDestructive ?? process.env.ALLOW_DESTRUCTIVE, false),
74
+ allowDestructive: parseBoolean(overrides.allowDestructive ?? args.allowDestructive ?? process.env.ALLOW_DESTRUCTIVE, false),
75
75
  allowShellTool: parseBoolean(overrides.allowShellTool ?? args.allowShellTool ?? process.env.ALLOW_SHELL_TOOL, false),
76
76
  allowFileTools: parseBoolean(overrides.allowFileTools ?? args.allowFileTools ?? process.env.ALLOW_FILE_TOOLS, true),
77
77
  allowWrapperTools: parseBoolean(
@@ -168,7 +168,7 @@ export async function ensureDockerSandboxReady(config, observers, options = {})
168
168
  function dockerRunArgs(command, config, policy = evaluateCommandPolicy(command, config)) {
169
169
  const uid = typeof process.getuid === "function" ? String(process.getuid()) : "";
170
170
  const gid = typeof process.getgid === "function" ? String(process.getgid()) : "";
171
- const userArgs = uid && gid ? ["--user", `${uid}:${gid}`] : [];
171
+ const userArgs = uid && gid && !policy.requiresDockerRoot ? ["--user", `${uid}:${gid}`] : [];
172
172
  const sandboxMode = normalizeSandboxMode(config.sandboxMode);
173
173
  const mountMode = sandboxMode === "docker-workspace" ? "rw" : "ro";
174
174
  const networkMode = policy.needsNetwork ? "bridge" : "none";
@@ -300,7 +300,7 @@ export async function requestNextStep(client, config, messages) {
300
300
  function: {
301
301
  name: "run_command",
302
302
  description:
303
- "Run an allowlisted terminal command in the configured working directory. Good for inspection, tests, and approved Docker package/environment setup. NPM publishing, tokens, sudo, arbitrary network commands, and destructive git/file actions are blocked.",
303
+ "Run a terminal command in the configured working directory under the active shell policy. Secrets and npm publishing are always blocked; Docker workspace mode with approved package installs supports broader network/setup commands, while host destructive or privileged work requires explicit trust.",
304
304
  parameters: {
305
305
  type: "object",
306
306
  properties: {
@@ -52,7 +52,7 @@ export const TASK_PROFILES = {
52
52
  id: "website",
53
53
  label: "Website testing",
54
54
  prompt:
55
- "For website-testing tasks, create or inspect the site, add a simple local test/check file when useful, and run safe checks without installing packages unless approved.",
55
+ "For website-testing tasks, create or inspect the site, add a local check when useful, and use the configured sandbox/package policy for dependencies.",
56
56
  tools: ["files", "shell", "canvas", "sandbox"],
57
57
  },
58
58
  aaps: {
@@ -73,7 +73,7 @@ export const TASK_PROFILES = {
73
73
  id: "maintenance",
74
74
  label: "System maintenance",
75
75
  prompt:
76
- "For system maintenance, diagnose first, prefer reversible project-local plans or dry-run scripts, and avoid privileged/global/destructive actions unless explicitly approved.",
76
+ "For system maintenance, diagnose first, use Docker for broad installs when available, and follow the configured trust/package policy for host-level changes.",
77
77
  tools: ["shell", "sandbox", "files"],
78
78
  },
79
79
  };