@lazyingart/agintiflow 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -6
- package/docs/real-deepseek-capabilities.md +2 -2
- package/package.json +1 -1
- package/scripts/real-deepseek-capabilities.js +3 -3
- package/scripts/smoke-capabilities.js +9 -1
- package/scripts/smoke-web-api.js +3 -0
- package/src/agent-runner.js +4 -4
- package/src/capabilities.js +38 -0
- package/src/cli.js +6 -1
- package/src/command-policy.js +100 -12
- package/src/config.js +1 -1
- package/src/docker-sandbox.js +1 -1
- package/src/model-client.js +1 -1
- package/src/task-profiles.js +2 -2
package/README.md
CHANGED
|
@@ -72,6 +72,7 @@ aginti sessions list
|
|
|
72
72
|
aginti sessions show <session-id>
|
|
73
73
|
aginti resume <session-id> "continue with a short follow-up"
|
|
74
74
|
aginti --profile code --provider mock --routing manual "Create notes/hello.md"
|
|
75
|
+
aginti --allow-shell --sandbox-mode docker-workspace --approve-package-installs "set up and test this project"
|
|
75
76
|
```
|
|
76
77
|
|
|
77
78
|
Run from a source checkout:
|
|
@@ -181,12 +182,12 @@ AgInTiFlow is intentionally conservative:
|
|
|
181
182
|
- Shell commands are disabled unless the shell tool is enabled.
|
|
182
183
|
- Workspace file tools stay inside `commandCwd` and block `.env`, secret-like paths, `.git`, node_modules writes, absolute escapes, binary files, and huge files.
|
|
183
184
|
- File writes record before/after SHA-256 hashes and compact redacted diffs.
|
|
184
|
-
- Guarded shell mode
|
|
185
|
+
- Guarded shell mode is policy-based: normal inspection/tests are available, Docker workspace mode can run broader setup/network commands when package installs are approved, and host privileged/destructive work requires explicit trust.
|
|
185
186
|
- Docker read-only mode mounts the workspace read-only and disables container network access.
|
|
186
187
|
- Docker workspace-write mode is the web UI default so plot, PDF, and test outputs can be written inside the mounted workspace.
|
|
187
|
-
- Docker workspace-write mode is
|
|
188
|
+
- Docker workspace-write mode is the preferred place for approved package or environment setup, including `apt-get`, `npm`, `pip`, `curl`, and `wget` workflows inside the container.
|
|
188
189
|
- Package installs default to `prompt`, so npm/pip/conda/venv setup is blocked until explicitly approved.
|
|
189
|
-
- NPM publishing, npm token commands,
|
|
190
|
+
- NPM publishing, npm token commands, API-key reads, and credential files stay blocked. Broader shell commands require either Docker workspace mode with package policy `allow`, or explicit host trust.
|
|
190
191
|
- NPM tokens and API keys are redacted from tool logs and API responses.
|
|
191
192
|
- Every tool request and result is written to structured logs.
|
|
192
193
|
|
|
@@ -311,9 +312,9 @@ The setup script idempotently builds `agintiflow-sandbox:latest` from `docker/sa
|
|
|
311
312
|
|
|
312
313
|
| Mode | Workspace mount | Network | Intended use |
|
|
313
314
|
| --- | --- | --- | --- |
|
|
314
|
-
| `host` | local process | host network |
|
|
315
|
+
| `host` | local process | host network | direct project work; privileged/destructive commands require explicit trust |
|
|
315
316
|
| `docker-readonly` | read-only `/workspace` | none | safe coding inspection and tests that do not write files |
|
|
316
|
-
| `docker-workspace` | writable `/workspace` | none by default, enabled
|
|
317
|
+
| `docker-workspace` | writable `/workspace` | none by default, enabled for approved package/setup/network commands | web UI default for environment setup, plotting, LaTeX/PDF, dependency installs, and broad toolchain work inside the mounted project |
|
|
317
318
|
|
|
318
319
|
Package policy values:
|
|
319
320
|
|
|
@@ -321,7 +322,7 @@ Package policy values:
|
|
|
321
322
|
| --- | --- |
|
|
322
323
|
| `block` | Always block npm/pip/conda/venv setup. |
|
|
323
324
|
| `prompt` | Return a clear approval-required error; the UI can switch to approved. |
|
|
324
|
-
| `allow` | Permit
|
|
325
|
+
| `allow` | Permit package/setup commands. Docker workspace mode also allows broader shell/network commands while keeping secrets and npm publishing blocked. |
|
|
325
326
|
|
|
326
327
|
Toolchain commands such as `python3 plot.py`, `latexmk -pdf paper.tex`, and `pdflatex -interaction=nonstopmode -halt-on-error paper.tex` are allowlisted only when the shell tool is enabled. In Docker mode they require `docker-workspace` because they write outputs back to `/workspace`. File and canvas tools accept both normal relative paths and Docker virtual paths like `/workspace/report.pdf`, while other absolute host paths remain blocked.
|
|
327
328
|
|
|
@@ -43,8 +43,8 @@ AGINTIFLOW_REAL_DEEPSEEK=1 AGINTIFLOW_REAL_CASES=flash,pro npm run real:deepseek
|
|
|
43
43
|
- `flash`: DeepSeek v4 flash creates a dependency-free Node/HTML app with `node:test` tests and runs `npm --prefix <app> test` when safe.
|
|
44
44
|
- `pro`: DeepSeek v4 pro resumes/improves the app and expands tests.
|
|
45
45
|
- `latex`: creates `.tex` source and compiles only if a TeX toolchain is available; otherwise writes an honest setup artifact.
|
|
46
|
-
- `website`: creates a
|
|
47
|
-
- `maintenance`:
|
|
46
|
+
- `website`: creates a website test sample and runs checks with the configured shell/package policy.
|
|
47
|
+
- `maintenance`: verifies the agent can prepare project-local maintenance work and use Docker/package policy for broader setup commands without exposing secrets.
|
|
48
48
|
- `aaps`: creates a project-local `.aaps` sample and notes for `@lazyingart/aaps` workflows without publishing.
|
|
49
49
|
- `web`: when `AGINTIFLOW_REAL_WEB_BASE_URL` is set, starts a web run and verifies CLI/web session sharing.
|
|
50
50
|
|
package/package.json
CHANGED
|
@@ -308,7 +308,7 @@ if (cases.has("website")) {
|
|
|
308
308
|
const websiteDir = `round9/website-test-${stamp}`;
|
|
309
309
|
const run = await runCliCase(
|
|
310
310
|
"website-test",
|
|
311
|
-
`Create a small website under ${websiteDir} with a simple local test or check file. Run
|
|
311
|
+
`Create a small website under ${websiteDir} with a simple local test or check file. Run useful checks under the configured policy.`,
|
|
312
312
|
{ routingMode: "fast", taskProfile: "website", maxSteps: 24 }
|
|
313
313
|
);
|
|
314
314
|
report.runs.push(run);
|
|
@@ -333,12 +333,12 @@ if (cases.has("maintenance")) {
|
|
|
333
333
|
const maintenanceDir = `maintenance/round9-${stamp}`;
|
|
334
334
|
const run = await runCliCase(
|
|
335
335
|
"maintenance-plan",
|
|
336
|
-
`Create project-local
|
|
336
|
+
`Create project-local maintenance plans and scripts under ${maintenanceDir} for Miniforge/conda, R, Python tooling, CmdStan/CmdStanR, and PyStan. Use Docker/package policy for setup checks when useful.`,
|
|
337
337
|
{
|
|
338
338
|
routingMode: "complex",
|
|
339
339
|
taskProfile: "maintenance",
|
|
340
340
|
sandboxMode: "docker-workspace",
|
|
341
|
-
packageInstallPolicy: "
|
|
341
|
+
packageInstallPolicy: "allow",
|
|
342
342
|
maxSteps: 24,
|
|
343
343
|
}
|
|
344
344
|
);
|
|
@@ -54,6 +54,14 @@ try {
|
|
|
54
54
|
capabilities.maintenancePolicy.some((check) => check.command.startsWith("sudo") && !check.allowed),
|
|
55
55
|
"sudo maintenance command was not blocked"
|
|
56
56
|
);
|
|
57
|
+
assert(
|
|
58
|
+
capabilities.trustedDockerPolicy.some((check) => check.command.startsWith("apt-get install") && check.allowed),
|
|
59
|
+
"trusted Docker policy did not allow apt-get install"
|
|
60
|
+
);
|
|
61
|
+
assert(
|
|
62
|
+
capabilities.trustedDockerPolicy.some((check) => check.command.startsWith("wget") && check.allowed),
|
|
63
|
+
"trusted Docker policy did not allow wget"
|
|
64
|
+
);
|
|
57
65
|
|
|
58
66
|
const doctor = JSON.parse(await runCli(["doctor", "--capabilities", "--json"]));
|
|
59
67
|
assert(doctor.project.root === tempRoot, "doctor --capabilities used the wrong project root");
|
|
@@ -63,7 +71,7 @@ try {
|
|
|
63
71
|
{
|
|
64
72
|
ok: true,
|
|
65
73
|
projectRoot: tempRoot,
|
|
66
|
-
checks: ["capabilities-cli", "doctor-capabilities", "maintenance-policy"],
|
|
74
|
+
checks: ["capabilities-cli", "doctor-capabilities", "maintenance-policy", "trusted-docker-policy"],
|
|
67
75
|
},
|
|
68
76
|
null,
|
|
69
77
|
2
|
package/scripts/smoke-web-api.js
CHANGED
|
@@ -90,6 +90,9 @@ try {
|
|
|
90
90
|
if (!capabilities.checks.some((check) => check.name === "npm-prefix-test-policy")) {
|
|
91
91
|
throw new Error("capability endpoint did not include command policy checks");
|
|
92
92
|
}
|
|
93
|
+
if (!capabilities.trustedDockerPolicy?.some((check) => check.command.startsWith("apt-get install") && check.allowed)) {
|
|
94
|
+
throw new Error("capability endpoint did not report trusted Docker package policy");
|
|
95
|
+
}
|
|
93
96
|
const savedKey = await fetchJson("/api/keys/deepseek", {
|
|
94
97
|
method: "POST",
|
|
95
98
|
headers: { "Content-Type": "application/json" },
|
package/src/agent-runner.js
CHANGED
|
@@ -144,8 +144,8 @@ function createInitialState(config, sessionId) {
|
|
|
144
144
|
"Avoid destructive actions, purchases, account changes, and sensitive workflows.",
|
|
145
145
|
config.allowShellTool
|
|
146
146
|
? config.useDockerSandbox
|
|
147
|
-
? `A shell command tool is available inside Docker sandbox mode ${config.sandboxMode}.
|
|
148
|
-
: "A
|
|
147
|
+
? `A shell command tool is available inside Docker sandbox mode ${config.sandboxMode}. Docker workspace mode with approved package installs supports broader setup and network commands.`
|
|
148
|
+
: "A host shell command tool is available under the configured trust policy."
|
|
149
149
|
: "No shell command tool is available.",
|
|
150
150
|
config.allowFileTools
|
|
151
151
|
? `Workspace file tools are available in ${config.commandCwd}: list_files, read_file, search_files, write_file, and apply_patch. Always use workspace-relative paths such as plot_fx.svg or docs/report.tex, never absolute host paths. Secret paths, .git internals, node_modules writes, and huge files are blocked.`
|
|
@@ -158,7 +158,7 @@ function createInitialState(config, sessionId) {
|
|
|
158
158
|
"For visual-output requests such as draw, plot, graph, chart, diagram, figure, image, or visualization, proactively publish a canvas artifact even when the user does not mention canvas. If workspace file tools are enabled, prefer creating a small SVG or markdown artifact and call send_to_canvas with selected=true.",
|
|
159
159
|
"Work like a practical coding agent: inspect when useful, edit with file tools, run safe checks when they add confidence, and keep outputs inside the workspace.",
|
|
160
160
|
"Use the canvas tunnel for outputs the user would likely want to inspect visually, such as figures, PDFs, screenshots, images, important markdown, or generated files.",
|
|
161
|
-
"For environment or system-maintenance work,
|
|
161
|
+
"For environment or system-maintenance work, use the configured sandbox and package policy; Docker workspace mode is the preferred place for installs and toolchain setup.",
|
|
162
162
|
"When the requested outcome is complete and a useful check has passed or been honestly skipped, stop and call finish.",
|
|
163
163
|
"When done, call finish with a concise result.",
|
|
164
164
|
].join(" "),
|
|
@@ -182,7 +182,7 @@ function createInitialState(config, sessionId) {
|
|
|
182
182
|
"Canvas/artifacts tunnel: available through send_to_canvas for optional frontend rendering.",
|
|
183
183
|
"Visual-output requests should produce a canvas artifact without requiring the user to ask for canvas explicitly.",
|
|
184
184
|
"Use file, shell, browser, canvas, and wrapper tools when they are useful; choose the workflow from the user's request.",
|
|
185
|
-
"
|
|
185
|
+
"Use the configured sandbox and package policy for environment or system-maintenance work.",
|
|
186
186
|
]
|
|
187
187
|
.filter(Boolean)
|
|
188
188
|
.join("\n"),
|
package/src/capabilities.js
CHANGED
|
@@ -64,6 +64,39 @@ function maintenancePolicyChecks(config) {
|
|
|
64
64
|
});
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
+
function trustedDockerPolicyChecks(config) {
|
|
68
|
+
const dockerConfig = {
|
|
69
|
+
...config,
|
|
70
|
+
allowShellTool: true,
|
|
71
|
+
allowDestructive: false,
|
|
72
|
+
sandboxMode: "docker-workspace",
|
|
73
|
+
useDockerSandbox: true,
|
|
74
|
+
packageInstallPolicy: "allow",
|
|
75
|
+
};
|
|
76
|
+
const sampleCommands = [
|
|
77
|
+
"apt-get update",
|
|
78
|
+
"apt-get install -y curl wget",
|
|
79
|
+
"wget https://example.com/file.txt",
|
|
80
|
+
"curl -fsSL https://example.com/file.txt -o downloads/file.txt",
|
|
81
|
+
"npm install lodash",
|
|
82
|
+
"python3 -m pip install requests",
|
|
83
|
+
];
|
|
84
|
+
|
|
85
|
+
return sampleCommands.map((command) => {
|
|
86
|
+
const policy = evaluateCommandPolicy(command, dockerConfig);
|
|
87
|
+
return {
|
|
88
|
+
command,
|
|
89
|
+
allowed: Boolean(policy.allowed),
|
|
90
|
+
category: policy.category || classifyCommand(command).category,
|
|
91
|
+
reason: policy.reason || "",
|
|
92
|
+
needsNetwork: Boolean(policy.needsNetwork),
|
|
93
|
+
requiresDockerRoot: Boolean(policy.requiresDockerRoot),
|
|
94
|
+
sandboxMode: policy.sandboxMode,
|
|
95
|
+
packageInstallPolicy: policy.packageInstallPolicy,
|
|
96
|
+
};
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
67
100
|
export async function buildCapabilityReport(projectRoot, packageVersion, config) {
|
|
68
101
|
const paths = projectPaths(projectRoot);
|
|
69
102
|
const keyStatus = providerKeyStatus(projectRoot);
|
|
@@ -155,6 +188,7 @@ export async function buildCapabilityReport(projectRoot, packageVersion, config)
|
|
|
155
188
|
},
|
|
156
189
|
checks,
|
|
157
190
|
maintenancePolicy: maintenancePolicyChecks(config),
|
|
191
|
+
trustedDockerPolicy: trustedDockerPolicyChecks(config),
|
|
158
192
|
sessions,
|
|
159
193
|
actionableSetup: checks
|
|
160
194
|
.filter((check) => !check.ok && check.setup)
|
|
@@ -184,6 +218,10 @@ export function printCapabilityReport(report) {
|
|
|
184
218
|
const suffix = check.version ? ` ${check.version}` : check.reason ? ` ${check.reason}` : check.hint ? ` ${check.hint}` : "";
|
|
185
219
|
console.log(`${check.ok ? "OK" : "MISS"} ${check.name}${suffix}`);
|
|
186
220
|
}
|
|
221
|
+
if (report.trustedDockerPolicy?.length) {
|
|
222
|
+
const allowed = report.trustedDockerPolicy.filter((item) => item.allowed).length;
|
|
223
|
+
console.log(`trustedDockerPolicy=${allowed}/${report.trustedDockerPolicy.length} allowed when sandbox=docker-workspace packageInstalls=allow`);
|
|
224
|
+
}
|
|
187
225
|
if (report.actionableSetup.length > 0) {
|
|
188
226
|
console.log("setup:");
|
|
189
227
|
for (const item of report.actionableSetup) {
|
package/src/cli.js
CHANGED
|
@@ -41,6 +41,7 @@ export function parseArgs(argv) {
|
|
|
41
41
|
allowShellTool: undefined,
|
|
42
42
|
allowFileTools: undefined,
|
|
43
43
|
allowWrapperTools: undefined,
|
|
44
|
+
allowDestructive: undefined,
|
|
44
45
|
preferredWrapper: "",
|
|
45
46
|
taskProfile: "",
|
|
46
47
|
useDockerSandbox: undefined,
|
|
@@ -132,6 +133,10 @@ export function parseArgs(argv) {
|
|
|
132
133
|
result.allowShellTool = true;
|
|
133
134
|
continue;
|
|
134
135
|
}
|
|
136
|
+
if (arg === "--allow-destructive" || arg === "--trusted-host-shell") {
|
|
137
|
+
result.allowDestructive = true;
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
135
140
|
if (arg === "--allow-file-tools") {
|
|
136
141
|
result.allowFileTools = true;
|
|
137
142
|
continue;
|
|
@@ -429,7 +434,7 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
429
434
|
|
|
430
435
|
if (!args.goal && !args.resume) {
|
|
431
436
|
console.error(
|
|
432
|
-
'Usage: aginti-cli web [--port 3210] OR aginti-cli [--routing smart|fast|complex|manual] [--provider deepseek|openai|mock] [--sandbox-mode host|docker-readonly|docker-workspace] [--package-install-policy block|prompt|allow] [--allow-shell] [--allow-file-tools|--no-file-tools] [--allow-wrappers --wrapper codex] [--sandbox-status|--sandbox-preflight] "your task"'
|
|
437
|
+
'Usage: aginti-cli web [--port 3210] OR aginti-cli [--routing smart|fast|complex|manual] [--provider deepseek|openai|mock] [--sandbox-mode host|docker-readonly|docker-workspace] [--package-install-policy block|prompt|allow] [--approve-package-installs] [--allow-shell] [--allow-destructive] [--allow-file-tools|--no-file-tools] [--allow-wrappers --wrapper codex] [--sandbox-status|--sandbox-preflight] "your task"'
|
|
433
438
|
);
|
|
434
439
|
process.exit(1);
|
|
435
440
|
}
|
package/src/command-policy.js
CHANGED
|
@@ -40,6 +40,11 @@ const TEST_PATTERNS = [
|
|
|
40
40
|
|
|
41
41
|
const SAFE_WORKSPACE_WRITE_PATTERNS = [/^mkdir\s+-p\s+[-\w./]+$/];
|
|
42
42
|
|
|
43
|
+
const NETWORK_FETCH_PATTERNS = [
|
|
44
|
+
/^curl\s+(?:-[A-Za-z0-9]*\s+)*https?:\/\/\S+(?:\s+-o\s+[-\w./]+)?$/,
|
|
45
|
+
/^wget\s+(?:-[A-Za-z0-9]*\s+)*(?:-O\s+[-\w./]+\s+)?https?:\/\/\S+$/,
|
|
46
|
+
];
|
|
47
|
+
|
|
43
48
|
const TOOLCHAIN_PATTERNS = [
|
|
44
49
|
/^python(?:3)?\s+[-\w./]+\.py(?:\s+[-\w./:=]+)*$/,
|
|
45
50
|
/^latexmk\s+(?=[-\w./=\s]*-pdf\b)(?:(?:-cd|-pdf|-interaction=nonstopmode|-halt-on-error|-output-directory=[-\w./]+)\s+)+[-\w./]+\.tex$/,
|
|
@@ -48,12 +53,24 @@ const TOOLCHAIN_PATTERNS = [
|
|
|
48
53
|
|
|
49
54
|
const PACKAGE_INSTALL_PATTERNS = [
|
|
50
55
|
/^npm\s+ci$/,
|
|
51
|
-
/^npm\s+install
|
|
56
|
+
/^npm\s+install(?:\s+[-@\w./:=]+)*$/,
|
|
52
57
|
/^pnpm\s+install$/,
|
|
58
|
+
/^pnpm\s+add(?:\s+[-@\w./:=]+)+$/,
|
|
53
59
|
/^yarn\s+install$/,
|
|
60
|
+
/^yarn\s+add(?:\s+[-@\w./:=]+)+$/,
|
|
54
61
|
/^python(?:3)?\s+-m\s+pip\s+install\s+-r\s+[-\w./]+$/,
|
|
62
|
+
/^python(?:3)?\s+-m\s+pip\s+install(?:\s+[-@\w./:=]+)+$/,
|
|
55
63
|
/^pip(?:3)?\s+install\s+-r\s+[-\w./]+$/,
|
|
64
|
+
/^pip(?:3)?\s+install(?:\s+[-@\w./:=]+)+$/,
|
|
65
|
+
/^uv\s+(sync|pip\s+install)(?:\s+[-@\w./:=]+)*$/,
|
|
56
66
|
/^conda\s+env\s+(create|update)\s+-f\s+[-\w./]+$/,
|
|
67
|
+
/^conda\s+install(?:\s+[-@\w./:=]+)+$/,
|
|
68
|
+
];
|
|
69
|
+
|
|
70
|
+
const SYSTEM_PACKAGE_INSTALL_PATTERNS = [
|
|
71
|
+
/^(?:sudo\s+)?apt(?:-get)?\s+update$/,
|
|
72
|
+
/^(?:sudo\s+)?apt(?:-get)?\s+install(?:\s+-y)?(?:\s+[-@\w.+:=]+)+$/,
|
|
73
|
+
/^apk\s+add(?:\s+--no-cache)?(?:\s+[-@\w.+:=]+)+$/,
|
|
57
74
|
];
|
|
58
75
|
|
|
59
76
|
const ENV_SETUP_PATTERNS = [
|
|
@@ -64,13 +81,10 @@ const ENV_SETUP_PATTERNS = [
|
|
|
64
81
|
|
|
65
82
|
const BLOCKED_SHELL_TOKENS = ["&&", "||", ";", "|", ">", "<", "$(", "`"];
|
|
66
83
|
const BLOCKED_WRITE_TOKENS = [
|
|
67
|
-
"sudo ",
|
|
68
84
|
" rm",
|
|
69
85
|
" mv",
|
|
70
|
-
" cp",
|
|
71
86
|
" chmod",
|
|
72
87
|
" chown",
|
|
73
|
-
" mkdir",
|
|
74
88
|
" rmdir",
|
|
75
89
|
" touch",
|
|
76
90
|
" tee",
|
|
@@ -83,8 +97,6 @@ const BLOCKED_WRITE_TOKENS = [
|
|
|
83
97
|
"git switch",
|
|
84
98
|
"git reset",
|
|
85
99
|
"git clean",
|
|
86
|
-
"curl ",
|
|
87
|
-
"wget ",
|
|
88
100
|
];
|
|
89
101
|
|
|
90
102
|
const ALWAYS_BLOCKED_PATTERNS = [
|
|
@@ -98,6 +110,13 @@ const ALWAYS_BLOCKED_PATTERNS = [
|
|
|
98
110
|
/DEEPSEEK_API_KEY\s*=/i,
|
|
99
111
|
];
|
|
100
112
|
|
|
113
|
+
const SENSITIVE_COMMAND_PATTERNS = [
|
|
114
|
+
/(^|[\s./])\.env(\s|$|[./])/i,
|
|
115
|
+
/(^|[\s./])\.npmrc(\s|$|[./])/i,
|
|
116
|
+
/^(env|printenv)(\s|$)/i,
|
|
117
|
+
/(api[_-]?key|auth[_-]?token|npm[_-]?token|_authToken|bearer\s+[A-Za-z0-9._-]+)/i,
|
|
118
|
+
];
|
|
119
|
+
|
|
101
120
|
function normalizePolicy(value, allowed, fallback) {
|
|
102
121
|
return allowed.includes(value) ? value : fallback;
|
|
103
122
|
}
|
|
@@ -130,10 +149,18 @@ function classifySimpleCommand(normalized) {
|
|
|
130
149
|
if (ALWAYS_BLOCKED_PATTERNS.some((pattern) => pattern.test(normalized))) {
|
|
131
150
|
return { category: "blocked", reason: "Command is blocked because it may expose secrets or publish packages." };
|
|
132
151
|
}
|
|
152
|
+
if (SENSITIVE_COMMAND_PATTERNS.some((pattern) => pattern.test(normalized))) {
|
|
153
|
+
return { category: "blocked", reason: "Command is blocked because it references secrets or credential files." };
|
|
154
|
+
}
|
|
133
155
|
|
|
134
156
|
const lowered = ` ${normalized.toLowerCase()} `;
|
|
135
157
|
if (BLOCKED_SHELL_TOKENS.some((part) => normalized.includes(part))) {
|
|
136
|
-
return {
|
|
158
|
+
return {
|
|
159
|
+
category: "general-shell",
|
|
160
|
+
needsNetwork: true,
|
|
161
|
+
writesWorkspace: true,
|
|
162
|
+
reason: `Command uses general shell syntax: ${normalized}`,
|
|
163
|
+
};
|
|
137
164
|
}
|
|
138
165
|
if (matchAny(SAFE_WORKSPACE_WRITE_PATTERNS, normalized)) {
|
|
139
166
|
const target = normalized.replace(/^mkdir\s+-p\s+/, "");
|
|
@@ -144,7 +171,12 @@ function classifySimpleCommand(normalized) {
|
|
|
144
171
|
return { category: "workspace-write", needsNetwork: false, writesWorkspace: true, virtualWorkspacePath };
|
|
145
172
|
}
|
|
146
173
|
if (BLOCKED_WRITE_TOKENS.some((part) => lowered.includes(part))) {
|
|
147
|
-
return {
|
|
174
|
+
return {
|
|
175
|
+
category: "destructive",
|
|
176
|
+
needsNetwork: false,
|
|
177
|
+
writesWorkspace: true,
|
|
178
|
+
reason: `Command contains a write-capable or destructive token: ${normalized}`,
|
|
179
|
+
};
|
|
148
180
|
}
|
|
149
181
|
|
|
150
182
|
if (matchAny(READ_ONLY_PATTERNS, normalized)) {
|
|
@@ -156,6 +188,12 @@ function classifySimpleCommand(normalized) {
|
|
|
156
188
|
if (matchAny(TOOLCHAIN_PATTERNS, normalized)) {
|
|
157
189
|
return { category: "toolchain", needsNetwork: false, writesWorkspace: true };
|
|
158
190
|
}
|
|
191
|
+
if (matchAny(NETWORK_FETCH_PATTERNS, normalized)) {
|
|
192
|
+
return { category: "network-fetch", needsNetwork: true, writesWorkspace: /(\s-o\s|\s-O\s)/.test(normalized) };
|
|
193
|
+
}
|
|
194
|
+
if (matchAny(SYSTEM_PACKAGE_INSTALL_PATTERNS, normalized)) {
|
|
195
|
+
return { category: "system-package-install", needsNetwork: true, writesWorkspace: false, requiresDockerRoot: true };
|
|
196
|
+
}
|
|
159
197
|
if (matchAny(PACKAGE_INSTALL_PATTERNS, normalized)) {
|
|
160
198
|
return { category: "package-install", needsNetwork: true, writesWorkspace: true };
|
|
161
199
|
}
|
|
@@ -163,7 +201,12 @@ function classifySimpleCommand(normalized) {
|
|
|
163
201
|
return { category: "env-setup", needsNetwork: false, writesWorkspace: true };
|
|
164
202
|
}
|
|
165
203
|
|
|
166
|
-
return {
|
|
204
|
+
return {
|
|
205
|
+
category: "general-shell",
|
|
206
|
+
needsNetwork: false,
|
|
207
|
+
writesWorkspace: false,
|
|
208
|
+
reason: `Command is outside the narrow allowlist and requires a trusted shell policy: ${normalized}`,
|
|
209
|
+
};
|
|
167
210
|
}
|
|
168
211
|
|
|
169
212
|
function classifyCdCommand(normalized) {
|
|
@@ -190,6 +233,10 @@ export function evaluateCommandPolicy(command, config) {
|
|
|
190
233
|
const classification = classifyCommand(command);
|
|
191
234
|
const sandboxMode = normalizeSandboxMode(config.sandboxMode);
|
|
192
235
|
const packageInstallPolicy = normalizePackageInstallPolicy(config.packageInstallPolicy);
|
|
236
|
+
const dockerWorkspace = sandboxMode === "docker-workspace";
|
|
237
|
+
const packageInstallsAllowed = packageInstallPolicy === "allow";
|
|
238
|
+
const trustedDockerShell = dockerWorkspace && packageInstallsAllowed;
|
|
239
|
+
const trustedHostShell = sandboxMode === "host" && Boolean(config.allowDestructive);
|
|
193
240
|
|
|
194
241
|
if (classification.category === "blocked") {
|
|
195
242
|
return { allowed: false, ...classification, sandboxMode, packageInstallPolicy };
|
|
@@ -215,7 +262,11 @@ export function evaluateCommandPolicy(command, config) {
|
|
|
215
262
|
};
|
|
216
263
|
}
|
|
217
264
|
|
|
218
|
-
if (
|
|
265
|
+
if (
|
|
266
|
+
classification.category === "package-install" ||
|
|
267
|
+
classification.category === "env-setup" ||
|
|
268
|
+
classification.category === "system-package-install"
|
|
269
|
+
) {
|
|
219
270
|
if (packageInstallPolicy !== "allow") {
|
|
220
271
|
return {
|
|
221
272
|
allowed: false,
|
|
@@ -230,17 +281,53 @@ export function evaluateCommandPolicy(command, config) {
|
|
|
230
281
|
};
|
|
231
282
|
}
|
|
232
283
|
|
|
233
|
-
if (
|
|
284
|
+
if (classification.category === "system-package-install" && sandboxMode === "host" && !config.allowDestructive) {
|
|
234
285
|
return {
|
|
235
286
|
allowed: false,
|
|
236
287
|
category: classification.category,
|
|
237
|
-
reason: "
|
|
288
|
+
reason: "Host system package installs require Allow destructive actions. Prefer Docker workspace mode.",
|
|
238
289
|
sandboxMode,
|
|
239
290
|
packageInstallPolicy,
|
|
240
291
|
};
|
|
241
292
|
}
|
|
242
293
|
}
|
|
243
294
|
|
|
295
|
+
if (classification.category === "general-shell" && !trustedDockerShell && !trustedHostShell) {
|
|
296
|
+
return {
|
|
297
|
+
allowed: false,
|
|
298
|
+
...classification,
|
|
299
|
+
needsApproval: true,
|
|
300
|
+
reason:
|
|
301
|
+
sandboxMode === "host"
|
|
302
|
+
? "General shell commands on the host require Allow destructive actions. Prefer Docker workspace mode for broad shell access."
|
|
303
|
+
: "General Docker shell commands require Package installs = allow in docker-workspace mode.",
|
|
304
|
+
sandboxMode,
|
|
305
|
+
packageInstallPolicy,
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
if (classification.category === "destructive" && !config.allowDestructive) {
|
|
310
|
+
return {
|
|
311
|
+
allowed: false,
|
|
312
|
+
...classification,
|
|
313
|
+
needsApproval: true,
|
|
314
|
+
reason: "Destructive shell commands require Allow destructive actions.",
|
|
315
|
+
sandboxMode,
|
|
316
|
+
packageInstallPolicy,
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
if (classification.category === "network-fetch" && config.useDockerSandbox && !trustedDockerShell) {
|
|
321
|
+
return {
|
|
322
|
+
allowed: false,
|
|
323
|
+
...classification,
|
|
324
|
+
needsApproval: true,
|
|
325
|
+
reason: "Docker network commands require docker-workspace mode with Package installs = allow.",
|
|
326
|
+
sandboxMode,
|
|
327
|
+
packageInstallPolicy,
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
|
|
244
331
|
if (classification.writesWorkspace && config.useDockerSandbox && sandboxMode !== "docker-workspace") {
|
|
245
332
|
return {
|
|
246
333
|
allowed: false,
|
|
@@ -254,6 +341,7 @@ export function evaluateCommandPolicy(command, config) {
|
|
|
254
341
|
return {
|
|
255
342
|
allowed: true,
|
|
256
343
|
...classification,
|
|
344
|
+
requiresDockerRoot: Boolean(classification.requiresDockerRoot && config.useDockerSandbox),
|
|
257
345
|
sandboxMode,
|
|
258
346
|
packageInstallPolicy,
|
|
259
347
|
};
|
package/src/config.js
CHANGED
|
@@ -71,7 +71,7 @@ export function resolveRuntimeConfig(args, overrides = {}) {
|
|
|
71
71
|
? overrides.allowedDomains
|
|
72
72
|
: parseList(process.env.ALLOWED_DOMAINS),
|
|
73
73
|
allowPasswords: parseBoolean(overrides.allowPasswords ?? process.env.ALLOW_PASSWORDS, false),
|
|
74
|
-
allowDestructive: parseBoolean(overrides.allowDestructive ?? process.env.ALLOW_DESTRUCTIVE, false),
|
|
74
|
+
allowDestructive: parseBoolean(overrides.allowDestructive ?? args.allowDestructive ?? process.env.ALLOW_DESTRUCTIVE, false),
|
|
75
75
|
allowShellTool: parseBoolean(overrides.allowShellTool ?? args.allowShellTool ?? process.env.ALLOW_SHELL_TOOL, false),
|
|
76
76
|
allowFileTools: parseBoolean(overrides.allowFileTools ?? args.allowFileTools ?? process.env.ALLOW_FILE_TOOLS, true),
|
|
77
77
|
allowWrapperTools: parseBoolean(
|
package/src/docker-sandbox.js
CHANGED
|
@@ -168,7 +168,7 @@ export async function ensureDockerSandboxReady(config, observers, options = {})
|
|
|
168
168
|
function dockerRunArgs(command, config, policy = evaluateCommandPolicy(command, config)) {
|
|
169
169
|
const uid = typeof process.getuid === "function" ? String(process.getuid()) : "";
|
|
170
170
|
const gid = typeof process.getgid === "function" ? String(process.getgid()) : "";
|
|
171
|
-
const userArgs = uid && gid ? ["--user", `${uid}:${gid}`] : [];
|
|
171
|
+
const userArgs = uid && gid && !policy.requiresDockerRoot ? ["--user", `${uid}:${gid}`] : [];
|
|
172
172
|
const sandboxMode = normalizeSandboxMode(config.sandboxMode);
|
|
173
173
|
const mountMode = sandboxMode === "docker-workspace" ? "rw" : "ro";
|
|
174
174
|
const networkMode = policy.needsNetwork ? "bridge" : "none";
|
package/src/model-client.js
CHANGED
|
@@ -300,7 +300,7 @@ export async function requestNextStep(client, config, messages) {
|
|
|
300
300
|
function: {
|
|
301
301
|
name: "run_command",
|
|
302
302
|
description:
|
|
303
|
-
"Run
|
|
303
|
+
"Run a terminal command in the configured working directory under the active shell policy. Secrets and npm publishing are always blocked; Docker workspace mode with approved package installs supports broader network/setup commands, while host destructive or privileged work requires explicit trust.",
|
|
304
304
|
parameters: {
|
|
305
305
|
type: "object",
|
|
306
306
|
properties: {
|
package/src/task-profiles.js
CHANGED
|
@@ -52,7 +52,7 @@ export const TASK_PROFILES = {
|
|
|
52
52
|
id: "website",
|
|
53
53
|
label: "Website testing",
|
|
54
54
|
prompt:
|
|
55
|
-
"For website-testing tasks, create or inspect the site, add a
|
|
55
|
+
"For website-testing tasks, create or inspect the site, add a local check when useful, and use the configured sandbox/package policy for dependencies.",
|
|
56
56
|
tools: ["files", "shell", "canvas", "sandbox"],
|
|
57
57
|
},
|
|
58
58
|
aaps: {
|
|
@@ -73,7 +73,7 @@ export const TASK_PROFILES = {
|
|
|
73
73
|
id: "maintenance",
|
|
74
74
|
label: "System maintenance",
|
|
75
75
|
prompt:
|
|
76
|
-
"For system maintenance, diagnose first,
|
|
76
|
+
"For system maintenance, diagnose first, use Docker for broad installs when available, and follow the configured trust/package policy for host-level changes.",
|
|
77
77
|
tools: ["shell", "sandbox", "files"],
|
|
78
78
|
},
|
|
79
79
|
};
|