@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 +2 -0
- package/docs/runtime-modes-and-autonomy.md +42 -1
- package/docs/student-committee-supervisor.md +1 -1
- package/package.json +1 -1
- package/public/app.js +9 -0
- package/references/student-committee-supervisor-mode.md +3 -3
- package/scripts/smoke-auto-update.js +4 -0
- package/scripts/smoke-cli-chat.js +7 -2
- package/scripts/smoke-coding-tools.js +46 -0
- package/src/agent-runner.js +34 -0
- package/src/cli.js +2 -2
- package/src/command-policy.js +27 -2
- package/src/interactive-cli.js +14 -9
- package/src/permission-advice.js +199 -0
- package/web.js +1 -0
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.
|
|
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.
|
|
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`.
|
|
288
|
-
- `src/cli.js` adds `--scs`, `--scs auto`, and `--no-scs`.
|
|
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
|
|
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("
|
|
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("/
|
|
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",
|
package/src/agent-runner.js
CHANGED
|
@@ -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 === "--
|
|
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 === "--
|
|
168
|
+
if (arg === "--disable-scs" || arg === "--no-scs") {
|
|
169
169
|
result.enableScs = "off";
|
|
170
170
|
continue;
|
|
171
171
|
}
|
package/src/command-policy.js
CHANGED
|
@@ -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\
|
|
46
|
-
/^wget\
|
|
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))) {
|
package/src/interactive-cli.js
CHANGED
|
@@ -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 === "
|
|
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