@lazyingart/agintiflow 0.6.0 → 0.8.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
@@ -43,9 +43,19 @@ aginti init
43
43
  aginti doctor
44
44
  aginti --list-routes
45
45
  aginti --list-profiles
46
- aginti --sandbox-status --sandbox-mode docker-readonly
46
+ aginti --sandbox-status
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`. The default is now Docker workspace mode with approved package installs, so coding, plotting, and LaTeX tasks can set up project-local tools without touching the host. Use `/help` for commands, `/latex on` for PDF work, `/docker off` only when you intentionally want host mode, `/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,8 +81,10 @@ 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"
75
- aginti --allow-shell --sandbox-mode docker-workspace --approve-package-installs "set up and test this project"
84
+ aginti queue <session-id> "extra instruction for the running agent"
85
+ aginti --profile code "write a small Python CLI app with tests"
86
+ aginti --latex "draw a figure, write a short LaTeX report, and compile the PDF"
87
+ aginti "set up this project and run the tests"
76
88
  ```
77
89
 
78
90
  Run from a source checkout:
@@ -184,9 +196,9 @@ AgInTiFlow is intentionally conservative:
184
196
  - File writes record before/after SHA-256 hashes and compact redacted diffs.
185
197
  - 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.
186
198
  - Docker read-only mode mounts the workspace read-only and disables container network access.
187
- - Docker workspace-write mode is the web UI default so plot, PDF, and test outputs can be written inside the mounted workspace.
188
- - Docker workspace-write mode is the preferred place for approved package or environment setup, including `apt-get`, `npm`, `pip`, `curl`, and `wget` workflows inside the container.
189
- - Package installs default to `prompt`, so npm/pip/conda/venv setup is blocked until explicitly approved.
199
+ - Docker workspace-write mode is the CLI/web default so plot, PDF, and test outputs can be written inside the mounted workspace.
200
+ - Docker workspace-write mode with package policy `allow` is the default for normal agent work. It permits practical setup commands such as `npm`, `pip`, `conda`, `curl`, `wget`, and `chmod` inside Docker while keeping host sudo/global installs blocked.
201
+ - Host mode falls back to `prompt`, so npm/pip/conda/venv setup and privileged/destructive commands require explicit trust.
190
202
  - 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.
191
203
  - NPM tokens and API keys are redacted from tool logs and API responses.
192
204
  - Every tool request and result is written to structured logs.
@@ -306,7 +318,7 @@ Build the companion agent toolchain sandbox:
306
318
  npm run smoke:toolchain-docker
307
319
  ```
308
320
 
309
- The setup script idempotently builds `agintiflow-sandbox:latest` from `docker/sandbox.Dockerfile` and verifies Node, npm, Python, NumPy, Matplotlib, `latexmk`, and `pdflatex` inside Docker. The smoke script runs Python plot generation, LaTeX PDF compilation, and PDF artifact preview against the same workspace-mounted sandbox path used by the agent.
321
+ The setup script idempotently builds `agintiflow-sandbox:latest` from `docker/sandbox.Dockerfile` and verifies Node, npm, Python, NumPy, Matplotlib, `latexmk`, and `pdflatex` inside Docker. It also creates the persistent companion folders under `~/.agintiflow/docker/`: `home/` maps to `/aginti-home`, `cache/` maps to `/aginti-cache`, and `env/` maps to `/aginti-env`. Python/conda-style toolchains should live under `/aginti-env` so they survive across agent runs. OS package changes from `apt-get` are container-ephemeral unless you rebuild the image.
310
322
 
311
323
  ## Sandbox Modes
312
324
 
@@ -324,7 +336,9 @@ Package policy values:
324
336
  | `prompt` | Return a clear approval-required error; the UI can switch to approved. |
325
337
  | `allow` | Permit package/setup commands. Docker workspace mode also allows broader shell/network commands while keeping secrets and npm publishing blocked. |
326
338
 
327
- Toolchain commands such as `python3 plot.py`, `latexmk -pdf paper.tex`, and `pdflatex -interaction=nonstopmode -halt-on-error paper.tex` are allowlisted only when the shell tool is enabled. In Docker mode they require `docker-workspace` because they write outputs back to `/workspace`. File and canvas tools accept both normal relative paths and Docker virtual paths like `/workspace/report.pdf`, while other absolute host paths remain blocked.
339
+ 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.
340
+
341
+ The web chat mirrors the CLI session store. Enter sends, Ctrl+J inserts a newline, and Tab submits/queues the message. Queued input is written to `.sessions/<session-id>/inbox.jsonl`; the running agent drains that pipe between steps and after tool calls.
328
342
 
329
343
  Safe preflight endpoints:
330
344
 
@@ -0,0 +1,14 @@
1
+ # Agent Runtime Pipe
2
+
3
+ AgInTiFlow keeps CLI and web runs equivalent by using the project folder as the source of truth.
4
+
5
+ - Project root: the folder where `aginti` or `aginti web` is launched.
6
+ - Session store: `<project>/.sessions/`.
7
+ - Web settings database: `<project>/.sessions/web-state.sqlite`.
8
+ - Runtime inbox: `<project>/.sessions/<session-id>/inbox.jsonl`.
9
+
10
+ When a run is active, the web chat and `aginti queue <session-id> "..."` append messages to the inbox instead of trying to mutate the running process directly. The runner drains the inbox at safe boundaries: before each model step and after tool execution. This mirrors the event-queue style used by mature agent UIs while keeping the backend decoupled from any specific frontend.
11
+
12
+ Default execution is Docker workspace mode with package installs approved inside the sandbox. The project is mounted at `/workspace`; persistent agent toolchain folders are mounted at `/aginti-home`, `/aginti-cache`, and `/aginti-env` from `~/.agintiflow/docker/`. Python, conda, and other language-level environments should be installed under `/aginti-env` so they survive across runs. Apt/apk package changes are ephemeral unless the Docker image is rebuilt.
13
+
14
+ The failed `f(f(x)) = f'(x)` LaTeX task exposed three issues: host-mode command policy blocked setup/path commands, a 15-step budget was too small for iterative numerical work plus TeX output, and follow-up input could not be queued while the agent was running. The current runtime defaults and inbox pipe address those without hardcoding that specific math task.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lazyingart/agintiflow",
3
- "version": "0.6.0",
3
+ "version": "0.8.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,8 +41,10 @@
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",
47
+ "scripts/smoke-inbox.js",
46
48
  "scripts/smoke-toolchain-docker.js",
47
49
  "scripts/smoke-web-api.js",
48
50
  "src/",
@@ -59,10 +61,12 @@
59
61
  "check": "node --check run.js && node --check web.js && node --check bin/aginti-cli.js && node --check src/*.js",
60
62
  "setup:toolchain-docker": "scripts/setup-agent-toolchain-docker.sh",
61
63
  "smoke:coding-tools": "node scripts/smoke-coding-tools.js",
64
+ "smoke:cli-chat": "node scripts/smoke-cli-chat.js",
62
65
  "smoke:toolchain-docker": "node scripts/smoke-toolchain-docker.js",
66
+ "smoke:inbox": "node scripts/smoke-inbox.js",
63
67
  "smoke:web-api": "node scripts/smoke-web-api.js",
64
68
  "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",
69
+ "test": "npm run check && npm run smoke:web-api && npm run smoke:coding-tools && npm run smoke:capabilities && npm run smoke:cli-chat && npm run smoke:inbox",
66
70
  "pack:dry-run": "npm pack --dry-run",
67
71
  "smoke:capabilities": "node scripts/smoke-capabilities.js"
68
72
  },
package/public/app.js CHANGED
@@ -114,7 +114,9 @@ const translations = {
114
114
  messageRequired: "Message is required.",
115
115
  sendingStatus: "Sending...",
116
116
  runningStatus: "Running...",
117
+ queuedStatus: "Queued for the running agent.",
117
118
  failedContinue: "Failed to continue the conversation.",
119
+ chatShortcutHelp: "Enter sends · Ctrl+J inserts a newline · Tab queues while the agent is running",
118
120
  conversationMenuLabel: "Manage conversations",
119
121
  manageConversationsTitle: "Manage conversations",
120
122
  manageConversationsHelp: "Rename, auto-rename, or delete saved chat history.",
@@ -862,8 +864,9 @@ function renderSandboxStatus(status = lastSandbox) {
862
864
  `package=${status.packageInstallPolicy}`,
863
865
  `docker=${status.dockerAvailable ? t("availableLabel") : t("missingLabel")}`,
864
866
  `imageReady=${status.imageReady}`,
867
+ status.persistentDocker?.env ? `env=${status.persistentDocker.env}` : "",
865
868
  ];
866
- sandboxStatusEl.textContent = parts.join(" · ");
869
+ sandboxStatusEl.textContent = parts.filter(Boolean).join(" · ");
867
870
  renderSandboxLogs(status.logs || []);
868
871
  }
869
872
 
@@ -958,7 +961,7 @@ function formPayload() {
958
961
  startUrl: document.querySelector("#startUrl").value.trim(),
959
962
  allowedDomains: document.querySelector("#allowedDomains").value.trim(),
960
963
  commandCwd: document.querySelector("#commandCwd").value.trim(),
961
- maxSteps: Number(document.querySelector("#maxSteps").value) || 15,
964
+ maxSteps: Number(document.querySelector("#maxSteps").value) || 24,
962
965
  sandboxMode: sandboxModeField.value,
963
966
  packageInstallPolicy: packageInstallPolicyField.value,
964
967
  headless: document.querySelector("#headless").checked,
@@ -1746,6 +1749,28 @@ saveApiKeyButton?.addEventListener("click", async () => {
1746
1749
  form.addEventListener("input", schedulePreferenceSave);
1747
1750
  form.addEventListener("change", schedulePreferenceSave);
1748
1751
 
1752
+ chatInputEl.addEventListener("keydown", (event) => {
1753
+ if (event.key === "j" && event.ctrlKey && !event.metaKey && !event.altKey) {
1754
+ event.preventDefault();
1755
+ const start = chatInputEl.selectionStart ?? chatInputEl.value.length;
1756
+ const end = chatInputEl.selectionEnd ?? chatInputEl.value.length;
1757
+ chatInputEl.value = `${chatInputEl.value.slice(0, start)}\n${chatInputEl.value.slice(end)}`;
1758
+ chatInputEl.selectionStart = chatInputEl.selectionEnd = start + 1;
1759
+ return;
1760
+ }
1761
+
1762
+ if (event.key === "Tab") {
1763
+ event.preventDefault();
1764
+ chatFormEl.requestSubmit();
1765
+ return;
1766
+ }
1767
+
1768
+ if (event.key === "Enter" && !event.shiftKey && !event.ctrlKey && !event.metaKey && !event.altKey) {
1769
+ event.preventDefault();
1770
+ chatFormEl.requestSubmit();
1771
+ }
1772
+ });
1773
+
1749
1774
  form.addEventListener("submit", async (event) => {
1750
1775
  event.preventDefault();
1751
1776
 
@@ -1838,9 +1863,19 @@ chatFormEl.addEventListener("submit", async (event) => {
1838
1863
 
1839
1864
  currentSessionId = data.sessionId;
1840
1865
  chatInputEl.value = "";
1841
- chatStatusEl.textContent = t("runningStatus");
1866
+ chatStatusEl.textContent = data.queued ? t("queuedStatus") : t("runningStatus");
1867
+ if (data.queued) {
1868
+ renderChat([
1869
+ ...lastChatEntries,
1870
+ {
1871
+ role: "user",
1872
+ content,
1873
+ at: new Date().toISOString(),
1874
+ },
1875
+ ]);
1876
+ }
1842
1877
  await refreshSessions();
1843
- await refreshChat();
1878
+ if (!data.queued) await refreshChat();
1844
1879
  await refreshWorkspaceChanges();
1845
1880
  await refreshArtifacts({ loadSelected: artifactTunnelDialog?.open });
1846
1881
 
@@ -1872,7 +1907,7 @@ async function loadConfig() {
1872
1907
  document.querySelector("#headless").checked = prefs.headless ?? data.defaults.headless;
1873
1908
  document.querySelector("#maxSteps").value = prefs.maxSteps ?? data.defaults.maxSteps;
1874
1909
  sandboxModeField.value = prefs.sandboxMode || (prefs.useDockerSandbox ? "docker-workspace" : "host");
1875
- packageInstallPolicyField.value = prefs.packageInstallPolicy || "prompt";
1910
+ packageInstallPolicyField.value = prefs.packageInstallPolicy || "allow";
1876
1911
  document.querySelector("#allowShellTool").checked = prefs.allowShellTool ?? true;
1877
1912
  document.querySelector("#allowFileTools").checked = prefs.allowFileTools ?? true;
1878
1913
  allowWrapperToolsField.checked = prefs.allowWrapperTools ?? false;
package/public/index.html CHANGED
@@ -135,7 +135,7 @@
135
135
  </label>
136
136
  <label>
137
137
  <span data-i18n="maxStepsLabel">Max steps</span>
138
- <input id="maxSteps" name="maxSteps" type="number" min="1" max="50" value="15" />
138
+ <input id="maxSteps" name="maxSteps" type="number" min="1" max="50" value="24" />
139
139
  </label>
140
140
  </div>
141
141
  <p id="package-warning" class="subtle package-warning"></p>
@@ -291,6 +291,9 @@
291
291
  data-i18n-placeholder="chatPlaceholder"
292
292
  placeholder="Continue the selected conversation here."
293
293
  ></textarea>
294
+ <p class="chat-shortcuts subtle" data-i18n="chatShortcutHelp">
295
+ Enter sends · Ctrl+J inserts a newline · Tab queues while the agent is running
296
+ </p>
294
297
  <div class="actions">
295
298
  <button type="submit" data-i18n="sendMessageButton">Send message</button>
296
299
  <span id="chat-status" class="subtle"></span>
package/public/styles.css CHANGED
@@ -524,6 +524,10 @@ button.danger {
524
524
  .chat-panel {
525
525
  display: grid;
526
526
  gap: 12px;
527
+ border-color: rgba(15, 118, 110, 0.2);
528
+ background:
529
+ linear-gradient(135deg, rgba(255, 253, 250, 0.95), rgba(236, 253, 245, 0.66)),
530
+ var(--panel);
527
531
  }
528
532
 
529
533
  .chat-thread {
@@ -561,7 +565,25 @@ button.danger {
561
565
  }
562
566
 
563
567
  .chat-form {
568
+ display: grid;
569
+ gap: 8px;
564
570
  margin: 0;
571
+ padding: 12px;
572
+ border: 1px solid rgba(15, 118, 110, 0.18);
573
+ border-radius: 16px;
574
+ background: rgba(255, 253, 250, 0.82);
575
+ }
576
+
577
+ #chat-input {
578
+ min-height: 92px;
579
+ border-color: rgba(15, 118, 110, 0.24);
580
+ background: white;
581
+ }
582
+
583
+ .chat-shortcuts {
584
+ margin: -2px 0 0;
585
+ font-size: 0.78rem;
586
+ line-height: 1.35;
565
587
  }
566
588
 
567
589
  .modal {
@@ -6,6 +6,7 @@ REPO_ROOT="$(cd -- "${SCRIPT_DIR}/.." && pwd)"
6
6
  IMAGE="${AGINTIFLOW_TOOLCHAIN_IMAGE:-agintiflow-sandbox:latest}"
7
7
  WORKSPACE="${AGINTIFLOW_TOOLCHAIN_WORKSPACE:-${REPO_ROOT}}"
8
8
  DOCKERFILE="${AGINTIFLOW_TOOLCHAIN_DOCKERFILE:-${REPO_ROOT}/docker/sandbox.Dockerfile}"
9
+ STATE_DIR="${AGINTIFLOW_DOCKER_STATE_DIR:-${HOME:-/tmp}/.agintiflow/docker}"
9
10
 
10
11
  if ! command -v docker >/dev/null 2>&1; then
11
12
  echo "Docker is not installed or not on PATH. Run scripts/install-docker-ubuntu.sh first." >&2
@@ -13,11 +14,13 @@ if ! command -v docker >/dev/null 2>&1; then
13
14
  fi
14
15
 
15
16
  mkdir -p "${WORKSPACE}"
17
+ mkdir -p "${STATE_DIR}/home" "${STATE_DIR}/cache" "${STATE_DIR}/env"
16
18
 
17
19
  echo "Building AgInTiFlow toolchain sandbox image:"
18
20
  echo " image=${IMAGE}"
19
21
  echo " dockerfile=${DOCKERFILE}"
20
22
  echo " context=${REPO_ROOT}"
23
+ echo " persistent=${STATE_DIR}"
21
24
  docker build -t "${IMAGE}" -f "${DOCKERFILE}" "${REPO_ROOT}"
22
25
 
23
26
  echo
@@ -30,11 +33,20 @@ docker run --rm \
30
33
  --memory 2g \
31
34
  --cpus 2 \
32
35
  --tmpfs /tmp:rw,nosuid,nodev,size=512m \
33
- -e HOME=/tmp \
36
+ -e HOME=/aginti-home \
37
+ -e XDG_CACHE_HOME=/aginti-cache \
38
+ -e PIP_CACHE_DIR=/aginti-cache/pip \
39
+ -e NPM_CONFIG_CACHE=/aginti-cache/npm \
40
+ -e CONDA_PKGS_DIRS=/aginti-cache/conda-pkgs \
34
41
  -v "${WORKSPACE}:/workspace:rw" \
42
+ -v "${STATE_DIR}/home:/aginti-home:rw" \
43
+ -v "${STATE_DIR}/cache:/aginti-cache:rw" \
44
+ -v "${STATE_DIR}/env:/aginti-env:rw" \
35
45
  -w /workspace \
36
46
  "${IMAGE}" \
37
47
  bash -lc 'set -Eeuo pipefail
48
+ if [ ! -x /aginti-env/python/bin/python ]; then python3 -m venv --system-site-packages /aginti-env/python; fi
49
+ . /aginti-env/python/bin/activate
38
50
  node -v
39
51
  npm -v
40
52
  python3 --version
@@ -62,6 +62,10 @@ try {
62
62
  capabilities.trustedDockerPolicy.some((check) => check.command.startsWith("wget") && check.allowed),
63
63
  "trusted Docker policy did not allow wget"
64
64
  );
65
+ assert(
66
+ capabilities.trustedDockerPolicy.some((check) => check.command.startsWith("chmod") && check.allowed),
67
+ "trusted Docker policy did not allow chmod"
68
+ );
65
69
 
66
70
  const doctor = JSON.parse(await runCli(["doctor", "--capabilities", "--json"]));
67
71
  assert(doctor.project.root === tempRoot, "doctor --capabilities used the wrong project root");
@@ -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
+ }
@@ -0,0 +1,36 @@
1
+ #!/usr/bin/env node
2
+ import fs from "node:fs/promises";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import { SessionStore } from "../src/session-store.js";
6
+
7
+ const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "agintiflow-inbox-"));
8
+
9
+ function assert(condition, message) {
10
+ if (!condition) throw new Error(message);
11
+ }
12
+
13
+ try {
14
+ const store = new SessionStore(path.join(tempRoot, ".sessions"), "inbox-smoke");
15
+ await store.appendInbox("first queued message", { source: "test" });
16
+ await store.appendInbox("second queued message", { source: "test" });
17
+
18
+ const drained = await store.drainInbox();
19
+ assert(drained.length === 2, "inbox did not drain both messages");
20
+ assert(drained[0].content === "first queued message", "first queued message mismatch");
21
+ assert(drained[1].content === "second queued message", "second queued message mismatch");
22
+ assert((await store.drainInbox()).length === 0, "inbox was not cleared after drain");
23
+
24
+ console.log(
25
+ JSON.stringify(
26
+ {
27
+ ok: true,
28
+ checks: ["session-inbox-append", "session-inbox-drain"],
29
+ },
30
+ null,
31
+ 2
32
+ )
33
+ );
34
+ } finally {
35
+ await fs.rm(tempRoot, { recursive: true, force: true });
36
+ }
@@ -76,6 +76,9 @@ try {
76
76
  if (config.preferences?.preferredWrapper !== "codex") throw new Error("Codex is not the default preferred wrapper");
77
77
  if (config.project?.root !== runtimeDir) throw new Error("web project root did not default to launch directory");
78
78
  if (config.preferences?.commandCwd !== runtimeDir) throw new Error("commandCwd did not default to project root");
79
+ if (config.preferences?.sandboxMode !== "docker-workspace") throw new Error("web did not default to docker workspace");
80
+ if (config.preferences?.packageInstallPolicy !== "allow") throw new Error("web did not default to Docker package installs");
81
+ if (Number(config.preferences?.maxSteps) < 24) throw new Error("web default max steps is too low");
79
82
  if (!Array.isArray(config.taskProfiles) || !config.taskProfiles.some((profile) => profile.id === "latex")) {
80
83
  throw new Error("task profiles are not advertised by /api/config");
81
84
  }
@@ -144,7 +144,7 @@ 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}. Docker workspace mode with approved package installs supports broader setup and network commands.`
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. The project is mounted at /workspace and the persistent agent toolchain is mounted at /aginti-env with caches under /aginti-cache.`
148
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
@@ -159,6 +159,8 @@ function createInitialState(config, sessionId) {
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
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
+ "Docker language/toolchain installs should prefer /aginti-env or project files so they persist across runs; apt/apk changes are ephemeral unless the image is rebuilt.",
163
+ "If the run is close to the max-step limit, finish with the best complete artifact and honest limitations instead of starting a new approach.",
162
164
  "When the requested outcome is complete and a useful check has passed or been honestly skipped, stop and call finish.",
163
165
  "When done, call finish with a concise result.",
164
166
  ].join(" "),
@@ -171,7 +173,7 @@ function createInitialState(config, sessionId) {
171
173
  config.allowedDomains.length > 0 ? `Allowed domains: ${config.allowedDomains.join(", ")}` : "",
172
174
  config.allowShellTool
173
175
  ? config.useDockerSandbox
174
- ? `Shell working directory mounted into Docker as /workspace from ${config.commandCwd}. Use relative paths or /workspace paths, not absolute host temp paths. Sandbox mode: ${config.sandboxMode}. Package install policy: ${config.packageInstallPolicy}.`
176
+ ? `Shell working directory mounted into Docker as /workspace from ${config.commandCwd}. Use relative paths or /workspace paths, not absolute host temp paths. Persistent Docker env: /aginti-env, caches: /aginti-cache. Sandbox mode: ${config.sandboxMode}. Package install policy: ${config.packageInstallPolicy}.`
175
177
  : `Shell working directory: ${config.commandCwd}`
176
178
  : "",
177
179
  config.allowFileTools ? `Workspace file tools enabled in: ${config.commandCwd}. Use workspace-relative paths.` : "",
@@ -273,7 +275,7 @@ function applyContinuationPrompt(state, config, observers) {
273
275
  config.allowedDomains.length > 0 ? `Allowed domains: ${config.allowedDomains.join(", ")}` : "",
274
276
  config.allowShellTool
275
277
  ? config.useDockerSandbox
276
- ? `Shell working directory mounted into Docker as /workspace from ${config.commandCwd}. Use relative paths or /workspace paths. Sandbox mode: ${config.sandboxMode}. Package install policy: ${config.packageInstallPolicy}.`
278
+ ? `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}.`
277
279
  : `Shell working directory: ${config.commandCwd}`
278
280
  : "",
279
281
  config.allowFileTools ? `Workspace file tools enabled in: ${config.commandCwd}. Use workspace-relative paths.` : "",
@@ -409,7 +411,7 @@ async function captureSyntheticSnapshot(store, step, config) {
409
411
  config.startUrl ? `Suggested start URL: ${config.startUrl}` : "",
410
412
  config.allowShellTool
411
413
  ? config.useDockerSandbox
412
- ? `Shell tool available in Docker with mounted workspace /workspace from ${config.commandCwd}. Use relative paths or /workspace paths. Sandbox mode: ${config.sandboxMode}. Package install policy: ${config.packageInstallPolicy}.`
414
+ ? `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}.`
413
415
  : `Shell tool available in: ${config.commandCwd}`
414
416
  : "Shell tool disabled.",
415
417
  config.allowFileTools
@@ -443,6 +445,29 @@ async function buildSnapshot(browserState, store, step, config) {
443
445
  return captureSyntheticSnapshot(store, step, config);
444
446
  }
445
447
 
448
+ async function injectQueuedUserMessages(store, state, observers) {
449
+ const inbox = await store.drainInbox();
450
+ if (inbox.length === 0) return;
451
+
452
+ for (const item of inbox) {
453
+ const content = String(item.content || "").trim();
454
+ if (!content) continue;
455
+ appendChatEntry(state, "user", content);
456
+ state.messages.push({
457
+ role: "user",
458
+ content: `Additional user message received while this run was active:\n${content}`,
459
+ });
460
+ await store.appendEvent("conversation.queued_input_applied", {
461
+ prompt: content,
462
+ source: item.source || "inbox",
463
+ });
464
+ observers.event("conversation.queued_input_applied", {
465
+ prompt: content,
466
+ source: item.source || "inbox",
467
+ });
468
+ }
469
+ }
470
+
446
471
  async function executeTool(browserState, toolCall, snapshot, config, store, observers, state) {
447
472
  const args = JSON.parse(toolCall.function.arguments || "{}");
448
473
  const safeArgs = sanitizeToolArgs(toolCall.function.name, args);
@@ -791,6 +816,17 @@ export async function runAgent(config) {
791
816
  console.log(`Provider: ${config.provider}`);
792
817
  console.log(`Model: ${config.model}`);
793
818
  console.log(`Routing: ${config.routingMode} (${config.routeReason})`);
819
+ console.log(`Workspace: ${config.commandCwd}`);
820
+ console.log(`Sessions: ${config.sessionsDir}`);
821
+ if (config.useDockerSandbox) {
822
+ console.log(
823
+ `Docker: image=${config.dockerSandboxImage} mode=${config.sandboxMode} packagePolicy=${config.packageInstallPolicy}`
824
+ );
825
+ console.log(`Docker workspace: /workspace -> ${config.commandCwd}`);
826
+ console.log("Docker env: /aginti-env persistent toolchain; /aginti-cache persistent caches");
827
+ } else if (config.allowShellTool) {
828
+ console.log(`Shell: host policy=${config.packageInstallPolicy}`);
829
+ }
794
830
  if (state.plan) {
795
831
  console.log("\nPlan:");
796
832
  console.log(state.plan);
@@ -798,6 +834,7 @@ export async function runAgent(config) {
798
834
  }
799
835
 
800
836
  for (let step = state.stepsCompleted + 1; step <= config.maxSteps; step += 1) {
837
+ await injectQueuedUserMessages(store, state, observers);
801
838
  const snapshot = await buildSnapshot(browserState, store, step, config);
802
839
  state.meta.lastUrl = snapshot.url || state.meta.lastUrl;
803
840
  await saveBrowserState(browserState, store).catch(() => {});
@@ -818,7 +855,7 @@ export async function runAgent(config) {
818
855
 
819
856
  state.messages.push({
820
857
  role: "user",
821
- content: `Step ${step}/${config.maxSteps}. Latest runtime snapshot:\n${JSON.stringify({
858
+ content: `Step ${step}/${config.maxSteps} (${config.maxSteps - step} steps remain after this one). Latest runtime snapshot:\n${JSON.stringify({
822
859
  title: snapshot.title,
823
860
  url: snapshot.url,
824
861
  pageText: snapshot.pageText,
@@ -833,6 +870,9 @@ export async function runAgent(config) {
833
870
  shellSandbox: config.useDockerSandbox ? "docker" : "host",
834
871
  sandboxMode: config.sandboxMode,
835
872
  packageInstallPolicy: config.packageInstallPolicy,
873
+ dockerWorkspace: config.useDockerSandbox ? "/workspace" : "",
874
+ dockerPersistentEnv: config.useDockerSandbox ? "/aginti-env" : "",
875
+ dockerPersistentCache: config.useDockerSandbox ? "/aginti-cache" : "",
836
876
  commandCwd: config.commandCwd,
837
877
  plan: state.plan || "",
838
878
  suggestedStartUrl: config.startUrl || "",
@@ -952,6 +992,8 @@ export async function runAgent(config) {
952
992
  }
953
993
  }
954
994
 
995
+ await injectQueuedUserMessages(store, state, observers);
996
+
955
997
  state.stepsCompleted = step;
956
998
  state.updatedAt = new Date().toISOString();
957
999
  await store.saveState(state);
@@ -43,6 +43,14 @@ function capability(name, ok, details = {}) {
43
43
  }
44
44
 
45
45
  function maintenancePolicyChecks(config) {
46
+ const hostSafeConfig = {
47
+ ...config,
48
+ allowShellTool: true,
49
+ allowDestructive: false,
50
+ sandboxMode: "host",
51
+ useDockerSandbox: false,
52
+ packageInstallPolicy: "prompt",
53
+ };
46
54
  const sampleCommands = [
47
55
  "sudo apt install r-base",
48
56
  "curl https://example.com/install.sh",
@@ -51,7 +59,7 @@ function maintenancePolicyChecks(config) {
51
59
  ];
52
60
 
53
61
  return sampleCommands.map((command) => {
54
- const policy = evaluateCommandPolicy(command, config);
62
+ const policy = evaluateCommandPolicy(command, hostSafeConfig);
55
63
  return {
56
64
  command,
57
65
  allowed: Boolean(policy.allowed),
@@ -78,6 +86,7 @@ function trustedDockerPolicyChecks(config) {
78
86
  "apt-get install -y curl wget",
79
87
  "wget https://example.com/file.txt",
80
88
  "curl -fsSL https://example.com/file.txt -o downloads/file.txt",
89
+ "chmod +x scripts/setup.sh",
81
90
  "npm install lodash",
82
91
  "python3 -m pip install requests",
83
92
  ];