@lazyingart/agintiflow 0.5.0 → 0.7.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
@@ -46,6 +46,16 @@ aginti --list-profiles
46
46
  aginti --sandbox-status --sandbox-mode docker-readonly
47
47
  ```
48
48
 
49
+ Start an interactive Codex-style CLI chat from any project folder:
50
+
51
+ ```bash
52
+ aginti
53
+ # or explicitly:
54
+ aginti chat
55
+ ```
56
+
57
+ Inside chat, type normal requests such as `write a small Python CLI app with tests`. Use `/help` for commands, `/docker on` to switch to Docker workspace mode with approved package installs, `/sessions` to list project runs, and `/resume <session-id>` to continue work.
58
+
49
59
  Launch the local web UI from an installed package:
50
60
 
51
61
  ```bash
@@ -71,7 +81,8 @@ aginti doctor --capabilities
71
81
  aginti sessions list
72
82
  aginti sessions show <session-id>
73
83
  aginti resume <session-id> "continue with a short follow-up"
74
- aginti --profile code --provider mock --routing manual "Create notes/hello.md"
84
+ aginti --profile code "write a small Python CLI app with tests"
85
+ aginti --sandbox-mode docker-workspace --approve-package-installs "set up this project and run the tests"
75
86
  ```
76
87
 
77
88
  Run from a source checkout:
@@ -181,12 +192,12 @@ AgInTiFlow is intentionally conservative:
181
192
  - Shell commands are disabled unless the shell tool is enabled.
182
193
  - Workspace file tools stay inside `commandCwd` and block `.env`, secret-like paths, `.git`, node_modules writes, absolute escapes, binary files, and huge files.
183
194
  - 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.
195
+ - 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
196
  - Docker read-only mode mounts the workspace read-only and disables container network access.
186
197
  - 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.
198
+ - 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
199
  - 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.
200
+ - 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
201
  - NPM tokens and API keys are redacted from tool logs and API responses.
191
202
  - Every tool request and result is written to structured logs.
192
203
 
@@ -311,9 +322,9 @@ The setup script idempotently builds `agintiflow-sandbox:latest` from `docker/sa
311
322
 
312
323
  | Mode | Workspace mount | Network | Intended use |
313
324
  | --- | --- | --- | --- |
314
- | `host` | local process | host network | legacy read-only inspection only |
325
+ | `host` | local process | host network | direct project work; privileged/destructive commands require explicit trust |
315
326
  | `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 |
327
+ | `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
328
 
318
329
  Package policy values:
319
330
 
@@ -321,9 +332,9 @@ Package policy values:
321
332
  | --- | --- |
322
333
  | `block` | Always block npm/pip/conda/venv setup. |
323
334
  | `prompt` | Return a clear approval-required error; the UI can switch to approved. |
324
- | `allow` | Permit allowlisted setup commands only in `docker-workspace`. |
335
+ | `allow` | Permit package/setup commands. Docker workspace mode also allows broader shell/network commands while keeping secrets and npm publishing blocked. |
325
336
 
326
- 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.
337
+ 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 the project folder is mounted as `/workspace`; any file written to `/workspace/report.pdf` appears on the host as `<your-project>/report.pdf`. CLI runs print both the host workspace and the Docker mapping before execution. 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
338
 
328
339
  Safe preflight endpoints:
329
340
 
@@ -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.7.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",
@@ -41,6 +41,7 @@
41
41
  "scripts/install-docker-ubuntu.sh",
42
42
  "scripts/setup-agent-toolchain-docker.sh",
43
43
  "scripts/real-deepseek-capabilities.js",
44
+ "scripts/smoke-cli-chat.js",
44
45
  "scripts/smoke-coding-tools.js",
45
46
  "scripts/smoke-capabilities.js",
46
47
  "scripts/smoke-toolchain-docker.js",
@@ -59,10 +60,11 @@
59
60
  "check": "node --check run.js && node --check web.js && node --check bin/aginti-cli.js && node --check src/*.js",
60
61
  "setup:toolchain-docker": "scripts/setup-agent-toolchain-docker.sh",
61
62
  "smoke:coding-tools": "node scripts/smoke-coding-tools.js",
63
+ "smoke:cli-chat": "node scripts/smoke-cli-chat.js",
62
64
  "smoke:toolchain-docker": "node scripts/smoke-toolchain-docker.js",
63
65
  "smoke:web-api": "node scripts/smoke-web-api.js",
64
66
  "real:deepseek": "node scripts/real-deepseek-capabilities.js",
65
- "test": "npm run check && npm run smoke:web-api && npm run smoke:coding-tools && npm run smoke:capabilities",
67
+ "test": "npm run check && npm run smoke:web-api && npm run smoke:coding-tools && npm run smoke:capabilities && npm run smoke:cli-chat",
66
68
  "pack:dry-run": "npm pack --dry-run",
67
69
  "smoke:capabilities": "node scripts/smoke-capabilities.js"
68
70
  },
@@ -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
@@ -0,0 +1,76 @@
1
+ #!/usr/bin/env node
2
+ import { spawn } from "node:child_process";
3
+ import fs from "node:fs/promises";
4
+ import os from "node:os";
5
+ import path from "node:path";
6
+ import { fileURLToPath } from "node:url";
7
+
8
+ const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
9
+ const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "agintiflow-cli-chat-"));
10
+ const binPath = path.join(repoRoot, "bin/aginti-cli.js");
11
+
12
+ function runChat(inputText) {
13
+ return new Promise((resolve, reject) => {
14
+ const child = spawn(
15
+ process.execPath,
16
+ [binPath, "chat", "--provider", "mock", "--routing", "manual", "--profile", "code"],
17
+ {
18
+ cwd: tempRoot,
19
+ stdio: ["pipe", "pipe", "pipe"],
20
+ env: {
21
+ ...process.env,
22
+ AGINTIFLOW_RUNTIME_DIR: "",
23
+ },
24
+ }
25
+ );
26
+
27
+ let stdout = "";
28
+ let stderr = "";
29
+ const timer = setTimeout(() => {
30
+ child.kill("SIGTERM");
31
+ reject(new Error("interactive chat smoke timed out"));
32
+ }, 25000);
33
+
34
+ child.stdout.on("data", (chunk) => {
35
+ stdout += String(chunk);
36
+ });
37
+ child.stderr.on("data", (chunk) => {
38
+ stderr += String(chunk);
39
+ });
40
+ child.on("error", (error) => {
41
+ clearTimeout(timer);
42
+ reject(error);
43
+ });
44
+ child.on("close", (code) => {
45
+ clearTimeout(timer);
46
+ if (code === 0) resolve({ stdout, stderr });
47
+ else reject(new Error(`interactive chat exited ${code}\n${stdout}\n${stderr}`));
48
+ });
49
+
50
+ child.stdin.end(inputText);
51
+ });
52
+ }
53
+
54
+ try {
55
+ const result = await runChat("Create notes/interactive.md with a short CLI chat smoke message\n/exit\n");
56
+ const written = await fs.readFile(path.join(tempRoot, "notes/interactive.md"), "utf8");
57
+ if (!written.includes("Created by AgInTiFlow mock mode.")) {
58
+ throw new Error("interactive chat did not create the expected file");
59
+ }
60
+ if (!result.stdout.includes("Interactive agent chat")) {
61
+ throw new Error("interactive chat did not print its banner");
62
+ }
63
+ console.log(
64
+ JSON.stringify(
65
+ {
66
+ ok: true,
67
+ projectRoot: tempRoot,
68
+ checks: ["interactive-chat", "mock-file-write"],
69
+ },
70
+ null,
71
+ 2
72
+ )
73
+ );
74
+ } finally {
75
+ await fs.rm(tempRoot, { recursive: true, force: true });
76
+ }
@@ -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"),
@@ -791,6 +791,16 @@ export async function runAgent(config) {
791
791
  console.log(`Provider: ${config.provider}`);
792
792
  console.log(`Model: ${config.model}`);
793
793
  console.log(`Routing: ${config.routingMode} (${config.routeReason})`);
794
+ console.log(`Workspace: ${config.commandCwd}`);
795
+ console.log(`Sessions: ${config.sessionsDir}`);
796
+ if (config.useDockerSandbox) {
797
+ console.log(
798
+ `Docker: image=${config.dockerSandboxImage} mode=${config.sandboxMode} packagePolicy=${config.packageInstallPolicy}`
799
+ );
800
+ console.log(`Docker workspace: /workspace -> ${config.commandCwd}`);
801
+ } else if (config.allowShellTool) {
802
+ console.log(`Shell: host policy=${config.packageInstallPolicy}`);
803
+ }
794
804
  if (state.plan) {
795
805
  console.log("\nPlan:");
796
806
  console.log(state.plan);
@@ -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
@@ -4,6 +4,7 @@ import { listAgentWrappers } from "./tool-wrappers.js";
4
4
  import { getModelPresets } from "./model-routing.js";
5
5
  import { getDockerSandboxStatus, runDockerPreflight } from "./docker-sandbox.js";
6
6
  import { buildCapabilityReport, printCapabilityReport } from "./capabilities.js";
7
+ import { startInteractiveCli } from "./interactive-cli.js";
7
8
  import {
8
9
  doctorReport,
9
10
  initProject,
@@ -41,6 +42,7 @@ export function parseArgs(argv) {
41
42
  allowShellTool: undefined,
42
43
  allowFileTools: undefined,
43
44
  allowWrapperTools: undefined,
45
+ allowDestructive: undefined,
44
46
  preferredWrapper: "",
45
47
  taskProfile: "",
46
48
  useDockerSandbox: undefined,
@@ -51,6 +53,7 @@ export function parseArgs(argv) {
51
53
  sandboxStatus: false,
52
54
  sandboxPreflight: false,
53
55
  web: false,
56
+ interactive: false,
54
57
  port: "",
55
58
  host: "",
56
59
  listProfiles: false,
@@ -63,6 +66,10 @@ export function parseArgs(argv) {
63
66
  result.web = true;
64
67
  continue;
65
68
  }
69
+ if (arg === "chat" || arg === "interactive" || arg === "--chat" || arg === "--interactive") {
70
+ result.interactive = true;
71
+ continue;
72
+ }
66
73
  if (arg === "--port") {
67
74
  result.port = readOption(argv, i);
68
75
  i += 1;
@@ -132,6 +139,14 @@ export function parseArgs(argv) {
132
139
  result.allowShellTool = true;
133
140
  continue;
134
141
  }
142
+ if (arg === "--no-shell") {
143
+ result.allowShellTool = false;
144
+ continue;
145
+ }
146
+ if (arg === "--allow-destructive" || arg === "--trusted-host-shell") {
147
+ result.allowDestructive = true;
148
+ continue;
149
+ }
135
150
  if (arg === "--allow-file-tools") {
136
151
  result.allowFileTools = true;
137
152
  continue;
@@ -192,6 +207,12 @@ export function parseArgs(argv) {
192
207
  return result;
193
208
  }
194
209
 
210
+ function printUsage() {
211
+ console.log(
212
+ 'Usage: aginti [chat] OR aginti web [--port 3210] OR aginti [--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|--no-shell] [--allow-destructive] [--allow-file-tools|--no-file-tools] [--allow-wrappers --wrapper codex] [--sandbox-status|--sandbox-preflight] "your task"'
213
+ );
214
+ }
215
+
195
216
  function printRoutes() {
196
217
  const presets = getModelPresets();
197
218
  for (const preset of Object.values(presets)) {
@@ -307,6 +328,11 @@ async function handleSessionsCommand(argv) {
307
328
  }
308
329
 
309
330
  export async function main(argv = process.argv.slice(2)) {
331
+ if (argv[0] === "--help" || argv[0] === "help" || argv[0] === "-h") {
332
+ printUsage();
333
+ return;
334
+ }
335
+
310
336
  if (argv[0] === "--version" || argv[0] === "version" || argv[0] === "-v") {
311
337
  console.log(packageJson.version);
312
338
  return;
@@ -388,13 +414,21 @@ export async function main(argv = process.argv.slice(2)) {
388
414
  console.error('Usage: aginti resume <session-id> "new prompt"');
389
415
  process.exit(1);
390
416
  }
391
- const config = loadConfig({ ...parseArgs([prompt]), resume: sessionId, goal: prompt }, { packageDir });
417
+ const config = loadConfig(
418
+ { ...parseArgs([prompt]), resume: sessionId, goal: prompt, allowShellTool: true, allowFileTools: true },
419
+ { packageDir }
420
+ );
392
421
  await runAgent(config);
393
422
  return;
394
423
  }
395
424
 
396
425
  const args = parseArgs(argv);
397
426
 
427
+ if (args.interactive || (!args.goal && !args.resume && process.stdin.isTTY)) {
428
+ await startInteractiveCli(args, { packageDir, packageVersion: packageJson.version });
429
+ return;
430
+ }
431
+
398
432
  if (args.web) {
399
433
  if (args.port) process.env.PORT = String(args.port);
400
434
  if (args.host) process.env.HOST = String(args.host);
@@ -428,12 +462,17 @@ export async function main(argv = process.argv.slice(2)) {
428
462
  }
429
463
 
430
464
  if (!args.goal && !args.resume) {
431
- 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"'
433
- );
465
+ printUsage();
434
466
  process.exit(1);
435
467
  }
436
468
 
437
- const config = loadConfig(args, { packageDir });
469
+ const config = loadConfig(
470
+ {
471
+ ...args,
472
+ allowShellTool: args.allowShellTool ?? true,
473
+ allowFileTools: args.allowFileTools ?? true,
474
+ },
475
+ { packageDir }
476
+ );
438
477
  await runAgent(config);
439
478
  }
@@ -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";
@@ -0,0 +1,225 @@
1
+ import readline from "node:readline/promises";
2
+ import { stdin as input, stdout as output } from "node:process";
3
+ import { runAgent } from "./agent-runner.js";
4
+ import { loadConfig } from "./config.js";
5
+ import { initProject, listProjectSessions } from "./project.js";
6
+ import { normalizePackageInstallPolicy, normalizeSandboxMode } from "./command-policy.js";
7
+ import { normalizeTaskProfile } from "./task-profiles.js";
8
+
9
+ function printHelp() {
10
+ console.log(
11
+ [
12
+ "Commands:",
13
+ " /help Show this help.",
14
+ " /status Show active route, workspace, sandbox, and session.",
15
+ " /new Start a fresh session on the next message.",
16
+ " /resume <session-id> Continue a saved session.",
17
+ " /sessions List recent sessions in this project.",
18
+ " /profile <name> Set task profile, e.g. code, website, latex, maintenance.",
19
+ " /routing <mode> Set routing: smart, fast, complex, manual.",
20
+ " /provider <name> Set provider: deepseek, openai, mock.",
21
+ " /model <name> Set an explicit model, or /model auto.",
22
+ " /docker on Use docker-workspace with approved package installs.",
23
+ " /docker off Use host shell policy.",
24
+ " /installs block|prompt|allow",
25
+ " /cwd <path> Change command workspace.",
26
+ " /exit Quit.",
27
+ "",
28
+ "Type a normal request to run the agent. Example: write a Python CLI app with tests",
29
+ ].join("\n")
30
+ );
31
+ }
32
+
33
+ function printStatus(state) {
34
+ console.log(`project=${process.cwd()}`);
35
+ console.log(`cwd=${state.commandCwd || process.cwd()}`);
36
+ console.log(`session=${state.sessionId || "new"}`);
37
+ console.log(`provider=${state.provider || "auto"} routing=${state.routingMode} model=${state.model || "auto"}`);
38
+ console.log(`profile=${state.taskProfile} maxSteps=${state.maxSteps}`);
39
+ console.log(
40
+ `shell=${state.allowShellTool} files=${state.allowFileTools} sandbox=${state.sandboxMode} installs=${state.packageInstallPolicy}`
41
+ );
42
+ if (state.sandboxMode !== "host") {
43
+ console.log(`dockerWorkspace=/workspace -> ${state.commandCwd || process.cwd()}`);
44
+ }
45
+ }
46
+
47
+ function createState(args = {}) {
48
+ return {
49
+ provider: args.provider || "",
50
+ model: args.model || "",
51
+ routingMode: args.routingMode || "smart",
52
+ commandCwd: args.commandCwd || process.cwd(),
53
+ sandboxMode: normalizeSandboxMode(args.sandboxMode || "host"),
54
+ packageInstallPolicy: normalizePackageInstallPolicy(args.packageInstallPolicy || "prompt"),
55
+ allowShellTool: args.allowShellTool ?? true,
56
+ allowFileTools: args.allowFileTools ?? true,
57
+ allowWrapperTools: args.allowWrapperTools ?? false,
58
+ allowDestructive: args.allowDestructive ?? false,
59
+ preferredWrapper: args.preferredWrapper || "codex",
60
+ taskProfile: normalizeTaskProfile(args.taskProfile || "auto"),
61
+ headless: args.headless ?? false,
62
+ maxSteps: Number.isFinite(args.maxSteps) && args.maxSteps > 0 ? args.maxSteps : 15,
63
+ sessionId: args.resume || "",
64
+ };
65
+ }
66
+
67
+ async function handleCommand(line, state, packageDir) {
68
+ const [command, ...rest] = line.slice(1).trim().split(/\s+/);
69
+ const value = rest.join(" ").trim();
70
+
71
+ if (!command || command === "help" || command === "?") {
72
+ printHelp();
73
+ return true;
74
+ }
75
+ if (command === "exit" || command === "quit" || command === "q") return false;
76
+ if (command === "status") {
77
+ printStatus(state);
78
+ return true;
79
+ }
80
+ if (command === "new") {
81
+ state.sessionId = "";
82
+ console.log("Next message will start a new session.");
83
+ return true;
84
+ }
85
+ if (command === "resume") {
86
+ if (!value) console.log("Usage: /resume <session-id>");
87
+ else {
88
+ state.sessionId = value;
89
+ console.log(`Resuming ${state.sessionId}`);
90
+ }
91
+ return true;
92
+ }
93
+ if (command === "sessions") {
94
+ const sessions = await listProjectSessions(process.cwd(), 20);
95
+ if (sessions.length === 0) console.log("No project-local sessions found.");
96
+ else {
97
+ for (const session of sessions) {
98
+ const goal = session.goal ? ` ${session.goal.slice(0, 80)}` : "";
99
+ console.log(`${session.sessionId} ${session.provider}/${session.model} ${session.updatedAt}${goal}`);
100
+ }
101
+ }
102
+ return true;
103
+ }
104
+ if (command === "profile") {
105
+ state.taskProfile = normalizeTaskProfile(value || "auto");
106
+ console.log(`profile=${state.taskProfile}`);
107
+ return true;
108
+ }
109
+ if (command === "routing") {
110
+ state.routingMode = value || "smart";
111
+ console.log(`routing=${state.routingMode}`);
112
+ return true;
113
+ }
114
+ if (command === "provider") {
115
+ state.provider = value === "auto" ? "" : value;
116
+ console.log(`provider=${state.provider || "auto"}`);
117
+ return true;
118
+ }
119
+ if (command === "model") {
120
+ state.model = value === "auto" ? "" : value;
121
+ console.log(`model=${state.model || "auto"}`);
122
+ return true;
123
+ }
124
+ if (command === "installs") {
125
+ state.packageInstallPolicy = normalizePackageInstallPolicy(value || "prompt");
126
+ console.log(`installs=${state.packageInstallPolicy}`);
127
+ return true;
128
+ }
129
+ if (command === "docker") {
130
+ if (value === "on") {
131
+ state.sandboxMode = "docker-workspace";
132
+ state.packageInstallPolicy = "allow";
133
+ console.log(`docker=on /workspace -> ${state.commandCwd || process.cwd()} installs=allow`);
134
+ } else if (value === "off") {
135
+ state.sandboxMode = "host";
136
+ state.packageInstallPolicy = "prompt";
137
+ console.log("docker=off sandbox=host installs=prompt");
138
+ } else {
139
+ console.log("Usage: /docker on OR /docker off");
140
+ }
141
+ return true;
142
+ }
143
+ if (command === "cwd") {
144
+ state.commandCwd = value || process.cwd();
145
+ console.log(`cwd=${state.commandCwd}`);
146
+ return true;
147
+ }
148
+ if (command === "init") {
149
+ const result = await initProject(process.cwd());
150
+ console.log(`initialized project=${result.projectRoot}`);
151
+ return true;
152
+ }
153
+ if (command === "web") {
154
+ const port = value || "3220";
155
+ console.log(`Run in another terminal: aginti web --port ${port}`);
156
+ return true;
157
+ }
158
+
159
+ console.log(`Unknown command: /${command}. Use /help.`);
160
+ return true;
161
+ }
162
+
163
+ async function runPrompt(prompt, state, packageDir) {
164
+ const config = loadConfig(
165
+ {
166
+ provider: state.provider,
167
+ model: state.model,
168
+ routingMode: state.routingMode,
169
+ commandCwd: state.commandCwd,
170
+ sandboxMode: state.sandboxMode,
171
+ packageInstallPolicy: state.packageInstallPolicy,
172
+ allowShellTool: state.allowShellTool,
173
+ allowFileTools: state.allowFileTools,
174
+ allowWrapperTools: state.allowWrapperTools,
175
+ allowDestructive: state.allowDestructive,
176
+ preferredWrapper: state.preferredWrapper,
177
+ taskProfile: state.taskProfile,
178
+ maxSteps: state.maxSteps,
179
+ headless: state.headless,
180
+ resume: state.sessionId,
181
+ goal: prompt,
182
+ },
183
+ { packageDir, baseDir: process.cwd() }
184
+ );
185
+
186
+ const result = await runAgent(config);
187
+ state.sessionId = result.sessionId || state.sessionId;
188
+ }
189
+
190
+ export async function startInteractiveCli(args = {}, { packageDir, packageVersion } = {}) {
191
+ const state = createState(args);
192
+ const rl = readline.createInterface({ input, output, terminal: Boolean(input.isTTY && output.isTTY) });
193
+
194
+ console.log(`AgInTiFlow ${packageVersion || ""}`.trim());
195
+ console.log(`Project: ${process.cwd()}`);
196
+ console.log("Interactive agent chat. Type /help for commands, /exit to quit.");
197
+ printStatus(state);
198
+
199
+ try {
200
+ while (true) {
201
+ let answer = "";
202
+ try {
203
+ answer = await rl.question("\naginti> ");
204
+ } catch (error) {
205
+ if (error?.code === "ERR_USE_AFTER_CLOSE") break;
206
+ throw error;
207
+ }
208
+ const line = answer.trim();
209
+ if (!line) continue;
210
+ if (line.startsWith("/")) {
211
+ const keepGoing = await handleCommand(line, state, packageDir);
212
+ if (!keepGoing) break;
213
+ continue;
214
+ }
215
+
216
+ try {
217
+ await runPrompt(line, state, packageDir);
218
+ } catch (error) {
219
+ console.error(`error: ${error.message}`);
220
+ }
221
+ }
222
+ } finally {
223
+ rl.close();
224
+ }
225
+ }
@@ -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
  };