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 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 for the core PTY tools (correlated agent completion is POSIX/WSL/macOS only for now) · MIT · see the [CHANGELOG](CHANGELOG.md).
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.5` by default (`model?` overrides) (xAI) | `prompt?`, `throughline_source_session?`, `model?`, `reasoning_effort?` (vendor-supported value), `env_vars?`, `cwd?`, `session_name?`, `write_scope?` |
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 requires POSIX filesystem semantics (Linux, WSL2, macOS).
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.5` by default (`model?` overrides) (xAI) | `prompt?`, `throughline_source_session?`, `model?`, `reasoning_effort?` (vendor-supported value), `env_vars?`, `cwd?`, `session_name?`, `write_scope?` |
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 can launch agents but correlated completion is not supported yet.
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 WSL form — `wsl tmux -S … attach -t <id>` — since the session lives inside WSL.
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 transparently runs tmux **inside WSL**. It needs [WSL](https://learn.microsoft.com/windows/wsl/) installed and initialized, with **tmux installed inside your WSL distro** (`sudo apt install tmux`); verify with `wsl tmux -V`. Sessions, the socket, and human `attach` all live on the WSL side — the AI just drives them from the Windows-side command. (You reach Windows tools the same way you reach SSH: `pty_send "powershell.exe …"` nests into PowerShell.)
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 must run as the interactive Windows user that owns the initialized WSL distro;
529
- `NETWORK SERVICE` cannot see that user's WSL/tmux environment and is not a valid runner identity.
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
 
@@ -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
- fail("POSIX getuid が使えません");
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 secureAgentsDir() {
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
- const agents = path.join(root, "agents");
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 nofollow = fs.constants.O_NOFOLLOW ?? 0;
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 | nofollow, 0o600);
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 | nofollow);
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
- if (!st.isFile() || st.uid !== uid() || st.nlink !== 1 || (st.mode & 0o077) !== 0 || st.size > 1024) {
136
- fail(`operation markerが安全ではありません: ${file}`);
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
- if (!st.isFile() ||
165
- st.isSymbolicLink() ||
166
- st.uid !== uid() ||
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 = secureAgentsDir();
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 が無い。その場合だけ全 tmux 呼び出しを WSL 経由へ橋渡しする
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 に渡すソケットパス。POSIX はログと同じツリーに置く。
25
- // Windows は tmux が WSL 内で動くため、ソケットは WSL ネイティブ fs に置く必要がある
26
- // (/mnt drvfs(9p) 上では AF_UNIX 非対応)。SOCKDIR から短い安定名を導出し、
27
- // TMPDIR ごとの隔離(テスト)と再起動跨ぎ再接続を両立する。
28
- const SOCK = isWin
29
- ? `/tmp/aiterm-${createHash("sha1").update(SOCKDIR).digest("hex").slice(0, 12)}.sock`
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 のドライブパス (C:\a\b) を WSL から見える /mnt/c/a/b へ変換する。
160
- // 一時領域は常にドライブ直下なので UNC は想定外=弾く(黙って壊れた //server パスを作らない)。
161
- export function toWslPath(p) {
162
- const m = /^([A-Za-z]):\/(.*)$/.exec(p.replace(/\\/g, "/"));
163
- if (!m)
164
- throw new AitermError(`WSL へ橋渡しできない一時パスです(ドライブ直下のみ対応): ${p}`, 2);
165
- return `/mnt/${m[1].toLowerCase()}/${m[2]}`;
166
- }
167
- // Windows で最初の tmux 呼び出し前に一度だけ WSL+tmux の可用性を確かめ、失敗は原因別に投げる。
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("wsl.exe", ["-e", "tmux", "-V"], { encoding: "utf8", timeout: 10000 });
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("wsl.exe が見つかりません。Windows では WSL 上の tmux 経由で動作します。WSL と tmux を導入してください。", observe);
180
- ptyDependencyError(`wsl.exe を起動できませんでした(${code ?? "unknown"})。`, observe);
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("WSL 経由で tmux を起動できませんでした。WSL のディストリ未導入、または distro 内に tmux が無い可能性があります。`wsl tmux -V` が通るか確認してください(tmux 導入例: sudo apt install tmux)。", observe);
186
- winBridgeOk = true;
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 は同じ tmux を WSL 経由(-e でログインシェル非経由=$ 展開やクオート崩れを防ぐ)で叩く。
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
- ensureWinBridge(observe);
255
- r = spawnSync("wsl.exe", ["-e", "tmux", "-S", SOCK, ...args], spawnOpts);
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
- return r.code === 0 ? r.stdout.trim() : "";
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
- const usage = listed.stdout.split("\n").find((line) => line.startsWith("paste-buffer "));
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 ensureSecureStateRoot() {
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
- const st = fs.lstatSync(root);
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(ensureSecureStateRoot(), "agents");
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
- if (typeof process.getuid !== "function")
454
- return null;
455
- const root = path.join(runtimeStateBase(), `aiterm-mcp-${process.getuid()}`);
456
- const dir = path.join(root, "agents");
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
- const rst = fs.lstatSync(root);
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 は WSL 内 tmux なので、人が打つのは wsl 経由(SOCK は WSL パス)。
557
- const cmd = isWin ? `wsl tmux -S ${SOCK} attach -t ${name}` : `tmux -S ${SOCK} attach -t ${name}`;
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 のログは /mnt/c (9p) 境界を越えて書かれるため、tmux が完了/セッション
679
- // 消滅を報告した後も最後の数百バイトが少し遅れて現れる。完了と判定する直前にログサイズが
680
- // 伸びなくなるまで待ち、readOutput が末尾欠けの出力を返さないようにする(POSIX は同一fsゆえ不要)。
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
- // Windows はコマンドが WSL 内で走るので rtk も WSL 側(-e)で評価する。不在は素通し。
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
- const r = tmux("new-session", "-d", "-s", nm, ...banner, "-f", "/dev/null", shell);
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 を壊すのを防ぐ。空白対策も兼ねる)。Windows は WSL から見える /mnt/c 形へ。
912
- const pipeTarget = isWin ? toWslPath(logpath(nm)) : logpath(nm);
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
- const loaded = tmuxWithInput(chunks[i], "load-buffer", "-b", bufferName, "-");
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
- const pasteArgs = ["paste-buffer", "-d", "-r"];
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
- // WSL 再起動等でログが作り直されると、Windows 側に残った旧 offset が新ログ長を超え、
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 未導入・WSL bridge 不全等。絶対 path や生 stderr を診断 JSON に出さない。
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 createEmpty0600NoFollow(p) {
1530
- const nofollow = fs.constants.O_NOFOLLOW ?? 0;
1531
- const fd = fs.openSync(p, fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_WRONLY | nofollow, 0o600);
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 | nofollow);
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 (!st.isFile() || st.uid !== currentUid() || st.nlink !== 1 || (st.mode & 0o077) !== 0 || st.size > 1024) {
1746
- throw new AitermError("Claude operation markerの安全検証に失敗しました", 2);
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
- createEmpty0600NoFollow(receipt);
1811
+ createEmpty0600(receipt);
1781
1812
  }
1782
1813
  catch (error) {
1783
1814
  if (error.code !== "EEXIST")
1784
1815
  throw error;
1785
- let st;
1786
- try {
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
- st = fs.lstatSync(file);
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
- createEmpty0600NoFollow(eventFile);
2166
- createEmpty0600NoFollow(resultFile);
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
- createEmpty0600NoFollow(eventFile);
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
- createEmpty0600NoFollow(eventFile);
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)❯/.test(screen);
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
- const markerRe = kind === "codex" ? /^\s*[›>]/ : kind === "claude" ? /^\s*❯/ : /(^|\s)❯/;
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
- function isUsableWslExecutable(candidate) {
4028
- if (!isWin)
4029
- return false;
4030
- let wslPath = candidate;
4031
- if (isWindowsDrivePath(candidate)) {
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
- return isUsableWslExecutable(candidate);
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
- function agentBinForWslShell(bin) {
4078
- return isWin && isWindowsDrivePath(bin) ? toWslPath(bin) : bin;
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, cwd, options) {
4081
- if (!isWin || isWindowsNativeExecutable(bin))
4082
- return spawnSync(bin, args, options);
4083
- const wslCwd = isWindowsDrivePath(cwd) ? toWslPath(cwd) : cwd;
4084
- return spawnSync("wsl.exe", ["--cd", wslCwd, "-e", agentBinForWslShell(bin), ...args], options);
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.5",
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
- parts.push("--sandbox", "read-only");
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
- if (!meta)
4223
- return inherited.length ? inherited.join(" ") + " " : "";
4224
- const common = [
4225
- ...inherited,
4226
- `AITERM_AGENT_KIND=${shq(meta.kind)}`,
4227
- `AITERM_SESSION_ID=${shq(sid)}`,
4228
- `AITERM_AGENT_SESSION_ID=${shq(sid)}`,
4229
- `AITERM_AGENT_LAUNCH_ID=${shq(meta.launch_id)}`,
4230
- `AITERM_AGENT_ROLE=${shq(meta.agent_role ?? "subagent")}`,
4231
- `AITERM_AGENT_PARENT_SESSION_ID=${shq(meta.parent_session_id ?? "host-root")}`,
4232
- `AITERM_AGENT_DEPTH=${shq(String(meta.delegation_depth ?? 1))}`,
4233
- `AITERM_AGENT_LINEAGE=${shq(meta.lineage ?? `host-root>${meta.kind}:${sid}`)}`,
4234
- `AITERM_AGENT_DELEGATION_ALLOWED=${shq(meta.delegation_allowed === true ? "true" : "false")}`,
4235
- ];
4236
- if (meta.kind === "claude") {
4237
- return common.join(" ") + " ";
4238
- }
4239
- if (meta.kind === "codex") {
4240
- return common.join(" ") + " ";
4241
- }
4242
- // Windows では起動コマンドが WSL 内 bash で走るため、検証済み auth 正本の Windows ドライブパスを
4243
- // bin/cwd と同じく /mnt/c/... 形へ変換して渡す。変換しないと WSL 側 grok が正本を開けず接続で停止する。
4244
- const grokAuthForCmd = meta.grok_auth_path && isWin && isWindowsDrivePath(meta.grok_auth_path)
4245
- ? toWslPath(meta.grok_auth_path)
4246
- : meta.grok_auth_path;
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 は起動コマンドが WSL 内 bash で走る(tmux ブリッジ)。bin/cwd を /mnt/c/... 形へ変換して
4417
- // 渡す(ログの toWslPath と対称・A1)。前提: Windows 側に CLI を導入(resolveAgentBin が Windows
4418
- // パスで解決)。toWslPath は session を作る前に呼ぶ=変換失敗(非ドライブパス)で残骸 session を残さない。
4419
- // 未検証リスク: npm グローバル導入の codex.cmd/.bat シムや WSL interop 上の対話 TUI 描画は実 Windows
4420
- // でしか確認できない(CI 非対象。docs/03_audit-sweep-2026-07.md 参照)。
4421
- const binForCmd = agentBinForWslShell(bin);
4422
- const cwdForCmd = cwd && isWin ? toWslPath(cwd) : cwd;
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);
@@ -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
- fail("POSIX getuid が使えません");
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 secureAgentsDir() {
44
+ function agentsDir() {
40
45
  const root = path.join(runtimeStateBase(), `aiterm-mcp-${uid()}`);
41
46
  const agents = path.join(root, "agents");
42
- const rst = fs.lstatSync(root);
43
- if (!rst.isDirectory() || rst.isSymbolicLink() || rst.uid !== uid() || (rst.mode & 0o077) !== 0) {
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 | nofollow, 0o600);
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
- if (!st.isFile() || st.uid !== uid() || st.nlink !== 1 || (st.mode & 0o077) !== 0) {
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 = secureAgentsDir();
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/Linux/mac のローカルで動かし、握るのはローカル端末1個。リモートは pty_send "ssh ..." で
6
- * 中に入る(ネスト)。バックエンドは tmux(実行時の前提)。ロジックは core.ts に集約。
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.5" : "grok-composer-2.5-fast"}。` +
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.5)】の対話エージェント TUI を永続端末に起動する。" +
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.25.3",
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": [