@lazyingart/agintiflow 0.20.55 → 0.20.57

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
@@ -133,6 +133,8 @@ aginti --resume <session-id> \
133
133
  "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."
134
134
  ```
135
135
 
136
+ Permission behavior is intentionally consistent: writes inside the current project are allowed through file tools, 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.
137
+
136
138
  ## Real Screenshots
137
139
 
138
140
  | CLI launch | Web app overview |
@@ -42,6 +42,48 @@ Docker package installs are safe when they match the sandbox contract.
42
42
 
43
43
  This is why a task can safely install Python packages in Docker, but cannot keep an interactive tmux server alive inside a one-shot Docker command.
44
44
 
45
+ ## Permission Contract
46
+
47
+ The runtime must be consistent when an action is not permitted. A blocked action should not become a loop of slight command variants, and a failed setup step should not be reported as success just because a stale folder already exists.
48
+
49
+ Current contract:
50
+
51
+ - Workspace file tools may read and write inside the configured project folder when file tools are enabled.
52
+ - Workspace writes outside that folder, secret paths, `.git` internals, and dependency folders such as `node_modules` are blocked.
53
+ - `git clone`, `git fetch`, `git pull --ff-only`, `git push`, `curl`, and `wget` are classified as network operations.
54
+ - Network/setup commands are allowed in `docker-workspace` only when package installs are approved.
55
+ - Host `sudo` and host OS package installs are not automatic. The agent should ask the user to run a manual command or switch to a safer Docker setup.
56
+ - Destructive host shell/git operations need explicit trusted host mode.
57
+
58
+ 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.
59
+
60
+ The common controlled rerun shape is:
61
+
62
+ ```bash
63
+ aginti --resume <session-id> \
64
+ --cwd /path/to/project \
65
+ --sandbox-mode docker-workspace \
66
+ --package-install-policy allow \
67
+ --approve-package-installs \
68
+ --allow-shell \
69
+ --allow-file-tools \
70
+ "Continue after approval and verify the output was created in this run."
71
+ ```
72
+
73
+ For host-only work, use the stricter trusted form deliberately:
74
+
75
+ ```bash
76
+ aginti --resume <session-id> \
77
+ --cwd /path/to/project \
78
+ --sandbox-mode host \
79
+ --package-install-policy allow \
80
+ --approve-package-installs \
81
+ --allow-shell \
82
+ --allow-file-tools \
83
+ --allow-destructive \
84
+ "Continue after trusted host approval. Inspect first and keep unrelated files untouched."
85
+ ```
86
+
45
87
  ## Recommended Defaults
46
88
 
47
89
  Default daily coding:
@@ -103,4 +145,3 @@ The documentation should be maintained as product code:
103
145
  - Add smoke tests when a workflow becomes behavior, not just guidance.
104
146
  - Update `AGINTI.md` for project-specific operating memory.
105
147
  - Treat docs changes as part of the release checklist before publishing npm.
106
-
@@ -22,7 +22,7 @@ Inside interactive chat:
22
22
  /scs status
23
23
  ```
24
24
 
25
- `/scs` without arguments toggles the feature: off becomes on, and on/auto becomes off. The old `/enabless` spelling is accepted only as a compatibility alias for older sessions and scripts.
25
+ `/scs` without arguments toggles the feature: off becomes on, and on/auto becomes off.
26
26
 
27
27
  ## Role Contract
28
28
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lazyingart/agintiflow",
3
- "version": "0.20.55",
3
+ "version": "0.20.57",
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",
package/public/app.js CHANGED
@@ -930,6 +930,14 @@ function renderWorkspacePanel(workspace = lastWorkspace, activity = lastWorkspac
930
930
  const label = blocked ? t("blockedLabel") : t("changedLabel");
931
931
  const path = escapeHtml(item.path || "");
932
932
  const reason = blocked ? `<div class="subtle">${escapeHtml(item.reason || "")}</div>` : "";
933
+ const advice =
934
+ blocked && item.permissionAdvice
935
+ ? `<div class="subtle"><strong>${escapeHtml(item.permissionAdvice.summary || "Permission advice")}</strong>${
936
+ item.permissionAdvice.suggestedCommand
937
+ ? `<br /><code>${escapeHtml(item.permissionAdvice.suggestedCommand)}</code>`
938
+ : ""
939
+ }</div>`
940
+ : "";
933
941
  const diff = item.diff ? `<pre class="change-diff">${renderDiffHtml(item.diff, 1200)}</pre>` : "";
934
942
  const hashes =
935
943
  item.beforeHash || item.afterHash
@@ -944,6 +952,7 @@ function renderWorkspacePanel(workspace = lastWorkspace, activity = lastWorkspac
944
952
  </div>
945
953
  <strong class="change-path">${path}</strong>
946
954
  ${reason}
955
+ ${advice}
947
956
  ${hashes}
948
957
  ${diff}
949
958
  </article>
@@ -284,8 +284,8 @@ Implemented first slice:
284
284
 
285
285
  - `src/scs-controller.js` provides mode normalization, auto activation, committee/student JSON calls, evidence packs, supervisor instructions, tool-failure monitoring, periodic progress review, and final finish gates.
286
286
  - `src/config.js` adds `enableScs`/`scsActive` and switches active execution to the main model role when SCS is active.
287
- - `src/interactive-cli.js` adds `/scs`, `/scs auto`, `/scs on`, `/scs off`, and `/scs status`. `/enabless` remains a compatibility alias only.
288
- - `src/cli.js` adds `--scs`, `--scs auto`, and `--no-scs`. The older `--enabless` flags remain compatibility aliases only.
287
+ - `src/interactive-cli.js` adds `/scs`, `/scs auto`, `/scs on`, `/scs off`, and `/scs status`. Legacy SCS slash aliases have been removed so there is only one interactive command.
288
+ - `src/cli.js` adds `--scs`, `--scs auto`, and `--no-scs`. Older SCS flags have been removed so scripts use the same public SCS vocabulary.
289
289
  - `src/agent-runner.js` persists typed `scs.*` events, saves `scs-phase-001.json`, injects the approved supervisor phase, reviews failed tools, and gates finish.
290
290
 
291
291
  Remaining roadmap:
@@ -318,7 +318,7 @@ Phase 5: auto mode
318
318
  - Should SCS approval be required before any write tool, or only before the first phase starts?
319
319
  - Should the student be allowed to require specific checks, or only accept/reject evidence?
320
320
  - Should committee be one call or multiple candidate calls synthesized into one plan?
321
- - Should `/enabless` remain as a hidden compatibility alias now that `/scs` is the serious public command?
321
+ - Should any future legacy SCS aliases be rejected with a migration hint, or stay as plain unknown commands?
322
322
  - Should SCS decisions be shared through Skill Mesh as learned supervision skills?
323
323
 
324
324
  ## Recommendation
@@ -53,8 +53,10 @@ assert(skipped.skipped === "source-checkout", "source checkout update guard fail
53
53
  const tempHome = await fs.mkdtemp(path.join(os.tmpdir(), "agintiflow-auto-update-"));
54
54
  const previousHome = process.env.AGINTIFLOW_HOME;
55
55
  const previousStartupInterval = process.env.AGINTIFLOW_AUTO_UPDATE_STARTUP_INTERVAL_MS;
56
+ const previousCi = process.env.CI;
56
57
  process.env.AGINTIFLOW_HOME = tempHome;
57
58
  process.env.AGINTIFLOW_AUTO_UPDATE_STARTUP_INTERVAL_MS = String(24 * 60 * 60 * 1000);
59
+ delete process.env.CI;
58
60
  await fs.mkdir(tempHome, { recursive: true });
59
61
  await fs.writeFile(
60
62
  path.join(tempHome, "update-check.json"),
@@ -96,5 +98,7 @@ if (previousHome === undefined) delete process.env.AGINTIFLOW_HOME;
96
98
  else process.env.AGINTIFLOW_HOME = previousHome;
97
99
  if (previousStartupInterval === undefined) delete process.env.AGINTIFLOW_AUTO_UPDATE_STARTUP_INTERVAL_MS;
98
100
  else process.env.AGINTIFLOW_AUTO_UPDATE_STARTUP_INTERVAL_MS = previousStartupInterval;
101
+ if (previousCi === undefined) delete process.env.CI;
102
+ else process.env.CI = previousCi;
99
103
 
100
104
  console.log("auto-update smoke ok");
@@ -209,7 +209,7 @@ try {
209
209
  throw new Error("terminal prompt layout did not localize the empty input hint");
210
210
  }
211
211
  const jaLaunchHeader = buildLaunchHeaderLines({ width: 120, packageVersion: "0.0.0", animated: false, language: "ja" }).join("\n");
212
- if (!jaLaunchHeader.includes("Web ファースト")) {
212
+ if (!jaLaunchHeader.includes("低コストでプロジェクトを理解するエージェント")) {
213
213
  throw new Error("launch header did not localize by language option");
214
214
  }
215
215
  const hugePromptLayout = buildPromptLayout(Array.from({ length: 30 }, (_unused, index) => `line ${index + 1}`).join("\n"), 120, 80, 20);
@@ -294,7 +294,7 @@ try {
294
294
  !helpResult.stdout.includes("/auxiliary") ||
295
295
  !helpResult.stdout.includes("/review") ||
296
296
  !helpResult.stdout.includes("/scs") ||
297
- helpResult.stdout.includes("/enabless") ||
297
+ helpResult.stdout.includes("/enable" + "ss") ||
298
298
  helpResult.stdout.includes(misspelledAuxiliary)
299
299
  ) {
300
300
  throw new Error("interactive help did not expose the expected slash commands");
@@ -315,6 +315,11 @@ try {
315
315
  if (!scsOnResult.stdout.includes("scs=on")) {
316
316
  throw new Error("interactive /scs did not toggle SCS on");
317
317
  }
318
+ const legacyScsAlias = "/enable" + "ss";
319
+ const oldScsAliasResult = await runChat(`${legacyScsAlias}\n/exit\n`);
320
+ if (!oldScsAliasResult.stdout.includes(`Unknown command: ${legacyScsAlias}`) || oldScsAliasResult.stdout.includes("scs=on")) {
321
+ throw new Error(`legacy ${legacyScsAlias} alias should be removed and must not toggle SCS`);
322
+ }
318
323
  const scsOffResult = await runCli(["chat", "--provider", "mock", "--routing", "manual", "--profile", "code", "--scs"], "/scs\n");
319
324
  if (!scsOffResult.stdout.includes("scs=off")) {
320
325
  throw new Error("interactive /scs did not toggle SCS off");
@@ -6,9 +6,11 @@ import { fileURLToPath } from "node:url";
6
6
  import { repairModelMessageHistory, runAgent } from "../src/agent-runner.js";
7
7
  import { resolveRuntimeConfig } from "../src/config.js";
8
8
  import { readCodebaseMap } from "../src/codebase-map.js";
9
+ import { evaluateCommandPolicy } from "../src/command-policy.js";
9
10
  import { engineeringGuidanceForTask, recommendedMaxStepsForTask } from "../src/engineering-guidance.js";
10
11
  import { selectModelRoute } from "../src/model-routing.js";
11
12
  import { listParallelScouts, runParallelScouts, shouldRunParallelScouts } from "../src/parallel-scouts.js";
13
+ import { buildFailedCommandAdvice, buildPermissionAdvice } from "../src/permission-advice.js";
12
14
  import { SessionStore } from "../src/session-store.js";
13
15
  import { searchWeb } from "../src/web-search.js";
14
16
  import { executeWorkspaceTool } from "../src/workspace-tools.js";
@@ -202,6 +204,48 @@ try {
202
204
  guidance.includes("find . -type d -name __pycache__"),
203
205
  "engineering guidance did not include recursive Python transient checks"
204
206
  );
207
+ const dockerWorkspacePolicy = {
208
+ allowShellTool: true,
209
+ useDockerSandbox: true,
210
+ sandboxMode: "docker-workspace",
211
+ packageInstallPolicy: "allow",
212
+ commandCwd: workspace,
213
+ };
214
+ const curlPolicy = evaluateCommandPolicy("curl -s -o /dev/null -w '%{http_code}' https://github.com/lazyingart/AgInTiFlow.git", dockerWorkspacePolicy);
215
+ assert(curlPolicy.allowed, "curl URL probe with flags should be allowed in docker-workspace allow mode");
216
+ assert(curlPolicy.needsNetwork, "curl URL probe with flags was not classified as network");
217
+ const clonePolicy = evaluateCommandPolicy("git clone https://github.com/lazyingart/AgInTiFlow.git", dockerWorkspacePolicy);
218
+ assert(clonePolicy.allowed, "git clone should be allowed in docker-workspace allow mode");
219
+ assert(clonePolicy.needsNetwork, "git clone was not classified as network");
220
+ assert(clonePolicy.writesWorkspace, "git clone was not classified as workspace-writing");
221
+ const unsafeCloneTarget = evaluateCommandPolicy("git clone https://github.com/lazyingart/AgInTiFlow.git ../AgInTiFlow", dockerWorkspacePolicy);
222
+ assert(!unsafeCloneTarget.allowed, "git clone outside the workspace should be blocked");
223
+ const blockedClonePolicy = evaluateCommandPolicy("git clone https://github.com/lazyingart/AgInTiFlow.git", {
224
+ ...dockerWorkspacePolicy,
225
+ packageInstallPolicy: "block",
226
+ });
227
+ assert(!blockedClonePolicy.allowed, "git clone should be blocked when Docker package/network setup is blocked");
228
+ const permissionAdvice = buildPermissionAdvice({
229
+ toolName: "run_command",
230
+ args: { command: "git clone https://github.com/lazyingart/AgInTiFlow.git" },
231
+ guard: blockedClonePolicy,
232
+ config: dockerWorkspacePolicy,
233
+ state: { sessionId: "coding-policy-smoke" },
234
+ });
235
+ assert(permissionAdvice.suggestedCommand.includes("coding-policy-smoke"), "permission advice did not include resume session id");
236
+ assert(permissionAdvice.suggestedCommand.includes("--sandbox-mode docker-workspace"), "permission advice did not suggest docker-workspace recovery");
237
+ const failedNetworkAdvice = buildFailedCommandAdvice({
238
+ args: { command: "git clone https://github.com/lazyingart/AgInTiFlow.git" },
239
+ commandPolicy: clonePolicy,
240
+ commandResult: { ok: false, stderr: "fatal: unable to access 'https://github.com/lazyingart/AgInTiFlow.git/': Could not resolve host: github.com" },
241
+ config: dockerWorkspacePolicy,
242
+ state: { sessionId: "coding-network-smoke" },
243
+ });
244
+ assert(failedNetworkAdvice?.failureKind === "network", "network failure advice was not generated");
245
+ assert(
246
+ failedNetworkAdvice.instruction.includes("Stop and present this blocker"),
247
+ "network failure advice did not tell the model to stop and ask"
248
+ );
205
249
  assert(
206
250
  shouldRunParallelScouts(
207
251
  {
@@ -459,6 +503,8 @@ try {
459
503
  "large_profile_pro_route",
460
504
  "auto_system_pro_route",
461
505
  "auto_engineering_guidance",
506
+ "command_policy_git_clone_network",
507
+ "permission_recovery_advice",
462
508
  "parallel_scout_trigger",
463
509
  "parallel_scout_roster",
464
510
  "parallel_scout_count_clamp",
@@ -27,6 +27,7 @@ import { hostShellOption, platformInfo, platformLabel } from "./platform.js";
27
27
  import { captureTmuxPane, listTmuxSessions, sendTmuxKeys, startTmuxSession } from "./tmux-tools.js";
28
28
  import { languageInstruction } from "./i18n.js";
29
29
  import { flushHousekeeping } from "./housekeeping.js";
30
+ import { buildFailedCommandAdvice, buildPermissionAdvice } from "./permission-advice.js";
30
31
  import {
31
32
  buildSupervisorInstruction,
32
33
  createScsPlan,
@@ -358,6 +359,8 @@ async function createInitialState(config, sessionId) {
358
359
  ? `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.`
359
360
  : `A host shell command tool is available under the configured trust policy on ${platformLabel(platform)}. On native Windows, prefer PowerShell/cmd-compatible commands or switch to WSL/Docker for bash-like toolchains.`
360
361
  : "No shell command tool is available.",
362
+ "Permission contract: current-workspace file writes are allowed through workspace file tools when enabled. Outside-workspace paths, host sudo, host OS package installs, destructive git/shell actions, and blocked network/setup must not be bypassed by retrying variants. If a tool result includes permissionAdvice or suggestedCommand, stop, explain the blocker, and ask the user to approve/rerun that mode or choose a safer workspace-relative path.",
363
+ "If an operation fails but a directory, artifact, or file already exists, treat it as pre-existing unless you have evidence this run created or updated it. Verify expected outputs before claiming success.",
361
364
  config.allowShellTool
362
365
  ? "Host tmux tools are available for long-running terminals: list sessions, capture panes, send safe keys/text, and start detached sessions. Prefer these tools for monitoring long installs/tests/dev servers without blocking; capture before sending input and never send secrets or sudo passwords. Do not start or install tmux inside Docker run_command containers because those containers are short-lived."
363
366
  : "",
@@ -856,12 +859,20 @@ async function executeTool(browserState, toolCall, snapshot, config, store, obse
856
859
  });
857
860
 
858
861
  if (!guard.allowed) {
862
+ const permissionAdvice = buildPermissionAdvice({
863
+ toolName: toolCall.function.name,
864
+ args: safeArgs,
865
+ guard,
866
+ config,
867
+ state,
868
+ });
859
869
  await store.appendEvent("tool.blocked", {
860
870
  toolName: toolCall.function.name,
861
871
  args: safeArgs,
862
872
  reason: guard.reason,
863
873
  category: guard.category,
864
874
  needsApproval: guard.needsApproval,
875
+ permissionAdvice,
865
876
  });
866
877
  observers.event("tool.blocked", {
867
878
  toolName: toolCall.function.name,
@@ -869,6 +880,7 @@ async function executeTool(browserState, toolCall, snapshot, config, store, obse
869
880
  reason: guard.reason,
870
881
  category: guard.category,
871
882
  needsApproval: guard.needsApproval,
883
+ permissionAdvice,
872
884
  });
873
885
  return {
874
886
  ok: false,
@@ -876,6 +888,7 @@ async function executeTool(browserState, toolCall, snapshot, config, store, obse
876
888
  reason: guard.reason,
877
889
  category: guard.category,
878
890
  needsApproval: guard.needsApproval,
891
+ permissionAdvice,
879
892
  toolName: toolCall.function.name,
880
893
  args: safeArgs,
881
894
  };
@@ -1014,17 +1027,27 @@ async function executeTool(browserState, toolCall, snapshot, config, store, obse
1014
1027
  const result = await executeWorkspaceTool(toolCall.function.name, args, config);
1015
1028
  const eventResult = sanitizeToolResult(result);
1016
1029
  if (result.blocked) {
1030
+ const permissionAdvice = buildPermissionAdvice({
1031
+ toolName: toolCall.function.name,
1032
+ args: safeArgs,
1033
+ guard: result,
1034
+ config,
1035
+ state,
1036
+ });
1037
+ result.permissionAdvice = permissionAdvice;
1017
1038
  await store.appendEvent("tool.blocked", {
1018
1039
  toolName: toolCall.function.name,
1019
1040
  args: safeArgs,
1020
1041
  reason: result.reason,
1021
1042
  category: result.category,
1043
+ permissionAdvice,
1022
1044
  });
1023
1045
  observers.event("tool.blocked", {
1024
1046
  toolName: toolCall.function.name,
1025
1047
  args: safeArgs,
1026
1048
  reason: result.reason,
1027
1049
  category: result.category,
1050
+ permissionAdvice,
1028
1051
  });
1029
1052
  return result;
1030
1053
  }
@@ -1049,6 +1072,15 @@ async function executeTool(browserState, toolCall, snapshot, config, store, obse
1049
1072
  await ensureDockerSandboxReady(config, observers);
1050
1073
  }
1051
1074
  const commandResult = await runShellCommand(String(args.command), config, policy);
1075
+ const permissionAdvice = commandResult.ok === false
1076
+ ? buildFailedCommandAdvice({
1077
+ args: safeArgs,
1078
+ commandPolicy: policy,
1079
+ commandResult,
1080
+ config,
1081
+ state,
1082
+ })
1083
+ : null;
1052
1084
  const result = {
1053
1085
  ok: commandResult.ok !== false,
1054
1086
  toolName: "run_command",
@@ -1062,6 +1094,7 @@ async function executeTool(browserState, toolCall, snapshot, config, store, obse
1062
1094
  writesWorkspace: Boolean(policy.writesWorkspace),
1063
1095
  },
1064
1096
  ...commandResult,
1097
+ ...(permissionAdvice ? { permissionAdvice } : {}),
1065
1098
  };
1066
1099
  await store.appendEvent("tool.completed", result);
1067
1100
  observers.event("tool.completed", result);
@@ -1743,6 +1776,7 @@ export async function runAgent(config) {
1743
1776
  commandPolicy: toolResult.commandPolicy,
1744
1777
  blocked: Boolean(toolResult.blocked),
1745
1778
  error: toolResult.error || toolResult.reason || "",
1779
+ permissionAdvice: toolResult.permissionAdvice || null,
1746
1780
  });
1747
1781
  }
1748
1782
 
package/src/cli.js CHANGED
@@ -159,13 +159,13 @@ export function parseArgs(argv) {
159
159
  result.autoUpdate = true;
160
160
  continue;
161
161
  }
162
- if (arg === "--enabless" || arg === "--enable-scs" || arg === "--scs") {
162
+ if (arg === "--enable-scs" || arg === "--scs") {
163
163
  const { mode, consumed } = readOptionalScsMode(argv, i);
164
164
  result.enableScs = mode;
165
165
  if (consumed) i += 1;
166
166
  continue;
167
167
  }
168
- if (arg === "--no-enabless" || arg === "--disable-scs" || arg === "--no-scs") {
168
+ if (arg === "--disable-scs" || arg === "--no-scs") {
169
169
  result.enableScs = "off";
170
170
  continue;
171
171
  }
@@ -42,8 +42,8 @@ const SAFE_WORKSPACE_WRITE_PATTERNS = [/^mkdir\s+-p\s+[-\w./]+$/];
42
42
  const PERMISSION_CHANGE_PATTERNS = [/^(?:sudo\s+)?chmod\s+[-+=,rwxugoXst0-7]+\s+[-\w./]+$/];
43
43
 
44
44
  const NETWORK_FETCH_PATTERNS = [
45
- /^curl\s+(?:-[A-Za-z0-9]*\s+)*https?:\/\/\S+(?:\s+-o\s+[-\w./]+)?$/,
46
- /^wget\s+(?:-[A-Za-z0-9]*\s+)*(?:-O\s+[-\w./]+\s+)?https?:\/\/\S+$/,
45
+ /^curl\b(?=[\s\S]*https?:\/\/\S+)[\s\S]*$/,
46
+ /^wget\b(?=[\s\S]*https?:\/\/\S+)[\s\S]*$/,
47
47
  ];
48
48
 
49
49
  const GIT_WORKFLOW_PATTERNS = [
@@ -163,6 +163,29 @@ function isSafeVirtualWorkspaceDir(value) {
163
163
  return isSafeRelativeDir(normalized.replace(/^\/workspace\//, ""));
164
164
  }
165
165
 
166
+ function classifyGitClone(normalized) {
167
+ const match = normalized.match(
168
+ /^git\s+clone(?:\s+--depth\s+\d+)?(?:\s+--branch\s+[-\w./]+)?\s+(https:\/\/\S+)(?:\s+([-\w./]+))?$/
169
+ );
170
+ if (!match) return null;
171
+
172
+ const target = match[2] || "";
173
+ if (target && !isSafeRelativeDir(target) && !isSafeVirtualWorkspaceDir(target)) {
174
+ return {
175
+ category: "blocked",
176
+ reason: `git clone target must be a safe workspace-relative directory: ${target}`,
177
+ };
178
+ }
179
+
180
+ return {
181
+ category: "git-remote",
182
+ needsNetwork: true,
183
+ writesWorkspace: true,
184
+ virtualWorkspacePath: Boolean(target && isSafeVirtualWorkspaceDir(target)),
185
+ reason: "Git clone writes into the workspace and requires network access.",
186
+ };
187
+ }
188
+
166
189
  function classifySimpleCommand(normalized) {
167
190
  if (ALWAYS_BLOCKED_PATTERNS.some((pattern) => pattern.test(normalized))) {
168
191
  return { category: "blocked", reason: "Command is blocked because it may expose secrets or publish packages." };
@@ -205,6 +228,8 @@ function classifySimpleCommand(normalized) {
205
228
  reason: "Git workflow command. Agent should run git status/diff first and stop on conflicts or divergence.",
206
229
  };
207
230
  }
231
+ const gitCloneClassification = classifyGitClone(normalized);
232
+ if (gitCloneClassification) return gitCloneClassification;
208
233
 
209
234
  const lowered = ` ${normalized.toLowerCase()} `;
210
235
  if (BLOCKED_WRITE_TOKENS.some((part) => lowered.includes(part))) {
@@ -561,6 +561,12 @@ function printCommandOutputLog(data = {}) {
561
561
  for (const line of preview.shown) outputLine(`${color(" | ", ansi.systemBg)} ${line}`);
562
562
  if (preview.hidden > 0) outputLine(`${color(" | ", ansi.systemBg)} ... ${preview.hidden} more line(s) folded`);
563
563
  }
564
+ if (data.permissionAdvice) {
565
+ const advice = data.permissionAdvice;
566
+ outputLine(`${label("perm", ansi.red)} ${compactLine(advice.summary || advice.reason || "Permission advice available.", 104)}`);
567
+ if (advice.suggestedCommand) outputLine(`${color(" | ", ansi.red)} rerun: ${compactLine(advice.suggestedCommand, 120)}`);
568
+ if (advice.trustedHostCommand) outputLine(`${color(" | ", ansi.red)} host: ${compactLine(advice.trustedHostCommand, 120)}`);
569
+ }
564
570
  }
565
571
 
566
572
  function printHeading(text) {
@@ -2805,17 +2811,10 @@ async function handleCommand(line, state, packageDir) {
2805
2811
  printSystemLine(`parallelScouts=${state.allowParallelScouts ? "on" : "off"} count=${state.parallelScoutCount}`);
2806
2812
  return true;
2807
2813
  }
2808
- if (command === "enabless" || command === "scs") {
2814
+ if (command === "scs") {
2809
2815
  const rawMode = String(value || "").trim().toLowerCase();
2810
2816
  const currentMode = normalizeScsMode(state.enableScs || "off");
2811
- const mode =
2812
- rawMode === "toggle" || !rawMode
2813
- ? command === "scs"
2814
- ? currentMode === "off"
2815
- ? "on"
2816
- : "off"
2817
- : "on"
2818
- : normalizeScsMode(value);
2817
+ const mode = rawMode === "toggle" || !rawMode ? (currentMode === "off" ? "on" : "off") : normalizeScsMode(value);
2819
2818
  const knownMode =
2820
2819
  !rawMode ||
2821
2820
  [
@@ -3206,6 +3205,12 @@ async function runPrompt(prompt, state, packageDir) {
3206
3205
  printWorkspaceChange(data);
3207
3206
  } else if (type === "tool.blocked") {
3208
3207
  printStatusEvent(state, "tool_blocked", data.toolName || data.reason || "unknown");
3208
+ if (data.permissionAdvice) {
3209
+ const advice = data.permissionAdvice;
3210
+ outputLine(`${label("perm", ansi.red)} ${compactLine(advice.summary || advice.reason || "Permission advice available.", 104)}`);
3211
+ if (advice.suggestedCommand) outputLine(`${color(" | ", ansi.red)} rerun: ${compactLine(advice.suggestedCommand, 120)}`);
3212
+ if (advice.trustedHostCommand) outputLine(`${color(" | ", ansi.red)} host: ${compactLine(advice.trustedHostCommand, 120)}`);
3213
+ }
3209
3214
  } else if (type === "loop.guard") {
3210
3215
  printStatusEvent(state, "loop_guard", data.toolName || "");
3211
3216
  } else if (type === "conversation.queued_input_applied") {
@@ -0,0 +1,199 @@
1
+ import { redactSensitiveText } from "./redaction.js";
2
+
3
+ const NETWORK_FAILURE_PATTERNS = [
4
+ /could not resolve host/i,
5
+ /temporary failure in name resolution/i,
6
+ /name or service not known/i,
7
+ /network is unreachable/i,
8
+ /failed to connect/i,
9
+ /connection timed out/i,
10
+ /unable to access ['"].*?:/i,
11
+ ];
12
+
13
+ function quoteShell(value = "") {
14
+ const text = String(value || "");
15
+ return `'${text.replace(/'/g, `'\\''`)}'`;
16
+ }
17
+
18
+ function compactLine(value = "", max = 220) {
19
+ const text = redactSensitiveText(String(value || "")).replace(/\s+/g, " ").trim();
20
+ return text.length <= max ? text : `${text.slice(0, max - 3)}...`;
21
+ }
22
+
23
+ function sessionIdFrom(config = {}, state = {}) {
24
+ return state.sessionId || config.resume || config.sessionId || "<session-id>";
25
+ }
26
+
27
+ function cwdFrom(config = {}) {
28
+ return config.commandCwd || process.cwd();
29
+ }
30
+
31
+ function resumeCommand({ config = {}, state = {}, sandboxMode = "docker-workspace", destructive = false, prompt = "Continue the same task from the last blocker. Do not repeat the blocked command without new permission evidence." } = {}) {
32
+ const parts = [
33
+ "aginti",
34
+ "--resume",
35
+ quoteShell(sessionIdFrom(config, state)),
36
+ "--cwd",
37
+ quoteShell(cwdFrom(config)),
38
+ "--sandbox-mode",
39
+ sandboxMode,
40
+ "--package-install-policy",
41
+ "allow",
42
+ "--approve-package-installs",
43
+ "--allow-shell",
44
+ "--allow-file-tools",
45
+ ];
46
+ if (destructive) parts.push("--allow-destructive");
47
+ parts.push(quoteShell(prompt));
48
+ return parts.join(" ");
49
+ }
50
+
51
+ function currentMode(config = {}) {
52
+ return {
53
+ sandboxMode: config.sandboxMode || "",
54
+ packageInstallPolicy: config.packageInstallPolicy || "",
55
+ allowShellTool: Boolean(config.allowShellTool),
56
+ allowFileTools: Boolean(config.allowFileTools),
57
+ allowDestructive: Boolean(config.allowDestructive),
58
+ commandCwd: cwdFrom(config),
59
+ };
60
+ }
61
+
62
+ function adviceForCategory(category = "", { toolName = "", args = {}, config = {}, state = {}, reason = "" } = {}) {
63
+ const command = compactLine(args.command || "");
64
+ const base = {
65
+ category: category || "permission",
66
+ reason: compactLine(reason || "The runtime policy blocked this operation."),
67
+ currentMode: currentMode(config),
68
+ blockedOperation: command ? `run_command: ${command}` : toolName,
69
+ instruction:
70
+ "Stop and present this blocker to the user instead of repeatedly trying variants. Continue only after the user approves a safer mode, changes the workspace, or gives a replacement instruction.",
71
+ };
72
+
73
+ if (category === "workspace-path") {
74
+ return {
75
+ ...base,
76
+ summary:
77
+ "The requested path is outside the configured project workspace or is a protected path. Current-folder writes are allowed; outside-folder writes need the user to change the working directory or choose a trusted run.",
78
+ options: [
79
+ "Refuse: keep all outputs inside the current workspace and ask for a workspace-relative path.",
80
+ "Allow this project: rerun from the intended project folder with --cwd <project-folder>.",
81
+ "Trusted host: only if the user explicitly wants host-wide writes, rerun in host mode with --allow-destructive.",
82
+ ],
83
+ suggestedCommand: resumeCommand({
84
+ config,
85
+ state,
86
+ sandboxMode: "host",
87
+ destructive: true,
88
+ prompt: "Continue after the user approved writing outside the previous workspace. Keep a clear audit trail and do not touch unrelated files.",
89
+ }),
90
+ };
91
+ }
92
+
93
+ if (category === "host-sudo" || category === "system-package-install") {
94
+ return {
95
+ ...base,
96
+ summary:
97
+ "Host sudo and host OS package installs are not run automatically. Use Docker workspace setup when possible, or ask the user to run the exact host command manually.",
98
+ options: [
99
+ "Refuse: report the exact missing dependency and the manual host command.",
100
+ "Allow contained setup: rerun in docker-workspace with package installs approved.",
101
+ "Manual host setup: user runs the sudo command, then resumes the session.",
102
+ ],
103
+ suggestedCommand: resumeCommand({
104
+ config,
105
+ state,
106
+ sandboxMode: "docker-workspace",
107
+ prompt: "Continue using Docker workspace setup where possible. If host sudo is still required, stop and provide the exact manual command.",
108
+ }),
109
+ };
110
+ }
111
+
112
+ if (category === "package-install" || category === "env-setup" || category === "network-fetch" || category === "git-remote") {
113
+ return {
114
+ ...base,
115
+ summary:
116
+ "This operation needs network or environment setup. It is allowed when shell is enabled in docker-workspace mode with package installs approved.",
117
+ options: [
118
+ "Refuse: stop and explain that network/setup is not approved.",
119
+ "Allow this task: rerun with docker-workspace and approved package installs.",
120
+ "Trusted host: use host mode only when the user specifically needs host tools or host network behavior.",
121
+ ],
122
+ suggestedCommand: resumeCommand({
123
+ config,
124
+ state,
125
+ sandboxMode: "docker-workspace",
126
+ prompt: "Continue the same task with network/setup approved in Docker workspace mode. Verify the operation actually creates the expected output before reporting success.",
127
+ }),
128
+ };
129
+ }
130
+
131
+ if (category === "destructive" || category === "permission-change" || category === "general-shell") {
132
+ return {
133
+ ...base,
134
+ summary:
135
+ "This command is broader or destructive enough to require a stronger trust mode. Prefer Docker workspace for project-local work; use trusted host mode only when necessary.",
136
+ options: [
137
+ "Refuse: explain the blocked command and ask for a safer project-local alternative.",
138
+ "Allow contained broad shell: rerun in docker-workspace with package installs approved.",
139
+ "Allow trusted host: rerun in host mode with --allow-destructive when the user accepts host risk.",
140
+ ],
141
+ suggestedCommand: resumeCommand({
142
+ config,
143
+ state,
144
+ sandboxMode: "docker-workspace",
145
+ prompt: "Continue with broad shell access inside Docker workspace. Avoid destructive host actions and verify outputs before finishing.",
146
+ }),
147
+ trustedHostCommand: resumeCommand({
148
+ config,
149
+ state,
150
+ sandboxMode: "host",
151
+ destructive: true,
152
+ prompt: "Continue after the user approved trusted host execution. Inspect first, avoid unrelated files, and keep git status understandable.",
153
+ }),
154
+ };
155
+ }
156
+
157
+ return {
158
+ ...base,
159
+ summary:
160
+ "The current runtime policy blocked this tool. Ask the user whether to keep the task inside the current workspace, approve a stronger mode, or stop.",
161
+ options: [
162
+ "Refuse: report the blocker and do not continue.",
163
+ "Allow this task: rerun with explicit shell/file/package flags appropriate to the blocker.",
164
+ "Change request: ask the user for a safer workspace-relative output or a manual setup step.",
165
+ ],
166
+ suggestedCommand: resumeCommand({ config, state }),
167
+ };
168
+ }
169
+
170
+ export function buildPermissionAdvice({ toolName = "", args = {}, guard = {}, config = {}, state = {}, reason = "" } = {}) {
171
+ const category = guard.category || "permission";
172
+ return adviceForCategory(category, {
173
+ toolName,
174
+ args,
175
+ config,
176
+ state,
177
+ reason: reason || guard.reason || "",
178
+ });
179
+ }
180
+
181
+ export function looksLikeNetworkFailure(result = {}) {
182
+ const text = `${result.stdout || ""}\n${result.stderr || ""}`;
183
+ return NETWORK_FAILURE_PATTERNS.some((pattern) => pattern.test(text));
184
+ }
185
+
186
+ export function buildFailedCommandAdvice({ args = {}, commandPolicy = {}, commandResult = {}, config = {}, state = {} } = {}) {
187
+ if (!looksLikeNetworkFailure(commandResult)) return null;
188
+ return {
189
+ ...adviceForCategory(commandPolicy.needsNetwork ? "network-fetch" : "general-shell", {
190
+ toolName: "run_command",
191
+ args,
192
+ config,
193
+ state,
194
+ reason:
195
+ "The command failed with a network-resolution/connectivity error. Do not report success unless a later check proves the expected artifact was created in this run.",
196
+ }),
197
+ failureKind: "network",
198
+ };
199
+ }
package/web.js CHANGED
@@ -527,6 +527,7 @@ async function collectWorkspaceActivity(limit = 24) {
527
527
  path: event.data?.args?.path || "",
528
528
  reason: event.data?.reason || "",
529
529
  category: event.data?.category || "",
530
+ permissionAdvice: event.data?.permissionAdvice || null,
530
531
  });
531
532
  }
532
533
  }