aiterm-mcp 0.61.5 → 0.61.7
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 +20 -1
- package/README.ja.md +9 -1
- package/README.md +9 -1
- package/dist/core.js +93 -21
- package/dist/execution-dispatch.js +38 -5
- package/dist/execution-launch-recovery.js +9 -0
- package/dist/execution-native-server.js +3 -0
- package/dist/execution-parent-producer.js +17 -4
- package/dist/execution-participant.js +4 -2
- package/dist/execution-runtime.js +3 -1
- package/dist/execution-service.js +49 -1
- package/dist/execution-tools.js +4 -4
- package/dist/execution-wire.js +4 -1
- package/docs/DESIGN.md +21 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.61.7] - 2026-10-23
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- Native host `agent_auth` now admits automatic-name and explicit starts, authenticated no-op starts, session status/onboarding, absent-session receipts and cancellation through the existing v1 contract. Authentication PTYs retain their actual creation native/server identity and generation; only those proven PTYs accept ref-less host input/keys. The dispatcher uses the serving SDK RPC identity, without synthetic delivery references or read-only downgrade.
|
|
15
|
+
- BellTeam model, effort and fast changes are admitted as native management RPCs bound to the existing session generation. Every native stage retains its normal stop/slot/generation checks, and partial failures retain possible effects. Fast-state metadata is revalidated before writing after asynchronous CLI observations.
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
|
|
19
|
+
- The opt-in native execution entry can explicitly reconcile a returned unknown, empty launch against its original native/server identity and current ready managed generation through `agent_execution/reconcile_launch`. It preserves the existing request and generation, issues no native input and permits a later ordinary send only through its usual admission. Issued RPCs and uncertain input or generations remain blocked; late callbacks cannot overwrite the reconciled stage.
|
|
20
|
+
|
|
21
|
+
## [0.61.6] - 2026-10-22
|
|
22
|
+
|
|
23
|
+
### Fixed
|
|
24
|
+
|
|
25
|
+
- Agent startup waits asynchronously for the existing metadata mutex before checking pane ownership, restoring its terminal mode, or accepting startup-menu keys. A short concurrent writer after promptless native creation no longer turns an otherwise valid launch into `unknown`. Stops, uncertain slots, generation changes and the existing lock-acquisition deadline still refuse further effects; no launch or input is replayed.
|
|
26
|
+
|
|
10
27
|
## [0.61.5] - 2026-10-22
|
|
11
28
|
|
|
12
29
|
### Fixed
|
|
@@ -2736,7 +2753,9 @@ prototype (preserved under `prototype/python/` as the porting source and referen
|
|
|
2736
2753
|
`ubuntu-latest` for Node 18/20/22, publishing to npm on `v*` tags with
|
|
2737
2754
|
provenance.
|
|
2738
2755
|
|
|
2739
|
-
[Unreleased]: https://github.com/kitepon/aiterm-mcp/compare/v0.61.
|
|
2756
|
+
[Unreleased]: https://github.com/kitepon/aiterm-mcp/compare/v0.61.7...HEAD
|
|
2757
|
+
[0.61.7]: https://github.com/kitepon/aiterm-mcp/compare/v0.61.6...v0.61.7
|
|
2758
|
+
[0.61.6]: https://github.com/kitepon/aiterm-mcp/compare/v0.61.5...v0.61.6
|
|
2740
2759
|
[0.61.5]: https://github.com/kitepon/aiterm-mcp/compare/v0.61.4...v0.61.5
|
|
2741
2760
|
[0.61.4]: https://github.com/kitepon/aiterm-mcp/compare/v0.61.3...v0.61.4
|
|
2742
2761
|
[0.61.3]: https://github.com/kitepon/aiterm-mcp/compare/v0.61.2...v0.61.3
|
package/README.ja.md
CHANGED
|
@@ -209,7 +209,7 @@ runtime-error store は canonical dotagents config の `collection.enabled: true
|
|
|
209
209
|
場合だけ収集し、既定OFF、network送信は行いません。tag起点CIのnpm provenance(OIDC Trusted
|
|
210
210
|
Publishing)で公開し、GitHub Release が Official MCP Registry を再登録します。
|
|
211
211
|
|
|
212
|
-
**状態:** 開発継続中 · 現行公開版 **v0.61.
|
|
212
|
+
**状態:** 開発継続中 · 現行公開版 **v0.61.7** · 動作対象は Linux · WSL2 · macOS · Windows ネイティブ · MIT · [変更履歴](CHANGELOG.md)。
|
|
213
213
|
|
|
214
214
|
### 更新と巻き戻し
|
|
215
215
|
|
|
@@ -559,6 +559,7 @@ MCPの本体を初めて使う時まで起こさない中継の後ろにAiterm
|
|
|
559
559
|
|
|
560
560
|
`agent_launch({ harness, cwd, trust_project: true })`はpromptなしでも既知のworkspace・project hooks・MCP初期同意を
|
|
561
561
|
進め、入力受付とharness生存を確認して`startup.status="ready"`を返す。指定なしのpromptなし起動は`not_checked`。
|
|
562
|
+
起動準備は競合するmetadata保存が終わるのを待って続行する。端末回復と起動メニューの各キーでは世代・停止状態を再確認し、native起動が未確認になった場合は自動再実行せず診断用に保持する。
|
|
562
563
|
Claude Code初回起動の文字表示テーマ選択では、画面で選択済みの項目を確定して起動を続ける。
|
|
563
564
|
続いてログイン方式の選択が出た場合は`vendor_onboarding_required`を返す。公式の対話型Claude Code CLIで初回設定を完了する。
|
|
564
565
|
入力受付の待ちは30秒。混んだ端末でagentの画面がまだ描かれていない間に切れた時だけ、起動から50秒まで待つ。
|
|
@@ -733,6 +734,13 @@ agent_launch({ "harness": "codex-cli", "remote": { "host": "rabbit" }, "cwd": "/
|
|
|
733
734
|
|
|
734
735
|
1回の `pty_send` が受理する本文はUTF-8で最大64KiB。同一sessionへの送信はaiterm processをまたいで直列化し、chunk同士の混線を防ぐ。全OSで長いPTY入力の欠落を避けるためplatformのmultiplexerへUTF-8境界を壊さない256-byte単位でpasteし、chunk間に10msのdrain間隔を置く。POSIX shellまたはPowerShellが前面にいる時のsanitize済み複数行は、改行を含まない単一入力へ符号化する。POSIXは`eval`、PowerShellはUTF-8のBase64を復元したscriptblockのdot-sourceを使い、変数と作業場所を現在のshellに保つ。shellがscript全体を所有してから先頭行を実行するため、途中で起動したpager/REPLが後続行を対話キーとして奪わない。単一行、`raw:true`、非shell前面は従来どおり直接PTYへpasteする。agent dispatchは本文全体を`ESC[200~/201~`で1回だけ包み、包んだ全体を同じchunkで流す。agent TUIには1回の貼り付けに見えるので画像pathが貼り付けの間で切れず、chunk投入中のキー解釈による語中文字化け・submit取り落としも抑える。途中chunkが失敗した場合は部分送信済みであることを明示し、自動でEnterを押さない。送信processの異常終了でlockが残った場合は送信前にfail-closedする。`pty_list`で対象sessionを確認し、`pty_close`で閉じてから同じsession IDを作り直す。公開の一括停止toolは存在しない。 POSIXの明示native execution構成では、unknown sendになった対象1席も`pty_close`で閉じて再作成できる。元requestはunknownのまま保持し、再送しない。発行中の操作、管理close自体のunknown、native identityが不明な席は引き続き拒否する。
|
|
735
736
|
|
|
737
|
+
BellTeamのmodel・effort・fast変更も、既存session世代に結合したnative管理要求で受け付ける。途中で世代・受付枠・停止状態が変われば、それ以降の打鍵を止める。途中まで変更した結果は作用不明のまま保持し、自動でやり直さない。
|
|
738
|
+
|
|
739
|
+
POSIXの明示native入口でも、`agent_auth`の名前なし開始、認証済みで何も起こさない開始、既存v1の確認・初回案内・取消を受け付ける。認証PTYは実際の作成元nativeと世代へ結合する。BellTeamの参照なし入力・キーは、この証拠がある認証PTYにだけ許可し、通常agentの本文には配送参照を引き続き必要とする。作成証拠のない旧認証metadataを自動登録しない。
|
|
740
|
+
|
|
741
|
+
この明示入口では `agent_execution` の `reconcile_launch` に元の `request` と正確な `target: {session_id, launch_id}` を渡し、結果不明の空起動を既存世代へ結合できる。保存したnative/serverと現在のmanaged世代、raw状態で初期promptなし、生存して入力待ちのharnessをmetadata受付内で照合し、該当するunknown launch枠だけを解放する。起動・打鍵・close・本文再送は行わない。issuedのRPC、入力状態不明、世代交代や世代未確認は拒否する。後続本文は元参照で通常のsend受付を通して一度だけ発行する。照合成功は失われた元RPC receiptの回収を意味しない。
|
|
742
|
+
|
|
743
|
+
|
|
736
744
|
## 人が覗く
|
|
737
745
|
|
|
738
746
|
POSIXの明示native実行構成では、検証済みcallerへ通常メッセージが届く間も、そのcallerから別席への送信を継続できる。callerの世代や各requestのslotは書き換えない。agentの貼付け・Enterはmetadata mutexの競合を入力前に待ち、不明な操作・世代交代・停止では入力を止める。BellTeamは閉じた席をpromptなしの明示管理`agent_launch`で再作成できる。promptを送る呼出しには元の実行参照が必要である。
|
package/README.md
CHANGED
|
@@ -227,7 +227,7 @@ collection is off by default and performs no network I/O. It ships via
|
|
|
227
227
|
tag-triggered CI with npm provenance (OIDC Trusted Publishing); the GitHub
|
|
228
228
|
Release re-registers the Official MCP Registry entry.
|
|
229
229
|
|
|
230
|
-
**Status:** actively maintained · current public release **v0.61.
|
|
230
|
+
**Status:** actively maintained · current public release **v0.61.7** · 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).
|
|
231
231
|
|
|
232
232
|
### Update and rollback
|
|
233
233
|
|
|
@@ -600,6 +600,7 @@ Owner start times are checked through `/proc` on Linux; elsewhere an existing PI
|
|
|
600
600
|
|
|
601
601
|
`agent_launch({ harness, cwd, trust_project: true })` completes known workspace, project-hook, and project-MCP startup
|
|
602
602
|
consent even without a prompt, then verifies input readiness and harness liveness before returning `startup.status="ready"`.
|
|
603
|
+
Startup checks wait for concurrent metadata writers before continuing. Each terminal recovery or startup-menu key still checks the current generation and stop state; an uncertain native launch remains available for status inspection without automatic replay.
|
|
603
604
|
For Claude Code's first-run text-style menu, it confirms the item already selected on screen before continuing startup.
|
|
604
605
|
If the CLI then requests an account login method, the launch reports `vendor_onboarding_required`; complete that choice in the official interactive Claude Code CLI.
|
|
605
606
|
The input-readiness wait is 30 seconds. Only when it expires while the agent has not drawn anything yet (a busy host), the launch
|
|
@@ -795,9 +796,16 @@ Each `pty_send` accepts at most 64 KiB of UTF-8 text. Sends to the same session
|
|
|
795
796
|
|
|
796
797
|
In the opt-in POSIX native execution entry, an ordinary message arriving at a verified caller can overlap that caller's outgoing send. It does not replace the caller's generation or clear either request's slot. Agent paste/submit segments wait for metadata mutex contention before input; uncertain operations, generation changes and stops still prevent input. BellTeam may explicitly recreate a closed session with a promptless management `agent_launch`; sending a prompt still requires its original execution reference.
|
|
797
798
|
|
|
799
|
+
An explicit `agent_execution` action `reconcile_launch` can bind an existing empty launch after a lost launch result in this opt-in entry. Supply the original `request` and exact `target: {session_id, launch_id}`. The server checks the original native/server identity, current managed generation, raw no-prompt state and a live ready harness under metadata admission, then releases only that unknown launch slot. It never launches, types, closes or resends. `issued` RPCs, unknown input and changed or unverified generations remain blocked. Any subsequent body uses the original reference through normal send admission once; a successful reconciliation does not prove the lost RPC receipt.
|
|
800
|
+
|
|
801
|
+
|
|
798
802
|
Sessions live on a shared tmux socket on POSIX or a shared psmux namespace on native Windows. The attach line printed by `pty_open` and `agent_launch` lets a human attach to the same terminal and intervene, including a Claude/Codex/Grok/Cursor harness session: `tmux -S … attach -t <id>` on POSIX, or `psmux -L <namespace> attach -t <id>` on native Windows.
|
|
799
803
|
On POSIX, the first `agent_launch` can create the tmux server when its socket does not yet exist. Aiterm verifies the exact created session and server before accepting that launch; an uncertain result is reported without retrying the input.
|
|
800
804
|
|
|
805
|
+
BellTeam model, effort and fast changes use a native management request bound to the existing session generation. Later stages stop if that generation, its admission slot or stop state changes. A partial change remains an uncertain effect and must not be blindly repeated.
|
|
806
|
+
|
|
807
|
+
In the opt-in POSIX native entry, `agent_auth` also supports omitted session names, authenticated no-op starts and the normal v1 status/onboarding/cancel receipts. Authentication PTYs are bound to their actual native creator and generation. Ref-less BellTeam input or keys are allowed only for those proven authentication PTYs; ordinary agent messages still require their delivery reference. Legacy authentication metadata without creator proof is not automatically enrolled.
|
|
808
|
+
|
|
801
809
|
## Requirements
|
|
802
810
|
|
|
803
811
|
- **Node.js >= 18**
|
package/dist/core.js
CHANGED
|
@@ -15,7 +15,7 @@ import { createHash, randomBytes } from "node:crypto";
|
|
|
15
15
|
import { fileURLToPath } from "node:url";
|
|
16
16
|
import * as rtk from "./rtk.js";
|
|
17
17
|
import { closeExpectedExecution, parseExecutionNativeSessions, executionNativeBindingSchema } from "./execution-close.js";
|
|
18
|
-
import { executionPointSchema } from "./execution-contract.js";
|
|
18
|
+
import { executionLaunchSchema, executionPointSchema } from "./execution-contract.js";
|
|
19
19
|
import { withExecutionInterruptEffectScope, bindExecutionNativeTarget, beforeExecutionEffect, bindExecutionCreatedLaunch, executionCreatedNativeBinding, hasExecutionCreationScope, assertExecutionContext, withExecutionPreparation, captureExecutionSendBoundary, readExecutionSendBoundary, executionManagementCloseTarget, withExecutionNativeAdmission } from "./execution-effect.js";
|
|
20
20
|
import { ExecutionError } from "./execution-wire.js";
|
|
21
21
|
import { readExecutionParentProcess } from "./execution-parent-process.js";
|
|
@@ -209,7 +209,10 @@ function paneTtyOf(name) {
|
|
|
209
209
|
* agent が tty 上に実在しない時は触らない(ready gate が入力受付不能として止める)。
|
|
210
210
|
*/
|
|
211
211
|
export async function ensureAgentOwnsPaneInput(name, kind) {
|
|
212
|
-
|
|
212
|
+
// Startup resumes after native creation/readiness awaits. A short metadata
|
|
213
|
+
// writer is not a reason to abandon that already created launch. Wait for
|
|
214
|
+
// admission, then run the unchanged source/target/generation checks.
|
|
215
|
+
await withExecutionNativeAdmission(name, () => assertExecutionContext(name));
|
|
213
216
|
if (isWin)
|
|
214
217
|
return [];
|
|
215
218
|
const tty = paneTtyOf(name);
|
|
@@ -225,9 +228,11 @@ export async function ensureAgentOwnsPaneInput(name, kind) {
|
|
|
225
228
|
return [];
|
|
226
229
|
if (state.agentPresent && !state.agentForeground) {
|
|
227
230
|
const wasStopped = state.agentStopped;
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
+
await withExecutionNativeAdmission(name, () => {
|
|
232
|
+
tmux("send-keys", "-t", name, "C-u");
|
|
233
|
+
tmux("send-keys", "-l", "-t", name, "fg");
|
|
234
|
+
tmux("send-keys", "-t", name, "Enter");
|
|
235
|
+
});
|
|
231
236
|
const deadline = performance.now() + AGENT_FOREGROUND_RECOVERY_TIMEOUT_MS;
|
|
232
237
|
for (;;) {
|
|
233
238
|
await sleep(AGENT_FOREGROUND_RECOVERY_POLL_MS);
|
|
@@ -250,11 +255,13 @@ export async function ensureAgentOwnsPaneInput(name, kind) {
|
|
|
250
255
|
// stty is a native mutation outside tmux's command gate. Recheck the
|
|
251
256
|
// captured device and the shared request generation/fence immediately
|
|
252
257
|
// before dispatch; a replaced pane must not inherit the old tty repair.
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
258
|
+
const set = await withExecutionNativeAdmission(name, () => {
|
|
259
|
+
beforeExecutionEffect(name, () => {
|
|
260
|
+
if (paneTtyOf(name) !== tty)
|
|
261
|
+
throw new ExecutionError("EXECUTION_STALE_GENERATION");
|
|
262
|
+
});
|
|
263
|
+
return spawnSync("/bin/stty", [ttyFlag, tty, "raw", "-echo"], { encoding: "utf8", timeout: 5000, env: psEnv });
|
|
256
264
|
});
|
|
257
|
-
const set = spawnSync("/bin/stty", [ttyFlag, tty, "raw", "-echo"], { encoding: "utf8", timeout: 5000, env: psEnv });
|
|
258
265
|
if (set.status !== 0) {
|
|
259
266
|
throw new AitermError(`AGENT_TTY_COOKED session=${name}\n` +
|
|
260
267
|
`tty が icanon/echo のままで raw へ戻せません: ${(set.stderr ?? "").trim() || `code=${set.status}`}。送信していません。`, 2);
|
|
@@ -1671,17 +1678,59 @@ export function observeExecutionSessionGeneration(name) {
|
|
|
1671
1678
|
const native = before.sessions.find(value => value.session_id === name);
|
|
1672
1679
|
if (!native)
|
|
1673
1680
|
return result(false, null);
|
|
1674
|
-
const meta = tryLoadAgentMetadata(name), proof = meta?.execution_native;
|
|
1681
|
+
const meta = tryLoadAgentMetadata(name) ?? readExecutionAuthMetadata(name), proof = meta?.execution_native;
|
|
1675
1682
|
if (!meta || !proof || !before.server || proof.native_id !== native.native_id
|
|
1676
1683
|
|| !isDeepStrictEqual(proof.server, before.server))
|
|
1677
1684
|
return result(true, null);
|
|
1678
|
-
const after = observeExecutionNativeCreationScope(), last = tryLoadAgentMetadata(name);
|
|
1685
|
+
const after = observeExecutionNativeCreationScope(), last = tryLoadAgentMetadata(name) ?? readExecutionAuthMetadata(name);
|
|
1679
1686
|
if (after.state !== "known" || !isDeepStrictEqual(after.server, proof.server)
|
|
1680
1687
|
|| after.sessions.find(value => value.session_id === name)?.native_id !== proof.native_id
|
|
1681
|
-
|| last?.launch_id !== meta.launch_id || !isDeepStrictEqual(last
|
|
1688
|
+
|| last?.launch_id !== meta.launch_id || !isDeepStrictEqual(last?.execution_native, proof))
|
|
1682
1689
|
return result(true, null);
|
|
1683
1690
|
return result(true, meta.launch_id);
|
|
1684
1691
|
}
|
|
1692
|
+
/** Only native-stamped authentication PTYs qualify for host auth input. */
|
|
1693
|
+
export function observeExecutionAuthSessionGeneration(name) {
|
|
1694
|
+
const current = observeExecutionSessionGeneration(name), meta = readExecutionAuthMetadata(name);
|
|
1695
|
+
return { ...current, launch_id: current.launch_id && current.launch_id === meta?.launch_id && !tryLoadAgentMetadata(name)
|
|
1696
|
+
? current.launch_id : null };
|
|
1697
|
+
}
|
|
1698
|
+
/** Allocate only a terminal name. The serving RPC then reserves it and the
|
|
1699
|
+
* actual native creation receipt supplies its identity; no existing seat is enrolled. */
|
|
1700
|
+
export function selectExecutionAuthSessionName() { return "auth-" + randomBytes(12).toString("hex"); }
|
|
1701
|
+
/** Read-only proof for reconciling a created, empty launch. Inspect raw prompt
|
|
1702
|
+
* state: legacy/default normalization must never prove that input was absent. */
|
|
1703
|
+
export function observeExecutionCreatedLaunch(target) {
|
|
1704
|
+
target = executionLaunchSchema.parse(target);
|
|
1705
|
+
const unknown = () => { throw new ExecutionError("EXECUTION_UNCONFIRMED"); };
|
|
1706
|
+
const generation = () => {
|
|
1707
|
+
const value = observeExecutionSessionGeneration(target.session_id);
|
|
1708
|
+
if (!value.exists || value.launch_id !== target.launch_id)
|
|
1709
|
+
unknown();
|
|
1710
|
+
};
|
|
1711
|
+
const read = () => {
|
|
1712
|
+
let raw;
|
|
1713
|
+
try {
|
|
1714
|
+
raw = JSON.parse(fs.readFileSync(agentMetadataPath(target.session_id, target.launch_id), "utf8"));
|
|
1715
|
+
}
|
|
1716
|
+
catch {
|
|
1717
|
+
return unknown();
|
|
1718
|
+
}
|
|
1719
|
+
const proof = executionNativeBindingSchema.safeParse(raw.execution_native);
|
|
1720
|
+
if (raw.aiterm_session !== target.session_id || raw.launch_id !== target.launch_id
|
|
1721
|
+
|| raw.initial_prompt !== "none" || raw.initial_prompt_delivery != null || !proof.success)
|
|
1722
|
+
return unknown();
|
|
1723
|
+
return proof.data;
|
|
1724
|
+
};
|
|
1725
|
+
generation();
|
|
1726
|
+
const native = read(), live = observeSession(target.session_id);
|
|
1727
|
+
if (live.launch_id !== target.launch_id || live.harness_alive !== true || live.harness_process === null || live.state !== "idle")
|
|
1728
|
+
unknown();
|
|
1729
|
+
generation();
|
|
1730
|
+
if (!isDeepStrictEqual(read(), native))
|
|
1731
|
+
unknown();
|
|
1732
|
+
return { target, native, initial_prompt: "none", initial_prompt_delivery: null, harness_alive: true, ready: true };
|
|
1733
|
+
}
|
|
1685
1734
|
/** Private source-provenance snapshot. It deliberately omits screen, argv,
|
|
1686
1735
|
* environment and cached vendor_session_id. No public tool is added. The
|
|
1687
1736
|
* consumer proves its native serving MCP process descends from this unique
|
|
@@ -4230,7 +4279,7 @@ async function prepareAgentInput(name, meta, options) {
|
|
|
4230
4279
|
handled.add(action.kind);
|
|
4231
4280
|
const keys = action.selected ? action.keys.slice(0, -1) : action.keys;
|
|
4232
4281
|
for (const key of keys) {
|
|
4233
|
-
sendKey(name, key, { preserveAgentOperation: true });
|
|
4282
|
+
await withExecutionNativeAdmission(name, () => sendKey(name, key, { preserveAgentOperation: true }));
|
|
4234
4283
|
await sleep(AGENT_SUBMIT_DELAY_MS);
|
|
4235
4284
|
}
|
|
4236
4285
|
if (action.selected) {
|
|
@@ -4238,14 +4287,14 @@ async function prepareAgentInput(name, meta, options) {
|
|
|
4238
4287
|
let moved = await waitStartupSelection(name, action.selected);
|
|
4239
4288
|
if (!moved && keys.length > 0) {
|
|
4240
4289
|
for (const key of keys) {
|
|
4241
|
-
sendKey(name, key, { preserveAgentOperation: true });
|
|
4290
|
+
await withExecutionNativeAdmission(name, () => sendKey(name, key, { preserveAgentOperation: true }));
|
|
4242
4291
|
await sleep(AGENT_SUBMIT_DELAY_MS);
|
|
4243
4292
|
}
|
|
4244
4293
|
moved = await waitStartupSelection(name, action.selected);
|
|
4245
4294
|
}
|
|
4246
4295
|
if (!moved)
|
|
4247
4296
|
break;
|
|
4248
|
-
sendKey(name, action.keys.at(-1), { preserveAgentOperation: true });
|
|
4297
|
+
await withExecutionNativeAdmission(name, () => sendKey(name, action.keys.at(-1), { preserveAgentOperation: true }));
|
|
4249
4298
|
await sleep(AGENT_SUBMIT_DELAY_MS);
|
|
4250
4299
|
}
|
|
4251
4300
|
ready = await waitAgentTuiReady(name, meta, options.ready_timeout ?? AGENT_TUI_READY_TIMEOUT_MS);
|
|
@@ -4515,6 +4564,19 @@ function loadAuthMetadata(name, kind) {
|
|
|
4515
4564
|
}
|
|
4516
4565
|
return value;
|
|
4517
4566
|
}
|
|
4567
|
+
function readExecutionAuthMetadata(name) {
|
|
4568
|
+
try {
|
|
4569
|
+
const raw = JSON.parse(fs.readFileSync(authMetadataPath(name), "utf8"));
|
|
4570
|
+
if (!["claude", "codex", "grok", "cursor"].includes(raw?.kind))
|
|
4571
|
+
return null;
|
|
4572
|
+
const meta = loadAuthMetadata(name, raw.kind), launch = executionLaunchSchema.shape.launch_id.safeParse(meta?.launch_id);
|
|
4573
|
+
const native = executionNativeBindingSchema.safeParse(meta?.execution_native);
|
|
4574
|
+
return meta && launch.success && native.success ? { ...meta, launch_id: launch.data, execution_native: native.data } : null;
|
|
4575
|
+
}
|
|
4576
|
+
catch {
|
|
4577
|
+
return null;
|
|
4578
|
+
}
|
|
4579
|
+
}
|
|
4518
4580
|
/** 一覧の取得失敗を端末の消失へ丸めず、正規PTY一覧で存在を確認する。 */
|
|
4519
4581
|
function authSessionExists(name) {
|
|
4520
4582
|
return listSessionsResult().sessions.some(session => session.session_id === name);
|
|
@@ -4557,7 +4619,7 @@ export async function authenticateAgent(kind, options) {
|
|
|
4557
4619
|
const meta = loadAuthMetadata(options.session_id, kind);
|
|
4558
4620
|
if (!authSessionExists(options.session_id)) {
|
|
4559
4621
|
if (meta)
|
|
4560
|
-
fs.unlinkSync(authMetadataPath(options.session_id));
|
|
4622
|
+
withExecutionPreparation(options.session_id, () => fs.unlinkSync(authMetadataPath(options.session_id)));
|
|
4561
4623
|
return result("blocked", null, "認証sessionは既に終了または取消済みです。");
|
|
4562
4624
|
}
|
|
4563
4625
|
if (!meta)
|
|
@@ -4588,7 +4650,13 @@ export async function authenticateAgent(kind, options) {
|
|
|
4588
4650
|
const kept = tmux("set-option", "-t", name, "remain-on-exit", "on");
|
|
4589
4651
|
if (kept.code !== 0)
|
|
4590
4652
|
throw new AitermError("AGENT_AUTH_PANE_CONFIG_FAILED: 認証processの終了観測を設定できません。", 2);
|
|
4591
|
-
|
|
4653
|
+
if (hasExecutionCreationScope()) {
|
|
4654
|
+
meta.launch_id = randomBytes(16).toString("hex");
|
|
4655
|
+
meta.execution_native = executionCreatedNativeBinding(name);
|
|
4656
|
+
}
|
|
4657
|
+
withExecutionPreparation(name, () => writeJson0600(authMetadataPath(name), meta));
|
|
4658
|
+
if (meta.launch_id && hasExecutionCreationScope())
|
|
4659
|
+
bindExecutionCreatedLaunch({ session_id: name, launch_id: meta.launch_id });
|
|
4592
4660
|
launchAuthProcess(name, meta);
|
|
4593
4661
|
}
|
|
4594
4662
|
catch (error) {
|
|
@@ -4636,8 +4704,10 @@ export async function authenticateAgent(kind, options) {
|
|
|
4636
4704
|
return result("authenticated", session, null);
|
|
4637
4705
|
if (meta.phase === "onboarding")
|
|
4638
4706
|
return result("blocked", session, "公式CLIの初回案内の完了を確認できません。");
|
|
4639
|
-
|
|
4640
|
-
|
|
4707
|
+
withExecutionPreparation(session, () => {
|
|
4708
|
+
meta.phase = "onboarding";
|
|
4709
|
+
writeJson0600(authMetadataPath(session), meta);
|
|
4710
|
+
});
|
|
4641
4711
|
const restarted = tmux("respawn-pane", "-k", "-t", session, resolveWinPaneShell("bash"));
|
|
4642
4712
|
if (restarted.code !== 0)
|
|
4643
4713
|
return result("failed", session, "公式CLIの初回案内を起動できません。");
|
|
@@ -5032,8 +5102,10 @@ const restoreCursorDefaults = (configPath) => (before) => {
|
|
|
5032
5102
|
restoreJsonSetting(configPath, before, CURSOR_DEFAULT_MODEL_KEYS);
|
|
5033
5103
|
};
|
|
5034
5104
|
function recordAgentFast(meta, fast) {
|
|
5035
|
-
meta.
|
|
5036
|
-
|
|
5105
|
+
withExecutionPreparation(meta.aiterm_session, () => {
|
|
5106
|
+
meta.fast = fast;
|
|
5107
|
+
writeAgentMetadata(meta);
|
|
5108
|
+
});
|
|
5037
5109
|
}
|
|
5038
5110
|
/** 動いている席のfastを切り替える。返りは切り替えた後の状態。席は入力待ちである事(呼び手が確かめる)。 */
|
|
5039
5111
|
async function switchAgentFast(name, meta, target, verified = false) {
|
|
@@ -33,14 +33,14 @@ function routeFor(name, args) {
|
|
|
33
33
|
if (!nativeTool.success)
|
|
34
34
|
throw new ExecutionError("EXECUTION_UNSUPPORTED");
|
|
35
35
|
const target = launches.has(name) ? args.session_name : name === "pty_open" ? args.name : args.session_id;
|
|
36
|
-
//
|
|
37
|
-
//
|
|
36
|
+
// Generic automatic names remain unsupported. Auth start selects its name
|
|
37
|
+
// through the private allocator before this route reserves it.
|
|
38
38
|
if (target === undefined || target === null)
|
|
39
39
|
throw new ExecutionError("EXECUTION_UNSUPPORTED");
|
|
40
40
|
const session = executionLaunchSchema.shape.session_id.safeParse(target);
|
|
41
41
|
if (!session.success)
|
|
42
42
|
throw new ExecutionError("EXECUTION_INVALID");
|
|
43
|
-
return { tool: nativeTool.data, target: session.data, stage: launches.has(name) ? "launch" : "send" };
|
|
43
|
+
return { tool: nativeTool.data, target: session.data, stage: launches.has(name) || tool === "agent_auth.start" ? "launch" : "send" };
|
|
44
44
|
}
|
|
45
45
|
// The SDK signal belongs to this serving RPC, not to a whole CLI turn. Keep
|
|
46
46
|
// an immediate local gate while the shared metadata lock may be awaiting I/O.
|
|
@@ -79,6 +79,19 @@ function requestCancellation(service, request, input) {
|
|
|
79
79
|
};
|
|
80
80
|
}
|
|
81
81
|
export function createExecutionDispatcher(bindings) {
|
|
82
|
+
const selectAuthName = bindings.selectAuthSessionName, observeAuth = bindings.observeAuthSessionGeneration;
|
|
83
|
+
for (const fn of [selectAuthName, observeAuth])
|
|
84
|
+
if (fn !== undefined && typeof fn !== "function")
|
|
85
|
+
throw new ExecutionError("EXECUTION_UNSUPPORTED");
|
|
86
|
+
const authGeneration = (session) => {
|
|
87
|
+
if (!observeAuth)
|
|
88
|
+
return null;
|
|
89
|
+
const value = observeAuth(session);
|
|
90
|
+
if (!value || value.schema !== "aiterm.pty-observe-result.v1" || value.session_id !== session || value.exists !== true)
|
|
91
|
+
return null;
|
|
92
|
+
const launch = executionLaunchSchema.safeParse({ session_id: session, launch_id: value.launch_id });
|
|
93
|
+
return launch.success ? { launch: launch.data, boundary: null } : null;
|
|
94
|
+
};
|
|
82
95
|
const captureNativeParent = bindings.captureNativeParent;
|
|
83
96
|
if (captureNativeParent !== undefined && typeof captureNativeParent !== "function")
|
|
84
97
|
throw new ExecutionError("EXECUTION_UNSUPPORTED");
|
|
@@ -129,6 +142,13 @@ export function createExecutionDispatcher(bindings) {
|
|
|
129
142
|
executionHostSourceFromMeta(fixedExtra._meta, bindings.initializedClientName());
|
|
130
143
|
return await body(args, fixedExtra);
|
|
131
144
|
}
|
|
145
|
+
if (name === "agent_auth" && (args.action !== "status" || args.session_id !== undefined) && !observeAuth)
|
|
146
|
+
throw new ExecutionError("EXECUTION_UNSUPPORTED");
|
|
147
|
+
if (name === "agent_auth" && args.action === "start" && args.session_id === undefined) {
|
|
148
|
+
if (!native || !selectAuthName)
|
|
149
|
+
throw new ExecutionError("EXECUTION_UNSUPPORTED");
|
|
150
|
+
args.session_id = executionLaunchSchema.shape.session_id.parse(selectAuthName());
|
|
151
|
+
}
|
|
132
152
|
const route = routeFor(name, args);
|
|
133
153
|
if (!route)
|
|
134
154
|
return await body(args, fixedExtra);
|
|
@@ -145,7 +165,12 @@ export function createExecutionDispatcher(bindings) {
|
|
|
145
165
|
const hostClose = client === "bellteam" && route.tool === "pty_close" && !supplied;
|
|
146
166
|
const hostLaunch = client === "bellteam" && route.tool === "agent_launch" && !supplied
|
|
147
167
|
&& args.prompt == null && args.throughline_source_session === undefined;
|
|
148
|
-
const
|
|
168
|
+
const hostConfigure = client === "bellteam" && route.tool === "agent_configure" && !supplied;
|
|
169
|
+
const authOperation = name === "agent_auth";
|
|
170
|
+
const authInput = (route.tool === "pty_send" || route.tool === "pty_key") && !supplied
|
|
171
|
+
&& client === "bellteam" ? authGeneration(route.target) : null;
|
|
172
|
+
const hostAuth = client === "bellteam" && !supplied && authOperation;
|
|
173
|
+
const hostManagement = hostClose || hostLaunch || hostConfigure || hostAuth || !!authInput;
|
|
149
174
|
if (args.remote !== undefined && args.remote !== null)
|
|
150
175
|
throw new ExecutionError("EXECUTION_UNSUPPORTED");
|
|
151
176
|
let reference = supplied;
|
|
@@ -195,8 +220,16 @@ export function createExecutionDispatcher(bindings) {
|
|
|
195
220
|
if ((client === "bellteam" && !hostManagement) || !directRequest)
|
|
196
221
|
throw new ExecutionError("EXECUTION_UNSUPPORTED");
|
|
197
222
|
const current = await observe(route.target);
|
|
223
|
+
if (authOperation && route.tool !== "agent_auth.start") {
|
|
224
|
+
// A missing auth PTY returns the public v1 failed/already-cancelled
|
|
225
|
+
// receipt. There is no native effect or replacement creation.
|
|
226
|
+
if (!current.point)
|
|
227
|
+
route.stage = "launch";
|
|
228
|
+
}
|
|
198
229
|
if (route.stage === "send" && !current.point)
|
|
199
230
|
throw new ExecutionError("EXECUTION_STALE_GENERATION");
|
|
231
|
+
if (authInput && !same(authInput, current.point))
|
|
232
|
+
throw new ExecutionError("EXECUTION_STALE_GENERATION");
|
|
200
233
|
const request = directRequest;
|
|
201
234
|
cancellation?.assert();
|
|
202
235
|
await bindings.service.execute({ action: "register", source_session_id: source?.launch.session_id ?? directSeat,
|
|
@@ -230,7 +263,7 @@ export function createExecutionDispatcher(bindings) {
|
|
|
230
263
|
// must not turn a valid accepted action into permission to resend.
|
|
231
264
|
let target = null;
|
|
232
265
|
try {
|
|
233
|
-
if (route.tool === "pty_close")
|
|
266
|
+
if (route.tool === "pty_close" || route.tool === "agent_auth.cancel")
|
|
234
267
|
target = expected ? { launch: expected, boundary: null } : null;
|
|
235
268
|
else {
|
|
236
269
|
const current = await observe(route.target);
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// Private read-only evidence from the serving native scope, never client claims.
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { executionLaunchSchema } from "./execution-contract.js";
|
|
4
|
+
import { executionNativeBindingSchema } from "./execution-close.js";
|
|
5
|
+
export const createdLaunchObservationSchema = z.object({
|
|
6
|
+
target: executionLaunchSchema, native: executionNativeBindingSchema,
|
|
7
|
+
initial_prompt: z.literal("none"), initial_prompt_delivery: z.null(),
|
|
8
|
+
harness_alive: z.literal(true), ready: z.literal(true),
|
|
9
|
+
}).strict();
|
|
@@ -35,6 +35,9 @@ export function createNativeExecutionServer(input) {
|
|
|
35
35
|
?? provider.cursorParentFromRequest(profile, client, { hookRoot: path.join(expectedScope.state_root, "cursor-parent-hooks") }),
|
|
36
36
|
observeDone: core.observeAgentDone,
|
|
37
37
|
observeNativeSessions: core.observeExecutionNativeCreationScope,
|
|
38
|
+
selectAuthSessionName: core.selectExecutionAuthSessionName,
|
|
39
|
+
observeAuthSessionGeneration: core.observeExecutionAuthSessionGeneration,
|
|
40
|
+
observeCreatedLaunchSync: core.observeExecutionCreatedLaunch,
|
|
38
41
|
closeExpectedSession: core.closeExpectedAgentSession, // explicit management only, never stop fallback
|
|
39
42
|
observeInterruptionSync: core.observeExpectedAgentRunInterruption,
|
|
40
43
|
selectInterruptionSync: core.selectExpectedAgentRunInterruption,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Private same-scope MCP parent producer. No public/index/profile mount. Queue
|
|
2
2
|
// aliases lacking current independent native proof remain unsupported.
|
|
3
3
|
import path from "node:path";
|
|
4
|
-
import { observeAgentDone, observeExecutionNativeCreationScope, closeExpectedAgentSession } from "./core.js";
|
|
4
|
+
import { observeAgentDone, selectExecutionAuthSessionName, observeExecutionAuthSessionGeneration, observeExecutionNativeCreationScope, closeExpectedAgentSession, observeExecutionCreatedLaunch } from "./core.js";
|
|
5
5
|
import { codexParentFromRequest } from "./codex-parent-receiver.js";
|
|
6
6
|
import { claudeParentFromRequest } from "./claude-parent-receiver.js";
|
|
7
7
|
import { cursorParentFromRequest } from "./cursor-parent-receiver.js";
|
|
@@ -30,13 +30,18 @@ const localProducerRuntime = {
|
|
|
30
30
|
?? cursorParentFromRequest(client, { hookRoot: path.join(stateRoot(), "cursor-parent-hooks") }),
|
|
31
31
|
observeDone: observeAgentDone,
|
|
32
32
|
observeNativeSessions: observeExecutionNativeCreationScope,
|
|
33
|
+
selectAuthSessionName: selectExecutionAuthSessionName,
|
|
34
|
+
observeAuthSessionGeneration: observeExecutionAuthSessionGeneration,
|
|
35
|
+
observeCreatedLaunchSync: observeExecutionCreatedLaunch,
|
|
33
36
|
closeExpectedSession: closeExpectedAgentSession,
|
|
34
37
|
};
|
|
35
38
|
export function createExecutionParentProducer(input) {
|
|
36
39
|
const runtime = input.runtime ?? localProducerRuntime;
|
|
37
|
-
for (const name of ["observeInterruptionSync", "selectInterruptionSync", "interruptKeySync", "observeOriginOwner"])
|
|
40
|
+
for (const name of ["selectAuthSessionName", "observeAuthSessionGeneration", "observeInterruptionSync", "selectInterruptionSync", "interruptKeySync", "observeOriginOwner", "observeCreatedLaunchSync"])
|
|
38
41
|
if (runtime[name] !== undefined && typeof runtime[name] !== "function")
|
|
39
42
|
throw new ExecutionError("EXECUTION_UNSUPPORTED");
|
|
43
|
+
const selectAuthName = runtime.selectAuthSessionName?.bind(runtime), observeAuth = runtime.observeAuthSessionGeneration?.bind(runtime);
|
|
44
|
+
const createdLaunch = runtime.observeCreatedLaunchSync?.bind(runtime);
|
|
40
45
|
const interruption = runtime.observeInterruptionSync?.bind(runtime);
|
|
41
46
|
const selectInterruption = runtime.selectInterruptionSync?.bind(runtime), interruptKey = runtime.interruptKeySync?.bind(runtime);
|
|
42
47
|
const observeActors = runtime.observeActors?.bind(runtime);
|
|
@@ -68,6 +73,10 @@ export function createExecutionParentProducer(input) {
|
|
|
68
73
|
} } : {}),
|
|
69
74
|
...(interruptKey ? { interruptKeySync: (point) => { assertScope(); return interruptKey(point); } } : {}),
|
|
70
75
|
observeSession: observe, observeSessionSync: observe,
|
|
76
|
+
...(createdLaunch ? { observeCreatedLaunchSync: (target) => {
|
|
77
|
+
assertScope();
|
|
78
|
+
return createdLaunch(target);
|
|
79
|
+
} } : {}),
|
|
71
80
|
...(typeof runtime.observeNativeSessions === "function" ? { observeNativeSessionsSync: () => {
|
|
72
81
|
assertScope();
|
|
73
82
|
return runtime.observeNativeSessions();
|
|
@@ -115,7 +124,10 @@ export function createExecutionParentProducer(input) {
|
|
|
115
124
|
requireExecutionEffectScope(true);
|
|
116
125
|
return { execution, parent, native, service, store, scope: structuredClone(scope), worker,
|
|
117
126
|
inventory: () => store.inventory(typeof runtime.observeActors === "function" ? pids => { assertScope(); return runtime.observeActors(pids); } : undefined),
|
|
118
|
-
startup: Object.freeze({
|
|
127
|
+
startup: Object.freeze({
|
|
128
|
+
...(selectAuthName ? { selectAuthSessionName: () => { assertScope(); return selectAuthName(); } } : {}),
|
|
129
|
+
...(observeAuth ? { observeAuthSessionGeneration: (session) => { assertScope(); return observeAuth(session); } } : {}),
|
|
130
|
+
actor: Object.freeze({ ...actor }), sourceEnvironment, assertScope, observeSessionGeneration: observe,
|
|
119
131
|
captureNativeParent: () => {
|
|
120
132
|
assertScope();
|
|
121
133
|
const fixed = sourceEvidence.captureIngress();
|
|
@@ -124,6 +136,7 @@ export function createExecutionParentProducer(input) {
|
|
|
124
136
|
if (fixed?.proof.endpoint.vendor === "claude")
|
|
125
137
|
producerEndpoint.verifyIngress(fixed.endpoint, fixed.proof);
|
|
126
138
|
return fixed;
|
|
127
|
-
}
|
|
139
|
+
}
|
|
140
|
+
}),
|
|
128
141
|
relation_coverage: "incomplete" };
|
|
129
142
|
}
|
|
@@ -12,7 +12,7 @@ function snapshotRuntime(input) {
|
|
|
12
12
|
for (const name of functions)
|
|
13
13
|
if (typeof input[name] !== "function")
|
|
14
14
|
throw new ExecutionError("EXECUTION_UNSUPPORTED");
|
|
15
|
-
const optional = ["observeNativeSessions", "closeExpectedSession", "observeActors", "observeInterruptionSync", "selectInterruptionSync", "interruptKeySync", "observeOriginOwner"];
|
|
15
|
+
const optional = ["selectAuthSessionName", "observeAuthSessionGeneration", "observeNativeSessions", "closeExpectedSession", "observeActors", "observeInterruptionSync", "selectInterruptionSync", "interruptKeySync", "observeOriginOwner", "observeCreatedLaunchSync"];
|
|
16
16
|
for (const name of optional)
|
|
17
17
|
if (input[name] !== undefined && typeof input[name] !== "function")
|
|
18
18
|
throw new ExecutionError("EXECUTION_UNSUPPORTED");
|
|
@@ -70,7 +70,9 @@ export function createExecutionParticipant(input) {
|
|
|
70
70
|
const server = createAitermServer({ execution: server => {
|
|
71
71
|
assertScope();
|
|
72
72
|
dispatcher = createExecutionDispatcher({ service: producer.service, nativeIdentity: actor, sourceEnvironment,
|
|
73
|
-
initializedClientName: () => server.server.getClientVersion()?.name, observeSessionGeneration, assertParticipant: assertScope, captureNativeParent
|
|
73
|
+
initializedClientName: () => server.server.getClientVersion()?.name, observeSessionGeneration, assertParticipant: assertScope, captureNativeParent,
|
|
74
|
+
...(producer.startup.selectAuthSessionName ? { selectAuthSessionName: producer.startup.selectAuthSessionName } : {}),
|
|
75
|
+
...(producer.startup.observeAuthSessionGeneration ? { observeAuthSessionGeneration: producer.startup.observeAuthSessionGeneration } : {}) });
|
|
74
76
|
return { dispatcher, parentExecution: producer.execution, parentDependencies, parentStateRoot: producer.scope.state_root };
|
|
75
77
|
} });
|
|
76
78
|
producer.store.enroll(["mcp"]);
|
|
@@ -45,7 +45,7 @@ const waitSchema = z.object({
|
|
|
45
45
|
});
|
|
46
46
|
const same = (a, b) => JSON.stringify(a) === JSON.stringify(b);
|
|
47
47
|
export function createExecutionRuntime(bindings) {
|
|
48
|
-
for (const name of ["observeInterruptionSync", "selectInterruptionSync", "interruptKeySync"])
|
|
48
|
+
for (const name of ["observeCreatedLaunchSync", "observeInterruptionSync", "selectInterruptionSync", "interruptKeySync"])
|
|
49
49
|
if (bindings[name] !== undefined && typeof bindings[name] !== "function")
|
|
50
50
|
throw new ExecutionError("EXECUTION_UNSUPPORTED");
|
|
51
51
|
const interruption = bindings.observeInterruptionSync;
|
|
@@ -72,7 +72,9 @@ export function createExecutionRuntime(bindings) {
|
|
|
72
72
|
return { state: "unknown" };
|
|
73
73
|
}
|
|
74
74
|
};
|
|
75
|
+
const created = bindings.observeCreatedLaunchSync;
|
|
75
76
|
return {
|
|
77
|
+
...(created ? { observeCreatedLaunchSync: (target) => created(executionLaunchSchema.parse(target)) } : {}),
|
|
76
78
|
...(interruption ? { interruptionSync: (point) => interruption(executionPointSchema.parse(point)) } : {}),
|
|
77
79
|
...(selectInterruption ? { selectInterruptionSync: (expected, candidates) => selectInterruption(executionLaunchSchema.parse(expected), z.array(executionPointSchema).parse(candidates ?? [])) } : {}),
|
|
78
80
|
...(interruptKey ? { interruptKeySync: (point) => interruptKey(executionPointSchema.parse(point)) } : {}),
|
|
@@ -8,6 +8,7 @@ import { nativeInterruptionObservationSchema, interruptionIdentity, interruption
|
|
|
8
8
|
import { interruptionKeyReference } from "./execution-interruption.js";
|
|
9
9
|
import { isDeepStrictEqual } from "node:util";
|
|
10
10
|
import { executionActorSchema } from "./execution-inventory.js";
|
|
11
|
+
import { createdLaunchObservationSchema } from "./execution-launch-recovery.js";
|
|
11
12
|
import { executionCheckpoint } from "./execution-checkpoint.js";
|
|
12
13
|
const same = (a, b) => JSON.stringify(a) === JSON.stringify(b);
|
|
13
14
|
const key = (value) => JSON.stringify(value);
|
|
@@ -544,6 +545,8 @@ export class ExecutionService {
|
|
|
544
545
|
return this.register(args, context);
|
|
545
546
|
if (args.action === "status")
|
|
546
547
|
return this.store.transaction(state => view(state, find(state, args.request)), { readOnly: true });
|
|
548
|
+
if (args.action === "reconcile_launch")
|
|
549
|
+
return this.reconcileLaunch(args);
|
|
547
550
|
return this.finish(args);
|
|
548
551
|
}
|
|
549
552
|
async admitNative(body) {
|
|
@@ -878,6 +881,9 @@ export class ExecutionService {
|
|
|
878
881
|
try {
|
|
879
882
|
await this.store.transaction(state => {
|
|
880
883
|
const record = find(state, request);
|
|
884
|
+
const owned = state.slots.find(slot => same(slot.request, request));
|
|
885
|
+
if (!owned || owned.stage !== stage || record.operations[stage] !== "issued")
|
|
886
|
+
return;
|
|
881
887
|
record.operations[stage] = certain ? "not_sent" : "unknown";
|
|
882
888
|
if (certain) {
|
|
883
889
|
state.slots = state.slots.filter(slot => !same(slot.request, request));
|
|
@@ -909,7 +915,7 @@ export class ExecutionService {
|
|
|
909
915
|
|| (expected !== null && targetPoint.launch.launch_id !== expected))
|
|
910
916
|
throw new ExecutionError("EXECUTION_UNCONFIRMED", "possible");
|
|
911
917
|
const current = await this.runtime.observe(slot.session_id);
|
|
912
|
-
const closing = request.kind === "mcp" && request.tool
|
|
918
|
+
const closing = request.kind === "mcp" && ["pty_close", "agent_auth.cancel"].includes(request.tool);
|
|
913
919
|
if (closing ? current.state !== "absent" : current.state !== "present" || !same(current.launch, targetPoint.launch))
|
|
914
920
|
throw new ExecutionError("EXECUTION_UNCONFIRMED", "possible");
|
|
915
921
|
record.operations[stage] = "accepted";
|
|
@@ -940,6 +946,9 @@ export class ExecutionService {
|
|
|
940
946
|
try {
|
|
941
947
|
await this.store.transaction(state => {
|
|
942
948
|
const record = find(state, request);
|
|
949
|
+
const owned = state.slots.find(slot => same(slot.request, request));
|
|
950
|
+
if (!owned || owned.stage !== stage || record.operations[stage] !== "issued")
|
|
951
|
+
return;
|
|
943
952
|
record.operations[stage] = "unknown";
|
|
944
953
|
record.relation.phase = "submitted_unknown";
|
|
945
954
|
record.relation.completion = "unknown";
|
|
@@ -953,6 +962,45 @@ export class ExecutionService {
|
|
|
953
962
|
});
|
|
954
963
|
return { value: outcome.value, record };
|
|
955
964
|
}
|
|
965
|
+
/** Explicit present-generation reconciliation. It never replays the original
|
|
966
|
+
* launch or input, and does not claim to recover the lost RPC receipt. */
|
|
967
|
+
async reconcileLaunch(args) {
|
|
968
|
+
return this.store.transaction(state => {
|
|
969
|
+
const record = find(state, args.request), relation = record.relation;
|
|
970
|
+
if (record.operations.launch === "accepted" && same(relation.target?.launch, args.target))
|
|
971
|
+
return view(state, record);
|
|
972
|
+
if (record.operations.launch === "issued")
|
|
973
|
+
throw new ExecutionError("EXECUTION_BUSY");
|
|
974
|
+
if (record.stopping || stopped(state, args.target) || stopped(state, relation.source?.launch)
|
|
975
|
+
|| interruptionBlocksRequest(state, args.request))
|
|
976
|
+
throw new ExecutionError("EXECUTION_STOPPING");
|
|
977
|
+
const slots = state.slots.filter(slot => slot.session_id === args.target.session_id);
|
|
978
|
+
const slot = slots[0];
|
|
979
|
+
if (record.operations.launch !== "unknown" || record.operations.send !== "not_issued"
|
|
980
|
+
|| relation.phase !== "submitted_unknown" || relation.completion !== "unknown" || relation.lifecycle !== "tracking"
|
|
981
|
+
|| record.reservation.target.session_id !== args.target.session_id
|
|
982
|
+
|| relation.target?.boundary != null || (relation.target && !same(relation.target.launch, args.target))
|
|
983
|
+
|| (record.reservation.target.expected_launch_id !== null && record.reservation.target.expected_launch_id !== args.target.launch_id)
|
|
984
|
+
|| slots.length !== 1 || !slot || !same(slot.request, args.request) || slot.stage !== "launch" || slot.state !== "unknown"
|
|
985
|
+
|| !slot.created_native_id || !slot.created_server
|
|
986
|
+
|| (slot.expected_launch_id !== null && slot.expected_launch_id !== args.target.launch_id)
|
|
987
|
+
|| openParentEffect(state, args.request))
|
|
988
|
+
throw new ExecutionError("EXECUTION_UNCONFIRMED");
|
|
989
|
+
if (!this.runtime.observeCreatedLaunchSync)
|
|
990
|
+
throw new ExecutionError("EXECUTION_UNSUPPORTED");
|
|
991
|
+
const parsed = createdLaunchObservationSchema.safeParse(this.runtime.observeCreatedLaunchSync(args.target));
|
|
992
|
+
if (!parsed.success || !same(parsed.data.target, args.target)
|
|
993
|
+
|| parsed.data.native.native_id !== slot.created_native_id
|
|
994
|
+
|| !isDeepStrictEqual(parsed.data.native.server, slot.created_server))
|
|
995
|
+
throw new ExecutionError("EXECUTION_UNCONFIRMED");
|
|
996
|
+
record.operations.launch = "accepted";
|
|
997
|
+
relation.target = { launch: args.target, boundary: null };
|
|
998
|
+
relation.phase = "invoked";
|
|
999
|
+
relation.completion = "open";
|
|
1000
|
+
state.slots = state.slots.filter(value => value !== slot);
|
|
1001
|
+
return view(state, record);
|
|
1002
|
+
});
|
|
1003
|
+
}
|
|
956
1004
|
async finish(args) {
|
|
957
1005
|
return this.store.transaction(async (state) => {
|
|
958
1006
|
const record = find(state, args.request);
|
package/dist/execution-tools.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { executionReservationSchema, executionRequestSchema, executionPointSchema, cascadeInterruptRequestSchema, cascadeInterruptResultObjectSchema } from "./execution-contract.js";
|
|
2
|
+
import { executionLaunchSchema, executionReservationSchema, executionRequestSchema, executionPointSchema, cascadeInterruptRequestSchema, cascadeInterruptResultObjectSchema } from "./execution-contract.js";
|
|
3
3
|
import { executionActionSchema, executionFinishSchema, executionRecordSchema, executionBindingSchema, executionRootsRequestSchema, executionRootsResultObjectSchema, executionRefFromMeta, executionErrorResult, ExecutionError, EXECUTION_BINDING_META_KEY, EXECUTION_ERROR_META_KEY, EXECUTION_SOURCE_META_KEY } from "./execution-wire.js";
|
|
4
4
|
import { withExecutionEffectScope, bindExecutionManagementCloseTarget } from "./execution-effect.js";
|
|
5
5
|
import { immediateStopInputSchema, immediateStopResultSchema } from "./execution-immediate-stop.js";
|
|
@@ -67,12 +67,12 @@ export function registerExecutionTools(server, service, options = {}) {
|
|
|
67
67
|
const handlers = createExecutionToolHandlers(service, { ...options,
|
|
68
68
|
...(registration ? { sourceContext: registration.sourceContext } : {}) });
|
|
69
69
|
server.registerTool("agent_execution", {
|
|
70
|
-
description: "
|
|
70
|
+
description: "既存の実配送/実行参照を登録・照会・終了証拠で更新し、結果不明の空起動を同世代へ照合する。本文や再送queueは保存しない。",
|
|
71
71
|
// MCP accepts a raw object shape; the handler enforces the stricter action
|
|
72
72
|
// union, including required fields and forbidden fields for each action.
|
|
73
|
-
inputSchema: { action: z.enum(["register", "status", "finish"]),
|
|
73
|
+
inputSchema: { action: z.enum(["register", "status", "finish", "reconcile_launch"]),
|
|
74
74
|
reservation: executionReservationSchema.optional(), source_session_id: z.string().nullable().optional(),
|
|
75
|
-
request: executionRequestSchema.optional(), evidence: executionFinishSchema.shape.evidence.optional(),
|
|
75
|
+
request: executionRequestSchema.optional(), target: executionLaunchSchema.optional(), evidence: executionFinishSchema.shape.evidence.optional(),
|
|
76
76
|
remote: z.never().optional() },
|
|
77
77
|
outputSchema: executionRecordSchema.shape,
|
|
78
78
|
}, async (args, extra) => handlers.agent_execution(args, extra));
|
package/dist/execution-wire.js
CHANGED
|
@@ -43,7 +43,10 @@ export const executionFinishSchema = z.object({
|
|
|
43
43
|
z.object({ kind: z.literal("interrupted"), target: executionLaunchSchema }).strict(),
|
|
44
44
|
]),
|
|
45
45
|
}).strict();
|
|
46
|
-
export const
|
|
46
|
+
export const executionReconcileLaunchSchema = z.object({
|
|
47
|
+
action: z.literal("reconcile_launch"), request: executionRequestSchema, target: executionLaunchSchema,
|
|
48
|
+
}).strict();
|
|
49
|
+
export const executionActionSchema = z.union([executionRegisterSchema, executionStatusSchema, executionFinishSchema, executionReconcileLaunchSchema]);
|
|
47
50
|
export const executionRefMetaSchema = z.object({
|
|
48
51
|
schema: z.literal("aiterm.execution-ref.v1"), request: executionRequestSchema,
|
|
49
52
|
}).strict();
|
package/docs/DESIGN.md
CHANGED
|
@@ -29,19 +29,24 @@ POSIXのraw tty回復もnative作用であり、tmux打鍵と同じ受付を通
|
|
|
29
29
|
実stty直前にも同じdeviceを確認する。停止や世代/tty交代後に古いdeviceへ回復を発行しない。
|
|
30
30
|
一つのRPCが複数のnative作用を発行するときも、各回に同じissued slotとsource/target世代・停止を再確認する。
|
|
31
31
|
agentの貼付け・Enterの同期区間は、既存metadata mutexを非同期に取得してから実行する。
|
|
32
|
+
native作成後に再開する起動準備も同じ非同期取得を通す。pane所有検査、fg/stty回復、
|
|
33
|
+
起動メニューの各キーを短い同期区間で実行し、待機後の停止・世代・slot検査を省略しない。
|
|
32
34
|
mutexをawait中へ持ち越さず、同じ同期stack内の各gateが発行記録を永続化してからnative作用へ進む。
|
|
33
35
|
検証済みsourceと同世代への通常sendがissuedである間も、そのsourceの送信先が別ならoutgoing RPCは継続できる。
|
|
34
36
|
incomingのslotを解放・書換・成功扱いせず、unknown・管理操作・世代不一致は従来どおり拒否する。
|
|
35
37
|
incomingの完了待ちをoutgoingへ課して相互送信を循環待ちにしない(ADR 0127)。
|
|
36
38
|
BellTeamのpromptなし明示管理launchは、管理close同様にserving process/request identityへ結合する。
|
|
37
39
|
promptやThroughline読出しを含む未登録送信、auto-name、remoteはこの管理例外へ含めない。
|
|
40
|
+
BellTeamのrefなしagent_configureも同じnative process/request identityの管理RPCとして受ける。
|
|
41
|
+
model/effort/fastの各stageは同じ既存世代・slot・source・停止のgateを通り、途中で変われば続きの打鍵を拒否する。
|
|
42
|
+
発行後の途中失敗はpossible/unknownを保持する。CLI結果待ち後のfast metadata書込も同じ準備受付で再検査する(ADR 0129)。
|
|
38
43
|
新launchは、既存POSIX serverのnative owner、またはserver未作成の完全な空観測を受付時と発行直前に確認する。
|
|
39
44
|
冷間時は最初のnew-session自身でserverを作り、その実receiptのnative IDとserver PIDを、作成後の安定したnative一覧とprocess開始識別子で照合する。
|
|
40
45
|
既存server時も冷間時も、作成したsessionのnative ID・server owner・core内部のlaunch世代を同じ要求slotへ段階結合する。
|
|
41
46
|
物理shell準備はnative IDへ固定し、未結合の同名世代を採用しない。冷間起動のための補助sessionや再送は作らない。
|
|
42
47
|
作成時のnative ID/server ownerをmanaged metadataへ保存し、private runtimeの世代読取と期待closeでも再照合する。
|
|
43
48
|
古いmetadataの欠測を現観測で補わず、既存席の自動登録は行わない。
|
|
44
|
-
証拠の無い新server起動、Windows固有のserver
|
|
49
|
+
証拠の無い新server起動、Windows固有のserver作成、一般のauto-name/通常PTY等はまだunsupported。認証専用PTYはADR 0130の限定経路で受け付ける。
|
|
45
50
|
実18 toolsのhandlerを作るconstructorは、private dispatcher/親execution callbackを明示注入して隔離SDKで検証できる。
|
|
46
51
|
通常のstdio mainは注入せず、全参加process/全core作用口・native fenceが揃うまで公開有効化しない。
|
|
47
52
|
private participant factoryはproducer/service/dispatcher/同process用worker profileを同じscopeとnative actorで組む。
|
|
@@ -403,6 +408,14 @@ shell、接続先、各harnessの公式CLIが所有する。
|
|
|
403
408
|
## Model catalog
|
|
404
409
|
|
|
405
410
|
`agent_auth`の共通進行はcore、公式command・status・認証画面の解釈は`src/harnesses/`が所有する。CLIの認証processを通常HOMEのPTYへ一度だけ起動し、`remain-on-exit`と`pane_dead_status`で終了を観測する。credentialを読み取る状態管理や独自OAuthは持たず、Aitermが保存するのは認証sessionのharness・実行file・cwd・phase・引き継いだenvの名前だけである。取消と通常のPTY closeでこの相関記録を削除する。
|
|
411
|
+
native明示入口のauth startは、既存SDK RPCのprocess/start/request identityへ結合し、session省略時は一意なPTY名だけを選んで先に予約する。
|
|
412
|
+
認証済みno-opはPTY/serverを作らず元のv1/session_id:nullを返す。作成時は実new-session receiptからauth metadataへ
|
|
413
|
+
native/server stampと新しいauth世代を保存・結合する。通常agent metadataや本文delivery refを捏造しない。
|
|
414
|
+
既存authのstatus/onboarding・取消は同じ世代を各作用前に再検査する。消失済みはnative作用なしで既存v1 receiptを返す。
|
|
415
|
+
BellTeamのrefなしpty_send/pty_keyは、この作成proofを保持するauth PTYにだけ許可し、通常Botの未登録本文には広げない。
|
|
416
|
+
初回案内のrespawnも同じsession世代を保ち、phase保存は準備受付を通す。旧auth metadataの欠測を現nativeから補わず、
|
|
417
|
+
不明な入力・世代・未返却RPCのslotをstatusだけで解除しない。credentialの保存・操作は公式CLIが所有する。
|
|
418
|
+
|
|
406
419
|
|
|
407
420
|
PTYが消失した場合は、相関記録の有無にかかわらず`status`が`failed`、`cancel`が既に終了・取消済みを示す`blocked`を返し、どちらも`session_id:null`となる。保存したsession IDを解除して`start`で再開始できる。生存中の通常PTYやharness不一致、記録の破損・読取り失敗はエラーを返す。
|
|
408
421
|
|
|
@@ -440,6 +453,13 @@ unknown send枠を保持したまま既存core closeへ進み、native close成
|
|
|
440
453
|
旧callbackは未知/消失した元slotで拒否され、同IDの新launchへnative作用を発行できない。
|
|
441
454
|
issued、管理close自体のunknown、親作用のslot、世代/native/serverの不明・交代は対象外。
|
|
442
455
|
既存の送信・待機lockの拒否も維持し、他席やissuerの停止を復旧条件へ追加しない。
|
|
456
|
+
空起動の結果不明は明示 `agent_execution/reconcile_launch` で既存世代へ結合できる(ADR 0128)。
|
|
457
|
+
対象は返却済みunknown launch、send:not_issued、元native/server stamp付きの単独slotだけ。
|
|
458
|
+
同じmetadata transaction内のprivate同期primitiveで現在のnative/managed世代、生のinitial_prompt:none・初期配送なし、
|
|
459
|
+
harness生存とidleを照合する。既存recordのlaunchをaccepted、targetを当該世代へ結合し、該当slotだけを外す。
|
|
460
|
+
これは現在世代の明示採用で、失われたRPC receiptの復元ではない。本文・初期prompt・claim・nativeには作用しない。
|
|
461
|
+
issued、入力済み・不明、停止、別slot、世代/native/serverの不一致は拒否。後続sendは元source世代を含め通常受付を通る。
|
|
462
|
+
遅着した元callbackはまだ所有するissued stage/slotがある時だけunknownを書け、照合後や別stageを上書きしない。
|
|
443
463
|
native handlerのisErrorは元の本文を保持し、`aiterm/execution_error`のcode/effectを併記する。
|
|
444
464
|
native作用後の不明はpossibleのままで、原因説明を再送可否や成功receiptへ変換しない。
|
|
445
465
|
agent sessionへの送信1本を守る`<name>.agent-send.lock`は持つ時間が長いので、持ち主が終了した残骸を次の送信が片付ける。消す役を`<name>.agent-send.lock.reap`の排他作成で1つに絞り、
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "aiterm-mcp",
|
|
3
|
-
"version": "0.61.
|
|
3
|
+
"version": "0.61.7",
|
|
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": [
|