aiterm-mcp 0.20.3 → 0.21.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.ja.md CHANGED
@@ -135,6 +135,8 @@ pty_read(id, { wait: true }) → 削減済みの出力を読む(完了
135
135
 
136
136
  同じ primitive が別エージェントの TUI を宿す。4 つの起動ツールが、Claude/Codex/Grok/Composer の対話 TUI を新しい永続端末の中に起動し、`session_id` を返す。既存の人間向けtextに加えて`aiterm.agent-launch-result.v1` structured receiptも返すため、durable callerは表示文字列を解析せずsession handleを取得できる。以後は同じ `pty_read` / `pty_send` で継続操作する。**起動は常に managed**(aiterm 所有の Stop hook 付き)で、agent session への `pty_send` は非ブロックの **dispatch** になり `event_cursor` 入り receipt を即返す。完了通知は `aiterm-wait --session <id> --cursor <event_cursor>` をホストのバックグラウンドタスクとして実行し、exit 時に receipt の `outcome` で判定する(exit 0=done / 3=timeout=未完了・既定600秒 / 4=closed。親はブロックもポーリングもしない)。起動時 `prompt` を渡した launch は structured receipt にコピペ可能な `wait_command` と `event_cursor`、そして `submit_residue` 観測を含む(true=prompt が composer に未 submit で残存している疑い=案内に従い画面確認から復旧 / false=残存観測せず・成立の保証ではない / null=対象外)。dispatch receipt にも同じ観測が付く。durable machine callerは`claude_turn({ action:"issue"|"recover", session_id, operation_id, ... })`を使い、人間向けerror文字列を解析せず`accepted`/`pending`/`completed`/`unknown`を判定できる。recoveryは再送せず、検証済み完了だけがexact `raw_output`を持つ。通常の`pty_send`/`pty_read`は対話callerと人間向けに維持する。`C-c`後もmarkerを保持し、Stopが来なければsessionをcloseする。`claude_agent` と `codex_agent` の初回 `prompt` は ready gate 経由で送信して待たずに返る(Grok/Composer は argv 渡し)。手動でキー操作したい場合は `pty_open` で素の端末を開き vendor CLI を自分で起動する。
137
137
 
138
+ `codex_agent`・`grok_agent`・`composer_agent`は任意の`write_scope`(`"read-only"`または書込み許可パスの説明)も受ける。指定値はlaunch receipt・session metadata・`pty_list`へ保存する。Codexの`write_scope:"read-only"`だけは実効能力壁であり、aitermがCLIの`--sandbox read-only`を付ける。Grok/Composerには対応する対話起動sandboxがなく、Codexにもパス説明をallowlistへ変換するフラグがないため、それらは強制済みと偽らず`write_scope_enforcement:"declaration_only_unsupported"`を返す。`write_scope`を省略した起動は従来どおりである。
139
+
138
140
  ```text
139
141
  codex_agent({ session_name: "codex1", cwd: "/repo",
140
142
  prompt: "port test/legacy.py to vitest" })
package/README.md CHANGED
@@ -145,6 +145,8 @@ pty_read(id, { wait: true }) → read the token-reduced output, completion
145
145
 
146
146
  The same primitive hosts another agent's TUI. Four launchers each start one vendor's interactive coding-agent TUI inside a fresh persistent terminal and return a `session_id`. Their existing human-readable text is accompanied by an `aiterm.agent-launch-result.v1` structured receipt, so durable callers never parse display text for the session handle; when the launch carries an initial `prompt`, the receipt also includes the `event_cursor`, a ready-made `wait_command` for the completion waiter, and a `submit_residue` observation (`true` = the prompt is likely still sitting unsubmitted in the composer — the hint explains recovery; `false` = no residue observed, not a proof of submission; `null` = not applicable). From there you drive it with the same `pty_read` / `pty_send` you'd use on any shell: read its output token-reduced, send it the next step. (The TUIs are full-screen apps, so `pty_read({ screen: true })` gives you the rendered view.) Every launch is **managed**: aiterm installs its own Stop hook, so turn completion is a first-class event. Sending to an agent session is a non-blocking **dispatch** — the call returns immediately with an `event_cursor`, and completion arrives via [`aiterm-wait`](#completion-push-for-parent-agents-aiterm-wait). Durable machine callers use `claude_turn({ action: "issue" | "recover", session_id, operation_id, ... })`: it returns fixed `accepted` / `pending` / `completed` / `unknown` states without parsing human-facing errors, never resends during recovery, and includes exact `raw_output` only for a verified completion. The same operation ID is carried through the dispatch receipt, active marker, Stop event, and result. The ordinary `pty_send` / `pty_read` surface remains available for interactive callers and humans. `C-c` keeps the marker for a delayed Stop; if no Stop arrives, close the session. An initial `prompt` on `claude_agent`/`codex_agent` is submitted through the same ready gate and the launcher returns without waiting; on Grok/Composer it is passed on the CLI's argv. This needs the vendor's own CLI installed and authenticated — see [Requirements](#requirements).
147
147
 
148
+ `codex_agent`, `grok_agent`, and `composer_agent` also accept an optional `write_scope`: either `"read-only"` or a human-readable description of writable paths. A supplied value is retained in the launch receipt, session metadata, and `pty_list`. For Codex, `write_scope: "read-only"` is an effective boundary: aiterm adds the CLI's `--sandbox read-only` flag. Grok/Composer have no corresponding interactive-launch sandbox, and Codex has no path-description allowlist flag; those cases return `write_scope_enforcement: "declaration_only_unsupported"` rather than claiming enforcement. Omitting `write_scope` preserves prior behavior.
149
+
148
150
  For a managed Claude turn stopped at `Do you want to proceed?`, use `claude_approval(action: "inspect", ...)` to capture the active operation and SHA-256 screen digest, review the displayed command, then call `respond` with that exact digest and either `approve_once` or `deny`. The relay rechecks the operation and screen under the send lock, never exposes arbitrary input or permanent approval, keeps the active marker intact, and records a prompt-free owner-only receipt. `pty_send(force: true)` does not bypass this boundary.
149
151
 
150
152
  ```text
File without changes
package/dist/core.js CHANGED
@@ -1230,6 +1230,7 @@ export function listSessions() {
1230
1230
  const agent = [
1231
1231
  `agent=${meta.kind}`,
1232
1232
  "agent_done=true",
1233
+ meta.write_scope === undefined ? null : `write_scope=${JSON.stringify(meta.write_scope)}`,
1233
1234
  meta.vendor_session_id ? `vendor_session_id=${meta.vendor_session_id}` : null,
1234
1235
  ]
1235
1236
  .filter(Boolean)
@@ -2261,7 +2262,7 @@ function createClaudeAgentMetadata(name, cwd, initialPrompt, launchOperationId,
2261
2262
  writeAgentMetadata(meta);
2262
2263
  return meta;
2263
2264
  }
2264
- function createCodexAgentMetadata(name, cwd, initialPrompt, overrides = {}) {
2265
+ function createCodexAgentMetadata(name, cwd, initialPrompt, overrides = {}, writeScope) {
2265
2266
  const launchId = randomBytes(16).toString("hex");
2266
2267
  const eventFile = agentEventPath(name, launchId);
2267
2268
  createEmpty0600NoFollow(eventFile);
@@ -2273,6 +2274,7 @@ function createCodexAgentMetadata(name, cwd, initialPrompt, overrides = {}) {
2273
2274
  event_file: eventFile,
2274
2275
  created_at: new Date().toISOString(),
2275
2276
  cwd,
2277
+ ...(writeScope === undefined ? {} : { write_scope: writeScope }),
2276
2278
  vendor_session_id: null,
2277
2279
  initial_prompt: initialPrompt,
2278
2280
  hook_route: "managed_codex_home",
@@ -2282,7 +2284,7 @@ function createCodexAgentMetadata(name, cwd, initialPrompt, overrides = {}) {
2282
2284
  writeAgentMetadata(meta);
2283
2285
  return meta;
2284
2286
  }
2285
- function createGrokAgentMetadata(kind, name, cwd, initialPrompt, authPath) {
2287
+ function createGrokAgentMetadata(kind, name, cwd, initialPrompt, authPath, writeScope) {
2286
2288
  const launchId = randomBytes(16).toString("hex");
2287
2289
  const eventFile = agentEventPath(name, launchId);
2288
2290
  createEmpty0600NoFollow(eventFile);
@@ -2294,6 +2296,7 @@ function createGrokAgentMetadata(kind, name, cwd, initialPrompt, authPath) {
2294
2296
  event_file: eventFile,
2295
2297
  created_at: new Date().toISOString(),
2296
2298
  cwd,
2299
+ ...(writeScope === undefined ? {} : { write_scope: writeScope }),
2297
2300
  vendor_session_id: null,
2298
2301
  initial_prompt: initialPrompt,
2299
2302
  hook_route: "managed_grok_home",
@@ -2355,6 +2358,7 @@ function loadAgentMetadata(name) {
2355
2358
  event_file: expectedEvent,
2356
2359
  created_at: typeof m.created_at === "string" ? m.created_at : "",
2357
2360
  cwd: typeof m.cwd === "string" ? m.cwd : null,
2361
+ ...(typeof m.write_scope === "string" ? { write_scope: m.write_scope } : {}),
2358
2362
  vendor_session_id: typeof m.vendor_session_id === "string" ? m.vendor_session_id : null,
2359
2363
  initial_prompt: normalizeInitialPromptState(m.initial_prompt),
2360
2364
  launch_operation_id: launchOperationId,
@@ -2377,6 +2381,7 @@ function loadAgentMetadata(name) {
2377
2381
  event_file: expectedEvent,
2378
2382
  created_at: typeof m.created_at === "string" ? m.created_at : "",
2379
2383
  cwd: typeof m.cwd === "string" ? m.cwd : null,
2384
+ ...(typeof m.write_scope === "string" ? { write_scope: m.write_scope } : {}),
2380
2385
  vendor_session_id: typeof m.vendor_session_id === "string" ? m.vendor_session_id : null,
2381
2386
  initial_prompt: normalizeInitialPromptState(m.initial_prompt),
2382
2387
  hook_route: "managed_codex_home",
@@ -2400,6 +2405,7 @@ function loadAgentMetadata(name) {
2400
2405
  event_file: expectedEvent,
2401
2406
  created_at: typeof m.created_at === "string" ? m.created_at : "",
2402
2407
  cwd: typeof m.cwd === "string" ? m.cwd : null,
2408
+ ...(typeof m.write_scope === "string" ? { write_scope: m.write_scope } : {}),
2403
2409
  vendor_session_id: typeof m.vendor_session_id === "string" ? m.vendor_session_id : null,
2404
2410
  initial_prompt: normalizeInitialPromptState(m.initial_prompt),
2405
2411
  hook_route: "managed_grok_home",
@@ -3448,6 +3454,10 @@ function buildAgentCmd(kind, bin, model, effort, prompt, meta = null) {
3448
3454
  else if (kind === "codex") {
3449
3455
  if (meta?.kind === "codex")
3450
3456
  parts.push("--dangerously-bypass-hook-trust");
3457
+ // `codex --help` で確認した実在フラグ。read-only 宣言だけはCLI sandboxへ落とし、
3458
+ // launcher自身が実効能力壁を作る。パス説明はCodex CLIに同等のallowlist引数がないため宣言のまま残す。
3459
+ if (meta?.kind === "codex" && meta.write_scope === "read-only")
3460
+ parts.push("--sandbox", "read-only");
3451
3461
  // model/effort は CLI 引数で明示(config 継承より優先)。agent_done 時は managed home 側
3452
3462
  // config.toml も同値で上書き済み(applyCodexConfigOverrides)。
3453
3463
  if (model)
@@ -3516,12 +3526,17 @@ function agentLabel(kind) {
3516
3526
  // model_reasoning_effort)が対話子へ波及する構造のため、引数・端末config継承・CLI既定の
3517
3527
  // どれで起動したかを起動時点で可視化し、実効 effort=ultra は proactive 自動委譲 ON を警告する。
3518
3528
  function buildAgentLaunchNote(kind, model, effort, meta) {
3529
+ const writeScopeNote = meta?.write_scope === undefined
3530
+ ? ""
3531
+ : kind === "codex" && meta.write_scope === "read-only"
3532
+ ? `\n能力宣言: write_scope=${JSON.stringify(meta.write_scope)}。Codex CLIへ --sandbox read-only を付与し、書込みを実効禁止。`
3533
+ : `\n能力宣言: write_scope=${JSON.stringify(meta.write_scope)}。${kind === "grok" || kind === "composer" ? "このCLIには起動sandbox機構がないため" : "パス単位のsandbox allowlistに対応するCLI引数がないため"}宣言の記録のみ(構造的unsupported)。`;
3519
3534
  if (kind === "claude") {
3520
- return `起動設定: model=${model ?? "CLI既定"} effort=${effort ?? "CLI既定"}。`;
3535
+ return `起動設定: model=${model ?? "CLI既定"} effort=${effort ?? "CLI既定"}。${writeScopeNote}`;
3521
3536
  }
3522
3537
  if (kind !== "codex") {
3523
3538
  return (`起動設定: model=${model ?? GROK_MODEL_DEFAULTS[kind]}(${model ? "引数" : "ツール既定"})。` +
3524
- "reasoning effort は対話 TUI 非対応=未指定で起動。");
3539
+ "reasoning effort は対話 TUI 非対応=未指定で起動。" + writeScopeNote);
3525
3540
  }
3526
3541
  const configPath = meta?.kind === "codex" && meta.codex_home
3527
3542
  ? path.join(meta.codex_home, "config.toml")
@@ -3540,7 +3555,7 @@ function buildAgentLaunchNote(kind, model, effort, meta) {
3540
3555
  ? "⚠ effort=ultra は max 推論+proactive 自動委譲 ON(子エージェント自動生成・使用量急増に注意)。"
3541
3556
  : "");
3542
3557
  const summary = meta?.kind === "codex" && meta.codex_home ? managedCodexConfigSummary(configPath, true) : "";
3543
- return summary ? `${launch}\n${summary}\n` : launch;
3558
+ return (summary ? `${launch}\n${summary}\n` : launch) + writeScopeNote;
3544
3559
  }
3545
3560
  function claudeLaunchRequestDigest({ sessionName, model, effort, cwd, agentDone, }) {
3546
3561
  const canonical = JSON.stringify({
@@ -3582,6 +3597,7 @@ export function openAgent(kind, opts = {}) {
3582
3597
  throw new AitermError("model が空文字です(省略するか有効なモデル名を指定してください)", 2);
3583
3598
  }
3584
3599
  const effort = opts.reasoning_effort ?? null;
3600
+ const writeScope = opts.write_scope;
3585
3601
  if (effort && kind === "claude" && !CLAUDE_EFFORTS.has(effort)) {
3586
3602
  throw new AitermError("Claude Code の reasoning_effort は low/medium/high/xhigh/max のいずれかです", 2);
3587
3603
  }
@@ -3690,8 +3706,8 @@ export function openAgent(kind, opts = {}) {
3690
3706
  ? kind === "claude"
3691
3707
  ? createClaudeAgentMetadata(sid, cwd, opts.prompt ? "pending" : "none", launchOperationId, launchRequestDigest)
3692
3708
  : kind === "codex"
3693
- ? createCodexAgentMetadata(sid, cwd, opts.prompt ? "pending" : "none", { model, effort })
3694
- : createGrokAgentMetadata(kind, sid, cwd, opts.prompt ? "pending" : "none", grokAuthPath)
3709
+ ? createCodexAgentMetadata(sid, cwd, opts.prompt ? "pending" : "none", { model, effort }, writeScope)
3710
+ : createGrokAgentMetadata(kind, sid, cwd, opts.prompt ? "pending" : "none", grokAuthPath, writeScope)
3695
3711
  : null;
3696
3712
  if (meta)
3697
3713
  agentMetadataNegativeCache.delete(sid);
@@ -3754,6 +3770,7 @@ export async function openAgentWithInitialPrompt(kind, opts = {}) {
3754
3770
  prompt,
3755
3771
  agent_done: true,
3756
3772
  launch_operation_id: opts.launch_operation_id ?? null,
3773
+ write_scope: opts.write_scope,
3757
3774
  });
3758
3775
  // argv prompt(grok/composer)は composer を経由しないため submit 座礁観測の対象外。
3759
3776
  return [sid, hint, prompt ? 0 : null, null];
@@ -3766,6 +3783,7 @@ export async function openAgentWithInitialPrompt(kind, opts = {}) {
3766
3783
  prompt: null,
3767
3784
  agent_done: true,
3768
3785
  launch_operation_id: opts.launch_operation_id ?? null,
3786
+ write_scope: opts.write_scope,
3769
3787
  });
3770
3788
  try {
3771
3789
  const initial = await sendInitialAgentPrompt(sid, prompt, {
package/dist/index.js CHANGED
@@ -420,6 +420,19 @@ const agentCompletionDesc = `起動して投げたら投げっぱなしでよい
420
420
  `結果回収は pty_read(agent_transcript:true)。`;
421
421
  function registerAgentTool(toolName, kind, desc) {
422
422
  const correlatedLaunchSchema = {};
423
+ const supportsWriteScope = kind === "codex" || kind === "grok" || kind === "composer";
424
+ const writeScopeInputSchema = supportsWriteScope
425
+ ? {
426
+ write_scope: z.string().min(1).optional().describe("能力宣言。read-only、または書込みを許可するパスの説明文字列。Codexのread-onlyだけはCLI sandboxで実効禁止する"),
427
+ }
428
+ : {};
429
+ const writeScopeOutputSchema = supportsWriteScope
430
+ ? {
431
+ // write_scope省略時は既存launch receiptを完全に保つため両fieldを出さない。
432
+ write_scope: z.string().optional(),
433
+ write_scope_enforcement: z.enum(["enforced_read_only", "declaration_only_unsupported"]).optional(),
434
+ }
435
+ : {};
423
436
  if (kind === "claude") {
424
437
  correlatedLaunchSchema.launch_operation_id = z
425
438
  .string()
@@ -437,6 +450,7 @@ function registerAgentTool(toolName, kind, desc) {
437
450
  reasoning_effort: z.string().nullish().describe(agentEffortDesc(kind)),
438
451
  cwd: z.string().nullish().describe("作業ディレクトリ(対象リポのルート等・任意)"),
439
452
  session_name: z.string().nullish().describe("セッション名(省略で自動採番)"),
453
+ ...writeScopeInputSchema,
440
454
  ...correlatedLaunchSchema,
441
455
  },
442
456
  outputSchema: {
@@ -451,8 +465,9 @@ function registerAgentTool(toolName, kind, desc) {
451
465
  // 初回prompt dispatch後のsubmit座礁観測(additive)。true=composerに残存を確認(submit未成立の疑い)/
452
466
  // false=残存を観測せず(submit成立の保証ではない)/ null=promptなし・argv prompt・判定不能。
453
467
  submit_residue: z.boolean().nullable(),
468
+ ...writeScopeOutputSchema,
454
469
  },
455
- }, async ({ prompt, model, reasoning_effort, cwd, session_name, launch_operation_id }) => {
470
+ }, async ({ prompt, model, reasoning_effort, cwd, session_name, launch_operation_id, write_scope }) => {
456
471
  try {
457
472
  const [sid, hint, eventCursor, submitResidue] = await core.openAgentWithInitialPrompt(kind, {
458
473
  prompt: prompt ?? undefined,
@@ -461,6 +476,7 @@ function registerAgentTool(toolName, kind, desc) {
461
476
  cwd: cwd ?? undefined,
462
477
  session_name: session_name ?? undefined,
463
478
  launch_operation_id: launch_operation_id ?? undefined,
479
+ ...(supportsWriteScope ? { write_scope } : {}),
464
480
  });
465
481
  const structured = {
466
482
  schema: "aiterm.agent-launch-result.v1",
@@ -470,6 +486,14 @@ function registerAgentTool(toolName, kind, desc) {
470
486
  event_cursor: eventCursor,
471
487
  wait_command: eventCursor === null ? null : `aiterm-wait --session ${sid} --cursor ${eventCursor}`,
472
488
  submit_residue: submitResidue,
489
+ ...(supportsWriteScope && write_scope !== undefined
490
+ ? {}
491
+ : supportsWriteScope ? {
492
+ write_scope,
493
+ write_scope_enforcement: kind === "codex" && write_scope === "read-only"
494
+ ? "enforced_read_only"
495
+ : "declaration_only_unsupported",
496
+ } : {}),
473
497
  };
474
498
  return {
475
499
  content: [{ type: "text", text: `session_id: ${sid}\n${hint}` }],
File without changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aiterm-mcp",
3
- "version": "0.20.3",
3
+ "version": "0.21.0",
4
4
  "mcpName": "io.github.kitepon-rgb/aiterm-mcp",
5
5
  "description": "Persistent tmux terminal MCP that lets Claude Code drive Codex CLI's interactive TUI, including slash commands and $imagegen. Also runs durable PTY sessions for SSH, containers, REPLs, and coding agents.",
6
6
  "keywords": [
@@ -54,9 +54,10 @@
54
54
  "scripts": {
55
55
  "build": "tsc",
56
56
  "mcpb:build": "npm run build && node scripts/build-mcpb.mjs && npm ci --omit=dev --ignore-scripts --no-audit --no-fund --prefix dist/mcpb-stage/server && npx --yes @anthropic-ai/mcpb@2.1.2 validate dist/mcpb-stage/manifest.json && npx --yes @anthropic-ai/mcpb@2.1.2 pack dist/mcpb-stage dist/aiterm-mcp.mcpb",
57
- "prepublishOnly": "npm run build",
57
+ "verify:release-commit": "node scripts/verify-release-commit.mjs",
58
+ "prepublishOnly": "npm run verify:release-commit && npm run build",
58
59
  "start": "node dist/index.js",
59
- "test": "npm run build && node --test test/*.test.mjs"
60
+ "test": "npm run build && node --test scripts/verify-release-commit.test.mjs test/*.test.mjs"
60
61
  },
61
62
  "dependencies": {
62
63
  "@modelcontextprotocol/sdk": "^1.29.0",