aiterm-mcp 0.27.6 → 0.27.8

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.
@@ -0,0 +1,209 @@
1
+ // vendor中立の共有プリミティブ。core と vendors/ の両方が依存する最下層で、
2
+ // tmux-runtime / errors 以外の内部moduleへ依存しない(依存方向: core → vendors → agent-shared)。
3
+ import * as fs from "node:fs";
4
+ import * as os from "node:os";
5
+ import * as path from "node:path";
6
+ import { randomBytes } from "node:crypto";
7
+ import { AitermError } from "./errors.js";
8
+ export const sleep = (ms) => new Promise((res) => setTimeout(res, ms));
9
+ export function currentUid() {
10
+ // Windows(native)は process.getuid を持たない。以前はここで throw していたが、
11
+ // agent metadata の存在確認経由で素の pty_send まで巻き込んで全 send を殺していた。
12
+ // Windows の fs.Stats.uid は常に 0 なので、ここも 0 を返せば owner 比較
13
+ // (st.uid !== currentUid()) は自然に通過する。Windows は POSIX owner 検証を
14
+ // 持たない(NTFS ACL は別体系)という既知の制約の明示的受容であり、POSIX 側は
15
+ // getuid をそのまま返すため挙動不変。
16
+ if (typeof process.getuid !== "function")
17
+ return 0;
18
+ return process.getuid();
19
+ }
20
+ export function runtimeStateBase() {
21
+ const xdg = process.env.XDG_RUNTIME_DIR;
22
+ if (xdg) {
23
+ try {
24
+ if (fs.statSync(xdg).isDirectory())
25
+ return xdg;
26
+ }
27
+ catch {
28
+ /* XDG_RUNTIME_DIR が壊れている CI/非 login 環境では os.tmpdir() に戻す */
29
+ }
30
+ }
31
+ return os.tmpdir();
32
+ }
33
+ export function safeStatSize(p) {
34
+ try {
35
+ return fs.statSync(p).size;
36
+ }
37
+ catch {
38
+ return 0;
39
+ }
40
+ }
41
+ export function readFileRange(p, from, to) {
42
+ const len = Math.max(0, to - from);
43
+ if (len === 0)
44
+ return Buffer.alloc(0);
45
+ let fd;
46
+ try {
47
+ fd = fs.openSync(p, "r");
48
+ const buf = Buffer.alloc(len);
49
+ const n = fs.readSync(fd, buf, 0, len, from);
50
+ return n === len ? buf : buf.subarray(0, Math.max(0, n));
51
+ }
52
+ catch {
53
+ return Buffer.alloc(0);
54
+ }
55
+ finally {
56
+ if (fd !== undefined) {
57
+ try {
58
+ fs.closeSync(fd);
59
+ }
60
+ catch {
61
+ /* noop */
62
+ }
63
+ }
64
+ }
65
+ }
66
+ export function writeJson0600(p, v) {
67
+ // truncate-in-place はクラッシュ/ENOSPC の窓で空・途中 JSON を残すので、temp→rename の原子的置換にする
68
+ const tmp = `${p}.${randomBytes(6).toString("hex")}.tmp`;
69
+ fs.writeFileSync(tmp, JSON.stringify(v, null, 2) + "\n", { mode: 0o600, flag: "wx" });
70
+ try {
71
+ fs.chmodSync(tmp, 0o600);
72
+ }
73
+ catch {
74
+ /* noop */
75
+ }
76
+ try {
77
+ fs.renameSync(tmp, p);
78
+ }
79
+ catch (e) {
80
+ try {
81
+ fs.unlinkSync(tmp);
82
+ }
83
+ catch {
84
+ /* noop */
85
+ }
86
+ throw e;
87
+ }
88
+ }
89
+ export function writeText0600(p, text) {
90
+ fs.writeFileSync(p, text, { mode: 0o600 });
91
+ try {
92
+ fs.chmodSync(p, 0o600);
93
+ }
94
+ catch {
95
+ /* noop */
96
+ }
97
+ }
98
+ export function createEmpty0600(p) {
99
+ // O_EXCL が「既存 path なら失敗」を保証するため、新規作成の一意性はこれで足りる。
100
+ // O_NOFOLLOW は撤去した(オーナー裁定 2026-08-19)。
101
+ const fd = fs.openSync(p, fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_WRONLY, 0o600);
102
+ fs.closeSync(fd);
103
+ }
104
+ // 単一引用符で安全に包む(' は '\'' で脱出)。send は raw:true で送るため自前で quote する。
105
+ export function shq(s) {
106
+ return `'${s.replace(/'/g, "'\\''")}'`;
107
+ }
108
+ export const LAUNCH_ID_RE = /^[0-9a-f]{32}$/;
109
+ export const AGENT_DONE_POLL_MS = 100;
110
+ export const AGENT_EVENT_MAX_BYTES = 1024 * 1024;
111
+ export const AGENT_EVENT_TAIL_BYTES = 64 * 1024;
112
+ export const CODEX_TRANSCRIPT_INCREMENT_MAX_BYTES = 16 * 1024 * 1024;
113
+ export const GROK_TRANSCRIPT_INCREMENT_MAX_BYTES = 16 * 1024 * 1024;
114
+ // session 名はファイルパス(logpath 等)と pipe-pane の /bin/sh 文字列へ流れる。英数 _ - のみ・64字に
115
+ // 限定し、パストラバーサル(../)とシェルインジェクション(' でのクオート破り・$・; 等)を全入口で断つ。
116
+ export function assertSessionName(name) {
117
+ if (!/^[A-Za-z0-9_-]{1,64}$/.test(name))
118
+ throw new AitermError(`session 名は英数字と _ - のみ・64文字以内にしてください: ${JSON.stringify(name)}`, 2);
119
+ }
120
+ export function stateRoot() {
121
+ const uid = currentUid();
122
+ const base = runtimeStateBase();
123
+ return path.join(base, `aiterm-mcp-${uid}`);
124
+ }
125
+ export function ensureStateRoot() {
126
+ // state root は OS が与える per-user runtime dir(XDG_RUNTIME_DIR / os.tmpdir())の下に作る。
127
+ // 以前はここで symlink・owner・mode を検査していたが、共有 /tmp に敵対的な同居主体がいる
128
+ // 前提の防御であり、対応 OS の既定配置では成立しない(オーナー裁定 2026-08-19)。
129
+ // 作成時の 0o700 は検査ではなく妥当な既定として残す。経路の異常は以降の
130
+ // open/stat が OS エラーとしてそのまま露出させる。
131
+ const root = stateRoot();
132
+ fs.mkdirSync(root, { recursive: true, mode: 0o700 });
133
+ fs.mkdirSync(path.join(root, "agents"), { recursive: true, mode: 0o700 });
134
+ return root;
135
+ }
136
+ export function agentsDir() {
137
+ return path.join(ensureStateRoot(), "agents");
138
+ }
139
+ export function agentEventPath(name, launchId) {
140
+ assertSessionName(name);
141
+ if (!LAUNCH_ID_RE.test(launchId))
142
+ throw new AitermError(`launch_id が不正です: ${launchId}`, 2);
143
+ return path.join(agentsDir(), `${name}.${launchId}.events.jsonl`);
144
+ }
145
+ export function agentMetadataPath(name, launchId) {
146
+ assertSessionName(name);
147
+ if (!LAUNCH_ID_RE.test(launchId))
148
+ throw new AitermError(`launch_id が不正です: ${launchId}`, 2);
149
+ return path.join(agentsDir(), `${name}.${launchId}.agent.json`);
150
+ }
151
+ export function agentWaitLockPath(name, launchId) {
152
+ assertSessionName(name);
153
+ if (!LAUNCH_ID_RE.test(launchId))
154
+ throw new AitermError(`launch_id が不正です: ${launchId}`, 2);
155
+ return path.join(agentsDir(), `${name}.${launchId}.wait.lock`);
156
+ }
157
+ export function writeAgentMetadata(meta) {
158
+ writeJson0600(agentMetadataPath(meta.aiterm_session, meta.launch_id), meta);
159
+ }
160
+ export function agentLabel(kind) {
161
+ return kind === "claude"
162
+ ? "Claude Code"
163
+ : kind === "composer"
164
+ ? "Grok Build(Composer)"
165
+ : kind === "grok"
166
+ ? "Grok Build(Grok)"
167
+ : "Codex";
168
+ }
169
+ export function subagentInstruction(meta) {
170
+ if (meta.agent_role !== "subagent" ||
171
+ !meta.parent_session_id ||
172
+ !Number.isSafeInteger(meta.delegation_depth) ||
173
+ !meta.lineage ||
174
+ meta.delegation_allowed !== true) {
175
+ throw new AitermError("sub-agent instructionに必要なlineage metadataがありません", 2);
176
+ }
177
+ return [
178
+ "<aiterm_subagent_context>",
179
+ "あなたはaitermから起動されたsub-agentであり、root agentではありません。",
180
+ `AITERM_AGENT_LAUNCH_ID=${meta.launch_id}`,
181
+ `role=${meta.agent_role}`,
182
+ `parent_session_id=${meta.parent_session_id}`,
183
+ `delegation_depth=${meta.delegation_depth}`,
184
+ `lineage=${meta.lineage}`,
185
+ "delegation_allowed=true",
186
+ "任務の所有権を保ち、結果を親へ返してください。必要なら追加のsub-agentへ委譲してよいです。",
187
+ "ただし、同じ任務全体を同型agentへ反射的に丸投げして自己複製ループを作らないでください。",
188
+ "</aiterm_subagent_context>",
189
+ ].join("\n");
190
+ }
191
+ // launch noteのwrite_scope説明。文言はkindに依存する分岐まで含めて単一実装で持つ
192
+ // (vendor別noteへ複製すると文言が発散する)。
193
+ export function writeScopeLaunchNote(kind, writeScope) {
194
+ return writeScope === undefined
195
+ ? ""
196
+ : (kind === "codex" || kind === "grok" || kind === "composer") && writeScope === "read-only"
197
+ ? `\n能力宣言: write_scope=${JSON.stringify(writeScope)}。${agentLabel(kind)} CLIへ --sandbox read-only を付与し、書込みを実効禁止。` +
198
+ (kind === "codex" ? "" : "MCPツール許可は --always-approve で自動承認(sandbox内のため能力拡大なし)。")
199
+ : `\n能力宣言: write_scope=${JSON.stringify(writeScope)}。パス単位のsandbox allowlistに対応するCLI引数がないため宣言の記録のみ(構造的unsupported)。`;
200
+ }
201
+ export function agentLineageFields(context) {
202
+ return {
203
+ agent_role: context.agentRole,
204
+ parent_session_id: context.parentSessionId,
205
+ delegation_depth: context.delegationDepth,
206
+ lineage: context.lineage,
207
+ delegation_allowed: context.delegationAllowed,
208
+ };
209
+ }