@lazyingart/agintiflow 0.20.94 → 0.20.109

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -24,6 +24,14 @@ Use the same agent workspace from Web or CLI, with DeepSeek/Venice/OpenAI routin
24
24
 
25
25
  The short version: run `aginti` inside a project, give it a task, inspect what it plans, see every tool call, resume later, and keep the outputs in your workspace.
26
26
 
27
+ ## Visual Preview
28
+
29
+ The two screenshots below match the current website hero: terminal-first launch on the left, browser console visibility on the right.
30
+
31
+ | CLI launch | Web console |
32
+ | --- | --- |
33
+ | <img src="./website/assets/screenshots/cli-launch.jpg" alt="AgInTiFlow CLI launch screen" width="480" /> | <img src="./website/assets/screenshots/web-console-conversation-run-output.jpg" alt="AgInTiFlow web console conversation and run output" width="480" /> |
34
+
27
35
  **Links**
28
36
 
29
37
  | Resource | URL |
@@ -35,8 +43,6 @@ The short version: run `aginti` inside a project, give it a task, inspect what i
35
43
  | Product positioning | [references/agintiflow-product-positioning.md](references/agintiflow-product-positioning.md) |
36
44
  | Full archived README reference | [references/notes/readme-full-reference-2026-05-05.md](references/notes/readme-full-reference-2026-05-05.md) |
37
45
 
38
- Screenshots are kept out of the README body so the start guide stays readable. View them on the website carousel or in [demos/](demos/) and [website/assets/screenshots/](website/assets/screenshots/).
39
-
40
46
  ## Why This Exists
41
47
 
42
48
  Most agent tools are either a chat box with hidden state or an expensive one-model loop. AgInTiFlow is built around a different philosophy:
@@ -128,6 +134,7 @@ aginti --language de
128
134
  | Use SCS only for complex work | `/scs auto` or `aginti --scs auto "task"` |
129
135
  | Work with AAPS workflows | `aginti aaps status`, `/aaps validate` |
130
136
  | Choose models | `/route`, `/model`, `/spare`, `/wrapper`, `/auxiliary model` |
137
+ | Switch permissions | `-s safe`, `-s normal`, `-s danger`, or `/safe`, `/normal`, `/danger` |
131
138
  | Enable Venice shortcut | `/venice` |
132
139
  | Generate images | `/auxiliary image`, then ask for an image |
133
140
  | Resume current project | `aginti resume` |
@@ -155,7 +162,7 @@ aginti --resume <session-id> \
155
162
  "Take a fresh screenshot of the running app in the emulator, save it with a durable filename in this project, and keep git status clean."
156
163
  ```
157
164
 
158
- Permission behavior is intentionally consistent: writes inside the current project are allowed through file tools, workspace-local toolchain builds/probes can run when their command is narrowly classified, network/setup runs are normal in approved Docker workspace mode, and outside-project or trusted-host actions stop with a clear blocker plus a suggested rerun command. See [runtime modes and autonomy](docs/runtime-modes-and-autonomy.md) for the full contract.
165
+ Permission behavior is intentionally consistent. Use `-s safe` for read-first sessions that ask before writes/setup, `-s normal` for current-project writes plus Docker setup, and `-s danger` for trusted host/full-access work. Inside chat, `/safe`, `/normal`, and `/danger` switch the current session. When a blocked action appears, CLI and web can offer `No`, `Yes this time`, or `Yes and always for this session` instead of making the agent retry command variants. Android/Gradle builds can use safe local env assignments such as `ANDROID_HOME=... JAVA_HOME=... ./gradlew assembleDebug` and relative workspace logs without requiring whole-host destructive mode. See [runtime modes and autonomy](docs/runtime-modes-and-autonomy.md) for the full contract.
159
166
 
160
167
  Tmux follows the same rule. In Docker sandbox mode, `tmux_start_session` and `tmux_send_keys` are durable host tools, but their commands must stay workspace-bound. In host mode, tmux startup/send command text follows the same host shell policy as `run_command`; broad host shell work needs explicit `--allow-destructive`. Use `--sandbox-mode host --allow-destructive` only when a tmux task really needs trusted whole-host execution.
161
168
 
@@ -165,20 +172,15 @@ Use these when you want explicit control instead of the default interactive poli
165
172
 
166
173
  | Mode | Command | What it permits |
167
174
  | --- | --- | --- |
168
- | Strict inspection | `aginti --sandbox-mode docker-readonly --package-install-policy block --allow-shell --no-file-tools --no-web-search "inspect this project without edits"` | Enforced read-only project inspection through shell commands such as `ls`, `rg`, `cat`, and test commands that do not write. No file-tool writes, web calls, workspace writes, or installs. |
169
- | Full write in current folder | `aginti --sandbox-mode docker-workspace --package-install-policy allow --approve-package-installs --allow-shell --allow-file-tools "build and test this project"` | Read/write inside the current project folder, run network/setup commands in Docker, keep host safer. |
170
- | Full host computer access | `aginti --sandbox-mode host --package-install-policy allow --approve-package-installs --allow-shell --allow-file-tools --allow-destructive "perform the trusted host maintenance task"` | Direct host shell and destructive actions. Use only when you trust the task and want whole-host access. Android emulator/device work often needs this because the SDK and emulator live on the host. |
175
+ | Safe | `aginti -s safe "inspect this project and ask before edits"` | Docker read-only posture, package/setup approval required, and file-tool writes stop for approval. |
176
+ | Normal | `aginti -s normal "build and test this project"` | Current-project file writes and Docker workspace package/setup are allowed; outside-project writes and host-system changes still stop. |
177
+ | Danger | `aginti -s danger "perform the trusted host maintenance task"` | Trusted host mode with destructive shell, host installs, password typing, and outside-workspace file paths enabled. Hard secret/publish guards still protect obvious credential leaks. |
171
178
 
172
179
  For resume:
173
180
 
174
181
  ```bash
175
182
  aginti --resume <session-id> \
176
- --sandbox-mode host \
177
- --package-install-policy allow \
178
- --approve-package-installs \
179
- --allow-shell \
180
- --allow-file-tools \
181
- --allow-destructive \
183
+ -s danger \
182
184
  "continue with trusted host access"
183
185
  ```
184
186
 
@@ -6,6 +6,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
6
6
  bash \
7
7
  ca-certificates \
8
8
  curl \
9
+ file \
9
10
  findutils \
10
11
  gawk \
11
12
  git \
@@ -27,6 +27,7 @@ Important boundary: host tmux tools are durable, but they are not a permission e
27
27
 
28
28
  Codex exposes these concerns as `--sandbox read-only|workspace-write|danger-full-access` and `--ask-for-approval untrusted|on-failure|on-request|never`. AgInTiFlow maps the same philosophy into:
29
29
 
30
+ - `-s safe|normal|danger` and `/safe`, `/normal`, `/danger`
30
31
  - `--sandbox-mode host|docker-readonly|docker-workspace`
31
32
  - `--package-install-policy block|prompt|allow`
32
33
  - `--allow-shell|--no-shell`
@@ -50,8 +51,12 @@ The runtime must be consistent when an action is not permitted. A blocked action
50
51
 
51
52
  Current contract:
52
53
 
54
+ - `safe` asks before file-tool writes and package/setup actions. It is the right mode for inspection, review, and cautious first passes.
55
+ - `normal` allows writes inside the current project folder and package/setup inside Docker workspace mode. It still blocks outside-project writes and host-system changes until approved.
56
+ - `danger` is trusted host/full-access mode: host shell, destructive actions, host installs, password typing, and outside-workspace file paths are enabled. Use it only for tasks you trust; obvious secret/publish exfiltration guards remain hard stops.
53
57
  - Workspace file tools may read and write inside the configured project folder when file tools are enabled.
54
58
  - Narrow workspace-local shell actions such as safe probes, Gradle/TeX builds, and `chmod +x` on a project script may run when the command policy can classify them precisely.
59
+ - Android/JVM toolchain commands may include safe local env assignments such as `ANDROID_HOME`, `ANDROID_SDK_ROOT`, `JAVA_HOME`, or `GRADLE_USER_HOME`, plus small workspace-local status-log redirects. Secret-like env vars and redirects outside the workspace remain blocked.
55
60
  - Workspace writes outside that folder, secret paths, `.git` internals, and dependency folders such as `node_modules` are blocked.
56
61
  - `git clone`, `git fetch`, `git pull --ff-only`, `git push`, `curl`, and `wget` are classified as network operations.
57
62
  - Network/setup commands are allowed in `docker-workspace` only when package installs are approved.
@@ -59,7 +64,7 @@ Current contract:
59
64
  - Destructive host shell/git operations need explicit trusted host mode.
60
65
  - Android SDK/emulator work commonly needs host mode because `adb`, emulator images, and device state live outside Docker; use trusted host mode for install/launch/screenshot steps when the user approves that host access.
61
66
 
62
- When a tool is blocked, the tool result includes `permissionAdvice` with three choices: refuse/stop, approve a safer rerun, or switch to a trusted host run. The model prompt requires the agent to present that blocker and avoid retrying variants until the user approves a path.
67
+ When a tool is blocked, the tool result includes `permissionAdvice`. Interactive CLI and web runs present the same approval language: `No`, `Yes this time`, and `Yes and always for this session`. One-time approval temporarily escalates the active continuation; always switches the session to the suggested shortcut mode. The model prompt requires the agent to present that blocker and avoid retrying variants until the user approves a path.
63
68
 
64
69
  The common controlled rerun shape is:
65
70
 
@@ -77,30 +82,14 @@ aginti --resume <session-id> \
77
82
  Three practical permission recipes:
78
83
 
79
84
  ```bash
80
- # Strict inspection: enforced read-only shell inspection, no file-tool writes, no web, no installs.
81
- aginti --sandbox-mode docker-readonly \
82
- --package-install-policy block \
83
- --allow-shell \
84
- --no-file-tools \
85
- --no-web-search \
86
- "inspect this project without edits"
85
+ # Safe: read-first, ask before writes and setup.
86
+ aginti -s safe "inspect this project without edits"
87
87
 
88
- # Full write in the current project folder, with setup commands isolated in Docker.
89
- aginti --sandbox-mode docker-workspace \
90
- --package-install-policy allow \
91
- --approve-package-installs \
92
- --allow-shell \
93
- --allow-file-tools \
94
- "build and test this project"
88
+ # Normal: write in the current project folder, setup isolated in Docker.
89
+ aginti -s normal "build and test this project"
95
90
 
96
- # Full host computer access for trusted maintenance.
97
- aginti --sandbox-mode host \
98
- --package-install-policy allow \
99
- --approve-package-installs \
100
- --allow-shell \
101
- --allow-file-tools \
102
- --allow-destructive \
103
- "perform the trusted host maintenance task"
91
+ # Danger: full trusted host access for maintenance or emulator/device work.
92
+ aginti -s danger "perform the trusted host maintenance task"
104
93
  ```
105
94
 
106
95
  For host-only work, use the stricter trusted form deliberately:
@@ -108,12 +97,7 @@ For host-only work, use the stricter trusted form deliberately:
108
97
  ```bash
109
98
  aginti --resume <session-id> \
110
99
  --cwd /path/to/project \
111
- --sandbox-mode host \
112
- --package-install-policy allow \
113
- --approve-package-installs \
114
- --allow-shell \
115
- --allow-file-tools \
116
- --allow-destructive \
100
+ -s danger \
117
101
  "Continue after trusted host approval. Inspect first and keep unrelated files untouched."
118
102
  ```
119
103
 
@@ -130,7 +114,7 @@ This starts interactive chat with Docker workspace mode, file tools, shell tools
130
114
  Direct trusted host mode:
131
115
 
132
116
  ```bash
133
- aginti --sandbox-mode host --allow-shell --allow-destructive "fix this local service"
117
+ aginti -s danger "fix this local service"
134
118
  ```
135
119
 
136
120
  Use this only when the task truly needs host access. The agent should inspect first, show risky commands, stop on ambiguity, and avoid secrets.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lazyingart/agintiflow",
3
- "version": "0.20.94",
3
+ "version": "0.20.109",
4
4
  "type": "module",
5
5
  "description": "Low-cost, project-aware Web and CLI agents with DeepSeek/Venice/OpenAI routing, visible tool calls, durable sessions, scouts, AAPS, SCS, and guarded local execution.",
6
6
  "license": "Apache-2.0",
@@ -61,6 +61,7 @@
61
61
  "scripts/smoke-inbox.js",
62
62
  "scripts/smoke-model-roles.js",
63
63
  "scripts/smoke-platform.js",
64
+ "scripts/smoke-permission-modes.js",
64
65
  "scripts/smoke-skills.js",
65
66
  "scripts/smoke-skillmesh.js",
66
67
  "scripts/smoke-tmux-tools.js",
@@ -92,13 +93,14 @@
92
93
  "smoke:inbox": "node scripts/smoke-inbox.js",
93
94
  "smoke:model-roles": "node scripts/smoke-model-roles.js",
94
95
  "smoke:platform": "node scripts/smoke-platform.js",
96
+ "smoke:permission-modes": "node scripts/smoke-permission-modes.js",
95
97
  "smoke:tmux-tools": "node scripts/smoke-tmux-tools.js",
96
98
  "smoke:web-api": "node scripts/smoke-web-api.js",
97
99
  "smoke:autoupdate": "node scripts/smoke-auto-update.js",
98
100
  "real:deepseek": "node scripts/real-deepseek-capabilities.js",
99
101
  "supervision:seed": "node scripts/seed-supervised-homework.js",
100
102
  "storage:migrate": "node bin/aginti-cli.js storage migrate",
101
- "test": "npm run check && npm run smoke:autoupdate && npm run smoke:web-api && npm run smoke:coding-tools && npm run smoke:aaps-adapter && npm run smoke:auxiliary-tools && npm run smoke:auth && npm run smoke:canvas-artifacts && npm run smoke:capabilities && npm run smoke:model-roles && npm run smoke:platform && npm run smoke:skills && npm run smoke:skillmesh && npm run smoke:tmux-tools && npm run smoke:cli-chat && npm run smoke:inbox",
103
+ "test": "npm run check && npm run smoke:autoupdate && npm run smoke:web-api && npm run smoke:coding-tools && npm run smoke:aaps-adapter && npm run smoke:auxiliary-tools && npm run smoke:auth && npm run smoke:canvas-artifacts && npm run smoke:capabilities && npm run smoke:model-roles && npm run smoke:platform && npm run smoke:permission-modes && npm run smoke:skills && npm run smoke:skillmesh && npm run smoke:tmux-tools && npm run smoke:cli-chat && npm run smoke:inbox",
102
104
  "pack:dry-run": "npm pack --dry-run",
103
105
  "smoke:capabilities": "node scripts/smoke-capabilities.js"
104
106
  },
package/public/app.js CHANGED
@@ -57,6 +57,18 @@ const translations = {
57
57
  commandCwdLabel: "Working directory",
58
58
  maxStepsLabel: "Max steps",
59
59
  sandboxModeLabel: "Sandbox mode",
60
+ permissionModeLabel: "Permission shortcut",
61
+ permissionSafeOption: "Safe: ask before writes/setup",
62
+ permissionNormalOption: "Normal: project writes + Docker setup",
63
+ permissionDangerOption: "Danger: trusted host/full access",
64
+ permissionHintSafe: "Safe mode asks before workspace writes and package/setup actions.",
65
+ permissionHintNormal: "Normal mode allows current-project writes and package setup inside Docker; outside-project and host-system changes still stop for approval.",
66
+ permissionHintDanger: "Danger mode enables trusted host access, destructive commands, password typing, and outside-workspace file paths. Use only for tasks you trust.",
67
+ permissionApprovalTitle: "Permission approval",
68
+ permissionApprovalNo: "No",
69
+ permissionApprovalOnce: "Yes this time",
70
+ permissionApprovalAlways: "Yes and always",
71
+ permissionApprovalFailed: "Permission approval failed.",
60
72
  sandboxHostOption: "Host read-only",
61
73
  sandboxDockerReadonlyOption: "Docker read-only",
62
74
  sandboxDockerWorkspaceOption: "Docker workspace-write",
@@ -672,6 +684,8 @@ const setupApiKeyField = document.querySelector("#setup-api-key");
672
684
  const saveApiKeyButton = document.querySelector("#save-api-key");
673
685
  const setupStatusEl = document.querySelector("#setup-status");
674
686
  const taskProfileField = document.querySelector("#taskProfile");
687
+ const permissionModeField = document.querySelector("#permissionMode");
688
+ const permissionHintEl = document.querySelector("#permission-hint");
675
689
  const sandboxModeField = document.querySelector("#sandboxMode");
676
690
  const packageInstallPolicyField = document.querySelector("#packageInstallPolicy");
677
691
  const packageWarningEl = document.querySelector("#package-warning");
@@ -1218,6 +1232,60 @@ function updatePackageWarning() {
1218
1232
  packageWarningEl.textContent = t(key);
1219
1233
  }
1220
1234
 
1235
+ function permissionDefaults(mode = "normal") {
1236
+ if (mode === "safe") {
1237
+ return {
1238
+ sandboxMode: "docker-readonly",
1239
+ packageInstallPolicy: "prompt",
1240
+ workspaceWritePolicy: "prompt",
1241
+ allowPasswords: false,
1242
+ allowDestructive: false,
1243
+ allowOutsideWorkspaceFileTools: false,
1244
+ };
1245
+ }
1246
+ if (mode === "danger") {
1247
+ return {
1248
+ sandboxMode: "host",
1249
+ packageInstallPolicy: "allow",
1250
+ workspaceWritePolicy: "allow",
1251
+ allowPasswords: true,
1252
+ allowDestructive: true,
1253
+ allowOutsideWorkspaceFileTools: true,
1254
+ };
1255
+ }
1256
+ return {
1257
+ sandboxMode: "docker-workspace",
1258
+ packageInstallPolicy: "allow",
1259
+ workspaceWritePolicy: "allow",
1260
+ allowPasswords: false,
1261
+ allowDestructive: false,
1262
+ allowOutsideWorkspaceFileTools: false,
1263
+ };
1264
+ }
1265
+
1266
+ function updatePermissionHint() {
1267
+ if (!permissionHintEl || !permissionModeField) return;
1268
+ const key =
1269
+ permissionModeField.value === "safe"
1270
+ ? "permissionHintSafe"
1271
+ : permissionModeField.value === "danger"
1272
+ ? "permissionHintDanger"
1273
+ : "permissionHintNormal";
1274
+ permissionHintEl.textContent = t(key);
1275
+ }
1276
+
1277
+ function applyPermissionModeToForm(mode = permissionModeField?.value || "normal") {
1278
+ const defaults = permissionDefaults(mode);
1279
+ if (sandboxModeField) sandboxModeField.value = defaults.sandboxMode;
1280
+ if (packageInstallPolicyField) packageInstallPolicyField.value = defaults.packageInstallPolicy;
1281
+ const allowPasswordsField = document.querySelector("#allowPasswords");
1282
+ const allowDestructiveField = document.querySelector("#allowDestructive");
1283
+ if (allowPasswordsField) allowPasswordsField.checked = defaults.allowPasswords;
1284
+ if (allowDestructiveField) allowDestructiveField.checked = defaults.allowDestructive;
1285
+ updatePermissionHint();
1286
+ updatePackageWarning();
1287
+ }
1288
+
1221
1289
  function applyLanguage(language, { persist = true } = {}) {
1222
1290
  currentLanguage = normalizeLanguage(language);
1223
1291
  document.documentElement.lang = currentLanguage;
@@ -1244,6 +1312,7 @@ function applyLanguage(language, { persist = true } = {}) {
1244
1312
  renderWorkspacePanel();
1245
1313
  renderSandboxStatus();
1246
1314
  updateRoutingHint();
1315
+ updatePermissionHint();
1247
1316
  updatePackageWarning();
1248
1317
  renderSessions(lastSessions);
1249
1318
  renderSessionManager();
@@ -1274,8 +1343,10 @@ function formPayload() {
1274
1343
  allowedDomains: document.querySelector("#allowedDomains").value.trim(),
1275
1344
  commandCwd: document.querySelector("#commandCwd").value.trim(),
1276
1345
  maxSteps: Number(document.querySelector("#maxSteps").value) || 24,
1346
+ permissionMode: permissionModeField?.value || "normal",
1277
1347
  sandboxMode: sandboxModeField.value,
1278
1348
  packageInstallPolicy: packageInstallPolicyField.value,
1349
+ workspaceWritePolicy: permissionDefaults(permissionModeField?.value || "normal").workspaceWritePolicy,
1279
1350
  headless: document.querySelector("#headless").checked,
1280
1351
  allowShellTool: document.querySelector("#allowShellTool").checked,
1281
1352
  allowFileTools: document.querySelector("#allowFileTools").checked,
@@ -1290,6 +1361,7 @@ function formPayload() {
1290
1361
  dockerSandboxImage: document.querySelector("#dockerSandboxImage").value.trim(),
1291
1362
  allowPasswords: document.querySelector("#allowPasswords").checked,
1292
1363
  allowDestructive: document.querySelector("#allowDestructive").checked,
1364
+ allowOutsideWorkspaceFileTools: permissionDefaults(permissionModeField?.value || "normal").allowOutsideWorkspaceFileTools,
1293
1365
  };
1294
1366
  }
1295
1367
 
@@ -1326,6 +1398,33 @@ function renderCommandOutputLog(entry) {
1326
1398
  `;
1327
1399
  }
1328
1400
 
1401
+ function renderPermissionApproval(entry) {
1402
+ const advice = entry.data?.permissionAdvice;
1403
+ if (!advice) return "";
1404
+ const summary = advice.summary || advice.reason || "Permission approval is required.";
1405
+ const category = advice.category || entry.data?.category || "";
1406
+ const suggested = advice.suggestedCommand
1407
+ ? `<div class="log-fold-note"><strong>rerun:</strong> <code>${escapeHtml(advice.suggestedCommand)}</code></div>`
1408
+ : "";
1409
+ const trusted = advice.trustedHostCommand
1410
+ ? `<div class="log-fold-note"><strong>host:</strong> <code>${escapeHtml(advice.trustedHostCommand)}</code></div>`
1411
+ : "";
1412
+ return `
1413
+ <article class="permission-approval">
1414
+ <strong>${escapeHtml(t("permissionApprovalTitle"))}</strong>
1415
+ <p>${escapeHtml(summary)}</p>
1416
+ <div class="subtle">${escapeHtml(category)}</div>
1417
+ ${suggested}
1418
+ ${trusted}
1419
+ <div class="permission-actions">
1420
+ <button type="button" class="mini-button danger" data-permission-action="no">${escapeHtml(t("permissionApprovalNo"))}</button>
1421
+ <button type="button" class="mini-button" data-permission-action="once">${escapeHtml(t("permissionApprovalOnce"))}</button>
1422
+ <button type="button" class="mini-button" data-permission-action="always">${escapeHtml(t("permissionApprovalAlways"))}</button>
1423
+ </div>
1424
+ </article>
1425
+ `;
1426
+ }
1427
+
1329
1428
  function renderLogs(run) {
1330
1429
  logsEl.dataset.mode = "active";
1331
1430
  const parts = [
@@ -1340,6 +1439,10 @@ function renderLogs(run) {
1340
1439
  continue;
1341
1440
  }
1342
1441
  parts.push(`<div class="log-line">${escapeHtml(`[${entry.at}] ${entry.kind}: ${entry.message}`)}</div>`);
1442
+ if (entry.message === "tool.blocked" && entry.data?.permissionAdvice) {
1443
+ parts.push(renderPermissionApproval(entry));
1444
+ continue;
1445
+ }
1343
1446
  if (entry.data && Object.keys(entry.data).length > 0) {
1344
1447
  parts.push(`<pre class="log-json">${escapeHtml(JSON.stringify(entry.data, null, 2))}</pre>`);
1345
1448
  }
@@ -2270,6 +2373,36 @@ async function stopCurrentRun() {
2270
2373
  await refreshRun();
2271
2374
  }
2272
2375
 
2376
+ async function approvePermission(action) {
2377
+ if (!currentSessionId) return;
2378
+ chatStatusEl.textContent = t("sendingStatus");
2379
+ const response = await fetch(`/api/sessions/${encodeURIComponent(currentSessionId)}/approve-permission`, {
2380
+ method: "POST",
2381
+ headers: { "Content-Type": "application/json" },
2382
+ body: JSON.stringify({
2383
+ ...formPayload(),
2384
+ action,
2385
+ }),
2386
+ });
2387
+ const data = await response.json().catch(() => ({}));
2388
+ if (!response.ok) {
2389
+ chatStatusEl.textContent = data.error || t("permissionApprovalFailed");
2390
+ return;
2391
+ }
2392
+ if (data.permissionMode && action === "always" && permissionModeField) {
2393
+ permissionModeField.value = data.permissionMode;
2394
+ applyPermissionModeToForm(data.permissionMode);
2395
+ }
2396
+ currentRunStatus = action === "no" ? currentRunStatus : "running";
2397
+ chatStatusEl.textContent = action === "no" ? t("permissionApprovalNo") : t("runningStatus");
2398
+ updateStopRunButton();
2399
+ await refreshRun();
2400
+ if (action !== "no") {
2401
+ if (pollTimer) clearInterval(pollTimer);
2402
+ pollTimer = setInterval(refreshRun, 1500);
2403
+ }
2404
+ }
2405
+
2273
2406
  async function refreshChat() {
2274
2407
  if (!currentSessionId) {
2275
2408
  pendingInboxItems = [];
@@ -2595,6 +2728,10 @@ modelField.addEventListener("change", updateRoutingHint);
2595
2728
  });
2596
2729
  sandboxModeField.addEventListener("change", updatePackageWarning);
2597
2730
  packageInstallPolicyField.addEventListener("change", updatePackageWarning);
2731
+ permissionModeField?.addEventListener("change", () => {
2732
+ applyPermissionModeToForm(permissionModeField.value);
2733
+ schedulePreferenceSave();
2734
+ });
2598
2735
  allowWrapperToolsField.addEventListener("change", () => renderWrapperStatus());
2599
2736
  preferredWrapperField.addEventListener("change", () => renderWrapperStatus());
2600
2737
  taskProfileField?.addEventListener("change", () => {
@@ -2687,6 +2824,14 @@ stopRunButton?.addEventListener("click", () => {
2687
2824
  });
2688
2825
  });
2689
2826
 
2827
+ logsEl?.addEventListener("click", (event) => {
2828
+ const button = event.target.closest("[data-permission-action]");
2829
+ if (!button) return;
2830
+ approvePermission(button.dataset.permissionAction || "no").catch((error) => {
2831
+ chatStatusEl.textContent = error instanceof Error ? error.message : String(error);
2832
+ });
2833
+ });
2834
+
2690
2835
  window.addEventListener("keydown", (event) => {
2691
2836
  if (event.key !== "Escape" || currentRunStatus !== "running") return;
2692
2837
  const tag = event.target?.tagName?.toLowerCase();
@@ -2870,6 +3015,7 @@ async function loadConfig() {
2870
3015
  document.querySelector("#commandCwd").value = prefs.commandCwd || data.project?.root || "";
2871
3016
  document.querySelector("#headless").checked = prefs.headless ?? data.defaults.headless;
2872
3017
  document.querySelector("#maxSteps").value = prefs.maxSteps ?? data.defaults.maxSteps;
3018
+ if (permissionModeField) permissionModeField.value = prefs.permissionMode || "normal";
2873
3019
  sandboxModeField.value = prefs.sandboxMode || (prefs.useDockerSandbox ? "docker-workspace" : "host");
2874
3020
  packageInstallPolicyField.value = prefs.packageInstallPolicy || "allow";
2875
3021
  document.querySelector("#allowShellTool").checked = prefs.allowShellTool ?? true;
@@ -2891,6 +3037,7 @@ async function loadConfig() {
2891
3037
  await refreshWorkspaceChanges();
2892
3038
  renderSandboxLogs(data.sandbox?.logs || []);
2893
3039
  updateRoutingHint();
3040
+ updatePermissionHint();
2894
3041
  updatePackageWarning();
2895
3042
 
2896
3043
  renderSessions(data.sessions || []);
package/public/index.html CHANGED
@@ -154,6 +154,16 @@
154
154
  </div>
155
155
  <p id="package-warning" class="subtle package-warning"></p>
156
156
 
157
+ <label>
158
+ <span data-i18n="permissionModeLabel">Permission shortcut</span>
159
+ <select id="permissionMode" name="permissionMode">
160
+ <option value="safe" data-i18n="permissionSafeOption">Safe: ask before writes/setup</option>
161
+ <option value="normal" data-i18n="permissionNormalOption">Normal: project writes + Docker setup</option>
162
+ <option value="danger" data-i18n="permissionDangerOption">Danger: trusted host/full access</option>
163
+ </select>
164
+ </label>
165
+ <p id="permission-hint" class="subtle route-hint"></p>
166
+
157
167
  <div class="grid">
158
168
  <label>
159
169
  <span data-i18n="sandboxModeLabel">Sandbox mode</span>
package/public/styles.css CHANGED
@@ -875,6 +875,26 @@ button.danger {
875
875
  background: rgba(255, 255, 255, 0.06);
876
876
  }
877
877
 
878
+ .permission-approval {
879
+ margin: 8px 0 14px;
880
+ padding: 12px;
881
+ border: 1px solid rgba(251, 146, 60, 0.42);
882
+ border-radius: 14px;
883
+ background: rgba(124, 45, 18, 0.22);
884
+ color: #ffedd5;
885
+ }
886
+
887
+ .permission-approval p {
888
+ margin: 6px 0;
889
+ }
890
+
891
+ .permission-actions {
892
+ display: flex;
893
+ flex-wrap: wrap;
894
+ gap: 8px;
895
+ margin-top: 10px;
896
+ }
897
+
878
898
  .log-stream-title,
879
899
  .log-fold-note {
880
900
  padding: 0 12px 8px;
@@ -64,7 +64,7 @@ function cellWidth(value = "") {
64
64
  }
65
65
 
66
66
  function runChat(inputText) {
67
- return runCli(["chat", "--provider", "mock", "--routing", "manual", "--profile", "code"], inputText);
67
+ return runCli(["chat", "--provider", "mock", "--routing", "manual", "--profile", "code", "--headless"], inputText);
68
68
  }
69
69
 
70
70
  function runCli(args, inputText) {
@@ -163,6 +163,7 @@ try {
163
163
  "| Check | Result |",
164
164
  "| --- | --- |",
165
165
  "| `/.dockerenv` | **Present** |",
166
+ "| `data_helper_report.md` | **Found** |",
166
167
  "| Hostname | `abc123` |",
167
168
  ].join("\n")
168
169
  );
@@ -175,6 +176,9 @@ try {
175
176
  if ((renderedMarkdown.match(/Present/g) || []).length !== 1) {
176
177
  throw new Error("terminal markdown renderer duplicated table rows");
177
178
  }
179
+ if (!renderedMarkdown.includes("data_helper_report.md")) {
180
+ throw new Error("terminal markdown renderer corrupted underscores in table cell literals");
181
+ }
178
182
  const renderedMarkdownFence = stripMarkdown(
179
183
  [
180
184
  "```markdown",
@@ -382,6 +386,14 @@ try {
382
386
  if (scsAutoArgs.enableScs !== "auto" || scsAutoArgs.goal !== "fix this project") {
383
387
  throw new Error("--scs auto should consume auto and preserve the task");
384
388
  }
389
+ const unknownOptionArgs = parseArgs(["--provider", "mock", "--allow-web-search", "research this"]);
390
+ if (unknownOptionArgs.unknownOptions[0] !== "--allow-web-search" || unknownOptionArgs.goal !== "research this") {
391
+ throw new Error("unknown option-like arguments before the prompt should be reported, not silently folded into the goal");
392
+ }
393
+ const dashedPromptArgs = parseArgs(["--provider", "mock", "--", "--allow-web-search should be prompt text"]);
394
+ if (dashedPromptArgs.unknownOptions.length || dashedPromptArgs.goal !== "--allow-web-search should be prompt text") {
395
+ throw new Error("explicit -- delimiter should allow prompt text that starts with a dash");
396
+ }
385
397
  const resumeAfterOptions = parseResumeCommandArgs([
386
398
  "web-agent-smoke",
387
399
  "--provider",
@@ -412,6 +424,10 @@ try {
412
424
  ) {
413
425
  throw new Error("resume subcommand should preserve explicit prompt text after --");
414
426
  }
427
+ const resumeUnknownOption = parseResumeCommandArgs(["web-agent-smoke", "--allow-web-search", "continue research"]);
428
+ if (resumeUnknownOption.unknownOptions[0] !== "--allow-web-search" || resumeUnknownOption.prompt !== "continue research") {
429
+ throw new Error("resume subcommand should report unknown option-like arguments before prompt text");
430
+ }
415
431
  const leadingResume = splitResumeCommandArgv(["--provider", "mock", "--routing", "manual", "resume", "latest"]);
416
432
  if (
417
433
  !leadingResume ||
@@ -440,6 +456,7 @@ try {
440
456
  !helpResult.stdout.includes("/auxiliary") ||
441
457
  !helpResult.stdout.includes("/review") ||
442
458
  !helpResult.stdout.includes("/scs") ||
459
+ !helpResult.stdout.includes("/safe") ||
443
460
  helpResult.stdout.includes("helpReview") ||
444
461
  helpResult.stdout.includes("helpRename") ||
445
462
  helpResult.stdout.includes("/enable" + "ss") ||
@@ -480,6 +497,18 @@ try {
480
497
  if (!scsAutoResult.stdout.includes("scs=auto")) {
481
498
  throw new Error("interactive /scs auto did not enable auto mode");
482
499
  }
500
+ const safeResult = await runChat("/safe\n");
501
+ if (!safeResult.stdout.includes("permission=safe") || !safeResult.stdout.includes("writePolicy=prompt")) {
502
+ throw new Error("interactive /safe did not switch to safe permission mode");
503
+ }
504
+ const normalResult = await runChat("/normal\n");
505
+ if (!normalResult.stdout.includes("permission=normal") || !normalResult.stdout.includes("sandbox=docker-workspace")) {
506
+ throw new Error("interactive /normal did not switch to normal permission mode");
507
+ }
508
+ const dangerResult = await runChat("/danger\n");
509
+ if (!dangerResult.stdout.includes("permission=danger") || !dangerResult.stdout.includes("sandbox=host")) {
510
+ throw new Error("interactive /danger did not switch to danger permission mode");
511
+ }
483
512
  const abbreviatedSkillsResult = await runChat("/sk website\n/ex\n");
484
513
  if (abbreviatedSkillsResult.stdout.includes("Unknown command") || !abbreviatedSkillsResult.stdout.includes("website-app")) {
485
514
  throw new Error("interactive slash command prefix did not auto-select the first matching command");
@@ -558,6 +587,7 @@ try {
558
587
  "skills-command",
559
588
  "review-command",
560
589
  "scs-command-toggle",
590
+ "permission-mode-slash-commands",
561
591
  "resume-command-options",
562
592
  "slash-prefix-autoselect",
563
593
  "slash-prefix-canonical-history",