aiterm-mcp 0.26.0 → 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 +9 -7
- package/dist/claude-stop-hook.js +20 -34
- package/dist/core.js +179 -259
- package/dist/grok-stop-hook.js +12 -16
- package/dist/index.js +3 -2
- 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
|
|
|
@@ -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.
|
|
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
|
|
|
@@ -449,7 +449,7 @@ Each launcher starts a specific vendor's interactive coding-agent TUI inside a f
|
|
|
449
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
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*
|
|
8
8
|
* 設計: docs/01_design-plan.md / docs/02_mcp-plan.md。出力削減は rag/ の RTK を移植。
|
|
9
9
|
*/
|
|
10
|
-
import {
|
|
10
|
+
import { spawnSync } from "node:child_process";
|
|
11
11
|
import * as fs from "node:fs";
|
|
12
12
|
import * as path from "node:path";
|
|
13
13
|
import * as os from "node:os";
|
|
@@ -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,110 +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
|
-
|
|
187
|
-
}
|
|
188
|
-
// Windows の tmux bridge では各 wsl.exe 呼び出しが短命で、tmux server と pane が継承する
|
|
189
|
-
// WSL_INTEROP は起動元 WSL session の終了とともに死ぬ。死んだ socket のまま pane 内から
|
|
190
|
-
// Windows .exe を起動すると binfmt interop が UtilAcceptVsock accept4=110(ETIMEDOUT) で失敗する。
|
|
191
|
-
// 生きた session leader の socket を WSL_INTEROP へ指せば同じ pane で成功する(どちらも実測
|
|
192
|
-
// 2026-08-15・WSL 2.6.1)。そのため aiterm が長寿命 anchor(sleep する wsl.exe process)を
|
|
193
|
-
// 1本所有し、native .exe 起動の env へその socket を供給する。anchor は起動時に自身の
|
|
194
|
-
// WSL_INTEROP と pid を state file へ書き、以後は生存確認だけで再利用する。
|
|
195
|
-
const WIN_INTEROP_ANCHOR_SPAWN_TIMEOUT_MS = 10_000;
|
|
196
|
-
const WIN_INTEROP_ANCHOR_POLL_MS = 100;
|
|
197
|
-
function winInteropAnchorStatePath() {
|
|
198
|
-
return path.join(ensureSecureStateRoot(), "interop-anchor.json");
|
|
199
|
-
}
|
|
200
|
-
function readWinInteropAnchorState() {
|
|
201
|
-
let raw;
|
|
202
|
-
try {
|
|
203
|
-
raw = fs.readFileSync(winInteropAnchorStatePath(), "utf8");
|
|
204
|
-
}
|
|
205
|
-
catch {
|
|
206
|
-
return null;
|
|
207
|
-
}
|
|
208
|
-
try {
|
|
209
|
-
const value = JSON.parse(raw);
|
|
210
|
-
if (typeof value.socket === "string" &&
|
|
211
|
-
value.socket.startsWith("/run/WSL/") &&
|
|
212
|
-
Number.isSafeInteger(value.pid) &&
|
|
213
|
-
value.pid > 0) {
|
|
214
|
-
return { socket: value.socket, pid: value.pid };
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
catch {
|
|
218
|
-
/* 不完全・破損 state は下の再生成経路で作り直す */
|
|
219
|
-
}
|
|
220
|
-
return null;
|
|
221
|
-
}
|
|
222
|
-
function isWinInteropAnchorAlive(state) {
|
|
223
|
-
const r = spawnSync("wsl.exe", ["-e", "sh", "-c", `test -S ${shq(state.socket)} && kill -0 ${state.pid}`], {
|
|
224
|
-
encoding: "utf8",
|
|
225
|
-
timeout: 10000,
|
|
226
|
-
windowsHide: true,
|
|
227
|
-
});
|
|
228
|
-
return !r.error && r.status === 0;
|
|
229
|
-
}
|
|
230
|
-
// openAgent は同期経路のため、anchor の起動待ちだけ Atomics.wait で有界に同期 sleep する。
|
|
231
|
-
function sleepSyncMs(ms) {
|
|
232
|
-
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
233
|
-
}
|
|
234
|
-
function ensureWinInteropAnchor() {
|
|
235
|
-
const existing = readWinInteropAnchorState();
|
|
236
|
-
if (existing && isWinInteropAnchorAlive(existing))
|
|
237
|
-
return existing.socket;
|
|
238
|
-
const statePath = winInteropAnchorStatePath();
|
|
239
|
-
fs.rmSync(statePath, { force: true });
|
|
240
|
-
// 並行launchが同時にここへ来ると anchor が余分に1本立つが、state は後着が上書きし
|
|
241
|
-
// どちらの socket も有効なので実害はない(余剰 process は wsl shutdown まで sleep のみ)。
|
|
242
|
-
const child = spawn("wsl.exe", [
|
|
243
|
-
"-e",
|
|
244
|
-
"sh",
|
|
245
|
-
"-c",
|
|
246
|
-
`printf '{"socket":"%s","pid":%s}' "$WSL_INTEROP" "$$" > ${shq(toWslPath(statePath))} && exec sleep infinity`,
|
|
247
|
-
], { detached: true, stdio: "ignore", windowsHide: true });
|
|
248
|
-
child.unref();
|
|
249
|
-
const deadline = Date.now() + WIN_INTEROP_ANCHOR_SPAWN_TIMEOUT_MS;
|
|
250
|
-
for (;;) {
|
|
251
|
-
const state = readWinInteropAnchorState();
|
|
252
|
-
if (state) {
|
|
253
|
-
if (!isWinInteropAnchorAlive(state)) {
|
|
254
|
-
throw new AitermError("WSL interop anchor が起動直後に停止しました。`wsl` が動作するか確認してください。", 2);
|
|
255
|
-
}
|
|
256
|
-
return state.socket;
|
|
257
|
-
}
|
|
258
|
-
if (Date.now() >= deadline) {
|
|
259
|
-
throw new AitermError("WSL interop anchor を起動できません。`wsl` が動作するか確認してください。", 2);
|
|
260
|
-
}
|
|
261
|
-
sleepSyncMs(WIN_INTEROP_ANCHOR_POLL_MS);
|
|
262
|
-
}
|
|
176
|
+
ptyDependencyError("psmux -V が失敗しました。`psmux -V` が通るか確認してください。", observe);
|
|
177
|
+
winPsmuxOk = true;
|
|
263
178
|
}
|
|
264
179
|
// tmux が見つからないときの説明。macOS は tmux を同梱せず、Homebrew の bin は GUI 起動時の PATH に
|
|
265
180
|
// 入らないため、原因と対処(導入・自動探索・AITERM_TMUX 上書き)を正直に提示する。
|
|
@@ -323,12 +238,12 @@ export function tmuxSpawnEnv() {
|
|
|
323
238
|
function tmuxCommandWithInput(observe, input, ...args) {
|
|
324
239
|
// maxBuffer は既定 1MiB。capture-pane(大きなスクロールバック)や多セッションの list-sessions で
|
|
325
240
|
// 頭打ちになり stdout が切れる/空になる。Python の subprocess.run は無制限だったので 64MiB へ広げる。
|
|
326
|
-
// Windows
|
|
241
|
+
// Windows は tmux CLI 互換の native psmux を -L namespace 隔離で叩く(WSL 非依存)。
|
|
327
242
|
let r;
|
|
328
243
|
const spawnOpts = { encoding: "utf8", maxBuffer: 64 * 1024 * 1024, input, env: tmuxSpawnEnv() };
|
|
329
244
|
if (isWin) {
|
|
330
|
-
|
|
331
|
-
r = spawnSync(
|
|
245
|
+
ensureWinPsmux(observe);
|
|
246
|
+
r = spawnSync(psmuxBin(), ["-L", WIN_NS, ...args], spawnOpts);
|
|
332
247
|
}
|
|
333
248
|
else {
|
|
334
249
|
// resolveTmux() は tmux を解決できなければ明確な AitermError を投げる(POSIX 版の事前確認)。
|
|
@@ -364,14 +279,23 @@ function sessionExists(name) {
|
|
|
364
279
|
}
|
|
365
280
|
function paneCurrentCommand(name) {
|
|
366
281
|
const r = tmux("display-message", "-p", "-t", name, "#{pane_current_command}");
|
|
367
|
-
|
|
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();
|
|
368
291
|
}
|
|
369
292
|
function pasteBufferSupportsNoSanitizeFlag() {
|
|
370
293
|
const listed = tmux("list-commands");
|
|
371
294
|
if (listed.code !== 0) {
|
|
372
295
|
throw new AitermError(`tmux paste-buffer 能力の確認に失敗しました: ${listed.stderr.trim() || `code=${listed.code}`}`, 2);
|
|
373
296
|
}
|
|
374
|
-
|
|
297
|
+
// psmux の list-commands は行頭にインデントを持つため trim して突合する。
|
|
298
|
+
const usage = listed.stdout.split("\n").find((line) => line.trim().startsWith("paste-buffer"));
|
|
375
299
|
if (!usage)
|
|
376
300
|
throw new AitermError("tmux list-commands にpaste-bufferがありません", 2);
|
|
377
301
|
// tmux 3.4は制御文字を無変換でpasteし、-S自体が無い。3.7は既定でvis(3)変換し、
|
|
@@ -450,31 +374,19 @@ function stateRoot() {
|
|
|
450
374
|
const base = runtimeStateBase();
|
|
451
375
|
return path.join(base, `aiterm-mcp-${uid}`);
|
|
452
376
|
}
|
|
453
|
-
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 エラーとしてそのまま露出させる。
|
|
454
383
|
const root = stateRoot();
|
|
455
384
|
fs.mkdirSync(root, { recursive: true, mode: 0o700 });
|
|
456
|
-
|
|
457
|
-
if (!st.isDirectory() || st.isSymbolicLink()) {
|
|
458
|
-
throw new AitermError(`agent state root が安全な directory ではありません: ${root}`, 2);
|
|
459
|
-
}
|
|
460
|
-
if (st.uid !== currentUid()) {
|
|
461
|
-
throw new AitermError(`agent state root の owner が現在ユーザーではありません: ${root}`, 2);
|
|
462
|
-
}
|
|
463
|
-
if ((st.mode & 0o077) !== 0) {
|
|
464
|
-
fs.chmodSync(root, 0o700);
|
|
465
|
-
}
|
|
466
|
-
const agents = path.join(root, "agents");
|
|
467
|
-
fs.mkdirSync(agents, { recursive: true, mode: 0o700 });
|
|
468
|
-
const ast = fs.lstatSync(agents);
|
|
469
|
-
if (!ast.isDirectory() || ast.isSymbolicLink() || ast.uid !== currentUid()) {
|
|
470
|
-
throw new AitermError(`agent state dir が安全な directory ではありません: ${agents}`, 2);
|
|
471
|
-
}
|
|
472
|
-
if ((ast.mode & 0o077) !== 0)
|
|
473
|
-
fs.chmodSync(agents, 0o700);
|
|
385
|
+
fs.mkdirSync(path.join(root, "agents"), { recursive: true, mode: 0o700 });
|
|
474
386
|
return root;
|
|
475
387
|
}
|
|
476
388
|
function agentsDir() {
|
|
477
|
-
return path.join(
|
|
389
|
+
return path.join(ensureStateRoot(), "agents");
|
|
478
390
|
}
|
|
479
391
|
function agentEventPath(name, launchId) {
|
|
480
392
|
assertSessionName(name);
|
|
@@ -526,24 +438,14 @@ function agentClaudeDispatchReceiptPath(name, launchId, operationId) {
|
|
|
526
438
|
return path.join(agentsDir(), `${name}.${launchId}.${validated.slice("sha256:".length)}.claude-dispatch`);
|
|
527
439
|
}
|
|
528
440
|
function existingAgentsDir() {
|
|
529
|
-
//
|
|
530
|
-
// owner
|
|
531
|
-
//
|
|
532
|
-
//
|
|
533
|
-
|
|
534
|
-
const dir = path.join(
|
|
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");
|
|
535
447
|
try {
|
|
536
|
-
|
|
537
|
-
if (!rst.isDirectory() || rst.isSymbolicLink() || rst.uid !== currentUid())
|
|
538
|
-
return null;
|
|
539
|
-
if (!isWin && (rst.mode & 0o077) !== 0)
|
|
540
|
-
fs.chmodSync(root, 0o700);
|
|
541
|
-
const st = fs.lstatSync(dir);
|
|
542
|
-
if (!st.isDirectory() || st.isSymbolicLink() || st.uid !== currentUid())
|
|
543
|
-
return null;
|
|
544
|
-
if (!isWin && (st.mode & 0o077) !== 0)
|
|
545
|
-
fs.chmodSync(dir, 0o700);
|
|
546
|
-
return dir;
|
|
448
|
+
return fs.statSync(dir).isDirectory() ? dir : null;
|
|
547
449
|
}
|
|
548
450
|
catch {
|
|
549
451
|
return null;
|
|
@@ -631,8 +533,8 @@ function readLastcmd(name) {
|
|
|
631
533
|
}
|
|
632
534
|
}
|
|
633
535
|
export function attachHint(name) {
|
|
634
|
-
// Windows は
|
|
635
|
-
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}`;
|
|
636
538
|
return (`このセッションを自分の目で見る/介入する:\n` +
|
|
637
539
|
` ${cmd}\n` +
|
|
638
540
|
` (抜けるには Ctrl-b d)`);
|
|
@@ -753,9 +655,10 @@ function captureScreen(name, lines) {
|
|
|
753
655
|
return r.code === 0 ? r.stdout : "";
|
|
754
656
|
}
|
|
755
657
|
const sleep = (ms) => new Promise((res) => setTimeout(res, ms));
|
|
756
|
-
// Windows 専用: pipe-pane のログは
|
|
757
|
-
//
|
|
758
|
-
//
|
|
658
|
+
// Windows 専用: pipe-pane のログは psmux server 側の in-process sink が書く=完了検知と
|
|
659
|
+
// 書き手が別 process のため、完了/セッション消滅の報告後も末尾数百バイトが遅れて現れうる。
|
|
660
|
+
// 完了と判定する直前にログサイズが伸びなくなるまで待ち、readOutput が末尾欠けの出力を
|
|
661
|
+
// 返さないようにする(POSIX の tmux は /bin/sh sink・同一 fs で実測上不要)。
|
|
759
662
|
async function settleWinLog(name) {
|
|
760
663
|
let prev = -1;
|
|
761
664
|
for (let i = 0; i < 8; i++) {
|
|
@@ -908,11 +811,8 @@ function completionSuffix(status) {
|
|
|
908
811
|
function rtkRewrite(text) {
|
|
909
812
|
if (text.trim().includes("\n"))
|
|
910
813
|
return text;
|
|
911
|
-
// timeout 無しだと rtk がハングしたとき send() ごと凍結する。Python は timeout=5
|
|
912
|
-
|
|
913
|
-
const r = isWin
|
|
914
|
-
? spawnSync("wsl.exe", ["-e", "rtk", "rewrite", text], { encoding: "utf8", timeout: 5000, maxBuffer: 1024 * 1024 })
|
|
915
|
-
: 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 });
|
|
916
816
|
if (r.error)
|
|
917
817
|
return text; // rtk 不在(ENOENT)・タイムアウト(ETIMEDOUT)等は元テキストへ素通し
|
|
918
818
|
if ((r.status === 0 || r.status === 3) && (r.stdout ?? "").trim())
|
|
@@ -928,7 +828,37 @@ function assertNotDestructive(text, code, context = "") {
|
|
|
928
828
|
}
|
|
929
829
|
}
|
|
930
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
|
+
}
|
|
931
860
|
export function openSession(name, shell = "bash") {
|
|
861
|
+
shell = resolveWinPaneShell(shell);
|
|
932
862
|
try {
|
|
933
863
|
fs.mkdirSync(SOCKDIR, { recursive: true });
|
|
934
864
|
}
|
|
@@ -955,7 +885,8 @@ export function openSession(name, shell = "bash") {
|
|
|
955
885
|
throw new AitermError(`session '${nm}' は既に存在します(list で確認)`, 2);
|
|
956
886
|
}
|
|
957
887
|
else {
|
|
958
|
-
|
|
888
|
+
// -f は端末個人の設定ファイルを読まないための空 config(Windows は NUL デバイス)。
|
|
889
|
+
const r = tmux("new-session", "-d", "-s", nm, ...banner, "-f", isWin ? "NUL" : "/dev/null", shell);
|
|
959
890
|
if (r.code === 0)
|
|
960
891
|
break;
|
|
961
892
|
// 自動採番かつ「重複名」由来の失敗(他エージェントが同名を先に取った)なら次名でリトライ。
|
|
@@ -986,8 +917,10 @@ export function openSession(name, shell = "bash") {
|
|
|
986
917
|
cleanupAgentState(nm);
|
|
987
918
|
// pipe-pane の引数は tmux 内部の /bin/sh -c で再解釈される(argv ではない)。パスは単一引用符で包み、
|
|
988
919
|
// パス自身の ' は '\'' イディオムでエスケープする(名前は検証済みだが、Windows ユーザー名 O'Brien 等が
|
|
989
|
-
// 一時パスに ' を持ち込み redirect を壊すのを防ぐ。空白対策も兼ねる)。
|
|
990
|
-
|
|
920
|
+
// 一時パスに ' を持ち込み redirect を壊すのを防ぐ。空白対策も兼ねる)。
|
|
921
|
+
// Windows の psmux は `cat > <path>` を in-process の直接ファイルsinkとして処理する
|
|
922
|
+
// (psmux e3e4b71 以降)。パスは Windows 形のまま渡す。
|
|
923
|
+
const pipeTarget = logpath(nm);
|
|
991
924
|
const quoted = `'${pipeTarget.replace(/'/g, "'\\''")}'`;
|
|
992
925
|
const pr = tmux("pipe-pane", "-t", nm, "-o", `cat >> ${quoted}`);
|
|
993
926
|
if (pr.code !== 0) {
|
|
@@ -1096,7 +1029,25 @@ export function send(name, text, o = {}) {
|
|
|
1096
1029
|
const partial = i > 0
|
|
1097
1030
|
? " 先行chunkはPTYに入力済みでEnterは未送信です。再送前に入力を確認・消去してください。"
|
|
1098
1031
|
: "";
|
|
1099
|
-
|
|
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
|
+
}
|
|
1100
1051
|
if (loaded.code !== 0) {
|
|
1101
1052
|
tmuxCleanup("delete-buffer", "-b", bufferName);
|
|
1102
1053
|
throw new AitermError(`tmux bufferへの送信準備に失敗しました` +
|
|
@@ -1104,7 +1055,8 @@ export function send(name, text, o = {}) {
|
|
|
1104
1055
|
}
|
|
1105
1056
|
// -r: LF→CR 置換を無効化。-Sは対応新版だけでvis(3)制御文字変換を無効化する。
|
|
1106
1057
|
// send 自身の raw/sanitize 契約だけを真実とし、tmux 側で黙って再変換させない。
|
|
1107
|
-
|
|
1058
|
+
// psmux は -r 非対応(受理フラグは d/p/b/t のみ)のため Windows では付けない。
|
|
1059
|
+
const pasteArgs = isWin ? ["paste-buffer", "-d"] : ["paste-buffer", "-d", "-r"];
|
|
1108
1060
|
if (o.bracketedPaste)
|
|
1109
1061
|
pasteArgs.push("-p");
|
|
1110
1062
|
if (pasteSupportsNoSanitize)
|
|
@@ -1257,7 +1209,7 @@ export async function readOutput(name, o = {}) {
|
|
|
1257
1209
|
}
|
|
1258
1210
|
else {
|
|
1259
1211
|
let off = readOffset(name);
|
|
1260
|
-
//
|
|
1212
|
+
// psmux server 再起動等でログが作り直されると、残った旧 offset が新ログ長を超え、
|
|
1261
1213
|
// 空を返して「何も読めない」状態になる。末尾越えは先頭から読み直す(POSIX では no-op)。
|
|
1262
1214
|
if (off > size)
|
|
1263
1215
|
off = 0;
|
|
@@ -1339,7 +1291,7 @@ export function readOnlyPtyListDiagnostic(runTmux = tmux) {
|
|
|
1339
1291
|
}
|
|
1340
1292
|
}
|
|
1341
1293
|
catch {
|
|
1342
|
-
// tmux
|
|
1294
|
+
// tmux/psmux 未導入等。絶対 path や生 stderr を診断 JSON に出さない。
|
|
1343
1295
|
}
|
|
1344
1296
|
return { status: "unverified", session_count: null };
|
|
1345
1297
|
}
|
|
@@ -1604,9 +1556,10 @@ function writeText0600(p, text) {
|
|
|
1604
1556
|
/* noop */
|
|
1605
1557
|
}
|
|
1606
1558
|
}
|
|
1607
|
-
function
|
|
1608
|
-
|
|
1609
|
-
|
|
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);
|
|
1610
1563
|
fs.closeSync(fd);
|
|
1611
1564
|
}
|
|
1612
1565
|
function realCodexHome() {
|
|
@@ -1808,10 +1761,9 @@ function readClaudeOperationMarker(meta) {
|
|
|
1808
1761
|
if (meta.kind !== "claude")
|
|
1809
1762
|
return null;
|
|
1810
1763
|
const file = agentClaudeOperationPath(meta.aiterm_session, meta.launch_id);
|
|
1811
|
-
const nofollow = fs.constants.O_NOFOLLOW ?? 0;
|
|
1812
1764
|
let fd;
|
|
1813
1765
|
try {
|
|
1814
|
-
fd = fs.openSync(file, fs.constants.O_RDONLY
|
|
1766
|
+
fd = fs.openSync(file, fs.constants.O_RDONLY);
|
|
1815
1767
|
}
|
|
1816
1768
|
catch (error) {
|
|
1817
1769
|
if (error.code === "ENOENT")
|
|
@@ -1819,9 +1771,10 @@ function readClaudeOperationMarker(meta) {
|
|
|
1819
1771
|
throw new AitermError(`Claude operation markerを確認できません: ${error.message}`, 2);
|
|
1820
1772
|
}
|
|
1821
1773
|
try {
|
|
1774
|
+
// parse する入力の上限だけ残す(owner・link 数・mode・O_NOFOLLOW の検査は撤去した)。
|
|
1822
1775
|
const st = fs.fstatSync(fd);
|
|
1823
|
-
if (
|
|
1824
|
-
throw new AitermError("Claude operation marker
|
|
1776
|
+
if (st.size > 1024) {
|
|
1777
|
+
throw new AitermError("Claude operation markerが大きすぎます", 2);
|
|
1825
1778
|
}
|
|
1826
1779
|
let value;
|
|
1827
1780
|
try {
|
|
@@ -1855,21 +1808,13 @@ function reserveClaudeOperation(meta, operationId) {
|
|
|
1855
1808
|
}
|
|
1856
1809
|
const receipt = agentClaudeDispatchReceiptPath(meta.aiterm_session, meta.launch_id, validated);
|
|
1857
1810
|
try {
|
|
1858
|
-
|
|
1811
|
+
createEmpty0600(receipt);
|
|
1859
1812
|
}
|
|
1860
1813
|
catch (error) {
|
|
1861
1814
|
if (error.code !== "EEXIST")
|
|
1862
1815
|
throw error;
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
st = fs.lstatSync(receipt);
|
|
1866
|
-
}
|
|
1867
|
-
catch {
|
|
1868
|
-
throw new AitermError("Claude dispatch receiptを確認できません", 2);
|
|
1869
|
-
}
|
|
1870
|
-
if (!st.isFile() || st.isSymbolicLink() || st.uid !== currentUid() || st.nlink !== 1 || (st.mode & 0o077) !== 0) {
|
|
1871
|
-
throw new AitermError("Claude dispatch receiptの安全検証に失敗しました", 2);
|
|
1872
|
-
}
|
|
1816
|
+
// EEXIST 自体が「この operation は既に dispatch 済み」を意味する。
|
|
1817
|
+
// receipt の owner・link 数・mode・symlink 検査は撤去した(オーナー裁定 2026-08-19)。
|
|
1873
1818
|
throw new AitermError(`operation ${validated} は既にdispatch済みです。再送しません。`, 2);
|
|
1874
1819
|
}
|
|
1875
1820
|
try {
|
|
@@ -2035,24 +1980,16 @@ export function runClaudeApproval({ action, session_id: name, operation_id: oper
|
|
|
2035
1980
|
}
|
|
2036
1981
|
function hasClaudeDispatchReceipt(meta, operationId) {
|
|
2037
1982
|
const file = agentClaudeDispatchReceiptPath(meta.aiterm_session, meta.launch_id, operationId);
|
|
2038
|
-
let st;
|
|
2039
1983
|
try {
|
|
2040
|
-
|
|
1984
|
+
// receipt は createEmpty0600 が作る空ファイル。存在=dispatch 済みで足りる。
|
|
1985
|
+
// owner・link 数・mode・symlink の検査は撤去した(オーナー裁定 2026-08-19)。
|
|
1986
|
+
return fs.statSync(file).isFile();
|
|
2041
1987
|
}
|
|
2042
1988
|
catch (error) {
|
|
2043
1989
|
if (error.code === "ENOENT")
|
|
2044
1990
|
return false;
|
|
2045
1991
|
throw new AitermError(`Claude dispatch receiptを確認できません: ${error.message}`, 2);
|
|
2046
1992
|
}
|
|
2047
|
-
if (!st.isFile() ||
|
|
2048
|
-
st.isSymbolicLink() ||
|
|
2049
|
-
st.uid !== currentUid() ||
|
|
2050
|
-
st.nlink !== 1 ||
|
|
2051
|
-
(st.mode & 0o077) !== 0 ||
|
|
2052
|
-
st.size !== 0) {
|
|
2053
|
-
throw new AitermError("Claude dispatch receiptの安全検証に失敗しました", 2);
|
|
2054
|
-
}
|
|
2055
|
-
return true;
|
|
2056
1993
|
}
|
|
2057
1994
|
function normalizeInitialPromptState(v) {
|
|
2058
1995
|
if (v === true)
|
|
@@ -2240,8 +2177,8 @@ function createClaudeAgentMetadata(name, cwd, initialPrompt, launchOperationId,
|
|
|
2240
2177
|
const launchId = randomBytes(16).toString("hex");
|
|
2241
2178
|
const eventFile = agentEventPath(name, launchId);
|
|
2242
2179
|
const resultFile = agentClaudeResultPath(name, launchId);
|
|
2243
|
-
|
|
2244
|
-
|
|
2180
|
+
createEmpty0600(eventFile);
|
|
2181
|
+
createEmpty0600(resultFile);
|
|
2245
2182
|
const claudeSettings = createClaudeCorrelationSettings(name, launchId);
|
|
2246
2183
|
const meta = {
|
|
2247
2184
|
kind: "claude",
|
|
@@ -2266,7 +2203,7 @@ function createClaudeAgentMetadata(name, cwd, initialPrompt, launchOperationId,
|
|
|
2266
2203
|
function createCodexAgentMetadata(name, cwd, initialPrompt, overrides = {}, writeScope, lineageContext) {
|
|
2267
2204
|
const launchId = randomBytes(16).toString("hex");
|
|
2268
2205
|
const eventFile = agentEventPath(name, launchId);
|
|
2269
|
-
|
|
2206
|
+
createEmpty0600(eventFile);
|
|
2270
2207
|
const codexHome = realCodexHome();
|
|
2271
2208
|
const meta = {
|
|
2272
2209
|
kind: "codex",
|
|
@@ -2290,7 +2227,7 @@ function createCodexAgentMetadata(name, cwd, initialPrompt, overrides = {}, writ
|
|
|
2290
2227
|
function createGrokAgentMetadata(kind, name, cwd, initialPrompt, authPath, writeScope, lineageContext) {
|
|
2291
2228
|
const launchId = randomBytes(16).toString("hex");
|
|
2292
2229
|
const eventFile = agentEventPath(name, launchId);
|
|
2293
|
-
|
|
2230
|
+
createEmpty0600(eventFile);
|
|
2294
2231
|
const grokHome = realGrokHome();
|
|
2295
2232
|
const meta = {
|
|
2296
2233
|
kind,
|
|
@@ -2979,7 +2916,7 @@ function readClaudeResultText(meta, done, operationId) {
|
|
|
2979
2916
|
st.isSymbolicLink() ||
|
|
2980
2917
|
st.uid !== currentUid() ||
|
|
2981
2918
|
st.nlink !== 1 ||
|
|
2982
|
-
(st.mode & 0o077) !== 0 ||
|
|
2919
|
+
(!isWin && (st.mode & 0o077) !== 0) ||
|
|
2983
2920
|
st.size > CLAUDE_RESULT_MAX_BYTES + 4096) {
|
|
2984
2921
|
throw new AitermError("Claude result file の安全検証に失敗しました", 2);
|
|
2985
2922
|
}
|
|
@@ -4104,35 +4041,22 @@ function isWindowsDrivePath(candidate) {
|
|
|
4104
4041
|
function isWindowsNativeExecutable(candidate) {
|
|
4105
4042
|
return isWindowsDrivePath(candidate) && /\.(?:exe|com|cmd|bat)$/i.test(candidate);
|
|
4106
4043
|
}
|
|
4107
|
-
|
|
4108
|
-
|
|
4109
|
-
|
|
4110
|
-
|
|
4111
|
-
|
|
4112
|
-
try {
|
|
4113
|
-
if (!fs.statSync(candidate).isFile())
|
|
4114
|
-
return false;
|
|
4115
|
-
wslPath = toWslPath(candidate);
|
|
4116
|
-
}
|
|
4117
|
-
catch {
|
|
4118
|
-
return false;
|
|
4119
|
-
}
|
|
4120
|
-
}
|
|
4121
|
-
else if (!candidate.startsWith("/")) {
|
|
4122
|
-
return false;
|
|
4123
|
-
}
|
|
4124
|
-
const checked = spawnSync("wsl.exe", ["-e", "test", "-f", wslPath, "-a", "-x", wslPath], {
|
|
4125
|
-
encoding: "utf8",
|
|
4126
|
-
timeout: 5000,
|
|
4127
|
-
});
|
|
4128
|
-
return checked.status === 0;
|
|
4129
|
-
}
|
|
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 側の専用ゲートが明示エラーで担う。
|
|
4130
4049
|
function isUsableAgentExecutableFile(candidate) {
|
|
4131
4050
|
if (!isWin)
|
|
4132
4051
|
return isUsableExecutableFile(candidate);
|
|
4133
4052
|
if (isWindowsNativeExecutable(candidate))
|
|
4134
4053
|
return isUsableExecutableFile(candidate);
|
|
4135
|
-
|
|
4054
|
+
try {
|
|
4055
|
+
return fs.statSync(candidate).isFile();
|
|
4056
|
+
}
|
|
4057
|
+
catch {
|
|
4058
|
+
return false;
|
|
4059
|
+
}
|
|
4136
4060
|
}
|
|
4137
4061
|
function resolveWindowsCodexShim(kind, candidate) {
|
|
4138
4062
|
if (!isWin || kind !== "codex" || !/\.(?:cmd|bat)$/i.test(candidate))
|
|
@@ -4154,14 +4078,26 @@ function resolveWindowsCodexShim(kind, candidate) {
|
|
|
4154
4078
|
throw new AitermError(`CODEX_BIN のnpm shimからWindows native codex.exeを解決できません: ${candidate}。` +
|
|
4155
4079
|
"@openai/codexを再インストールするか、CODEX_BINへcodex.exeを指定してください", 2);
|
|
4156
4080
|
}
|
|
4157
|
-
|
|
4158
|
-
|
|
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;
|
|
4159
4085
|
}
|
|
4160
|
-
function spawnAgentControlCommand(bin, args,
|
|
4161
|
-
|
|
4162
|
-
|
|
4163
|
-
|
|
4164
|
-
|
|
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);
|
|
4165
4101
|
}
|
|
4166
4102
|
const THROUGHLINE_HANDOFF_CONTEXT_SCHEMA = "throughline.handoff_context.v1";
|
|
4167
4103
|
const PORTABLE_FORK_MISSION_SEPARATOR = "\n\n---\n\n## Portable fork mission\n\n";
|
|
@@ -4298,20 +4234,7 @@ function buildAgentCmd(kind, bin, model, effort, prompt, meta = null) {
|
|
|
4298
4234
|
parts.push(shq(prompt)); // 初手プロンプト(任意)
|
|
4299
4235
|
return parts.join(" ");
|
|
4300
4236
|
}
|
|
4301
|
-
|
|
4302
|
-
// 注入 env を Win32 process まで運ぶ WSLENV(/w) を先頭へ加える(WSL 側の env は interop 先の
|
|
4303
|
-
// Windows process へ既定では渡らない・実測 2026-08-15)。POSIX と非 native bin では不変。
|
|
4304
|
-
export function winInteropEnvTokens(tokens, interopSocket) {
|
|
4305
|
-
if (!interopSocket)
|
|
4306
|
-
return tokens;
|
|
4307
|
-
const names = tokens.map((token) => token.slice(0, token.indexOf("=")));
|
|
4308
|
-
return [
|
|
4309
|
-
`WSL_INTEROP=${shq(interopSocket)}`,
|
|
4310
|
-
...(names.length ? [`WSLENV=${names.map((name) => `${name}/w`).join(":")}`] : []),
|
|
4311
|
-
...tokens,
|
|
4312
|
-
];
|
|
4313
|
-
}
|
|
4314
|
-
function agentEnvPrefix(meta, sid, envVars = [], interopSocket = null) {
|
|
4237
|
+
function agentEnvPrefix(meta, sid, envVars = []) {
|
|
4315
4238
|
const inherited = envVars.flatMap((name) => {
|
|
4316
4239
|
const value = process.env[name];
|
|
4317
4240
|
return value === undefined ? [] : [`${name}=${shq(value)}`];
|
|
@@ -4341,8 +4264,7 @@ function agentEnvPrefix(meta, sid, envVars = [], interopSocket = null) {
|
|
|
4341
4264
|
...common,
|
|
4342
4265
|
];
|
|
4343
4266
|
})();
|
|
4344
|
-
|
|
4345
|
-
return finalTokens.length ? finalTokens.join(" ") + " " : "";
|
|
4267
|
+
return tokens.length ? tokens.join(" ") + " " : "";
|
|
4346
4268
|
}
|
|
4347
4269
|
function agentLabel(kind) {
|
|
4348
4270
|
return kind === "claude"
|
|
@@ -4508,12 +4430,11 @@ export function openAgent(kind, opts = {}) {
|
|
|
4508
4430
|
}
|
|
4509
4431
|
if (kind === "claude")
|
|
4510
4432
|
assertClaudeAuthenticationReady(bin);
|
|
4511
|
-
// Windows
|
|
4512
|
-
//
|
|
4513
|
-
// パスで解決)。toWslPath は session を作る前に呼ぶ=変換失敗(非ドライブパス)で残骸 session を残さない。
|
|
4433
|
+
// Windows の起動コマンドは native psmux pane の Git Bash で走る(WSL 橋・/mnt/c 変換は
|
|
4434
|
+
// e3f5fc8 で全廃。bin/cwd は Windows パスの forward slash 形のまま渡す)。
|
|
4514
4435
|
// 未検証リスク: npm グローバル導入の codex.cmd/.bat シムの対話 TUI 描画は実 Windows でしか確認
|
|
4515
|
-
// できない(CI 非対象。docs/03_audit-sweep-2026-07.md 参照)。native .exe の
|
|
4516
|
-
//
|
|
4436
|
+
// できない(CI 非対象。docs/03_audit-sweep-2026-07.md 参照)。native .exe の TUI 描画は
|
|
4437
|
+
// grok.exe で実測済み(2026-08-15)。
|
|
4517
4438
|
// Windows の grok/composer は Windows native の grok.exe だけを起動する(オーナー裁定 2026-08-15:
|
|
4518
4439
|
// WindowsネイティブはWindowsネイティブで完結させ、WSL2へ持ち込まない)。WSL 側 grok を起動すると
|
|
4519
4440
|
// vendor 実体が WSL process になり、auth・session 記録(events/chat_history)が WSL home 側へ分裂して
|
|
@@ -4522,11 +4443,10 @@ export function openAgent(kind, opts = {}) {
|
|
|
4522
4443
|
ownTelemetryFailure("AITERM.VENDOR_LAUNCHER_FAILED", new AitermError(`Windows の ${label} launcher は Windows native の grok.exe だけを起動できます(現在の解決先: ${bin})。` +
|
|
4523
4444
|
"Windows 版 Grok CLI を導入するか、GROK_BIN に grok.exe の絶対パスを指定してください。", 2), 2);
|
|
4524
4445
|
}
|
|
4525
|
-
const binForCmd =
|
|
4526
|
-
|
|
4527
|
-
//
|
|
4528
|
-
|
|
4529
|
-
const interopSocket = isWin && isWindowsNativeExecutable(bin) ? ensureWinInteropAnchor() : null;
|
|
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;
|
|
4530
4450
|
const grokAuthPath = agentDone && (kind === "grok" || kind === "composer") ? resolveAndValidateGrokAuth(realGrokHome()) : null;
|
|
4531
4451
|
if (kind === "grok" || kind === "composer") {
|
|
4532
4452
|
const requestedModel = model ?? (kind === "composer" ? GROK_MODEL_DEFAULTS.composer : null);
|
|
@@ -4559,7 +4479,7 @@ export function openAgent(kind, opts = {}) {
|
|
|
4559
4479
|
agentMetadataNegativeCache.delete(sid);
|
|
4560
4480
|
launchNote = buildAgentLaunchNote(kind, model, effort, meta);
|
|
4561
4481
|
const cmd = buildAgentCmd(kind, binForCmd, model, effort, opts.prompt ?? null, meta);
|
|
4562
|
-
const envPrefix = agentEnvPrefix(meta, sid, envVars
|
|
4482
|
+
const envPrefix = agentEnvPrefix(meta, sid, envVars);
|
|
4563
4483
|
const full = cwdForCmd ? `cd ${shq(cwdForCmd)} && ${envPrefix}${cmd}` : `${envPrefix}${cmd}`;
|
|
4564
4484
|
// force:true で送る。起動骨格は `bin '...'` の固定形で、prompt/cwd/effort は shq でクオート済みの
|
|
4565
4485
|
// 引数=シェルは決して破壊コマンドとして実行しない。破壊ゲート(生シェルコマンド想定)を prompt に
|
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 登録のコマンドに同じ)。
|
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": [
|