@lazyingart/agintiflow 0.20.83 → 0.20.85
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 +3 -1
- package/docs/aaps.md +15 -0
- package/docs/runtime-modes-and-autonomy.md +3 -1
- package/package.json +1 -1
- package/scripts/smoke-tmux-tools.js +57 -4
- package/src/agent-runner.js +3 -3
- package/src/model-client.js +3 -3
- package/src/tmux-tools.js +46 -1
package/README.md
CHANGED
|
@@ -157,6 +157,8 @@ 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 absolute host paths outside the project.
|
|
161
|
+
|
|
160
162
|
## Permission Recipes
|
|
161
163
|
|
|
162
164
|
Use these when you want explicit control instead of the default interactive policy:
|
|
@@ -256,7 +258,7 @@ Inside chat:
|
|
|
256
258
|
/aaps dry-run workflows/main.aaps
|
|
257
259
|
```
|
|
258
260
|
|
|
259
|
-
Use AAPS when the task is bigger than a single chat: app development with stages, paper/book workflows, validation gates, recovery steps, artifact production, or top-down agentic scripts.
|
|
261
|
+
Use AAPS when the task is bigger than a single chat: app development with stages, paper/book workflows, validation gates, recovery steps, artifact production, or top-down agentic scripts. There are two bridge directions: AgInTiFlow can inspect/validate/compile/run `.aaps` workflows with `aginti aaps ...`, and AAPS can call AgInTiFlow as the backend implementation agent with `aaps prompt "goal" --backend aginti`. See [docs/aaps.md](docs/aaps.md) and the package [https://www.npmjs.com/package/@lazyingart/aaps](https://www.npmjs.com/package/@lazyingart/aaps).
|
|
260
262
|
|
|
261
263
|
## Local API Quick Reference
|
|
262
264
|
|
package/docs/aaps.md
CHANGED
|
@@ -9,6 +9,11 @@ The integration is intentionally lightweight. AgInTiFlow does not vendor the AAP
|
|
|
9
9
|
3. `aaps` on `PATH`.
|
|
10
10
|
4. A sibling development checkout at `~/ProjectsLFS/AAPS/scripts/aaps.js` when present.
|
|
11
11
|
|
|
12
|
+
There are two useful bridge directions:
|
|
13
|
+
|
|
14
|
+
- `aginti aaps ...`: AgInTiFlow is the interactive runtime and calls the AAPS CLI to inspect, validate, compile, dry-run, or run project workflows.
|
|
15
|
+
- `aaps prompt "goal" --backend aginti`: AAPS writes a durable backend-agent handoff under `.aaps-work/prompts/` and invokes AgInTiFlow as the implementation agent for the goal.
|
|
16
|
+
|
|
12
17
|
## CLI
|
|
13
18
|
|
|
14
19
|
```bash
|
|
@@ -46,6 +51,16 @@ Current lightweight adapter boundary: prompt-only AAPS tasks are recorded as han
|
|
|
46
51
|
|
|
47
52
|
`aginti aaps install` installs `@lazyingart/aaps` as a project dev dependency only when the current project has `package.json`. Use `aginti aaps install global` only when you intentionally want a global npm install.
|
|
48
53
|
|
|
54
|
+
If you want AAPS itself to hand a top-level goal to AgInTiFlow, use the AAPS CLI:
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
aaps prompt "Create and validate an executable workflow that writes reports/smoke.md" --project . --backend aginti
|
|
58
|
+
aaps "Create a small AAPS workflow and run it" --project . --backend aginti
|
|
59
|
+
aaps prompt "Prepare the backend prompt only" --project . --backend print --json
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
The generated AAPS handoff is sandbox-aware: it asks the backend to prefer installed `aaps`, use Docker-safe `npx -y @lazyingart/aaps@<version>` when package installs/network are approved, and use a source checkout only when that path is visible inside the active sandbox.
|
|
63
|
+
|
|
49
64
|
## Project Shape
|
|
50
65
|
|
|
51
66
|
`aginti aaps init` creates a starter project without overwriting existing files:
|
|
@@ -13,6 +13,8 @@ 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`.
|
|
17
|
+
|
|
16
18
|
## Execution Modes
|
|
17
19
|
|
|
18
20
|
| Mode | Similar Codex idea | Best use | What persists | Risk |
|
|
@@ -137,7 +139,7 @@ Durable tmux task:
|
|
|
137
139
|
aginti "start a tmux session named demo, run ls in it, keep it open, and tell me how to attach"
|
|
138
140
|
```
|
|
139
141
|
|
|
140
|
-
The agent should use `tmux_start_session`, `tmux_send_keys`, and `tmux_capture_pane`, not Docker `run_command`.
|
|
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.
|
|
141
143
|
|
|
142
144
|
## Future Persistent Container Mode
|
|
143
145
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lazyingart/agintiflow",
|
|
3
|
-
"version": "0.20.
|
|
3
|
+
"version": "0.20.85",
|
|
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",
|
|
@@ -22,6 +22,12 @@ const config = {
|
|
|
22
22
|
allowShellTool: true,
|
|
23
23
|
commandCwd: workspace,
|
|
24
24
|
};
|
|
25
|
+
const dockerConfig = {
|
|
26
|
+
...config,
|
|
27
|
+
useDockerSandbox: true,
|
|
28
|
+
sandboxMode: "docker-workspace",
|
|
29
|
+
packageInstallPolicy: "allow",
|
|
30
|
+
};
|
|
25
31
|
|
|
26
32
|
function sleep(ms) {
|
|
27
33
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
@@ -65,23 +71,66 @@ try {
|
|
|
65
71
|
const destructive = checkTmuxToolUse("tmux_send_keys", { target: start.target, text: "rm -rf /" }, config);
|
|
66
72
|
assert.equal(destructive.allowed, false, "tmux guardrail did not block destructive text");
|
|
67
73
|
|
|
74
|
+
const outsidePath = path.join(os.tmpdir(), "agintiflow-outside-workspace-canary.txt");
|
|
75
|
+
const workspacePath = path.join(workspace, "inside-workspace.txt");
|
|
76
|
+
const dockerTmuxOutsideStart = checkToolUse({
|
|
77
|
+
toolName: "tmux_start_session",
|
|
78
|
+
args: { name: `${session}-outside`, cwd: ".", command: `cat ${outsidePath}` },
|
|
79
|
+
config: dockerConfig,
|
|
80
|
+
});
|
|
81
|
+
assert.equal(dockerTmuxOutsideStart.allowed, false, "Docker-mode tmux_start_session should block outside host paths");
|
|
82
|
+
assert.equal(dockerTmuxOutsideStart.category, "tmux", "outside tmux path block should be categorized as tmux");
|
|
83
|
+
|
|
84
|
+
const dockerTmuxOutsideSend = checkTmuxToolUse(
|
|
85
|
+
"tmux_send_keys",
|
|
86
|
+
{ target: start.target, text: `cat ${outsidePath}` },
|
|
87
|
+
dockerConfig
|
|
88
|
+
);
|
|
89
|
+
assert.equal(dockerTmuxOutsideSend.allowed, false, "Docker-mode tmux_send_keys should block outside host paths");
|
|
90
|
+
|
|
91
|
+
const directOutsideStart = await startTmuxSession(
|
|
92
|
+
{ name: `${session}-direct-outside`, cwd: ".", command: `cat ${outsidePath}` },
|
|
93
|
+
dockerConfig
|
|
94
|
+
);
|
|
95
|
+
assert.equal(directOutsideStart.ok, false, "tmux_start_session should enforce outside host path guard at execution time");
|
|
96
|
+
|
|
97
|
+
const directOutsideSend = await sendTmuxKeys(
|
|
98
|
+
{ target: start.target, text: `cat ${outsidePath}`, enter: false },
|
|
99
|
+
dockerConfig
|
|
100
|
+
);
|
|
101
|
+
assert.equal(directOutsideSend.ok, false, "tmux_send_keys should enforce outside host path guard at execution time");
|
|
102
|
+
|
|
103
|
+
const dockerTmuxWorkspaceStart = checkToolUse({
|
|
104
|
+
toolName: "tmux_start_session",
|
|
105
|
+
args: { name: `${session}-inside`, cwd: ".", command: `cat ${workspacePath}` },
|
|
106
|
+
config: dockerConfig,
|
|
107
|
+
});
|
|
108
|
+
assert.equal(dockerTmuxWorkspaceStart.allowed, true, "Docker-mode tmux_start_session should allow project absolute paths");
|
|
109
|
+
|
|
110
|
+
const dockerTmuxRelativeStart = checkToolUse({
|
|
111
|
+
toolName: "tmux_start_session",
|
|
112
|
+
args: { name: `${session}-relative`, cwd: ".", command: "cat inside-workspace.txt" },
|
|
113
|
+
config: dockerConfig,
|
|
114
|
+
});
|
|
115
|
+
assert.equal(dockerTmuxRelativeStart.allowed, true, "Docker-mode tmux_start_session should allow workspace-relative paths");
|
|
116
|
+
|
|
68
117
|
const dockerTmuxCommand = checkToolUse({
|
|
69
118
|
toolName: "run_command",
|
|
70
119
|
args: { command: "tmux new-session -d -s should-not-run" },
|
|
71
|
-
config:
|
|
120
|
+
config: dockerConfig,
|
|
72
121
|
});
|
|
73
122
|
assert.equal(dockerTmuxCommand.allowed, false, "Docker run_command tmux usage should be blocked in favor of host tmux tools");
|
|
74
123
|
const dockerTmuxSearch = checkToolUse({
|
|
75
124
|
toolName: "run_command",
|
|
76
125
|
args: { command: "rg tmux README.md" },
|
|
77
|
-
config:
|
|
126
|
+
config: dockerConfig,
|
|
78
127
|
});
|
|
79
128
|
assert.equal(dockerTmuxSearch.allowed, true, "Docker run_command should still allow harmless tmux text searches");
|
|
80
129
|
|
|
81
130
|
const dockerNpxAginti = checkToolUse({
|
|
82
131
|
toolName: "run_command",
|
|
83
132
|
args: { command: "npx aginti doctor --json" },
|
|
84
|
-
config:
|
|
133
|
+
config: dockerConfig,
|
|
85
134
|
});
|
|
86
135
|
assert.equal(dockerNpxAginti.allowed, false, "Docker run_command should block npx aginti self-invocation");
|
|
87
136
|
assert.equal(dockerNpxAginti.category, "nested-aginti", "npx aginti block should be categorized");
|
|
@@ -89,7 +138,7 @@ try {
|
|
|
89
138
|
const dockerNestedAginti = checkToolUse({
|
|
90
139
|
toolName: "run_command",
|
|
91
140
|
args: { command: "aginti storage status" },
|
|
92
|
-
config:
|
|
141
|
+
config: dockerConfig,
|
|
93
142
|
});
|
|
94
143
|
assert.equal(dockerNestedAginti.allowed, false, "Docker run_command should block nested aginti CLI calls");
|
|
95
144
|
assert.equal(dockerNestedAginti.category, "nested-aginti", "nested aginti block should be categorized");
|
|
@@ -107,6 +156,10 @@ try {
|
|
|
107
156
|
"list-sessions",
|
|
108
157
|
"secret-guardrail",
|
|
109
158
|
"destructive-guardrail",
|
|
159
|
+
"docker-tmux-start-outside-path-guardrail",
|
|
160
|
+
"docker-tmux-send-outside-path-guardrail",
|
|
161
|
+
"docker-tmux-start-project-path-allowed",
|
|
162
|
+
"docker-tmux-start-relative-path-allowed",
|
|
110
163
|
"docker-run-command-tmux-guardrail",
|
|
111
164
|
"docker-run-command-tmux-search-allowed",
|
|
112
165
|
"docker-run-command-npx-aginti-guardrail",
|
package/src/agent-runner.js
CHANGED
|
@@ -428,7 +428,7 @@ async function createInitialState(config, sessionId) {
|
|
|
428
428
|
"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, copy the exact suggestedCommand when giving a rerun path, and ask the user to approve/rerun that mode or choose a safer workspace-relative path. Never invent legacy AgInTi syntax such as `aginti run --sandbox host`; use the exact flags from permissionAdvice.",
|
|
429
429
|
"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.",
|
|
430
430
|
config.allowShellTool
|
|
431
|
-
? "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."
|
|
431
|
+
? "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. In Docker sandbox mode, tmux start/send commands must stay workspace-bound and must not reference absolute host paths outside the project; ask for --sandbox-mode host for trusted whole-host work."
|
|
432
432
|
+ " For one-shot tmux commands, redirect stdout/stderr 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."
|
|
433
433
|
: "",
|
|
434
434
|
config.allowFileTools
|
|
@@ -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. 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. 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.`
|
|
@@ -1224,7 +1224,7 @@ async function executeTool(browserState, toolCall, snapshot, config, store, obse
|
|
|
1224
1224
|
return result;
|
|
1225
1225
|
}
|
|
1226
1226
|
case "tmux_send_keys": {
|
|
1227
|
-
const result = await sendTmuxKeys(args);
|
|
1227
|
+
const result = await sendTmuxKeys(args, config);
|
|
1228
1228
|
const eventResult = sanitizeToolResult(result);
|
|
1229
1229
|
await store.appendEvent(result.ok ? "tool.completed" : "tool.failed", eventResult);
|
|
1230
1230
|
observers.event(result.ok ? "tool.completed" : "tool.failed", eventResult);
|
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."
|
|
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."
|
|
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.`
|
|
@@ -786,7 +786,7 @@ export async function requestNextStep(client, config, messages) {
|
|
|
786
786
|
function: {
|
|
787
787
|
name: "tmux_send_keys",
|
|
788
788
|
description:
|
|
789
|
-
"Send literal text and/or safe control keys to a durable host tmux pane. Use for interacting with known shells or agent sessions after capturing context. Do not send secrets, passwords, sudo passwords, or
|
|
789
|
+
"Send literal text and/or safe control keys to a durable host tmux pane. Use for interacting with known shells or agent sessions after capturing context. Do not send secrets, passwords, sudo passwords, destructive commands, or absolute host paths outside the workspace unless the run is explicitly in host sandbox mode.",
|
|
790
790
|
parameters: {
|
|
791
791
|
type: "object",
|
|
792
792
|
properties: {
|
|
@@ -812,7 +812,7 @@ export async function requestNextStep(client, config, messages) {
|
|
|
812
812
|
function: {
|
|
813
813
|
name: "tmux_start_session",
|
|
814
814
|
description:
|
|
815
|
-
"Start a detached durable host tmux session rooted inside the workspace, optionally with a startup command. Use for long-running local jobs that should be monitored with tmux_capture_pane instead of blocking the agent. For one-shot commands, redirect output and exit status to a durable workspace log or keep the shell open so capture can verify; do not claim results from an auto-terminated session.
|
|
815
|
+
"Start a detached durable host tmux session rooted inside the workspace, optionally with a startup command. Use for long-running local jobs that should be monitored with tmux_capture_pane instead of blocking the agent. For one-shot commands, redirect output and exit status to a durable workspace log or keep the shell open so capture can verify; do not claim results from an auto-terminated session. In Docker sandbox mode, startup commands must stay workspace-bound and must not reference absolute host paths outside the project; ask for --sandbox-mode host for trusted whole-host work.",
|
|
816
816
|
parameters: {
|
|
817
817
|
type: "object",
|
|
818
818
|
properties: {
|
package/src/tmux-tools.js
CHANGED
|
@@ -30,6 +30,8 @@ const ALLOWED_KEYS = new Set([
|
|
|
30
30
|
const SECRET_PATTERN = /(api[_-]?key|auth[_-]?token|npm[_-]?token|_authToken|password|passwd|secret|bearer\s+[A-Za-z0-9._-]+)/i;
|
|
31
31
|
const DESTRUCTIVE_PATTERN =
|
|
32
32
|
/\b(rm\s+-[^\n;]*[rf][^\n;]*(\/|\*|~|\$HOME)|mkfs(?:\.[a-z0-9]+)?\b|dd\s+if=.*\s+of=\/dev\/|shutdown\b|reboot\b|poweroff\b)/i;
|
|
33
|
+
const ABSOLUTE_PATH_PATTERN = /(^|[\s"'`=(:])((?:\/[A-Za-z0-9._@%+~:-]+)+\/?)/g;
|
|
34
|
+
const ALWAYS_ALLOWED_ABSOLUTE_PATHS = new Set(["/dev/null"]);
|
|
33
35
|
|
|
34
36
|
export const TMUX_TOOL_NAMES = ["tmux_list_sessions", "tmux_capture_pane", "tmux_send_keys", "tmux_start_session"];
|
|
35
37
|
|
|
@@ -103,6 +105,37 @@ function resolveCwd(config, cwd = ".") {
|
|
|
103
105
|
return { ok: true, cwd: requested };
|
|
104
106
|
}
|
|
105
107
|
|
|
108
|
+
function isInsideDirectory(root, candidate) {
|
|
109
|
+
const relative = path.relative(root, candidate);
|
|
110
|
+
return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function uniqueAbsolutePaths(text = "") {
|
|
114
|
+
const paths = new Set();
|
|
115
|
+
for (const match of String(text || "").matchAll(ABSOLUTE_PATH_PATTERN)) {
|
|
116
|
+
const candidate = match[2];
|
|
117
|
+
if (!candidate || candidate.startsWith("//")) continue;
|
|
118
|
+
paths.add(candidate.replace(/[),.;]+$/g, ""));
|
|
119
|
+
}
|
|
120
|
+
return [...paths].filter(Boolean);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function checkWorkspaceBoundTmuxText(text = "", config = {}, label = "tmux text") {
|
|
124
|
+
if (!config.useDockerSandbox) return { ok: true };
|
|
125
|
+
const root = path.resolve(config.commandCwd || process.cwd());
|
|
126
|
+
for (const candidate of uniqueAbsolutePaths(text)) {
|
|
127
|
+
if (ALWAYS_ALLOWED_ABSOLUTE_PATHS.has(candidate)) continue;
|
|
128
|
+
const resolved = path.resolve(candidate);
|
|
129
|
+
if (!isInsideDirectory(root, resolved)) {
|
|
130
|
+
return {
|
|
131
|
+
ok: false,
|
|
132
|
+
reason: `${label} references an absolute host path outside the configured workspace while Docker sandbox mode is active: ${candidate}. Use a workspace-relative path or rerun with --sandbox-mode host for trusted whole-host access.`,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return { ok: true };
|
|
137
|
+
}
|
|
138
|
+
|
|
106
139
|
function parseSessions(stdout = "") {
|
|
107
140
|
return String(stdout || "")
|
|
108
141
|
.split(/\r?\n/)
|
|
@@ -186,7 +219,7 @@ export async function captureTmuxPane(args = {}) {
|
|
|
186
219
|
};
|
|
187
220
|
}
|
|
188
221
|
|
|
189
|
-
export async function sendTmuxKeys(args = {}) {
|
|
222
|
+
export async function sendTmuxKeys(args = {}, config = {}) {
|
|
190
223
|
const target = validateTarget(args.target);
|
|
191
224
|
if (!target.ok) return { ok: false, toolName: "tmux_send_keys", blocked: true, reason: target.reason };
|
|
192
225
|
const text = String(args.text || "");
|
|
@@ -201,6 +234,10 @@ export async function sendTmuxKeys(args = {}) {
|
|
|
201
234
|
if (DESTRUCTIVE_PATTERN.test(text)) {
|
|
202
235
|
return { ok: false, toolName: "tmux_send_keys", blocked: true, reason: "tmux text appears destructive; ask the user before sending it." };
|
|
203
236
|
}
|
|
237
|
+
const workspaceBound = checkWorkspaceBoundTmuxText(text, config, "tmux text");
|
|
238
|
+
if (!workspaceBound.ok) {
|
|
239
|
+
return { ok: false, toolName: "tmux_send_keys", blocked: true, reason: workspaceBound.reason };
|
|
240
|
+
}
|
|
204
241
|
for (const key of keys) {
|
|
205
242
|
if (!ALLOWED_KEYS.has(key)) {
|
|
206
243
|
return { ok: false, toolName: "tmux_send_keys", blocked: true, reason: `Unsupported tmux key: ${key}` };
|
|
@@ -252,6 +289,10 @@ export async function startTmuxSession(args = {}, config = {}) {
|
|
|
252
289
|
reason: "tmux startup command appears destructive; ask the user before starting it.",
|
|
253
290
|
};
|
|
254
291
|
}
|
|
292
|
+
const workspaceBound = checkWorkspaceBoundTmuxText(command, config, "tmux startup command");
|
|
293
|
+
if (!workspaceBound.ok) {
|
|
294
|
+
return { ok: false, toolName: "tmux_start_session", blocked: true, reason: workspaceBound.reason };
|
|
295
|
+
}
|
|
255
296
|
|
|
256
297
|
const tmuxArgs = ["new-session", "-d", "-s", name.name, "-c", cwd.cwd];
|
|
257
298
|
if (command) tmuxArgs.push(command);
|
|
@@ -289,6 +330,8 @@ export function checkTmuxToolUse(toolName, args = {}, config = {}) {
|
|
|
289
330
|
if (DESTRUCTIVE_PATTERN.test(text)) {
|
|
290
331
|
return { allowed: false, reason: "tmux text appears destructive; ask the user before sending it.", category: "tmux" };
|
|
291
332
|
}
|
|
333
|
+
const workspaceBound = checkWorkspaceBoundTmuxText(text, config, "tmux text");
|
|
334
|
+
if (!workspaceBound.ok) return { allowed: false, reason: workspaceBound.reason, category: "tmux" };
|
|
292
335
|
for (const key of Array.isArray(args.keys) ? args.keys : []) {
|
|
293
336
|
if (!ALLOWED_KEYS.has(String(key))) return { allowed: false, reason: `Unsupported tmux key: ${key}`, category: "tmux" };
|
|
294
337
|
}
|
|
@@ -309,6 +352,8 @@ export function checkTmuxToolUse(toolName, args = {}, config = {}) {
|
|
|
309
352
|
if (DESTRUCTIVE_PATTERN.test(command)) {
|
|
310
353
|
return { allowed: false, reason: "tmux startup command appears destructive; ask the user before starting it.", category: "tmux" };
|
|
311
354
|
}
|
|
355
|
+
const workspaceBound = checkWorkspaceBoundTmuxText(command, config, "tmux startup command");
|
|
356
|
+
if (!workspaceBound.ok) return { allowed: false, reason: workspaceBound.reason, category: "tmux" };
|
|
312
357
|
return { allowed: true, category: "tmux" };
|
|
313
358
|
}
|
|
314
359
|
return { allowed: true, category: "tmux" };
|