aiterm-mcp 0.25.3 → 0.27.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 +11 -9
- package/dist/claude-stop-hook.js +20 -34
- package/dist/core.js +223 -196
- package/dist/grok-stop-hook.js +12 -16
- package/dist/index.js +5 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -165,7 +165,7 @@ collection is off by default and performs no network I/O. It ships via
|
|
|
165
165
|
tag-triggered CI with npm provenance (OIDC Trusted Publishing); the GitHub
|
|
166
166
|
Release re-registers the Official MCP Registry entry.
|
|
167
167
|
|
|
168
|
-
**Status:** actively maintained · the newcomer here, betting on a different shape (see [vs. the alternatives](#vs-the-alternatives)) · runs on Linux · WSL2 · macOS · native Windows
|
|
168
|
+
**Status:** actively maintained · the newcomer here, betting on a different shape (see [vs. the alternatives](#vs-the-alternatives)) · runs on Linux · WSL2 · macOS · native Windows (tmux on POSIX, the tmux-CLI-compatible [psmux](https://github.com/psmux/psmux) on native Windows — all four launchers and correlated completion included, no WSL required) · MIT · see the [CHANGELOG](CHANGELOG.md).
|
|
169
169
|
|
|
170
170
|
## Why now
|
|
171
171
|
|
|
@@ -221,7 +221,7 @@ One call per model, so the tool name itself tells you which model you get:
|
|
|
221
221
|
| --- | --- | --- |
|
|
222
222
|
| `claude_agent` | Claude Code CLI (Anthropic) | `prompt?`, `throughline_source_session?`, `model?`, `reasoning_effort?` (`low`/`medium`/`high`/`xhigh`/`max`), `env_vars?`, `cwd?`, `session_name?`, `launch_operation_id?` |
|
|
223
223
|
| `codex_agent` | Codex CLI (OpenAI; terminal config/CLI default unless overridden) | `prompt?`, `throughline_source_session?`, `model?`, `reasoning_effort?` (`low`/`medium`/`high`/`xhigh`/`max`/`ultra`; ultra enables proactive automatic delegation), `env_vars?`, `cwd?`, `session_name?`, `write_scope?` |
|
|
224
|
-
| `grok_agent` | Grok Build, model `grok-4.
|
|
224
|
+
| `grok_agent` | Grok Build, model `grok-4.6` by default (`model?` overrides) (xAI) | `prompt?`, `throughline_source_session?`, `model?`, `reasoning_effort?` (vendor-supported value), `env_vars?`, `cwd?`, `session_name?`, `write_scope?` |
|
|
225
225
|
| `composer_agent` | Grok Build, model `grok-composer-2.5-fast` by default (`model?` overrides); every model is live-catalog checked (xAI) | `prompt?`, `throughline_source_session?`, `model?`, `reasoning_effort?` (vendor-supported value), `env_vars?`, `cwd?`, `session_name?`, `write_scope?` |
|
|
226
226
|
|
|
227
227
|
`env_vars` is an allowlist of environment-variable **names**, not a name/value map. At launch,
|
|
@@ -242,7 +242,7 @@ places its returned context before a fixed separator and the mission. This route
|
|
|
242
242
|
`throughline >= 0.9.0`; it reads the source memory without changing that database's session
|
|
243
243
|
ownership. No Throughline dependency is needed when the field is omitted.
|
|
244
244
|
|
|
245
|
-
All four launchers forward `model` and `reasoning_effort` through public CLI flags. Explicit Grok/Composer models and Composer's default model are checked against the current `grok models` catalog before a PTY exists; missing models are errors, with no cache, retry, or fallback to another model. Pass an absolute path for `cwd` — `~` is not expanded. Durable callers can make a promptless Claude launch exactly replayable by passing an explicit `session_name` and `launch_operation_id`. Claude adds a launch-local settings file only for the correlated Stop hook and loads it together with normal `user,project,local` setting sources; it does not replace normal hooks, MCPs, plugins, permissions, or trust state. The hook event contains no answer body, and the bounded owner-only result is returned by `pty_read({ agent_transcript:true })` without reading Claude's private transcript. While a correlated Claude turn is active, raw sends and non-interrupt keys are rejected. Exact `/login` and `/logout` dispatches are rejected so shared authentication is repaired once in a normal terminal. Codex reads its normal rollout store; Grok/Composer read their normal session event/history files. Before dispatch, Codex waits for an idle TUI, while Grok/Composer additionally require the vendor's structured `mcp_init_completed` event so a visible input box cannot accept a prompt too early. Correlated completion
|
|
245
|
+
All four launchers forward `model` and `reasoning_effort` through public CLI flags. Explicit Grok/Composer models and Composer's default model are checked against the current `grok models` catalog before a PTY exists; missing models are errors, with no cache, retry, or fallback to another model. Pass an absolute path for `cwd` — `~` is not expanded. Durable callers can make a promptless Claude launch exactly replayable by passing an explicit `session_name` and `launch_operation_id`. Claude adds a launch-local settings file only for the correlated Stop hook and loads it together with normal `user,project,local` setting sources; it does not replace normal hooks, MCPs, plugins, permissions, or trust state. The hook event contains no answer body, and the bounded owner-only result is returned by `pty_read({ agent_transcript:true })` without reading Claude's private transcript. While a correlated Claude turn is active, raw sends and non-interrupt keys are rejected. Exact `/login` and `/logout` dispatches are rejected so shared authentication is repaired once in a normal terminal. Codex reads its normal rollout store; Grok/Composer read their normal session event/history files. Before dispatch, Codex waits for an idle TUI, while Grok/Composer additionally require the vendor's structured `mcp_init_completed` event so a visible input box cannot accept a prompt too early. Correlated completion works for all four vendors on Linux, WSL2, macOS, and native Windows. On native Windows the Grok/Composer launchers start only the Windows-native `grok.exe` (a WSL-side grok is rejected before a session is created, so vendor auth and session records never split across an OS boundary), and Claude/Codex completion reads the same Windows-side hook/rollout records the vendors write.
|
|
246
246
|
|
|
247
247
|
There is no hidden protocol between agents: a launched Claude, Codex, Grok, or Composer is another user-visible persistent terminal session. The MCP client drives that TUI with ordinary PTY operations, and a human can attach to watch or take over.
|
|
248
248
|
|
|
@@ -446,10 +446,10 @@ Each launcher starts a specific vendor's interactive coding-agent TUI inside a f
|
|
|
446
446
|
| --- | --- | --- |
|
|
447
447
|
| `claude_agent` | Claude Code CLI (Anthropic) | `prompt?`, `throughline_source_session?`, `model?`, `reasoning_effort?` (`low`/`medium`/`high`/`xhigh`/`max`), `env_vars?`, `cwd?`, `session_name?`, `launch_operation_id?` |
|
|
448
448
|
| `codex_agent` | Codex CLI (OpenAI; terminal config/CLI default unless overridden) | `prompt?`, `throughline_source_session?`, `model?`, `reasoning_effort?` (`low`/`medium`/`high`/`xhigh`/`max`/`ultra`; ultra enables proactive automatic delegation), `env_vars?`, `cwd?`, `session_name?`, `write_scope?` |
|
|
449
|
-
| `grok_agent` | Grok Build, model `grok-4.
|
|
449
|
+
| `grok_agent` | Grok Build, model `grok-4.6` by default (`model?` overrides) (xAI) | `prompt?`, `throughline_source_session?`, `model?`, `reasoning_effort?` (vendor-supported value), `env_vars?`, `cwd?`, `session_name?`, `write_scope?` |
|
|
450
450
|
| `composer_agent` | Grok Build, model `grok-composer-2.5-fast` by default (`model?` overrides); every model is live-catalog checked (xAI) | `prompt?`, `throughline_source_session?`, `model?`, `reasoning_effort?` (vendor-supported value), `env_vars?`, `cwd?`, `session_name?`, `write_scope?` |
|
|
451
451
|
|
|
452
|
-
The vendor CLI must be installed and authenticated (`claude` for `claude_agent`; `codex` for `codex_agent`; `grok` for both Grok tools). Binary resolution uses `CLAUDE_BIN` / `CODEX_BIN` / `GROK_BIN`, then each documented default location, then `PATH`. Missing binaries, invalid model/effort values, unavailable Grok/Composer catalog models, and nonexistent `cwd` fail before a session is created. Claude additionally requires a structured healthy authentication status before any PTY exists, and correlated Claude sessions reject `/login` and `/logout`; repair authentication once in a normal terminal. All four launchers share the normal project/user environment and the same non-blocking dispatch contract. Claude, Codex, Grok, and Composer depth-1 live smokes and a Claude depth-2 nested-delegation smoke are green; fixture coverage remains a separate claim. Native Windows
|
|
452
|
+
The vendor CLI must be installed and authenticated (`claude` for `claude_agent`; `codex` for `codex_agent`; `grok` for both Grok tools). Binary resolution uses `CLAUDE_BIN` / `CODEX_BIN` / `GROK_BIN`, then each documented default location, then `PATH`. Missing binaries, invalid model/effort values, unavailable Grok/Composer catalog models, and nonexistent `cwd` fail before a session is created. Claude additionally requires a structured healthy authentication status before any PTY exists, and correlated Claude sessions reject `/login` and `/logout`; repair authentication once in a normal terminal. All four launchers share the normal project/user environment and the same non-blocking dispatch contract. Claude, Codex, Grok, and Composer depth-1 live smokes and a Claude depth-2 nested-delegation smoke are green; fixture coverage remains a separate claim. Native Windows supports all four launchers with correlated completion (Claude verified with a live end-to-end launch on psmux ≥ 3.3.8; Grok launches only the Windows-native `grok.exe`).
|
|
453
453
|
|
|
454
454
|
Set `throughline_source_session` together with a non-empty mission in `prompt` to prepend
|
|
455
455
|
Throughline's read-only handoff context. This optional route requires `throughline >= 0.9.0`,
|
|
@@ -490,14 +490,14 @@ Each `pty_send` accepts at most 64 KiB of UTF-8 text. Sends to the same session
|
|
|
490
490
|
|
|
491
491
|
## A human can watch
|
|
492
492
|
|
|
493
|
-
Sessions live on a shared tmux socket. The `tmux -S … attach -t <id>` line printed by `pty_open` (and by each agent launcher) lets a human attach to the same terminal and intervene (`Ctrl-b d` to detach) — including watching a launched Claude/Codex/Grok/Composer session run and taking the keyboard from your AI mid-task. On native Windows the printed line is the
|
|
493
|
+
Sessions live on a shared tmux socket. The `tmux -S … attach -t <id>` line printed by `pty_open` (and by each agent launcher) lets a human attach to the same terminal and intervene (`Ctrl-b d` to detach) — including watching a launched Claude/Codex/Grok/Composer session run and taking the keyboard from your AI mid-task. On native Windows the printed line is the psmux form — `psmux -L <namespace> attach -t <id>` — pointing at the same Windows-native session.
|
|
494
494
|
|
|
495
495
|
## Requirements
|
|
496
496
|
|
|
497
497
|
- **Node.js >= 18**
|
|
498
498
|
- **tmux** (runtime prerequisite; check with `tmux -V`. Install with `apt install tmux` / `brew install tmux`)
|
|
499
499
|
- **macOS / Linux / WSL2** run tmux directly. On macOS install it with `brew install tmux` (stock macOS ships none). If your MCP client is launched from the **GUI** rather than a terminal, Homebrew's bin (`/opt/homebrew/bin` on Apple Silicon, `/usr/local/bin` on Intel) may be off its `PATH`; aiterm auto-searches those locations, or set **`AITERM_TMUX=/path/to/tmux`** to point at it explicitly.
|
|
500
|
-
- **Native Windows** has no tmux, so aiterm
|
|
500
|
+
- **Native Windows** has no tmux, so aiterm drives [psmux](https://github.com/psmux/psmux) — a tmux-CLI-compatible native multiplexer — with a per-install `-L` namespace. **No WSL is required.** Install psmux **3.3.8 or newer** (`winget install marlocarlo.psmux`; 3.3.8 is the first release whose `pipe-pane` file sink, byte-exact `paste-buffer` wire, and foreground `#{pane_current_command}` behave the way aiterm's capture/dispatch paths rely on), plus [Git for Windows](https://gitforwindows.org/) whose `bash.exe` becomes the pane shell (System32's `bash.exe` is the WSL launcher and is deliberately not used). Override resolution with **`AITERM_PSMUX`** / **`AITERM_BASH`** when the binaries live elsewhere. You reach Windows tools the same way you reach SSH: `pty_send "powershell.exe …"` nests into PowerShell. `grok_agent`/`composer_agent` launch the **Windows-native** Grok CLI (`%USERPROFILE%\.grok\bin\grok.exe`, or `GROK_BIN` pointing at a `.exe`) as a Windows process, and a WSL-side grok is rejected before a session is created so vendor auth and session records never split across an OS boundary.
|
|
501
501
|
- For the **agent launchers**: the corresponding vendor CLI, installed and authenticated — `claude` for `claude_agent`, `codex` for `codex_agent`, `grok` for `grok_agent` / `composer_agent`. Portable fork additionally needs `throughline >= 0.9.0`; ordinary clean launch does not. (Not needed if you only use the PTY tools.)
|
|
502
502
|
- Optional: the [`rtk`](https://github.com/rtk-ai/rtk) binary (used by `pty_send`'s `rtk: true` delegation; works fine without it)
|
|
503
503
|
|
|
@@ -525,8 +525,10 @@ Development uses focused local tests first. The final GitHub Actions gate starts
|
|
|
525
525
|
`npm test` concurrently on self-hosted macOS native, Linux native, Windows native, and WSL2
|
|
526
526
|
runners; it does not replace any OS with a reduced suite. Tag-triggered npm publishing runs only
|
|
527
527
|
after all four environments pass and the tagged commit is confirmed on `origin/main`. The native
|
|
528
|
-
Windows runner
|
|
529
|
-
`NETWORK SERVICE`
|
|
528
|
+
Windows runner needs psmux ≥ 3.3.8 and Git for Windows on its PATH, and must run as an
|
|
529
|
+
interactive Windows user; `NETWORK SERVICE` lacks the per-user environment the pane shell and
|
|
530
|
+
vendor CLIs rely on and is not a valid runner identity (the separate WSL2 runner still owns the
|
|
531
|
+
initialized WSL distro).
|
|
530
532
|
|
|
531
533
|
Logic lives in `src/core.ts` (tmux control, reduction, completion detection, safety, agent launch) and `src/rtk.ts` (per-command reducers); `src/index.ts` is the MCP surface. The design origin and the reducer's porting source (the pytest reducer is ported to match upstream rtk 0.42.0, except the deliberate `FAILED`-line difference noted above, and is locked by regression tests) are in `prototype/python/`.
|
|
532
534
|
|
package/dist/claude-stop-hook.js
CHANGED
|
@@ -22,8 +22,10 @@ function hasAitermEnv() {
|
|
|
22
22
|
process.env.AITERM_AGENT_LAUNCH_ID);
|
|
23
23
|
}
|
|
24
24
|
function uid() {
|
|
25
|
+
// state root のパス構成要素 `aiterm-mcp-<uid>` にだけ使う。Windows(native) は
|
|
26
|
+
// process.getuid を持たないため core の currentUid() と同じ受容で 0 を返す。
|
|
25
27
|
if (typeof process.getuid !== "function")
|
|
26
|
-
|
|
28
|
+
return 0;
|
|
27
29
|
return process.getuid();
|
|
28
30
|
}
|
|
29
31
|
function runtimeStateBase() {
|
|
@@ -39,18 +41,13 @@ function runtimeStateBase() {
|
|
|
39
41
|
}
|
|
40
42
|
return os.tmpdir();
|
|
41
43
|
}
|
|
42
|
-
function
|
|
44
|
+
function agentsDir() {
|
|
45
|
+
// state root は OS が与える per-user runtime dir(XDG_RUNTIME_DIR / os.tmpdir())の下にある。
|
|
46
|
+
// 以前はここで symlink・owner・mode を検査していたが、共有 /tmp に敵対的な同居主体がいる
|
|
47
|
+
// 前提の防御であり、対応 OS の既定配置では成立しない(オーナー裁定 2026-08-19)。
|
|
48
|
+
// 経路の異常は open/stat の OS エラーとしてそのまま露出させる。
|
|
43
49
|
const root = path.join(runtimeStateBase(), `aiterm-mcp-${uid()}`);
|
|
44
|
-
|
|
45
|
-
const rst = fs.lstatSync(root);
|
|
46
|
-
if (!rst.isDirectory() || rst.isSymbolicLink() || rst.uid !== uid() || (rst.mode & 0o077) !== 0) {
|
|
47
|
-
fail(`agent state root が安全ではありません: ${root}`);
|
|
48
|
-
}
|
|
49
|
-
const ast = fs.lstatSync(agents);
|
|
50
|
-
if (!ast.isDirectory() || ast.isSymbolicLink() || ast.uid !== uid() || (ast.mode & 0o077) !== 0) {
|
|
51
|
-
fail(`agent state dir が安全ではありません: ${agents}`);
|
|
52
|
-
}
|
|
53
|
-
return agents;
|
|
50
|
+
return path.join(root, "agents");
|
|
54
51
|
}
|
|
55
52
|
async function readStdin() {
|
|
56
53
|
const chunks = [];
|
|
@@ -69,12 +66,8 @@ function writeResult(file, value) {
|
|
|
69
66
|
if (Buffer.byteLength(body, "utf8") > MAX_STDIN_BYTES)
|
|
70
67
|
fail("result file が大きすぎます");
|
|
71
68
|
const tmp = `${file}.${process.pid}.${randomBytes(6).toString("hex")}.tmp`;
|
|
72
|
-
const
|
|
73
|
-
const fd = fs.openSync(tmp, fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_WRONLY | nofollow, 0o600);
|
|
69
|
+
const fd = fs.openSync(tmp, fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_WRONLY, 0o600);
|
|
74
70
|
try {
|
|
75
|
-
const st = fs.fstatSync(fd);
|
|
76
|
-
if (!st.isFile() || st.uid !== uid() || st.nlink !== 1 || (st.mode & 0o077) !== 0)
|
|
77
|
-
fail("result temp file が安全ではありません");
|
|
78
71
|
const expected = Buffer.byteLength(body, "utf8");
|
|
79
72
|
const written = fs.writeSync(fd, body, undefined, "utf8");
|
|
80
73
|
if (written !== expected)
|
|
@@ -97,16 +90,13 @@ function writeResult(file, value) {
|
|
|
97
90
|
}
|
|
98
91
|
}
|
|
99
92
|
function appendEvent(file, event) {
|
|
100
|
-
const nofollow = fs.constants.O_NOFOLLOW ?? 0;
|
|
101
93
|
const line = JSON.stringify(event) + "\n";
|
|
102
94
|
if (Buffer.byteLength(line, "utf8") > 64 * 1024)
|
|
103
95
|
fail("event line が大きすぎます");
|
|
104
|
-
const fd = fs.openSync(file, fs.constants.O_CREAT | fs.constants.O_APPEND | fs.constants.O_WRONLY
|
|
96
|
+
const fd = fs.openSync(file, fs.constants.O_CREAT | fs.constants.O_APPEND | fs.constants.O_WRONLY, 0o600);
|
|
105
97
|
try {
|
|
98
|
+
// st は短書き込み時の巻き戻し(ftruncate)に使う。安全性検査としては使わない。
|
|
106
99
|
const st = fs.fstatSync(fd);
|
|
107
|
-
if (!st.isFile() || st.uid !== uid() || st.nlink !== 1 || (st.mode & 0o077) !== 0) {
|
|
108
|
-
fail(`event file が安全ではありません: ${file}`);
|
|
109
|
-
}
|
|
110
100
|
const expected = Buffer.byteLength(line, "utf8");
|
|
111
101
|
const written = fs.writeSync(fd, line, undefined, "utf8");
|
|
112
102
|
if (written !== expected) {
|
|
@@ -120,10 +110,9 @@ function appendEvent(file, event) {
|
|
|
120
110
|
}
|
|
121
111
|
}
|
|
122
112
|
function readOperationMarker(file) {
|
|
123
|
-
const nofollow = fs.constants.O_NOFOLLOW ?? 0;
|
|
124
113
|
let fd;
|
|
125
114
|
try {
|
|
126
|
-
fd = fs.openSync(file, fs.constants.O_RDONLY
|
|
115
|
+
fd = fs.openSync(file, fs.constants.O_RDONLY);
|
|
127
116
|
}
|
|
128
117
|
catch (error) {
|
|
129
118
|
if (error.code === "ENOENT")
|
|
@@ -132,8 +121,9 @@ function readOperationMarker(file) {
|
|
|
132
121
|
}
|
|
133
122
|
try {
|
|
134
123
|
const st = fs.fstatSync(fd);
|
|
135
|
-
|
|
136
|
-
|
|
124
|
+
// parse する入力の上限だけ残す(owner・link 数・mode の検査は撤去した)。
|
|
125
|
+
if (st.size > 1024) {
|
|
126
|
+
fail(`operation markerが大きすぎます: ${file}`);
|
|
137
127
|
}
|
|
138
128
|
const body = fs.readFileSync(fd, "utf8");
|
|
139
129
|
const value = JSON.parse(body);
|
|
@@ -161,13 +151,9 @@ function consumeOperationMarker(file, marker) {
|
|
|
161
151
|
catch {
|
|
162
152
|
fail("operation markerが完了記録中に消失しました");
|
|
163
153
|
}
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
st.nlink !== 1 ||
|
|
168
|
-
(st.mode & 0o077) !== 0 ||
|
|
169
|
-
st.dev !== marker.dev ||
|
|
170
|
-
st.ino !== marker.ino) {
|
|
154
|
+
// dev/ino は「先に stat したのと同じ実体か」という同一性の検査であり、operation 相関の
|
|
155
|
+
// 正しさそのもの。撤去した安全設備(symlink・owner・link 数・mode)とは別物なので残す。
|
|
156
|
+
if (st.dev !== marker.dev || st.ino !== marker.ino) {
|
|
171
157
|
fail("operation markerが完了記録中に置換されました");
|
|
172
158
|
}
|
|
173
159
|
fs.unlinkSync(file);
|
|
@@ -202,7 +188,7 @@ async function main() {
|
|
|
202
188
|
if (resultBytes > MAX_RESULT_BYTES)
|
|
203
189
|
fail(`assistant result が${MAX_RESULT_BYTES} bytesを超えています`);
|
|
204
190
|
const resultDigest = createHash("sha256").update(text, "utf8").digest("hex");
|
|
205
|
-
const agents =
|
|
191
|
+
const agents = agentsDir();
|
|
206
192
|
const resultFile = path.join(agents, `${session}.${launchId}.claude-result.json`);
|
|
207
193
|
const eventFile = path.join(agents, `${session}.${launchId}.events.jsonl`);
|
|
208
194
|
const operationFile = path.join(agents, `${session}.${launchId}.claude-operation.json`);
|
package/dist/core.js
CHANGED
|
@@ -15,19 +15,18 @@ import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
|
15
15
|
import { fileURLToPath } from "node:url";
|
|
16
16
|
import * as rtk from "./rtk.js";
|
|
17
17
|
import { recordRuntimeError } from "./runtime-error-store.js";
|
|
18
|
-
// Windows ネイティブには tmux
|
|
19
|
-
// (POSIX = Linux/WSL2/macOS は従来どおり tmux
|
|
18
|
+
// Windows ネイティブには tmux が無いため、tmux CLI 互換の native psmux を叩く
|
|
19
|
+
// (POSIX = Linux/WSL2/macOS は従来どおり tmux を直接叩く。WSL 橋は e3f5fc8 で全廃)。
|
|
20
20
|
const isWin = process.platform === "win32";
|
|
21
21
|
// .log/.offset/.lastcmd を置くディレクトリ(Node が直接読み書きする)。
|
|
22
22
|
// POSIX は従来どおり。Windows は TMPDIR→TEMP→os.tmpdir() の順で Windows 側の一時領域に置く。
|
|
23
23
|
const SOCKDIR = path.join(process.env.TMPDIR ?? (isWin ? process.env.TEMP ?? os.tmpdir() : "/tmp"), "claude-tmux-sockets");
|
|
24
|
-
// tmux -S
|
|
25
|
-
// Windows は
|
|
26
|
-
//
|
|
27
|
-
// TMPDIR ごとの隔離(テスト)と再起動跨ぎ再接続を両立する。
|
|
28
|
-
const SOCK =
|
|
29
|
-
|
|
30
|
-
: path.join(SOCKDIR, "claude.sock");
|
|
24
|
+
// tmux -S に渡すソケットパス(POSIX はログと同じツリーに置く)。
|
|
25
|
+
// Windows は native psmux を使い、-S でなく -L server namespace で隔離する
|
|
26
|
+
// (psmux の -S は黙って既定 namespace へ落ちるため使わない・実測 2026-08-16)。
|
|
27
|
+
// SOCKDIR から短い安定名を導出し、TMPDIR ごとの隔離(テスト)と再起動跨ぎ再接続を両立する。
|
|
28
|
+
const SOCK = path.join(SOCKDIR, "claude.sock");
|
|
29
|
+
const WIN_NS = `aiterm-${createHash("sha1").update(SOCKDIR).digest("hex").slice(0, 12)}`;
|
|
31
30
|
// 完了検出
|
|
32
31
|
export const DEFAULT_TIMEOUT = 10.0;
|
|
33
32
|
const POLL = 0.25;
|
|
@@ -156,34 +155,26 @@ function ptyDependencyError(message, observe = true) {
|
|
|
156
155
|
ownTelemetryFailure("AITERM.PTY_DEPENDENCY_UNAVAILABLE", error, 2);
|
|
157
156
|
throw error;
|
|
158
157
|
}
|
|
159
|
-
// Windows
|
|
160
|
-
//
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
// -e(ログインシェル非経由)+短い timeout で、初回セットアップ未完了の wsl によるハングも防ぐ。
|
|
169
|
-
let winBridgeOk = false;
|
|
170
|
-
function ensureWinBridge(observe = true) {
|
|
171
|
-
if (winBridgeOk)
|
|
158
|
+
// Windows で最初の呼び出し前に一度だけ native psmux の可用性を確かめ、失敗は原因別に投げる。
|
|
159
|
+
// psmux は tmux CLI 互換の Windows ネイティブ実装(ConPTY・WSL 不要)。AITERM_PSMUX で
|
|
160
|
+
// バイナリを明示上書きできる(POSIX の AITERM_TMUX に対応)。
|
|
161
|
+
function psmuxBin() {
|
|
162
|
+
return process.env.AITERM_PSMUX || "psmux";
|
|
163
|
+
}
|
|
164
|
+
let winPsmuxOk = false;
|
|
165
|
+
function ensureWinPsmux(observe = true) {
|
|
166
|
+
if (winPsmuxOk)
|
|
172
167
|
return;
|
|
173
|
-
const r = spawnSync(
|
|
168
|
+
const r = spawnSync(psmuxBin(), ["-V"], { encoding: "utf8", timeout: 10000 });
|
|
174
169
|
if (r.error) {
|
|
175
170
|
const code = r.error.code;
|
|
176
|
-
if (code === "ETIMEDOUT")
|
|
177
|
-
ptyDependencyError("WSL が応答しません(初回セットアップ未完了の可能性)。一度 `wsl` を起動してから再実行してください。", observe);
|
|
178
171
|
if (code === "ENOENT")
|
|
179
|
-
ptyDependencyError("
|
|
180
|
-
ptyDependencyError(`
|
|
172
|
+
ptyDependencyError("psmux が見つかりません。Windows ネイティブでは psmux が必要です(導入例: winget install marlocarlo.psmux)。既存の psmux を使う場合は AITERM_PSMUX でパスを指定してください。", observe);
|
|
173
|
+
ptyDependencyError(`psmux を起動できませんでした(${code ?? "unknown"})。`, observe);
|
|
181
174
|
}
|
|
182
|
-
// wsl.exe は System32 にあるので「起動」は成功するが、ディストリ未導入や distro 内に tmux が無いと
|
|
183
|
-
// 非ゼロで終わる。両方を区別せず(wsl の出力は UTF-16 で文字化けし得るため)正直に表す。
|
|
184
175
|
if (r.status !== 0)
|
|
185
|
-
ptyDependencyError("
|
|
186
|
-
|
|
176
|
+
ptyDependencyError("psmux -V が失敗しました。`psmux -V` が通るか確認してください。", observe);
|
|
177
|
+
winPsmuxOk = true;
|
|
187
178
|
}
|
|
188
179
|
// tmux が見つからないときの説明。macOS は tmux を同梱せず、Homebrew の bin は GUI 起動時の PATH に
|
|
189
180
|
// 入らないため、原因と対処(導入・自動探索・AITERM_TMUX 上書き)を正直に提示する。
|
|
@@ -247,12 +238,12 @@ export function tmuxSpawnEnv() {
|
|
|
247
238
|
function tmuxCommandWithInput(observe, input, ...args) {
|
|
248
239
|
// maxBuffer は既定 1MiB。capture-pane(大きなスクロールバック)や多セッションの list-sessions で
|
|
249
240
|
// 頭打ちになり stdout が切れる/空になる。Python の subprocess.run は無制限だったので 64MiB へ広げる。
|
|
250
|
-
// Windows
|
|
241
|
+
// Windows は tmux CLI 互換の native psmux を -L namespace 隔離で叩く(WSL 非依存)。
|
|
251
242
|
let r;
|
|
252
243
|
const spawnOpts = { encoding: "utf8", maxBuffer: 64 * 1024 * 1024, input, env: tmuxSpawnEnv() };
|
|
253
244
|
if (isWin) {
|
|
254
|
-
|
|
255
|
-
r = spawnSync(
|
|
245
|
+
ensureWinPsmux(observe);
|
|
246
|
+
r = spawnSync(psmuxBin(), ["-L", WIN_NS, ...args], spawnOpts);
|
|
256
247
|
}
|
|
257
248
|
else {
|
|
258
249
|
// resolveTmux() は tmux を解決できなければ明確な AitermError を投げる(POSIX 版の事前確認)。
|
|
@@ -288,14 +279,23 @@ function sessionExists(name) {
|
|
|
288
279
|
}
|
|
289
280
|
function paneCurrentCommand(name) {
|
|
290
281
|
const r = tmux("display-message", "-p", "-t", name, "#{pane_current_command}");
|
|
291
|
-
|
|
282
|
+
if (r.code !== 0)
|
|
283
|
+
return "";
|
|
284
|
+
const cmd = r.stdout.trim();
|
|
285
|
+
// Windows(psmux)は "bash.exe" やフルパス形で報告しうる。SHELLS 等の POSIX 名
|
|
286
|
+
// 集合と突合できるよう、basename・.exe 除去・小文字化へ正規化する(Windows の
|
|
287
|
+
// 実行ファイル名は case-insensitive)。POSIX は従来どおり無加工。
|
|
288
|
+
if (!isWin)
|
|
289
|
+
return cmd;
|
|
290
|
+
return path.basename(cmd).replace(/\.exe$/i, "").toLowerCase();
|
|
292
291
|
}
|
|
293
292
|
function pasteBufferSupportsNoSanitizeFlag() {
|
|
294
293
|
const listed = tmux("list-commands");
|
|
295
294
|
if (listed.code !== 0) {
|
|
296
295
|
throw new AitermError(`tmux paste-buffer 能力の確認に失敗しました: ${listed.stderr.trim() || `code=${listed.code}`}`, 2);
|
|
297
296
|
}
|
|
298
|
-
|
|
297
|
+
// psmux の list-commands は行頭にインデントを持つため trim して突合する。
|
|
298
|
+
const usage = listed.stdout.split("\n").find((line) => line.trim().startsWith("paste-buffer"));
|
|
299
299
|
if (!usage)
|
|
300
300
|
throw new AitermError("tmux list-commands にpaste-bufferがありません", 2);
|
|
301
301
|
// tmux 3.4は制御文字を無変換でpasteし、-S自体が無い。3.7は既定でvis(3)変換し、
|
|
@@ -374,31 +374,19 @@ function stateRoot() {
|
|
|
374
374
|
const base = runtimeStateBase();
|
|
375
375
|
return path.join(base, `aiterm-mcp-${uid}`);
|
|
376
376
|
}
|
|
377
|
-
function
|
|
377
|
+
function ensureStateRoot() {
|
|
378
|
+
// state root は OS が与える per-user runtime dir(XDG_RUNTIME_DIR / os.tmpdir())の下に作る。
|
|
379
|
+
// 以前はここで symlink・owner・mode を検査していたが、共有 /tmp に敵対的な同居主体がいる
|
|
380
|
+
// 前提の防御であり、対応 OS の既定配置では成立しない(オーナー裁定 2026-08-19)。
|
|
381
|
+
// 作成時の 0o700 は検査ではなく妥当な既定として残す。経路の異常は以降の
|
|
382
|
+
// open/stat が OS エラーとしてそのまま露出させる。
|
|
378
383
|
const root = stateRoot();
|
|
379
384
|
fs.mkdirSync(root, { recursive: true, mode: 0o700 });
|
|
380
|
-
|
|
381
|
-
if (!st.isDirectory() || st.isSymbolicLink()) {
|
|
382
|
-
throw new AitermError(`agent state root が安全な directory ではありません: ${root}`, 2);
|
|
383
|
-
}
|
|
384
|
-
if (st.uid !== currentUid()) {
|
|
385
|
-
throw new AitermError(`agent state root の owner が現在ユーザーではありません: ${root}`, 2);
|
|
386
|
-
}
|
|
387
|
-
if ((st.mode & 0o077) !== 0) {
|
|
388
|
-
fs.chmodSync(root, 0o700);
|
|
389
|
-
}
|
|
390
|
-
const agents = path.join(root, "agents");
|
|
391
|
-
fs.mkdirSync(agents, { recursive: true, mode: 0o700 });
|
|
392
|
-
const ast = fs.lstatSync(agents);
|
|
393
|
-
if (!ast.isDirectory() || ast.isSymbolicLink() || ast.uid !== currentUid()) {
|
|
394
|
-
throw new AitermError(`agent state dir が安全な directory ではありません: ${agents}`, 2);
|
|
395
|
-
}
|
|
396
|
-
if ((ast.mode & 0o077) !== 0)
|
|
397
|
-
fs.chmodSync(agents, 0o700);
|
|
385
|
+
fs.mkdirSync(path.join(root, "agents"), { recursive: true, mode: 0o700 });
|
|
398
386
|
return root;
|
|
399
387
|
}
|
|
400
388
|
function agentsDir() {
|
|
401
|
-
return path.join(
|
|
389
|
+
return path.join(ensureStateRoot(), "agents");
|
|
402
390
|
}
|
|
403
391
|
function agentEventPath(name, launchId) {
|
|
404
392
|
assertSessionName(name);
|
|
@@ -450,22 +438,14 @@ function agentClaudeDispatchReceiptPath(name, launchId, operationId) {
|
|
|
450
438
|
return path.join(agentsDir(), `${name}.${launchId}.${validated.slice("sha256:".length)}.claude-dispatch`);
|
|
451
439
|
}
|
|
452
440
|
function existingAgentsDir() {
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
441
|
+
// 既存の agents dir があればその path、無ければ null(作成はしない)。
|
|
442
|
+
// 以前はここで symlink・owner・mode を検査していたが撤去した(オーナー裁定 2026-08-19)。
|
|
443
|
+
// なお「Windows で常に null を返す」形だった頃は close/killAll の agent state 掃除が
|
|
444
|
+
// 常に no-op になり、同名 session の再起動が「agent metadata が複数あります」で
|
|
445
|
+
// 失敗していた(実被弾 2026-08-15)。存在判定だけに絞ることでその轍も踏まない。
|
|
446
|
+
const dir = path.join(runtimeStateBase(), `aiterm-mcp-${currentUid()}`, "agents");
|
|
457
447
|
try {
|
|
458
|
-
|
|
459
|
-
if (!rst.isDirectory() || rst.isSymbolicLink() || rst.uid !== process.getuid())
|
|
460
|
-
return null;
|
|
461
|
-
if ((rst.mode & 0o077) !== 0)
|
|
462
|
-
fs.chmodSync(root, 0o700);
|
|
463
|
-
const st = fs.lstatSync(dir);
|
|
464
|
-
if (!st.isDirectory() || st.isSymbolicLink() || st.uid !== process.getuid())
|
|
465
|
-
return null;
|
|
466
|
-
if ((st.mode & 0o077) !== 0)
|
|
467
|
-
fs.chmodSync(dir, 0o700);
|
|
468
|
-
return dir;
|
|
448
|
+
return fs.statSync(dir).isDirectory() ? dir : null;
|
|
469
449
|
}
|
|
470
450
|
catch {
|
|
471
451
|
return null;
|
|
@@ -553,8 +533,8 @@ function readLastcmd(name) {
|
|
|
553
533
|
}
|
|
554
534
|
}
|
|
555
535
|
export function attachHint(name) {
|
|
556
|
-
// Windows は
|
|
557
|
-
const cmd = isWin ? `
|
|
536
|
+
// Windows は native psmux(tmux CLI 互換)を -L namespace で叩く。
|
|
537
|
+
const cmd = isWin ? `psmux -L ${WIN_NS} attach -t ${name}` : `tmux -S ${SOCK} attach -t ${name}`;
|
|
558
538
|
return (`このセッションを自分の目で見る/介入する:\n` +
|
|
559
539
|
` ${cmd}\n` +
|
|
560
540
|
` (抜けるには Ctrl-b d)`);
|
|
@@ -675,9 +655,10 @@ function captureScreen(name, lines) {
|
|
|
675
655
|
return r.code === 0 ? r.stdout : "";
|
|
676
656
|
}
|
|
677
657
|
const sleep = (ms) => new Promise((res) => setTimeout(res, ms));
|
|
678
|
-
// Windows 専用: pipe-pane のログは
|
|
679
|
-
//
|
|
680
|
-
//
|
|
658
|
+
// Windows 専用: pipe-pane のログは psmux server 側の in-process sink が書く=完了検知と
|
|
659
|
+
// 書き手が別 process のため、完了/セッション消滅の報告後も末尾数百バイトが遅れて現れうる。
|
|
660
|
+
// 完了と判定する直前にログサイズが伸びなくなるまで待ち、readOutput が末尾欠けの出力を
|
|
661
|
+
// 返さないようにする(POSIX の tmux は /bin/sh sink・同一 fs で実測上不要)。
|
|
681
662
|
async function settleWinLog(name) {
|
|
682
663
|
let prev = -1;
|
|
683
664
|
for (let i = 0; i < 8; i++) {
|
|
@@ -830,11 +811,8 @@ function completionSuffix(status) {
|
|
|
830
811
|
function rtkRewrite(text) {
|
|
831
812
|
if (text.trim().includes("\n"))
|
|
832
813
|
return text;
|
|
833
|
-
// timeout 無しだと rtk がハングしたとき send() ごと凍結する。Python は timeout=5
|
|
834
|
-
|
|
835
|
-
const r = isWin
|
|
836
|
-
? spawnSync("wsl.exe", ["-e", "rtk", "rewrite", text], { encoding: "utf8", timeout: 5000, maxBuffer: 1024 * 1024 })
|
|
837
|
-
: spawnSync("rtk", ["rewrite", text], { encoding: "utf8", timeout: 5000, maxBuffer: 1024 * 1024 });
|
|
814
|
+
// timeout 無しだと rtk がハングしたとき send() ごと凍結する。Python は timeout=5。不在は素通し。
|
|
815
|
+
const r = spawnSync("rtk", ["rewrite", text], { encoding: "utf8", timeout: 5000, maxBuffer: 1024 * 1024 });
|
|
838
816
|
if (r.error)
|
|
839
817
|
return text; // rtk 不在(ENOENT)・タイムアウト(ETIMEDOUT)等は元テキストへ素通し
|
|
840
818
|
if ((r.status === 0 || r.status === 3) && (r.stdout ?? "").trim())
|
|
@@ -850,7 +828,37 @@ function assertNotDestructive(text, code, context = "") {
|
|
|
850
828
|
}
|
|
851
829
|
}
|
|
852
830
|
// ---------------------------------------------------------------- 操作(return で返す / 失敗は AitermError)
|
|
831
|
+
// Windows native pane の既定 shell 解決。裸の "bash" は PATH 上で System32 の
|
|
832
|
+
// WSL launcher (bash.exe) に解決されてしまうため、Git for Windows の bash.exe を
|
|
833
|
+
// 明示解決する(WSL 非依存の裁定に従う)。AITERM_BASH で上書き可。
|
|
834
|
+
function resolveWinPaneShell(shell) {
|
|
835
|
+
if (!isWin || shell !== "bash")
|
|
836
|
+
return shell;
|
|
837
|
+
const fromEnv = process.env.AITERM_BASH;
|
|
838
|
+
if (fromEnv) {
|
|
839
|
+
if (isUsableExecutableFile(fromEnv))
|
|
840
|
+
return fromEnv;
|
|
841
|
+
throw new AitermError(`AITERM_BASH に指定された bash が存在しません: ${fromEnv}`, 2);
|
|
842
|
+
}
|
|
843
|
+
const candidates = [
|
|
844
|
+
path.join(process.env.ProgramFiles ?? "C:\\Program Files", "Git", "bin", "bash.exe"),
|
|
845
|
+
path.join(process.env["ProgramFiles(x86)"] ?? "C:\\Program Files (x86)", "Git", "bin", "bash.exe"),
|
|
846
|
+
];
|
|
847
|
+
const gitPath = spawnSync("where.exe", ["git.exe"], { encoding: "utf8", timeout: 5000 })
|
|
848
|
+
.stdout?.split(/\r?\n/)
|
|
849
|
+
.find(Boolean);
|
|
850
|
+
if (gitPath)
|
|
851
|
+
candidates.push(path.join(path.dirname(path.dirname(gitPath)), "bin", "bash.exe"));
|
|
852
|
+
for (const cand of candidates) {
|
|
853
|
+
if (isUsableExecutableFile(cand))
|
|
854
|
+
return cand;
|
|
855
|
+
}
|
|
856
|
+
throw new AitermError("Git Bash が見つかりません。Windows ネイティブの pane shell には Git for Windows の bash.exe が必要です" +
|
|
857
|
+
"(System32 の bash.exe は WSL launcher のため使いません)。Git for Windows を導入するか、" +
|
|
858
|
+
"AITERM_BASH に bash.exe のパスを指定してください。", 2);
|
|
859
|
+
}
|
|
853
860
|
export function openSession(name, shell = "bash") {
|
|
861
|
+
shell = resolveWinPaneShell(shell);
|
|
854
862
|
try {
|
|
855
863
|
fs.mkdirSync(SOCKDIR, { recursive: true });
|
|
856
864
|
}
|
|
@@ -877,7 +885,8 @@ export function openSession(name, shell = "bash") {
|
|
|
877
885
|
throw new AitermError(`session '${nm}' は既に存在します(list で確認)`, 2);
|
|
878
886
|
}
|
|
879
887
|
else {
|
|
880
|
-
|
|
888
|
+
// -f は端末個人の設定ファイルを読まないための空 config(Windows は NUL デバイス)。
|
|
889
|
+
const r = tmux("new-session", "-d", "-s", nm, ...banner, "-f", isWin ? "NUL" : "/dev/null", shell);
|
|
881
890
|
if (r.code === 0)
|
|
882
891
|
break;
|
|
883
892
|
// 自動採番かつ「重複名」由来の失敗(他エージェントが同名を先に取った)なら次名でリトライ。
|
|
@@ -908,8 +917,10 @@ export function openSession(name, shell = "bash") {
|
|
|
908
917
|
cleanupAgentState(nm);
|
|
909
918
|
// pipe-pane の引数は tmux 内部の /bin/sh -c で再解釈される(argv ではない)。パスは単一引用符で包み、
|
|
910
919
|
// パス自身の ' は '\'' イディオムでエスケープする(名前は検証済みだが、Windows ユーザー名 O'Brien 等が
|
|
911
|
-
// 一時パスに ' を持ち込み redirect を壊すのを防ぐ。空白対策も兼ねる)。
|
|
912
|
-
|
|
920
|
+
// 一時パスに ' を持ち込み redirect を壊すのを防ぐ。空白対策も兼ねる)。
|
|
921
|
+
// Windows の psmux は `cat > <path>` を in-process の直接ファイルsinkとして処理する
|
|
922
|
+
// (psmux e3e4b71 以降)。パスは Windows 形のまま渡す。
|
|
923
|
+
const pipeTarget = logpath(nm);
|
|
913
924
|
const quoted = `'${pipeTarget.replace(/'/g, "'\\''")}'`;
|
|
914
925
|
const pr = tmux("pipe-pane", "-t", nm, "-o", `cat >> ${quoted}`);
|
|
915
926
|
if (pr.code !== 0) {
|
|
@@ -1018,7 +1029,25 @@ export function send(name, text, o = {}) {
|
|
|
1018
1029
|
const partial = i > 0
|
|
1019
1030
|
? " 先行chunkはPTYに入力済みでEnterは未送信です。再送前に入力を確認・消去してください。"
|
|
1020
1031
|
: "";
|
|
1021
|
-
|
|
1032
|
+
// psmux の load-buffer は stdin (`-`) 非対応で path 位置引数だけを取る。
|
|
1033
|
+
// Windows は owner-only の SOCKDIR 内へ一時ファイル経由で渡す。
|
|
1034
|
+
let loaded;
|
|
1035
|
+
if (isWin) {
|
|
1036
|
+
const chunkFile = path.join(SOCKDIR, `${bufferName}.chunk`);
|
|
1037
|
+
try {
|
|
1038
|
+
fs.writeFileSync(chunkFile, chunks[i], { encoding: "utf8" });
|
|
1039
|
+
loaded = tmux("load-buffer", "-b", bufferName, chunkFile);
|
|
1040
|
+
}
|
|
1041
|
+
finally {
|
|
1042
|
+
try {
|
|
1043
|
+
fs.unlinkSync(chunkFile);
|
|
1044
|
+
}
|
|
1045
|
+
catch { /* noop */ }
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
else {
|
|
1049
|
+
loaded = tmuxWithInput(chunks[i], "load-buffer", "-b", bufferName, "-");
|
|
1050
|
+
}
|
|
1022
1051
|
if (loaded.code !== 0) {
|
|
1023
1052
|
tmuxCleanup("delete-buffer", "-b", bufferName);
|
|
1024
1053
|
throw new AitermError(`tmux bufferへの送信準備に失敗しました` +
|
|
@@ -1026,7 +1055,8 @@ export function send(name, text, o = {}) {
|
|
|
1026
1055
|
}
|
|
1027
1056
|
// -r: LF→CR 置換を無効化。-Sは対応新版だけでvis(3)制御文字変換を無効化する。
|
|
1028
1057
|
// send 自身の raw/sanitize 契約だけを真実とし、tmux 側で黙って再変換させない。
|
|
1029
|
-
|
|
1058
|
+
// psmux は -r 非対応(受理フラグは d/p/b/t のみ)のため Windows では付けない。
|
|
1059
|
+
const pasteArgs = isWin ? ["paste-buffer", "-d"] : ["paste-buffer", "-d", "-r"];
|
|
1030
1060
|
if (o.bracketedPaste)
|
|
1031
1061
|
pasteArgs.push("-p");
|
|
1032
1062
|
if (pasteSupportsNoSanitize)
|
|
@@ -1179,7 +1209,7 @@ export async function readOutput(name, o = {}) {
|
|
|
1179
1209
|
}
|
|
1180
1210
|
else {
|
|
1181
1211
|
let off = readOffset(name);
|
|
1182
|
-
//
|
|
1212
|
+
// psmux server 再起動等でログが作り直されると、残った旧 offset が新ログ長を超え、
|
|
1183
1213
|
// 空を返して「何も読めない」状態になる。末尾越えは先頭から読み直す(POSIX では no-op)。
|
|
1184
1214
|
if (off > size)
|
|
1185
1215
|
off = 0;
|
|
@@ -1261,7 +1291,7 @@ export function readOnlyPtyListDiagnostic(runTmux = tmux) {
|
|
|
1261
1291
|
}
|
|
1262
1292
|
}
|
|
1263
1293
|
catch {
|
|
1264
|
-
// tmux
|
|
1294
|
+
// tmux/psmux 未導入等。絶対 path や生 stderr を診断 JSON に出さない。
|
|
1265
1295
|
}
|
|
1266
1296
|
return { status: "unverified", session_count: null };
|
|
1267
1297
|
}
|
|
@@ -1526,9 +1556,10 @@ function writeText0600(p, text) {
|
|
|
1526
1556
|
/* noop */
|
|
1527
1557
|
}
|
|
1528
1558
|
}
|
|
1529
|
-
function
|
|
1530
|
-
|
|
1531
|
-
|
|
1559
|
+
function createEmpty0600(p) {
|
|
1560
|
+
// O_EXCL が「既存 path なら失敗」を保証するため、新規作成の一意性はこれで足りる。
|
|
1561
|
+
// O_NOFOLLOW は撤去した(オーナー裁定 2026-08-19)。
|
|
1562
|
+
const fd = fs.openSync(p, fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_WRONLY, 0o600);
|
|
1532
1563
|
fs.closeSync(fd);
|
|
1533
1564
|
}
|
|
1534
1565
|
function realCodexHome() {
|
|
@@ -1730,10 +1761,9 @@ function readClaudeOperationMarker(meta) {
|
|
|
1730
1761
|
if (meta.kind !== "claude")
|
|
1731
1762
|
return null;
|
|
1732
1763
|
const file = agentClaudeOperationPath(meta.aiterm_session, meta.launch_id);
|
|
1733
|
-
const nofollow = fs.constants.O_NOFOLLOW ?? 0;
|
|
1734
1764
|
let fd;
|
|
1735
1765
|
try {
|
|
1736
|
-
fd = fs.openSync(file, fs.constants.O_RDONLY
|
|
1766
|
+
fd = fs.openSync(file, fs.constants.O_RDONLY);
|
|
1737
1767
|
}
|
|
1738
1768
|
catch (error) {
|
|
1739
1769
|
if (error.code === "ENOENT")
|
|
@@ -1741,9 +1771,10 @@ function readClaudeOperationMarker(meta) {
|
|
|
1741
1771
|
throw new AitermError(`Claude operation markerを確認できません: ${error.message}`, 2);
|
|
1742
1772
|
}
|
|
1743
1773
|
try {
|
|
1774
|
+
// parse する入力の上限だけ残す(owner・link 数・mode・O_NOFOLLOW の検査は撤去した)。
|
|
1744
1775
|
const st = fs.fstatSync(fd);
|
|
1745
|
-
if (
|
|
1746
|
-
throw new AitermError("Claude operation marker
|
|
1776
|
+
if (st.size > 1024) {
|
|
1777
|
+
throw new AitermError("Claude operation markerが大きすぎます", 2);
|
|
1747
1778
|
}
|
|
1748
1779
|
let value;
|
|
1749
1780
|
try {
|
|
@@ -1777,21 +1808,13 @@ function reserveClaudeOperation(meta, operationId) {
|
|
|
1777
1808
|
}
|
|
1778
1809
|
const receipt = agentClaudeDispatchReceiptPath(meta.aiterm_session, meta.launch_id, validated);
|
|
1779
1810
|
try {
|
|
1780
|
-
|
|
1811
|
+
createEmpty0600(receipt);
|
|
1781
1812
|
}
|
|
1782
1813
|
catch (error) {
|
|
1783
1814
|
if (error.code !== "EEXIST")
|
|
1784
1815
|
throw error;
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
st = fs.lstatSync(receipt);
|
|
1788
|
-
}
|
|
1789
|
-
catch {
|
|
1790
|
-
throw new AitermError("Claude dispatch receiptを確認できません", 2);
|
|
1791
|
-
}
|
|
1792
|
-
if (!st.isFile() || st.isSymbolicLink() || st.uid !== currentUid() || st.nlink !== 1 || (st.mode & 0o077) !== 0) {
|
|
1793
|
-
throw new AitermError("Claude dispatch receiptの安全検証に失敗しました", 2);
|
|
1794
|
-
}
|
|
1816
|
+
// EEXIST 自体が「この operation は既に dispatch 済み」を意味する。
|
|
1817
|
+
// receipt の owner・link 数・mode・symlink 検査は撤去した(オーナー裁定 2026-08-19)。
|
|
1795
1818
|
throw new AitermError(`operation ${validated} は既にdispatch済みです。再送しません。`, 2);
|
|
1796
1819
|
}
|
|
1797
1820
|
try {
|
|
@@ -1957,24 +1980,16 @@ export function runClaudeApproval({ action, session_id: name, operation_id: oper
|
|
|
1957
1980
|
}
|
|
1958
1981
|
function hasClaudeDispatchReceipt(meta, operationId) {
|
|
1959
1982
|
const file = agentClaudeDispatchReceiptPath(meta.aiterm_session, meta.launch_id, operationId);
|
|
1960
|
-
let st;
|
|
1961
1983
|
try {
|
|
1962
|
-
|
|
1984
|
+
// receipt は createEmpty0600 が作る空ファイル。存在=dispatch 済みで足りる。
|
|
1985
|
+
// owner・link 数・mode・symlink の検査は撤去した(オーナー裁定 2026-08-19)。
|
|
1986
|
+
return fs.statSync(file).isFile();
|
|
1963
1987
|
}
|
|
1964
1988
|
catch (error) {
|
|
1965
1989
|
if (error.code === "ENOENT")
|
|
1966
1990
|
return false;
|
|
1967
1991
|
throw new AitermError(`Claude dispatch receiptを確認できません: ${error.message}`, 2);
|
|
1968
1992
|
}
|
|
1969
|
-
if (!st.isFile() ||
|
|
1970
|
-
st.isSymbolicLink() ||
|
|
1971
|
-
st.uid !== currentUid() ||
|
|
1972
|
-
st.nlink !== 1 ||
|
|
1973
|
-
(st.mode & 0o077) !== 0 ||
|
|
1974
|
-
st.size !== 0) {
|
|
1975
|
-
throw new AitermError("Claude dispatch receiptの安全検証に失敗しました", 2);
|
|
1976
|
-
}
|
|
1977
|
-
return true;
|
|
1978
1993
|
}
|
|
1979
1994
|
function normalizeInitialPromptState(v) {
|
|
1980
1995
|
if (v === true)
|
|
@@ -2162,8 +2177,8 @@ function createClaudeAgentMetadata(name, cwd, initialPrompt, launchOperationId,
|
|
|
2162
2177
|
const launchId = randomBytes(16).toString("hex");
|
|
2163
2178
|
const eventFile = agentEventPath(name, launchId);
|
|
2164
2179
|
const resultFile = agentClaudeResultPath(name, launchId);
|
|
2165
|
-
|
|
2166
|
-
|
|
2180
|
+
createEmpty0600(eventFile);
|
|
2181
|
+
createEmpty0600(resultFile);
|
|
2167
2182
|
const claudeSettings = createClaudeCorrelationSettings(name, launchId);
|
|
2168
2183
|
const meta = {
|
|
2169
2184
|
kind: "claude",
|
|
@@ -2188,7 +2203,7 @@ function createClaudeAgentMetadata(name, cwd, initialPrompt, launchOperationId,
|
|
|
2188
2203
|
function createCodexAgentMetadata(name, cwd, initialPrompt, overrides = {}, writeScope, lineageContext) {
|
|
2189
2204
|
const launchId = randomBytes(16).toString("hex");
|
|
2190
2205
|
const eventFile = agentEventPath(name, launchId);
|
|
2191
|
-
|
|
2206
|
+
createEmpty0600(eventFile);
|
|
2192
2207
|
const codexHome = realCodexHome();
|
|
2193
2208
|
const meta = {
|
|
2194
2209
|
kind: "codex",
|
|
@@ -2212,7 +2227,7 @@ function createCodexAgentMetadata(name, cwd, initialPrompt, overrides = {}, writ
|
|
|
2212
2227
|
function createGrokAgentMetadata(kind, name, cwd, initialPrompt, authPath, writeScope, lineageContext) {
|
|
2213
2228
|
const launchId = randomBytes(16).toString("hex");
|
|
2214
2229
|
const eventFile = agentEventPath(name, launchId);
|
|
2215
|
-
|
|
2230
|
+
createEmpty0600(eventFile);
|
|
2216
2231
|
const grokHome = realGrokHome();
|
|
2217
2232
|
const meta = {
|
|
2218
2233
|
kind,
|
|
@@ -2901,7 +2916,7 @@ function readClaudeResultText(meta, done, operationId) {
|
|
|
2901
2916
|
st.isSymbolicLink() ||
|
|
2902
2917
|
st.uid !== currentUid() ||
|
|
2903
2918
|
st.nlink !== 1 ||
|
|
2904
|
-
(st.mode & 0o077) !== 0 ||
|
|
2919
|
+
(!isWin && (st.mode & 0o077) !== 0) ||
|
|
2905
2920
|
st.size > CLAUDE_RESULT_MAX_BYTES + 4096) {
|
|
2906
2921
|
throw new AitermError("Claude result file の安全検証に失敗しました", 2);
|
|
2907
2922
|
}
|
|
@@ -3381,8 +3396,9 @@ function isAgentTuiReady(kind, screen) {
|
|
|
3381
3396
|
}
|
|
3382
3397
|
// Grok Build 0.2.117 は起動完了後に製品名を消し、model footerだけを残す。
|
|
3383
3398
|
// Composerも同じfrontendでmodel名だけが異なるため、両方をvendor UIの根拠にする。
|
|
3399
|
+
// Windows native grok.exe(1.0.4 実測)は入力欄markerを `❯` でなく `>` で描画するため両方を受ける。
|
|
3384
3400
|
const grokFrontend = screen.includes("Grok Build") || /\b(?:Grok|Composer)\s+[\w.()-]+/.test(screen);
|
|
3385
|
-
return grokFrontend && /(^|\n|\s)
|
|
3401
|
+
return grokFrontend && /(^|\n|\s)[❯>]/.test(screen);
|
|
3386
3402
|
}
|
|
3387
3403
|
// Codex/Claude は実行中に「(esc to interrupt)」を表示する(実機採取)。startup 側の処理
|
|
3388
3404
|
// (MCP initialize 等)が走ったまま composer だけ描画されている画面は入力受付とみなさない。
|
|
@@ -3446,7 +3462,8 @@ function agentSubmitResidueOnScreen(kind, screen, tail) {
|
|
|
3446
3462
|
const lines = screen.split("\n");
|
|
3447
3463
|
// 入力欄マーカーは ready 判定と同じ記号を行頭基準で探す。submit 済みの transcript echo は
|
|
3448
3464
|
// マーカー行より上に出るため、最後のマーカー行以降だけを composer 領域として見る。
|
|
3449
|
-
|
|
3465
|
+
// grok/composer は Windows native 描画(`>`・実測 1.0.4)も ready 判定と同様に受ける。
|
|
3466
|
+
const markerRe = kind === "codex" ? /^\s*[›>]/ : kind === "claude" ? /^\s*❯/ : /(^|\s)[❯>]/;
|
|
3450
3467
|
let markerIdx = -1;
|
|
3451
3468
|
for (let i = lines.length - 1; i >= 0; i--) {
|
|
3452
3469
|
if (markerRe.test(lines[i])) {
|
|
@@ -3942,7 +3959,7 @@ function resolveAgentBin(kind) {
|
|
|
3942
3959
|
? ["CLAUDE_BIN", [".local", "bin", "claude"], "claude"]
|
|
3943
3960
|
: kind === "codex"
|
|
3944
3961
|
? ["CODEX_BIN", [".local", "bin", "codex"], "codex"]
|
|
3945
|
-
: ["GROK_BIN", [".grok", "bin", "grok"], "grok"];
|
|
3962
|
+
: ["GROK_BIN", [".grok", "bin", isWin ? "grok.exe" : "grok"], "grok"];
|
|
3946
3963
|
const fromEnv = process.env[envVar];
|
|
3947
3964
|
if (fromEnv) {
|
|
3948
3965
|
// 明示指定 env は実在を検証する。存在しないパスを黙って返すと、session を作って
|
|
@@ -4024,35 +4041,22 @@ function isWindowsDrivePath(candidate) {
|
|
|
4024
4041
|
function isWindowsNativeExecutable(candidate) {
|
|
4025
4042
|
return isWindowsDrivePath(candidate) && /\.(?:exe|com|cmd|bat)$/i.test(candidate);
|
|
4026
4043
|
}
|
|
4027
|
-
|
|
4028
|
-
|
|
4029
|
-
|
|
4030
|
-
|
|
4031
|
-
|
|
4032
|
-
try {
|
|
4033
|
-
if (!fs.statSync(candidate).isFile())
|
|
4034
|
-
return false;
|
|
4035
|
-
wslPath = toWslPath(candidate);
|
|
4036
|
-
}
|
|
4037
|
-
catch {
|
|
4038
|
-
return false;
|
|
4039
|
-
}
|
|
4040
|
-
}
|
|
4041
|
-
else if (!candidate.startsWith("/")) {
|
|
4042
|
-
return false;
|
|
4043
|
-
}
|
|
4044
|
-
const checked = spawnSync("wsl.exe", ["-e", "test", "-f", wslPath, "-a", "-x", wslPath], {
|
|
4045
|
-
encoding: "utf8",
|
|
4046
|
-
timeout: 5000,
|
|
4047
|
-
});
|
|
4048
|
-
return checked.status === 0;
|
|
4049
|
-
}
|
|
4044
|
+
// Windows の bin 受入: native 実行ファイル(.exe/.cmd/.bat)に加え、pane shell
|
|
4045
|
+
// (Git Bash)が shebang で実行できる script も実在すれば受け入れる。旧 WSL 側
|
|
4046
|
+
// バイナリ検査への黙ったフォールバックは廃止(別 HOME・別 auth の subagent を
|
|
4047
|
+
// 作るため)。native 実行ファイルの強制が要る vendor(grok/composer の実効
|
|
4048
|
+
// sandbox 等)は openAgent 側の専用ゲートが明示エラーで担う。
|
|
4050
4049
|
function isUsableAgentExecutableFile(candidate) {
|
|
4051
4050
|
if (!isWin)
|
|
4052
4051
|
return isUsableExecutableFile(candidate);
|
|
4053
4052
|
if (isWindowsNativeExecutable(candidate))
|
|
4054
4053
|
return isUsableExecutableFile(candidate);
|
|
4055
|
-
|
|
4054
|
+
try {
|
|
4055
|
+
return fs.statSync(candidate).isFile();
|
|
4056
|
+
}
|
|
4057
|
+
catch {
|
|
4058
|
+
return false;
|
|
4059
|
+
}
|
|
4056
4060
|
}
|
|
4057
4061
|
function resolveWindowsCodexShim(kind, candidate) {
|
|
4058
4062
|
if (!isWin || kind !== "codex" || !/\.(?:cmd|bat)$/i.test(candidate))
|
|
@@ -4074,14 +4078,26 @@ function resolveWindowsCodexShim(kind, candidate) {
|
|
|
4074
4078
|
throw new AitermError(`CODEX_BIN のnpm shimからWindows native codex.exeを解決できません: ${candidate}。` +
|
|
4075
4079
|
"@openai/codexを再インストールするか、CODEX_BINへcodex.exeを指定してください", 2);
|
|
4076
4080
|
}
|
|
4077
|
-
|
|
4078
|
-
|
|
4081
|
+
// pane 内の POSIX shell(Windows は Git Bash 等)へ渡す bin パス。Git Bash は
|
|
4082
|
+
// バックスラッシュをエスケープとして解釈しうるため、ドライブパスは forward slash 形へ。
|
|
4083
|
+
function agentBinForPaneShell(bin) {
|
|
4084
|
+
return isWin && isWindowsDrivePath(bin) ? bin.replace(/\\/g, "/") : bin;
|
|
4079
4085
|
}
|
|
4080
|
-
function spawnAgentControlCommand(bin, args,
|
|
4081
|
-
|
|
4082
|
-
|
|
4083
|
-
|
|
4084
|
-
|
|
4086
|
+
function spawnAgentControlCommand(bin, args, _cwd, options) {
|
|
4087
|
+
// 受入(isUsableAgentExecutableFile)が「使える」と判定した bin は、control command
|
|
4088
|
+
//(`claude auth status --json`/`grok models`)でも同じく実行できなければならない。
|
|
4089
|
+
if (isWin && !/\.(?:exe|com)$/i.test(bin)) {
|
|
4090
|
+
if (/\.(?:cmd|bat)$/i.test(bin)) {
|
|
4091
|
+
// Node は CVE-2024-27980 対処以降、.cmd/.bat の直接 spawn を EINVAL で拒否する。
|
|
4092
|
+
// 受入が .cmd/.bat を許す以上、control 経路は shell 経由で実行する(args は固定語彙)。
|
|
4093
|
+
return spawnSync(bin, args, { ...options, shell: true });
|
|
4094
|
+
}
|
|
4095
|
+
// 受入は pane shell(Git Bash)が shebang で実行できる script も許す。Windows の
|
|
4096
|
+
// CreateProcess は shebang を解さないため、control command も同じ Git Bash で実行する。
|
|
4097
|
+
// これを直接 spawn すると常に失敗し、「受入が通した bin で起動が必ず失敗する」矛盾になる。
|
|
4098
|
+
return spawnSync(resolveWinPaneShell("bash"), [bin, ...args], options);
|
|
4099
|
+
}
|
|
4100
|
+
return spawnSync(bin, args, options);
|
|
4085
4101
|
}
|
|
4086
4102
|
const THROUGHLINE_HANDOFF_CONTEXT_SCHEMA = "throughline.handoff_context.v1";
|
|
4087
4103
|
const PORTABLE_FORK_MISSION_SEPARATOR = "\n\n---\n\n## Portable fork mission\n\n";
|
|
@@ -4163,8 +4179,9 @@ function shq(s) {
|
|
|
4163
4179
|
}
|
|
4164
4180
|
// grok CLI はモデル未指定だと端末側 default に従うため、ツール契約として既定 slug を固定する。
|
|
4165
4181
|
// codex は既定 slug を持たず端末 config/CLI 既定に委ねる(起動応答で実効値を報告する)。
|
|
4182
|
+
// grok 既定は dotagents 規範(docs/02_models.md: xAI 旗艦 = grok-4.6)に従う。
|
|
4166
4183
|
const GROK_MODEL_DEFAULTS = {
|
|
4167
|
-
grok: "grok-4.
|
|
4184
|
+
grok: "grok-4.6",
|
|
4168
4185
|
composer: "grok-composer-2.5-fast",
|
|
4169
4186
|
};
|
|
4170
4187
|
const CLAUDE_EFFORTS = new Set(["low", "medium", "high", "xhigh", "max"]);
|
|
@@ -4202,7 +4219,10 @@ function buildAgentCmd(kind, bin, model, effort, prompt, meta = null) {
|
|
|
4202
4219
|
if (effort)
|
|
4203
4220
|
parts.push("--reasoning-effort", shq(effort));
|
|
4204
4221
|
if ((meta?.kind === "grok" || meta?.kind === "composer") && meta.write_scope === "read-only") {
|
|
4205
|
-
|
|
4222
|
+
// read-only は sandbox が実効書込み禁止を作るため、MCP ツール許可ダイアログの自動承認を
|
|
4223
|
+
// 付けても能力は増えない。無人 subagent が初回 MCP 使用の許可待ちで停止する実障害への対処。
|
|
4224
|
+
// read-only 以外の launch には付けない=権限拡大しない。
|
|
4225
|
+
parts.push("--sandbox", "read-only", "--always-approve");
|
|
4206
4226
|
}
|
|
4207
4227
|
if ((meta?.kind === "grok" || meta?.kind === "composer") && meta.hook_route === "shared_grok_home") {
|
|
4208
4228
|
parts.push("--session-id", shq(meta.vendor_session_id ?? ""), "--rules", shq(subagentInstruction(meta)));
|
|
@@ -4219,36 +4239,32 @@ function agentEnvPrefix(meta, sid, envVars = []) {
|
|
|
4219
4239
|
const value = process.env[name];
|
|
4220
4240
|
return value === undefined ? [] : [`${name}=${shq(value)}`];
|
|
4221
4241
|
});
|
|
4222
|
-
|
|
4223
|
-
|
|
4224
|
-
|
|
4225
|
-
|
|
4226
|
-
|
|
4227
|
-
|
|
4228
|
-
|
|
4229
|
-
|
|
4230
|
-
|
|
4231
|
-
|
|
4232
|
-
|
|
4233
|
-
|
|
4234
|
-
|
|
4235
|
-
|
|
4236
|
-
|
|
4237
|
-
|
|
4238
|
-
|
|
4239
|
-
|
|
4240
|
-
|
|
4241
|
-
|
|
4242
|
-
|
|
4243
|
-
|
|
4244
|
-
|
|
4245
|
-
|
|
4246
|
-
|
|
4247
|
-
return
|
|
4248
|
-
...(grokAuthForCmd ? [`GROK_AUTH_PATH=${shq(grokAuthForCmd)}`] : []),
|
|
4249
|
-
"GROK_DISABLE_AUTOUPDATER=1",
|
|
4250
|
-
...common,
|
|
4251
|
-
].join(" ") + " ";
|
|
4242
|
+
const tokens = (() => {
|
|
4243
|
+
if (!meta)
|
|
4244
|
+
return inherited;
|
|
4245
|
+
const common = [
|
|
4246
|
+
...inherited,
|
|
4247
|
+
`AITERM_AGENT_KIND=${shq(meta.kind)}`,
|
|
4248
|
+
`AITERM_SESSION_ID=${shq(sid)}`,
|
|
4249
|
+
`AITERM_AGENT_SESSION_ID=${shq(sid)}`,
|
|
4250
|
+
`AITERM_AGENT_LAUNCH_ID=${shq(meta.launch_id)}`,
|
|
4251
|
+
`AITERM_AGENT_ROLE=${shq(meta.agent_role ?? "subagent")}`,
|
|
4252
|
+
`AITERM_AGENT_PARENT_SESSION_ID=${shq(meta.parent_session_id ?? "host-root")}`,
|
|
4253
|
+
`AITERM_AGENT_DEPTH=${shq(String(meta.delegation_depth ?? 1))}`,
|
|
4254
|
+
`AITERM_AGENT_LINEAGE=${shq(meta.lineage ?? `host-root>${meta.kind}:${sid}`)}`,
|
|
4255
|
+
`AITERM_AGENT_DELEGATION_ALLOWED=${shq(meta.delegation_allowed === true ? "true" : "false")}`,
|
|
4256
|
+
];
|
|
4257
|
+
if (meta.kind === "claude" || meta.kind === "codex")
|
|
4258
|
+
return common;
|
|
4259
|
+
// grok/composer: 検証済み auth 正本をそのままの path 形で渡す。Windows では native 強制により
|
|
4260
|
+
// vendor は Windows process なので、Windows ドライブパスが正しい形(WSL 形への変換はしない)。
|
|
4261
|
+
return [
|
|
4262
|
+
...(meta.grok_auth_path ? [`GROK_AUTH_PATH=${shq(meta.grok_auth_path)}`] : []),
|
|
4263
|
+
"GROK_DISABLE_AUTOUPDATER=1",
|
|
4264
|
+
...common,
|
|
4265
|
+
];
|
|
4266
|
+
})();
|
|
4267
|
+
return tokens.length ? tokens.join(" ") + " " : "";
|
|
4252
4268
|
}
|
|
4253
4269
|
function agentLabel(kind) {
|
|
4254
4270
|
return kind === "claude"
|
|
@@ -4266,7 +4282,8 @@ function buildAgentLaunchNote(kind, model, effort, meta) {
|
|
|
4266
4282
|
const writeScopeNote = meta?.write_scope === undefined
|
|
4267
4283
|
? ""
|
|
4268
4284
|
: (kind === "codex" || kind === "grok" || kind === "composer") && meta.write_scope === "read-only"
|
|
4269
|
-
? `\n能力宣言: write_scope=${JSON.stringify(meta.write_scope)}。${agentLabel(kind)} CLIへ --sandbox read-only を付与し、書込みを実効禁止。`
|
|
4285
|
+
? `\n能力宣言: write_scope=${JSON.stringify(meta.write_scope)}。${agentLabel(kind)} CLIへ --sandbox read-only を付与し、書込みを実効禁止。` +
|
|
4286
|
+
(kind === "codex" ? "" : "MCPツール許可は --always-approve で自動承認(sandbox内のため能力拡大なし)。")
|
|
4270
4287
|
: `\n能力宣言: write_scope=${JSON.stringify(meta.write_scope)}。パス単位のsandbox allowlistに対応するCLI引数がないため宣言の記録のみ(構造的unsupported)。`;
|
|
4271
4288
|
if (kind === "claude") {
|
|
4272
4289
|
return `起動設定: model=${model ?? "CLI既定"} effort=${effort ?? "CLI既定"}。${writeScopeNote}`;
|
|
@@ -4413,13 +4430,23 @@ export function openAgent(kind, opts = {}) {
|
|
|
4413
4430
|
}
|
|
4414
4431
|
if (kind === "claude")
|
|
4415
4432
|
assertClaudeAuthenticationReady(bin);
|
|
4416
|
-
// Windows
|
|
4417
|
-
//
|
|
4418
|
-
//
|
|
4419
|
-
//
|
|
4420
|
-
//
|
|
4421
|
-
|
|
4422
|
-
|
|
4433
|
+
// Windows の起動コマンドは native psmux pane の Git Bash で走る(WSL 橋・/mnt/c 変換は
|
|
4434
|
+
// e3f5fc8 で全廃。bin/cwd は Windows パスの forward slash 形のまま渡す)。
|
|
4435
|
+
// 未検証リスク: npm グローバル導入の codex.cmd/.bat シムの対話 TUI 描画は実 Windows でしか確認
|
|
4436
|
+
// できない(CI 非対象。docs/03_audit-sweep-2026-07.md 参照)。native .exe の TUI 描画は
|
|
4437
|
+
// grok.exe で実測済み(2026-08-15)。
|
|
4438
|
+
// Windows の grok/composer は Windows native の grok.exe だけを起動する(オーナー裁定 2026-08-15:
|
|
4439
|
+
// WindowsネイティブはWindowsネイティブで完結させ、WSL2へ持ち込まない)。WSL 側 grok を起動すると
|
|
4440
|
+
// vendor 実体が WSL process になり、auth・session 記録(events/chat_history)が WSL home 側へ分裂して
|
|
4441
|
+
// transcript/completion を回収できない(実被弾: 2026-08-15 olc-plan-review-grok2)。
|
|
4442
|
+
if (isWin && (kind === "grok" || kind === "composer") && !isWindowsNativeExecutable(bin)) {
|
|
4443
|
+
ownTelemetryFailure("AITERM.VENDOR_LAUNCHER_FAILED", new AitermError(`Windows の ${label} launcher は Windows native の grok.exe だけを起動できます(現在の解決先: ${bin})。` +
|
|
4444
|
+
"Windows 版 Grok CLI を導入するか、GROK_BIN に grok.exe の絶対パスを指定してください。", 2), 2);
|
|
4445
|
+
}
|
|
4446
|
+
const binForCmd = agentBinForPaneShell(bin);
|
|
4447
|
+
// Windows native pane(psmux + Git Bash)は Windows パスをそのまま扱える。
|
|
4448
|
+
// Git Bash の cd はドライブパスを forward slash 形で受けるのが安全。
|
|
4449
|
+
const cwdForCmd = cwd && isWin ? cwd.replace(/\\/g, "/") : cwd;
|
|
4423
4450
|
const grokAuthPath = agentDone && (kind === "grok" || kind === "composer") ? resolveAndValidateGrokAuth(realGrokHome()) : null;
|
|
4424
4451
|
if (kind === "grok" || kind === "composer") {
|
|
4425
4452
|
const requestedModel = model ?? (kind === "composer" ? GROK_MODEL_DEFAULTS.composer : null);
|
package/dist/grok-stop-hook.js
CHANGED
|
@@ -19,8 +19,13 @@ function hasAitermEnv() {
|
|
|
19
19
|
process.env.AITERM_AGENT_LAUNCH_ID);
|
|
20
20
|
}
|
|
21
21
|
function uid() {
|
|
22
|
+
// Windows(native) は process.getuid を持たない。core の currentUid()・claude-stop-hook と
|
|
23
|
+
// 同じ受容として 0 を返す(Windows の fs.Stats.uid は常に 0 のため owner 比較は自然に
|
|
24
|
+
// 通過する。NTFS ACL は別体系)。POSIX は getuid のまま=挙動不変。
|
|
25
|
+
// 以前はここで fail していたため、Windows では Grok/Composer の完了 event が
|
|
26
|
+
// 一度も書かれず aiterm-wait が timeout まで返らなかった。
|
|
22
27
|
if (typeof process.getuid !== "function")
|
|
23
|
-
|
|
28
|
+
return 0;
|
|
24
29
|
return process.getuid();
|
|
25
30
|
}
|
|
26
31
|
function runtimeStateBase() {
|
|
@@ -36,17 +41,11 @@ function runtimeStateBase() {
|
|
|
36
41
|
}
|
|
37
42
|
return os.tmpdir();
|
|
38
43
|
}
|
|
39
|
-
function
|
|
44
|
+
function agentsDir() {
|
|
40
45
|
const root = path.join(runtimeStateBase(), `aiterm-mcp-${uid()}`);
|
|
41
46
|
const agents = path.join(root, "agents");
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
fail(`agent state root が安全ではありません: ${root}`);
|
|
45
|
-
}
|
|
46
|
-
const ast = fs.lstatSync(agents);
|
|
47
|
-
if (!ast.isDirectory() || ast.isSymbolicLink() || ast.uid !== uid() || (ast.mode & 0o077) !== 0) {
|
|
48
|
-
fail(`agent state dir が安全ではありません: ${agents}`);
|
|
49
|
-
}
|
|
47
|
+
// per-user runtime dir 前提のため、symlink・owner・link 数・mode の検査は撤去した
|
|
48
|
+
// (共有 /tmp に敵対的同居主体がいる前提の防御。オーナー裁定 2026-08-19)。
|
|
50
49
|
return agents;
|
|
51
50
|
}
|
|
52
51
|
function str(v) {
|
|
@@ -65,16 +64,13 @@ async function readStdin() {
|
|
|
65
64
|
return Buffer.concat(chunks).toString("utf8");
|
|
66
65
|
}
|
|
67
66
|
function appendEvent(file, event) {
|
|
68
|
-
const nofollow = fs.constants.O_NOFOLLOW ?? 0;
|
|
69
67
|
const line = JSON.stringify(event) + "\n";
|
|
70
68
|
if (Buffer.byteLength(line, "utf8") > 64 * 1024)
|
|
71
69
|
fail("event line が大きすぎます");
|
|
72
|
-
const fd = fs.openSync(file, fs.constants.O_CREAT | fs.constants.O_APPEND | fs.constants.O_WRONLY
|
|
70
|
+
const fd = fs.openSync(file, fs.constants.O_CREAT | fs.constants.O_APPEND | fs.constants.O_WRONLY, 0o600);
|
|
73
71
|
try {
|
|
74
72
|
const st = fs.fstatSync(fd);
|
|
75
|
-
|
|
76
|
-
fail(`event file が安全ではありません: ${file}`);
|
|
77
|
-
}
|
|
73
|
+
// st は短書き込み時の巻き戻し(ftruncate)に使う。安全性検査としては使わない。
|
|
78
74
|
const written = fs.writeSync(fd, line, undefined, "utf8");
|
|
79
75
|
if (written < Buffer.byteLength(line, "utf8")) {
|
|
80
76
|
fs.ftruncateSync(fd, st.size);
|
|
@@ -107,7 +103,7 @@ async function main() {
|
|
|
107
103
|
payload = {};
|
|
108
104
|
}
|
|
109
105
|
}
|
|
110
|
-
const agents =
|
|
106
|
+
const agents = agentsDir();
|
|
111
107
|
const eventFile = path.join(agents, `${session}.${launchId}.events.jsonl`);
|
|
112
108
|
appendEvent(eventFile, {
|
|
113
109
|
type: "agent_done",
|
package/dist/index.js
CHANGED
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* aiterm-mcp — AI が握るローカル永続端末を stdio MCP サーバとして公開する(Node/TS 版)。
|
|
4
4
|
*
|
|
5
|
-
* WSL2/
|
|
6
|
-
*
|
|
5
|
+
* Linux/WSL2/mac/Windows native のローカルで動かし、握るのはローカル端末1個。リモートは
|
|
6
|
+
* pty_send "ssh ..." で中に入る(ネスト)。バックエンドは tmux(Windows native は tmux CLI
|
|
7
|
+
* 互換の psmux)。ロジックは core.ts に集約。
|
|
7
8
|
*
|
|
8
9
|
* 重要: stdio MCP は stdout が JSON-RPC 専用。診断は stderr/console.error のみ(console.log 禁止)。
|
|
9
10
|
* 起動: npx -y aiterm-mcp(または mcp 登録のコマンドに同じ)。
|
|
@@ -430,7 +431,7 @@ const agentModelDesc = (kind) => kind === "claude"
|
|
|
430
431
|
: kind === "codex"
|
|
431
432
|
? "起動モデル(例: gpt-5.6-sol / gpt-5.6-terra / gpt-5.6-luna)。省略時は端末 config/CLI 既定を継承" +
|
|
432
433
|
"(端末側のピンがそのまま効く。実効値は起動応答に明示される)"
|
|
433
|
-
: `起動モデル。省略時は ${kind === "grok" ? "grok-4.
|
|
434
|
+
: `起動モデル。省略時は ${kind === "grok" ? "grok-4.6" : "grok-composer-2.5-fast"}。` +
|
|
434
435
|
(kind === "composer"
|
|
435
436
|
? "既定/explicit modelを起動前にlive catalogへ照合し、不在ならfallbackせずエラー"
|
|
436
437
|
: "explicit modelを起動前にlive catalogへ照合し、不在ならfallbackせずエラー");
|
|
@@ -560,7 +561,7 @@ registerAgentTool("codex_agent", "codex", "【Codex (OpenAI)】の対話エー
|
|
|
560
561
|
agentCompletionDesc +
|
|
561
562
|
"model / reasoning_effort を引数で指定可" +
|
|
562
563
|
"(省略時は端末 config/CLI 既定を継承。実効値は起動応答に明示)。");
|
|
563
|
-
registerAgentTool("grok_agent", "grok", "【Grok Build の Grok モデル (既定 grok-4.
|
|
564
|
+
registerAgentTool("grok_agent", "grok", "【Grok Build の Grok モデル (既定 grok-4.6)】の対話エージェント TUI を永続端末に起動する。" +
|
|
564
565
|
agentEnvironmentDesc +
|
|
565
566
|
"turn は pty_send で送る(自動で非ブロック dispatch になる)。" +
|
|
566
567
|
agentCompletionDesc +
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "aiterm-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.27.0",
|
|
4
4
|
"mcpName": "io.github.kitepon/aiterm-mcp",
|
|
5
5
|
"description": "Persistent tmux terminal MCP for launching and driving Claude, Codex, Grok, or Composer from any MCP client, cross-vendor or same-vendor. Also runs durable PTY sessions for SSH, containers, and REPLs.",
|
|
6
6
|
"keywords": [
|