@lazyingart/agintiflow 0.20.85 → 0.20.87
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 +1 -1
- package/docs/runtime-modes-and-autonomy.md +2 -2
- package/package.json +1 -1
- package/scripts/smoke-cli-chat.js +1 -0
- package/scripts/smoke-model-roles.js +7 -1
- package/scripts/smoke-tmux-tools.js +47 -1
- package/src/agent-runner.js +2 -2
- package/src/interactive-cli.js +33 -5
- package/src/model-client.js +1 -1
- package/src/permission-advice.js +2 -2
- package/src/static-preview-server.js +14 -0
- package/src/tmux-tools.js +57 -0
package/README.md
CHANGED
|
@@ -157,7 +157,7 @@ aginti --resume <session-id> \
|
|
|
157
157
|
|
|
158
158
|
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.
|
|
159
159
|
|
|
160
|
-
Tmux follows the same rule. In Docker sandbox mode, `tmux_start_session` and `tmux_send_keys` are durable host tools, but their commands must stay workspace-bound. Use `--sandbox-mode host` when a tmux task needs
|
|
160
|
+
Tmux follows the same rule. In Docker sandbox mode, `tmux_start_session` and `tmux_send_keys` are durable host tools, but their commands must stay workspace-bound. In host mode, tmux startup/send command text follows the same host shell policy as `run_command`; broad host shell work needs explicit `--allow-destructive`. Use `--sandbox-mode host --allow-destructive` only when a tmux task really needs trusted whole-host execution.
|
|
161
161
|
|
|
162
162
|
## Permission Recipes
|
|
163
163
|
|
|
@@ -13,7 +13,7 @@ The failed task was expected under the current Docker design, but the agent shou
|
|
|
13
13
|
|
|
14
14
|
The fix is guidance plus guardrails: raw `tmux` shell commands are blocked inside Docker `run_command`, and the model is told to use host tmux tools for durable sessions.
|
|
15
15
|
|
|
16
|
-
Important boundary: host tmux tools are durable, but they are not a permission escape hatch. In `docker-readonly` and `docker-workspace` modes, `tmux_start_session` and `tmux_send_keys` are still workspace-bound. Startup commands and sent shell text must use relative paths under the project, not absolute host paths outside the project. If a task really needs `/home/...`, `/etc/...`, an emulator outside the project, or whole-host maintenance, rerun explicitly with `--sandbox-mode host`.
|
|
16
|
+
Important boundary: host tmux tools are durable, but they are not a permission escape hatch. In `docker-readonly` and `docker-workspace` modes, `tmux_start_session` and `tmux_send_keys` are still workspace-bound. Startup commands and sent shell text must use relative paths under the project, not absolute host paths outside the project. In `host` mode, tmux startup/send command text follows the same host shell policy as `run_command`, so broad host shell work requires `--allow-destructive`. If a task really needs `/home/...`, `/etc/...`, an emulator outside the project, or whole-host maintenance, rerun explicitly with `--sandbox-mode host --allow-destructive`.
|
|
17
17
|
|
|
18
18
|
## Execution Modes
|
|
19
19
|
|
|
@@ -139,7 +139,7 @@ Durable tmux task:
|
|
|
139
139
|
aginti "start a tmux session named demo, run ls in it, keep it open, and tell me how to attach"
|
|
140
140
|
```
|
|
141
141
|
|
|
142
|
-
The agent should use `tmux_start_session`, `tmux_send_keys`, and `tmux_capture_pane`, not Docker `run_command`. In Docker sandbox mode, tmux commands must stay inside the project. For trusted whole-host tmux work, use the host recipe above.
|
|
142
|
+
The agent should use `tmux_start_session`, `tmux_send_keys`, and `tmux_capture_pane`, not Docker `run_command`. In Docker sandbox mode, tmux commands must stay inside the project. In host mode, tmux command text is still governed by host shell policy; if a tmux command is blocked, present the suggested rerun path instead of trying tmux as a workaround. For trusted whole-host tmux work, use the host recipe above.
|
|
143
143
|
|
|
144
144
|
## Future Persistent Container Mode
|
|
145
145
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lazyingart/agintiflow",
|
|
3
|
-
"version": "0.20.
|
|
3
|
+
"version": "0.20.87",
|
|
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",
|
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
selectModelRoute,
|
|
11
11
|
} from "../src/model-routing.js";
|
|
12
12
|
import { normalizeTextToolCallResponse, parseTextToolCalls, usesTextToolProtocol } from "../src/model-client.js";
|
|
13
|
-
import { modelRoleChoices } from "../src/interactive-cli.js";
|
|
13
|
+
import { modelRoleChoices, selectorVisibleWindow } from "../src/interactive-cli.js";
|
|
14
14
|
import { buildScsEvidencePack, buildSupervisorInstruction } from "../src/scs-controller.js";
|
|
15
15
|
|
|
16
16
|
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
@@ -151,6 +151,11 @@ assert(!routeChoices.includes("venice/e2ee-venice-uncensored-24b-p"), "shared mo
|
|
|
151
151
|
assert(modelRoleChoices("route").some((item) => item.provider === "openai" && item.model === "gpt-5.5" && item.reasoningOptions.includes("xhigh")), "OpenAI selector missing reasoning levels");
|
|
152
152
|
assert(modelRoleChoices("auxiliary").some((item) => item.provider === "grsai"), "auxiliary selector missing GRS AI");
|
|
153
153
|
assert(modelRoleChoices("auxiliary").some((item) => item.provider === "venice" && item.model === "wan-2-7-pro-edit"), "auxiliary selector missing Venice Wan edit");
|
|
154
|
+
const longAuxiliaryWindow = selectorVisibleWindow(32, 17, 24);
|
|
155
|
+
assert(longAuxiliaryWindow.end - longAuxiliaryWindow.start <= 8, "selector should cap visible rows for long option lists");
|
|
156
|
+
assert(longAuxiliaryWindow.topHidden > 0 && longAuxiliaryWindow.bottomHidden > 0, "selector should expose scroll indicators around middle selections");
|
|
157
|
+
const topAuxiliaryWindow = selectorVisibleWindow(32, 0, 24);
|
|
158
|
+
assert(topAuxiliaryWindow.start === 0 && topAuxiliaryWindow.bottomHidden > 0, "selector should keep first option visible at top of long lists");
|
|
154
159
|
const parsedTextToolCalls = parseTextToolCalls('[TOOL_CALLS]list_files[ARGS]call_123[ARGS]{"path":".","maxDepth":1}');
|
|
155
160
|
assert(parsedTextToolCalls.length === 1, "Venice text tool-call parser did not detect encoded tool call");
|
|
156
161
|
assert(parsedTextToolCalls[0].function.name === "list_files", "Venice text tool-call parser returned wrong tool name");
|
|
@@ -253,6 +258,7 @@ console.log(
|
|
|
253
258
|
"route-overrides",
|
|
254
259
|
"provider-groups",
|
|
255
260
|
"auxiliary-catalog",
|
|
261
|
+
"selector-windowing",
|
|
256
262
|
"shared-model-selectors",
|
|
257
263
|
"venice-text-tool-parser",
|
|
258
264
|
"requested-tools-parser",
|
|
@@ -20,10 +20,18 @@ const workspace = await fs.mkdtemp(path.join(os.tmpdir(), "agintiflow-tmux-"));
|
|
|
20
20
|
const session = `aginti-smoke-${process.pid}`;
|
|
21
21
|
const config = {
|
|
22
22
|
allowShellTool: true,
|
|
23
|
+
allowDestructive: true,
|
|
23
24
|
commandCwd: workspace,
|
|
25
|
+
sandboxMode: "host",
|
|
26
|
+
useDockerSandbox: false,
|
|
27
|
+
};
|
|
28
|
+
const strictHostConfig = {
|
|
29
|
+
...config,
|
|
30
|
+
allowDestructive: false,
|
|
24
31
|
};
|
|
25
32
|
const dockerConfig = {
|
|
26
33
|
...config,
|
|
34
|
+
allowDestructive: false,
|
|
27
35
|
useDockerSandbox: true,
|
|
28
36
|
sandboxMode: "docker-workspace",
|
|
29
37
|
packageInstallPolicy: "allow",
|
|
@@ -47,7 +55,7 @@ try {
|
|
|
47
55
|
target: start.target,
|
|
48
56
|
text: "printf 'aginti tmux smoke\\n'; pwd",
|
|
49
57
|
enter: true,
|
|
50
|
-
});
|
|
58
|
+
}, config);
|
|
51
59
|
assert.equal(send.ok, true, send.error || send.reason);
|
|
52
60
|
await sleep(500);
|
|
53
61
|
|
|
@@ -114,6 +122,41 @@ try {
|
|
|
114
122
|
});
|
|
115
123
|
assert.equal(dockerTmuxRelativeStart.allowed, true, "Docker-mode tmux_start_session should allow workspace-relative paths");
|
|
116
124
|
|
|
125
|
+
const hostBroadTmuxStart = checkToolUse({
|
|
126
|
+
toolName: "tmux_start_session",
|
|
127
|
+
args: { name: `${session}-host-broad`, cwd: ".", command: 'echo "HOST_TMUX_STARTED"; sleep 1' },
|
|
128
|
+
config: strictHostConfig,
|
|
129
|
+
});
|
|
130
|
+
assert.equal(hostBroadTmuxStart.allowed, false, "Host-mode tmux_start_session should not bypass host shell approval");
|
|
131
|
+
assert.equal(hostBroadTmuxStart.category, "general-shell", "Host tmux broad command should keep shell policy category");
|
|
132
|
+
|
|
133
|
+
const hostBroadTmuxSend = checkTmuxToolUse(
|
|
134
|
+
"tmux_send_keys",
|
|
135
|
+
{ target: start.target, text: 'echo "HOST_TMUX_SEND"; sleep 1' },
|
|
136
|
+
strictHostConfig
|
|
137
|
+
);
|
|
138
|
+
assert.equal(hostBroadTmuxSend.allowed, false, "Host-mode tmux_send_keys should not bypass host shell approval");
|
|
139
|
+
assert.equal(hostBroadTmuxSend.category, "general-shell", "Host tmux send should keep shell policy category");
|
|
140
|
+
|
|
141
|
+
const directHostBroadStart = await startTmuxSession(
|
|
142
|
+
{ name: `${session}-direct-host-broad`, cwd: ".", command: 'echo "HOST_TMUX_STARTED"; sleep 1' },
|
|
143
|
+
strictHostConfig
|
|
144
|
+
);
|
|
145
|
+
assert.equal(directHostBroadStart.ok, false, "tmux_start_session direct path should enforce host shell approval");
|
|
146
|
+
|
|
147
|
+
const directHostBroadSend = await sendTmuxKeys(
|
|
148
|
+
{ target: start.target, text: 'echo "HOST_TMUX_SEND"; sleep 1', enter: false },
|
|
149
|
+
strictHostConfig
|
|
150
|
+
);
|
|
151
|
+
assert.equal(directHostBroadSend.ok, false, "tmux_send_keys direct path should enforce host shell approval");
|
|
152
|
+
|
|
153
|
+
const hostInterruptKey = checkTmuxToolUse(
|
|
154
|
+
"tmux_send_keys",
|
|
155
|
+
{ target: start.target, text: "", keys: ["C-c"], enter: false },
|
|
156
|
+
strictHostConfig
|
|
157
|
+
);
|
|
158
|
+
assert.equal(hostInterruptKey.allowed, true, "Host-mode tmux_send_keys should still allow safe control keys");
|
|
159
|
+
|
|
117
160
|
const dockerTmuxCommand = checkToolUse({
|
|
118
161
|
toolName: "run_command",
|
|
119
162
|
args: { command: "tmux new-session -d -s should-not-run" },
|
|
@@ -160,6 +203,9 @@ try {
|
|
|
160
203
|
"docker-tmux-send-outside-path-guardrail",
|
|
161
204
|
"docker-tmux-start-project-path-allowed",
|
|
162
205
|
"docker-tmux-start-relative-path-allowed",
|
|
206
|
+
"host-tmux-start-shell-policy-guardrail",
|
|
207
|
+
"host-tmux-send-shell-policy-guardrail",
|
|
208
|
+
"host-tmux-control-key-allowed",
|
|
163
209
|
"docker-run-command-tmux-guardrail",
|
|
164
210
|
"docker-run-command-tmux-search-allowed",
|
|
165
211
|
"docker-run-command-npx-aginti-guardrail",
|
package/src/agent-runner.js
CHANGED
|
@@ -151,7 +151,7 @@ async function isPortAvailable(port) {
|
|
|
151
151
|
async function findAvailablePort(preferredPort = 8765) {
|
|
152
152
|
const preferred = Number(preferredPort);
|
|
153
153
|
const start = Number.isFinite(preferred) && preferred > 0 ? preferred : 8765;
|
|
154
|
-
for (let port = start; port < start +
|
|
154
|
+
for (let port = start; port < start + 1000 && port < 65535; port += 1) {
|
|
155
155
|
if (await isPortAvailable(port)) return port;
|
|
156
156
|
}
|
|
157
157
|
throw new Error(`No available preview port found near ${start}.`);
|
|
@@ -894,7 +894,7 @@ async function captureSyntheticSnapshot(store, step, config) {
|
|
|
894
894
|
: `Shell tool available in: ${config.commandCwd} on ${platformLabel(platform)}. Use OS-compatible commands; prefer WSL/Docker for bash-heavy workflows on Windows.`
|
|
895
895
|
: "Shell tool disabled.",
|
|
896
896
|
config.allowShellTool
|
|
897
|
-
? "Host tmux tools available: tmux_list_sessions, tmux_capture_pane, tmux_send_keys, tmux_start_session. Use them for long-running jobs and agent terminals; capture before sending input. Docker run_command containers are ephemeral, so tmux there will not persist. In Docker sandbox mode, tmux start/send commands must stay workspace-bound and cannot reference absolute host paths outside the project; use --sandbox-mode host for trusted whole-host work. For one-shot tmux commands, redirect output and exit status to a durable workspace log or keep the pane alive for capture; if capture fails because the session ended, do not infer output or exit status."
|
|
897
|
+
? "Host tmux tools available: tmux_list_sessions, tmux_capture_pane, tmux_send_keys, tmux_start_session. Use them for long-running jobs and agent terminals; capture before sending input. Docker run_command containers are ephemeral, so tmux there will not persist. In Docker sandbox mode, tmux start/send commands must stay workspace-bound and cannot reference absolute host paths outside the project; use --sandbox-mode host for trusted whole-host work. In host mode, tmux startup/send command text follows the same host shell policy as run_command; if a broad host command is blocked, present the approval/rerun path instead of trying tmux as a workaround. For one-shot tmux commands, redirect output and exit status to a durable workspace log or keep the pane alive for capture; if capture fails because the session ended, do not infer output or exit status."
|
|
898
898
|
: "",
|
|
899
899
|
config.allowFileTools
|
|
900
900
|
? `Workspace file tools available in: ${config.commandCwd}. Use inspect_project first for large or unfamiliar codebases, then search/read exact files before editing. Use workspace-relative paths. Use apply_patch for code edits; it supports exact single-file replacement and multi-file Codex-style/unified patches. For new standalone generated content, pick a descriptive non-conflicting filename and avoid overwriting unless explicitly requested.`
|
package/src/interactive-cli.js
CHANGED
|
@@ -2220,21 +2220,49 @@ function clearSelectorSequence(lineCount) {
|
|
|
2220
2220
|
return Array.from({ length: Math.max(lineCount, 0) }, () => "\x1b[1A\r\x1b[2K").join("");
|
|
2221
2221
|
}
|
|
2222
2222
|
|
|
2223
|
+
export function selectorVisibleWindow(optionCount = 0, selectedIndex = 0, height = terminalHeight()) {
|
|
2224
|
+
const count = Math.max(Number(optionCount) || 0, 0);
|
|
2225
|
+
const maxRows = Math.max(4, Math.min(8, Math.floor(Math.max(Number(height) || 24, 10) * 0.36)));
|
|
2226
|
+
if (count <= maxRows) {
|
|
2227
|
+
return { start: 0, end: count, topHidden: 0, bottomHidden: 0 };
|
|
2228
|
+
}
|
|
2229
|
+
const selected = clamp(selectedIndex, 0, count - 1);
|
|
2230
|
+
const half = Math.floor(maxRows / 2);
|
|
2231
|
+
const start = clamp(selected - half, 0, count - maxRows);
|
|
2232
|
+
const end = Math.min(start + maxRows, count);
|
|
2233
|
+
return {
|
|
2234
|
+
start,
|
|
2235
|
+
end,
|
|
2236
|
+
topHidden: start,
|
|
2237
|
+
bottomHidden: count - end,
|
|
2238
|
+
};
|
|
2239
|
+
}
|
|
2240
|
+
|
|
2223
2241
|
function renderSelector({ title, subtitle, options, selectedIndex, lineCount = 0 }) {
|
|
2224
2242
|
const width = Math.min(Math.max(terminalWidth() - 2, 60), 110);
|
|
2225
2243
|
const bodyWidth = width - 4;
|
|
2226
2244
|
const safeTitle = compactLine(title, bodyWidth);
|
|
2227
2245
|
const safeSubtitle = compactLine(subtitle, bodyWidth);
|
|
2246
|
+
const visible = selectorVisibleWindow(options.length, selectedIndex, terminalHeight());
|
|
2247
|
+
const optionRows = [];
|
|
2248
|
+
if (visible.topHidden > 0) {
|
|
2249
|
+
optionRows.push(`│ ${padVisible(color(` ... ${visible.topHidden} earlier option${visible.topHidden === 1 ? "" : "s"}`, ansi.dim), bodyWidth)} │`);
|
|
2250
|
+
}
|
|
2251
|
+
for (let index = visible.start; index < visible.end; index += 1) {
|
|
2252
|
+
const option = options[index];
|
|
2253
|
+
const marker = index === selectedIndex ? ">" : " ";
|
|
2254
|
+
const rendered = `${marker} ${compactLine(modelChoiceLine(option), bodyWidth - 2)}`;
|
|
2255
|
+
optionRows.push(`│ ${padVisible(index === selectedIndex ? color(rendered, ansi.userBg, ansi.bold) : rendered, bodyWidth)} │`);
|
|
2256
|
+
}
|
|
2257
|
+
if (visible.bottomHidden > 0) {
|
|
2258
|
+
optionRows.push(`│ ${padVisible(color(` ... ${visible.bottomHidden} later option${visible.bottomHidden === 1 ? "" : "s"}`, ansi.dim), bodyWidth)} │`);
|
|
2259
|
+
}
|
|
2228
2260
|
const rows = [
|
|
2229
2261
|
`╭${"─".repeat(width - 2)}╮`,
|
|
2230
2262
|
`│ ${padVisible(safeTitle, bodyWidth)} │`,
|
|
2231
2263
|
`│ ${padVisible(safeSubtitle, bodyWidth)} │`,
|
|
2232
2264
|
`├${"─".repeat(width - 2)}┤`,
|
|
2233
|
-
...
|
|
2234
|
-
const marker = index === selectedIndex ? ">" : " ";
|
|
2235
|
-
const rendered = `${marker} ${compactLine(modelChoiceLine(option), bodyWidth - 2)}`;
|
|
2236
|
-
return `│ ${padVisible(index === selectedIndex ? color(rendered, ansi.userBg, ansi.bold) : rendered, bodyWidth)} │`;
|
|
2237
|
-
}),
|
|
2265
|
+
...optionRows,
|
|
2238
2266
|
`╰${"─".repeat(width - 2)}╯`,
|
|
2239
2267
|
];
|
|
2240
2268
|
output.write(`${lineCount > 0 ? clearSelectorSequence(lineCount) : ""}${rows.join("\n")}\n`);
|
package/src/model-client.js
CHANGED
|
@@ -512,7 +512,7 @@ export async function createPlan(client, config, state) {
|
|
|
512
512
|
? `Shell tool is enabled in ${config.commandCwd}. Host platform: ${platformLabel(platform)}. In Docker, this path is mounted as /workspace with persistent /aginti-env and /aginti-cache mounts. Use relative paths or /workspace paths, not absolute host temp paths. Sandbox mode: ${config.sandboxMode}. Package install policy: ${config.packageInstallPolicy}. For npm/pip/conda/venv setup, explain the need and wait for approval unless policy is allow. Do not run npx aginti, npm exec aginti, or nested aginti diagnostics from this shell; they may resolve stale project packages or create recursive agent sessions. On native Windows host mode, prefer PowerShell/cmd-compatible commands or WSL/Docker for bash-like toolchains.`
|
|
513
513
|
: "",
|
|
514
514
|
config.allowShellTool
|
|
515
|
-
? "Host tmux tools are enabled for long-running sessions. Plan to use tmux_start_session for durable jobs, tmux_capture_pane to monitor, tmux_send_keys to interact after capture, and tmux_list_sessions to discover existing sessions. Do not install or run tmux inside Docker run_command containers; those containers are short-lived and cannot preserve tmux servers. In Docker sandbox mode, tmux startup/send commands are still workspace-bound: use relative project paths, not absolute host paths outside the workspace. Ask for --sandbox-mode host before whole-host tmux work."
|
|
515
|
+
? "Host tmux tools are enabled for long-running sessions. Plan to use tmux_start_session for durable jobs, tmux_capture_pane to monitor, tmux_send_keys to interact after capture, and tmux_list_sessions to discover existing sessions. Do not install or run tmux inside Docker run_command containers; those containers are short-lived and cannot preserve tmux servers. In Docker sandbox mode, tmux startup/send commands are still workspace-bound: use relative project paths, not absolute host paths outside the workspace. In host mode, tmux startup/send command text follows the same host shell policy as run_command; if blocked, present the suggested approval/rerun path instead of trying tmux as a workaround. Ask for --sandbox-mode host --allow-destructive before trusted whole-host tmux work."
|
|
516
516
|
: "",
|
|
517
517
|
config.allowFileTools
|
|
518
518
|
? `Workspace file tools are enabled in ${config.commandCwd}: inspect_project, list_files, read_file, search_files, write_file, apply_patch, open_workspace_file, preview_workspace. For large or unfamiliar repos, plan to call inspect_project first, then search/read AGINTI.md/AGENTS.md/README/manifests and exact files. apply_patch supports exact single-file replacements and Codex-style/unified multi-file patches; prefer it for edits after reading relevant context. Keep all paths workspace-relative, for example plot_fx.svg or docs/report.tex, and avoid secrets. For newly generated standalone prose/docs/stories/assets, choose a descriptive non-conflicting filename from the topic/language instead of generic names like story.txt or output.txt; do not overwrite existing files unless the user explicitly asked to update/replace/overwrite that file. For generated local HTML/SVG/PDF/static sites, plan to use open_workspace_file or preview_workspace rather than starting a localhost server inside Docker.`
|
package/src/permission-advice.js
CHANGED
|
@@ -67,12 +67,12 @@ function currentMode(config = {}) {
|
|
|
67
67
|
}
|
|
68
68
|
|
|
69
69
|
function adviceForCategory(category = "", { toolName = "", args = {}, config = {}, state = {}, reason = "" } = {}) {
|
|
70
|
-
const command = compactLine(args.command || "");
|
|
70
|
+
const command = compactLine(args.command || args.text || "");
|
|
71
71
|
const base = {
|
|
72
72
|
category: category || "permission",
|
|
73
73
|
reason: compactLine(reason || "The runtime policy blocked this operation."),
|
|
74
74
|
currentMode: currentMode(config),
|
|
75
|
-
blockedOperation: command ?
|
|
75
|
+
blockedOperation: command ? `${toolName || "tool"}: ${command}` : toolName,
|
|
76
76
|
instruction:
|
|
77
77
|
"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.",
|
|
78
78
|
};
|
|
@@ -5,6 +5,8 @@ import path from "node:path";
|
|
|
5
5
|
const root = path.resolve(process.argv[2] || process.cwd());
|
|
6
6
|
const port = Number(process.argv[3] || 0);
|
|
7
7
|
const host = "127.0.0.1";
|
|
8
|
+
const idleTtlMs = Math.max(Number(process.env.AGINTIFLOW_PREVIEW_TTL_MS) || 6 * 60 * 60 * 1000, 1000);
|
|
9
|
+
let lastRequestAt = Date.now();
|
|
8
10
|
|
|
9
11
|
const MIME_TYPES = new Map([
|
|
10
12
|
[".html", "text/html; charset=utf-8"],
|
|
@@ -62,6 +64,7 @@ function isBlockedPath(relativePath) {
|
|
|
62
64
|
}
|
|
63
65
|
|
|
64
66
|
const server = http.createServer(async (req, res) => {
|
|
67
|
+
lastRequestAt = Date.now();
|
|
65
68
|
if (!["GET", "HEAD"].includes(req.method || "")) {
|
|
66
69
|
send(res, 405, "Method Not Allowed");
|
|
67
70
|
return;
|
|
@@ -110,3 +113,14 @@ server.listen(port, host, () => {
|
|
|
110
113
|
const actualPort = typeof address === "object" && address ? address.port : port;
|
|
111
114
|
console.log(`AgInTiFlow static preview http://${host}:${actualPort}/ root=${root}`);
|
|
112
115
|
});
|
|
116
|
+
|
|
117
|
+
const ttlTimer = setInterval(async () => {
|
|
118
|
+
const rootExists = await fs
|
|
119
|
+
.stat(root)
|
|
120
|
+
.then((stat) => stat.isDirectory())
|
|
121
|
+
.catch(() => false);
|
|
122
|
+
if (rootExists && Date.now() - lastRequestAt < idleTtlMs) return;
|
|
123
|
+
server.close(() => process.exit(0));
|
|
124
|
+
setTimeout(() => process.exit(0), 1000).unref();
|
|
125
|
+
}, Math.min(Math.max(Math.floor(idleTtlMs / 2), 1000), 60_000));
|
|
126
|
+
ttlTimer.unref();
|
package/src/tmux-tools.js
CHANGED
|
@@ -2,6 +2,7 @@ import crypto from "node:crypto";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { execFile as execFileCallback } from "node:child_process";
|
|
4
4
|
import { promisify } from "node:util";
|
|
5
|
+
import { evaluateCommandPolicy } from "./command-policy.js";
|
|
5
6
|
import { redactSensitiveText } from "./redaction.js";
|
|
6
7
|
|
|
7
8
|
const execFile = promisify(execFileCallback);
|
|
@@ -136,6 +137,22 @@ function checkWorkspaceBoundTmuxText(text = "", config = {}, label = "tmux text"
|
|
|
136
137
|
return { ok: true };
|
|
137
138
|
}
|
|
138
139
|
|
|
140
|
+
function checkHostShellPolicyForTmuxText(text = "", config = {}, label = "tmux text") {
|
|
141
|
+
const command = String(text || "").trim();
|
|
142
|
+
if (!command) return { ok: true };
|
|
143
|
+
if (config.useDockerSandbox || config.sandboxMode !== "host" || config.allowDestructive) {
|
|
144
|
+
return { ok: true };
|
|
145
|
+
}
|
|
146
|
+
const policy = evaluateCommandPolicy(command, config);
|
|
147
|
+
if (policy.allowed) return { ok: true };
|
|
148
|
+
return {
|
|
149
|
+
ok: false,
|
|
150
|
+
reason: `${label} is blocked by the same host shell policy as run_command: ${policy.reason}`,
|
|
151
|
+
category: policy.category || "tmux",
|
|
152
|
+
needsApproval: policy.needsApproval,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
139
156
|
function parseSessions(stdout = "") {
|
|
140
157
|
return String(stdout || "")
|
|
141
158
|
.split(/\r?\n/)
|
|
@@ -238,6 +255,17 @@ export async function sendTmuxKeys(args = {}, config = {}) {
|
|
|
238
255
|
if (!workspaceBound.ok) {
|
|
239
256
|
return { ok: false, toolName: "tmux_send_keys", blocked: true, reason: workspaceBound.reason };
|
|
240
257
|
}
|
|
258
|
+
const hostPolicy = checkHostShellPolicyForTmuxText(text, config, "tmux text");
|
|
259
|
+
if (!hostPolicy.ok) {
|
|
260
|
+
return {
|
|
261
|
+
ok: false,
|
|
262
|
+
toolName: "tmux_send_keys",
|
|
263
|
+
blocked: true,
|
|
264
|
+
reason: hostPolicy.reason,
|
|
265
|
+
category: hostPolicy.category,
|
|
266
|
+
needsApproval: hostPolicy.needsApproval,
|
|
267
|
+
};
|
|
268
|
+
}
|
|
241
269
|
for (const key of keys) {
|
|
242
270
|
if (!ALLOWED_KEYS.has(key)) {
|
|
243
271
|
return { ok: false, toolName: "tmux_send_keys", blocked: true, reason: `Unsupported tmux key: ${key}` };
|
|
@@ -293,6 +321,17 @@ export async function startTmuxSession(args = {}, config = {}) {
|
|
|
293
321
|
if (!workspaceBound.ok) {
|
|
294
322
|
return { ok: false, toolName: "tmux_start_session", blocked: true, reason: workspaceBound.reason };
|
|
295
323
|
}
|
|
324
|
+
const hostPolicy = checkHostShellPolicyForTmuxText(command, config, "tmux startup command");
|
|
325
|
+
if (!hostPolicy.ok) {
|
|
326
|
+
return {
|
|
327
|
+
ok: false,
|
|
328
|
+
toolName: "tmux_start_session",
|
|
329
|
+
blocked: true,
|
|
330
|
+
reason: hostPolicy.reason,
|
|
331
|
+
category: hostPolicy.category,
|
|
332
|
+
needsApproval: hostPolicy.needsApproval,
|
|
333
|
+
};
|
|
334
|
+
}
|
|
296
335
|
|
|
297
336
|
const tmuxArgs = ["new-session", "-d", "-s", name.name, "-c", cwd.cwd];
|
|
298
337
|
if (command) tmuxArgs.push(command);
|
|
@@ -332,6 +371,15 @@ export function checkTmuxToolUse(toolName, args = {}, config = {}) {
|
|
|
332
371
|
}
|
|
333
372
|
const workspaceBound = checkWorkspaceBoundTmuxText(text, config, "tmux text");
|
|
334
373
|
if (!workspaceBound.ok) return { allowed: false, reason: workspaceBound.reason, category: "tmux" };
|
|
374
|
+
const hostPolicy = checkHostShellPolicyForTmuxText(text, config, "tmux text");
|
|
375
|
+
if (!hostPolicy.ok) {
|
|
376
|
+
return {
|
|
377
|
+
allowed: false,
|
|
378
|
+
reason: hostPolicy.reason,
|
|
379
|
+
category: hostPolicy.category || "tmux",
|
|
380
|
+
needsApproval: hostPolicy.needsApproval,
|
|
381
|
+
};
|
|
382
|
+
}
|
|
335
383
|
for (const key of Array.isArray(args.keys) ? args.keys : []) {
|
|
336
384
|
if (!ALLOWED_KEYS.has(String(key))) return { allowed: false, reason: `Unsupported tmux key: ${key}`, category: "tmux" };
|
|
337
385
|
}
|
|
@@ -354,6 +402,15 @@ export function checkTmuxToolUse(toolName, args = {}, config = {}) {
|
|
|
354
402
|
}
|
|
355
403
|
const workspaceBound = checkWorkspaceBoundTmuxText(command, config, "tmux startup command");
|
|
356
404
|
if (!workspaceBound.ok) return { allowed: false, reason: workspaceBound.reason, category: "tmux" };
|
|
405
|
+
const hostPolicy = checkHostShellPolicyForTmuxText(command, config, "tmux startup command");
|
|
406
|
+
if (!hostPolicy.ok) {
|
|
407
|
+
return {
|
|
408
|
+
allowed: false,
|
|
409
|
+
reason: hostPolicy.reason,
|
|
410
|
+
category: hostPolicy.category || "tmux",
|
|
411
|
+
needsApproval: hostPolicy.needsApproval,
|
|
412
|
+
};
|
|
413
|
+
}
|
|
357
414
|
return { allowed: true, category: "tmux" };
|
|
358
415
|
}
|
|
359
416
|
return { allowed: true, category: "tmux" };
|