@lazyingart/agintiflow 0.20.117 → 0.20.119
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 +1 -1
- package/docs/runtime-modes-and-autonomy.md +3 -1
- package/package.json +1 -1
- package/scripts/smoke-cli-chat.js +10 -0
- package/scripts/smoke-coding-tools.js +8 -0
- package/scripts/smoke-toolchain-docker.js +32 -1
- package/src/agent-runner.js +130 -25
- package/src/docker-sandbox.js +57 -3
- package/src/interactive-cli.js +71 -14
- package/src/model-client.js +2 -2
package/README.md
CHANGED
|
@@ -170,7 +170,7 @@ aginti --resume <session-id> \
|
|
|
170
170
|
"Take a fresh screenshot of the running app in the emulator, save it with a durable filename in this project, and keep git status clean."
|
|
171
171
|
```
|
|
172
172
|
|
|
173
|
-
Permission behavior is intentionally consistent. Use `-s safe` for read-first sessions that ask before writes/setup, `-s normal` for current-project writes plus Docker setup, and `-s danger` for trusted host/full-access work. Inside chat, `/safe`, `/normal`, and `/danger` switch the current session. When a blocked action appears, CLI and web can offer `No`, `Yes this time`, or `Yes and always for this session` instead of making the agent retry command variants. Android/Gradle builds can use safe local env assignments such as `ANDROID_HOME=... JAVA_HOME=... ./gradlew assembleDebug` and relative workspace logs without requiring whole-host destructive mode. See [runtime modes and autonomy](docs/runtime-modes-and-autonomy.md) for the full contract.
|
|
173
|
+
Permission behavior is intentionally consistent. Use `-s safe` for read-first sessions that ask before writes/setup, `-s normal` for current-project writes plus Docker setup, and `-s danger` for trusted host/full-access work. In Docker workspace mode, common host data roots such as the user's home parent are mounted read-only at their original absolute paths so sibling projects and datasets can be inspected without granting host-write permission. Inside chat, `/safe`, `/normal`, and `/danger` switch the current session, and `/status` remains available while a run is active. When a blocked action appears, CLI and web can offer `No`, `Yes this time`, or `Yes and always for this session` instead of making the agent retry command variants. Android/Gradle builds can use safe local env assignments such as `ANDROID_HOME=... JAVA_HOME=... ./gradlew assembleDebug` and relative workspace logs without requiring whole-host destructive mode. See [runtime modes and autonomy](docs/runtime-modes-and-autonomy.md) for the full contract.
|
|
174
174
|
|
|
175
175
|
Tmux follows the same rule. In Docker sandbox mode, `tmux_start_session` and `tmux_send_keys` are durable host tools, but their commands must stay workspace-bound. In host mode, tmux startup/send command text follows the same host shell policy as `run_command`; broad host shell work needs explicit `--allow-destructive`. Use `--sandbox-mode host --allow-destructive` only when a tmux task really needs trusted whole-host execution.
|
|
176
176
|
|
|
@@ -52,7 +52,7 @@ The runtime must be consistent when an action is not permitted. A blocked action
|
|
|
52
52
|
Current contract:
|
|
53
53
|
|
|
54
54
|
- `safe` asks before file-tool writes and package/setup actions. It is the right mode for inspection, review, and cautious first passes.
|
|
55
|
-
- `normal` allows writes inside the current project folder and package/setup inside Docker workspace mode. It still blocks outside-project writes and host-system changes until approved.
|
|
55
|
+
- `normal` allows writes inside the current project folder, read-only inspection of visible host data roots, and package/setup inside Docker workspace mode. It still blocks outside-project writes and host-system changes until approved.
|
|
56
56
|
- `danger` is trusted host/full-access mode: host shell, destructive actions, host installs, password typing, and outside-workspace file paths are enabled. Use it only for tasks you trust; obvious secret/publish exfiltration guards remain hard stops.
|
|
57
57
|
- Workspace file tools may read and write inside the configured project folder when file tools are enabled.
|
|
58
58
|
- Narrow workspace-local shell actions such as safe probes, Gradle/TeX builds, and `chmod +x` on a project script may run when the command policy can classify them precisely.
|
|
@@ -111,6 +111,8 @@ aginti
|
|
|
111
111
|
|
|
112
112
|
This starts interactive chat with Docker workspace mode, file tools, shell tools, web search, and package installs allowed inside Docker.
|
|
113
113
|
|
|
114
|
+
Docker workspace mode also mounts common host data roots, such as the user's home parent, read-only at their original absolute paths. This lets a task in `/home/lachlan/ProjectsLFS/aginti-test` inspect a sibling dataset such as `/home/lachlan/ProjectsLFS/ProteinStructure` without switching to host-write mode. Outputs and edits still belong under `/workspace` unless the user explicitly approves stronger host permissions. Set `AGINTI_DOCKER_READONLY_HOST_MOUNTS=/path/a:/path/b` to override the read-only mounts, or set it to `off` to disable them.
|
|
115
|
+
|
|
114
116
|
Direct trusted host mode:
|
|
115
117
|
|
|
116
118
|
```bash
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lazyingart/agintiflow",
|
|
3
|
-
"version": "0.20.
|
|
3
|
+
"version": "0.20.119",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "AgInTiFlow is a project-aware agent workspace for hybrid wet-dry R&D, hardware-aware intelligence, software automation, and industrial workflows.",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
buildPromptLayout,
|
|
11
11
|
buildPromptRenderSequence,
|
|
12
12
|
ComposerHistory,
|
|
13
|
+
activeRunSlashCommandAction,
|
|
13
14
|
canonicalSlashPromptBuffer,
|
|
14
15
|
classifyEscapeAction,
|
|
15
16
|
formatElapsedDuration,
|
|
@@ -368,6 +369,15 @@ try {
|
|
|
368
369
|
if (classifyEscapeAction({ active: true, pendingAsap: [] }) !== "abort") {
|
|
369
370
|
throw new Error("active Esc should abort when no ASAP pipe messages are pending");
|
|
370
371
|
}
|
|
372
|
+
if (activeRunSlashCommandAction("/status") !== "status" || activeRunSlashCommandAction("/status verbose") !== "status") {
|
|
373
|
+
throw new Error("active /status should be allowed during a run");
|
|
374
|
+
}
|
|
375
|
+
if (activeRunSlashCommandAction("/scs") !== "blocked") {
|
|
376
|
+
throw new Error("active mutating slash commands should remain blocked during a run");
|
|
377
|
+
}
|
|
378
|
+
if (activeRunSlashCommandAction("continue writing") !== "") {
|
|
379
|
+
throw new Error("active normal input should still pipe into the running task");
|
|
380
|
+
}
|
|
371
381
|
const composerHistory = new ComposerHistory([
|
|
372
382
|
"first prompt",
|
|
373
383
|
"second prompt\nwith code",
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
runAgent,
|
|
10
10
|
sanitizeToolResult,
|
|
11
11
|
shouldShortCircuitToolBatch,
|
|
12
|
+
shellDiagnosticHint,
|
|
12
13
|
skippedAfterBlockedToolResult,
|
|
13
14
|
} from "../src/agent-runner.js";
|
|
14
15
|
import { formatBehaviorContractForPrompt } from "../src/behavior-contract.js";
|
|
@@ -410,6 +411,13 @@ try {
|
|
|
410
411
|
quotedDangerSearchPolicy.category !== "destructive",
|
|
411
412
|
"quoted destructive strings in grep pattern should not classify as a destructive command"
|
|
412
413
|
);
|
|
414
|
+
const grepCountHint = shellDiagnosticHint("grep -c 'Fatal\\|Emergency' article.log && echo done", {
|
|
415
|
+
ok: false,
|
|
416
|
+
exitCode: 1,
|
|
417
|
+
stdout: "0\n",
|
|
418
|
+
stderr: "",
|
|
419
|
+
});
|
|
420
|
+
assert(grepCountHint.includes("grep -c exits 1"), "failed grep-count validation should explain clean zero-match exit behavior");
|
|
413
421
|
const actualDangerAfterQuotePolicy = evaluateCommandPolicy('echo "rm -rf is text" && rm -rf reports', dockerWorkspacePolicy);
|
|
414
422
|
assert(!actualDangerAfterQuotePolicy.allowed, "actual destructive command after quoted text should still be blocked");
|
|
415
423
|
assert(actualDangerAfterQuotePolicy.category === "destructive", "actual destructive command after quoted text was not classified as destructive");
|
|
@@ -5,7 +5,7 @@ import path from "node:path";
|
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
import { readArtifactContent } from "../src/artifact-tunnel.js";
|
|
7
7
|
import { evaluateCommandPolicy } from "../src/command-policy.js";
|
|
8
|
-
import { runDockerPreflight, runDockerSandboxCommand } from "../src/docker-sandbox.js";
|
|
8
|
+
import { dockerReadOnlyHostMounts, runDockerPreflight, runDockerSandboxCommand } from "../src/docker-sandbox.js";
|
|
9
9
|
import { resolveRuntimeConfig } from "../src/config.js";
|
|
10
10
|
|
|
11
11
|
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
@@ -13,7 +13,10 @@ const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "agintiflow-toolchain-"
|
|
|
13
13
|
process.env.AGINTIFLOW_HOME = path.join(tempRoot, ".agintiflow-home");
|
|
14
14
|
const runtimeDir = path.join(tempRoot, "runtime");
|
|
15
15
|
const workspace = path.join(tempRoot, "workspace");
|
|
16
|
+
const outsideData = path.join(tempRoot, "outside-data");
|
|
16
17
|
await fs.mkdir(workspace, { recursive: true });
|
|
18
|
+
await fs.mkdir(outsideData, { recursive: true });
|
|
19
|
+
process.env.AGINTI_DOCKER_READONLY_HOST_MOUNTS = tempRoot;
|
|
17
20
|
|
|
18
21
|
function assert(condition, message) {
|
|
19
22
|
if (!condition) throw new Error(message);
|
|
@@ -53,6 +56,33 @@ try {
|
|
|
53
56
|
|
|
54
57
|
const preflight = await runDockerPreflight(config, { buildImage: false });
|
|
55
58
|
assert(preflight.ok, "Docker toolchain preflight failed. Run scripts/setup-agent-toolchain-docker.sh first.");
|
|
59
|
+
assert(
|
|
60
|
+
dockerReadOnlyHostMounts(config).includes(tempRoot),
|
|
61
|
+
"Docker read-only host mount configuration did not include the requested outside-data root"
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
await fs.writeFile(path.join(outsideData, "source-note.txt"), "READ_ONLY_HOST_MOUNT_OK\n", "utf8");
|
|
65
|
+
const outsideRead = await runToolchainCommand(`cat ${path.join(outsideData, "source-note.txt")}`, config);
|
|
66
|
+
assert(outsideRead.stdout.includes("READ_ONLY_HOST_MOUNT_OK"), "Docker normal mode could not read the configured outside data root");
|
|
67
|
+
|
|
68
|
+
const abortController = new AbortController();
|
|
69
|
+
const abortCommand = `python3 -c 'import time; time.sleep(20)'`;
|
|
70
|
+
const abortPolicy = evaluateCommandPolicy(abortCommand, { ...config, packageInstallPolicy: "allow" });
|
|
71
|
+
assert(abortPolicy.allowed, "long-running Docker command should be allowed for interrupt smoke");
|
|
72
|
+
const abortStartedAt = Date.now();
|
|
73
|
+
const abortRun = runDockerSandboxCommand(abortCommand, { ...config, packageInstallPolicy: "allow" }, abortPolicy, {
|
|
74
|
+
signal: abortController.signal,
|
|
75
|
+
});
|
|
76
|
+
setTimeout(() => abortController.abort(new Error("smoke interrupt")), 500);
|
|
77
|
+
let interrupted = false;
|
|
78
|
+
try {
|
|
79
|
+
await abortRun;
|
|
80
|
+
} catch (error) {
|
|
81
|
+
interrupted = /abort|interrupt|cancel/i.test(String(error?.name || "")) || /abort|interrupt|cancel/i.test(String(error?.message || ""));
|
|
82
|
+
}
|
|
83
|
+
const abortElapsed = Date.now() - abortStartedAt;
|
|
84
|
+
assert(interrupted, "Docker command did not reject as interrupted after AbortController abort");
|
|
85
|
+
assert(abortElapsed < 8000, `Docker interrupt took too long: ${abortElapsed}ms`);
|
|
56
86
|
|
|
57
87
|
await fs.writeFile(
|
|
58
88
|
path.join(workspace, "plot_fx.py"),
|
|
@@ -174,6 +204,7 @@ try {
|
|
|
174
204
|
ok: true,
|
|
175
205
|
image: config.dockerSandboxImage,
|
|
176
206
|
workspace,
|
|
207
|
+
outsideReadRoot: outsideData,
|
|
177
208
|
outputs: ["plot_fx.svg", "paper.pdf", "figure-note/fx_figure.pdf", "figure-note/figure_note.pdf"],
|
|
178
209
|
artifactKinds: ["image/svg+xml", "application/pdf"],
|
|
179
210
|
},
|
package/src/agent-runner.js
CHANGED
|
@@ -2,9 +2,8 @@ import crypto from "node:crypto";
|
|
|
2
2
|
import fs from "node:fs/promises";
|
|
3
3
|
import net from "node:net";
|
|
4
4
|
import path from "node:path";
|
|
5
|
-
import {
|
|
5
|
+
import { spawn } from "node:child_process";
|
|
6
6
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
7
|
-
import { promisify } from "node:util";
|
|
8
7
|
import { chromium } from "playwright";
|
|
9
8
|
import { createClient, createPlan, requestNextStep } from "./model-client.js";
|
|
10
9
|
import { SessionStore } from "./session-store.js";
|
|
@@ -54,7 +53,6 @@ import {
|
|
|
54
53
|
serializeStepBudgetState,
|
|
55
54
|
} from "./step-budget-controller.js";
|
|
56
55
|
|
|
57
|
-
const exec = promisify(execCallback);
|
|
58
56
|
const BROWSER_TOOLS = new Set(["open_url", "open_workspace_file", "preview_workspace", "click", "type", "scroll", "press", "back"]);
|
|
59
57
|
const WORKSPACE_TOOLS = new Set(WORKSPACE_TOOL_NAMES);
|
|
60
58
|
const STATIC_PREVIEW_SERVER_PATH = fileURLToPath(new URL("./static-preview-server.js", import.meta.url));
|
|
@@ -580,13 +578,14 @@ async function createInitialState(config, sessionId) {
|
|
|
580
578
|
"Treat AGINTI.md as durable project memory and operating instructions for this project. The user can edit it manually or ask you in chat to update it; use workspace file tools for that and never store secrets there.",
|
|
581
579
|
config.allowShellTool
|
|
582
580
|
? config.useDockerSandbox
|
|
583
|
-
? `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. The project is mounted at /workspace
|
|
581
|
+
? `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. The project is mounted at /workspace, persistent agent toolchain state is mounted at /aginti-env with caches under /aginti-cache, and common host data roots such as the user's home parent are mounted read-only at their original absolute paths. Use /workspace for outputs and writes; use absolute host paths only for read-only inspection when visible. Do not run npx aginti, npm exec aginti, or nested aginti diagnostics from this Docker shell; they may resolve stale project packages or create recursive agent sessions.`
|
|
584
582
|
: `A host shell command tool is available under the configured trust policy on ${platformLabel(platform)}. On native Windows, prefer PowerShell/cmd-compatible commands or switch to WSL/Docker for bash-like toolchains.`
|
|
585
583
|
: "No shell command tool is available.",
|
|
586
|
-
`Permission contract: permission mode is ${config.permissionMode || "normal"}. Safe mode asks before workspace writes/setup. Normal mode allows current-project writes and approved Docker setup, but outside-workspace
|
|
584
|
+
`Permission contract: permission mode is ${config.permissionMode || "normal"}. Safe mode asks before workspace writes/setup. Normal mode allows current-project writes, read-only inspection of visible host paths, and approved Docker setup, but outside-workspace writes and host-system changes require approval. Danger mode is trusted host/full-access mode. Do not bypass blockers by retrying variants. If a tool result includes permissionAdvice or suggestedCommand, stop, explain the blocker, copy the exact suggestedCommand when giving a rerun path, and ask the user to approve/rerun that mode or choose a safer workspace-relative path. Never invent legacy AgInTi syntax such as \`aginti run --sandbox host\`; use the exact flags from permissionAdvice.`,
|
|
587
585
|
"If an operation fails but a directory, artifact, or file already exists, treat it as pre-existing unless you have evidence this run created or updated it. Verify expected outputs before claiming success.",
|
|
586
|
+
"For validation/evidence commands, remember that grep exits 1 on zero matches. If zero matches is the expected clean result, use `grep -c PATTERN file || true`, split evidence checks into independent commands, or use awk/python so a clean zero count does not stop an `&&` chain.",
|
|
588
587
|
config.allowShellTool
|
|
589
|
-
? "Host tmux tools are available for long-running terminals: list sessions, capture panes, send safe keys/text, and start detached sessions. Prefer these tools for monitoring long installs/tests/dev servers without blocking; capture before sending input and never send secrets or sudo passwords. Do not start or install tmux inside Docker run_command containers because those containers are short-lived. In Docker sandbox mode, tmux start/send commands must stay workspace-bound
|
|
588
|
+
? "Host tmux tools are available for long-running terminals: list sessions, capture panes, send safe keys/text, and start detached sessions. Prefer these tools for monitoring long installs/tests/dev servers without blocking; capture before sending input and never send secrets or sudo passwords. Do not start or install tmux inside Docker run_command containers because those containers are short-lived. In Docker sandbox mode, tmux start/send commands must stay workspace-write-bound; prefer run_command for read-only host absolute path inspection through read-only mounts, and ask for --sandbox-mode host for trusted whole-host write/system work."
|
|
590
589
|
+ " For one-shot tmux commands, redirect stdout/stderr and exit status to a durable workspace log or keep the pane alive for capture; if capture fails because the session ended, do not infer output or exit status."
|
|
591
590
|
: "",
|
|
592
591
|
config.allowFileTools
|
|
@@ -640,7 +639,7 @@ async function createInitialState(config, sessionId) {
|
|
|
640
639
|
config.allowedDomains.length > 0 ? `Allowed domains: ${config.allowedDomains.join(", ")}` : "",
|
|
641
640
|
config.allowShellTool
|
|
642
641
|
? config.useDockerSandbox
|
|
643
|
-
? `Shell working directory mounted into Docker as /workspace from ${config.commandCwd}. Use relative paths or /workspace paths
|
|
642
|
+
? `Shell working directory mounted into Docker as /workspace from ${config.commandCwd}. Use relative paths or /workspace paths for outputs/writes; common host data roots are read-only at original absolute paths for inspection. Persistent Docker env: /aginti-env, caches: /aginti-cache. Sandbox mode: ${config.sandboxMode}. Package install policy: ${config.packageInstallPolicy}.`
|
|
644
643
|
: `Shell working directory: ${config.commandCwd}`
|
|
645
644
|
: "",
|
|
646
645
|
config.allowFileTools
|
|
@@ -1006,9 +1005,10 @@ async function applyContinuationPrompt(state, config, observers) {
|
|
|
1006
1005
|
temporalContext,
|
|
1007
1006
|
config.startUrl ? `Suggested start URL: ${config.startUrl}` : "",
|
|
1008
1007
|
config.allowedDomains.length > 0 ? `Allowed domains: ${config.allowedDomains.join(", ")}` : "",
|
|
1008
|
+
"Validation reminder: grep exits 1 on zero matches. For clean-zero checks, guard `grep -c` with `|| true` or split evidence commands so the validation can continue.",
|
|
1009
1009
|
config.allowShellTool
|
|
1010
1010
|
? config.useDockerSandbox
|
|
1011
|
-
? `Shell working directory mounted into Docker as /workspace from ${config.commandCwd}. Use relative paths or /workspace paths. Persistent Docker env: /aginti-env, caches: /aginti-cache. Sandbox mode: ${config.sandboxMode}. Package install policy: ${config.packageInstallPolicy}.`
|
|
1011
|
+
? `Shell working directory mounted into Docker as /workspace from ${config.commandCwd}. Use relative paths or /workspace paths for outputs/writes; common host data roots are read-only at original absolute paths for inspection. Persistent Docker env: /aginti-env, caches: /aginti-cache. Sandbox mode: ${config.sandboxMode}. Package install policy: ${config.packageInstallPolicy}.`
|
|
1012
1012
|
: `Shell working directory: ${config.commandCwd}. Host platform: ${platformLabel(platform)}. Use OS-compatible commands; prefer WSL/Docker for bash-heavy workflows on Windows.`
|
|
1013
1013
|
: "",
|
|
1014
1014
|
config.allowFileTools
|
|
@@ -1076,6 +1076,112 @@ function safeExecutionEnv() {
|
|
|
1076
1076
|
};
|
|
1077
1077
|
}
|
|
1078
1078
|
|
|
1079
|
+
function trimOutput(value = "", limit = 8000) {
|
|
1080
|
+
const text = redactSensitiveText(String(value || ""));
|
|
1081
|
+
return text.trim().slice(0, limit);
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
function killChildTree(child, signal = "SIGTERM") {
|
|
1085
|
+
if (!child || child.killed) return;
|
|
1086
|
+
try {
|
|
1087
|
+
if (process.platform === "win32") child.kill(signal);
|
|
1088
|
+
else process.kill(-child.pid, signal);
|
|
1089
|
+
} catch {
|
|
1090
|
+
try {
|
|
1091
|
+
child.kill(signal);
|
|
1092
|
+
} catch {
|
|
1093
|
+
// Best effort; process may already be gone.
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
function runHostShellCommand(command, config) {
|
|
1099
|
+
return new Promise((resolve, reject) => {
|
|
1100
|
+
const shell = hostShellOption();
|
|
1101
|
+
const child = spawn(String(command || ""), {
|
|
1102
|
+
cwd: config.commandCwd,
|
|
1103
|
+
env: safeExecutionEnv(),
|
|
1104
|
+
shell,
|
|
1105
|
+
detached: process.platform !== "win32",
|
|
1106
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
1107
|
+
});
|
|
1108
|
+
let stdout = "";
|
|
1109
|
+
let stderr = "";
|
|
1110
|
+
let settled = false;
|
|
1111
|
+
let timedOut = false;
|
|
1112
|
+
const timeoutMs = Number(config.shellTimeoutMs || process.env.AGINTI_SHELL_TIMEOUT_MS || 30000);
|
|
1113
|
+
const maxStdout = 220 * 1024;
|
|
1114
|
+
const maxStderr = 120 * 1024;
|
|
1115
|
+
|
|
1116
|
+
const settle = (callback) => {
|
|
1117
|
+
if (settled) return;
|
|
1118
|
+
settled = true;
|
|
1119
|
+
if (timer) clearTimeout(timer);
|
|
1120
|
+
if (config.abortSignal && onAbort) config.abortSignal.removeEventListener("abort", onAbort);
|
|
1121
|
+
callback();
|
|
1122
|
+
};
|
|
1123
|
+
const onAbort = () => {
|
|
1124
|
+
killChildTree(child, "SIGTERM");
|
|
1125
|
+
setTimeout(() => killChildTree(child, "SIGKILL"), 1200).unref?.();
|
|
1126
|
+
const error = new Error("Run interrupted by user.");
|
|
1127
|
+
error.name = "AbortError";
|
|
1128
|
+
error.code = "ABORT_ERR";
|
|
1129
|
+
settle(() => reject(error));
|
|
1130
|
+
};
|
|
1131
|
+
const timer =
|
|
1132
|
+
Number.isFinite(timeoutMs) && timeoutMs > 0
|
|
1133
|
+
? setTimeout(() => {
|
|
1134
|
+
timedOut = true;
|
|
1135
|
+
killChildTree(child, "SIGTERM");
|
|
1136
|
+
setTimeout(() => killChildTree(child, "SIGKILL"), 1200).unref?.();
|
|
1137
|
+
}, timeoutMs)
|
|
1138
|
+
: null;
|
|
1139
|
+
timer?.unref?.();
|
|
1140
|
+
|
|
1141
|
+
if (config.abortSignal?.aborted) return onAbort();
|
|
1142
|
+
if (config.abortSignal) config.abortSignal.addEventListener("abort", onAbort, { once: true });
|
|
1143
|
+
|
|
1144
|
+
child.stdout?.on("data", (chunk) => {
|
|
1145
|
+
if (stdout.length < maxStdout) stdout += chunk.toString();
|
|
1146
|
+
});
|
|
1147
|
+
child.stderr?.on("data", (chunk) => {
|
|
1148
|
+
if (stderr.length < maxStderr) stderr += chunk.toString();
|
|
1149
|
+
});
|
|
1150
|
+
child.on("error", (error) => {
|
|
1151
|
+
settle(() =>
|
|
1152
|
+
resolve({
|
|
1153
|
+
ok: false,
|
|
1154
|
+
exitCode: 1,
|
|
1155
|
+
stdout,
|
|
1156
|
+
stderr: `${stderr}${stderr ? "\n" : ""}${error instanceof Error ? error.message : String(error)}`,
|
|
1157
|
+
})
|
|
1158
|
+
);
|
|
1159
|
+
});
|
|
1160
|
+
child.on("close", (code, signalName) => {
|
|
1161
|
+
settle(() =>
|
|
1162
|
+
resolve({
|
|
1163
|
+
ok: !timedOut && Number(code || 0) === 0,
|
|
1164
|
+
exitCode: timedOut ? 124 : Number.isInteger(code) ? code : signalName ? 130 : 1,
|
|
1165
|
+
stdout,
|
|
1166
|
+
stderr: `${stderr}${timedOut ? `\nCommand timed out after ${timeoutMs}ms.` : ""}`,
|
|
1167
|
+
})
|
|
1168
|
+
);
|
|
1169
|
+
});
|
|
1170
|
+
});
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
export function shellDiagnosticHint(command = "", result = {}) {
|
|
1174
|
+
if (result?.ok !== false) return "";
|
|
1175
|
+
const text = String(command || "");
|
|
1176
|
+
if (/\bgrep\b[\s\S]*\s-c\b|\bgrep\s+-c\b/.test(text)) {
|
|
1177
|
+
return "grep -c exits 1 when it finds zero matches even though it prints 0. For count-only validation, use `grep -c PATTERN file || true`, split evidence commands, or use awk/python when zero matches is the expected clean state.";
|
|
1178
|
+
}
|
|
1179
|
+
if (/\bgrep\b/.test(text) && /&&/.test(text)) {
|
|
1180
|
+
return "grep exits 1 when it finds no matches, so an `&&` evidence chain can stop early. If no matches is acceptable, guard that grep with `|| true` or run independent validation commands.";
|
|
1181
|
+
}
|
|
1182
|
+
return "";
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1079
1185
|
function hashForLog(value) {
|
|
1080
1186
|
return crypto.createHash("sha256").update(String(value ?? "")).digest("hex");
|
|
1081
1187
|
}
|
|
@@ -1261,29 +1367,26 @@ async function runShellCommand(command, config, policy = evaluateCommandPolicy(c
|
|
|
1261
1367
|
throwIfAborted(config);
|
|
1262
1368
|
const result = config.useDockerSandbox
|
|
1263
1369
|
? await runDockerSandboxCommand(command, config, policy, { signal: config.abortSignal })
|
|
1264
|
-
: await
|
|
1265
|
-
|
|
1266
|
-
timeout: 30000,
|
|
1267
|
-
maxBuffer: 200 * 1024,
|
|
1268
|
-
shell: hostShellOption(),
|
|
1269
|
-
env: safeExecutionEnv(),
|
|
1270
|
-
signal: config.abortSignal,
|
|
1271
|
-
});
|
|
1370
|
+
: await runHostShellCommand(command, config);
|
|
1371
|
+
const diagnosticHint = shellDiagnosticHint(command, result);
|
|
1272
1372
|
|
|
1273
1373
|
return {
|
|
1274
|
-
ok:
|
|
1275
|
-
exitCode: 0,
|
|
1276
|
-
stdout:
|
|
1277
|
-
stderr:
|
|
1374
|
+
ok: result.ok !== false,
|
|
1375
|
+
exitCode: Number.isInteger(result.exitCode) ? result.exitCode : 0,
|
|
1376
|
+
stdout: trimOutput(result.stdout, 8000),
|
|
1377
|
+
stderr: trimOutput(result.stderr, 4000),
|
|
1378
|
+
...(diagnosticHint ? { diagnosticHint } : {}),
|
|
1278
1379
|
};
|
|
1279
1380
|
} catch (error) {
|
|
1280
1381
|
if (isAbortError(error, config)) throw error;
|
|
1281
|
-
|
|
1382
|
+
const failedResult = {
|
|
1282
1383
|
ok: false,
|
|
1283
1384
|
exitCode: Number.isInteger(error?.code) ? error.code : 1,
|
|
1284
|
-
stdout:
|
|
1285
|
-
stderr:
|
|
1385
|
+
stdout: trimOutput(error?.stdout || "", 8000),
|
|
1386
|
+
stderr: trimOutput(error?.stderr || error?.message || "", 4000),
|
|
1286
1387
|
};
|
|
1388
|
+
const diagnosticHint = shellDiagnosticHint(command, failedResult);
|
|
1389
|
+
return diagnosticHint ? { ...failedResult, diagnosticHint } : failedResult;
|
|
1287
1390
|
}
|
|
1288
1391
|
}
|
|
1289
1392
|
|
|
@@ -1295,13 +1398,14 @@ async function captureSyntheticSnapshot(store, step, config) {
|
|
|
1295
1398
|
pageText: [
|
|
1296
1399
|
"No browser page is currently open.",
|
|
1297
1400
|
config.startUrl ? `Suggested start URL: ${config.startUrl}` : "",
|
|
1401
|
+
"Validation reminder: grep exits 1 on zero matches; guard expected clean-zero grep checks or split evidence commands.",
|
|
1298
1402
|
config.allowShellTool
|
|
1299
1403
|
? config.useDockerSandbox
|
|
1300
|
-
? `Shell tool available in Docker with mounted workspace /workspace from ${config.commandCwd}. Use relative paths or /workspace paths. Persistent Docker env: /aginti-env, caches: /aginti-cache. Sandbox mode: ${config.sandboxMode}. Package install policy: ${config.packageInstallPolicy}.`
|
|
1404
|
+
? `Shell tool available in Docker with mounted workspace /workspace from ${config.commandCwd}. Use relative paths or /workspace paths for outputs/writes; common host data roots are read-only at original absolute paths for inspection. Persistent Docker env: /aginti-env, caches: /aginti-cache. Sandbox mode: ${config.sandboxMode}. Package install policy: ${config.packageInstallPolicy}.`
|
|
1301
1405
|
: `Shell tool available in: ${config.commandCwd} on ${platformLabel(platform)}. Use OS-compatible commands; prefer WSL/Docker for bash-heavy workflows on Windows.`
|
|
1302
1406
|
: "Shell tool disabled.",
|
|
1303
1407
|
config.allowShellTool
|
|
1304
|
-
? "Host tmux tools available: tmux_list_sessions, tmux_capture_pane, tmux_send_keys, tmux_start_session. Use them for long-running jobs and agent terminals; capture before sending input. Docker run_command containers are ephemeral, so tmux there will not persist. In Docker sandbox mode, tmux start/send commands must stay workspace-bound
|
|
1408
|
+
? "Host tmux tools available: tmux_list_sessions, tmux_capture_pane, tmux_send_keys, tmux_start_session. Use them for long-running jobs and agent terminals; capture before sending input. Docker run_command containers are ephemeral, so tmux there will not persist. In Docker sandbox mode, tmux start/send commands must stay workspace-write-bound; when sending text into a shell pane, tmux follows the same Docker workspace command policy as run_command and is not a bypass for package installs, destructive git history rewrites, or broad shell text. Prefer run_command for read-only host absolute path inspection through read-only mounts. Use --sandbox-mode host for trusted whole-host write/system work. In host mode, tmux startup/send command text follows the same host shell policy as run_command; if a broad host command is blocked, present the approval/rerun path instead of trying tmux as a workaround. For one-shot tmux commands, redirect output and exit status to a durable workspace log or keep the pane alive for capture; if capture fails because the session ended, do not infer output or exit status."
|
|
1305
1409
|
: "",
|
|
1306
1410
|
config.allowFileTools
|
|
1307
1411
|
? `Workspace file tools available in: ${config.commandCwd}. Use inspect_project first for large or unfamiliar codebases, then search/read exact files before editing. Use workspace-relative paths. Use apply_patch for code edits; it supports exact single-file replacement and multi-file Codex-style/unified patches. For new standalone generated content, pick a descriptive non-conflicting filename and avoid overwriting unless explicitly requested.`
|
|
@@ -2377,6 +2481,7 @@ export async function runAgent(config) {
|
|
|
2377
2481
|
command: redactSensitiveText(toolResult.args?.command || ""),
|
|
2378
2482
|
stdout: toolResult.stdout || "",
|
|
2379
2483
|
stderr: toolResult.stderr || "",
|
|
2484
|
+
diagnosticHint: toolResult.diagnosticHint || "",
|
|
2380
2485
|
commandPolicy: toolResult.commandPolicy,
|
|
2381
2486
|
blocked: Boolean(toolResult.blocked),
|
|
2382
2487
|
error: toolResult.error || toolResult.reason || "",
|
package/src/docker-sandbox.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import fsSync, { constants as fsConstants } from "node:fs";
|
|
2
3
|
import fs from "node:fs/promises";
|
|
3
4
|
import os from "node:os";
|
|
4
5
|
import path from "node:path";
|
|
@@ -15,6 +16,7 @@ const DOCKER_ENV = "/aginti-env";
|
|
|
15
16
|
const READY_IMAGES = new Set();
|
|
16
17
|
const SANDBOX_LOG_LIMIT = 80;
|
|
17
18
|
const sandboxLogs = [];
|
|
19
|
+
const DISABLE_HOST_READ_MOUNTS = new Set(["0", "false", "no", "off", "none"]);
|
|
18
20
|
|
|
19
21
|
function recordSandboxLog(type, data = {}) {
|
|
20
22
|
sandboxLogs.push({
|
|
@@ -62,6 +64,23 @@ async function execDocker(args, options = {}) {
|
|
|
62
64
|
signal: options.signal,
|
|
63
65
|
};
|
|
64
66
|
recordSandboxLog("docker.command", { args });
|
|
67
|
+
const containerName = options.containerName || "";
|
|
68
|
+
let abortHandler = null;
|
|
69
|
+
if (options.signal && containerName) {
|
|
70
|
+
abortHandler = () => {
|
|
71
|
+
void execFile("docker", ["kill", containerName], {
|
|
72
|
+
timeout: 8000,
|
|
73
|
+
maxBuffer: 32 * 1024,
|
|
74
|
+
}).catch((error) => {
|
|
75
|
+
recordSandboxLog("docker.kill.failed", {
|
|
76
|
+
containerName,
|
|
77
|
+
error: error instanceof Error ? error.message : String(error),
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
};
|
|
81
|
+
if (options.signal.aborted) abortHandler();
|
|
82
|
+
else options.signal.addEventListener("abort", abortHandler, { once: true });
|
|
83
|
+
}
|
|
65
84
|
|
|
66
85
|
try {
|
|
67
86
|
const result = await execFile("docker", args, execOptions);
|
|
@@ -80,6 +99,8 @@ async function execDocker(args, options = {}) {
|
|
|
80
99
|
stdout: redactSensitiveText(result.stdout),
|
|
81
100
|
stderr: redactSensitiveText(result.stderr),
|
|
82
101
|
};
|
|
102
|
+
} finally {
|
|
103
|
+
if (options.signal && abortHandler) options.signal.removeEventListener("abort", abortHandler);
|
|
83
104
|
}
|
|
84
105
|
}
|
|
85
106
|
|
|
@@ -140,6 +161,30 @@ function persistentDockerDirs(config) {
|
|
|
140
161
|
};
|
|
141
162
|
}
|
|
142
163
|
|
|
164
|
+
function uniqueHostMounts(mounts = []) {
|
|
165
|
+
const seen = new Set();
|
|
166
|
+
const result = [];
|
|
167
|
+
for (const mount of mounts) {
|
|
168
|
+
const normalized = path.resolve(String(mount || "").trim());
|
|
169
|
+
if (!normalized || normalized === "/" || seen.has(normalized)) continue;
|
|
170
|
+
if (!path.isAbsolute(normalized) || !fsSync.existsSync(normalized)) continue;
|
|
171
|
+
seen.add(normalized);
|
|
172
|
+
result.push(normalized);
|
|
173
|
+
}
|
|
174
|
+
return result;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export function dockerReadOnlyHostMounts(config = {}, sandboxMode = normalizeSandboxMode(config.sandboxMode)) {
|
|
178
|
+
if (sandboxMode !== "docker-workspace") return [];
|
|
179
|
+
const configured = process.env.AGINTI_DOCKER_READONLY_HOST_MOUNTS || process.env.AGINTI_DOCKER_HOST_READ_MOUNTS || "";
|
|
180
|
+
if (DISABLE_HOST_READ_MOUNTS.has(configured.trim().toLowerCase())) return [];
|
|
181
|
+
if (Array.isArray(config.readOnlyHostMounts)) return uniqueHostMounts(config.readOnlyHostMounts);
|
|
182
|
+
if (configured.trim()) return uniqueHostMounts(configured.split(path.delimiter));
|
|
183
|
+
|
|
184
|
+
const homeParent = path.dirname(os.homedir() || "");
|
|
185
|
+
return uniqueHostMounts([homeParent, "/mnt", "/media", "/Volumes"]);
|
|
186
|
+
}
|
|
187
|
+
|
|
143
188
|
async function ensurePersistentDockerDirs(config) {
|
|
144
189
|
const dirs = persistentDockerDirs(config);
|
|
145
190
|
await Promise.all([fs.mkdir(dirs.home, { recursive: true }), fs.mkdir(dirs.cache, { recursive: true }), fs.mkdir(dirs.env, { recursive: true })]);
|
|
@@ -257,7 +302,11 @@ function dockerCommand(command, policy) {
|
|
|
257
302
|
return [...envLines, dockerUserCommand(command, policy)].join("\n");
|
|
258
303
|
}
|
|
259
304
|
|
|
260
|
-
function
|
|
305
|
+
function dockerContainerName() {
|
|
306
|
+
return `aginti-${process.pid}-${Date.now()}-${crypto.randomBytes(4).toString("hex")}`.slice(0, 63);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function dockerRunArgs(command, config, policy = evaluateCommandPolicy(command, config), persistentDirs = persistentDockerDirs(config), options = {}) {
|
|
261
310
|
const uid = typeof process.getuid === "function" ? String(process.getuid()) : "";
|
|
262
311
|
const gid = typeof process.getgid === "function" ? String(process.getgid()) : "";
|
|
263
312
|
const userArgs = uid && gid && !policy.requiresDockerRoot ? ["--user", `${uid}:${gid}`] : [];
|
|
@@ -265,6 +314,7 @@ function dockerRunArgs(command, config, policy = evaluateCommandPolicy(command,
|
|
|
265
314
|
const mountMode = sandboxMode === "docker-workspace" ? "rw" : "ro";
|
|
266
315
|
const networkMode = policy.needsNetwork ? "bridge" : "none";
|
|
267
316
|
const toolchain = policy.category === "toolchain";
|
|
317
|
+
const readOnlyHostMountArgs = dockerReadOnlyHostMounts(config, sandboxMode).flatMap((mount) => ["-v", `${mount}:${mount}:ro`]);
|
|
268
318
|
const readOnlyArgs =
|
|
269
319
|
mountMode === "ro"
|
|
270
320
|
? ["--read-only", "--tmpfs", "/tmp:rw,nosuid,nodev,size=128m"]
|
|
@@ -273,6 +323,7 @@ function dockerRunArgs(command, config, policy = evaluateCommandPolicy(command,
|
|
|
273
323
|
return [
|
|
274
324
|
"run",
|
|
275
325
|
"--rm",
|
|
326
|
+
...(options.containerName ? ["--name", options.containerName] : []),
|
|
276
327
|
"--network",
|
|
277
328
|
networkMode,
|
|
278
329
|
"--cap-drop",
|
|
@@ -291,6 +342,7 @@ function dockerRunArgs(command, config, policy = evaluateCommandPolicy(command,
|
|
|
291
342
|
"NPM_CONFIG_USERCONFIG=/tmp/.npmrc",
|
|
292
343
|
"-e",
|
|
293
344
|
"PIP_DISABLE_PIP_VERSION_CHECK=1",
|
|
345
|
+
...readOnlyHostMountArgs,
|
|
294
346
|
"-v",
|
|
295
347
|
`${config.commandCwd}:${DOCKER_WORKSPACE}:${mountMode}`,
|
|
296
348
|
"-v",
|
|
@@ -310,10 +362,12 @@ function dockerRunArgs(command, config, policy = evaluateCommandPolicy(command,
|
|
|
310
362
|
|
|
311
363
|
export async function runDockerSandboxCommand(command, config, policy = evaluateCommandPolicy(command, config), options = {}) {
|
|
312
364
|
const persistentDirs = await ensurePersistentDockerDirs(config);
|
|
313
|
-
const
|
|
365
|
+
const containerName = dockerContainerName();
|
|
366
|
+
const result = await execDocker(dockerRunArgs(command, config, policy, persistentDirs, { containerName }), {
|
|
314
367
|
timeout: dockerExecTimeoutMs(policy),
|
|
315
368
|
maxBuffer: 300 * 1024,
|
|
316
369
|
signal: options.signal,
|
|
370
|
+
containerName,
|
|
317
371
|
});
|
|
318
372
|
|
|
319
373
|
const payload = {
|
package/src/interactive-cli.js
CHANGED
|
@@ -720,6 +720,9 @@ function printCommandOutputLog(data = {}) {
|
|
|
720
720
|
if (advice.suggestedCommand) outputLine(`${color(" | ", ansi.red)} rerun: ${compactLine(advice.suggestedCommand, 120)}`);
|
|
721
721
|
if (advice.trustedHostCommand) outputLine(`${color(" | ", ansi.red)} host: ${compactLine(advice.trustedHostCommand, 120)}`);
|
|
722
722
|
}
|
|
723
|
+
if (data.diagnosticHint) {
|
|
724
|
+
outputLine(`${label("hint", ansi.yellow)} ${compactLine(data.diagnosticHint, 112)}`);
|
|
725
|
+
}
|
|
723
726
|
}
|
|
724
727
|
|
|
725
728
|
function printHeading(text) {
|
|
@@ -1182,6 +1185,13 @@ export function classifyEscapeAction({ active = false, pendingAsap = [] } = {})
|
|
|
1182
1185
|
return Array.isArray(pendingAsap) && pendingAsap.length > 0 ? "wait-for-asap" : "abort";
|
|
1183
1186
|
}
|
|
1184
1187
|
|
|
1188
|
+
export function activeRunSlashCommandAction(value = "") {
|
|
1189
|
+
const content = canonicalSlashPromptBuffer(value).trim();
|
|
1190
|
+
if (!content.startsWith("/")) return "";
|
|
1191
|
+
if (/^\/status(?:\s|$)/.test(content)) return "status";
|
|
1192
|
+
return "blocked";
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1185
1195
|
function readTtyPrompt(options = {}) {
|
|
1186
1196
|
return new Promise((resolve, reject) => {
|
|
1187
1197
|
emitKeypressEvents(input);
|
|
@@ -1581,8 +1591,18 @@ class LiveRunInput {
|
|
|
1581
1591
|
async submitAsap() {
|
|
1582
1592
|
const content = this.buffer.trim();
|
|
1583
1593
|
if (!content) return;
|
|
1584
|
-
|
|
1585
|
-
|
|
1594
|
+
const slashAction = activeRunSlashCommandAction(content);
|
|
1595
|
+
if (slashAction === "status") {
|
|
1596
|
+
this.setBuffer("");
|
|
1597
|
+
printStatus(this.state);
|
|
1598
|
+
if (this.pendingAsap.length || this.pendingQueued.length) {
|
|
1599
|
+
printSystemLine(`pending asap=${this.pendingAsap.length} queued=${this.pendingQueued.length}`);
|
|
1600
|
+
}
|
|
1601
|
+
this.redraw();
|
|
1602
|
+
return;
|
|
1603
|
+
}
|
|
1604
|
+
if (slashAction === "blocked") {
|
|
1605
|
+
this.printLine(`${label("warn", ansi.red)} Slash commands are disabled while a run is active except /status. Press Esc/Ctrl+C to stop, or wait until idle.`);
|
|
1586
1606
|
this.setStatus(`running · command ${compactLine(content, 24)} not accepted during active run`);
|
|
1587
1607
|
return;
|
|
1588
1608
|
}
|
|
@@ -1610,7 +1630,14 @@ class LiveRunInput {
|
|
|
1610
1630
|
queueAfterFinish() {
|
|
1611
1631
|
const content = this.buffer.trim();
|
|
1612
1632
|
if (!content) return;
|
|
1613
|
-
|
|
1633
|
+
const slashAction = activeRunSlashCommandAction(content);
|
|
1634
|
+
if (slashAction === "status") {
|
|
1635
|
+
this.setBuffer("");
|
|
1636
|
+
printStatus(this.state);
|
|
1637
|
+
this.redraw();
|
|
1638
|
+
return;
|
|
1639
|
+
}
|
|
1640
|
+
if (slashAction === "blocked") {
|
|
1614
1641
|
this.printLine(`${label("warn", ansi.red)} Slash commands cannot be queued during a run. Wait until idle, then run ${content}.`);
|
|
1615
1642
|
this.setStatus(`running · command ${compactLine(content, 24)} not queued`);
|
|
1616
1643
|
return;
|
|
@@ -1651,7 +1678,8 @@ class LiveRunInput {
|
|
|
1651
1678
|
|
|
1652
1679
|
handleKey(str = "", key = {}) {
|
|
1653
1680
|
if (key.ctrl && key.name === "c") {
|
|
1654
|
-
this.
|
|
1681
|
+
this.setStatus("stopping · ctrl-c");
|
|
1682
|
+
this.controller.abort(createAbortError("Interrupted by ctrl-c."));
|
|
1655
1683
|
return;
|
|
1656
1684
|
}
|
|
1657
1685
|
if (key.name === "escape") {
|
|
@@ -1662,7 +1690,8 @@ class LiveRunInput {
|
|
|
1662
1690
|
this.redraw();
|
|
1663
1691
|
return;
|
|
1664
1692
|
}
|
|
1665
|
-
this.
|
|
1693
|
+
this.setStatus("stopping · escape");
|
|
1694
|
+
this.controller.abort(createAbortError("Interrupted by escape."));
|
|
1666
1695
|
return;
|
|
1667
1696
|
}
|
|
1668
1697
|
if (key.meta && key.name === "up") {
|
|
@@ -1760,7 +1789,9 @@ function printStatus(state) {
|
|
|
1760
1789
|
printSystemLine(`project=${process.cwd()}`);
|
|
1761
1790
|
printSystemLine(`cwd=${state.commandCwd || process.cwd()}`);
|
|
1762
1791
|
printSystemLine(`session=${state.sessionId || "new"}`);
|
|
1763
|
-
|
|
1792
|
+
const progress =
|
|
1793
|
+
state.currentStep && state.currentMaxSteps ? ` progress=${state.currentStep}/${state.currentMaxSteps}` : state.currentStep ? ` progress=${state.currentStep}` : "";
|
|
1794
|
+
printSystemLine(`status=${state.status || "idle"}${progress}${state.activeGoal ? ` workingOn=${state.activeGoal}` : ""}`);
|
|
1764
1795
|
printSystemLine(`language=${state.language || cliLanguage} (${languageLabel(state.language || cliLanguage)})`);
|
|
1765
1796
|
if (state.lastEvent) printSystemLine(`last=${state.lastEvent}`);
|
|
1766
1797
|
printSystemLine(`provider=${state.provider || "auto"} routing=${state.routingMode} model=${state.model || "auto"}`);
|
|
@@ -1930,9 +1961,26 @@ async function printResumeHistory(state, { limit = 0 } = {}) {
|
|
|
1930
1961
|
for (const entry of shown) printHistoryEntry(entry);
|
|
1931
1962
|
}
|
|
1932
1963
|
|
|
1933
|
-
function
|
|
1964
|
+
function toolStatusDetails(data = {}) {
|
|
1965
|
+
const tool = data.toolName || "unknown";
|
|
1966
|
+
const args = data.args || {};
|
|
1967
|
+
if (tool === "run_command" && args.command) return `${tool}: ${compactLine(args.command, 58)}`;
|
|
1968
|
+
if ((tool === "write_file" || tool === "apply_patch" || tool === "read_file" || tool === "open_workspace_file") && args.path) {
|
|
1969
|
+
return `${tool}: ${compactLine(args.path, 58)}`;
|
|
1970
|
+
}
|
|
1971
|
+
if ((tool === "open_url" || tool === "web_research" || tool === "web_search") && (args.url || args.query || args.q)) {
|
|
1972
|
+
return `${tool}: ${compactLine(args.url || args.query || args.q, 58)}`;
|
|
1973
|
+
}
|
|
1974
|
+
return tool;
|
|
1975
|
+
}
|
|
1976
|
+
|
|
1977
|
+
function printStatusEvent(state, label, details = "", meta = {}) {
|
|
1934
1978
|
const safeDetails = compactLine(details, 72);
|
|
1935
|
-
state.
|
|
1979
|
+
if (meta.step) state.currentStep = meta.step;
|
|
1980
|
+
if (meta.maxSteps) state.currentMaxSteps = meta.maxSteps;
|
|
1981
|
+
const progress =
|
|
1982
|
+
state.currentStep && state.currentMaxSteps ? `step ${state.currentStep}/${state.currentMaxSteps} · ` : state.currentStep ? `step ${state.currentStep} · ` : "";
|
|
1983
|
+
state.lastEvent = `${progress}${safeDetails ? `${label}: ${safeDetails}` : label}`;
|
|
1936
1984
|
const statusText = `${state.status || "running"} · ${state.lastEvent}`;
|
|
1937
1985
|
if (activeRunInput) {
|
|
1938
1986
|
activeRunInput.setStatus(statusText);
|
|
@@ -1955,7 +2003,7 @@ function attachRunInterrupts(controller) {
|
|
|
1955
2003
|
if (controller.signal.aborted) return;
|
|
1956
2004
|
const reason = isEscape ? "escape" : "ctrl-c";
|
|
1957
2005
|
printSystemLine(`status=stopping reason=${reason}`);
|
|
1958
|
-
controller.abort(
|
|
2006
|
+
controller.abort(createAbortError(`Interrupted by ${reason}.`));
|
|
1959
2007
|
};
|
|
1960
2008
|
input.on("keypress", handler);
|
|
1961
2009
|
return () => {
|
|
@@ -3589,12 +3637,18 @@ async function runPrompt(prompt, state, packageDir, { approvalDepth = 0 } = {})
|
|
|
3589
3637
|
onEvent: (type, data = {}) => {
|
|
3590
3638
|
if (type === "plan.created") {
|
|
3591
3639
|
printStatusEvent(state, "planned");
|
|
3640
|
+
} else if (type === "budget.initialized") {
|
|
3641
|
+
state.currentMaxSteps = data.currentMaxSteps || data.initialMaxSteps || state.maxSteps;
|
|
3642
|
+
printStatusEvent(state, "budget", `${state.currentMaxSteps} steps`);
|
|
3592
3643
|
} else if (type === "model.requested") {
|
|
3593
|
-
printStatusEvent(state, "model_wait", `${data.provider || "model"}/${data.model || ""}
|
|
3644
|
+
printStatusEvent(state, "model_wait", `${data.provider || "model"}/${data.model || ""}`, {
|
|
3645
|
+
step: data.step,
|
|
3646
|
+
maxSteps: state.currentMaxSteps,
|
|
3647
|
+
});
|
|
3594
3648
|
} else if (type === "tool.started") {
|
|
3595
|
-
printStatusEvent(state, "tool", data
|
|
3596
|
-
} else if (type === "tool.completed") {
|
|
3597
|
-
printStatusEvent(state, "
|
|
3649
|
+
printStatusEvent(state, "tool", toolStatusDetails(data));
|
|
3650
|
+
} else if (type === "tool.completed" || type === "tool.failed") {
|
|
3651
|
+
printStatusEvent(state, type === "tool.failed" || data.ok === false ? "tool_failed" : "tool_done", toolStatusDetails(data));
|
|
3598
3652
|
} else if (type === "file.changed") {
|
|
3599
3653
|
printWorkspaceChange(data);
|
|
3600
3654
|
} else if (type === "tool.blocked") {
|
|
@@ -3619,7 +3673,10 @@ async function runPrompt(prompt, state, packageDir, { approvalDepth = 0 } = {})
|
|
|
3619
3673
|
} else if (type === "session.stopped") {
|
|
3620
3674
|
printStatusEvent(state, "stopped", data.reason || "");
|
|
3621
3675
|
} else if (type === "model.responded") {
|
|
3622
|
-
printStatusEvent(state, "model_responded", data.content ? data.content.slice(0, 80).replace(/\s+/g, " ") : ""
|
|
3676
|
+
printStatusEvent(state, "model_responded", data.content ? data.content.slice(0, 80).replace(/\s+/g, " ") : "", {
|
|
3677
|
+
step: data.step,
|
|
3678
|
+
maxSteps: state.currentMaxSteps,
|
|
3679
|
+
});
|
|
3623
3680
|
}
|
|
3624
3681
|
},
|
|
3625
3682
|
});
|
package/src/model-client.js
CHANGED
|
@@ -603,10 +603,10 @@ export async function createPlan(client, config, state) {
|
|
|
603
603
|
state.startUrl ? `Suggested start URL: ${state.startUrl}` : "",
|
|
604
604
|
config.allowedDomains.length > 0 ? `Allowed domains: ${config.allowedDomains.join(", ")}` : "",
|
|
605
605
|
config.allowShellTool
|
|
606
|
-
? `Shell tool is enabled in ${config.commandCwd}. Host platform: ${platformLabel(platform)}. In Docker, this path is mounted as /workspace with persistent /aginti-env and /aginti-cache mounts. Use relative paths or /workspace paths, not
|
|
606
|
+
? `Shell tool is enabled in ${config.commandCwd}. Host platform: ${platformLabel(platform)}. In Docker, this path is mounted as /workspace with persistent /aginti-env and /aginti-cache mounts, and common host data roots such as the user's home parent are mounted read-only at their original absolute paths for inspection. Use relative paths or /workspace for outputs and writes. Absolute host paths are acceptable for read-only inspection when visible, but do not write outside /workspace unless the user approves host mode. Permission mode: ${config.permissionMode || "normal"}. Sandbox mode: ${config.sandboxMode}. Package install policy: ${config.packageInstallPolicy}. For npm/pip/conda/venv setup, explain the need and wait for approval unless policy is allow. Do not run npx aginti, npm exec aginti, or nested aginti diagnostics from this shell; they may resolve stale project packages or create recursive agent sessions. On native Windows host mode, prefer PowerShell/cmd-compatible commands or WSL/Docker for bash-like toolchains.`
|
|
607
607
|
: "",
|
|
608
608
|
config.allowShellTool
|
|
609
|
-
? "Host tmux tools are enabled for long-running sessions. Plan to use tmux_start_session for durable jobs, tmux_capture_pane to monitor, tmux_send_keys to interact after capture, and tmux_list_sessions to discover existing sessions. Do not install or run tmux inside Docker run_command containers; those containers are short-lived and cannot preserve tmux servers. In Docker sandbox mode, tmux startup/send commands are still workspace-bound and shell-pane text follows the same Docker workspace command policy as run_command: use relative project paths
|
|
609
|
+
? "Host tmux tools are enabled for long-running sessions. Plan to use tmux_start_session for durable jobs, tmux_capture_pane to monitor, tmux_send_keys to interact after capture, and tmux_list_sessions to discover existing sessions. Do not install or run tmux inside Docker run_command containers; those containers are short-lived and cannot preserve tmux servers. In Docker sandbox mode, tmux startup/send commands are still workspace-write-bound and shell-pane text follows the same Docker workspace command policy as run_command: use relative project paths for writes and outputs. Read-only inspection of visible host absolute paths is allowed through run_command's read-only mounts; do not use tmux as a workaround for package installs, destructive git rewrites, or broad shell commands. In host mode, tmux startup/send command text follows the same host shell policy as run_command; if blocked, present the suggested approval/rerun path instead of trying tmux as a workaround. Ask for --sandbox-mode host --allow-destructive before trusted whole-host write/system work."
|
|
610
610
|
: "",
|
|
611
611
|
config.allowFileTools
|
|
612
612
|
? `Workspace file tools are enabled in ${config.commandCwd}: inspect_project, list_files, read_file, search_files, write_file, apply_patch, open_workspace_file, preview_workspace. For large or unfamiliar repos, plan to call inspect_project first, then search/read AGINTI.md/AGENTS.md/README/manifests and exact files. apply_patch supports exact single-file replacements and Codex-style/unified multi-file patches; prefer it for edits after reading relevant context. Keep all paths workspace-relative, for example plot_fx.svg or docs/report.tex, and avoid secrets. For newly generated standalone prose/docs/stories/assets, choose a descriptive non-conflicting filename from the topic/language instead of generic names like story.txt or output.txt; do not overwrite existing files unless the user explicitly asked to update/replace/overwrite that file. For generated local HTML/SVG/PDF/static sites, plan to use open_workspace_file or preview_workspace rather than starting a localhost server inside Docker.`
|