@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 +22 -8
- package/docs/agent-runtime-pipe.md +14 -0
- package/package.json +6 -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-cli-chat.js +76 -0
- package/scripts/smoke-inbox.js +36 -0
- package/scripts/smoke-web-api.js +3 -0
- package/src/agent-runner.js +47 -5
- package/src/capabilities.js +10 -1
- package/src/cli.js +77 -5
- package/src/command-policy.js +33 -9
- package/src/config.js +6 -6
- package/src/docker-sandbox.js +79 -6
- package/src/interactive-cli.js +241 -0
- 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,9 +43,19 @@ 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
|
+
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
|
|
75
|
-
aginti --
|
|
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
|
|
188
|
-
- Docker workspace-write mode is the
|
|
189
|
-
-
|
|
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.
|
|
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
|
|
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.
|
|
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) ||
|
|
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,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
|
+
}
|
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);
|
|
@@ -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);
|
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
|
];
|