@lazyingart/agintiflow 0.7.0 → 0.8.1
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 +11 -7
- package/docker/sandbox.Dockerfile +1 -0
- package/docs/agent-runtime-pipe.md +14 -0
- package/package.json +4 -2
- package/public/app.js +40 -5
- package/public/index.html +4 -1
- package/public/styles.css +22 -0
- package/scripts/setup-agent-toolchain-docker.sh +13 -1
- package/scripts/smoke-capabilities.js +4 -0
- package/scripts/smoke-inbox.js +36 -0
- package/scripts/smoke-web-api.js +3 -0
- package/src/agent-runner.js +37 -5
- package/src/capabilities.js +10 -1
- package/src/cli.js +52 -14
- package/src/command-policy.js +33 -9
- package/src/config.js +6 -6
- package/src/docker-sandbox.js +80 -6
- package/src/interactive-cli.js +19 -3
- package/src/model-client.js +3 -1
- package/src/session-store.js +29 -0
- package/src/task-profiles.js +1 -1
- package/src/web-db.js +8 -5
- package/web.js +25 -3
package/README.md
CHANGED
|
@@ -43,7 +43,7 @@ aginti init
|
|
|
43
43
|
aginti doctor
|
|
44
44
|
aginti --list-routes
|
|
45
45
|
aginti --list-profiles
|
|
46
|
-
aginti --sandbox-status
|
|
46
|
+
aginti --sandbox-status
|
|
47
47
|
```
|
|
48
48
|
|
|
49
49
|
Start an interactive Codex-style CLI chat from any project folder:
|
|
@@ -54,7 +54,7 @@ aginti
|
|
|
54
54
|
aginti chat
|
|
55
55
|
```
|
|
56
56
|
|
|
57
|
-
Inside chat, type normal requests such as `write a small Python CLI app with tests`. Use `/help` for commands, `/
|
|
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
58
|
|
|
59
59
|
Launch the local web UI from an installed package:
|
|
60
60
|
|
|
@@ -81,8 +81,10 @@ aginti doctor --capabilities
|
|
|
81
81
|
aginti sessions list
|
|
82
82
|
aginti sessions show <session-id>
|
|
83
83
|
aginti resume <session-id> "continue with a short follow-up"
|
|
84
|
+
aginti queue <session-id> "extra instruction for the running agent"
|
|
84
85
|
aginti --profile code "write a small Python CLI app with tests"
|
|
85
|
-
aginti --
|
|
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"
|
|
86
88
|
```
|
|
87
89
|
|
|
88
90
|
Run from a source checkout:
|
|
@@ -194,9 +196,9 @@ AgInTiFlow is intentionally conservative:
|
|
|
194
196
|
- File writes record before/after SHA-256 hashes and compact redacted diffs.
|
|
195
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.
|
|
196
198
|
- Docker read-only mode mounts the workspace read-only and disables container network access.
|
|
197
|
-
- Docker workspace-write mode is the web
|
|
198
|
-
- Docker workspace-write mode is the
|
|
199
|
-
-
|
|
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.
|
|
200
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.
|
|
201
203
|
- NPM tokens and API keys are redacted from tool logs and API responses.
|
|
202
204
|
- Every tool request and result is written to structured logs.
|
|
@@ -316,7 +318,7 @@ Build the companion agent toolchain sandbox:
|
|
|
316
318
|
npm run smoke:toolchain-docker
|
|
317
319
|
```
|
|
318
320
|
|
|
319
|
-
The setup script idempotently builds `agintiflow-sandbox:latest` from `docker/sandbox.Dockerfile` and verifies Node, npm, Python, NumPy, Matplotlib, `latexmk`, and `pdflatex` inside Docker.
|
|
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.
|
|
320
322
|
|
|
321
323
|
## Sandbox Modes
|
|
322
324
|
|
|
@@ -336,6 +338,8 @@ Package policy values:
|
|
|
336
338
|
|
|
337
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.
|
|
338
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.
|
|
342
|
+
|
|
339
343
|
Safe preflight endpoints:
|
|
340
344
|
|
|
341
345
|
```bash
|
|
@@ -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.
|
|
3
|
+
"version": "0.8.1",
|
|
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",
|
|
@@ -44,6 +44,7 @@
|
|
|
44
44
|
"scripts/smoke-cli-chat.js",
|
|
45
45
|
"scripts/smoke-coding-tools.js",
|
|
46
46
|
"scripts/smoke-capabilities.js",
|
|
47
|
+
"scripts/smoke-inbox.js",
|
|
47
48
|
"scripts/smoke-toolchain-docker.js",
|
|
48
49
|
"scripts/smoke-web-api.js",
|
|
49
50
|
"src/",
|
|
@@ -62,9 +63,10 @@
|
|
|
62
63
|
"smoke:coding-tools": "node scripts/smoke-coding-tools.js",
|
|
63
64
|
"smoke:cli-chat": "node scripts/smoke-cli-chat.js",
|
|
64
65
|
"smoke:toolchain-docker": "node scripts/smoke-toolchain-docker.js",
|
|
66
|
+
"smoke:inbox": "node scripts/smoke-inbox.js",
|
|
65
67
|
"smoke:web-api": "node scripts/smoke-web-api.js",
|
|
66
68
|
"real:deepseek": "node scripts/real-deepseek-capabilities.js",
|
|
67
|
-
"test": "npm run check && npm run smoke:web-api && npm run smoke:coding-tools && npm run smoke:capabilities && npm run smoke:cli-chat",
|
|
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",
|
|
68
70
|
"pack:dry-run": "npm pack --dry-run",
|
|
69
71
|
"smoke:capabilities": "node scripts/smoke-capabilities.js"
|
|
70
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) ||
|
|
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 || "
|
|
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="
|
|
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=/
|
|
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,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
|
+
}
|
package/scripts/smoke-web-api.js
CHANGED
|
@@ -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
|
}
|
package/src/agent-runner.js
CHANGED
|
@@ -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);
|
|
@@ -798,6 +823,7 @@ export async function runAgent(config) {
|
|
|
798
823
|
`Docker: image=${config.dockerSandboxImage} mode=${config.sandboxMode} packagePolicy=${config.packageInstallPolicy}`
|
|
799
824
|
);
|
|
800
825
|
console.log(`Docker workspace: /workspace -> ${config.commandCwd}`);
|
|
826
|
+
console.log("Docker env: /aginti-env persistent toolchain; /aginti-cache persistent caches");
|
|
801
827
|
} else if (config.allowShellTool) {
|
|
802
828
|
console.log(`Shell: host policy=${config.packageInstallPolicy}`);
|
|
803
829
|
}
|
|
@@ -808,6 +834,7 @@ export async function runAgent(config) {
|
|
|
808
834
|
}
|
|
809
835
|
|
|
810
836
|
for (let step = state.stepsCompleted + 1; step <= config.maxSteps; step += 1) {
|
|
837
|
+
await injectQueuedUserMessages(store, state, observers);
|
|
811
838
|
const snapshot = await buildSnapshot(browserState, store, step, config);
|
|
812
839
|
state.meta.lastUrl = snapshot.url || state.meta.lastUrl;
|
|
813
840
|
await saveBrowserState(browserState, store).catch(() => {});
|
|
@@ -828,7 +855,7 @@ export async function runAgent(config) {
|
|
|
828
855
|
|
|
829
856
|
state.messages.push({
|
|
830
857
|
role: "user",
|
|
831
|
-
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({
|
|
832
859
|
title: snapshot.title,
|
|
833
860
|
url: snapshot.url,
|
|
834
861
|
pageText: snapshot.pageText,
|
|
@@ -843,6 +870,9 @@ export async function runAgent(config) {
|
|
|
843
870
|
shellSandbox: config.useDockerSandbox ? "docker" : "host",
|
|
844
871
|
sandboxMode: config.sandboxMode,
|
|
845
872
|
packageInstallPolicy: config.packageInstallPolicy,
|
|
873
|
+
dockerWorkspace: config.useDockerSandbox ? "/workspace" : "",
|
|
874
|
+
dockerPersistentEnv: config.useDockerSandbox ? "/aginti-env" : "",
|
|
875
|
+
dockerPersistentCache: config.useDockerSandbox ? "/aginti-cache" : "",
|
|
846
876
|
commandCwd: config.commandCwd,
|
|
847
877
|
plan: state.plan || "",
|
|
848
878
|
suggestedStartUrl: config.startUrl || "",
|
|
@@ -962,6 +992,8 @@ export async function runAgent(config) {
|
|
|
962
992
|
}
|
|
963
993
|
}
|
|
964
994
|
|
|
995
|
+
await injectQueuedUserMessages(store, state, observers);
|
|
996
|
+
|
|
965
997
|
state.stepsCompleted = step;
|
|
966
998
|
state.updatedAt = new Date().toISOString();
|
|
967
999
|
await store.saveState(state);
|
package/src/capabilities.js
CHANGED
|
@@ -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,
|
|
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
|
];
|
package/src/cli.js
CHANGED
|
@@ -5,6 +5,7 @@ import { getModelPresets } from "./model-routing.js";
|
|
|
5
5
|
import { getDockerSandboxStatus, runDockerPreflight } from "./docker-sandbox.js";
|
|
6
6
|
import { buildCapabilityReport, printCapabilityReport } from "./capabilities.js";
|
|
7
7
|
import { startInteractiveCli } from "./interactive-cli.js";
|
|
8
|
+
import { SessionStore } from "./session-store.js";
|
|
8
9
|
import {
|
|
9
10
|
doctorReport,
|
|
10
11
|
initProject,
|
|
@@ -57,6 +58,7 @@ export function parseArgs(argv) {
|
|
|
57
58
|
port: "",
|
|
58
59
|
host: "",
|
|
59
60
|
listProfiles: false,
|
|
61
|
+
latex: false,
|
|
60
62
|
};
|
|
61
63
|
|
|
62
64
|
const parts = [];
|
|
@@ -130,6 +132,14 @@ export function parseArgs(argv) {
|
|
|
130
132
|
result.sandboxMode = result.sandboxMode || "docker-workspace";
|
|
131
133
|
continue;
|
|
132
134
|
}
|
|
135
|
+
if (arg === "--latex") {
|
|
136
|
+
result.latex = true;
|
|
137
|
+
result.taskProfile = "latex";
|
|
138
|
+
result.sandboxMode = result.sandboxMode || "docker-workspace";
|
|
139
|
+
result.packageInstallPolicy = result.packageInstallPolicy || "allow";
|
|
140
|
+
result.maxSteps = result.maxSteps || 30;
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
133
143
|
if (arg === "--max-steps") {
|
|
134
144
|
result.maxSteps = Number(readOption(argv, i));
|
|
135
145
|
i += 1;
|
|
@@ -209,10 +219,29 @@ export function parseArgs(argv) {
|
|
|
209
219
|
|
|
210
220
|
function printUsage() {
|
|
211
221
|
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"'
|
|
222
|
+
'Usage: aginti [chat] OR aginti web [--port 3210] OR aginti queue <session-id> "message" OR aginti [--latex] [--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
223
|
);
|
|
214
224
|
}
|
|
215
225
|
|
|
226
|
+
function agentDefaults(args) {
|
|
227
|
+
const defaults = {
|
|
228
|
+
...args,
|
|
229
|
+
allowShellTool: args.allowShellTool ?? true,
|
|
230
|
+
allowFileTools: args.allowFileTools ?? true,
|
|
231
|
+
sandboxMode: args.sandboxMode || "docker-workspace",
|
|
232
|
+
packageInstallPolicy: args.packageInstallPolicy || "allow",
|
|
233
|
+
useDockerSandbox: args.useDockerSandbox ?? true,
|
|
234
|
+
maxSteps: args.maxSteps || (args.latex || args.taskProfile === "latex" ? 30 : 24),
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
if (defaults.sandboxMode === "host") {
|
|
238
|
+
defaults.useDockerSandbox = false;
|
|
239
|
+
defaults.packageInstallPolicy = args.packageInstallPolicy || "prompt";
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return defaults;
|
|
243
|
+
}
|
|
244
|
+
|
|
216
245
|
function printRoutes() {
|
|
217
246
|
const presets = getModelPresets();
|
|
218
247
|
for (const preset of Object.values(presets)) {
|
|
@@ -327,6 +356,20 @@ async function handleSessionsCommand(argv) {
|
|
|
327
356
|
process.exit(1);
|
|
328
357
|
}
|
|
329
358
|
|
|
359
|
+
async function handleQueueCommand(argv) {
|
|
360
|
+
const sessionId = argv[0] || "";
|
|
361
|
+
const content = argv.slice(1).join(" ").trim();
|
|
362
|
+
if (!/^[A-Za-z0-9._:-]+$/.test(sessionId) || !content) {
|
|
363
|
+
console.error('Usage: aginti queue <session-id> "message to apply during the next agent step"');
|
|
364
|
+
process.exit(1);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
const store = new SessionStore(path.resolve(process.cwd(), ".sessions"), sessionId);
|
|
368
|
+
await store.appendInbox(content, { source: "cli" });
|
|
369
|
+
await store.appendEvent("conversation.queued_input", { prompt: content, source: "cli" }).catch(() => {});
|
|
370
|
+
console.log(`queued message for ${sessionId}`);
|
|
371
|
+
}
|
|
372
|
+
|
|
330
373
|
export async function main(argv = process.argv.slice(2)) {
|
|
331
374
|
if (argv[0] === "--help" || argv[0] === "help" || argv[0] === "-h") {
|
|
332
375
|
printUsage();
|
|
@@ -407,6 +450,11 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
407
450
|
return;
|
|
408
451
|
}
|
|
409
452
|
|
|
453
|
+
if (argv[0] === "queue") {
|
|
454
|
+
await handleQueueCommand(argv.slice(1));
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
|
|
410
458
|
if (argv[0] === "resume") {
|
|
411
459
|
const sessionId = argv[1] || "";
|
|
412
460
|
const prompt = argv.slice(2).join(" ").trim();
|
|
@@ -414,10 +462,7 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
414
462
|
console.error('Usage: aginti resume <session-id> "new prompt"');
|
|
415
463
|
process.exit(1);
|
|
416
464
|
}
|
|
417
|
-
const config = loadConfig(
|
|
418
|
-
{ ...parseArgs([prompt]), resume: sessionId, goal: prompt, allowShellTool: true, allowFileTools: true },
|
|
419
|
-
{ packageDir }
|
|
420
|
-
);
|
|
465
|
+
const config = loadConfig(agentDefaults({ ...parseArgs([prompt]), resume: sessionId, goal: prompt }), { packageDir });
|
|
421
466
|
await runAgent(config);
|
|
422
467
|
return;
|
|
423
468
|
}
|
|
@@ -425,7 +470,7 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
425
470
|
const args = parseArgs(argv);
|
|
426
471
|
|
|
427
472
|
if (args.interactive || (!args.goal && !args.resume && process.stdin.isTTY)) {
|
|
428
|
-
await startInteractiveCli(args, { packageDir, packageVersion: packageJson.version });
|
|
473
|
+
await startInteractiveCli(agentDefaults(args), { packageDir, packageVersion: packageJson.version });
|
|
429
474
|
return;
|
|
430
475
|
}
|
|
431
476
|
|
|
@@ -466,13 +511,6 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
466
511
|
process.exit(1);
|
|
467
512
|
}
|
|
468
513
|
|
|
469
|
-
const config = loadConfig(
|
|
470
|
-
{
|
|
471
|
-
...args,
|
|
472
|
-
allowShellTool: args.allowShellTool ?? true,
|
|
473
|
-
allowFileTools: args.allowFileTools ?? true,
|
|
474
|
-
},
|
|
475
|
-
{ packageDir }
|
|
476
|
-
);
|
|
514
|
+
const config = loadConfig(agentDefaults(args), { packageDir });
|
|
477
515
|
await runAgent(config);
|
|
478
516
|
}
|
package/src/command-policy.js
CHANGED
|
@@ -39,6 +39,7 @@ const TEST_PATTERNS = [
|
|
|
39
39
|
];
|
|
40
40
|
|
|
41
41
|
const SAFE_WORKSPACE_WRITE_PATTERNS = [/^mkdir\s+-p\s+[-\w./]+$/];
|
|
42
|
+
const PERMISSION_CHANGE_PATTERNS = [/^(?:sudo\s+)?chmod\s+[-+=,rwxugoXst0-7]+\s+[-\w./]+$/];
|
|
42
43
|
|
|
43
44
|
const NETWORK_FETCH_PATTERNS = [
|
|
44
45
|
/^curl\s+(?:-[A-Za-z0-9]*\s+)*https?:\/\/\S+(?:\s+-o\s+[-\w./]+)?$/,
|
|
@@ -153,15 +154,6 @@ function classifySimpleCommand(normalized) {
|
|
|
153
154
|
return { category: "blocked", reason: "Command is blocked because it references secrets or credential files." };
|
|
154
155
|
}
|
|
155
156
|
|
|
156
|
-
const lowered = ` ${normalized.toLowerCase()} `;
|
|
157
|
-
if (BLOCKED_SHELL_TOKENS.some((part) => normalized.includes(part))) {
|
|
158
|
-
return {
|
|
159
|
-
category: "general-shell",
|
|
160
|
-
needsNetwork: true,
|
|
161
|
-
writesWorkspace: true,
|
|
162
|
-
reason: `Command uses general shell syntax: ${normalized}`,
|
|
163
|
-
};
|
|
164
|
-
}
|
|
165
157
|
if (matchAny(SAFE_WORKSPACE_WRITE_PATTERNS, normalized)) {
|
|
166
158
|
const target = normalized.replace(/^mkdir\s+-p\s+/, "");
|
|
167
159
|
const virtualWorkspacePath = isSafeVirtualWorkspaceDir(target);
|
|
@@ -170,6 +162,16 @@ function classifySimpleCommand(normalized) {
|
|
|
170
162
|
}
|
|
171
163
|
return { category: "workspace-write", needsNetwork: false, writesWorkspace: true, virtualWorkspacePath };
|
|
172
164
|
}
|
|
165
|
+
if (matchAny(PERMISSION_CHANGE_PATTERNS, normalized)) {
|
|
166
|
+
return {
|
|
167
|
+
category: "permission-change",
|
|
168
|
+
needsNetwork: false,
|
|
169
|
+
writesWorkspace: true,
|
|
170
|
+
reason: `Command changes workspace file mode: ${normalized}`,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const lowered = ` ${normalized.toLowerCase()} `;
|
|
173
175
|
if (BLOCKED_WRITE_TOKENS.some((part) => lowered.includes(part))) {
|
|
174
176
|
return {
|
|
175
177
|
category: "destructive",
|
|
@@ -178,6 +180,14 @@ function classifySimpleCommand(normalized) {
|
|
|
178
180
|
reason: `Command contains a write-capable or destructive token: ${normalized}`,
|
|
179
181
|
};
|
|
180
182
|
}
|
|
183
|
+
if (BLOCKED_SHELL_TOKENS.some((part) => normalized.includes(part))) {
|
|
184
|
+
return {
|
|
185
|
+
category: "general-shell",
|
|
186
|
+
needsNetwork: true,
|
|
187
|
+
writesWorkspace: true,
|
|
188
|
+
reason: `Command uses general shell syntax: ${normalized}`,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
181
191
|
|
|
182
192
|
if (matchAny(READ_ONLY_PATTERNS, normalized)) {
|
|
183
193
|
return { category: "read-only", needsNetwork: false, writesWorkspace: false };
|
|
@@ -306,6 +316,20 @@ export function evaluateCommandPolicy(command, config) {
|
|
|
306
316
|
};
|
|
307
317
|
}
|
|
308
318
|
|
|
319
|
+
if (classification.category === "permission-change" && !trustedDockerShell && !trustedHostShell) {
|
|
320
|
+
return {
|
|
321
|
+
allowed: false,
|
|
322
|
+
...classification,
|
|
323
|
+
needsApproval: true,
|
|
324
|
+
reason:
|
|
325
|
+
sandboxMode === "host"
|
|
326
|
+
? "Workspace permission changes on the host require Allow destructive actions. Prefer Docker workspace mode."
|
|
327
|
+
: "Docker permission changes require docker-workspace mode with Package installs = allow.",
|
|
328
|
+
sandboxMode,
|
|
329
|
+
packageInstallPolicy,
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
|
|
309
333
|
if (classification.category === "destructive" && !config.allowDestructive) {
|
|
310
334
|
return {
|
|
311
335
|
allowed: false,
|
package/src/config.js
CHANGED
|
@@ -42,9 +42,9 @@ export function resolveRuntimeConfig(args, overrides = {}) {
|
|
|
42
42
|
|
|
43
43
|
const defaults = getProviderDefaults(route.provider);
|
|
44
44
|
const packageDir = path.resolve(overrides.packageDir || process.env.AGINTIFLOW_PACKAGE_DIR || baseDir);
|
|
45
|
-
const dockerRequested = parseBoolean(overrides.useDockerSandbox ?? args.useDockerSandbox ?? process.env.USE_DOCKER_SANDBOX,
|
|
45
|
+
const dockerRequested = parseBoolean(overrides.useDockerSandbox ?? args.useDockerSandbox ?? process.env.USE_DOCKER_SANDBOX, true);
|
|
46
46
|
const requestedSandboxMode =
|
|
47
|
-
overrides.sandboxMode || args.sandboxMode || process.env.SANDBOX_MODE || (dockerRequested ? "docker-
|
|
47
|
+
overrides.sandboxMode || args.sandboxMode || process.env.SANDBOX_MODE || (dockerRequested ? "docker-workspace" : "host");
|
|
48
48
|
const sandboxMode = normalizeSandboxMode(requestedSandboxMode);
|
|
49
49
|
|
|
50
50
|
return {
|
|
@@ -65,14 +65,14 @@ export function resolveRuntimeConfig(args, overrides = {}) {
|
|
|
65
65
|
apiKey: overrides.apiKey || defaults.apiKey,
|
|
66
66
|
baseURL: overrides.baseURL || defaults.baseURL,
|
|
67
67
|
model: route.model || defaults.model,
|
|
68
|
-
maxSteps: parseNumber(overrides.maxSteps ?? args.maxSteps ?? process.env.MAX_STEPS,
|
|
68
|
+
maxSteps: parseNumber(overrides.maxSteps ?? args.maxSteps ?? process.env.MAX_STEPS, 24),
|
|
69
69
|
headless: parseBoolean(overrides.headless ?? args.headless ?? process.env.HEADLESS, false),
|
|
70
70
|
allowedDomains: Array.isArray(overrides.allowedDomains)
|
|
71
71
|
? overrides.allowedDomains
|
|
72
72
|
: parseList(process.env.ALLOWED_DOMAINS),
|
|
73
73
|
allowPasswords: parseBoolean(overrides.allowPasswords ?? process.env.ALLOW_PASSWORDS, false),
|
|
74
74
|
allowDestructive: parseBoolean(overrides.allowDestructive ?? args.allowDestructive ?? process.env.ALLOW_DESTRUCTIVE, false),
|
|
75
|
-
allowShellTool: parseBoolean(overrides.allowShellTool ?? args.allowShellTool ?? process.env.ALLOW_SHELL_TOOL,
|
|
75
|
+
allowShellTool: parseBoolean(overrides.allowShellTool ?? args.allowShellTool ?? process.env.ALLOW_SHELL_TOOL, true),
|
|
76
76
|
allowFileTools: parseBoolean(overrides.allowFileTools ?? args.allowFileTools ?? process.env.ALLOW_FILE_TOOLS, true),
|
|
77
77
|
allowWrapperTools: parseBoolean(
|
|
78
78
|
overrides.allowWrapperTools ?? args.allowWrapperTools ?? process.env.ALLOW_WRAPPER_TOOLS,
|
|
@@ -84,9 +84,9 @@ export function resolveRuntimeConfig(args, overrides = {}) {
|
|
|
84
84
|
wrapperTimeoutMs: parseNumber(overrides.wrapperTimeoutMs ?? process.env.WRAPPER_TIMEOUT_MS, 120000),
|
|
85
85
|
sandboxMode,
|
|
86
86
|
packageInstallPolicy: normalizePackageInstallPolicy(
|
|
87
|
-
overrides.packageInstallPolicy || args.packageInstallPolicy || process.env.PACKAGE_INSTALL_POLICY || "prompt"
|
|
87
|
+
overrides.packageInstallPolicy || args.packageInstallPolicy || process.env.PACKAGE_INSTALL_POLICY || (sandboxMode === "host" ? "prompt" : "allow")
|
|
88
88
|
),
|
|
89
|
-
useDockerSandbox: sandboxMode !== "host"
|
|
89
|
+
useDockerSandbox: sandboxMode !== "host",
|
|
90
90
|
dockerSandboxImage: overrides.dockerSandboxImage || process.env.DOCKER_SANDBOX_IMAGE || "agintiflow-sandbox:latest",
|
|
91
91
|
commandCwd: path.resolve(overrides.commandCwd || args.commandCwd || process.env.COMMAND_CWD || baseDir),
|
|
92
92
|
sessionsDir: path.resolve(baseDir, ".sessions"),
|
package/src/docker-sandbox.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { constants as fsConstants } from "node:fs";
|
|
2
2
|
import fs from "node:fs/promises";
|
|
3
|
+
import os from "node:os";
|
|
3
4
|
import path from "node:path";
|
|
4
5
|
import { execFile as execFileCallback } from "node:child_process";
|
|
5
6
|
import { promisify } from "node:util";
|
|
@@ -8,6 +9,9 @@ import { redactSensitiveText, redactValue } from "./redaction.js";
|
|
|
8
9
|
|
|
9
10
|
const execFile = promisify(execFileCallback);
|
|
10
11
|
const DOCKER_WORKSPACE = "/workspace";
|
|
12
|
+
const DOCKER_HOME = "/aginti-home";
|
|
13
|
+
const DOCKER_CACHE = "/aginti-cache";
|
|
14
|
+
const DOCKER_ENV = "/aginti-env";
|
|
11
15
|
const READY_IMAGES = new Set();
|
|
12
16
|
const SANDBOX_LOG_LIMIT = 80;
|
|
13
17
|
const sandboxLogs = [];
|
|
@@ -101,10 +105,31 @@ function sandboxDockerfilePath(config) {
|
|
|
101
105
|
return path.join(sandboxPackageDir(config), "docker", "sandbox.Dockerfile");
|
|
102
106
|
}
|
|
103
107
|
|
|
108
|
+
function persistentDockerDirs(config) {
|
|
109
|
+
const root = path.resolve(
|
|
110
|
+
config.dockerStateDir || process.env.AGINTIFLOW_DOCKER_STATE_DIR || path.join(os.homedir(), ".agintiflow", "docker")
|
|
111
|
+
);
|
|
112
|
+
return {
|
|
113
|
+
root,
|
|
114
|
+
home: path.join(root, "home"),
|
|
115
|
+
cache: path.join(root, "cache"),
|
|
116
|
+
env: path.join(root, "env"),
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async function ensurePersistentDockerDirs(config) {
|
|
121
|
+
const dirs = persistentDockerDirs(config);
|
|
122
|
+
await Promise.all([fs.mkdir(dirs.home, { recursive: true }), fs.mkdir(dirs.cache, { recursive: true }), fs.mkdir(dirs.env, { recursive: true })]);
|
|
123
|
+
return dirs;
|
|
124
|
+
}
|
|
125
|
+
|
|
104
126
|
export async function getDockerSandboxStatus(config) {
|
|
105
127
|
const image = config.dockerSandboxImage;
|
|
106
128
|
const dockerfilePath = sandboxDockerfilePath(config);
|
|
107
129
|
const workspace = config.commandCwd;
|
|
130
|
+
if (config.useDockerSandbox) {
|
|
131
|
+
await ensurePersistentDockerDirs(config).catch(() => null);
|
|
132
|
+
}
|
|
108
133
|
const [dockerReady, dockerfileExists, workspaceExists, workspaceReadable, workspaceWritable] = await Promise.all([
|
|
109
134
|
dockerAvailable(),
|
|
110
135
|
pathAccess(dockerfilePath, fsConstants.R_OK),
|
|
@@ -114,6 +139,12 @@ export async function getDockerSandboxStatus(config) {
|
|
|
114
139
|
]);
|
|
115
140
|
|
|
116
141
|
const imageReady = dockerReady ? await dockerImageExists(image) : false;
|
|
142
|
+
const persistentDirs = persistentDockerDirs(config);
|
|
143
|
+
const [persistentHomeExists, persistentCacheExists, persistentEnvExists] = await Promise.all([
|
|
144
|
+
pathAccess(persistentDirs.home, fsConstants.F_OK),
|
|
145
|
+
pathAccess(persistentDirs.cache, fsConstants.F_OK),
|
|
146
|
+
pathAccess(persistentDirs.env, fsConstants.F_OK),
|
|
147
|
+
]);
|
|
117
148
|
return {
|
|
118
149
|
sandboxMode: normalizeSandboxMode(config.sandboxMode),
|
|
119
150
|
useDockerSandbox: Boolean(config.useDockerSandbox),
|
|
@@ -127,6 +158,18 @@ export async function getDockerSandboxStatus(config) {
|
|
|
127
158
|
workspaceReadable,
|
|
128
159
|
workspaceWritable,
|
|
129
160
|
dockerfileExists,
|
|
161
|
+
persistentDocker: {
|
|
162
|
+
home: persistentDirs.home,
|
|
163
|
+
cache: persistentDirs.cache,
|
|
164
|
+
env: persistentDirs.env,
|
|
165
|
+
homeExists: persistentHomeExists,
|
|
166
|
+
cacheExists: persistentCacheExists,
|
|
167
|
+
envExists: persistentEnvExists,
|
|
168
|
+
containerHome: DOCKER_HOME,
|
|
169
|
+
containerCache: DOCKER_CACHE,
|
|
170
|
+
containerEnv: DOCKER_ENV,
|
|
171
|
+
pythonVenv: `${DOCKER_ENV}/python`,
|
|
172
|
+
},
|
|
130
173
|
logs: getSandboxLogs(),
|
|
131
174
|
};
|
|
132
175
|
}
|
|
@@ -165,7 +208,31 @@ export async function ensureDockerSandboxReady(config, observers, options = {})
|
|
|
165
208
|
await buildDockerSandboxImage(config, observers);
|
|
166
209
|
}
|
|
167
210
|
|
|
168
|
-
function
|
|
211
|
+
function dockerCommand(command, policy) {
|
|
212
|
+
const envLines = [
|
|
213
|
+
`export HOME=${DOCKER_HOME}`,
|
|
214
|
+
`export XDG_CACHE_HOME=${DOCKER_CACHE}`,
|
|
215
|
+
`export PIP_CACHE_DIR=${DOCKER_CACHE}/pip`,
|
|
216
|
+
`export UV_CACHE_DIR=${DOCKER_CACHE}/uv`,
|
|
217
|
+
`export NPM_CONFIG_CACHE=${DOCKER_CACHE}/npm`,
|
|
218
|
+
`export CONDA_PKGS_DIRS=${DOCKER_CACHE}/conda-pkgs`,
|
|
219
|
+
`export PYTHONUSERBASE=${DOCKER_ENV}/python-user`,
|
|
220
|
+
`export PATH=${DOCKER_ENV}/python/bin:${DOCKER_ENV}/python-user/bin:${DOCKER_ENV}/miniforge/bin:${DOCKER_ENV}/bin:$PATH`,
|
|
221
|
+
`cd ${DOCKER_WORKSPACE}`,
|
|
222
|
+
`if ! command -v sudo >/dev/null 2>&1; then sudo() { "$@"; }; export -f sudo; fi`,
|
|
223
|
+
];
|
|
224
|
+
|
|
225
|
+
if (!policy.requiresDockerRoot) {
|
|
226
|
+
envLines.push(
|
|
227
|
+
`if command -v python3 >/dev/null 2>&1 && [ ! -x ${DOCKER_ENV}/python/bin/python ]; then python3 -m venv --system-site-packages ${DOCKER_ENV}/python >/dev/null 2>&1 || true; fi`,
|
|
228
|
+
`if [ -f ${DOCKER_ENV}/python/bin/activate ]; then . ${DOCKER_ENV}/python/bin/activate; fi`
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return [...envLines, String(command)].join("\n");
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function dockerRunArgs(command, config, policy = evaluateCommandPolicy(command, config), persistentDirs = persistentDockerDirs(config)) {
|
|
169
236
|
const uid = typeof process.getuid === "function" ? String(process.getuid()) : "";
|
|
170
237
|
const gid = typeof process.getgid === "function" ? String(process.getgid()) : "";
|
|
171
238
|
const userArgs = uid && gid && !policy.requiresDockerRoot ? ["--user", `${uid}:${gid}`] : [];
|
|
@@ -196,24 +263,29 @@ function dockerRunArgs(command, config, policy = evaluateCommandPolicy(command,
|
|
|
196
263
|
...readOnlyArgs,
|
|
197
264
|
...userArgs,
|
|
198
265
|
"-e",
|
|
199
|
-
"HOME=/tmp",
|
|
200
|
-
"-e",
|
|
201
266
|
"NPM_CONFIG_USERCONFIG=/tmp/.npmrc",
|
|
202
267
|
"-e",
|
|
203
268
|
"PIP_DISABLE_PIP_VERSION_CHECK=1",
|
|
204
269
|
"-v",
|
|
205
270
|
`${config.commandCwd}:${DOCKER_WORKSPACE}:${mountMode}`,
|
|
271
|
+
"-v",
|
|
272
|
+
`${persistentDirs.home}:${DOCKER_HOME}:rw`,
|
|
273
|
+
"-v",
|
|
274
|
+
`${persistentDirs.cache}:${DOCKER_CACHE}:rw`,
|
|
275
|
+
"-v",
|
|
276
|
+
`${persistentDirs.env}:${DOCKER_ENV}:rw`,
|
|
206
277
|
"-w",
|
|
207
278
|
DOCKER_WORKSPACE,
|
|
208
279
|
config.dockerSandboxImage,
|
|
209
280
|
"bash",
|
|
210
281
|
"-lc",
|
|
211
|
-
|
|
282
|
+
dockerCommand(command, policy),
|
|
212
283
|
];
|
|
213
284
|
}
|
|
214
285
|
|
|
215
286
|
export async function runDockerSandboxCommand(command, config, policy = evaluateCommandPolicy(command, config)) {
|
|
216
|
-
const
|
|
287
|
+
const persistentDirs = await ensurePersistentDockerDirs(config);
|
|
288
|
+
const result = await execDocker(dockerRunArgs(command, config, policy, persistentDirs), {
|
|
217
289
|
timeout: policy.needsNetwork ? 120000 : policy.category === "toolchain" ? 90000 : 15000,
|
|
218
290
|
maxBuffer: 300 * 1024,
|
|
219
291
|
});
|
|
@@ -227,6 +299,7 @@ export async function runDockerSandboxCommand(command, config, policy = evaluate
|
|
|
227
299
|
category: policy.category,
|
|
228
300
|
sandboxMode: policy.sandboxMode,
|
|
229
301
|
network: policy.needsNetwork ? "bridge" : "none",
|
|
302
|
+
persistentDocker: persistentDirs,
|
|
230
303
|
stdout: payload.stdout.slice(0, 1200),
|
|
231
304
|
stderr: payload.stderr.slice(0, 1200),
|
|
232
305
|
});
|
|
@@ -274,6 +347,7 @@ export async function runDockerPreflight(config, options = {}) {
|
|
|
274
347
|
|
|
275
348
|
let status = await getDockerSandboxStatus(config);
|
|
276
349
|
const runChecks = async () => {
|
|
350
|
+
const persistentDirs = await ensurePersistentDockerDirs(config);
|
|
277
351
|
const results = [];
|
|
278
352
|
for (const command of [
|
|
279
353
|
"node -v",
|
|
@@ -287,7 +361,7 @@ export async function runDockerPreflight(config, options = {}) {
|
|
|
287
361
|
"rg --version",
|
|
288
362
|
]) {
|
|
289
363
|
try {
|
|
290
|
-
const result = await execDocker(dockerRunArgs(command, config, { needsNetwork: false, category: "preflight" }), {
|
|
364
|
+
const result = await execDocker(dockerRunArgs(command, config, { needsNetwork: false, category: "preflight" }, persistentDirs), {
|
|
291
365
|
timeout: 15000,
|
|
292
366
|
maxBuffer: 100 * 1024,
|
|
293
367
|
});
|
package/src/interactive-cli.js
CHANGED
|
@@ -21,6 +21,7 @@ function printHelp() {
|
|
|
21
21
|
" /model <name> Set an explicit model, or /model auto.",
|
|
22
22
|
" /docker on Use docker-workspace with approved package installs.",
|
|
23
23
|
" /docker off Use host shell policy.",
|
|
24
|
+
" /latex on Use the LaTeX/PDF profile in Docker with a larger step budget.",
|
|
24
25
|
" /installs block|prompt|allow",
|
|
25
26
|
" /cwd <path> Change command workspace.",
|
|
26
27
|
" /exit Quit.",
|
|
@@ -50,8 +51,8 @@ function createState(args = {}) {
|
|
|
50
51
|
model: args.model || "",
|
|
51
52
|
routingMode: args.routingMode || "smart",
|
|
52
53
|
commandCwd: args.commandCwd || process.cwd(),
|
|
53
|
-
sandboxMode: normalizeSandboxMode(args.sandboxMode || "
|
|
54
|
-
packageInstallPolicy: normalizePackageInstallPolicy(args.packageInstallPolicy || "
|
|
54
|
+
sandboxMode: normalizeSandboxMode(args.sandboxMode || "docker-workspace"),
|
|
55
|
+
packageInstallPolicy: normalizePackageInstallPolicy(args.packageInstallPolicy || "allow"),
|
|
55
56
|
allowShellTool: args.allowShellTool ?? true,
|
|
56
57
|
allowFileTools: args.allowFileTools ?? true,
|
|
57
58
|
allowWrapperTools: args.allowWrapperTools ?? false,
|
|
@@ -59,7 +60,7 @@ function createState(args = {}) {
|
|
|
59
60
|
preferredWrapper: args.preferredWrapper || "codex",
|
|
60
61
|
taskProfile: normalizeTaskProfile(args.taskProfile || "auto"),
|
|
61
62
|
headless: args.headless ?? false,
|
|
62
|
-
maxSteps: Number.isFinite(args.maxSteps) && args.maxSteps > 0 ? args.maxSteps :
|
|
63
|
+
maxSteps: Number.isFinite(args.maxSteps) && args.maxSteps > 0 ? args.maxSteps : args.latex ? 30 : 24,
|
|
63
64
|
sessionId: args.resume || "",
|
|
64
65
|
};
|
|
65
66
|
}
|
|
@@ -140,6 +141,21 @@ async function handleCommand(line, state, packageDir) {
|
|
|
140
141
|
}
|
|
141
142
|
return true;
|
|
142
143
|
}
|
|
144
|
+
if (command === "latex") {
|
|
145
|
+
if (value === "on" || value === "") {
|
|
146
|
+
state.taskProfile = "latex";
|
|
147
|
+
state.sandboxMode = "docker-workspace";
|
|
148
|
+
state.packageInstallPolicy = "allow";
|
|
149
|
+
state.maxSteps = Math.max(state.maxSteps, 30);
|
|
150
|
+
console.log("latex=on profile=latex sandbox=docker-workspace installs=allow maxSteps=30");
|
|
151
|
+
} else if (value === "off") {
|
|
152
|
+
state.taskProfile = "auto";
|
|
153
|
+
console.log("latex=off profile=auto");
|
|
154
|
+
} else {
|
|
155
|
+
console.log("Usage: /latex on OR /latex off");
|
|
156
|
+
}
|
|
157
|
+
return true;
|
|
158
|
+
}
|
|
143
159
|
if (command === "cwd") {
|
|
144
160
|
state.commandCwd = value || process.cwd();
|
|
145
161
|
console.log(`cwd=${state.commandCwd}`);
|
package/src/model-client.js
CHANGED
|
@@ -146,7 +146,7 @@ export async function createPlan(client, config, state) {
|
|
|
146
146
|
state.startUrl ? `Suggested start URL: ${state.startUrl}` : "",
|
|
147
147
|
config.allowedDomains.length > 0 ? `Allowed domains: ${config.allowedDomains.join(", ")}` : "",
|
|
148
148
|
config.allowShellTool
|
|
149
|
-
? `Shell tool is enabled in ${config.commandCwd}. In Docker, this path is mounted as /workspace. Use relative paths or /workspace paths, not absolute host temp paths. 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.`
|
|
149
|
+
? `Shell tool is enabled in ${config.commandCwd}. In Docker, this path is mounted as /workspace with persistent /aginti-env and /aginti-cache mounts. Use relative paths or /workspace paths, not absolute host temp paths. 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.`
|
|
150
150
|
: "",
|
|
151
151
|
config.allowFileTools
|
|
152
152
|
? `Workspace file tools are enabled in ${config.commandCwd}: list_files, read_file, search_files, write_file, apply_patch. Keep all paths workspace-relative, for example plot_fx.svg or docs/report.tex, and avoid secrets.`
|
|
@@ -159,6 +159,8 @@ export async function createPlan(client, config, state) {
|
|
|
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, prefer project-local dry-run plans/scripts unless the configured policy explicitly allows stronger actions.",
|
|
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
|
"Plan for a complete result, not endless exploration; finish once the request is satisfied and checks have passed or been honestly skipped.",
|
|
163
165
|
"Return a numbered plan only.",
|
|
164
166
|
]
|
package/src/session-store.js
CHANGED
|
@@ -10,6 +10,7 @@ export class SessionStore {
|
|
|
10
10
|
this.statePath = path.join(this.sessionDir, "state.json");
|
|
11
11
|
this.planPath = path.join(this.sessionDir, "plan.md");
|
|
12
12
|
this.eventsPath = path.join(this.sessionDir, "events.jsonl");
|
|
13
|
+
this.inboxPath = path.join(this.sessionDir, "inbox.jsonl");
|
|
13
14
|
this.storageStatePath = path.join(this.sessionDir, "storage-state.json");
|
|
14
15
|
}
|
|
15
16
|
|
|
@@ -59,6 +60,34 @@ export class SessionStore {
|
|
|
59
60
|
}
|
|
60
61
|
}
|
|
61
62
|
|
|
63
|
+
async appendInbox(content, metadata = {}) {
|
|
64
|
+
await this.ensure();
|
|
65
|
+
const text = String(content || "").trim();
|
|
66
|
+
if (!text) return;
|
|
67
|
+
const line = JSON.stringify({
|
|
68
|
+
timestamp: new Date().toISOString(),
|
|
69
|
+
content: text,
|
|
70
|
+
...metadata,
|
|
71
|
+
});
|
|
72
|
+
await fs.appendFile(this.inboxPath, `${line}\n`, "utf8");
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async drainInbox() {
|
|
76
|
+
await this.ensure();
|
|
77
|
+
let raw = "";
|
|
78
|
+
try {
|
|
79
|
+
raw = await fs.readFile(this.inboxPath, "utf8");
|
|
80
|
+
} catch {
|
|
81
|
+
return [];
|
|
82
|
+
}
|
|
83
|
+
await fs.writeFile(this.inboxPath, "", "utf8");
|
|
84
|
+
return raw
|
|
85
|
+
.split("\n")
|
|
86
|
+
.map((line) => line.trim())
|
|
87
|
+
.filter(Boolean)
|
|
88
|
+
.map((line) => JSON.parse(line));
|
|
89
|
+
}
|
|
90
|
+
|
|
62
91
|
async saveSnapshot(step, snapshot) {
|
|
63
92
|
await this.ensure();
|
|
64
93
|
const filename = `step-${String(step).padStart(3, "0")}.snapshot.json`;
|
package/src/task-profiles.js
CHANGED
|
@@ -66,7 +66,7 @@ export const TASK_PROFILES = {
|
|
|
66
66
|
id: "latex",
|
|
67
67
|
label: "LaTeX",
|
|
68
68
|
prompt:
|
|
69
|
-
"For LaTeX/PDF tasks, create source and figures in a subfolder, compile when a TeX toolchain is available, and send the PDF through the canvas tunnel.",
|
|
69
|
+
"For LaTeX/PDF tasks, create source and figures in a subfolder, compile when a TeX toolchain is available, and send the PDF through the canvas tunnel. In Docker, use /workspace for project outputs and the persistent Python/conda/tool cache under /aginti-env when setup is needed.",
|
|
70
70
|
tools: ["files", "shell", "canvas", "sandbox"],
|
|
71
71
|
},
|
|
72
72
|
maintenance: {
|
package/src/web-db.js
CHANGED
|
@@ -3,7 +3,7 @@ import path from "node:path";
|
|
|
3
3
|
import { DatabaseSync } from "node:sqlite";
|
|
4
4
|
import { getModelPresets } from "./model-routing.js";
|
|
5
5
|
|
|
6
|
-
const PREFERENCES_SCHEMA_VERSION =
|
|
6
|
+
const PREFERENCES_SCHEMA_VERSION = 4;
|
|
7
7
|
|
|
8
8
|
function defaultPreferences(baseDir) {
|
|
9
9
|
const presets = getModelPresets();
|
|
@@ -13,7 +13,7 @@ function defaultPreferences(baseDir) {
|
|
|
13
13
|
provider: "deepseek",
|
|
14
14
|
model: presets.fast.model,
|
|
15
15
|
headless: true,
|
|
16
|
-
maxSteps:
|
|
16
|
+
maxSteps: 24,
|
|
17
17
|
startUrl: "",
|
|
18
18
|
allowedDomains: "",
|
|
19
19
|
commandCwd: path.resolve(baseDir),
|
|
@@ -23,7 +23,7 @@ function defaultPreferences(baseDir) {
|
|
|
23
23
|
preferredWrapper: "codex",
|
|
24
24
|
wrapperTimeoutMs: 120000,
|
|
25
25
|
sandboxMode: "docker-workspace",
|
|
26
|
-
packageInstallPolicy: "
|
|
26
|
+
packageInstallPolicy: "allow",
|
|
27
27
|
useDockerSandbox: true,
|
|
28
28
|
dockerSandboxImage: "agintiflow-sandbox:latest",
|
|
29
29
|
allowPasswords: false,
|
|
@@ -89,8 +89,11 @@ export class WebDatabase {
|
|
|
89
89
|
preferences.sandboxMode = "docker-workspace";
|
|
90
90
|
preferences.useDockerSandbox = true;
|
|
91
91
|
}
|
|
92
|
-
if (parsed.packageInstallPolicy
|
|
93
|
-
preferences.packageInstallPolicy = "
|
|
92
|
+
if ((parsed.packageInstallPolicy || "") !== "allow") {
|
|
93
|
+
preferences.packageInstallPolicy = "allow";
|
|
94
|
+
}
|
|
95
|
+
if (!Number.isFinite(Number(parsed.maxSteps)) || Number(parsed.maxSteps) < 24) {
|
|
96
|
+
preferences.maxSteps = 24;
|
|
94
97
|
}
|
|
95
98
|
this.savePreferences(preferences);
|
|
96
99
|
}
|
package/web.js
CHANGED
|
@@ -130,7 +130,7 @@ function normalizePreferencePayload(body = {}, current = db.getPreferences()) {
|
|
|
130
130
|
? providerDefaults.model
|
|
131
131
|
: current.model || modelPresets.fast.model || providerDefaults.model,
|
|
132
132
|
headless: typeof body.headless === "boolean" ? body.headless : Boolean(current.headless),
|
|
133
|
-
maxSteps: Number.isFinite(parsedMaxSteps) && parsedMaxSteps > 0 ? parsedMaxSteps : Number(current.maxSteps) ||
|
|
133
|
+
maxSteps: Number.isFinite(parsedMaxSteps) && parsedMaxSteps > 0 ? parsedMaxSteps : Number(current.maxSteps) || 24,
|
|
134
134
|
startUrl: typeof body.startUrl === "string" ? body.startUrl.trim() : current.startUrl || "",
|
|
135
135
|
allowedDomains:
|
|
136
136
|
typeof body.allowedDomains === "string" ? body.allowedDomains.trim() : current.allowedDomains || "",
|
|
@@ -156,7 +156,7 @@ function normalizePreferencePayload(body = {}, current = db.getPreferences()) {
|
|
|
156
156
|
: Boolean(current.useDockerSandbox),
|
|
157
157
|
sandboxMode,
|
|
158
158
|
packageInstallPolicy: normalizePackageInstallPolicy(
|
|
159
|
-
body.packageInstallPolicy || current.packageInstallPolicy || "prompt"
|
|
159
|
+
body.packageInstallPolicy || current.packageInstallPolicy || (sandboxMode === "host" ? "prompt" : "allow")
|
|
160
160
|
),
|
|
161
161
|
dockerSandboxImage:
|
|
162
162
|
typeof body.dockerSandboxImage === "string" && body.dockerSandboxImage.trim()
|
|
@@ -519,7 +519,7 @@ app.get("/api/config", async (_req, res) => {
|
|
|
519
519
|
deepseek: publicProviderDefault("deepseek"),
|
|
520
520
|
mock: publicProviderDefault("mock"),
|
|
521
521
|
headless: true,
|
|
522
|
-
maxSteps:
|
|
522
|
+
maxSteps: 24,
|
|
523
523
|
},
|
|
524
524
|
taskProfiles: listTaskProfiles(),
|
|
525
525
|
routing: {
|
|
@@ -768,6 +768,28 @@ app.post("/api/sessions/:sessionId/messages", async (req, res) => {
|
|
|
768
768
|
return;
|
|
769
769
|
}
|
|
770
770
|
|
|
771
|
+
if (!isSafeSessionId(sessionId)) {
|
|
772
|
+
res.status(400).json({ error: "Invalid session id." });
|
|
773
|
+
return;
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
const active = runs.get(sessionId);
|
|
777
|
+
if (active?.status === "running") {
|
|
778
|
+
const store = sessionStore(sessionId);
|
|
779
|
+
await store.appendInbox(content, { source: "web" });
|
|
780
|
+
await store.appendEvent("conversation.queued_input", { prompt: content, source: "web" }).catch(() => {});
|
|
781
|
+
active.logs.push({
|
|
782
|
+
at: new Date().toISOString(),
|
|
783
|
+
kind: "event",
|
|
784
|
+
message: "conversation.queued_input",
|
|
785
|
+
data: { prompt: content, source: "web" },
|
|
786
|
+
});
|
|
787
|
+
active.updatedAt = new Date().toISOString();
|
|
788
|
+
db.upsertSession(active);
|
|
789
|
+
res.json({ sessionId, queued: true });
|
|
790
|
+
return;
|
|
791
|
+
}
|
|
792
|
+
|
|
771
793
|
try {
|
|
772
794
|
await ensureNotRunning(sessionId);
|
|
773
795
|
} catch (error) {
|