aiterm-mcp 0.36.0 → 0.37.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/CHANGELOG.md CHANGED
@@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.37.0] - 2026-09-13
11
+
12
+ ### 追加
13
+
14
+ - WindowsのCodex Desktop親へのSteerと終了後再開に対応。公式CLI、認証付きloopback接続、native launcher、設定・解除をAitermだけで提供する。
15
+ - 稼働中の互換launcherは親process・ACL・公式RPCを確認した時だけ共有し、既存の起動設定を保持する。
16
+
10
17
  ## [0.36.0] - 2026-09-13
11
18
 
12
19
  ### 追加
@@ -1657,7 +1664,8 @@ prototype (preserved under `prototype/python/` as the porting source and referen
1657
1664
  `ubuntu-latest` for Node 18/20/22, publishing to npm on `v*` tags with
1658
1665
  provenance.
1659
1666
 
1660
- [Unreleased]: https://github.com/kitepon/aiterm-mcp/compare/v0.36.0...HEAD
1667
+ [Unreleased]: https://github.com/kitepon/aiterm-mcp/compare/v0.37.0...HEAD
1668
+ [0.37.0]: https://github.com/kitepon/aiterm-mcp/compare/v0.36.0...v0.37.0
1661
1669
  [0.36.0]: https://github.com/kitepon/aiterm-mcp/compare/v0.35.1...v0.36.0
1662
1670
  [0.35.1]: https://github.com/kitepon/aiterm-mcp/compare/v0.35.0...v0.35.1
1663
1671
  [0.35.0]: https://github.com/kitepon/aiterm-mcp/compare/v0.34.0...v0.35.0
package/LICENSE CHANGED
@@ -1,6 +1,7 @@
1
1
  MIT License
2
2
 
3
3
  Copyright (c) 2026 kitepon
4
+ Copyright (c) 2026 kitepon-rgb (Windows Codex中継の移植部分)
4
5
 
5
6
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
7
  of this software and associated documentation files (the "Software"), to deal
package/README.ja.md CHANGED
@@ -47,7 +47,7 @@ Ubuntu/Debianはsudoとaptでtmuxを準備する。必要な公式package mana
47
47
  AI別の`integrations`と選択機能の`codex_steer`を持つ。失敗時は`reason_code`を付け、終了コードはreadyなら0、再起動待ちは3、それ以外は2となる。
48
48
 
49
49
 
50
- ### Codex DesktopへSteerを有効にする(macOS)
50
+ ### Codex DesktopへSteerを有効にする(macOS・Windows)
51
51
 
52
52
  対話実行の`aiterm-setup`で「Aiterm単品」と「Steer付き」を選べます。無人導入では明示します。
53
53
 
@@ -60,11 +60,15 @@ aiterm-setup --json --codex-steer enable
60
60
  Steerの初回設定後は`restart_required`(終了コード3)を返します。Codexを完全終了して再起動し、
61
61
  `aiterm-setup --codex-steer status`で`ready`を確認してください。接続できない時にキューへ自動退避しません。
62
62
 
63
- 選択と復元情報は`~/.config/aiterm-mcp/codex-relay/`へ保存し、ユーザーLaunchAgentがログイン時にも
64
- 起動設定を適用します。更新時の`aiterm-setup --json`は選択を維持して中継を更新します。
63
+ 選択と復元情報は`~/.config/aiterm-mcp/codex-relay/`へ保存します。macOSはユーザーLaunchAgent、
64
+ Windowsはユーザー環境変数`CODEX_CLI_PATH`で起動設定を維持します。更新時の`aiterm-setup --json`は選択を維持して中継を更新します。
65
65
  解除・旧版への巻き戻し前は`aiterm-setup --codex-steer disable`を実行してCodexを再起動してください。
66
- SteerはmacOSのCodex Desktopと同梱CLI 0.154以上に対応します。Steer有効時の宛先は中継で起動したDesktopに限ります。
67
- Windows・LinuxのSteer付き導入は理由付き`unsupported`を返します。Aiterm単品は従来どおり利用できます。
66
+ SteerはmacOS・Windowsの公式Codex Desktopと同梱CLI 0.154以上に対応します。Steer有効時の宛先は中継で起動したDesktopに限ります。
67
+ Windowsでは公式Desktopが展開した実行ファイルを照合し、認証付きのローカル接続へ中継します。
68
+ Aitermのpackageがlauncher・中継・設定・親への配送を含み、gpt-connectorやdotagentsの導入は不要です。
69
+ 既存の互換launcherがある場合は、稼働中のDesktop接続を確認した時だけ共有し、その起動設定を変更しません。
70
+ 互換性を確認できない設定は理由付きで停止します。WindowsのDesktop更新後はsetupを再実行してください。
71
+ LinuxのSteer付き導入は理由付き`unsupported`を返します。Aiterm単品は従来どおり利用できます。
68
72
 
69
73
  cloneもビルドも不要。どのクライアントでも公開パッケージを次のコマンドで起動する:
70
74
 
@@ -191,7 +195,7 @@ runtime-error store は canonical dotagents config の `collection.enabled: true
191
195
  場合だけ収集し、既定OFF、network送信は行いません。tag起点CIのnpm provenance(OIDC Trusted
192
196
  Publishing)で公開し、GitHub Release が Official MCP Registry を再登録します。
193
197
 
194
- **状態:** 開発継続中 · 現行公開版 **v0.36.0** · 動作対象は Linux · WSL2 · macOS · Windows ネイティブ · MIT · [変更履歴](CHANGELOG.md)。
198
+ **状態:** 開発継続中 · 現行公開版 **v0.37.0** · 動作対象は Linux · WSL2 · macOS · Windows ネイティブ · MIT · [変更履歴](CHANGELOG.md)。
195
199
 
196
200
  ### 更新と巻き戻し
197
201
 
package/README.md CHANGED
@@ -47,7 +47,7 @@ Ubuntu/Debianはsudoとaptでtmuxを準備する。必要な公式package mana
47
47
  AI別の`integrations`と選択機能の`codex_steer`を持つ。失敗時は`reason_code`を付け、終了コードはreadyなら0、再起動待ちは3、それ以外は2となる。
48
48
 
49
49
 
50
- ### Codex DesktopへSteerを有効にする(macOS)
50
+ ### Codex DesktopへSteerを有効にする(macOS・Windows)
51
51
 
52
52
  対話実行の`aiterm-setup`で「Aiterm単品」と「Steer付き」を選べます。無人導入では明示します。
53
53
 
@@ -60,11 +60,15 @@ aiterm-setup --json --codex-steer enable
60
60
  Steerの初回設定後は`restart_required`(終了コード3)を返します。Codexを完全終了して再起動し、
61
61
  `aiterm-setup --codex-steer status`で`ready`を確認してください。接続できない時にキューへ自動退避しません。
62
62
 
63
- 選択と復元情報は`~/.config/aiterm-mcp/codex-relay/`へ保存し、ユーザーLaunchAgentがログイン時にも
64
- 起動設定を適用します。更新時の`aiterm-setup --json`は選択を維持して中継を更新します。
63
+ 選択と復元情報は`~/.config/aiterm-mcp/codex-relay/`へ保存します。macOSはユーザーLaunchAgent、
64
+ Windowsはユーザー環境変数`CODEX_CLI_PATH`で起動設定を維持します。更新時の`aiterm-setup --json`は選択を維持して中継を更新します。
65
65
  解除・旧版への巻き戻し前は`aiterm-setup --codex-steer disable`を実行してCodexを再起動してください。
66
- SteerはmacOSのCodex Desktopと同梱CLI 0.154以上に対応します。Steer有効時の宛先は中継で起動したDesktopに限ります。
67
- Windows・LinuxのSteer付き導入は理由付き`unsupported`を返します。Aiterm単品は従来どおり利用できます。
66
+ SteerはmacOS・Windowsの公式Codex Desktopと同梱CLI 0.154以上に対応します。Steer有効時の宛先は中継で起動したDesktopに限ります。
67
+ Windowsでは公式Desktopが展開した実行ファイルを照合し、認証付きのローカル接続へ中継します。
68
+ Aitermのpackageがlauncher・中継・設定・親への配送を含み、gpt-connectorやdotagentsの導入は不要です。
69
+ 既存の互換launcherがある場合は、稼働中のDesktop接続を確認した時だけ共有し、その起動設定を変更しません。
70
+ 互換性を確認できない設定は理由付きで停止します。WindowsのDesktop更新後はsetupを再実行してください。
71
+ LinuxのSteer付き導入は理由付き`unsupported`を返します。Aiterm単品は従来どおり利用できます。
68
72
 
69
73
  No clone or build is required. Each client launches the published package with:
70
74
 
@@ -205,7 +209,7 @@ collection is off by default and performs no network I/O. It ships via
205
209
  tag-triggered CI with npm provenance (OIDC Trusted Publishing); the GitHub
206
210
  Release re-registers the Official MCP Registry entry.
207
211
 
208
- **Status:** actively maintained · current public release **v0.36.0** · runs on Linux · WSL2 · macOS · native Windows (tmux on POSIX, the tmux-CLI-compatible [psmux](https://github.com/psmux/psmux) on native Windows — no WSL required) · MIT · see the [CHANGELOG](CHANGELOG.md).
212
+ **Status:** actively maintained · current public release **v0.37.0** · runs on Linux · WSL2 · macOS · native Windows (tmux on POSIX, the tmux-CLI-compatible [psmux](https://github.com/psmux/psmux) on native Windows — no WSL required) · MIT · see the [CHANGELOG](CHANGELOG.md).
209
213
 
210
214
  ### Update and rollback
211
215
 
@@ -2,18 +2,25 @@ import { createConnection } from "node:net";
2
2
  import WebSocket from "ws";
3
3
  import { CodexDeliveryError } from "./codex-delivery-error.js";
4
4
  import { verifyRelaySocket } from "./codex-relay-config.js";
5
+ import { windowsSocketConnection } from "./windows-codex-connection.js";
5
6
  /** 公式受付へ追加接続する。Desktopへの承認要求や通知には応答しない。 */
6
7
  export async function withCodexRelay(socketPath, action, timeout = 15_000) {
8
+ let connection;
7
9
  try {
8
- verifyRelaySocket(socketPath);
10
+ if (process.platform === "win32")
11
+ connection = windowsSocketConnection(socketPath);
12
+ else {
13
+ verifyRelaySocket(socketPath);
14
+ connection = { url: "ws://localhost/rpc", options: { createConnection: () => createConnection(socketPath) } };
15
+ }
9
16
  }
10
17
  catch (error) {
11
18
  if (error instanceof CodexDeliveryError)
12
19
  throw error;
13
20
  throw new CodexDeliveryError("CODEX_RELAY_UNAVAILABLE", "公式App Serverのsocketがありません。Codexを再起動してください");
14
21
  }
15
- const socket = new WebSocket("ws://localhost/rpc", {
16
- createConnection: () => createConnection(socketPath), handshakeTimeout: timeout, perMessageDeflate: false,
22
+ const socket = new WebSocket(connection.url, {
23
+ ...connection.options, handshakeTimeout: timeout, perMessageDeflate: false,
17
24
  });
18
25
  let sequence = 0;
19
26
  let failure = null;
@@ -5,6 +5,7 @@ import { homedir } from "node:os";
5
5
  import { z } from "zod";
6
6
  import { readRuntimeProcesses } from "./process-runtime.js";
7
7
  import { CodexDeliveryError } from "./codex-delivery-error.js";
8
+ import { findWindowsParentSocket } from "./windows-codex-connection.js";
8
9
  export const relayConfigSchema = z.object({
9
10
  schema: z.literal("aiterm.codex-relay.v1"),
10
11
  enabled: z.boolean(),
@@ -49,7 +50,10 @@ export function verifyRelaySocket(socket) {
49
50
  throw new CodexDeliveryError("CODEX_RELAY_PATH_INVALID", "本人所有の公式socketを確認できません");
50
51
  }
51
52
  }
52
- export function parentRelaySocket(config, processes = readRuntimeProcesses(), pid = process.pid) {
53
+ export function parentRelaySocket(config, processes, pid = process.pid) {
54
+ if (process.platform === "win32")
55
+ return findWindowsParentSocket(undefined, pid, config.launcher === config.previous_cli_path ? undefined : config.socket_root);
56
+ processes ??= readRuntimeProcesses();
53
57
  const rows = new Map(processes.map(row => [row.pid, row]));
54
58
  const seen = new Set();
55
59
  let current = rows.get(pid)?.parent_pid;
@@ -44,7 +44,7 @@ export function readRuntimeProcesses() {
44
44
  "$ErrorActionPreference='Stop'",
45
45
  "[Console]::OutputEncoding=[Text.UTF8Encoding]::new($false)",
46
46
  "@(Get-CimInstance Win32_Process | Where-Object { $null -ne $_.CreationDate -and $null -ne $_.CommandLine } | ForEach-Object {",
47
- "[ordered]@{ pid=[int]$_.ProcessId; parent_pid=[int]$_.ParentProcessId; started_identity=$_.CreationDate.ToUniversalTime().ToString('o'); command=$_.CommandLine; cpu_seconds=([double]$_.KernelModeTime+[double]$_.UserModeTime)/10000000 }",
47
+ "[ordered]@{ pid=[int]$_.ProcessId; parent_pid=[int]$_.ParentProcessId; executable=[string]$_.ExecutablePath; started_identity=$_.CreationDate.ToUniversalTime().ToString('o'); command=$_.CommandLine; cpu_seconds=([double]$_.KernelModeTime+[double]$_.UserModeTime)/10000000 }",
48
48
  "}) | ConvertTo-Json -Compress",
49
49
  ].join("\n");
50
50
  const result = spawnSync(resolveWindowsPowerShell7(), ["-NoLogo", "-NoProfile", "-NonInteractive", "-EncodedCommand", Buffer.from(script, "utf16le").toString("base64")], {
@@ -69,6 +69,7 @@ export function readRuntimeProcesses() {
69
69
  const command = row.command.trim();
70
70
  return {
71
71
  pid: row.pid, parent_pid: row.parent_pid, process_group_id: null, stopped: null,
72
+ executable: row.executable,
72
73
  started_identity: new Date(row.started_identity).toISOString(),
73
74
  command, cpu_seconds: row.cpu_seconds, argv_digest: createHash("sha256").update(command).digest("hex"),
74
75
  };
package/dist/setup-cli.js CHANGED
@@ -18,7 +18,7 @@ if (args.length === 1 && args[0] === "--remove-claude-parent-hooks") {
18
18
  }
19
19
  }
20
20
  else if (args.length === 1 && ["--help", "-h"].includes(args[0])) {
21
- process.stdout.write("使い方: aiterm-setup [--json] [--codex-steer enable|disable|status]\n依存準備、AIへの登録、MCPと端末の実動作確認を行います。対話実行ではAiterm単品かCodex Desktop Steer付きかを選べます。SteerはmacOS対応で、初回はCodexの再起動が必要です。disableは元の起動設定へ戻し、statusは実際の接続を確認します。--jsonは対話せず、Steerの選択を維持します。\n旧版へ戻す前のClaude専用hook解除: --remove-claude-parent-hooks\n");
21
+ process.stdout.write("使い方: aiterm-setup [--json] [--codex-steer enable|disable|status]\n依存準備、AIへの登録、MCPと端末の実動作確認を行います。対話実行ではAiterm単品かCodex Desktop Steer付きかを選べます。SteerはmacOS・Windows対応で、初回はCodexの再起動が必要です。disableは元の起動設定へ戻し、statusは実際の接続を確認します。--jsonは対話せず、Steerの選択を維持します。\n旧版へ戻す前のClaude専用hook解除: --remove-claude-parent-hooks\n");
22
22
  }
23
23
  else {
24
24
  try {
@@ -33,7 +33,7 @@ else {
33
33
  throw new Error("使い方: aiterm-setup [--json] [--codex-steer enable|disable|status]");
34
34
  }
35
35
  const onlySteer = action === "disable" || action === "status";
36
- if (!action && !json && process.stdin.isTTY && process.stderr.isTTY && process.platform === "darwin") {
36
+ if (!action && !json && process.stdin.isTTY && process.stderr.isTTY && ["darwin", "win32"].includes(process.platform)) {
37
37
  const enabled = readRelayConfig()?.enabled ?? false;
38
38
  const prompt = createInterface({ input: process.stdin, output: process.stderr });
39
39
  try {
@@ -12,6 +12,7 @@ import { codexRelayLauncher } from "./codex-relay-launcher.js";
12
12
  import { withCodexRelay } from "./codex-relay-client.js";
13
13
  import { readRuntimeProcesses } from "./process-runtime.js";
14
14
  import { installRelayLogin, removeRelayLogin } from "./codex-relay-login.js";
15
+ import { configureWindowsCodexSteer } from "./windows-codex-setup.js";
15
16
  function command(executable, args) {
16
17
  const result = spawnSync(executable, args, { encoding: "utf8", timeout: 15_000 });
17
18
  if (result.error || result.status !== 0)
@@ -51,7 +52,7 @@ export async function verifyRelayLauncher(launcher) {
51
52
  const temporary = fs.mkdtempSync(path.join(tmpdir(), "aiterm-relay-setup-"));
52
53
  fs.mkdirSync(path.join(temporary, ".codex"));
53
54
  const child = spawn(launcher, ["-c", 'cli_auth_credentials_store="file"', "-c", 'mcp_oauth_credentials_store="file"', "app-server"], {
54
- env: { ...process.env, HOME: temporary, CODEX_HOME: path.join(temporary, ".codex") }, stdio: ["pipe", "pipe", "ignore"],
55
+ env: { ...process.env, HOME: temporary, CODEX_HOME: path.join(temporary, ".codex") }, stdio: ["pipe", "pipe", "ignore"], windowsHide: true,
55
56
  });
56
57
  const reader = createInterface({ input: child.stdout });
57
58
  const exited = new Promise(resolve => child.once("close", resolve));
@@ -121,6 +122,8 @@ function writeConfig(directory, config) {
121
122
  fs.renameSync(temporary, path.join(directory, "config.json"));
122
123
  }
123
124
  export async function configureCodexSteer(action = "status", overrides = {}) {
125
+ if ((overrides.platform ?? process.platform) === "win32")
126
+ return configureWindowsCodexSteer(action, overrides);
124
127
  const runtime = {
125
128
  platform: process.platform, directory: relayConfigDirectory(), socket_root: `/tmp/aiterm-codex-${process.getuid?.() ?? 0}`,
126
129
  node: process.execPath, relay: fileURLToPath(new URL("./codex-stdio-relay.js", import.meta.url)),
@@ -0,0 +1,111 @@
1
+ // gpt-connectorのWindows実装をMIT条件で移植。実行時の外部製品依存は持たない。
2
+ import { lstatSync, readFileSync } from "node:fs";
3
+ import { basename, dirname, isAbsolute, join, relative } from "node:path";
4
+ import { z } from "zod";
5
+ import { windowsPowerShellSync, quotePowerShell } from "./windows-codex-state.js";
6
+ import { CodexDeliveryError } from "./codex-delivery-error.js";
7
+ import { readRuntimeProcesses } from "./process-runtime.js";
8
+ export const windowsRelaySchema = z.object({
9
+ schema: z.enum(["aiterm.windows-relay.v1", "gpt-connector.windows-relay.v1"]),
10
+ serverPid: z.number().int().positive(), serverStarted: z.string(), binary: z.string(),
11
+ endpoint: z.string().regex(/^ws:\/\/127\.0\.0\.1:[1-9][0-9]{0,4}$/),
12
+ }).strict();
13
+ const processSchema = z.object({ pid: z.number(), parent_pid: z.number(), command: z.string(), executable: z.string(), started: z.string() });
14
+ export function readWindowsProcesses() {
15
+ return readRuntimeProcesses().map(row => ({ pid: row.pid, parent_pid: row.parent_pid, command: row.command, executable: row.executable ?? "", started: row.started_identity }));
16
+ }
17
+ function invalid(message) { throw new CodexDeliveryError("CODEX_RELAY_CONNECTION_INVALID", message); }
18
+ export function readWindowsRelay(file, processes = readWindowsProcesses()) {
19
+ if (!isAbsolute(file) || lstatSync(file).isSymbolicLink() || !lstatSync(file).isFile())
20
+ invalid("WindowsのSteer接続記録が不正です。");
21
+ const root = dirname(file);
22
+ if (lstatSync(root).isSymbolicLink() || !lstatSync(root).isDirectory())
23
+ invalid("WindowsのSteer保存先が不正です。");
24
+ // 接続情報は外部入力。読む前に所有者とACLを検証し、権限を修復して成功扱いしない。
25
+ windowsPowerShellSync(`
26
+ $sid = [System.Security.Principal.WindowsIdentity]::GetCurrent().User.Value
27
+ foreach ($entry in @(${quotePowerShell(root)}, ${quotePowerShell(file)}, ${quotePowerShell(join(root, "token"))})) {
28
+ $acl = Get-Acl -LiteralPath $entry
29
+ if ($acl.GetOwner([System.Security.Principal.SecurityIdentifier]).Value -notin @($sid, 'S-1-5-32-544')) { throw 'Steer保存先の所有者が一致しません' }
30
+ foreach ($rule in $acl.Access) {
31
+ $identity = $rule.IdentityReference.Translate([System.Security.Principal.SecurityIdentifier]).Value
32
+ if ($rule.AccessControlType -eq 'Allow' -and $identity -notin @($sid, 'S-1-5-18', 'S-1-5-32-544')) { throw 'Steer保存先が他userへ公開されています' }
33
+ }
34
+ }`);
35
+ const record = windowsRelaySchema.parse(JSON.parse(readFileSync(file, "utf8")));
36
+ const server = processes.find(row => row.pid === record.serverPid);
37
+ if (!server || Date.parse(server.started) !== Date.parse(record.serverStarted) || server.executable.toLowerCase() !== record.binary.toLowerCase()
38
+ || !server.command.includes(record.endpoint) || !server.command.includes(join(root, "token")))
39
+ invalid("Windowsの親Codex processを照合できません。");
40
+ return record;
41
+ }
42
+ export function findWindowsParentSocket(rows = readWindowsProcesses(), callerPid = process.pid, root) {
43
+ const parents = new Map(rows.map(row => [row.pid, row]));
44
+ const seen = new Set();
45
+ let pid = parents.get(callerPid)?.parent_pid;
46
+ while (pid && !seen.has(pid)) {
47
+ const row = parents.get(pid);
48
+ // 自分が起動した公式CLIの引数から接続先を取得する。MCPへの環境変数継承を前提にしない。
49
+ const token = row && basename(row.executable).toLowerCase() === "codex.exe"
50
+ ? /(?:^|\s)--ws-token-file\s+(?:"([^"]+)"|(\S+))/.exec(row.command) : null;
51
+ const tokenFile = token?.[1] ?? token?.[2];
52
+ if (tokenFile && isAbsolute(tokenFile) && basename(tokenFile) === "token") {
53
+ const file = join(dirname(tokenFile), "connection.json");
54
+ if (root) {
55
+ const location = relative(root, file);
56
+ if (isAbsolute(location) || location === ".." || location.startsWith("..\\"))
57
+ invalid("親CodexはAitermのSteer起動設定を使っていません。Codexを完全再起動してください。");
58
+ }
59
+ const record = readWindowsRelay(file, rows);
60
+ if (record.serverPid !== pid)
61
+ invalid("Steer接続記録と親Codexが一致しません。");
62
+ return file;
63
+ }
64
+ seen.add(pid);
65
+ pid = row?.parent_pid;
66
+ }
67
+ invalid("親CodexのSteer接続がありません。aiterm-setup --codex-steer enableを実行し、Codexを再起動してください。");
68
+ }
69
+ export function windowsSocketConnection(file) {
70
+ const record = readWindowsRelay(file);
71
+ const tokenFile = join(dirname(file), "token");
72
+ if (lstatSync(tokenFile).isSymbolicLink() || !lstatSync(tokenFile).isFile())
73
+ invalid("Steer認証fileが不正です。");
74
+ const token = readFileSync(tokenFile, "utf8");
75
+ if (!/^[a-f0-9]{64}$/.test(token))
76
+ invalid("Steer認証情報が不正です。");
77
+ return { url: record.endpoint, options: { headers: { Authorization: `Bearer ${token}` } } };
78
+ }
79
+ /** 登録済みlauncherとDesktopの子孫にある、対応する公式接続だけを共有できる。 */
80
+ export function windowsDesktopRelay(launcher, rows = readWindowsProcesses()) {
81
+ const parents = new Map(rows.map(row => [row.pid, row]));
82
+ for (const server of rows) {
83
+ if (basename(server.executable).toLowerCase() !== "codex.exe" || !server.command.includes("--ws-token-file"))
84
+ continue;
85
+ let row = parents.get(server.parent_pid);
86
+ let matchedLauncher = false;
87
+ let matchedDesktop = false;
88
+ const seen = new Set();
89
+ while (row && !seen.has(row.pid)) {
90
+ seen.add(row.pid);
91
+ if (row.executable.toLowerCase() === launcher.toLowerCase())
92
+ matchedLauncher = true;
93
+ if (/\\WindowsApps\\OpenAI\.Codex_[^\\]+\\app\\(?:ChatGPT|Codex)\.exe$/i.test(row.executable)) {
94
+ matchedDesktop = true;
95
+ break;
96
+ }
97
+ row = parents.get(row.parent_pid);
98
+ }
99
+ if (!matchedLauncher || !matchedDesktop)
100
+ continue;
101
+ const token = /(?:^|\s)--ws-token-file\s+(?:"([^"]+)"|(\S+))/.exec(server.command);
102
+ const tokenFile = token?.[1] ?? token?.[2];
103
+ if (!tokenFile || !isAbsolute(tokenFile) || basename(tokenFile) !== "token")
104
+ continue;
105
+ const file = join(dirname(tokenFile), "connection.json");
106
+ if (readWindowsRelay(file, rows).serverPid !== server.pid)
107
+ invalid("Steer接続記録と親Codexが一致しません。");
108
+ return file;
109
+ }
110
+ return null;
111
+ }
@@ -0,0 +1,154 @@
1
+ // gpt-connectorのWindows実装をMIT条件で移植。実行時の外部製品依存は持たない。
2
+ // Windowsの起動processと認証付きloopback接続を所有する。JSON-RPC本文は変更しない。
3
+ import { spawn } from "node:child_process";
4
+ import { createServer } from "node:net";
5
+ import { createInterface } from "node:readline";
6
+ import { once } from "node:events";
7
+ import { randomBytes, randomUUID } from "node:crypto";
8
+ import { join } from "node:path";
9
+ import { fileURLToPath } from "node:url";
10
+ import { writeFileSync, rmSync } from "node:fs";
11
+ import { setTimeout as delay } from "node:timers/promises";
12
+ import WebSocket from "ws";
13
+ import { ensurePrivateDirectory, makeFilePrivate } from "./windows-codex-state.js";
14
+ import { readWindowsProcesses } from "./windows-codex-connection.js";
15
+ export function windowsServerArguments(args) {
16
+ let server = false;
17
+ const result = [];
18
+ for (let i = 0; i < args.length; i++) {
19
+ const arg = args[i];
20
+ if (["-c", "--config", "--enable", "--disable"].includes(arg)) {
21
+ result.push(arg, args[++i]);
22
+ continue;
23
+ }
24
+ if (/^(?:--config=|--enable=|--disable=|-c.)/.test(arg)) {
25
+ result.push(arg);
26
+ continue;
27
+ }
28
+ if (!server) {
29
+ if (arg !== "app-server")
30
+ return null;
31
+ server = true;
32
+ result.push(arg);
33
+ continue;
34
+ }
35
+ if (["proxy", "daemon", "start", "stop", "status", "generate-ts", "generate-json-schema", "--help", "-h", "--version", "-V"].includes(arg))
36
+ return null;
37
+ if (arg === "--stdio" || arg === "--listen=stdio://")
38
+ continue;
39
+ if (arg === "--listen") {
40
+ if (args[++i] !== "stdio://")
41
+ throw new Error("stdio以外の接続指定は変更できません");
42
+ continue;
43
+ }
44
+ if (arg.startsWith("--listen=") || arg.startsWith("--ws-"))
45
+ throw new Error("既存の接続・認証指定は変更できません");
46
+ result.push(arg);
47
+ }
48
+ return server ? result : null;
49
+ }
50
+ export async function runWindowsCodexRelay(binary, root, args) {
51
+ const serverArgs = windowsServerArguments(args);
52
+ if (!serverArgs) {
53
+ const child = spawn(binary, args, { stdio: "inherit", windowsHide: true });
54
+ return (await once(child, "close"))[0] ?? 1;
55
+ }
56
+ const directory = join(root, randomUUID());
57
+ ensurePrivateDirectory(directory);
58
+ const file = join(directory, "connection.json");
59
+ const tokenFile = join(directory, "token");
60
+ const token = randomBytes(32).toString("hex");
61
+ writeFileSync(tokenFile, token, { flag: "wx" });
62
+ makeFilePrivate(tokenFile);
63
+ // OSが割り当てた空きportを使い、取得競合は公式serverの起動失敗として明示する。
64
+ const reservation = createServer();
65
+ reservation.listen(0, "127.0.0.1");
66
+ await once(reservation, "listening");
67
+ const port = reservation.address().port;
68
+ await new Promise((resolve, reject) => reservation.close(error => error ? reject(error) : resolve()));
69
+ const endpoint = `ws://127.0.0.1:${port}`;
70
+ const child = spawn(binary, [...serverArgs, "--listen", endpoint, "--ws-auth", "capability-token", "--ws-token-file", tokenFile], {
71
+ stdio: ["ignore", "ignore", "pipe"], windowsHide: true,
72
+ });
73
+ const exited = new Promise(resolve => child.once("close", () => resolve()));
74
+ let failed;
75
+ child.on("error", error => { failed = error; });
76
+ // WebSocketの定型起動案内だけを除き、公式CLIの診断はDesktopへそのまま返す。
77
+ const diagnostics = createInterface({ input: child.stderr });
78
+ diagnostics.on("line", line => {
79
+ if (!/^(?:\s*|codex app-server \(WebSockets\)| {2}(?:listening on:|readyz:|healthz:|note:).*)$/.test(line))
80
+ process.stderr.write(line + "\n");
81
+ });
82
+ let socket;
83
+ const input = createInterface({ input: process.stdin, crlfDelay: Infinity });
84
+ const lines = input[Symbol.asyncIterator]();
85
+ let ending = false;
86
+ const stop = () => { ending = true; child.kill(); socket?.terminate(); input.close(); process.stdin.destroy(); };
87
+ input.once("close", stop);
88
+ process.stdout.once("error", stop);
89
+ try {
90
+ const deadline = Date.now() + 15_000;
91
+ while (!socket) {
92
+ if (failed || child.exitCode !== null || child.signalCode !== null)
93
+ throw new Error("公式Codexの起動に失敗しました", { cause: failed });
94
+ const candidate = new WebSocket(endpoint, { headers: { Authorization: `Bearer ${token}` }, handshakeTimeout: 1_000 });
95
+ try {
96
+ await once(candidate, "open");
97
+ socket = candidate;
98
+ }
99
+ catch (error) {
100
+ candidate.on("error", () => { });
101
+ candidate.terminate();
102
+ if (error.code !== "ECONNREFUSED" || Date.now() >= deadline)
103
+ throw error;
104
+ await delay(50);
105
+ }
106
+ }
107
+ const server = readWindowsProcesses().find(row => row.pid === child.pid);
108
+ if (!server)
109
+ throw new Error("公式Codexのprocess情報がありません");
110
+ const record = { schema: "aiterm.windows-relay.v1", serverPid: server.pid, serverStarted: server.started, binary, endpoint };
111
+ writeFileSync(file, JSON.stringify(record), { flag: "wx" });
112
+ makeFilePrivate(file);
113
+ const connected = socket;
114
+ connected.on("message", (data, binary) => {
115
+ if (binary) {
116
+ failed = new Error("公式Codexから予期しないバイナリ応答を受信しました");
117
+ stop();
118
+ return;
119
+ }
120
+ if (!process.stdout.write(data.toString() + "\n"))
121
+ connected.pause();
122
+ });
123
+ process.stdout.on("drain", () => connected.resume());
124
+ connected.on("error", () => { failed = new Error("公式Codexとの接続に失敗しました"); stop(); });
125
+ connected.on("close", () => { if (!ending) {
126
+ failed = new Error("公式Codexとの接続が終了しました");
127
+ stop();
128
+ } });
129
+ // initializeが接続準備前に届いても失わないよう、iteratorを接続前から保持する。
130
+ for await (const line of lines)
131
+ await new Promise((resolve, reject) => connected.send(line, error => error ? reject(error) : resolve()));
132
+ if (failed)
133
+ throw failed;
134
+ return 0;
135
+ }
136
+ finally {
137
+ stop();
138
+ await exited;
139
+ diagnostics.close();
140
+ rmSync(directory, { recursive: true, force: true });
141
+ }
142
+ }
143
+ if (process.argv[1] === fileURLToPath(import.meta.url)) {
144
+ const [binary, root, ...args] = process.argv.slice(2);
145
+ try {
146
+ if (!binary || !root)
147
+ throw new Error("Windows中継の起動指定がありません");
148
+ process.exitCode = await runWindowsCodexRelay(binary, root, args);
149
+ }
150
+ catch (error) {
151
+ process.stderr.write(`aiterm-mcp: ${error instanceof Error ? error.message : "Windows中継が失敗しました"}\n`);
152
+ process.exitCode = 1;
153
+ }
154
+ }
@@ -0,0 +1,201 @@
1
+ // gpt-connectorのWindows実装をMIT条件で移植。実行時の外部製品依存は持たない。
2
+ // Windows Desktop用の実行file、ユーザー環境変数、親processの接続確認を所有する。
3
+ import { existsSync, readFileSync, writeFileSync, renameSync, readdirSync, rmSync } from "node:fs";
4
+ import { join, dirname } from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ import { spawnSync } from "node:child_process";
7
+ import { randomUUID, createHash } from "node:crypto";
8
+ import { homedir } from "node:os";
9
+ import { relayConfigDirectory, readRelayConfig } from "./codex-relay-config.js";
10
+ import { SetupError } from "./setup-platform.js";
11
+ import { verifyRelayLauncher } from "./setup-codex-relay.js";
12
+ import { withCodexRelay } from "./codex-relay-client.js";
13
+ import { windowsPowerShellSync, quotePowerShell } from "./windows-codex-state.js";
14
+ import { ensurePrivateDirectory, makeFilePrivate } from "./windows-codex-state.js";
15
+ import { readWindowsRelay, windowsDesktopRelay } from "./windows-codex-connection.js";
16
+ export function findWindowsCodexCache(resources, cache) {
17
+ // Desktopが展開した4実行fileを配布元と照合する。展開・更新はDesktop自身が所有する。
18
+ const names = ["codex.exe", "codex-code-mode-host.exe", "codex-windows-sandbox-setup.exe", "codex-command-runner.exe"];
19
+ const digest = (file) => createHash("sha256").update(readFileSync(file)).digest("hex");
20
+ const expected = names.map(name => digest(join(resources, name)));
21
+ const found = (existsSync(cache) ? readdirSync(cache, { withFileTypes: true }) : [])
22
+ .filter(entry => entry.isDirectory() && !entry.isSymbolicLink() && /^[0-9a-f]{16}$/.test(entry.name))
23
+ .filter(entry => names.every((name, index) => {
24
+ const file = join(cache, entry.name, name);
25
+ return existsSync(file) && digest(file) === expected[index];
26
+ }));
27
+ if (found.length !== 1)
28
+ throw new SetupError("codex_desktop_runtime_unavailable", "現在のCodex Desktopが展開した実行fileを特定できません。公式Desktopを起動してからsetupを再実行してください。");
29
+ return join(cache, found[0].name, "codex.exe");
30
+ }
31
+ export function findWindowsCodexBinary() {
32
+ const resources = windowsPowerShellSync(`
33
+ $packages = @(Get-AppxPackage -Name OpenAI.Codex)
34
+ if ($packages.Count -ne 1) { throw '公式Codex Desktopを一つに特定できません' }
35
+ Join-Path $packages[0].InstallLocation 'app/resources'`);
36
+ const value = findWindowsCodexCache(resources, join(process.env.LOCALAPPDATA ?? join(homedir(), "AppData", "Local"), "OpenAI", "Codex", "bin"));
37
+ const result = spawnSync(value, ["--version"], { encoding: "utf8", windowsHide: true, timeout: 10_000 });
38
+ if (result.error || result.status !== 0)
39
+ throw new SetupError("codex_binary_unavailable", "公式Codex Desktopの実行fileを起動できません。");
40
+ const match = /codex-cli (\d+)\.(\d+)\.(\d+)/.exec(result.stdout ?? "");
41
+ if (result.status !== 0 || !match || (Number(match[1]) === 0 && Number(match[2]) < 154))
42
+ throw new SetupError("codex_version_unsupported", "公式Codex Desktopの対応CLIを確認できません。");
43
+ return value;
44
+ }
45
+ function getGui(key) {
46
+ return windowsPowerShellSync(`[Environment]::GetEnvironmentVariable(${quotePowerShell(key)}, 'User')`) || null;
47
+ }
48
+ function setGui(key, value) {
49
+ windowsPowerShellSync(`
50
+ [Environment]::SetEnvironmentVariable(${quotePowerShell(key)}, ${value === null ? "$null" : quotePowerShell(value)}, 'User')
51
+ Add-Type -TypeDefinition @'
52
+ using System;
53
+ using System.Runtime.InteropServices;
54
+ public static class AitermEnvironment {
55
+ [DllImport("user32.dll", CharSet=CharSet.Unicode)]
56
+ public static extern IntPtr SendMessageTimeout(IntPtr window, uint message, UIntPtr wparam, string value, uint flags, uint timeout, out UIntPtr result);
57
+ }
58
+ '@
59
+ $result = [UIntPtr]::Zero
60
+ [void][AitermEnvironment]::SendMessageTimeout([IntPtr]0xffff, 0x1a, [UIntPtr]::Zero, 'Environment', 2, 5000, [ref]$result)
61
+ `, 10_000);
62
+ }
63
+ export function windowsLauncherSource(node, relay, binary, root) {
64
+ return `using System;
65
+ using System.Diagnostics;
66
+ using System.Text;
67
+ public static class AitermLauncher {
68
+ static async System.Threading.Tasks.Task Input(Process child) {
69
+ var input = Console.OpenStandardInput(); var output = child.StandardInput.BaseStream;
70
+ var buffer = new byte[8192]; int count;
71
+ while ((count = await input.ReadAsync(buffer, 0, buffer.Length)) > 0) {
72
+ await output.WriteAsync(buffer, 0, count); await output.FlushAsync();
73
+ }
74
+ child.StandardInput.Close();
75
+ }
76
+ static string Quote(string text) {
77
+ var result = new StringBuilder().Append('"'); int slashes = 0;
78
+ foreach (char c in text) {
79
+ if (c == '\\\\') { slashes++; continue; }
80
+ if (c == '"') { result.Append('\\\\', slashes * 2 + 1); result.Append(c); }
81
+ else { result.Append('\\\\', slashes); result.Append(c); }
82
+ slashes = 0;
83
+ }
84
+ result.Append('\\\\', slashes * 2); return result.Append('"').ToString();
85
+ }
86
+ public static int Main(string[] args) {
87
+ try {
88
+ var arguments = new StringBuilder();
89
+ foreach (var value in new string[] { ${[relay, binary, root].map(value => JSON.stringify(value)).join(", ")} }) arguments.Append(Quote(value)).Append(' ');
90
+ foreach (var value in args) arguments.Append(Quote(value)).Append(' ');
91
+ var start = new ProcessStartInfo(${JSON.stringify(node)}, arguments.ToString());
92
+ start.UseShellExecute = false; start.CreateNoWindow = true;
93
+ start.RedirectStandardInput = true; start.RedirectStandardOutput = true; start.RedirectStandardError = true;
94
+ using (var child = Process.Start(start)) {
95
+ Input(child).ContinueWith(task => { if (task.IsFaulted && !child.HasExited) child.Kill(); });
96
+ var output = child.StandardOutput.BaseStream.CopyToAsync(Console.OpenStandardOutput());
97
+ var error = child.StandardError.BaseStream.CopyToAsync(Console.OpenStandardError());
98
+ child.WaitForExit(); System.Threading.Tasks.Task.WaitAll(output, error); return child.ExitCode;
99
+ }
100
+ } catch { Console.Error.WriteLine("aiterm-mcp: Windows中継を起動できません"); return 1; }
101
+ }
102
+ }
103
+ `;
104
+ }
105
+ export function buildWindowsLauncher(directory, source) {
106
+ const digest = createHash("sha256").update(source).digest("hex").slice(0, 16);
107
+ const launcher = join(directory, `codex-${digest}.exe`);
108
+ const sourceFile = join(directory, `codex-${digest}.cs`);
109
+ if (existsSync(launcher) && existsSync(sourceFile) && readFileSync(sourceFile, "utf8") === source)
110
+ return launcher;
111
+ const compiler = join(process.env.SystemRoot ?? "C:\\Windows", "Microsoft.NET", "Framework64", "v4.0.30319", "csc.exe");
112
+ if (!existsSync(compiler))
113
+ throw new SetupError("codex_launcher_compiler_unavailable", "Windows標準.NET Frameworkのコンパイラが見つかりません。");
114
+ writeFileSync(sourceFile, source);
115
+ makeFilePrivate(sourceFile);
116
+ const result = spawnSync(compiler, ["/nologo", "/target:exe", `/out:${launcher}`, sourceFile], { encoding: "utf8", windowsHide: true, timeout: 20_000 });
117
+ if (result.status !== 0)
118
+ throw new SetupError("codex_launcher_build_failed", "Windows用のCodex起動fileを作成できません。");
119
+ makeFilePrivate(launcher);
120
+ return launcher;
121
+ }
122
+ async function live(config) {
123
+ const file = windowsDesktopRelay(config.launcher);
124
+ if (!file)
125
+ return false;
126
+ await withCodexRelay(file, request => request("thread/loaded/list", { limit: 1 }));
127
+ return true;
128
+ }
129
+ export async function configureWindowsCodexSteer(action, overrides = {}) {
130
+ const runtime = { directory: relayConfigDirectory(), node: process.execPath,
131
+ relay: fileURLToPath(new URL("./windows-codex-relay.js", import.meta.url)), findBinary: findWindowsCodexBinary, getGui, setGui,
132
+ verify: verifyRelayLauncher, live,
133
+ shared: async (launcher) => {
134
+ const file = windowsDesktopRelay(launcher);
135
+ if (!file)
136
+ return null;
137
+ await withCodexRelay(file, request => request("thread/loaded/list", { limit: 1 }));
138
+ return { binary: readWindowsRelay(file).binary, root: dirname(dirname(file)) };
139
+ }, ...overrides };
140
+ const previous = readRelayConfig(runtime.directory);
141
+ const current = runtime.getGui("CODEX_CLI_PATH");
142
+ if (action === "status") {
143
+ if (!previous?.enabled)
144
+ return { status: "disabled" };
145
+ if (current !== previous.launcher)
146
+ return { status: "failed", reason_code: "codex_steer_configuration_changed" };
147
+ return await runtime.live(previous) ? { status: "ready" } : { status: "restart_required", reason_code: "codex_restart_required" };
148
+ }
149
+ if (action === "disable") {
150
+ if (!previous?.enabled)
151
+ return { status: "disabled" };
152
+ if (current !== previous.launcher)
153
+ throw new SetupError("codex_steer_configuration_changed", "変更されたCodex起動設定は上書きしません。");
154
+ if (previous.launcher !== previous.previous_cli_path)
155
+ runtime.setGui("CODEX_CLI_PATH", previous.previous_cli_path);
156
+ if (runtime.getGui("CODEX_CLI_PATH") !== previous.previous_cli_path)
157
+ throw new SetupError("codex_steer_readback_failed", "元の起動設定を確認できません。");
158
+ save(runtime.directory, { ...previous, enabled: false });
159
+ return { status: "restart_required", reason_code: "codex_restart_required" };
160
+ }
161
+ if (previous?.enabled && previous.launcher !== previous.previous_cli_path && ![previous.launcher, previous.previous_cli_path].includes(current)) {
162
+ throw new SetupError("codex_steer_configuration_changed", "変更されたCodex起動設定は上書きしません。");
163
+ }
164
+ if (current && (current !== previous?.launcher || previous.launcher === previous.previous_cli_path)) {
165
+ const shared = await runtime.shared(current);
166
+ if (!shared)
167
+ throw new SetupError("codex_steer_configuration_conflict", "別のCodex起動設定との互換接続を確認できないため変更していません。");
168
+ ensurePrivateDirectory(runtime.directory);
169
+ save(runtime.directory, { schema: "aiterm.codex-relay.v1", enabled: true, binary: shared.binary,
170
+ node: runtime.node, launcher: current, socket_root: shared.root, previous_cli_path: current });
171
+ return { status: "ready" };
172
+ }
173
+ for (const key of ["CODEX_APP_SERVER_WS_URL", "CODEX_APP_SERVER_USE_LOCAL_DAEMON", "CODEX_APP_SERVER_FORCE_CLI"]) {
174
+ if (runtime.getGui(key))
175
+ throw new SetupError("codex_steer_configuration_conflict", `${key}が設定されているため変更していません。`);
176
+ }
177
+ ensurePrivateDirectory(runtime.directory);
178
+ const binary = runtime.findBinary();
179
+ const root = join(runtime.directory, "sessions");
180
+ ensurePrivateDirectory(root);
181
+ const launcher = buildWindowsLauncher(runtime.directory, windowsLauncherSource(runtime.node, runtime.relay, binary, root));
182
+ await runtime.verify(launcher);
183
+ const config = { schema: "aiterm.codex-relay.v1", enabled: true, binary, node: runtime.node, launcher,
184
+ socket_root: root, previous_cli_path: previous?.enabled && previous.launcher !== previous.previous_cli_path ? previous.previous_cli_path : current };
185
+ save(runtime.directory, config);
186
+ runtime.setGui("CODEX_CLI_PATH", launcher);
187
+ if (runtime.getGui("CODEX_CLI_PATH") !== launcher)
188
+ throw new SetupError("codex_steer_readback_failed", "WindowsのCodex起動設定を確認できません。");
189
+ return await runtime.live(config) ? { status: "ready" } : { status: "restart_required", reason_code: "codex_restart_required" };
190
+ }
191
+ function save(directory, config) {
192
+ const file = join(directory, `${randomUUID()}.tmp`);
193
+ try {
194
+ writeFileSync(file, JSON.stringify(config, null, 2) + "\n", { flag: "wx" });
195
+ makeFilePrivate(file);
196
+ renameSync(file, join(directory, "config.json"));
197
+ }
198
+ finally {
199
+ rmSync(file, { force: true });
200
+ }
201
+ }
@@ -0,0 +1,44 @@
1
+ // WindowsのSteer保存先とPowerShell境界。接続記録の読取ではACLを変更しない。
2
+ import { execFileSync } from "node:child_process";
3
+ import { mkdirSync, lstatSync } from "node:fs";
4
+ import { isAbsolute } from "node:path";
5
+ import { resolveWindowsPowerShell7 } from "./windows-powershell.js";
6
+ import { CodexDeliveryError } from "./codex-delivery-error.js";
7
+ export const quotePowerShell = (value) => `'${value.replace(/'/g, "''")}'`;
8
+ export function windowsPowerShellSync(script, timeout = 15_000) {
9
+ const source = "$ErrorActionPreference = 'Stop'\n[Console]::OutputEncoding = [Text.UTF8Encoding]::new($false)\n" + script;
10
+ try {
11
+ return execFileSync(resolveWindowsPowerShell7(), ["-NoLogo", "-NoProfile", "-NonInteractive", "-EncodedCommand",
12
+ Buffer.from(source, "utf16le").toString("base64")], {
13
+ encoding: "utf8", windowsHide: true, timeout, maxBuffer: 16 * 1024 * 1024, stdio: ["ignore", "pipe", "pipe"],
14
+ }).trim();
15
+ }
16
+ catch {
17
+ throw new CodexDeliveryError("CODEX_WINDOWS_OPERATION_FAILED", "WindowsのSteer設定・接続情報を確認できません");
18
+ }
19
+ }
20
+ function makePrivate(target, directory) {
21
+ const stat = lstatSync(target);
22
+ if (!isAbsolute(target) || stat.isSymbolicLink() || (directory ? !stat.isDirectory() : !stat.isFile())) {
23
+ throw new CodexDeliveryError("CODEX_RELAY_PATH_INVALID", "Steer保存先が不正です");
24
+ }
25
+ windowsPowerShellSync(`
26
+ $target = ${quotePowerShell(target)}
27
+ $sid = [System.Security.Principal.WindowsIdentity]::GetCurrent().User
28
+ $previous = Get-Acl -LiteralPath $target
29
+ if ($previous.GetOwner([System.Security.Principal.SecurityIdentifier]).Value -notin @($sid.Value, 'S-1-5-32-544')) { throw '所有者が一致しません' }
30
+ $acl = [System.Security.AccessControl.${directory ? "DirectorySecurity" : "FileSecurity"}]::new()
31
+ $acl.SetOwner($sid)
32
+ $acl.SetAccessRuleProtection($true, $false)
33
+ $rule = [System.Security.AccessControl.FileSystemAccessRule]::new($sid, 'FullControl', '${directory ? "ContainerInherit, ObjectInherit" : "None"}', 'None', 'Allow')
34
+ $acl.AddAccessRule($rule)
35
+ Set-Acl -LiteralPath $target -AclObject $acl
36
+ $actual = Get-Acl -LiteralPath $target
37
+ if ($actual.GetOwner([System.Security.Principal.SecurityIdentifier]).Value -ne $sid.Value -or !$actual.AreAccessRulesProtected) { throw 'ACLの適用を確認できません' }
38
+ `);
39
+ }
40
+ export function ensurePrivateDirectory(directory) {
41
+ mkdirSync(directory, { recursive: true });
42
+ makePrivate(directory, true);
43
+ }
44
+ export function makeFilePrivate(file) { makePrivate(file, false); }
package/docs/DESIGN.md CHANGED
@@ -82,7 +82,18 @@ macOSのユーザーLaunchAgentはログイン時に同じ起動設定を適用
82
82
  保存した元の`CODEX_CLI_PATH`へ戻す。他から変更された設定は上書きしない。
83
83
  `status=ready`は公式binary・Desktopの直接子・同じsocketのRPC応答を確認した場合だけ返す。
84
84
  初回の設定保存後は`restart_required`であり、Desktopの完全終了・再起動を要する。
85
- Linux/WindowsのSteer選択は`unsupported`とする。通常の単品導入・queue配送は両OSで維持する。
85
+ WindowsはAitermのnative launcherからNode中継と公式Desktop CLIを起動し、通常の親子関係を保つ。
86
+ Windows標準の.NET Frameworkでlauncherを作り、引数とstdioをそのまま中継する。
87
+ 公式Desktopが展開した4実行ファイルを配布元のSHA-256と照合し、AitermがCodexをコピー・再配布しない。
88
+ 公式CLIの認証付きloopback WebSocketを使い、本人専用ACLのtokenと接続記録をAitermのsessions下へ置く。
89
+ Windowsのprocess情報はprocess-runtimeが所有し、接続はPID・開始時刻・実行ファイル・token引数で照合する。
90
+ MCP processの祖先から親を選び、同じloaded taskへ送る。開始時刻は小数桁の表記差を正規化して比較する。
91
+ stdio EOFで自分が起動した公式serverを停止し、接続記録を削除する。
92
+ 既存の互換launcherは公式serverからlauncherとDesktopへの祖先関係、ACL、RPCが成立した場合だけ共有する。
93
+ 対応する接続記録はaiterm.windows-relay.v1とgpt-connector.windows-relay.v1で、後者もAiterm内で解釈する。
94
+ 共有時はlauncherとprevious_cli_pathを同値で記録し、enableとdisableで所有外の起動設定を変更しない。
95
+ 接続共有は任意であり、他製品のコード・設定・コマンドはAiterm単独導入の前提にならない。
96
+ LinuxのSteer選択はunsupportedとする。通常の単品導入・queue配送は全対応OSで維持する。
86
97
 
87
98
  単品導入のqueue配送は親の実行中turnを中断せず、親がidleになった後に処理される。
88
99
  `parent_delivery`が配送IDと状態を返し、自動配送時の`wait_process`/`wait_command`はnullになる。
package/docs/RELEASE.md CHANGED
@@ -39,8 +39,11 @@ Codex Steerを変更した場合は、公式バイナリを指定した`test/cod
39
39
  実行中Steer・終了後再開・承認応答・stdio終了を先に確認する。公開packageの
40
40
  `aiterm-setup --json --codex-steer enable`で選択導入し、`restart_required`なら人がDesktopを完全再起動する。
41
41
  再起動後の`aiterm-setup --codex-steer status`が`ready`であることと、通常の親からの子の回答配送を確認する。
42
- socketがあるだけで成功とせず、公式binaryとDesktopの直接の子であることまで確認する。
43
- Linux/Windowsの単品導入と、未対応のSteer選択が理由付きで停止することもCIで確認する。
42
+ 接続があるだけで成功とせず、公式binaryとDesktopの親子関係まで確認する。
43
+ Windowsは`AITERM_TEST_CODEX_BINARY`に公式Desktopの実行fileを指定し、
44
+ `test/windows-codex.test.mjs`で単独launcher、ACL、親識別、既存接続共有、設定解除も確認する。
45
+ 同じ環境変数で`test/codex-relay-official.test.mjs`の実行中Steer・終了後再開・承認中継・EOFを確認できる。
46
+ Linuxの未対応Steer選択が理由付きで停止することもCIで確認する。
44
47
 
45
48
  setupを変更した場合は、公開packageのglobal install後に`aiterm-setup --json`を実行し、
46
49
  端末実行と検出した各AIの登録結果を確認する。初回と再実行は一時設定領域でも試験し、所有外の設定保持を確かめる。
@@ -70,7 +73,8 @@ aiterm-setup --json
70
73
  巻き戻しは既知の正常版を指定する。
71
74
 
72
75
  Steerを持たない旧版へ戻す時は、install前に`aiterm-setup --codex-steer disable`を実行する。
73
- 専用LaunchAgentの解除と元のGUI起動設定の復元を確認し、MCP clientを再起動する。
76
+ macOSは専用LaunchAgentの解除、Windowsは元のユーザー環境変数の復元を確認し、MCP clientを再起動する。
77
+ 互換launcherを共有していた場合はAitermのSteer選択だけを解除し、所有外の起動設定を保持する。
74
78
  回答record schemaは従来と共通であり、Steer送信済みrecordの`queued_submission_id`はnullとなる。
75
79
 
76
80
  Claude親配送hookのない旧版へ戻す場合は、旧版のinstall前に`aiterm-setup --remove-claude-parent-hooks`を
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aiterm-mcp",
3
- "version": "0.36.0",
3
+ "version": "0.37.0",
4
4
  "mcpName": "io.github.kitepon/aiterm-mcp",
5
5
  "description": "Persistent terminal MCP with one harness-based launcher for Claude Code, Codex CLI, Grok CLI, and Cursor Agent CLI, plus durable PTYs for SSH, containers, and REPLs.",
6
6
  "keywords": [