diff --git a/PARITY.md b/PARITY.md
index 2b8e2be..67b73b2 100644
--- a/PARITY.md
+++ b/PARITY.md
@@ -1039,3 +1039,23 @@ automated ACK support in the TS implementation. Cross-runtime execution,
receipt bootstrap, original generation and platform acceptance remain #69–72
implementation gates. This design-identity record alone does not repin a runtime
reference or establish runtime conformance.
+
+
+## Unreleased handoff implementation — #70
+
+The frozen #69 design pin is unchanged. Source-only private POSIX ledger and
+strict protocol helpers now implement prepare, at-most-once sender fencing,
+status, bounded wait records and explicit receipt/operator-confirmation
+validation. [CLI details](docs/api.md#unreleased-opt-in-handoff-cli--040-work)
+separate current source support from the published 0.3.5 runtime. Automated ACK
+bootstrap, Windows private ACL support and original-sender SSH coordination are
+still implementation gates. Fixture passes are not live receipt/injection
+qualification. Default output and the historical Python runtime parity pin stay
+unchanged; no future Python runtime is claimed shipped.
+
+The journal encrypts its private message-binding digest with a separate
+mode-0600 `ledger.key`; missing/changed/untrusted keys and plaintext journals
+are refused without automatic key recovery or history migration. This is an
+at-rest journal boundary, not protection against a reader of both private
+files. Duplicate committed operator confirmation returns the original receipt
+without rewriting its wait classification or journal.
diff --git a/README.ja.md b/README.ja.md
index e2a7c0c..a3e3183 100644
--- a/README.ja.md
+++ b/README.ja.md
@@ -114,6 +114,8 @@ session-peer --version
- [0.4.0 handoff 設計(共同合意・未実装)](PARITY.md#handoff-v1-design-candidate--69).
+- [開発中の private handoff CLI と制限](docs/api.md#unreleased-opt-in-handoff-cli--040-work).
+
## License
[MIT ライセンス](LICENSE)で公開しています。
diff --git a/README.ko.md b/README.ko.md
index 6e8ad66..6247269 100644
--- a/README.ko.md
+++ b/README.ko.md
@@ -128,6 +128,8 @@ session-peer --version
- [0.4.0 handoff 설계(공동 확정·미구현)](PARITY.md#handoff-v1-design-candidate--69).
+- [개발 중인 private handoff CLI와 지원 한계](docs/api.md#unreleased-opt-in-handoff-cli--040-work).
+
## 라이선스
diff --git a/README.md b/README.md
index f57d25a..1cb8c7e 100644
--- a/README.md
+++ b/README.md
@@ -114,6 +114,8 @@ does not install a skill. Review its pinned-source instructions in the guide.
- [0.4.0 handoff design (jointly agreed, unimplemented)](PARITY.md#handoff-v1-design-candidate--69).
+- [Unreleased private handoff CLI and limitations](docs/api.md#unreleased-opt-in-handoff-cli--040-work).
+
## License
Licensed under the [MIT License](LICENSE).
diff --git a/README.zh-CN.md b/README.zh-CN.md
index d289cb4..6effd7b 100644
--- a/README.zh-CN.md
+++ b/README.zh-CN.md
@@ -110,6 +110,8 @@ session-peer --version
- [0.4.0 handoff 设计(联合确认、未实现)](PARITY.md#handoff-v1-design-candidate--69).
+- [开发中的 private handoff CLI 与限制](docs/api.md#unreleased-opt-in-handoff-cli--040-work).
+
## License
采用 [MIT 许可证](LICENSE)。
diff --git a/docs/api.md b/docs/api.md
index c5d094c..1a40149 100644
--- a/docs/api.md
+++ b/docs/api.md
@@ -33,3 +33,52 @@ not package subpath exports. Use the [CLI guide](guide.md#use) for sending and
The ordinary SSH response correction is documented separately as the [0.3.3 ordinary SSH response correction](design/ordinary-ssh-responses.md); its internal parser is not a package-root export.
+
+## Unreleased opt-in handoff CLI — 0.4.0 work
+
+This source adds an explicit private POSIX sender ledger. The published 0.3.5
+package does not contain these commands. Default sends retain their native
+status, optional fields and exit behavior. The frozen design is tracked in #69;
+this implementation does not change its exact vendored document or fixture.
+
+Initialize once with `session-peer handoff init --json`. The optional absolute
+`SESSION_PEER_HANDOFF_HOME` selects its directory; commands never auto-initialize
+missing or corrupt history. `handoff prepare --to TARGET --message-file FILE
+--json` reserves a fresh correlation ID without sending. Use a private regular
+UTF-8 file (POSIX mode 0600), then pass the retained ID with
+`send --correlation-id UUID --message-file FILE --json`. A changed destination,
+original generation or message binding is rejected. An unknown supplied ID is
+not accepted. Ordinary opted-in sends can automatically prepare a fresh ID.
+
+Only opted-in results add `handoff`. The inert correlation metadata in a native
+message is not authentication. Durable intent is fenced before native effect;
+crash recovery, `handoff status`, `handoff wait`, `ack` and `handoff confirm`
+never submit again. Retention/quota limits preserve epoch-bound fences rather
+than evicting old authority. Clone/rollback protection assumes intact, retained,
+non-rolled-back history. Native local/SSH retry permission remains false.
+
+Keep `ledger.key` with the private ledger: the journal stores an authenticated
+encrypted message-binding digest, using that separate mode-0600 key. The raw
+digest is not stored in the journal. Missing, changed or untrusted keys and old
+plaintext journals fail closed; commands do not regenerate a key or migrate
+history automatically. This does not protect against a same-user reader who
+can obtain both files. Lost history never authorizes resubmission.
+
+`--request-ack`, `--observe-delivery` and `--wait-for delivered|acknowledged`
+select separate evidence goals. A required unsupported goal refuses before
+submission; best-effort mode may submit while reporting the channel unsupported.
+`--wait-timeout` is an ASCII integer 1..60 (default 30); its total operation
+budget includes a 5-second cleanup reserve. Budgets 1..5 are syntactically valid
+but cannot authorize an effect. A successful submission is preserved when
+subsequent ledger/evidence operations fail. Best-effort sends keep their native
+success exit and may add `handoffWarning: persistence_failed`; required waits
+fail without erasing independently established native submission facts. No body, transcript, raw native
+stderr, payload digest or receipt secret appears in handoff JSON.
+
+Automated receipt producer/bootstrap, Windows ledger and original-sender SSH
+handoff coordination remain unsupported. `ack --receipt -` and `handoff confirm
+--receipt -` consume at most 4096 raw UTF-8 bytes from private stdin; no receipt
+secret is accepted in argv/URI/environment. Receipt ACK means receipt only, not
+ownership, completed work or permission to act. Operator confirmation is
+explicitly labeled and must match retained original context. No arbitrary
+message/transcript scan manufactures ACK. Ordinary generated Reply-To stays v1.
diff --git a/docs/guide.ja.md b/docs/guide.ja.md
index 1471f07..c89accb 100644
--- a/docs/guide.ja.md
+++ b/docs/guide.ja.md
@@ -338,3 +338,10 @@ npm `--ignore-scripts` は利用でき、postinstall は Skills CLI を呼び出
任意の `sp` 短縮名は 0.3.0 以降に含まれ(0.2.1 以前にはありません)、自動では有効になりません。
[明示的な有効化・衝突・解除(英語)](shorthand.md)を参照してください。
+
+
+## Unreleased handoff
+
+開発中の 0.4.0 ソースには opt-in private POSIX handoff ledger が追加されます。公開 0.3.1 にこのコマンドはありません。必須の証拠が unsupported の場合は提出前に拒否し、best-effort の提出は ACK を証明しません。自動 receipt bootstrap、Windows ledger、SSH handoff は未対応です。
+
+[CLI](api.md#unreleased-opt-in-handoff-cli--040-work)
diff --git a/docs/guide.ko.md b/docs/guide.ko.md
index e9faeea..2da46aa 100644
--- a/docs/guide.ko.md
+++ b/docs/guide.ko.md
@@ -592,3 +592,9 @@ npm `--ignore-scripts`로 설치해도 되며, 설치 후 Skills CLI를 호출
선택적 `sp` 단축 이름은 0.3.0 이상에 포함되며(0.2.1 이하에는 없음) 자동으로 켜지지 않습니다.
[명시적 활성화와 이름 충돌 안내(영문)](shorthand.md)를 참고하세요.
+
+## Unreleased handoff
+
+개발 중인 0.4.0 소스에는 opt-in private POSIX handoff ledger가 추가됩니다. 공개 0.3.1에는 이 명령이 없습니다. 필수 증거 채널이 unsupported이면 제출 전에 거절하며, best-effort 제출은 ACK를 증명하지 않습니다. 자동 receipt bootstrap, Windows ledger 및 SSH handoff는 아직 미지원입니다.
+
+[CLI](api.md#unreleased-opt-in-handoff-cli--040-work)
diff --git a/docs/guide.md b/docs/guide.md
index 44c4eef..36a41f6 100644
--- a/docs/guide.md
+++ b/docs/guide.md
@@ -618,3 +618,10 @@ install a runtime. See [compatibility and validation](../PARITY.md#source-ts-ski
Optional `sp` shorthand ships with 0.3.0 and later (not 0.2.1 or earlier) and is
never enabled automatically. See [explicit activation and collisions](shorthand.md).
+
+
+## Unreleased handoff
+
+Unreleased 0.4.0 source adds an opt-in private POSIX handoff ledger. Published 0.3.1 has no handoff commands. Required unsupported evidence refuses before submission; best-effort unsupported evidence never proves ACK. Automated receipt bootstrap, Windows ledger and SSH handoff remain unsupported.
+
+[CLI](api.md#unreleased-opt-in-handoff-cli--040-work)
diff --git a/docs/guide.zh-CN.md b/docs/guide.zh-CN.md
index 47ade0f..86c1814 100644
--- a/docs/guide.zh-CN.md
+++ b/docs/guide.zh-CN.md
@@ -322,3 +322,10 @@ postinstall 不会调用 Skills CLI,安装此技能不会覆盖 Python 技能
可选的 `sp` 简写包含在 0.3.0 及更高版本中(0.2.1 及更早版本没有),不会自动启用。
请参阅[显式启用、冲突与停用(英文)](shorthand.md)。
+
+
+## Unreleased handoff
+
+开发中的 0.4.0 源码加入 opt-in private POSIX handoff ledger。已发布的 0.3.1 不含这些命令。所需证据通道 unsupported 时在提交前拒绝;best-effort 提交并不证明 ACK。自动 receipt bootstrap、Windows ledger 和 SSH handoff 尚不支持。
+
+[CLI](api.md#unreleased-opt-in-handoff-cli--040-work)
diff --git a/scripts/release.mjs b/scripts/release.mjs
index a03e175..db44366 100644
--- a/scripts/release.mjs
+++ b/scripts/release.mjs
@@ -19,7 +19,7 @@ export const packageFiles = ['UNICODE-LICENSE.txt', 'dist/casefold.js', 'dist/ca
'dist/cli.js', 'dist/discovery.js', 'dist/diagnostics.js', 'dist/skill-metadata.js', 'dist/process.js', 'dist/protocol.js', 'dist/replies.js', 'dist/send.js', 'dist/ssh.js', 'dist/ssh-response.js',
'dist/windows.js', 'dist/writer.js', 'dist/index.js', 'dist/index.d.ts', 'dist/cli.d.ts',
'dist/discovery.d.ts', 'dist/diagnostics.d.ts', 'dist/skill-metadata.d.ts', 'dist/process.d.ts', 'dist/protocol.d.ts',
- 'dist/replies.d.ts', 'dist/send.d.ts', 'dist/ssh.d.ts', 'dist/ssh-response.d.ts', 'dist/windows.d.ts', 'dist/writer.d.ts', 'dist/updates.js', 'dist/updates.d.ts', 'package.json'].sort();
+ 'dist/replies.d.ts', 'dist/send.d.ts', 'dist/ssh.d.ts', 'dist/ssh-response.d.ts', 'dist/windows.d.ts', 'dist/writer.d.ts', 'dist/updates.js', 'dist/updates.d.ts', 'dist/handoff.js', 'dist/handoff.d.ts', 'dist/handoff-ledger.js', 'dist/handoff-ledger.d.ts', 'dist/handoff-operations.js', 'dist/handoff-operations.d.ts', 'package.json'].sort();
const json = path => JSON.parse(readFileSync(path, 'utf8'));
const digest = (bytes, algorithm, encoding = 'hex') => createHash(algorithm).update(bytes).digest(encoding);
const npm = args => execFileSync('npm', args, { encoding: 'utf8', timeout: 120000 });
diff --git a/src/cli.ts b/src/cli.ts
index ad8b272..a8c97f5 100644
--- a/src/cli.ts
+++ b/src/cli.ts
@@ -1,4 +1,6 @@
#!/usr/bin/env node
+import { handoffCommand, handoffSend, privateMessage } from './handoff-operations.js';
+import { canonicalId, HandoffBudget, parseWaitTimeout } from './handoff.js';
import { doctor } from './diagnostics.js';
import { help } from './help.js';
import { renderOutput, type OutputFormat } from './output.js';
@@ -16,8 +18,8 @@ import { configuredHost, detectedHost, isSelf, probeReturnRoute, unsafeReturnHos
type Options = { command: 'list' | 'send' | 'doctor' | 'update'; values: Map; flags: Set; hosts: string[]; ssh: SshOptions; jump?: SshJump;
address?: { uri: string; transport: 'local' | 'ssh'; implicit: boolean } };
-const FLAGS = ['--json', '--all', '--dry-run', '--no-from', '--no-reply-to', '--no-update-notice', '--allow-inactive-codex-home', '--check-return-route', '--check'];
-const VALUES = ['--agent', '--codex-home', '--codex-bin', '--output-format', '--to', '--message', '-m', '--host', '--remote-bin', '--remote-platform', '--ssh-control-path', '--reply-address', '--ssh-opt', '--ssh-jump', '--reply-to', '--return-route-host', '--channel'];
+const FLAGS = ['--json', '--all', '--dry-run', '--no-from', '--no-reply-to', '--no-update-notice', '--allow-inactive-codex-home', '--check-return-route', '--check', '--request-ack', '--observe-delivery'];
+const VALUES = ['--agent', '--codex-home', '--codex-bin', '--output-format', '--to', '--message', '-m', '--host', '--remote-bin', '--remote-platform', '--ssh-control-path', '--reply-address', '--ssh-opt', '--ssh-jump', '--reply-to', '--return-route-host', '--channel', '--correlation-id', '--wait-for', '--wait-timeout', '--message-file'];
// Repeated in order; every other value option is single.
const REPEATED = ['--host', '--ssh-opt'];
// Requested destinations, so a command-wide refusal can be attributed to each.
@@ -68,6 +70,20 @@ export function parse(args: string[]): Options {
if (values.has('--output-format') && !['json', 'text'].includes(values.get('--output-format')!)) throw new Refusal('unsupported_output_format');
if (flags.has('--json') && values.get('--output-format') === 'text') throw new Refusal('conflicting_output_options');
if (!flags.has('--json') && !values.has('--output-format')) throw new Refusal('json_output_required');
+ const handoff = flags.has('--request-ack') || flags.has('--observe-delivery') || values.has('--correlation-id') || values.has('--wait-for') || values.has('--wait-timeout');
+ if (handoff) {
+ if (command !== 'send') throw new Refusal('inapplicable_option');
+ if (values.has('--correlation-id') && !canonicalId(values.get('--correlation-id'))) throw new Refusal('invalid_correlation_id');
+ if (values.has('--wait-for') && !['delivered', 'acknowledged'].includes(values.get('--wait-for')!)) throw new Refusal('invalid_wait_for');
+ if (values.has('--wait-timeout')) {
+ parseWaitTimeout(values.get('--wait-timeout')!);
+ if (!values.has('--correlation-id') && !values.has('--wait-for') && !flags.has('--request-ack') && !flags.has('--observe-delivery')) throw new Refusal('inapplicable_wait_timeout');
+ }
+ if (flags.has('--dry-run') && (flags.has('--request-ack') || flags.has('--observe-delivery') || values.has('--wait-for'))) throw new Refusal('incompatible_handoff_dry_run');
+ // The original-sender ledger/receipt route is not yet qualified over SSH.
+ if (hosts.length) throw new Refusal('remote_handoff_unsupported', 1);
+ }
+ if (values.has('--message-file') && (command !== 'send' || values.has('--message'))) throw new Refusal('conflicting_message_sources');
if (command === 'update') {
if (values.has('--channel') && !CHANNELS.includes(values.get('--channel')!)) throw new Refusal('unsupported_update_channel');
// Local client only: remote hosts and other installations update through their own managers.
@@ -102,6 +118,9 @@ export function parse(args: string[]): Options {
values.set('--to', route.to);
}
}
+ // Reply-To URI parsing can introduce an SSH destination after the early
+ // flag check. Never let that route bypass the original sender's ledger.
+ if (handoff && hosts.length) throw new Refusal('remote_handoff_unsupported', 1);
if (command === 'send' && flags.has('--allow-inactive-codex-home')) {
if (!values.get('--to')!.startsWith('codex:')) throw new Refusal('inapplicable_option');
if (!values.get('--codex-home')) throw new Refusal('inactive_opt_in_requires_explicit_home');
@@ -143,17 +162,19 @@ export function parse(args: string[]): Options {
}
return { command, values, flags, hosts, ssh, ...(jump ? { jump } : {}), ...(address ? { address } : {}) };
}
-async function input(): Promise {
- const chunks: Buffer[] = [];
- let bytes = 0;
- for await (const chunk of process.stdin) {
- const part = Buffer.from(chunk); bytes += part.length;
- if (bytes > 4_100_000) throw new Refusal('input_too_large');
- chunks.push(part);
- }
- try { return new TextDecoder('utf-8', { fatal: true }).decode(Buffer.concat(chunks)); }
- catch { throw new Refusal('invalid_utf8'); }
+async function input(limit = 4_100_000, preserveBOM = false, timeoutMs?: number): Promise {
+ const chunks: Buffer[] = []; let bytes = 0;
+ const timer = timeoutMs === undefined ? undefined : setTimeout(() => process.stdin.destroy(new Refusal('deadline_before_effect', 1)), Math.max(1,timeoutMs));
+ try {
+ for await (const chunk of process.stdin) {
+ const part = Buffer.from(chunk); bytes += part.length;
+ if (bytes > limit) throw new Refusal('input_too_large'); chunks.push(part);
+ }
+ try { return new TextDecoder('utf-8', {fatal:true,ignoreBOM:preserveBOM}).decode(Buffer.concat(chunks)); }
+ catch { throw new Refusal('invalid_utf8'); }
+ } finally { if(timer) clearTimeout(timer); }
}
+
const quote = (s: string) => "'" + s.replace(/'/g, "'\\''") + "'";
// Classify only the no-message version preflight. Never expose SSH stderr,
// which can contain user paths or agent output.
@@ -214,6 +235,8 @@ async function remote(options: Options, ssh: string, target: string, resolved: {
const { value } = verified;
// `clientUpdate` is client-local: a remote-supplied value is never trusted or shown.
delete value.clientUpdate;
+ // Ordinary SSH responses cannot opt into peer-supplied handoff evidence.
+ delete value.handoff; delete value.handoffQuery; delete value.handoffWarning;
return { value: { ...value, host: resolved.canonical, sshHost: target }, exitCode: verified.exitCode };
}
function failure(error: unknown, command: string, where: string): { value: Record; exitCode: number } {
@@ -270,7 +293,13 @@ try {
if (args.length === 1 && args[0] === REFRESH_ARG) await refreshCache();
else if (args.length === 1 && args[0] === '--version') console.log(VERSION_LINE);
else if ((args.length === 1 && ['--help', '-h'].includes(args[0]!)) ||
- (args.length === 2 && ['list', 'send', 'doctor', 'update'].includes(args[0]!) && ['--help', '-h'].includes(args[1]!))) console.log(help(args.length === 2 ? args[0] : undefined));
+ (args.length === 2 && ['list', 'send', 'doctor', 'update', 'handoff', 'ack'].includes(args[0]!) && ['--help', '-h'].includes(args[1]!))) console.log(help(args.length === 2 ? args[0] : undefined));
+ else if (args[0] === 'handoff' || args[0] === 'ack') {
+ command = args[0];
+ const operation = await handoffCommand(args, limit => input(limit, true));
+ console.log(JSON.stringify({ schemaVersion: 1, host: hostname(), command, ok: operation.exitCode === 0, ...operation.value }));
+ process.exitCode = operation.exitCode;
+ }
else {
wire = args.length === 1 && args[0] === '--stdio-request';
if (wire) {
@@ -282,6 +311,8 @@ try {
}
command = ['list', 'send', 'doctor', 'update'].includes(args[0] ?? '') ? args[0]! : 'unknown';
const options = parse(args);
+ const handoffEnabled = options.flags.has('--request-ack') || options.flags.has('--observe-delivery') || options.values.has('--correlation-id') || options.values.has('--wait-for') || options.values.has('--wait-timeout');
+ const handoffBudget = handoffEnabled ? new HandoffBudget(parseWaitTimeout(options.values.get('--wait-timeout') ?? '30')) : undefined;
format = !wire && options.values.get('--output-format') === 'text' ? 'text' : 'json';
if (wire && options.values.get('--output-format') === 'text') throw new Refusal('remote_json_required');
if (wire && command === 'update') throw new Refusal('remote_update_unsupported');
@@ -291,11 +322,21 @@ try {
// Tailscale status is queried at most once, and only when a route needs it.
let status: Tailnet | undefined, queried = false;
const tailnetStatus = async () => { if (!queried) { queried = true; status = await tailnet(); } return status; };
+ let rawMessage: string | undefined;
let message: string | undefined, routing: Record = {};
if (command === 'send') {
- message = options.values.get('--message');
- if (message === undefined || message === '-') { if (wire) throw new Refusal('remote_message_required'); message = await input(); }
- checkMessage(message);
+ if (options.values.has('--message-file')) {
+ // File contents are literal, including a single dash. Only the local
+ // command-line message source uses '-' as the stdin selector.
+ message = privateMessage(options.values.get('--message-file')!);
+ } else {
+ message = options.values.get('--message');
+ if (message === undefined || (!wire && message === '-')) {
+ if (wire) throw new Refusal('remote_message_required');
+ message = await input(4_100_000, false, handoffBudget?.observationRemainingMs());
+ }
+ }
+ checkMessage(message); rawMessage = message;
const noFrom = options.flags.has('--no-from'), noReply = options.flags.has('--no-reply-to');
if (options.address) {
const resolution: Record = { uri: options.address.uri, transport: options.address.transport };
@@ -337,7 +378,13 @@ try {
let result: Record = {}, exitCode: number | undefined, results: Record[] | undefined;
// Propagate the verified remote exit code so SSH and local refusals match.
if (options.hosts.length) ({ value: results, exitCode } = await remotes(options, await tailnetStatus(), message, returnTo));
- else if (command === 'send') result = await send({ to: options.values.get('--to')!, home: options.values.get('--codex-home'), codexBin: options.values.get('--codex-bin'), message: message!, dryRun: options.flags.has('--dry-run'), allowInactive: options.flags.has('--allow-inactive-codex-home') });
+ else if (command === 'send') {
+ const settings = { to: options.values.get('--to')!, home: options.values.get('--codex-home'), codexBin: options.values.get('--codex-bin'), message: message!, dryRun: options.flags.has('--dry-run'), allowInactive: options.flags.has('--allow-inactive-codex-home') };
+ const enabled = options.flags.has('--request-ack') || options.flags.has('--observe-delivery') || options.values.has('--correlation-id') || options.values.has('--wait-for') || options.values.has('--wait-timeout');
+ if (wire && enabled) throw new Refusal('remote_handoff_unsupported', 1);
+ if (enabled) ({ value: result, exitCode } = await handoffSend(settings, { correlationId: options.values.get('--correlation-id'), requestAck: options.flags.has('--request-ack'), observeDelivery: options.flags.has('--observe-delivery'), waitFor: options.values.get('--wait-for') as 'delivered' | 'acknowledged' | undefined, seconds: parseWaitTimeout(options.values.get('--wait-timeout') ?? '30'), payload: rawMessage, budget: handoffBudget, resultOverhead: Buffer.byteLength(JSON.stringify(routing)) + 1024 }));
+ else result = await send(settings);
+ }
else if (command === 'update') result = options.flags.has('--check') ? await checkUpdate(options.values.get('--channel')) : refuseSelfUpdate(options.values.get('--channel'));
else if (command === 'doctor') {
result = await doctor(options.values.get('--agent') as 'claude' | 'codex' | undefined, options.values.get('--codex-home'), options.values.get('--codex-bin'));
@@ -366,7 +413,13 @@ try {
}
} catch (error) {
// A command-wide refusal before dispatch applies to every requested host.
+ if (command === 'handoff' || command === 'ack') {
+ const failureCode = error instanceof Refusal ? error.code : 'handoff_operation_failed';
+ console.log(JSON.stringify({ schemaVersion: 1, host: hostname(), command, ok: false, error: failureCode }));
+ process.exitCode = error instanceof Refusal ? error.exitCode : 1;
+ } else {
const { value, exitCode } = failure(error, command, hostname());
console.log(renderOutput(!wire && requested.length > 1 ? requested.map(item => ({ ...value, host: item })) : value, format));
process.exitCode = exitCode;
+ }
}
diff --git a/src/handoff-ledger.ts b/src/handoff-ledger.ts
new file mode 100644
index 0000000..6c6b8c0
--- /dev/null
+++ b/src/handoff-ledger.ts
@@ -0,0 +1,469 @@
+// Private POSIX ledger. No native submission or transcript access occurs here.
+// Fencing assumes intact retained history: unmarked clones/rollbacks cannot be
+// detected by a local-only ledger. Windows ACL/bootstrap support is not claimed.
+import { randomBytes, randomUUID, createHash, timingSafeEqual, createCipheriv, createDecipheriv } from 'node:crypto';
+import { closeSync, constants, fstatSync, fsyncSync, lstatSync, mkdirSync, openSync, readSync, renameSync, unlinkSync, writeFileSync } from 'node:fs';
+import { isAbsolute, join, resolve } from 'node:path';
+import { createRequire } from 'node:module';
+import { canonicalId, closed, fail, HANDOFF_LIMITS, HandoffBudget, parseStrictJson, safeId, safeInteger,
+ systemClock, validateConfirmation, validateHandoff, validateReceipt, type Clock, type Handoff, type WaitFor, type WaitReason, type WaitStatus } from './handoff.js';
+
+export type HandoffBinding = Readonly<{ agent: 'claude' | 'codex'; destination: string; target: string;
+ home?: string; writerIdentity?: string; generation: string | null; payloadDigest: string }>;
+export type PrepareOptions = { correlationId?: string; requestAck?: boolean; observeDelivery?: boolean; waitFor?: WaitFor;
+ budget?: HandoffBudget; dryRun?: boolean; channels?: { delivery: boolean; receipt?: boolean };
+ observationSupported?: boolean; clientUserMessageId?: string };
+export type HandoffObservation = { clientUserMessageId: string; injectionObserved: boolean; turn?: Handoff['observation']['turn'] };
+type StoredWait = { public: Handoff['wait']; clockId: string; cutoff: number };
+type RecordIntent = { binding: HandoffBinding; preparedUtc: number; clockId: string; preparedMono: number;
+ phase: 'prepared' | 'attempted' | 'finished' | 'refused'; handoff: Handoff; waits: StoredWait[];
+ reserve: number; capabilityHash?: string; capabilityClockId?: string; receiptId?: string; revoked?: boolean; expired?: boolean;
+ deliverySupported?: boolean; clientUserMessageId?: string };
+type Ledger = { version: 1; epoch: string; keyId: string; quarantined: boolean; records: Record };
+export type HandoffQueryError = { schemaVersion: 1; correlationId: string; status: 'unknown';
+ context: 'ledger_missing' | 'ledger_corrupt' | 'id_unknown'; retry: { allowed: false; reason: 'history_unavailable' } };
+export type HandoffStatus = { handoff: Handoff } | { reason: 'handoff_history_unavailable'; handoffQuery: HandoffQueryError };
+const reserveBytes = 65536;
+export const payloadDigest = (payload: string | Uint8Array): string => createHash('sha256').update(payload).digest('hex');
+function checkBinding(value: HandoffBinding): HandoffBinding {
+ const row = closed(value, ['agent', 'destination', 'target', 'generation', 'payloadDigest'], ['home', 'writerIdentity']);
+ if (!['claude', 'codex'].includes(row.agent as string) || !safeId(row.destination, 512) || !safeId(row.target, 256) ||
+ (row.generation !== null && !safeId(row.generation, 256)) || typeof row.payloadDigest !== 'string' || !/^[a-f0-9]{64}$/.test(row.payloadDigest) ||
+ (row.home !== undefined && (!safeId(row.home, 4096) || !isAbsolute(row.home))) ||
+ (row.writerIdentity !== undefined && !safeId(row.writerIdentity, 256))) fail('invalid_handoff_binding', 2);
+ return structuredClone(value);
+}
+function sameBinding(a: HandoffBinding, b: HandoffBinding): boolean {
+ return ['agent', 'destination', 'target', 'home', 'writerIdentity', 'generation', 'payloadDigest'].every(key => a[key as keyof HandoffBinding] === b[key as keyof HandoffBinding]);
+}
+function privateStat(path: string, directory: boolean) {
+ const uid = process.getuid?.(); if (uid === undefined || process.platform === 'win32') fail('handoff_storage_unsupported');
+ const stat = lstatSync(path);
+ if (stat.isSymbolicLink() || (directory ? !stat.isDirectory() : !stat.isFile()) || stat.uid !== uid ||
+ (stat.mode & 0o777) !== (directory ? 0o700 : 0o600) || (!directory && stat.nlink !== 1)) fail('handoff_storage_untrusted');
+ return stat;
+}
+export class HandoffLedger {
+ readonly path: string;
+ private readonly clock: Clock;
+ private readonly clockId = randomUUID();
+ constructor(path: string, options: { clock?: Clock } = {}) {
+ if (!isAbsolute(path)) fail('invalid_handoff_ledger', 2);
+ this.path = resolve(path); this.clock = options.clock ?? systemClock;
+ }
+ private filename(): string { return join(this.path, 'ledger.json'); }
+ // The separate private key is created only by fresh explicit init. A missing,
+ // changed or untrusted key cannot authorize migration or new submission.
+ private key(): Buffer {
+ const path = join(this.path, 'ledger.key'), before = privateStat(path, false);
+ if (before.size !== 32) fail('handoff_ledger_corrupt');
+ const fd = openSync(path, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
+ try {
+ const opened = fstatSync(fd), bytes = Buffer.alloc(33);
+ if (opened.dev !== before.dev || opened.ino !== before.ino || opened.size !== 32 || opened.nlink !== 1 || opened.uid !== before.uid || (opened.mode & 0o777) !== 0o600) fail('handoff_storage_untrusted');
+ let length = 0;
+ while (length < bytes.length) { const count = readSync(fd, bytes, length, bytes.length - length, length); if (!count) break; length += count; }
+ const after = fstatSync(fd), current = privateStat(path, false);
+ if (length !== 32 || after.size !== 32 || after.mtimeMs !== opened.mtimeMs || after.ctimeMs !== opened.ctimeMs ||
+ after.nlink !== 1 || after.uid !== opened.uid || after.mode !== opened.mode || current.dev !== opened.dev || current.ino !== opened.ino ||
+ current.size !== after.size || current.mtimeMs !== after.mtimeMs || current.ctimeMs !== after.ctimeMs) fail('handoff_storage_untrusted');
+ return Buffer.from(bytes.subarray(0, 32));
+ } finally { closeSync(fd); }
+ }
+ private protectDigest(digest: string, key: Buffer, epoch: string, id: string): string {
+ const nonce = randomBytes(12), cipher = createCipheriv('aes-256-gcm', key, nonce);
+ cipher.setAAD(Buffer.from(JSON.stringify(['handoff-binding-v1', epoch, id])));
+ const ciphertext = Buffer.concat([cipher.update(Buffer.from(digest, 'hex')), cipher.final()]);
+ return `sealed-v1:${nonce.toString('hex')}:${ciphertext.toString('hex')}:${cipher.getAuthTag().toString('hex')}`;
+ }
+ private revealDigest(value: unknown, key: Buffer, epoch: string, id: string): string {
+ if (typeof value !== 'string' || !/^sealed-v1:[a-f0-9]{24}:[a-f0-9]{64}:[a-f0-9]{32}$/.test(value)) fail('handoff_ledger_corrupt');
+ const [, nonce, ciphertext, tag] = (value as string).split(':');
+ const cipher = createDecipheriv('aes-256-gcm', key, Buffer.from(nonce!, 'hex'));
+ cipher.setAAD(Buffer.from(JSON.stringify(['handoff-binding-v1', epoch, id]))); cipher.setAuthTag(Buffer.from(tag!, 'hex'));
+ return Buffer.concat([cipher.update(Buffer.from(ciphertext!, 'hex')), cipher.final()]).toString('hex');
+ }
+ private lock(operation: () => T, create = false): T {
+ try { privateStat(this.path, true); } catch (error) { if ((error as NodeJS.ErrnoException).code === 'ENOENT') fail('handoff_history_unavailable'); throw error; }
+ const path = join(this.path, 'ledger.lock');
+ if (create) {
+ try { const created = openSync(path, constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY, 0o600); fsyncSync(created); closeSync(created); }
+ catch (error) { if ((error as NodeJS.ErrnoException).code !== 'EEXIST') throw error; }
+ }
+ const before = privateStat(path, false);
+ const fd = openSync(path, constants.O_RDWR | constants.O_NOFOLLOW | constants.O_NONBLOCK);
+ try {
+ const opened = fstatSync(fd); if (opened.dev !== before.dev || opened.ino !== before.ino) fail('handoff_storage_untrusted');
+ const locks = createRequire(import.meta.url)('fs-ext-extra-prebuilt') as { flockSync: (fd: number, mode: string) => void };
+ try { locks.flockSync(fd, 'exnb'); } catch { fail('handoff_ledger_busy'); }
+ try { return operation(); } finally { locks.flockSync(fd, 'un'); }
+ } finally { closeSync(fd); }
+ }
+ private read(): Ledger {
+ privateStat(this.path, true);
+ const path = this.filename(), stat = privateStat(path, false);
+ if (stat.size > HANDOFF_LIMITS.journal) fail('handoff_ledger_corrupt');
+ const fd = openSync(path, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
+ let bytes: Buffer;
+ try {
+ const opened = fstatSync(fd); if (opened.dev !== stat.dev || opened.ino !== stat.ino || opened.size > HANDOFF_LIMITS.journal) fail('handoff_storage_untrusted');
+ const buffer = Buffer.alloc(Math.min(opened.size + 1, HANDOFF_LIMITS.journal + 1));
+ let length = 0;
+ while (length < buffer.length) { const count = readSync(fd, buffer, length, buffer.length - length, length); if (!count) break; length += count; }
+ if (length > opened.size || length > HANDOFF_LIMITS.journal) fail('handoff_ledger_corrupt');
+ bytes = buffer.subarray(0, length);
+ } finally { closeSync(fd); }
+ try {
+ const wrapper = closed(parseStrictJson(bytes, { scope: 'outer', maxBytes: HANDOFF_LIMITS.journal }), ['digest', 'data']);
+ if (typeof wrapper.digest !== 'string' || wrapper.digest !== payloadDigest(JSON.stringify(wrapper.data))) fail();
+ const data = closed(wrapper.data, ['version', 'epoch', 'keyId', 'quarantined', 'records']);
+ if (data.version !== 1 || !canonicalId(data.epoch) || typeof data.quarantined !== 'boolean' || !data.records || typeof data.records !== 'object' || Array.isArray(data.records)) fail();
+ const key = this.key();
+ if (data.keyId !== payloadDigest(key)) fail();
+ const records = data.records as Record;
+ if (Object.keys(records).length > HANDOFF_LIMITS.intents) fail();
+ for (const [id, item] of Object.entries(records)) {
+ closed(item, ['binding', 'preparedUtc', 'clockId', 'preparedMono', 'phase', 'handoff', 'waits', 'reserve'], ['capabilityHash', 'capabilityClockId', 'receiptId', 'revoked', 'expired', 'deliverySupported', 'clientUserMessageId']);
+ if (!canonicalId(id)) fail();
+ item.binding = { ...item.binding, payloadDigest: this.revealDigest(item.binding?.payloadDigest, key, data.epoch as string, id) };
+ checkBinding(item.binding);
+ if (!canonicalId(id) || !canonicalId(item.clockId) || !safeInteger(item.preparedUtc) || typeof item.preparedMono !== 'number' || item.preparedMono < 0 ||
+ !['prepared', 'attempted', 'finished', 'refused'].includes(item.phase) || !Array.isArray(item.waits) || item.waits.length > HANDOFF_LIMITS.waits ||
+ !safeInteger(item.reserve) || item.reserve > reserveBytes || (item.capabilityHash !== undefined && !/^[a-f0-9]{64}$/.test(item.capabilityHash)) ||
+ (item.capabilityClockId !== undefined && !canonicalId(item.capabilityClockId)) ||
+ (item.receiptId !== undefined && !canonicalId(item.receiptId)) ||
+ (item.deliverySupported !== undefined && typeof item.deliverySupported !== 'boolean') ||
+ (item.clientUserMessageId !== undefined && !safeId(item.clientUserMessageId)) ||
+ (item.deliverySupported && (!safeId(item.clientUserMessageId) || item.binding.generation === null))) fail();
+ const h = validateHandoff(item.handoff);
+ if (h.correlationId !== id || h.ledgerEpoch !== data.epoch || h.targetGeneration !== item.binding.generation) fail();
+ for (const wait of item.waits) {
+ closed(wait, ['public', 'clockId', 'cutoff']);
+ if (!canonicalId(wait.clockId) || typeof wait.cutoff !== 'number' || !Number.isFinite(wait.cutoff)) fail();
+ // Current actions belong to the latest operation, not archived waits.
+ validateHandoff({ ...h, wait: wait.public, nextActions: ['reconcile'] });
+ }
+ }
+ return data as Ledger;
+ } catch { fail('handoff_ledger_corrupt'); }
+ }
+ private write(data: Ledger): void {
+ for (const item of Object.values(data.records)) validateHandoff(item.handoff);
+ const key = this.key();
+ if (data.keyId !== payloadDigest(key)) fail('handoff_ledger_corrupt');
+ const stored = structuredClone(data);
+ for (const [id, item] of Object.entries(stored.records)) {
+ checkBinding(item.binding);
+ item.binding = { ...item.binding, payloadDigest: this.protectDigest(item.binding.payloadDigest, key, data.epoch, id) };
+ }
+ // Both quota accounting and the corruption checksum use only ciphertext.
+ // A checksum over transient plaintext would itself permit payload guessing.
+ const text = JSON.stringify({ digest: payloadDigest(JSON.stringify(stored)), data: stored });
+ const reserves = Object.values(stored.records).reduce((sum, item) => sum + Math.max(0, item.reserve - Buffer.byteLength(JSON.stringify(item))), 0);
+ if (Buffer.byteLength(text) + reserves > HANDOFF_LIMITS.journal) fail('handoff_ledger_full');
+ const temporary = join(this.path, `.ledger-${randomUUID()}.tmp`);
+ const fd = openSync(temporary, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 0o600);
+ try { writeFileSync(fd, text, 'utf8'); fsyncSync(fd); }
+ catch (error) { try { unlinkSync(temporary); } catch {} throw error; }
+ finally { closeSync(fd); }
+ try {
+ renameSync(temporary, this.filename());
+ const directory = openSync(this.path, constants.O_RDONLY); try { fsyncSync(directory); } finally { closeSync(directory); }
+ } catch (error) { try { unlinkSync(temporary); } catch {} throw error; }
+ }
+ init(): { ledgerEpoch: string; storage: 'private_posix'; cloneRollbackProtection: 'intact_retained_history_only' } {
+ if (process.platform === 'win32' || process.getuid?.() === undefined) fail('handoff_storage_unsupported');
+ try { mkdirSync(this.path, { mode: 0o700 }); } catch (error) { if ((error as NodeJS.ErrnoException).code !== 'EEXIST') throw error; }
+ return this.lock(() => {
+ try { lstatSync(this.filename()); fail('handoff_ledger_exists'); } catch (error) { if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error; }
+ const key = randomBytes(32), keyPath = join(this.path, 'ledger.key');
+ const keyFd = openSync(keyPath, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 0o600);
+ try { writeFileSync(keyFd, key); fsyncSync(keyFd); } finally { closeSync(keyFd); }
+ const directory = openSync(this.path, constants.O_RDONLY); try { fsyncSync(directory); } finally { closeSync(directory); }
+ const epoch = randomUUID(); this.write({ version: 1, epoch, keyId: payloadDigest(key), quarantined: false, records: {} });
+ return { ledgerEpoch: epoch, storage: 'private_posix', cloneRollbackProtection: 'intact_retained_history_only' };
+ }, true);
+ }
+ private usable(data: Ledger, id: string): RecordIntent {
+ if (!canonicalId(id)) fail('invalid_correlation_id', 2);
+ const item = data.records[id]; if (!item) fail('handoff_id_unknown');
+ if (data.quarantined) fail('handoff_ledger_quarantined');
+ if (item.expired || this.clock.utc() - item.preparedUtc >= HANDOFF_LIMITS.detailMs) fail('handoff_history_unavailable');
+ return item;
+ }
+ private snapshot(item: RecordIntent, quarantine = false): Handoff {
+ const h = structuredClone(item.handoff);
+ if (quarantine || item.expired || this.clock.utc() - item.preparedUtc >= HANDOFF_LIMITS.detailMs) {
+ h.state = 'unknown'; h.submission = { status: 'unknown' }; h.observation = { status: 'not_requested', injectionObserved: false };
+ h.ack = { status: 'unsupported' }; h.wait = { for: 'none', status: 'not_requested' }; h.nextActions = ['reconcile'];
+ } else if (item.phase === 'attempted') { h.state = 'unknown'; h.submission = { status: 'unknown' }; h.nextActions = ['reconcile']; }
+ if ((item.capabilityClockId !== this.clockId || item.revoked || this.clock.monotonic() - item.preparedMono >= HANDOFF_LIMITS.capabilityMs) && h.ack.status === 'pending') {
+ h.ack = { status: 'unsupported' }; h.nextActions = ['reconcile'];
+ }
+ if (item.clockId !== this.clockId && h.observation.status === 'pending') {
+ h.observation = { status: 'unsupported', injectionObserved: false }; h.nextActions = ['reconcile'];
+ }
+ return validateHandoff(h);
+ }
+ status(id: string): HandoffStatus {
+ if (!canonicalId(id)) fail('invalid_correlation_id', 2);
+ let context: HandoffQueryError['context'] = 'ledger_corrupt';
+ try {
+ const data = this.read(), item = data.records[id];
+ if (item) return { handoff: this.snapshot(item, data.quarantined) };
+ context = 'id_unknown';
+ } catch (error) { if ((error as NodeJS.ErrnoException).code === 'ENOENT') context = 'ledger_missing'; }
+ return { reason: 'handoff_history_unavailable', handoffQuery: { schemaVersion: 1, correlationId: id, status: 'unknown', context, retry: { allowed: false, reason: 'history_unavailable' } } };
+ }
+ prepare(binding: HandoffBinding, options: PrepareOptions = {}): Handoff {
+ binding = checkBinding(binding);
+ if (options.dryRun && (options.requestAck || options.observeDelivery || (options.waitFor && options.waitFor !== 'none'))) fail('incompatible_handoff_dry_run', 2);
+ if (options.correlationId && !canonicalId(options.correlationId)) fail('invalid_correlation_id', 2);
+ return this.lock(() => {
+ const data = this.read(); if (data.quarantined) fail('handoff_ledger_quarantined');
+ let id = options.correlationId;
+ if (id) {
+ const item = this.usable(data, id);
+ if (!sameBinding(item.binding, binding)) fail('handoff_binding_conflict');
+ if (item.phase !== 'prepared') fail('handoff_already_attempted');
+ if (!options.dryRun) { this.configure(item, options); this.write(data); }
+ return this.snapshot(item);
+ }
+ if (options.dryRun) fail('prepared_correlation_required', 2);
+ if (Object.keys(data.records).length >= HANDOFF_LIMITS.intents) fail('handoff_ledger_full');
+ id = randomUUID(); const goal = options.waitFor ?? 'none';
+ if (!['none', 'delivered', 'acknowledged'].includes(goal)) fail('invalid_wait_for', 2);
+ // Runtime channels remain unsupported until a compatible private bootstrap
+ // and producer are proven by the integration, not merely by generation.
+ const h: Handoff = { schemaVersion: 1, correlationId: id, ledgerEpoch: data.epoch, state: 'validated', submission: { status: 'not_attempted' },
+ observation: { status: options.observeDelivery ? 'unsupported' : 'not_requested', injectionObserved: false },
+ ack: { status: options.requestAck ? 'unsupported' : 'not_requested' }, wait: { for: 'none', status: 'not_requested' },
+ targetGeneration: binding.generation, decisionOwner: 'sender_operator', retry: { allowed: false, reason: 'receiver_dedup_unavailable' }, nextActions: [] };
+ const item: RecordIntent = { binding, preparedUtc: Math.floor(this.clock.utc()), clockId: this.clockId, preparedMono: this.clock.monotonic(), phase: 'prepared', handoff: h, waits: [], reserve: reserveBytes };
+ data.records[id] = item;
+ this.configure(item, options);
+ this.write(data); return this.snapshot(item);
+ });
+ }
+ private configure(item: RecordIntent, options: PrepareOptions): void {
+ const h = item.handoff, goal = options.waitFor ?? 'none';
+ if (!['none', 'delivered', 'acknowledged'].includes(goal)) fail('invalid_wait_for', 2);
+ if (options.clientUserMessageId !== undefined && !safeId(options.clientUserMessageId)) fail('invalid_client_user_message_id', 2);
+ const clientId = options.clientUserMessageId ?? h.correlationId;
+ if (item.clientUserMessageId !== undefined && item.clientUserMessageId !== clientId) fail('handoff_binding_conflict');
+ if (options.requestAck) h.ack.status = 'unsupported';
+ const delivery = (options.observationSupported === true || options.channels?.delivery === true) && item.binding.generation !== null;
+ if (options.observeDelivery || goal === 'delivered') {
+ item.deliverySupported = delivery; item.clientUserMessageId = clientId;
+ h.observation.status = delivery ? 'pending' : 'unsupported';
+ }
+ if (goal !== 'none') {
+ const budget = options.budget ?? new HandoffBudget(30, this.clock); this.addWait(item, goal, budget);
+ if (goal === 'delivered' && delivery) h.wait.status = 'pending';
+ else { h.wait.status = 'unsupported'; h.wait.reason = 'evidence_unsupported'; }
+ try { budget.beforeEffect(); } catch (error) {
+ h.wait.status = 'failed'; h.wait.reason = (error as { code: WaitReason }).code;
+ }
+ if (goal === 'acknowledged') h.ack.status = 'unsupported';
+ if (h.wait.status !== 'pending') { h.state = 'refused'; h.submission.status = 'refused'; item.phase = 'refused'; }
+ item.waits.at(-1)!.public = structuredClone(h.wait);
+ }
+ }
+ // Durable intent is synced before returning authorization to the caller.
+ // A crash after this point, even before native effect, can never authorize retry.
+ commitEffect(id: string, binding: HandoffBinding, budget?: HandoffBudget): Handoff {
+ binding = checkBinding(binding);
+ return this.lock(() => {
+ const data = this.read(), item = this.usable(data, id);
+ if (!sameBinding(item.binding, binding)) fail('handoff_binding_conflict');
+ if (item.phase !== 'prepared') fail('handoff_already_attempted');
+ try { budget?.beforeEffect(); } catch (error) {
+ this.refuse(item, (error as { code: WaitReason }).code); this.write(data); throw error;
+ }
+ item.phase = 'attempted'; item.clockId = this.clockId;
+ item.handoff.state = 'unknown'; item.handoff.submission.status = 'unknown'; item.handoff.nextActions = ['reconcile'];
+ this.write(data); return this.snapshot(item);
+ });
+ }
+ private refuse(item: RecordIntent, reason?: WaitReason): void {
+ item.phase = 'refused'; item.handoff.state = 'refused'; item.handoff.submission.status = 'refused'; item.handoff.nextActions = [];
+ if (item.handoff.observation.status === 'pending') item.handoff.observation = { status: 'failed', injectionObserved: false };
+ if (item.handoff.ack.status === 'pending') { item.handoff.ack = { status: 'unsupported' }; item.revoked = true; }
+ const wait = item.waits.at(-1);
+ if (wait?.public.status === 'pending') {
+ item.handoff.wait.status = 'failed'; item.handoff.wait.reason = reason ?? 'evidence_failed'; wait.public = structuredClone(item.handoff.wait);
+ }
+ }
+ refusePrepared(id: string, binding: HandoffBinding, reason?: WaitReason): Handoff {
+ binding = checkBinding(binding);
+ return this.lock(() => {
+ const data = this.read(), item = this.usable(data, id);
+ if (!sameBinding(item.binding, binding)) fail('handoff_binding_conflict');
+ if (item.phase !== 'prepared') fail('handoff_already_attempted');
+ this.refuse(item, reason); this.write(data); return this.snapshot(item);
+ });
+ }
+ recordSubmission(id: string, status: 'submitted' | 'unknown' | 'refused'): Handoff {
+ return this.lock(() => {
+ const data = this.read(), item = this.usable(data, id);
+ if (item.phase !== 'attempted' || item.clockId !== this.clockId) fail('handoff_outcome_not_authorized');
+ item.phase = 'finished'; item.handoff.submission.status = status;
+ item.handoff.state = status; item.handoff.nextActions = ['reconcile'];
+ if (status === 'refused') this.refuse(item, 'evidence_failed');
+ this.write(data); return this.snapshot(item);
+ });
+ }
+ recordObservation(id: string, observation: HandoffObservation, originalBinding: HandoffBinding): Handoff {
+ originalBinding = checkBinding(originalBinding);
+ closed(observation, ['clientUserMessageId', 'injectionObserved'], ['turn']);
+ if (!safeId(observation.clientUserMessageId) || typeof observation.injectionObserved !== 'boolean' ||
+ (!observation.injectionObserved && observation.turn !== undefined)) fail('invalid_handoff_observation');
+ return this.lock(() => {
+ const data = this.read(), item = this.usable(data, id), h = item.handoff;
+ if (!sameBinding(item.binding, originalBinding)) fail('handoff_binding_conflict');
+ if (!item.deliverySupported || item.binding.generation === null || item.clientUserMessageId !== observation.clientUserMessageId ||
+ item.phase !== 'finished' || h.submission.status !== 'submitted') fail('handoff_observation_unverified');
+ if (!observation.injectionObserved) return this.snapshot(item);
+ if (observation.turn !== undefined) {
+ closed(observation.turn, ['id', 'status']);
+ if (!safeId(observation.turn.id) || !['running', 'completed', 'failed', 'interrupted', 'unknown'].includes(observation.turn.status)) fail('invalid_handoff_observation');
+ const prior = h.observation.turn;
+ if (prior && (prior.id !== observation.turn.id || (['completed', 'failed', 'interrupted'].includes(prior.status) && prior.status !== observation.turn.status))) fail('handoff_turn_conflict');
+ }
+ h.observation = { status: 'observed', injectionObserved: true, clientUserMessageId: observation.clientUserMessageId,
+ ...(observation.turn ? { turn: structuredClone(observation.turn) } : h.observation.turn ? { turn: h.observation.turn } : {}) };
+ if (h.ack.status !== 'acknowledged') h.state = 'delivered';
+ // All delivery operations share this evidence instant, but keep their own
+ // cutoff and terminal result. A newer ACK wait cannot hide older waits.
+ const observedAt = this.clock.monotonic();
+ for (const wait of item.waits) {
+ if (wait.public.status !== 'pending' || wait.public.for !== 'delivered') continue;
+ if (wait.clockId !== this.clockId) { wait.public.status = 'failed'; wait.public.reason = 'history_unavailable'; }
+ else wait.public.status = observedAt < wait.cutoff ? 'satisfied' : 'timed_out_unknown';
+ }
+ if (item.waits.length) h.wait = structuredClone(item.waits.at(-1)!.public);
+ h.nextActions = ['reconcile']; this.write(data); return this.snapshot(item);
+ });
+ }
+ private addWait(item: RecordIntent, goal: Exclude, budget: HandoffBudget): void {
+ if (item.waits.length >= HANDOFF_LIMITS.waits) fail('handoff_wait_quota');
+ item.handoff.wait = { for: goal, status: 'pending', operationId: randomUUID(), deadlineAtUtcMs: budget.deadlineAtUtcMs };
+ item.waits.push({ public: structuredClone(item.handoff.wait), clockId: this.clockId, cutoff: budget.cutoff });
+ }
+ beginWait(id: string, goal: Exclude, budget = new HandoffBudget(30, this.clock)): Handoff {
+ if (!['delivered', 'acknowledged'].includes(goal)) fail('invalid_wait_for', 2);
+ return this.lock(() => {
+ const data = this.read(), item = this.usable(data, id); this.addWait(item, goal, budget);
+ const h = item.handoff;
+ h.nextActions = ['reconcile'];
+ if ((goal === 'acknowledged' && h.ack.status === 'acknowledged') || (goal === 'delivered' && h.observation.injectionObserved)) h.wait.status = 'satisfied';
+ else if (budget.seconds <= 5) { h.wait.status = 'failed'; h.wait.reason = 'insufficient_budget'; }
+ else if (goal === 'delivered' && item.deliverySupported && item.binding.generation !== null && item.clockId === this.clockId && h.submission.status === 'submitted') {
+ h.nextActions = ['keep_waiting', 'reconcile', 'stop_waiting'];
+ }
+ else if (goal === 'acknowledged' && item.capabilityHash && !item.revoked && item.capabilityClockId === this.clockId &&
+ this.clock.monotonic() - item.preparedMono < HANDOFF_LIMITS.capabilityMs && item.binding.generation !== null && h.submission.status === 'submitted') {
+ h.ack.status = 'pending'; h.nextActions = ['keep_waiting', 'reconcile', 'stop_waiting'];
+ } else { h.wait.status = 'unsupported'; h.wait.reason = 'evidence_unsupported'; }
+ item.waits.at(-1)!.public = structuredClone(h.wait);
+ this.write(data); return this.snapshot(item);
+ });
+ }
+ private selectWait(item: RecordIntent, operationId?: string): StoredWait {
+ if (operationId !== undefined && !canonicalId(operationId)) fail('invalid_wait_operation_id', 2);
+ const wait = operationId === undefined ? item.waits.at(-1) : item.waits.find(value => value.public.operationId === operationId);
+ if (!wait) fail('handoff_wait_unknown');
+ return wait;
+ }
+ private operationSnapshot(item: RecordIntent, wait: StoredWait): Handoff {
+ const h = this.snapshot(item); h.wait = structuredClone(wait.public);
+ // Actions of the newest operation cannot authorize an older operation.
+ h.nextActions = ['reconcile']; return validateHandoff(h);
+ }
+ operationStatus(id: string, operationId: string): Handoff {
+ const data = this.read(), item = this.usable(data, id);
+ return this.operationSnapshot(item, this.selectWait(item, operationId));
+ }
+ recordWait(id: string, status: Exclude, reason?: WaitReason, operationId?: string): Handoff {
+ return this.lock(() => {
+ const data = this.read(), item = this.usable(data, id), wait = this.selectWait(item, operationId);
+ if (wait.public.status !== 'pending') fail('handoff_wait_terminal');
+ if (status === 'timed_out_unknown' && (wait.clockId !== this.clockId || this.clock.monotonic() < wait.cutoff)) fail('receipt_order_unprovable');
+ wait.public.status = status; if (reason) wait.public.reason = reason;
+ if (status === 'timed_out_unknown' && !item.handoff.observation.injectionObserved && item.handoff.ack.status !== 'acknowledged') item.handoff.state = 'timed_out_unknown';
+ item.handoff.nextActions = ['reconcile'];
+ item.handoff.wait = structuredClone(item.waits.at(-1)!.public);
+ validateHandoff({ ...item.handoff, wait: wait.public });
+ this.write(data); return operationId !== undefined ? this.operationSnapshot(item, wait) : this.snapshot(item);
+ });
+ }
+ private commitAck(item: RecordIntent, assurance: 'operator_confirmed' | 'token_possession'): void {
+ const h = item.handoff, origin = item.waits[0];
+ // Classify every pending ACK operation against one observed instant. A
+ // newer wait must not hide an older operation's deadline or clock loss.
+ const now = this.clock.monotonic();
+ const pending = item.waits.filter(wait => wait.public.status === 'pending' && wait.public.for === 'acknowledged');
+ if (pending.some(wait => wait.clockId !== this.clockId)) fail('receipt_order_unprovable');
+ let late = false;
+ if (origin) {
+ if (origin.public.status === 'timed_out_unknown') late = true;
+ else if (origin.clockId === this.clockId) late = now >= origin.cutoff;
+ else fail('receipt_order_unprovable');
+ }
+ h.state = 'acknowledged'; h.ack = { status: 'acknowledged', assurance, receivedAtUtcMs: Math.floor(this.clock.utc()), late };
+ for (const wait of pending) wait.public.status = now < wait.cutoff ? 'satisfied' : 'timed_out_unknown';
+ if (item.waits.length) h.wait = structuredClone(item.waits.at(-1)!.public);
+ h.nextActions = ['reconcile'];
+ }
+ confirm(raw: string | Uint8Array): Handoff {
+ const proof = validateConfirmation(raw);
+ return this.lock(() => {
+ const data = this.read(), item = this.usable(data, proof.correlationId);
+ if (data.epoch !== proof.ledgerEpoch || item.binding.generation !== proof.targetGeneration || item.handoff.submission.status !== 'submitted') fail('confirmation_binding_invalid');
+ if (item.handoff.ack.status === 'acknowledged') return this.snapshot(item);
+ this.commitAck(item, 'operator_confirmed');
+ this.write(data); return this.snapshot(item);
+ });
+ }
+ // Caller must already validate a compatible private receipt-only producer.
+ // This returns authority for authorized private effect input only; never argv,
+ // environment, public output or persistence. The raw value is not recoverable.
+ mintCapability(id: string, bootstrap: { privateConsumerValidated: true; compatibleProducerValidated: true }): string {
+ if (bootstrap.privateConsumerValidated !== true || bootstrap.compatibleProducerValidated !== true) fail('receipt_bootstrap_unsupported');
+ return this.lock(() => {
+ const data = this.read(), item = this.usable(data, id);
+ if (item.phase !== 'prepared' || item.binding.generation === null || item.capabilityHash || item.clockId !== this.clockId ||
+ this.clock.monotonic() - item.preparedMono >= HANDOFF_LIMITS.capabilityMs) fail('receipt_authority_unavailable');
+ const capability = randomBytes(32).toString('base64url'); item.capabilityHash = payloadDigest(capability);
+ item.capabilityClockId = this.clockId;
+ item.handoff.ack.status = 'pending'; item.clockId = this.clockId; this.write(data); return capability;
+ });
+ }
+ acceptReceipt(raw: string | Uint8Array): Handoff {
+ const proof = validateReceipt(raw);
+ return this.lock(() => {
+ const data = this.read(), item = this.usable(data, proof.correlationId);
+ const expected = Buffer.from(item.capabilityHash ?? '0'.repeat(64), 'hex'), actual = Buffer.from(payloadDigest(proof.capability), 'hex');
+ const authenticated = timingSafeEqual(expected, actual);
+ if (!authenticated || !item.capabilityHash || item.revoked || data.epoch !== proof.ledgerEpoch ||
+ item.binding.generation !== proof.targetGeneration || item.handoff.submission.status !== 'submitted') fail('receipt_authentication_failed');
+ if (item.receiptId) { if (item.receiptId !== proof.receiptId) fail('receipt_already_committed'); return this.snapshot(item); }
+ if (item.handoff.ack.status === 'acknowledged') fail('receipt_already_committed');
+ if (item.capabilityClockId !== this.clockId || this.clock.monotonic() - item.preparedMono >= HANDOFF_LIMITS.capabilityMs) fail('receipt_authority_expired');
+ this.commitAck(item, 'token_possession'); item.receiptId = proof.receiptId;
+ this.write(data); return this.snapshot(item);
+ });
+ }
+ revoke(id: string): void { this.lock(() => { const data = this.read(), item = this.usable(data, id); item.revoked = true; this.write(data); }); }
+ quarantine(): void { this.lock(() => { const data = this.read(); data.quarantined = true; this.write(data); }); }
+ prune(): void {
+ this.lock(() => {
+ const data = this.read();
+ for (const item of Object.values(data.records)) if (this.clock.utc() - item.preparedUtc >= HANDOFF_LIMITS.detailMs) {
+ item.expired = true; item.phase = 'finished'; item.handoff = this.snapshot(item); item.waits = []; item.reserve = 0;
+ delete item.capabilityHash; delete item.capabilityClockId; delete item.receiptId;
+ }
+ this.write(data);
+ });
+ }
+}
diff --git a/src/handoff-operations.ts b/src/handoff-operations.ts
new file mode 100644
index 0000000..aa49b4e
--- /dev/null
+++ b/src/handoff-operations.ts
@@ -0,0 +1,160 @@
+// Opt-in integration only. Default CLI imports this without opening storage or
+// starting an observer/collector. Production receipt bootstrap is unqualified.
+import { homedir, hostname } from 'node:os';
+import { dirname, isAbsolute, join, parse, resolve } from 'node:path';
+import { closeSync, constants, fstatSync, lstatSync, mkdirSync, openSync, readSync, realpathSync } from 'node:fs';
+import { HandoffLedger, payloadDigest, type HandoffBinding } from './handoff-ledger.js';
+import { canonicalId, HANDOFF_LIMITS, HandoffBudget, parseWaitTimeout, type Handoff, type WaitFor } from './handoff.js';
+import { checkMessage, send, type SendContext, type SendOptions } from './send.js';
+import { Refusal } from './discovery.js';
+import { UnknownOutcome } from './process.js';
+
+export type HandoffOptions = { correlationId?: string; requestAck?: boolean; observeDelivery?: boolean;
+ waitFor?: Exclude; seconds?: number; ledgerPath?: string; payload?: string; budget?: HandoffBudget; resultOverhead?: number };
+export type OperationResult = { value: Record; exitCode: number };
+export function ledgerPath(): string {
+ const path = process.env.SESSION_PEER_HANDOFF_HOME ?? join(homedir(), '.local', 'state', 'session-peer-ts', 'handoff');
+ if (!isAbsolute(path)) throw new Refusal('invalid_handoff_ledger', 2);
+ return resolve(path);
+}
+export function privateMessage(path: string): string {
+ const before = lstatSync(path);
+ if (!before.isFile() || before.isSymbolicLink() || before.size > 4_100_000 ||
+ (process.platform !== 'win32' && (before.uid !== process.getuid?.() || (before.mode & 0o777) !== 0o600))) throw new Refusal('private_message_file_untrusted', 2);
+ const fd = openSync(path, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
+ try {
+ const actual = fstatSync(fd);
+ if (!actual.isFile() || actual.ino !== before.ino || actual.dev !== before.dev || actual.size !== before.size) throw new Refusal('private_message_file_untrusted', 2);
+ const buffer = Buffer.alloc(4_100_001); let length = 0;
+ while (length < buffer.length) { const count = readSync(fd, buffer, length, buffer.length - length, null); if (!count) break; length += count; }
+ const bytes = buffer.subarray(0, length); if (bytes.length > 4_100_000) throw new Refusal('input_too_large');
+ const after = fstatSync(fd);
+ if (after.size !== actual.size || after.mtimeMs !== actual.mtimeMs || length !== actual.size) throw new Refusal('private_message_file_changed', 1);
+ try { return new TextDecoder('utf-8', { fatal: true }).decode(bytes); } catch { throw new Refusal('invalid_utf8'); }
+ } finally { closeSync(fd); }
+}
+function binding(context: SendContext, message: string, destination = hostname()): HandoffBinding {
+ return { ...context, destination, payloadDigest: payloadDigest(message) };
+}
+export function correlationEnvelope(message: string, id: string): string {
+ if (!canonicalId(id)) throw new Refusal('invalid_correlation_id', 2);
+ // A dedicated inert metadata line, not an executable Reply-To extension.
+ return message + '\n\n---\nHandoff: ' + JSON.stringify({ schemaVersion: 1, correlationId: id });
+}
+class BeforeEffectRefused extends Error {}
+export async function prepareHandoff(options: SendOptions, path = ledgerPath()): Promise {
+ const ledger = new HandoffLedger(path); let handoff: Handoff | undefined;
+ const value = await send({ ...options, dryRun: true, hooks: { resolved(context) {
+ handoff = ledger.prepare(binding(context, options.message));
+ } } });
+ return { value: { ...value, handoff }, exitCode: 0 };
+}
+export async function handoffSend(options: SendOptions, handoffOptions: HandoffOptions): Promise {
+ const ledger = new HandoffLedger(handoffOptions.ledgerPath ?? ledgerPath());
+ const budget = handoffOptions.budget ?? new HandoffBudget(handoffOptions.seconds ?? 30);
+ let handoff: Handoff | undefined, snapshot: Record | undefined;
+ let original: HandoffBinding | undefined, fenced = false, knownNative: Record | undefined;
+ const finish = (value: Record, exitCode: number): OperationResult => ({ value: { ...value, ...(handoff ? { handoff } : {}) }, exitCode });
+ try {
+ const value = await send({ ...options, hooks: {
+ resolved(context, native) {
+ snapshot = native; original = binding(context, handoffOptions.payload ?? options.message);
+ if (Buffer.byteLength(JSON.stringify(native)) + HANDOFF_LIMITS.publicFrame + (handoffOptions.resultOverhead ?? 1024) > HANDOFF_LIMITS.outerFrame) throw new Refusal('handoff_result_too_large',1);
+ // Account for the fixed correlation envelope before allocating an intent.
+ checkMessage(correlationEnvelope(options.message, '00000000-0000-4000-8000-000000000000'), context.agent === 'codex');
+ handoff = ledger.prepare(original, { correlationId: handoffOptions.correlationId,
+ requestAck: handoffOptions.requestAck || handoffOptions.waitFor === 'acknowledged',
+ observeDelivery: handoffOptions.observeDelivery || handoffOptions.waitFor === 'delivered',
+ waitFor: handoffOptions.waitFor ?? 'none', budget, dryRun: options.dryRun });
+ if (handoff.state === 'refused') throw new BeforeEffectRefused();
+ return correlationEnvelope(options.message, handoff.correlationId);
+ },
+ beforeEffect() { budget.beforeEffect(); handoff = ledger.commitEffect(handoff!.correlationId, original!, budget); fenced = true; },
+ timeoutMs() { budget.beforeEffect(); return Math.max(1, Math.floor(budget.observationRemainingMs())); },
+ } });
+ knownNative = value;
+ if (!options.dryRun) handoff = ledger.recordSubmission(handoff!.correlationId, 'submitted');
+ return finish(value, 0);
+ } catch (error) {
+ if (!handoff || !snapshot) throw error;
+ if (knownNative) {
+ // An I/O failure after positive native acceptance cannot erase that fact.
+ handoff = { ...handoff, state: 'submitted', submission: { status: 'submitted' }, nextActions: ['reconcile'] };
+ if (handoffOptions.waitFor) {
+ if (handoff.wait.status === 'pending') handoff.wait = { ...handoff.wait, status: 'failed', reason: 'evidence_failed' };
+ return finish({ ...knownNative, ok: false, error: 'handoff_persistence_failed', retryAllowed: false }, 1);
+ }
+ return finish({ ...knownNative, handoffWarning: 'persistence_failed', retryAllowed: false }, 0);
+ }
+ const uncertain = error instanceof UnknownOutcome;
+ if (fenced) {
+ try { handoff = ledger.recordSubmission(handoff.correlationId, uncertain ? 'unknown' : 'refused'); }
+ catch { const state = ledger.status(handoff.correlationId); if ('handoff' in state) handoff = state.handoff; }
+ }
+ if (!fenced && original && handoff.state !== 'refused') {
+ try { handoff = ledger.refusePrepared(handoff.correlationId, original); } catch { /* Keep the last verified facts; never retry. */ }
+ }
+ const code = error instanceof BeforeEffectRefused ? handoff.wait.reason ?? 'evidence_unsupported' : error instanceof Refusal ? error.code : uncertain ? 'outcome_unknown' : 'handoff_operation_failed';
+ return finish({ ...snapshot, ok: false, error: code, status: uncertain ? 'unknown' : 'refused', submitted: uncertain ? null : false,
+ consumptionConfirmed: false, retryAllowed: false }, error instanceof Refusal ? error.exitCode : 1);
+ }
+}
+
+// New commands are local owner operations; forwarding them through native send
+// or interpreting a receipt as a model prompt is never allowed.
+export async function handoffCommand(args: string[], readInput: (limit: number) => Promise): Promise {
+ const ack = args[0] === 'ack', operation = ack ? 'ack' : args[1];
+ if (!operation || !['init', 'prepare', 'status', 'wait', 'confirm', 'ack'].includes(operation)) throw new Refusal('unsupported_handoff_command');
+ const values = new Map(); const flags = new Set();
+ const names = ['--correlation-id', '--wait-for', '--wait-timeout', '--receipt', '--to', '--message-file', '--codex-home', '--codex-bin', '--output-format'];
+ for (let i = ack ? 1 : 2; i < args.length; i++) {
+ const token = args[i]!, equal = token.indexOf('='), name = equal < 0 ? token : token.slice(0, equal);
+ if (['--json', '--no-update-notice', '--allow-inactive-codex-home'].includes(name)) {
+ if (equal >= 0 || flags.has(name)) throw new Refusal('invalid_option'); flags.add(name); continue;
+ }
+ if (!names.includes(name) || values.has(name)) throw new Refusal('unsupported_option');
+ const value = equal < 0 ? args[++i] : token.slice(equal + 1);
+ if (value === undefined || (equal < 0 && value.startsWith('--'))) throw new Refusal('invalid_option'); values.set(name, value);
+ }
+ if (!flags.has('--json') && values.get('--output-format') !== 'json') throw new Refusal('json_output_required');
+ if (values.has('--output-format') && values.get('--output-format') !== 'json') throw new Refusal('unsupported_output_format');
+ const permitted: Record = { init: [], prepare: ['--to', '--message-file', '--codex-home', '--codex-bin'],
+ status: ['--correlation-id'], wait: ['--correlation-id', '--wait-for', '--wait-timeout'], confirm: ['--receipt'], ack: ['--receipt'] };
+ if ([...values.keys()].some(k => k !== '--output-format' && !permitted[operation]!.includes(k)) ||
+ (flags.has('--allow-inactive-codex-home') && operation !== 'prepare')) throw new Refusal('inapplicable_option');
+ const path = ledgerPath(), ledger = new HandoffLedger(path);
+ if (operation === 'init') {
+ if (process.platform === 'win32') throw new Refusal('handoff_platform_unsupported', 1);
+ const parent = dirname(path);
+ // Initialization is explicit; never follow a symlinked state prefix.
+ let current = parse(parent).root;
+ for (const component of parent.slice(current.length).split('/').filter(Boolean)) {
+ current = join(current, component);
+ try { const stat = lstatSync(current); if (!stat.isDirectory() || stat.isSymbolicLink()) throw new Refusal('handoff_storage_untrusted', 1); }
+ catch (error) { if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error; mkdirSync(current, { mode: 0o700 }); }
+ }
+ if (realpathSync(parent) !== parent) throw new Refusal('handoff_storage_untrusted', 1);
+ return { value: ledger.init(), exitCode: 0 };
+ }
+ if (operation === 'prepare') {
+ if (!values.get('--to') || !values.get('--message-file')) throw new Refusal('invalid_send_options');
+ const value = await prepareHandoff({ to: values.get('--to')!, message: privateMessage(values.get('--message-file')!),
+ home: values.get('--codex-home'), codexBin: values.get('--codex-bin'), allowInactive: flags.has('--allow-inactive-codex-home') }, path);
+ return { value: { handoff: value.value.handoff }, exitCode: 0 };
+ }
+ if (operation === 'confirm' || operation === 'ack') {
+ if (values.get('--receipt') !== '-') throw new Refusal('private_receipt_stdin_required', 2);
+ const raw = await readInput(4096);
+ return { value: { handoff: operation === 'confirm' ? ledger.confirm(raw) : ledger.acceptReceipt(raw) }, exitCode: 0 };
+ }
+ const id = values.get('--correlation-id'); if (!canonicalId(id)) throw new Refusal('invalid_correlation_id', 2);
+ const result = ledger.status(id);
+ if ('handoffQuery' in result) return { value: { ok: false, ...result }, exitCode: 1 };
+ if (operation === 'status') return { value: result, exitCode: 0 };
+ const goal = values.get('--wait-for'); if (!['delivered', 'acknowledged'].includes(goal ?? '')) throw new Refusal('invalid_wait_for', 2);
+ const budget = new HandoffBudget(parseWaitTimeout(values.get('--wait-timeout') ?? '30'));
+ const handoff = ledger.beginWait(id, goal as 'delivered' | 'acknowledged', budget);
+ // No qualified observer/receipt bootstrap exists yet. This path never polls
+ // transcripts, opens a native client or repeats a prior submission.
+ return { value: { handoff, ok: handoff.wait.status === 'satisfied' }, exitCode: handoff.wait.status === 'satisfied' ? 0 : 1 };
+}
diff --git a/src/handoff.ts b/src/handoff.ts
new file mode 100644
index 0000000..9876db5
--- /dev/null
+++ b/src/handoff.ts
@@ -0,0 +1,176 @@
+// Pure opt-in protocol helpers. Importing this module performs no filesystem IO.
+import { Refusal } from './discovery.js';
+
+export const HANDOFF_LIMITS = Object.freeze({ privateFrame: 4096, publicFrame: 8192, outerFrame: 1048576,
+ fanout: 32, aggregateFrame: 34603008, intents: 10000, journal: 33554432, waits: 64,
+ detailMs: 30 * 86400000, capabilityMs: 86400000, cleanupMs: 5000 });
+export type WaitFor = 'none' | 'delivered' | 'acknowledged';
+export type WaitStatus = 'not_requested' | 'pending' | 'satisfied' | 'timed_out_unknown' | 'stopped' | 'unsupported' | 'failed';
+export type WaitReason = 'insufficient_budget' | 'deadline_before_effect' | 'evidence_unsupported' | 'evidence_failed' | 'history_unavailable' | 'stopped_by_operator' | 'invalid_handoff';
+export type Handoff = {
+ schemaVersion: 1; correlationId: string; ledgerEpoch: string;
+ state: 'validated' | 'refused' | 'submitted' | 'delivered' | 'acknowledged' | 'unknown' | 'timed_out_unknown';
+ submission: { status: 'not_attempted' | 'submitted' | 'refused' | 'unknown' };
+ observation: { status: 'not_requested' | 'pending' | 'observed' | 'unsupported' | 'failed'; injectionObserved: boolean;
+ clientUserMessageId?: string; turn?: { id: string; status: 'running' | 'completed' | 'failed' | 'interrupted' | 'unknown' } };
+ ack: { status: 'not_requested' | 'pending' | 'acknowledged' | 'unsupported'; assurance?: 'token_possession' | 'operator_confirmed'; receivedAtUtcMs?: number; late?: boolean };
+ wait: { for: WaitFor; status: WaitStatus; operationId?: string; deadlineAtUtcMs?: number; reason?: WaitReason };
+ targetGeneration: string | null; decisionOwner: 'sender_operator';
+ retry: { allowed: false; reason: 'receiver_dedup_unavailable' };
+ nextActions: ('keep_waiting' | 'reconcile' | 'stop_waiting')[];
+};
+export type PublicHandoff = Handoff;
+export type Clock = { monotonic: () => number; utc: () => number };
+export const systemClock: Clock = { monotonic: () => performance.now(), utc: () => Date.now() };
+export function fail(code = 'invalid_handoff', exitCode = 1): never { throw new Refusal(code, exitCode); }
+export function canonicalId(value: unknown): value is string {
+ return typeof value === 'string' && /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/.test(value);
+}
+export function safeInteger(value: unknown): value is number { return typeof value === 'number' && Number.isSafeInteger(value) && value >= 0; }
+export function safeId(value: unknown, bytes = 128): value is string {
+ return typeof value === 'string' && value.length > 0 && Buffer.byteLength(value, 'utf8') <= bytes &&
+ !/[\x00-\x1f\x7f-\x9f]/.test(value) && !/[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?= this.cutoff) fail('deadline_before_effect');
+ }
+}
+
+// Recursive token parser keeps integer lexemes before JavaScript can round them.
+// Outer mode restricts new-schema subtrees only; native numeric siblings retain
+// their existing grammar. Duplicate keys/nonfinite numbers always fail closed.
+export function parseStrictJson(raw: string | Uint8Array, options: { scope?: 'private' | 'handoff' | 'query' | 'outer'; maxBytes?: number } = {}): unknown {
+ if (typeof raw === 'string' && /[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(? maximum) fail('handoff_frame_too_large');
+ let text: string;
+ try { text = new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(bytes); } catch { fail('invalid_handoff_json'); }
+ let position = 0;
+ const space = () => { while (/[\x20\t\r\n]/.test(text[position] ?? 'x')) position++; };
+ const string = (): string => {
+ const start = position++;
+ while (position < text.length) {
+ const c = text[position++];
+ if (c === '"') { try { return JSON.parse(text.slice(start, position)) as string; } catch { fail('invalid_handoff_json'); } }
+ if (c === '\\') position++;
+ }
+ fail('invalid_handoff_json');
+ };
+ const value = (strict: boolean, depth: number): unknown => {
+ if (depth > 64) fail('invalid_handoff_json');
+ space(); const c = text[position];
+ if (c === '"') return string();
+ if (c === '{') {
+ position++; space(); const result: Record = Object.create(null), keys = new Set();
+ if (text[position] === '}') { position++; return result; }
+ while (true) {
+ space(); if (text[position] !== '"') fail('invalid_handoff_json'); const key = string();
+ if (keys.has(key)) fail('invalid_handoff_json'); keys.add(key);
+ space(); if (text[position++] !== ':') fail('invalid_handoff_json');
+ result[key] = value(strict || (scope === 'outer' && depth === 0 && ['handoff', 'handoffQuery'].includes(key)), depth + 1);
+ space(); const separator = text[position++]; if (separator === '}') break; if (separator !== ',') fail('invalid_handoff_json');
+ }
+ return result;
+ }
+ if (c === '[') {
+ position++; space(); const result: unknown[] = []; if (text[position] === ']') { position++; return result; }
+ while (true) { result.push(value(strict, depth + 1)); space(); const separator = text[position++]; if (separator === ']') break; if (separator !== ',') fail('invalid_handoff_json'); }
+ return result;
+ }
+ for (const [literal, item] of [['true', true], ['false', false], ['null', null]] as const) {
+ if (text.startsWith(literal, position)) { position += literal.length; return item; }
+ }
+ const token = /^-?(?:0|[1-9][0-9]*)(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?/.exec(text.slice(position))?.[0];
+ if (!token) fail('invalid_handoff_json'); position += token.length; const number = Number(token);
+ if (!Number.isFinite(number) || (strict && (!/^(?:0|[1-9][0-9]*)$/.test(token) || !safeInteger(number)))) fail('invalid_handoff_json');
+ return number;
+ };
+ const result = value(scope !== 'outer', 0); space(); if (position !== text.length) fail('invalid_handoff_json'); return result;
+}
+export function closed(value: unknown, required: string[], optional: string[] = []): Record {
+ if (!value || typeof value !== 'object' || Array.isArray(value)) fail();
+ const row = value as Record;
+ if (required.some(key => !Object.hasOwn(row, key)) || Object.keys(row).some(key => !required.includes(key) && !optional.includes(key))) fail();
+ return row;
+}
+const member = (value: unknown, values: string[]) => typeof value === 'string' && values.includes(value);
+export function validateHandoff(value: unknown): Handoff {
+ const row = closed(value, ['schemaVersion', 'correlationId', 'ledgerEpoch', 'state', 'submission', 'observation', 'ack', 'wait', 'targetGeneration', 'decisionOwner', 'retry', 'nextActions']);
+ const sub = closed(row.submission, ['status']), obs = closed(row.observation, ['status', 'injectionObserved'], ['clientUserMessageId', 'turn']);
+ const ack = closed(row.ack, ['status'], ['assurance', 'receivedAtUtcMs', 'late']);
+ const wait = closed(row.wait, ['for', 'status'], ['operationId', 'deadlineAtUtcMs', 'reason']);
+ const retry = closed(row.retry, ['allowed', 'reason']);
+ if (row.schemaVersion !== 1 || !canonicalId(row.correlationId) || !canonicalId(row.ledgerEpoch) ||
+ !member(row.state, ['validated', 'refused', 'submitted', 'delivered', 'acknowledged', 'unknown', 'timed_out_unknown']) ||
+ !member(sub.status, ['not_attempted', 'submitted', 'refused', 'unknown']) ||
+ !member(obs.status, ['not_requested', 'pending', 'observed', 'unsupported', 'failed']) || typeof obs.injectionObserved !== 'boolean' ||
+ !member(ack.status, ['not_requested', 'pending', 'acknowledged', 'unsupported']) ||
+ !member(wait.for, ['none', 'delivered', 'acknowledged']) || !member(wait.status, ['not_requested', 'pending', 'satisfied', 'timed_out_unknown', 'stopped', 'unsupported', 'failed']) ||
+ (row.targetGeneration !== null && !safeId(row.targetGeneration, 256)) || row.decisionOwner !== 'sender_operator' ||
+ retry.allowed !== false || retry.reason !== 'receiver_dedup_unavailable' || !Array.isArray(row.nextActions) ||
+ row.nextActions.some(item => !member(item, ['keep_waiting', 'reconcile', 'stop_waiting'])) || new Set(row.nextActions).size !== row.nextActions.length) fail();
+ if (Object.hasOwn(obs, 'clientUserMessageId') && !safeId(obs.clientUserMessageId)) fail();
+ if (obs.injectionObserved !== (obs.status === 'observed') ||
+ (obs.injectionObserved && (!safeId(obs.clientUserMessageId) || row.targetGeneration === null || sub.status !== 'submitted')) ||
+ (!obs.injectionObserved && Object.hasOwn(obs, 'clientUserMessageId'))) fail();
+ if (Object.hasOwn(obs, 'turn')) {
+ const turn = closed(obs.turn, ['id', 'status']);
+ if (!obs.injectionObserved || !safeId(turn.id) || !member(turn.status, ['running', 'completed', 'failed', 'interrupted', 'unknown'])) fail();
+ }
+ if (ack.status === 'acknowledged') {
+ if (!member(ack.assurance, ['token_possession', 'operator_confirmed']) || !safeInteger(ack.receivedAtUtcMs) || typeof ack.late !== 'boolean' || row.state !== 'acknowledged' || sub.status !== 'submitted' || row.targetGeneration === null) fail();
+ } else if (['assurance', 'receivedAtUtcMs', 'late'].some(key => Object.hasOwn(ack, key))) fail();
+ if (row.targetGeneration === null && (ack.status === 'pending' || ack.status === 'acknowledged' || wait.status === 'pending' || wait.status === 'satisfied')) fail();
+ const states: Record = { validated: 'not_attempted', refused: 'refused', submitted: 'submitted', delivered: 'submitted', acknowledged: 'submitted', unknown: 'unknown' };
+ if ((row.state !== 'timed_out_unknown' && sub.status !== states[row.state as string]) ||
+ (row.state === 'timed_out_unknown' && !['submitted', 'unknown'].includes(sub.status as string)) ||
+ (row.state === 'delivered' && !obs.injectionObserved) || (row.state === 'acknowledged' && ack.status !== 'acknowledged')) fail();
+ if (wait.for === 'none') {
+ if (wait.status !== 'not_requested' || Object.keys(wait).length !== 2) fail();
+ } else if (wait.status === 'not_requested' || !canonicalId(wait.operationId) || !safeInteger(wait.deadlineAtUtcMs)) fail();
+ if (Object.hasOwn(wait, 'reason') && !member(wait.reason, ['insufficient_budget', 'deadline_before_effect', 'evidence_unsupported', 'evidence_failed', 'history_unavailable', 'stopped_by_operator', 'invalid_handoff'])) fail();
+ if (wait.status === 'stopped' && wait.reason !== 'stopped_by_operator') fail();
+ if (wait.status === 'satisfied' && (wait.for === 'delivered' ? !obs.injectionObserved : ack.status !== 'acknowledged')) fail();
+ if (row.nextActions.includes('keep_waiting') && (row.targetGeneration === null || wait.status === 'unsupported' ||
+ (wait.for === 'delivered' ? obs.status !== 'pending' : wait.for !== 'acknowledged' || ack.status !== 'pending') ||
+ !['submitted', 'unknown'].includes(sub.status as string))) fail();
+ if (row.nextActions.includes('stop_waiting') && wait.status !== 'pending') fail();
+ if (Buffer.byteLength(JSON.stringify(row), 'utf8') > HANDOFF_LIMITS.publicFrame) fail('handoff_frame_too_large');
+ return structuredClone(row) as Handoff;
+}
+export type Receipt = { schemaVersion: 1; kind: 'receipt'; ledgerEpoch: string; correlationId: string; targetGeneration: string; receiptId: string; capability: string };
+export type Confirmation = { schemaVersion: 1; ledgerEpoch: string; correlationId: string; targetGeneration: string; confirmed: true };
+export function validateHandoffQuery(value: unknown): { schemaVersion: 1; correlationId: string; status: 'unknown'; context: 'ledger_missing' | 'ledger_corrupt' | 'id_unknown'; retry: { allowed: false; reason: 'history_unavailable' } } {
+ const row = closed(value, ['schemaVersion', 'correlationId', 'status', 'context', 'retry']);
+ const retry = closed(row.retry, ['allowed', 'reason']);
+ if (row.schemaVersion !== 1 || !canonicalId(row.correlationId) || row.status !== 'unknown' ||
+ !member(row.context, ['ledger_missing', 'ledger_corrupt', 'id_unknown']) || retry.allowed !== false || retry.reason !== 'history_unavailable') fail();
+ return structuredClone(row) as ReturnType;
+}
+export function validateReceipt(raw: string | Uint8Array): Receipt {
+ const row = closed(parseStrictJson(raw), ['schemaVersion', 'kind', 'ledgerEpoch', 'correlationId', 'targetGeneration', 'receiptId', 'capability']);
+ if (row.schemaVersion !== 1 || row.kind !== 'receipt' || !canonicalId(row.ledgerEpoch) || !canonicalId(row.correlationId) || !canonicalId(row.receiptId) || !safeId(row.targetGeneration, 256) ||
+ typeof row.capability !== 'string' || !/^[A-Za-z0-9_-]{43}$/.test(row.capability) || Buffer.from(row.capability, 'base64url').length !== 32 || Buffer.from(row.capability, 'base64url').toString('base64url') !== row.capability) fail('invalid_receipt');
+ return row as Receipt;
+}
+export function validateConfirmation(raw: string | Uint8Array): Confirmation {
+ const row = closed(parseStrictJson(raw), ['schemaVersion', 'ledgerEpoch', 'correlationId', 'targetGeneration', 'confirmed']);
+ if (row.schemaVersion !== 1 || !canonicalId(row.ledgerEpoch) || !canonicalId(row.correlationId) || !safeId(row.targetGeneration, 256) || row.confirmed !== true) fail('invalid_confirmation');
+ return row as Confirmation;
+}
diff --git a/src/help.ts b/src/help.ts
index 1bfdc15..a2d222f 100644
--- a/src/help.ts
+++ b/src/help.ts
@@ -17,6 +17,16 @@ Transport:
known online peers use their MagicDNS name, known offline peers are refused.
--no-update-notice Suppress the opt-in cached update notice
-h, --help Show command help`;
+ if (command === 'handoff' || command === 'ack') return `Usage: session-peer handoff init|prepare|status|wait|confirm --json
+ init Initialize private POSIX sender ledger explicitly
+ prepare --to TARGET --message-file FILE Reserve a fresh ID without submitting (FILE mode 0600)
+ status --correlation-id UUID Inspect retained evidence; never submit again
+ wait --correlation-id UUID --wait-for delivered|acknowledged --wait-timeout SECONDS
+ confirm --receipt - Read private operator-confirmation JSON from stdin
+ ack --receipt - Record a qualified private receipt; never prompt an agent
+SESSION_PEER_HANDOFF_HOME selects an absolute private ledger directory.
+Production automated receipt bootstrap and Windows ledger are unsupported.
+Missing history never authorizes retry. Secrets must never be passed through argv.`;
if (command === 'list') return `Usage: session-peer list [options]
List reachable Claude and saved Codex sessions. Listing is not send authorization.
--agent claude|codex Filter one agent (default: both)
@@ -47,6 +57,12 @@ Notices on other commands are opt-in: SESSION_PEER_UPDATE_NOTICE=1.`;
TARGET: Claude name/PID, claude:NAME/PID, codex:UUID, or session-peer reply URI.
Omit the body or use - for UTF-8 stdin. Only one body source is permitted.
Unicode names use exact Unicode 14.0.0 default casefold; collisions require PID.
+ --correlation-id UUID Use an already-prepared private handoff ID (opt-in)
+ --request-ack Add handoff metadata; automated ACK currently unsupported
+ --observe-delivery Request optional body-free observation (qualified routes only)
+ --wait-for delivered|acknowledged Refuse before submission if required evidence is unsupported
+ --wait-timeout SECONDS Total budget 1..60 (default 30, includes 5 s cleanup)
+ --message-file FILE Private UTF-8 input file (POSIX mode 0600)
--dry-run Validate without submission
--codex-home HOME Explicit Codex home; otherwise select unique live writer
--codex-bin PATH Native Codex executable
@@ -66,6 +82,7 @@ Usage: session-peer [options]
send Submit one guarded message
doctor Inspect read-only metadata and capabilities
update --check Check npm for a newer version (no self-update)
+ handoff / ack Opt-in private evidence operations (see handoff --help)
--version Print exact runtime version
--help, -h Show help; --help for details
${common}
diff --git a/src/output.ts b/src/output.ts
index afea0b0..089075d 100644
--- a/src/output.ts
+++ b/src/output.ts
@@ -47,7 +47,12 @@ export function renderOutput(value: Record | Record;
+ lines.push(`Handoff: ${safe(h.correlationId)} (${safe(h.state)})`);
+ for (const key of ['observation', 'ack', 'wait']) lines.push(`${key}: ${safe(JSON.stringify(h[key]))}`);
}
if (value.sshTransport && typeof value.sshTransport === 'object') {
const transport = value.sshTransport as Record;
diff --git a/src/send.ts b/src/send.ts
index f251ab3..ffede82 100644
--- a/src/send.ts
+++ b/src/send.ts
@@ -1,11 +1,23 @@
+import { createHash } from 'node:crypto';
import { casefold } from './casefold.js';
import { createConnection } from 'node:net';
+import { closeSync, constants, fstatSync, lstatSync, openSync, type BigIntStats } from 'node:fs';
+import { homedir } from 'node:os';
+import { join } from 'node:path';
import { claude, Refusal, sqliteHome } from './discovery.js';
import { executable, run, UnknownOutcome } from './process.js';
import { HomeRefusal, resolveWriter, revalidateWriter, uuid, type HomeResolution } from './writer.js';
import { postWindowsPipe } from './windows.js';
-export type SendOptions = { to: string; home?: string; codexBin?: string; message: string; dryRun?: boolean; allowInactive?: boolean };
+export type SendContext = { agent: 'claude' | 'codex'; target: string; home?: string; generation: string | null; writerIdentity?: string };
+export type SendHooks = {
+ // Only explicitly opted-in callers resolve this additional context. Default
+ // send output, inspection count and native execution remain unchanged.
+ resolved: (context: SendContext, snapshot: Record) => Promise | string | void;
+ beforeEffect?: () => Promise | void;
+ timeoutMs?: () => number;
+};
+export type SendOptions = { to: string; home?: string; codexBin?: string; message: string; dryRun?: boolean; allowInactive?: boolean; hooks?: SendHooks };
export class CodexUnknownOutcome extends UnknownOutcome {
constructor(readonly codexHomeResolution: HomeResolution) { super(); }
}
@@ -18,11 +30,12 @@ export function checkMessage(text: string, codex = false): void {
if (!text.trim() || text.includes('\0') || [...text].length > 1_000_000 ||
(codex && Buffer.byteLength(text, 'utf8') > 32768)) throw new Refusal('invalid_message', 2);
}
-export async function postSocket(path: string, text: string): Promise {
+export async function postSocket(path: string, text: string, timeout?: number): Promise {
return new Promise((resolve, reject) => {
let attempted = false, finished = false;
+ const startedAt = performance.now();
const socket = createConnection(path);
- const timer = setTimeout(() => finish(new Error('timeout')), 10000);
+ const timer = setTimeout(() => finish(new Error('timeout')), timeout ?? 10000);
const finish = (error?: Error) => {
if (finished) return;
finished = true; clearTimeout(timer); socket.destroy();
@@ -35,13 +48,39 @@ export async function postSocket(path: string, text: string): Promise {
socket.end(JSON.stringify({ type: 'user', message: { role: 'user', content: text } }) + '\n', () => {
// Write completion is submission only, never consumption. Match Python's drain window.
clearTimeout(timer);
- const drain = setTimeout(() => finish(), 2000);
+ const drain = setTimeout(() => finish(), timeout === undefined ? 2000 : Math.min(2000, Math.max(0, timeout - (performance.now() - startedAt))));
socket.once('close', () => { clearTimeout(drain); finish(); });
socket.once('data', () => { clearTimeout(drain); finish(); });
});
});
});
}
+// A private continuity fingerprint, not proof of a native incarnation. Do not
+// read or persist registry contents (which can include authentication material).
+function claudeBindingIdentity(row: Record): string {
+ const stable = (s: BigIntStats) => [s.dev,s.ino,s.uid,s.size,s.mtimeNs,s.ctimeNs,s.birthtimeNs].map(String);
+ const same = (a: BigIntStats,b: BigIntStats) => JSON.stringify(stable(a))===JSON.stringify(stable(b));
+ const directory=process.env.CLAUDE_CONFIG_DIR || process.env.ANTHROPIC_CONFIG_DIR || join(homedir(),'.claude');
+ const path=join(directory,'sessions',String(row.pid)+'.json');
+ try {
+ const before=lstatSync(path,{bigint:true});
+ if(!before.isFile() || before.size>1024n*1024n)throw new Error();
+ const fd=openSync(path,constants.O_RDONLY | (process.platform==='win32'?0:constants.O_NOFOLLOW | constants.O_NONBLOCK));
+ let metadata:string[];
+ try {
+ const opened=fstatSync(fd,{bigint:true});
+ if(!opened.isFile() || !same(before,opened) || !same(opened,lstatSync(path,{bigint:true})))throw new Error();
+ metadata=stable(opened);
+ } finally {closeSync(fd);}
+ let socket:string[]|null=null;
+ if(process.platform!=='win32'){
+ const file=lstatSync(String(row.socket),{bigint:true});
+ if(!file.isSocket())throw new Error();
+ socket=stable(file);
+ }
+ return createHash('sha256').update(JSON.stringify({pid:row.pid,name:row.name,socketPath:row.socket,metadata,socket})).digest('hex');
+ } catch {throw new Refusal('target_changed',1);}
+}
export async function send(options: SendOptions): Promise> {
checkMessage(options.message, options.to.startsWith('codex:'));
const base = { ok: true, submitted: !options.dryRun, consumptionConfirmed: false,
@@ -56,14 +95,34 @@ export async function send(options: SendOptions): Promise B -> A.
+ const proof = JSON.parse(selection.evidence) as { fingerprints: { home: string; database: string; writer: string }[] };
+ const selected = proof.fingerprints.find(item => item.home === home);
+ const identity = selected?.writer;
+ const live = selection.resolution.candidates.find(item => item.codexHome === home)?.activity === 'live_writer';
+ if (!identity) throw new HomeRefusal('active_writer_unverified', selection.resolution);
+ const generation = live ? createHash('sha256').update(identity).digest('hex') : null;
+ const body = await options.hooks.resolved({ agent: 'codex', target: id, home, generation,
+ // Inactive evidence still binds a prepared intent across invocations. This
+ // private fingerprint is not a native incarnation or public generation.
+ writerIdentity: generation ?? createHash('sha256').update(JSON.stringify(selected)).digest('hex') }, result);
+ if (body !== undefined) { checkMessage(body, true); options = { ...options, message: body }; result.chars = [...body].length; }
+ }
if (options.dryRun) return result;
await revalidateWriter(selection, options.home, id, options.allowInactive);
+ if (options.hooks) {
+ const currentStorage = sqliteHome(home);
+ if (currentStorage !== 'default') throw new HomeRefusal(currentStorage === 'configured' ? 'unsupported_codex_sqlite_home' : 'codex_config_unreadable', selection.resolution);
+ }
// `--message=` keeps a body starting with `-` from being parsed as an option.
// An inherited CODEX_SQLITE_HOME would queue outside the validated home.
// Windows environment names are case-insensitive.
const inherited = Object.fromEntries(Object.entries(process.env).filter(([key]) => key.toUpperCase() !== 'CODEX_SQLITE_HOME'));
+ await options.hooks?.beforeEffect?.();
const done = await run(binary, ['queue', '--thread', id, `--message=${options.message}`],
- { env: { ...inherited, CODEX_HOME: home }, timeout: 30000 });
+ { env: { ...inherited, CODEX_HOME: home }, timeout: options.hooks?.timeoutMs?.() ?? 30000 });
if (!done.spawned) throw new HomeRefusal('native_spawn_failed', selection.resolution);
if (done.interrupted || done.code !== 0) throw new CodexUnknownOutcome(selection.resolution);
const queuedId = queueId(done.stdout, id);
@@ -81,11 +140,20 @@ export async function send(options: SendOptions): Promise { let data = ''; s.on('data', c => data += c); s.on('end', () => { messages.push(data); s.end(); }); });
+ server.listen(socket); await once(server, 'listening');
+ t.after(async () => { await new Promise(ok => server.close(() => ok())); rmSync(path, { recursive: true }); });
+ writeFileSync(join(path, '.claude/sessions', `${process.pid}.json`), JSON.stringify({ pid: process.pid, name: null, messagingSocketPath: socket }));
+ const file = join(path, 'message'); writeFileSync(file, 'fixture body', { mode: 0o600 });
+ const env: NodeJS.ProcessEnv = { ...process.env, HOME: path, USERPROFILE: path, CLAUDE_CONFIG_DIR: join(path, '.claude'), CODEX_HOME: '', CODEX_THREAD_ID: '', CODEX_SESSION_ID: '', SESSION_PEER_TAILSCALE: 'off', SESSION_PEER_UPDATE_NOTICE: '', SESSION_PEER_HANDOFF_HOME: join(path, 'state', 'handoff') };
+ async function invoke(args: string[], input = '', closeInput = true, timeout = 10000) { const child = spawn(process.execPath, [cli, ...args, '--json'], { env }); let stdout = ''; child.stdout.on('data', c => stdout += c); child.stderr.resume(); if (closeInput) child.stdin.end(input); else child.stdin.write(input); const timer = setTimeout(() => child.kill('SIGKILL'), timeout); const [code, signal] = await once(child, 'close'); clearTimeout(timer); assert.equal(signal, null); return { code, value: JSON.parse(stdout) }; }
+ return { path, env, messages, file, invoke, send: ['send', '--to', String(process.pid), '--message-file', file, '--no-from', '--no-reply-to'] };
+}
+test('opt-out untouched; explicit init/prepare/status/dry-run/single effect fence', { skip: process.platform === 'win32' }, async t => {
+ const f = await fixture(t);
+ const plain = await f.invoke([...f.send, '--dry-run']); assert.equal(plain.code, 0); assert.equal(plain.value.handoff, undefined); assert.equal(plain.value.target.name, null);
+ const missing = await f.invoke(['handoff','status','--correlation-id','11111111-1111-4111-8111-111111111111']); assert.equal(missing.code, 1); assert.equal(missing.value.handoffQuery.context,'ledger_missing'); assert.equal(missing.value.submitted,undefined);
+ assert.equal((await f.invoke(['handoff','init'])).code,0);
+ const prepare = await f.invoke(['handoff','prepare','--to',String(process.pid),'--message-file',f.file]); assert.equal(prepare.code,0); const id = prepare.value.handoff.correlationId;
+ const dry = await f.invoke([...f.send,'--correlation-id',id,'--dry-run']); assert.equal(dry.code,0); assert.equal(dry.value.handoff.submission.status,'not_attempted'); assert.equal(f.messages.length,0);
+ const sent = await f.invoke([...f.send,'--correlation-id',id]); assert.equal(sent.code,0); assert.equal(sent.value.status,'posted'); assert.equal(sent.value.submitted,true); assert.equal(sent.value.handoff.submission.status,'submitted'); assert.equal(sent.value.handoff.ack.status,'not_requested'); assert.equal(f.messages.length,1); assert.match(f.messages[0]!,new RegExp(id));
+ const duplicate = await f.invoke([...f.send,'--correlation-id',id]); assert.equal(duplicate.code,1); assert.equal(duplicate.value.error,'handoff_already_attempted'); assert.equal(f.messages.length,1);
+ const status = await f.invoke(['handoff','status','--correlation-id',id]); assert.equal(status.code,0); assert.equal(status.value.handoff.submission.status,'submitted'); assert.equal(f.messages.length,1);
+});
+test('unsupported required ACK refuses before effect; best-effort sends once without claiming ACK', { skip: process.platform === 'win32' }, async t => {
+ const f = await fixture(t); await f.invoke(['handoff','init']);
+ const refused = await f.invoke([...f.send,'--wait-for','acknowledged']); assert.equal(refused.code,1); assert.equal(refused.value.submitted,false); assert.equal(refused.value.handoff.wait.status,'unsupported'); assert.equal(f.messages.length,0);
+ const best = await f.invoke([...f.send,'--request-ack']); assert.equal(best.code,0); assert.equal(best.value.submitted,true); assert.equal(best.value.handoff.ack.status,'unsupported'); assert.equal(f.messages.length,1);
+ const query = await f.invoke(['handoff','wait','--correlation-id',best.value.handoff.correlationId,'--wait-for','acknowledged']); assert.equal(query.code,1); assert.equal(query.value.handoff.wait.status,'unsupported'); assert.equal(query.value.submitted,undefined); assert.equal(f.messages.length,1);
+});
+test('syntax/private inputs reject without minting authority or submitting', { skip: process.platform === 'win32' }, async t => {
+ const f = await fixture(t); await f.invoke(['handoff','init']);
+ for (const seconds of ['01','1.0','1e0','+1','61']) { const r = await f.invoke([...f.send,'--wait-timeout',seconds]); assert.equal(r.code,2); assert.equal(r.value.error,'invalid_wait_timeout'); }
+ const small = await f.invoke([...f.send,'--request-ack','--wait-timeout','5']); assert.equal(small.code,1); assert.equal(small.value.submitted,false);
+ const remote = await f.invoke([...f.send,'--request-ack','--host','fixture']); assert.equal(remote.code,1); assert.equal(remote.value.error,'remote_handoff_unsupported');
+ const uri = await f.invoke(['send','--to','session-peer://v1/reply?agent=claude&session=fixture&transport=ssh&host=worker','--request-ack','--message','fixture','--no-from','--no-reply-to']); assert.equal(uri.code,1); assert.equal(uri.value.error,'remote_handoff_unsupported');
+ const receipt = await f.invoke(['ack','--receipt','-'], ' '.repeat(4097)); assert.equal(receipt.code,2); assert.equal(receipt.value.error,'input_too_large'); assert.equal(receipt.value.submitted,undefined);
+ const bomReceipt = {schemaVersion:1,kind:'receipt',ledgerEpoch:'11111111-1111-4111-8111-111111111111',correlationId:'11111111-1111-4111-8111-111111111111',targetGeneration:'fixture',receiptId:'22222222-2222-4222-8222-222222222222',capability:Buffer.alloc(32).toString('base64url')};
+ const bom = await f.invoke(['ack','--receipt','-'], '\ufeff'+JSON.stringify(bomReceipt));assert.equal(bom.value.error,'invalid_handoff_json');
+ assert.equal(f.messages.length,0);
+});
+
+test('init refuses a symlinked parent before creating a directory through it', { skip: process.platform === 'win32' }, async t => {
+ const f = await fixture(t); const external = join(f.path, 'external'); mkdirSync(external); symlinkSync(external, join(f.path,'link'));
+ const child = spawn(process.execPath, [cli,'handoff','init','--json'], {env:{...process.env,SESSION_PEER_HANDOFF_HOME:join(f.path,'link','new','handoff')}}); let stdout='';child.stdout.on('data',c=>stdout+=c);child.stderr.resume();child.stdin.end();const [code]=await once(child,'close');assert.equal(code,1);assert.equal(JSON.parse(stdout).error,'handoff_storage_untrusted');assert.equal(existsSync(join(external,'new')),false);
+});
+
+test('positive native acceptance survives a subsequent durable-record I/O failure', { skip: process.platform === 'win32' }, async t => {
+ const f = await fixture(t); await f.invoke(['handoff','init']);
+ const fs = await import('node:fs'), { syncBuiltinESMExports } = await import('node:module');
+ const { handoffSend } = await import('../dist/handoff-operations.js');
+ const { HandoffLedger } = await import('../dist/handoff-ledger.js');
+ const oldDir = process.env.CLAUDE_CONFIG_DIR; process.env.CLAUDE_CONFIG_DIR = join(f.path,'.claude');
+ const rename = fs.default.renameSync; let writes = 0;
+ const mocked = t.mock.method(fs.default, 'renameSync', (...args: Parameters) => {
+ if (++writes === 3) throw Object.assign(new Error('fixture'), {code:'EIO'});
+ return rename(...args);
+ }); syncBuiltinESMExports();
+ try {
+ const r = await handoffSend({to:String(process.pid),message:'fixture body'}, {requestAck:true,ledgerPath:join(f.path,'state','handoff')});
+ assert.equal(r.exitCode,0); assert.equal(r.value.ok,true); assert.equal(r.value.status,'posted'); assert.equal(r.value.submitted,true);
+ assert.equal(r.value.handoffWarning,'persistence_failed'); const h = r.value.handoff as {correlationId:string;submission:{status:string}}; assert.equal(h.submission.status,'submitted'); assert.equal(f.messages.length,1);
+ const retained = new HandoffLedger(join(f.path,'state','handoff')).status(h.correlationId); assert.ok('handoff' in retained); assert.equal(retained.handoff.submission.status,'unknown');
+ await assert.rejects(handoffSend({to:String(process.pid),message:'fixture body'}, {correlationId:h.correlationId,ledgerPath:join(f.path,'state','handoff')}), /handoff_already_attempted/); assert.equal(f.messages.length,1);
+ } finally { mocked.mock.restore(); syncBuiltinESMExports(); if(oldDir===undefined) delete process.env.CLAUDE_CONFIG_DIR;else process.env.CLAUDE_CONFIG_DIR=oldDir; }
+});
+
+test('opt-in reserves bounded outer-result space before any native effect', {skip:process.platform==='win32'}, async t=>{
+ const f=await fixture(t);await f.invoke(['handoff','init']);
+ writeFileSync(join(f.path,'.claude/sessions',`${process.pid}.json`),JSON.stringify({pid:process.pid,name:'x'.repeat(1040000),messagingSocketPath:join(f.path,'sock')}));
+ const r=await f.invoke([...f.send,'--request-ack']);assert.equal(r.code,1);assert.equal(r.value.error,'handoff_result_too_large');assert.equal(r.value.submitted,false);assert.equal(f.messages.length,0);
+});
+
+
+test('message-file dash is literal with stdin open, while --message dash keeps stdin semantics', {skip:process.platform==='win32'}, async t=>{
+ const f=await fixture(t);writeFileSync(f.file,'-');
+ const file=await f.invoke(f.send,'different stdin body',false);
+ assert.equal(file.code,0);assert.equal(file.value.status,'posted');assert.equal(file.value.chars,1);
+ assert.equal(f.messages.length,1);assert.equal(JSON.parse(f.messages[0]!).message.content,'-');
+ await f.invoke(['handoff','init']);
+ const opted=await f.invoke([...f.send,'--request-ack'],'different stdin body',false);
+ assert.equal(opted.code,0);assert.equal(opted.value.submitted,true);assert.equal(f.messages.length,2);
+ assert.equal(JSON.parse(f.messages[1]!).message.content,'-\n\n---\nHandoff: '+JSON.stringify({schemaVersion:1,correlationId:opted.value.handoff.correlationId}));
+ const stdin=await f.invoke(['send','--to',String(process.pid),'--message','-','--no-from','--no-reply-to'],'chosen stdin body');
+ assert.equal(stdin.code,0);assert.equal(JSON.parse(f.messages[2]!).message.content,'chosen stdin body');
+ // SSH transports carry the already selected literal body in their JSON frame;
+ // a wire receiver must not interpret that body as a second stdin selector.
+ const receiver=spawn(process.execPath,[cli,'--stdio-request'],{env:f.env});let stdout='';receiver.stdout.on('data',c=>stdout+=c);receiver.stderr.resume();
+ const timer=setTimeout(()=>receiver.kill('SIGKILL'),10000);
+ receiver.stdin.end(JSON.stringify({schemaVersion:1,args:['send','--to',String(process.pid),'--message=-','--no-from','--no-reply-to','--json']}));
+ const [code,signal]=await once(receiver,'close');clearTimeout(timer);assert.equal(signal,null);assert.equal(code,0,stdout);
+ assert.equal(JSON.parse(f.messages[3]!).message.content,'-');
+
+});
+
+
+test('prepared inactive Codex binding retains lock and saved-home evidence without claiming generation', {skip:process.platform==='win32'}, async t=>{
+ const {DatabaseSync}=await import('node:sqlite');
+ const {inspectWriter,resolveWriter}=await import('../dist/writer.js');
+ const id='11111111-1111-4111-8111-111111111111';
+ for (const change of ['unchanged','lock','database'] as const) await t.test(change, async t=>{
+ const f=await fixture(t),home=join(f.path,'codex-home'),locks=join(home,'thread-writer-locks');mkdirSync(locks,{recursive:true});
+ const dbPath=join(home,'state_5.sqlite');
+ const database=()=>{const db=new DatabaseSync(dbPath);try{db.exec('CREATE TABLE threads(id TEXT)');db.prepare('INSERT INTO threads VALUES (?)').run(id);}finally{db.close();}};database();
+ const log=join(f.path,'queue-calls');writeFileSync(log,'');f.env.FIXTURE_QUEUE_LOG=log;f.env.SESSION_PEER_CODEX_HOMES='[]';
+ const binary=join(f.path,'fake-codex');writeFileSync(binary,`#!${process.execPath}\nrequire('node:fs').appendFileSync(process.env.FIXTURE_QUEUE_LOG,'call\\n');console.log('Queued message inactive-queue for thread '+process.argv[4]+'.');`,{mode:0o700});
+ const target=['--to','codex:'+id,'--codex-home',home,'--codex-bin',binary,'--allow-inactive-codex-home','--message-file',f.file];
+ const before=await inspectWriter(home,id);assert.equal(before.activity,'inactive');assert.equal(before.writerLock,'absent');
+ assert.equal((await f.invoke(['handoff','init'])).code,0);
+ const prepared=await f.invoke(['handoff','prepare',...target]);assert.equal(prepared.code,0);assert.equal((await resolveWriter(home,id,true)).resolution.candidates.find(c=>c.codexHome===home)?.savedThread,true);
+ const correlation=prepared.value.handoff.correlationId;assert.equal(prepared.value.handoff.targetGeneration,null);assert.equal(readFileSync(log,'utf8'),'');
+ if(change==='lock')writeFileSync(join(locks,id+'.lock'),'');
+ if(change==='database'){renameSync(dbPath,dbPath+'.previous');database();}
+ const current=await inspectWriter(home,id);assert.equal(current.activity,'inactive');assert.equal(current.writerLock,change==='lock'?'free':'absent');
+ const sent=await f.invoke(['send',...target,'--correlation-id',correlation,'--request-ack','--no-from','--no-reply-to']);
+ if(change==='unchanged'){
+ assert.equal(sent.code,0);assert.equal(sent.value.status,'queued');assert.equal(sent.value.submitted,true);assert.equal(sent.value.queueId,'inactive-queue');assert.equal(sent.value.handoff.targetGeneration,null);assert.equal(sent.value.handoff.retry.allowed,false);assert.equal(sent.value.handoff.ack.status,'unsupported');assert.equal(readFileSync(log,'utf8'),'call\n');
+ }else{
+ assert.equal(sent.code,1);assert.equal(sent.value.error,'handoff_binding_conflict');assert.equal(sent.value.submitted,false);assert.equal(readFileSync(log,'utf8'),'');
+ }
+ });
+});
+
+
+test('prepared Claude binding refuses same-PID registry or inbox replacement', {skip:process.platform==='win32'}, async t=>{
+ for(const change of ['unchanged','record','socket','socket-same-path'] as const)await t.test(change,async t=>{
+ const f=await fixture(t);await f.invoke(['handoff','init']);
+ const prepared=await f.invoke(['handoff','prepare','--to',String(process.pid),'--message-file',f.file]);assert.equal(prepared.code,0);
+ const correlation=prepared.value.handoff.correlationId;assert.equal(prepared.value.handoff.targetGeneration,null);
+ const replacementMessages:string[]=[];
+ if(change==='record'){
+ const record=join(f.path,'.claude/sessions',`${process.pid}.json`);renameSync(record,record+'.old');
+ writeFileSync(record,JSON.stringify({pid:process.pid,name:null,messagingSocketPath:join(f.path,'sock')}));
+ }
+ if(change==='socket'||change==='socket-same-path'){
+ const socket=join(f.path,change==='socket'?'replacement-sock':'sock');if(change==='socket-same-path')unlinkSync(socket);const server=createServer(s=>{let data='';s.on('data',c=>data+=c);s.on('end',()=>{replacementMessages.push(data);s.end();});});
+ server.listen(socket);await once(server,'listening');t.after(()=>new Promise(ok=>server.close(()=>ok())));
+ if(change==='socket')writeFileSync(join(f.path,'.claude/sessions',`${process.pid}.json`),JSON.stringify({pid:process.pid,name:null,messagingSocketPath:socket}));
+ }
+ const sent=await f.invoke([...f.send,'--correlation-id',correlation,'--request-ack']);
+ if(change==='unchanged'){
+ assert.equal(sent.code,0);assert.equal(sent.value.status,'posted');assert.equal(sent.value.submitted,true);assert.equal(sent.value.handoff.targetGeneration,null);assert.equal(sent.value.handoff.ack.status,'unsupported');assert.equal(sent.value.handoff.retry.allowed,false);assert.equal(f.messages.length,1);
+ }else{
+ assert.equal(sent.code,1);assert.equal(sent.value.error,'handoff_binding_conflict');assert.equal(sent.value.submitted,false);assert.equal(f.messages.length,0);
+ }
+ assert.equal(replacementMessages.length,0);
+ });
+});
diff --git a/test/handoff-core.test.ts b/test/handoff-core.test.ts
new file mode 100644
index 0000000..8452d28
--- /dev/null
+++ b/test/handoff-core.test.ts
@@ -0,0 +1,483 @@
+import { test, type TestContext } from 'node:test';
+import assert from 'node:assert/strict';
+import { createCipheriv, randomBytes, randomUUID } from 'node:crypto';
+import { spawn } from 'node:child_process';
+import { once } from 'node:events';
+import { createRequire } from 'node:module';
+import { chmodSync, existsSync, mkdirSync, mkdtempSync, readFileSync, realpathSync, rmSync, statSync, symlinkSync, writeFileSync } from 'node:fs';
+import { tmpdir } from 'node:os';
+import { join } from 'node:path';
+import { HandoffBudget, parseStrictJson, parseWaitTimeout, validateHandoff, validateHandoffQuery, validateReceipt, validateConfirmation, HANDOFF_LIMITS, type Clock, type Handoff } from '../dist/handoff.js';
+import { HandoffLedger, payloadDigest, type HandoffBinding } from '../dist/handoff-ledger.js';
+
+const fixture = JSON.parse(readFileSync(new URL('./fixtures/handoff-v1.json', import.meta.url), 'utf8'));
+const posix = { skip: process.platform === 'win32' };
+function fixtureLedger(t: TestContext) {
+ const root = realpathSync(mkdtempSync(join(process.env.TASK_TEMP ?? tmpdir(), 'codex-ho-')));
+ chmodSync(root, 0o700); t.after(() => rmSync(root, { recursive: true, force: true }));
+ let mono = 10, utc = 100000;
+ const clock: Clock = { monotonic: () => mono, utc: () => utc };
+ const ledger = new HandoffLedger(join(root, 'ledger'), { clock });
+ const binding: HandoffBinding = { agent: 'codex', destination: 'fixture', target: 'aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa', home: '/fixture/home',
+ writerIdentity: 'fixture-writer', generation: 'fixture-generation', payloadDigest: payloadDigest('private fixture body') };
+ return { root, clock, ledger, binding, advance: (ms: number) => { mono += ms; utc += ms; }, jumpUtc: (ms: number) => { utc += ms; } };
+}
+function handoff(ledger: HandoffLedger, id: string): Handoff {
+ const value = ledger.status(id); assert.ok('handoff' in value); return value.handoff;
+}
+function receipt(h: Handoff, capability: string, receiptId = randomUUID()) {
+ return JSON.stringify({ schemaVersion: 1, kind: 'receipt', ledgerEpoch: h.ledgerEpoch, correlationId: h.correlationId,
+ targetGeneration: h.targetGeneration, receiptId, capability });
+}
+function confirmation(h: Handoff) {
+ return JSON.stringify({ schemaVersion: 1, ledgerEpoch: h.ledgerEpoch, correlationId: h.correlationId,
+ targetGeneration: h.targetGeneration, confirmed: true });
+}
+function submit(f: ReturnType, h: Handoff) {
+ f.ledger.commitEffect(h.correlationId, f.binding); return f.ledger.recordSubmission(h.correlationId, 'submitted');
+}
+// Build large, valid quota fixtures without O(n²) durable API writes. Each
+// synthetic record still needs the real ID/epoch-authenticated encrypted field;
+// copying one ciphertext to another ID must remain invalid (privacy regression).
+function sealedQuotaDigest(path: string, epoch: string, id: string, digest: string): string {
+ const key = readFileSync(join(path, 'ledger.key')), nonce = randomBytes(12);
+ const cipher = createCipheriv('aes-256-gcm', key, nonce);
+ cipher.setAAD(Buffer.from(JSON.stringify(['handoff-binding-v1', epoch, id])));
+ const encrypted = Buffer.concat([cipher.update(Buffer.from(digest, 'hex')), cipher.final()]);
+ return `sealed-v1:${nonce.toString('hex')}:${encrypted.toString('hex')}:${cipher.getAuthTag().toString('hex')}`;
+}
+const bootstrap = { privateConsumerValidated: true, compatibleProducerValidated: true } as const;
+
+test('all pinned positive public schema fixtures validate without rewriting native fields', () => {
+ for (const sample of fixture.accepted) {
+ const original = JSON.stringify(sample.result);
+ assert.deepEqual(validateHandoff(sample.result.handoff), sample.result.handoff, sample.name);
+ assert.equal(JSON.stringify(sample.result), original);
+ }
+});
+test('all pinned negative handoff schema fixtures fail closed', () => {
+ for (const sample of fixture.rejected) {
+ const base = fixture.accepted.find((item: any) => item.name === sample.base);
+ const h = structuredClone(base.result.handoff);
+ for (const [path, value] of Object.entries(sample.set)) {
+ const keys = path.split('.'); let target = h;
+ for (const key of keys.slice(0, -1)) target = target[key];
+ target[keys.at(-1)!] = value;
+ }
+ assert.throws(() => validateHandoff(h), /invalid_handoff/, sample.name);
+ }
+});
+test('keep_waiting requires the selected goal evidence channel to remain pending', () => {
+ const ackPending = structuredClone(fixture.accepted.find((item: any) => item.name === 'queued_explicit_ack_timeout').result.handoff);
+ // An expired/stopped wait may suggest a new wait while its evidence channel is
+ // still pending; the frozen positive fixtures retain that distinction.
+ assert.deepEqual(validateHandoff(ackPending), ackPending);
+ for (const status of ['not_requested', 'unsupported']) {
+ const invalid = structuredClone(ackPending);
+ invalid.state = 'submitted'; invalid.wait.status = 'pending';
+ invalid.ack = { status }; invalid.nextActions = ['keep_waiting'];
+ assert.throws(() => validateHandoff(invalid), /invalid_handoff/, `ACK ${status}`);
+ }
+ const alreadyAcknowledged = structuredClone(fixture.accepted.find((item: any) => item.name === 'receipt_satisfies_wait_without_injection_claim').result.handoff);
+ alreadyAcknowledged.wait.status = 'pending'; alreadyAcknowledged.nextActions = ['keep_waiting'];
+ assert.throws(() => validateHandoff(alreadyAcknowledged), /invalid_handoff/, 'ACK already committed');
+ const noGoal = structuredClone(ackPending);
+ noGoal.state = 'submitted'; noGoal.wait = { for: 'none', status: 'not_requested' }; noGoal.nextActions = ['keep_waiting'];
+ assert.throws(() => validateHandoff(noGoal), /invalid_handoff/, 'no selected evidence goal');
+});
+test('keep_waiting permits pending delivery evidence but rejects an observed channel', () => {
+ const pending = structuredClone(fixture.accepted.find((item: any) => item.name === 'queued_explicit_ack_timeout').result.handoff);
+ pending.state = 'submitted'; pending.wait.for = 'delivered'; pending.wait.status = 'pending';
+ pending.observation = { status: 'pending', injectionObserved: false }; pending.ack = { status: 'not_requested' };
+ pending.nextActions = ['keep_waiting'];
+ assert.deepEqual(validateHandoff(pending), pending);
+ for (const status of ['not_requested', 'failed', 'unsupported']) {
+ const invalid = structuredClone(pending); invalid.observation.status = status;
+ assert.throws(() => validateHandoff(invalid), /invalid_handoff/, `delivery ${status}`);
+ }
+ const observed = structuredClone(fixture.accepted.find((item: any) => item.name === 'delivered_wait_satisfied').result.handoff);
+ observed.wait.status = 'pending'; observed.nextActions = ['keep_waiting'];
+ assert.throws(() => validateHandoff(observed), /invalid_handoff/, 'delivery already observed');
+});
+test('raw strict JSON rejects duplicate keys, lossy integer tokens, invalid UTF-8 and trailing data', () => {
+ for (const sample of fixture.wireRejected) assert.throws(() => parseStrictJson(sample.raw ?? Buffer.from(sample.bytesHex, 'hex')), /invalid_handoff_json/, sample.name);
+ assert.throws(() => parseStrictJson('{"schemaVersion":-0}'));
+ assert.throws(() => parseStrictJson('{"n":9007199254740993}'));
+ assert.equal((parseStrictJson('{"native":1e0,"handoff":{"schemaVersion":1}}', { scope: 'outer' }) as any).native, 1);
+ assert.throws(() => parseStrictJson('{"native":1.0,"handoff":{"schemaVersion":1e0}}', { scope: 'outer' }));
+ assert.throws(() => parseStrictJson('{"handoffQuery":{"schemaVersion":1.0}}', { scope: 'outer' }));
+ assert.throws(() => parseStrictJson('['.repeat(65) + '0' + ']'.repeat(65)));
+ assert.throws(() => parseStrictJson('"\ud800"'));
+ assert.throws(() => parseStrictJson(Buffer.from('\ufeff{}', 'utf8')));
+});
+test('closed missing-history query schemas have no fabricated epoch or native facts', () => {
+ for (const sample of fixture.queryErrors) {
+ assert.deepEqual(validateHandoffQuery(sample.result.handoffQuery), sample.result.handoffQuery);
+ assert.throws(() => validateHandoffQuery({ ...sample.result.handoffQuery, ledgerEpoch: randomUUID() }));
+ }
+});
+test('private frame bound counts every byte and no extra trailing LF allowance', () => {
+ const frame = '{}' + ' '.repeat(HANDOFF_LIMITS.privateFrame - 2);
+ assert.deepEqual(Object.keys(parseStrictJson(frame) as object), []);
+ assert.throws(() => parseStrictJson(frame + '\n'), /handoff_frame_too_large/);
+});
+test('strict timeout grammar and monotonic budget reserve obey all cutoff boundaries', () => {
+ for (let n = 1; n <= 60; n++) assert.equal(parseWaitTimeout(String(n)), n);
+ for (const value of ['', '0', '01', '61', '+1', '-1', '1.0', '1e0', ' 1', '1 ', '١', '1']) assert.throws(() => parseWaitTimeout(value), /invalid_wait_timeout/);
+ let mono = 10, utc = 10000; const clock = { monotonic: () => mono, utc: () => utc };
+ const budget = new HandoffBudget(6, clock); assert.equal(budget.deadlineAtUtcMs, 11000);
+ utc += 1000000000; assert.equal(budget.observationRemainingMs(), 1000);
+ mono = 1009; budget.beforeEffect(); mono = 1010; assert.throws(() => budget.beforeEffect(), /deadline_before_effect/);
+ for (let n = 1; n <= 5; n++) assert.throws(() => new HandoffBudget(n, clock).beforeEffect(), /insufficient_budget/);
+});
+
+test('ledger use never auto-initializes and missing/unknown queries invent no epoch', posix, t => {
+ const f = fixtureLedger(t), id = randomUUID();
+ const missing = f.ledger.status(id); assert.ok('handoffQuery' in missing); assert.equal(missing.handoffQuery.context, 'ledger_missing');
+ assert.equal('ledgerEpoch' in missing, false); assert.equal('handoff' in missing, false);
+ assert.throws(() => f.ledger.prepare(f.binding), /handoff_history_unavailable/); assert.equal(existsSync(f.ledger.path), false);
+ f.ledger.init(); const unknown = f.ledger.status(id); assert.ok('handoffQuery' in unknown); assert.equal(unknown.handoffQuery.context, 'id_unknown');
+ assert.throws(() => f.ledger.prepare(f.binding, { correlationId: id }), /handoff_id_unknown/);
+ assert.throws(() => f.ledger.init(), /handoff_ledger_exists/);
+});
+test('prepared exact bindings and correlation-only dry runs reserve authority without effect', posix, t => {
+ const f = fixtureLedger(t); f.ledger.init(); const h = f.ledger.prepare(f.binding);
+ const before = readFileSync(join(f.ledger.path, 'ledger.json'), 'utf8');
+ assert.equal(f.ledger.prepare(f.binding, { correlationId: h.correlationId, dryRun: true }).state, 'validated');
+ assert.equal(readFileSync(join(f.ledger.path, 'ledger.json'), 'utf8'), before);
+ for (const binding of [{ ...f.binding, generation: 'successor' }, { ...f.binding, payloadDigest: payloadDigest('other') }, { ...f.binding, home: '/different' }]) {
+ assert.throws(() => f.ledger.commitEffect(h.correlationId, binding), /handoff_binding_conflict/);
+ }
+ assert.equal(handoff(f.ledger, h.correlationId).state, 'validated'); submit(f, h);
+ assert.throws(() => f.ledger.commitEffect(h.correlationId, f.binding), /handoff_already_attempted/);
+});
+test('durable effect fence recovers unknown after a crash and never authorizes another attempt', posix, t => {
+ const f = fixtureLedger(t); f.ledger.init(); const h = f.ledger.prepare(f.binding);
+ f.ledger.commitEffect(h.correlationId, f.binding);
+ const restarted = new HandoffLedger(f.ledger.path, { clock: f.clock });
+ assert.equal(handoff(restarted, h.correlationId).state, 'unknown');
+ assert.throws(() => restarted.commitEffect(h.correlationId, f.binding), /handoff_already_attempted/);
+ assert.throws(() => restarted.recordSubmission(h.correlationId, 'submitted'), /handoff_outcome_not_authorized/);
+});
+test('process crash after synced intent still fences the native effect and releases the OS lock', posix, async t => {
+ const f = fixtureLedger(t); f.ledger.init(); const h = f.ledger.prepare(f.binding);
+ const moduleUrl = new URL('../dist/handoff-ledger.js', import.meta.url).href;
+ const script = `import { HandoffLedger } from ${JSON.stringify(moduleUrl)};
+let input=''; for await (const chunk of process.stdin) input+=chunk;
+const request=JSON.parse(input); new HandoffLedger(request.path,{clock:{monotonic:()=>10,utc:()=>100000}}).commitEffect(request.id,request.binding);
+process.kill(process.pid,'SIGKILL');`;
+ const child = spawn(process.execPath, ['--input-type=module', '-e', script], { stdio: ['pipe', 'pipe', 'pipe'] });
+ let stderr = ''; child.stdout.resume(); child.stderr.on('data', chunk => { stderr += chunk; }); child.stdin.end(JSON.stringify({ path: f.ledger.path, id: h.correlationId, binding: f.binding }));
+ const [, signal] = await once(child, 'close'); assert.equal(signal, 'SIGKILL', stderr);
+ assert.equal(handoff(f.ledger, h.correlationId).state, 'unknown');
+ assert.throws(() => f.ledger.commitEffect(h.correlationId, f.binding), /handoff_already_attempted/);
+});
+test('cross-process private advisory lock serializes mutations without overwriting history', posix, async t => {
+ const f = fixtureLedger(t); f.ledger.init();
+ const native = createRequire(import.meta.url).resolve('fs-ext-extra-prebuilt');
+ const script = `const fs=require('node:fs'), locks=require(${JSON.stringify(native)});
+const fd=fs.openSync(process.argv[1],'r+'); locks.flockSync(fd,'exnb');console.log('ready');setInterval(()=>{},1000);`;
+ const child = spawn(process.execPath, ['-e', script, join(f.ledger.path, 'ledger.lock')], { stdio: ['ignore', 'pipe', 'pipe'] });
+ child.stderr.resume();
+ t.after(async () => { if (child.exitCode === null && child.signalCode === null) { child.kill('SIGTERM'); await once(child, 'close'); } });
+ await once(child.stdout, 'data');
+ assert.throws(() => f.ledger.prepare(f.binding), /handoff_ledger_busy/);
+ child.kill('SIGTERM'); await once(child, 'close');
+ assert.equal(f.ledger.prepare(f.binding).state, 'validated');
+});
+test('required channels and null generation refuse before durable effect, including supplied prepared IDs', posix, t => {
+ const f = fixtureLedger(t); f.ledger.init();
+ for (const waitFor of ['delivered', 'acknowledged'] as const) {
+ const binding = { ...f.binding, generation: null }, h = f.ledger.prepare(binding);
+ const refused = f.ledger.prepare(binding, { correlationId: h.correlationId, waitFor });
+ assert.equal(refused.state, 'refused'); assert.equal(refused.wait.status, 'unsupported');
+ assert.throws(() => f.ledger.commitEffect(h.correlationId, binding), /handoff_already_attempted/);
+ assert.throws(() => f.ledger.mintCapability(h.correlationId, bootstrap), /receipt_authority_unavailable/);
+ }
+ const best = f.ledger.prepare({ ...f.binding, generation: null }, { requestAck: true, observeDelivery: true });
+ assert.equal(best.ack.status, 'unsupported'); assert.equal(best.observation.status, 'unsupported');
+});
+test('qualified internal delivery supports required waits while receipt flags cannot qualify ACK', posix, t => {
+ const f = fixtureLedger(t); f.ledger.init();
+ const pending = f.ledger.prepare(f.binding, { waitFor: 'delivered', observationSupported: true, requestAck: true, budget: new HandoffBudget(6, f.clock) });
+ assert.equal(pending.wait.status, 'pending'); assert.equal(pending.observation.status, 'pending'); assert.equal(pending.ack.status, 'unsupported');
+ submit(f, pending);
+ const observed = f.ledger.recordObservation(pending.correlationId, { clientUserMessageId: pending.correlationId, injectionObserved: true, turn: { id: 'turn', status: 'completed' } }, f.binding);
+ assert.equal(observed.state, 'delivered'); assert.equal(observed.wait.status, 'satisfied'); assert.equal(observed.ack.status, 'unsupported');
+ const refused = f.ledger.prepare(f.binding, { waitFor: 'acknowledged', channels: { delivery: true, receipt: true } });
+ assert.equal(refused.wait.status, 'unsupported'); assert.equal(refused.state, 'refused');
+});
+test('observation requires qualified original generation, exact binding/message ID and known submission', posix, t => {
+ const f = fixtureLedger(t); f.ledger.init();
+ const h = f.ledger.prepare(f.binding, { observeDelivery: true, channels: { delivery: true }, clientUserMessageId: 'original-user-message' });
+ const evidence = { clientUserMessageId: 'original-user-message', injectionObserved: true };
+ assert.throws(() => f.ledger.recordObservation(h.correlationId, evidence, f.binding), /handoff_observation_unverified/);
+ submit(f, h);
+ assert.throws(() => f.ledger.recordObservation(h.correlationId, { ...evidence, clientUserMessageId: 'different-user-message' }, f.binding), /handoff_observation_unverified/);
+ for (const binding of [{ ...f.binding, generation: 'successor' }, { ...f.binding, destination: 'different' }, { ...f.binding, payloadDigest: payloadDigest('different') }]) {
+ assert.throws(() => f.ledger.recordObservation(h.correlationId, evidence, binding), /handoff_binding_conflict/);
+ }
+ const observed = f.ledger.recordObservation(h.correlationId, evidence, f.binding);
+ assert.equal(observed.state, 'delivered'); assert.equal(observed.submission.status, 'submitted'); assert.equal(observed.observation.clientUserMessageId, evidence.clientUserMessageId);
+ assert.equal(f.ledger.recordObservation(h.correlationId, { ...evidence, injectionObserved: false }, f.binding).state, 'delivered');
+ assert.throws(() => f.ledger.commitEffect(h.correlationId, f.binding), /handoff_already_attempted/);
+ const unqualified = f.ledger.prepare(f.binding); submit(f, unqualified);
+ assert.throws(() => f.ledger.recordObservation(unqualified.correlationId, { clientUserMessageId: unqualified.correlationId, injectionObserved: true }, f.binding), /handoff_observation_unverified/);
+ const nullBinding = { ...f.binding, generation: null }, nullH = f.ledger.prepare(nullBinding, { observeDelivery: true, observationSupported: true });
+ f.ledger.commitEffect(nullH.correlationId, nullBinding); f.ledger.recordSubmission(nullH.correlationId, 'submitted');
+ assert.throws(() => f.ledger.recordObservation(nullH.correlationId, { clientUserMessageId: nullH.correlationId, injectionObserved: true }, nullBinding), /handoff_observation_unverified/);
+});
+test('delivery at cutoff preserves timed-out wait; repeated observation cannot change original turn', posix, t => {
+ const f = fixtureLedger(t); f.ledger.init(); const h = f.ledger.prepare(f.binding, { waitFor: 'delivered', observationSupported: true, budget: new HandoffBudget(6, f.clock) });
+ submit(f, h); f.advance(1000);
+ const evidence = { clientUserMessageId: h.correlationId, injectionObserved: true, turn: { id: 'turn', status: 'completed' as const } };
+ const observed = f.ledger.recordObservation(h.correlationId, evidence, f.binding);
+ assert.equal(observed.wait.status, 'timed_out_unknown'); assert.equal(observed.state, 'delivered');
+ assert.throws(() => f.ledger.recordObservation(h.correlationId, { ...evidence, turn: { id: 'successor-turn', status: 'completed' } }, f.binding), /handoff_turn_conflict/);
+ assert.throws(() => f.ledger.recordObservation(h.correlationId, { ...evidence, turn: { id: 'turn', status: 'running' } }, f.binding), /handoff_turn_conflict/);
+ const later = f.ledger.beginWait(h.correlationId, 'delivered', new HandoffBudget(6, f.clock)); assert.equal(later.wait.status, 'satisfied');
+});
+test('exact injection evidence survives observer restart without inventing old wait ordering or ACK', posix, t => {
+ const f = fixtureLedger(t); f.ledger.init(); const h = f.ledger.prepare(f.binding, { waitFor: 'delivered', observationSupported: true }); submit(f, h);
+ const restarted = new HandoffLedger(f.ledger.path, { clock: f.clock });
+ const observed = restarted.recordObservation(h.correlationId, { clientUserMessageId: h.correlationId, injectionObserved: true }, f.binding);
+ assert.equal(observed.state, 'delivered'); assert.equal(observed.wait.status, 'failed'); assert.equal(observed.wait.reason, 'history_unavailable');
+ assert.equal(observed.ack.status, 'not_requested'); assert.equal(observed.observation.injectionObserved, true);
+});
+test('late final resolution refusal consumes only a valid bound prepared reservation and fences restart', posix, t => {
+ const f = fixtureLedger(t); f.ledger.init(); const h = f.ledger.prepare(f.binding, { waitFor: 'delivered', observationSupported: true });
+ assert.throws(() => f.ledger.refusePrepared(randomUUID(), f.binding), /handoff_id_unknown/);
+ assert.throws(() => f.ledger.refusePrepared(h.correlationId, { ...f.binding, generation: 'successor' }), /handoff_binding_conflict/);
+ assert.equal(handoff(f.ledger, h.correlationId).state, 'validated');
+ const refused = f.ledger.refusePrepared(h.correlationId, f.binding, 'evidence_failed');
+ assert.equal(refused.state, 'refused'); assert.equal(refused.submission.status, 'refused'); assert.equal(refused.wait.status, 'failed');
+ const restarted = new HandoffLedger(f.ledger.path, { clock: f.clock });
+ assert.throws(() => restarted.commitEffect(h.correlationId, f.binding), /handoff_already_attempted/);
+ assert.throws(() => restarted.prepare(f.binding, { correlationId: h.correlationId }), /handoff_already_attempted/);
+});
+test('insufficient budget consumes a real send reservation but dry runs do not', posix, t => {
+ const f = fixtureLedger(t); f.ledger.init();
+ const h = f.ledger.prepare(f.binding, { waitFor: 'acknowledged', budget: new HandoffBudget(5, f.clock) });
+ assert.equal(h.state, 'refused'); assert.equal(h.wait.reason, 'insufficient_budget');
+ assert.throws(() => f.ledger.commitEffect(h.correlationId, f.binding), /handoff_already_attempted/);
+ const other = f.ledger.prepare(f.binding);
+ assert.throws(() => f.ledger.commitEffect(other.correlationId, f.binding, new HandoffBudget(5, f.clock)), /insufficient_budget/);
+ assert.equal(handoff(f.ledger, other.correlationId).state, 'refused');
+});
+test('owned 0700/0600 storage rejects permissive paths and symlink records', posix, t => {
+ const f = fixtureLedger(t); f.ledger.init();
+ assert.equal(statSync(f.ledger.path).mode & 0o777, 0o700);
+ assert.equal(statSync(join(f.ledger.path, 'ledger.json')).mode & 0o777, 0o600);
+ chmodSync(f.ledger.path, 0o755); assert.throws(() => f.ledger.prepare(f.binding), /handoff_storage_untrusted/); chmodSync(f.ledger.path, 0o700);
+ const target = join(f.root, 'external'); writeFileSync(target, 'private', { mode: 0o600 });
+ rmSync(join(f.ledger.path, 'ledger.json')); symlinkSync(target, join(f.ledger.path, 'ledger.json'));
+ assert.throws(() => f.ledger.prepare(f.binding), /handoff_storage_untrusted/);
+ assert.equal(readFileSync(target, 'utf8'), 'private');
+});
+test('corrupt ledger status remains a query error and does not silently recreate history', posix, t => {
+ const f = fixtureLedger(t); f.ledger.init(); const h = f.ledger.prepare(f.binding);
+ writeFileSync(join(f.ledger.path, 'ledger.json'), '{}');
+ const value = f.ledger.status(h.correlationId); assert.ok('handoffQuery' in value); assert.equal(value.handoffQuery.context, 'ledger_corrupt');
+ assert.throws(() => f.ledger.prepare(f.binding), /handoff_ledger_corrupt/);
+ assert.equal(readFileSync(join(f.ledger.path, 'ledger.json'), 'utf8'), '{}');
+});
+test('operator confirmation is metadata-only, correlated and never manufactures injection', posix, t => {
+ const f = fixtureLedger(t); f.ledger.init(); const h = f.ledger.prepare(f.binding); submit(f, h);
+ assert.throws(() => f.ledger.confirm(confirmation({ ...h, targetGeneration: 'successor' })), /confirmation_binding_invalid/);
+ assert.throws(() => validateConfirmation(JSON.stringify({ ...JSON.parse(confirmation(h)), body: 'transcript' })), /invalid_handoff/);
+ const acknowledged = f.ledger.confirm(confirmation(h));
+ assert.equal(acknowledged.ack.assurance, 'operator_confirmed'); assert.equal(acknowledged.ack.late, false);
+ assert.equal(acknowledged.observation.injectionObserved, false);
+ const restarted = new HandoffLedger(f.ledger.path, { clock: { monotonic: () => 0, utc: () => 1 } });
+ assert.deepEqual(handoff(restarted, h.correlationId).ack, acknowledged.ack);
+});
+test('private receipt authority is hash-only, bound to original tuple and first receipt', posix, t => {
+ const f = fixtureLedger(t); f.ledger.init(); const h = f.ledger.prepare(f.binding);
+ const capability = f.ledger.mintCapability(h.correlationId, bootstrap); assert.equal(capability.length, 43);
+ assert.equal(readFileSync(join(f.ledger.path, 'ledger.json'), 'utf8').includes(capability), false);
+ assert.equal(JSON.stringify(handoff(f.ledger, h.correlationId)).includes(capability), false);
+ submit(f, h);
+ const raw = receipt(h, capability), wrong = receipt(h, Buffer.alloc(32).toString('base64url'));
+ assert.throws(() => f.ledger.acceptReceipt(wrong), /receipt_authentication_failed/);
+ assert.throws(() => f.ledger.acceptReceipt(receipt({ ...h, targetGeneration: 'successor' }, capability)), /receipt_authentication_failed/);
+ const accepted = f.ledger.acceptReceipt(raw);
+ assert.equal(accepted.ack.assurance, 'token_possession'); assert.equal(accepted.observation.injectionObserved, false);
+ assert.deepEqual(f.ledger.acceptReceipt(raw), accepted);
+ assert.throws(() => f.ledger.acceptReceipt(receipt(h, capability)), /receipt_already_committed/);
+ f.advance(HANDOFF_LIMITS.capabilityMs + 1);
+ const restarted = new HandoffLedger(f.ledger.path, { clock: f.clock });
+ assert.deepEqual(restarted.acceptReceipt(raw).ack, accepted.ack);
+ assert.throws(() => restarted.acceptReceipt(wrong), /receipt_authentication_failed/);
+ restarted.revoke(h.correlationId); assert.throws(() => restarted.acceptReceipt(raw), /receipt_authentication_failed/);
+});
+test('restarted unused authority expires conservatively and never regenerates a secret', posix, t => {
+ const f = fixtureLedger(t); f.ledger.init(); const h = f.ledger.prepare(f.binding);
+ const capability = f.ledger.mintCapability(h.correlationId, bootstrap); submit(f, h);
+ const restarted = new HandoffLedger(f.ledger.path, { clock: f.clock });
+ assert.throws(() => restarted.acceptReceipt(receipt(h, capability)), /receipt_authority_expired/);
+ assert.throws(() => restarted.mintCapability(h.correlationId, bootstrap), /receipt_authority_unavailable/);
+ assert.equal(handoff(restarted, h.correlationId).ack.status, 'unsupported');
+});
+test('a new native effect owner cannot revive receipt authority minted before process restart', posix, t => {
+ const f = fixtureLedger(t); f.ledger.init(); const h = f.ledger.prepare(f.binding);
+ const capability = f.ledger.mintCapability(h.correlationId, bootstrap);
+ const restarted = new HandoffLedger(f.ledger.path, { clock: { monotonic: () => 0, utc: f.clock.utc } });
+ restarted.commitEffect(h.correlationId, f.binding); restarted.recordSubmission(h.correlationId, 'submitted');
+ assert.throws(() => restarted.acceptReceipt(receipt(h, capability)), /receipt_authority_expired/);
+ assert.equal(handoff(restarted, h.correlationId).ack.status, 'unsupported');
+});
+test('expired unspent receipt authority never offers keep_waiting or accepts a new receipt', posix, t => {
+ const f = fixtureLedger(t); f.ledger.init(); const h = f.ledger.prepare(f.binding);
+ const capability = f.ledger.mintCapability(h.correlationId, bootstrap); submit(f, h);
+ f.ledger.beginWait(h.correlationId, 'acknowledged', new HandoffBudget(6, f.clock)); f.advance(HANDOFF_LIMITS.capabilityMs);
+ assert.equal(handoff(f.ledger, h.correlationId).nextActions.includes('keep_waiting'), false);
+ assert.throws(() => f.ledger.acceptReceipt(receipt(h, capability)), /receipt_authority_expired/);
+ assert.equal(f.ledger.beginWait(h.correlationId, 'acknowledged').wait.status, 'unsupported');
+ assert.equal(handoff(f.ledger, h.correlationId).wait.status, 'unsupported');
+});
+test('receipt at cutoff is late, preserves original terminal wait, and satisfies a later wait', posix, t => {
+ const f = fixtureLedger(t); f.ledger.init(); const h = f.ledger.prepare(f.binding);
+ const capability = f.ledger.mintCapability(h.correlationId, bootstrap); submit(f, h);
+ const pending = f.ledger.beginWait(h.correlationId, 'acknowledged', new HandoffBudget(6, f.clock));
+ assert.equal(pending.wait.status, 'pending'); f.advance(1000);
+ const accepted = f.ledger.acceptReceipt(receipt(h, capability));
+ assert.equal(accepted.ack.late, true); assert.equal(accepted.wait.status, 'timed_out_unknown');
+ const later = f.ledger.beginWait(h.correlationId, 'acknowledged', new HandoffBudget(6, f.clock));
+ assert.equal(later.wait.status, 'satisfied'); assert.notEqual(later.wait.operationId, pending.wait.operationId); assert.equal(later.ack.late, true);
+});
+test('strictly-before-cutoff receipt satisfies wait and terminal results cannot be rewritten', posix, t => {
+ const f = fixtureLedger(t); f.ledger.init(); const h = f.ledger.prepare(f.binding);
+ const capability = f.ledger.mintCapability(h.correlationId, bootstrap); submit(f, h);
+ f.ledger.beginWait(h.correlationId, 'acknowledged', new HandoffBudget(6, f.clock)); f.advance(999);
+ assert.equal(f.ledger.acceptReceipt(receipt(h, capability)).wait.status, 'satisfied');
+ assert.throws(() => f.ledger.recordWait(h.correlationId, 'stopped', 'stopped_by_operator'), /handoff_wait_terminal/);
+});
+test('one receipt or confirmation resolves concurrent ACK waits by their own immutable deadlines', posix, t => {
+ for (const route of ['receipt', 'confirmation'] as const) {
+ const f = fixtureLedger(t); f.ledger.init(); const h = f.ledger.prepare(f.binding);
+ const capability = f.ledger.mintCapability(h.correlationId, bootstrap); submit(f, h);
+ const first = f.ledger.beginWait(h.correlationId, 'acknowledged', new HandoffBudget(6, f.clock));
+ f.advance(500);
+ const second = f.ledger.beginWait(h.correlationId, 'acknowledged', new HandoffBudget(6, f.clock));
+ f.advance(500); // At the first cutoff, strictly before the second cutoff.
+ const raw = receipt(h, capability);
+ const accepted = route === 'receipt' ? f.ledger.acceptReceipt(raw) : f.ledger.confirm(confirmation(h));
+ assert.equal(accepted.ack.late, true, route);
+ assert.equal(accepted.wait.operationId, second.wait.operationId, route);
+ assert.equal(accepted.wait.status, 'satisfied', route);
+ const stored = JSON.parse(readFileSync(join(f.ledger.path, 'ledger.json'), 'utf8')).data.records[h.correlationId];
+ assert.deepEqual(stored.waits.map((wait: any) => [wait.public.operationId, wait.public.status]), [
+ [first.wait.operationId, 'timed_out_unknown'], [second.wait.operationId, 'satisfied'],
+ ], route);
+ const before = readFileSync(join(f.ledger.path, 'ledger.json'), 'utf8');
+ f.advance(1000);
+ const duplicate = route === 'receipt' ? f.ledger.acceptReceipt(raw) : f.ledger.confirm(confirmation(h));
+ assert.deepEqual(duplicate.ack, accepted.ack, route);
+ assert.equal(readFileSync(join(f.ledger.path, 'ledger.json'), 'utf8'), before, route);
+ }
+});
+test('receipt preserves an earlier stopped ACK wait while satisfying a newer wait', posix, t => {
+ const f = fixtureLedger(t); f.ledger.init(); const h = f.ledger.prepare(f.binding);
+ const capability = f.ledger.mintCapability(h.correlationId, bootstrap); submit(f, h);
+ const first = f.ledger.beginWait(h.correlationId, 'acknowledged', new HandoffBudget(6, f.clock));
+ f.ledger.recordWait(h.correlationId, 'stopped', 'stopped_by_operator');
+ const second = f.ledger.beginWait(h.correlationId, 'acknowledged', new HandoffBudget(6, f.clock));
+ const accepted = f.ledger.acceptReceipt(receipt(h, capability));
+ assert.equal(accepted.wait.operationId, second.wait.operationId); assert.equal(accepted.wait.status, 'satisfied');
+ const stored = JSON.parse(readFileSync(join(f.ledger.path, 'ledger.json'), 'utf8')).data.records[h.correlationId];
+ assert.deepEqual(stored.waits[0].public, { ...first.wait, status: 'stopped', reason: 'stopped_by_operator' });
+});
+test('newer unsupported wait cannot hide a restarted pending ACK operation from receipt ordering checks', posix, t => {
+ const f = fixtureLedger(t); f.ledger.init(); const h = f.ledger.prepare(f.binding);
+ f.ledger.mintCapability(h.correlationId, bootstrap); submit(f, h);
+ f.ledger.beginWait(h.correlationId, 'acknowledged', new HandoffBudget(6, f.clock));
+ f.advance(1000); f.ledger.recordWait(h.correlationId, 'timed_out_unknown');
+ f.ledger.beginWait(h.correlationId, 'acknowledged', new HandoffBudget(6, f.clock));
+ const restarted = new HandoffLedger(f.ledger.path, { clock: f.clock });
+ assert.equal(restarted.beginWait(h.correlationId, 'acknowledged', new HandoffBudget(6, f.clock)).wait.status, 'unsupported');
+ const before = readFileSync(join(f.ledger.path, 'ledger.json'), 'utf8');
+ assert.throws(() => restarted.confirm(confirmation(h)), /receipt_order_unprovable/);
+ assert.equal(readFileSync(join(f.ledger.path, 'ledger.json'), 'utf8'), before);
+ assert.notEqual(handoff(restarted, h.correlationId).ack.status, 'acknowledged');
+});
+test('manual confirmation with unprovable restarted pending wait ordering refuses without ACK', posix, t => {
+ const f = fixtureLedger(t); f.ledger.init(); const h = f.ledger.prepare(f.binding);
+ f.ledger.mintCapability(h.correlationId, bootstrap); submit(f, h);
+ f.ledger.beginWait(h.correlationId, 'acknowledged', new HandoffBudget(6, f.clock));
+ const restarted = new HandoffLedger(f.ledger.path, { clock: f.clock });
+ assert.throws(() => restarted.confirm(confirmation(h)), /receipt_order_unprovable/);
+ assert.notEqual(handoff(restarted, h.correlationId).ack.status, 'acknowledged');
+});
+test('known durable timeout proves a new late manual event after restart without rewriting that wait', posix, t => {
+ const f = fixtureLedger(t); f.ledger.init(); const h = f.ledger.prepare(f.binding);
+ f.ledger.mintCapability(h.correlationId, bootstrap); submit(f, h);
+ const pending = f.ledger.beginWait(h.correlationId, 'acknowledged', new HandoffBudget(6, f.clock)); f.advance(1000);
+ f.ledger.recordWait(h.correlationId, 'timed_out_unknown');
+ const restarted = new HandoffLedger(f.ledger.path, { clock: f.clock });
+ const accepted = restarted.confirm(confirmation(h)); assert.equal(accepted.ack.late, true); assert.equal(accepted.wait.status, 'timed_out_unknown');
+ assert.equal(accepted.wait.operationId, pending.wait.operationId);
+});
+test('wait quota exhaustion and insufficient new-wait budget preserve native submission facts', posix, t => {
+ const f = fixtureLedger(t); f.ledger.init(); const h = f.ledger.prepare(f.binding); submit(f, h);
+ for (let i = 0; i < 64; i++) {
+ const wait = f.ledger.beginWait(h.correlationId, 'acknowledged', new HandoffBudget(5, f.clock));
+ assert.equal(wait.wait.reason, 'insufficient_budget'); assert.equal(wait.submission.status, 'submitted');
+ }
+ assert.throws(() => f.ledger.beginWait(h.correlationId, 'acknowledged'), /handoff_wait_quota/);
+ assert.equal(handoff(f.ledger, h.correlationId).state, 'submitted');
+});
+test('expired details compact to an epoch-bound fence, never restoring send or receipt authority', posix, t => {
+ const f = fixtureLedger(t); f.ledger.init(); const h = f.ledger.prepare(f.binding);
+ const capability = f.ledger.mintCapability(h.correlationId, bootstrap); submit(f, h);
+ const raw = receipt(h, capability); f.ledger.acceptReceipt(raw); f.advance(HANDOFF_LIMITS.detailMs); f.ledger.prune();
+ const tombstone = handoff(f.ledger, h.correlationId); assert.equal(tombstone.state, 'unknown'); assert.equal(tombstone.ledgerEpoch, h.ledgerEpoch);
+ assert.equal(tombstone.targetGeneration, h.targetGeneration); assert.equal(tombstone.ack.status, 'unsupported');
+ assert.throws(() => f.ledger.commitEffect(h.correlationId, f.binding), /handoff_history_unavailable/);
+ assert.throws(() => f.ledger.acceptReceipt(raw), /handoff_history_unavailable/);
+});
+test('known restoration quarantines original intents and authentic receipts', posix, t => {
+ const f = fixtureLedger(t); f.ledger.init(); const h = f.ledger.prepare(f.binding);
+ const capability = f.ledger.mintCapability(h.correlationId, bootstrap); submit(f, h); f.ledger.quarantine();
+ assert.equal(handoff(f.ledger, h.correlationId).state, 'unknown');
+ assert.throws(() => f.ledger.prepare(f.binding), /handoff_ledger_quarantined/);
+ assert.throws(() => f.ledger.acceptReceipt(receipt(h, capability)), /handoff_ledger_quarantined/);
+});
+test('byte admission reserves terminal/wait/receipt room and never evicts an existing fence', posix, t => {
+ const f = fixtureLedger(t); f.ledger.init(); const h = f.ledger.prepare(f.binding);
+ const file = join(f.ledger.path, 'ledger.json'), wrapper = JSON.parse(readFileSync(file, 'utf8'));
+ const template = wrapper.data.records[h.correlationId];
+ for (let i = 1; i < 511; i++) {
+ const id = randomUUID(), item = structuredClone(template); item.handoff.correlationId = id;
+ item.binding.payloadDigest = sealedQuotaDigest(f.ledger.path, wrapper.data.epoch, id, f.binding.payloadDigest);
+ wrapper.data.records[id] = item;
+ }
+ writeFileSync(file, JSON.stringify({ digest: payloadDigest(JSON.stringify(wrapper.data)), data: wrapper.data }));
+ const before = readFileSync(file, 'utf8');
+ assert.throws(() => f.ledger.prepare(f.binding), /handoff_ledger_full/);
+ assert.equal(readFileSync(file, 'utf8'), before);
+ assert.equal(handoff(f.ledger, h.correlationId).state, 'validated');
+ // Reserved terminal space still permits the existing intent's one transition.
+ submit(f, h); assert.equal(handoff(f.ledger, h.correlationId).state, 'submitted');
+});
+test('retained tombstones count toward the 10,000 intent limit with no LRU replacement', posix, t => {
+ const f = fixtureLedger(t); f.ledger.init(); const h = f.ledger.prepare(f.binding);
+ const file = join(f.ledger.path, 'ledger.json'), wrapper = JSON.parse(readFileSync(file, 'utf8'));
+ const template = wrapper.data.records[h.correlationId]; template.reserve = 0; template.expired = true;
+ for (let i = 1; i < HANDOFF_LIMITS.intents; i++) {
+ const id = randomUUID(), item = structuredClone(template); item.handoff.correlationId = id;
+ item.binding.payloadDigest = sealedQuotaDigest(f.ledger.path, wrapper.data.epoch, id, f.binding.payloadDigest);
+ wrapper.data.records[id] = item;
+ }
+ writeFileSync(file, JSON.stringify({ digest: payloadDigest(JSON.stringify(wrapper.data)), data: wrapper.data }));
+ const before = readFileSync(file, 'utf8');
+ assert.throws(() => f.ledger.prepare(f.binding), /handoff_ledger_full/);
+ assert.equal(readFileSync(file, 'utf8'), before);
+ assert.equal(handoff(f.ledger, h.correlationId).state, 'unknown');
+});
+test('private receipt rejects incorrect capability encoding, extra fields and lexical versions', () => {
+ const sample = { schemaVersion: 1, kind: 'receipt', ledgerEpoch: randomUUID(), correlationId: randomUUID(), targetGeneration: 'g', receiptId: randomUUID(), capability: Buffer.alloc(32).toString('base64url') };
+ assert.equal(validateReceipt(JSON.stringify(sample)).capability, sample.capability);
+ for (const capability of ['x'.repeat(42), 'x'.repeat(44), sample.capability + '=', 'A'.repeat(42) + 'B']) assert.throws(() => validateReceipt(JSON.stringify({ ...sample, capability })));
+ assert.throws(() => validateReceipt(JSON.stringify({ ...sample, receivedAtUtcMs: 1 })));
+ assert.throws(() => validateReceipt(JSON.stringify(sample).replace('"schemaVersion":1', '"schemaVersion":1.0')));
+});
diff --git a/test/handoff-ledger-privacy.test.ts b/test/handoff-ledger-privacy.test.ts
new file mode 100644
index 0000000..71cf406
--- /dev/null
+++ b/test/handoff-ledger-privacy.test.ts
@@ -0,0 +1,102 @@
+import { test, type TestContext } from 'node:test';
+import assert from 'node:assert/strict';
+import { randomBytes, randomUUID } from 'node:crypto';
+import { chmodSync, existsSync, mkdtempSync, readFileSync, readdirSync, realpathSync, rmSync, statSync, symlinkSync, unlinkSync, writeFileSync } from 'node:fs';
+import { tmpdir } from 'node:os';
+import { join } from 'node:path';
+import { HandoffLedger, payloadDigest, type HandoffBinding } from '../dist/handoff-ledger.js';
+import { HandoffBudget, type Clock } from '../dist/handoff.js';
+
+const posix = { skip: process.platform === 'win32' };
+function fixture(t: TestContext) {
+ const root = realpathSync(mkdtempSync(join(process.env.TASK_TEMP ?? tmpdir(), 'codex-ledger-privacy-')));
+ chmodSync(root, 0o700); t.after(() => rmSync(root, { recursive: true, force: true }));
+ const clock: Clock = { monotonic: () => 100, utc: () => 100000 };
+ const path = join(root, 'ledger'), ledger = new HandoffLedger(path, { clock });
+ const body = 'private low entropy fixture message';
+ const binding: HandoffBinding = { agent: 'codex', destination: 'fixture', target: 'aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa',
+ home: '/fixture/home', generation: 'fixture-generation', writerIdentity: 'fixture-writer', payloadDigest: payloadDigest(body) };
+ ledger.init(); return { root, path, ledger, clock, body, binding, journal: join(path, 'ledger.json'), key: join(path, 'ledger.key') };
+}
+
+test('journal never persists raw payload, payload digest, receipt capability or encryption key', posix, t => {
+ const f = fixture(t), h = f.ledger.prepare(f.binding);
+ const capability = f.ledger.mintCapability(h.correlationId, { privateConsumerValidated: true, compatibleProducerValidated: true });
+ const text = readFileSync(f.journal, 'utf8');
+ assert.equal(text.includes(f.body), false);
+ assert.equal(text.includes(f.binding.payloadDigest), false);
+ assert.equal(text.includes(capability), false);
+ const key = readFileSync(f.key);
+ assert.equal(key.length, 32); assert.equal(statSync(f.key).mode & 0o777, 0o600);
+ assert.equal(text.includes(key.toString('hex')), false); assert.equal(text.includes(key.toString('base64')), false);
+ assert.deepEqual(readdirSync(f.path).sort(), ['ledger.json', 'ledger.key', 'ledger.lock']);
+ assert.equal(JSON.stringify(h).includes(f.binding.payloadDigest), false);
+});
+
+test('encrypted binding survives restart, refuses conflicting body, and keeps receipt and wait evidence', posix, t => {
+ const f = fixture(t), h = f.ledger.prepare(f.binding);
+ const restarted = new HandoffLedger(f.path, { clock: f.clock });
+ assert.equal(restarted.prepare(f.binding, { correlationId: h.correlationId }).correlationId, h.correlationId);
+ const before = readFileSync(f.journal);
+ assert.throws(() => restarted.commitEffect(h.correlationId, { ...f.binding, payloadDigest: payloadDigest('conflict') }), /handoff_binding_conflict/);
+ assert.deepEqual(readFileSync(f.journal), before);
+ restarted.commitEffect(h.correlationId, f.binding); restarted.recordSubmission(h.correlationId, 'submitted');
+ const confirmed = restarted.confirm(JSON.stringify({ schemaVersion: 1, ledgerEpoch: h.ledgerEpoch, correlationId: h.correlationId,
+ targetGeneration: h.targetGeneration, confirmed: true }));
+ assert.equal(confirmed.ack.assurance, 'operator_confirmed');
+ const wait = restarted.beginWait(h.correlationId, 'acknowledged', new HandoffBudget(30, f.clock));
+ assert.equal(wait.wait.status, 'satisfied');
+ const status = new HandoffLedger(f.path, { clock: f.clock }).status(h.correlationId);
+ assert.ok('handoff' in status); assert.equal(status.handoff.ack.status, 'acknowledged');
+ assert.equal(status.handoff.wait.operationId, wait.wait.operationId);
+ assert.equal(readFileSync(f.journal, 'utf8').includes(f.binding.payloadDigest), false);
+});
+
+for (const damage of ['missing', 'short', 'rotated', 'permissions', 'symlink'] as const) {
+ test(`a ${damage} key fails closed without journal changes or key regeneration`, posix, t => {
+ const f = fixture(t), h = f.ledger.prepare(f.binding), before = readFileSync(f.journal);
+ if (damage === 'missing') unlinkSync(f.key);
+ if (damage === 'short') writeFileSync(f.key, 'bad', { mode: 0o600 });
+ if (damage === 'rotated') writeFileSync(f.key, randomBytes(32));
+ if (damage === 'permissions') chmodSync(f.key, 0o644);
+ if (damage === 'symlink') { const other = join(f.root, 'other'); writeFileSync(other, randomBytes(32), { mode: 0o600 }); unlinkSync(f.key); symlinkSync(other, f.key); }
+ const keyBefore = damage === 'missing' ? undefined : readFileSync(f.key);
+ const restarted = new HandoffLedger(f.path, { clock: f.clock });
+ assert.throws(() => restarted.commitEffect(h.correlationId, f.binding));
+ assert.ok('handoffQuery' in restarted.status(h.correlationId));
+ assert.throws(() => restarted.init(), /handoff_ledger_exists/);
+ assert.deepEqual(readFileSync(f.journal), before);
+ if (damage === 'missing') assert.equal(existsSync(f.key), false);
+ else assert.deepEqual(readFileSync(f.key), keyBefore);
+ });
+}
+
+test('rotated key is rejected even for an empty ledger; orphan key is never reused by init', posix, t => {
+ const f = fixture(t), before = readFileSync(f.journal); writeFileSync(f.key, randomBytes(32));
+ assert.throws(() => new HandoffLedger(f.path).prepare(f.binding));
+ assert.deepEqual(readFileSync(f.journal), before);
+ unlinkSync(f.journal); const keyBefore = readFileSync(f.key);
+ assert.throws(() => new HandoffLedger(f.path).init());
+ assert.equal(existsSync(f.journal), false); assert.deepEqual(readFileSync(f.key), keyBefore);
+});
+
+test('ciphertext is authenticated against the original epoch and intent ID', posix, t => {
+ const f = fixture(t), a = f.ledger.prepare(f.binding), b = f.ledger.prepare({ ...f.binding, target: 'bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb' });
+ const original = readFileSync(f.journal, 'utf8'), wrapper = JSON.parse(original);
+ wrapper.data.records[b.correlationId].binding.payloadDigest = wrapper.data.records[a.correlationId].binding.payloadDigest;
+ wrapper.digest = payloadDigest(JSON.stringify(wrapper.data)); writeFileSync(f.journal, JSON.stringify(wrapper));
+ assert.throws(() => new HandoffLedger(f.path).commitEffect(b.correlationId, f.binding), /handoff_ledger_corrupt/);
+ const changedEpoch = JSON.parse(original); changedEpoch.data.epoch = randomUUID();
+ for (const item of Object.values(changedEpoch.data.records) as { handoff: { ledgerEpoch: string } }[]) item.handoff.ledgerEpoch = changedEpoch.data.epoch;
+ changedEpoch.digest = payloadDigest(JSON.stringify(changedEpoch.data)); writeFileSync(f.journal, JSON.stringify(changedEpoch));
+ assert.throws(() => new HandoffLedger(f.path).prepare(f.binding, { correlationId: a.correlationId }), /handoff_ledger_corrupt/);
+});
+
+test('plaintext historical journal is refused without silently migrating or replacing the key', posix, t => {
+ const f = fixture(t), h = f.ledger.prepare(f.binding), wrapper = JSON.parse(readFileSync(f.journal, 'utf8'));
+ wrapper.data.records[h.correlationId].binding.payloadDigest = f.binding.payloadDigest;
+ wrapper.digest = payloadDigest(JSON.stringify(wrapper.data)); writeFileSync(f.journal, JSON.stringify(wrapper));
+ const journalBefore = readFileSync(f.journal), keyBefore = readFileSync(f.key);
+ assert.throws(() => new HandoffLedger(f.path).prepare(f.binding, { correlationId: h.correlationId }), /handoff_ledger_corrupt/);
+ assert.deepEqual(readFileSync(f.journal), journalBefore); assert.deepEqual(readFileSync(f.key), keyBefore);
+});
diff --git a/test/handoff-observation-waits.test.ts b/test/handoff-observation-waits.test.ts
new file mode 100644
index 0000000..c96ab50
--- /dev/null
+++ b/test/handoff-observation-waits.test.ts
@@ -0,0 +1,92 @@
+import { test, type TestContext } from 'node:test';
+import assert from 'node:assert/strict';
+import { chmodSync, mkdtempSync, readFileSync, realpathSync, rmSync } from 'node:fs';
+import { tmpdir } from 'node:os';
+import { join } from 'node:path';
+import { HandoffBudget, type Clock } from '../dist/handoff.js';
+import { HandoffLedger, payloadDigest, type HandoffBinding } from '../dist/handoff-ledger.js';
+const posix = { skip: process.platform === 'win32' };
+function fixture(t: TestContext) {
+ const root = realpathSync(mkdtempSync(join(process.env.TASK_TEMP ?? tmpdir(), 'codex-ho-observe-')));
+ chmodSync(root, 0o700); t.after(() => rmSync(root, { recursive: true, force: true }));
+ let mono = 10;
+ const clock: Clock = { monotonic: () => mono, utc: () => 100000 + mono };
+ const ledger = new HandoffLedger(join(root, 'ledger'), { clock }); ledger.init();
+ const binding: HandoffBinding = { agent: 'codex', destination: 'fixture', target: 'aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa',
+ home: '/fixture/home', generation: 'fixture-generation', payloadDigest: payloadDigest('fixture body') };
+ const h = ledger.prepare(binding, { observeDelivery: true, observationSupported: true });
+ ledger.mintCapability(h.correlationId, { privateConsumerValidated: true, compatibleProducerValidated: true });
+ ledger.commitEffect(h.correlationId, binding); ledger.recordSubmission(h.correlationId, 'submitted');
+ const waits = () => JSON.parse(readFileSync(join(ledger.path, 'ledger.json'), 'utf8')).data.records[h.correlationId].waits.map((w: any) => w.public);
+ const observe = (reader = ledger) => reader.recordObservation(h.correlationId, { clientUserMessageId: h.correlationId, injectionObserved: true }, binding);
+ return { ledger, clock, h, waits, observe, advance: (ms: number) => { mono += ms; } };
+}
+test('one injection resolves every pending delivery wait using its own cutoff even when ACK is newest', posix, t => {
+ const f = fixture(t);
+ const first = f.ledger.beginWait(f.h.correlationId, 'delivered', new HandoffBudget(6, f.clock));
+ f.advance(500);
+ const second = f.ledger.beginWait(f.h.correlationId, 'delivered', new HandoffBudget(6, f.clock));
+ const ack = f.ledger.beginWait(f.h.correlationId, 'acknowledged', new HandoffBudget(6, f.clock));
+ f.advance(500); // First cutoff is inclusive; second cutoff is still in the future.
+ const result = f.observe();
+ assert.equal(result.state, 'delivered'); assert.equal(result.ack.status, 'pending');
+ assert.equal(result.wait.operationId, ack.wait.operationId); assert.equal(result.wait.status, 'pending');
+ assert.deepEqual(f.waits().map((w: any) => [w.operationId, w.status]), [
+ [first.wait.operationId, 'timed_out_unknown'], [second.wait.operationId, 'satisfied'], [ack.wait.operationId, 'pending'],
+ ]);
+});
+test('injection preserves terminal delivery waits and never resets their deadlines', posix, t => {
+ const f = fixture(t);
+ const stopped = f.ledger.beginWait(f.h.correlationId, 'delivered', new HandoffBudget(6, f.clock));
+ f.ledger.recordWait(f.h.correlationId, 'stopped', 'stopped_by_operator');
+ const completed = f.ledger.beginWait(f.h.correlationId, 'delivered', new HandoffBudget(6, f.clock));
+ f.advance(1000); f.ledger.recordWait(f.h.correlationId, 'timed_out_unknown');
+ const before = f.waits();
+ const pending = f.ledger.beginWait(f.h.correlationId, 'delivered', new HandoffBudget(6, f.clock));
+ f.observe(); const observed = f.waits();
+ assert.deepEqual(observed.slice(0, 2), before);
+ assert.equal(observed[0].operationId, stopped.wait.operationId);
+ assert.equal(observed[1].operationId, completed.wait.operationId);
+ assert.equal(observed[2].operationId, pending.wait.operationId); assert.equal(observed[2].status, 'satisfied');
+ f.advance(1000); f.observe(); assert.deepEqual(f.waits(), observed);
+});
+test('restarted observer retains injection but fails every unorderable delivery wait conservatively', posix, t => {
+ const f = fixture(t);
+ f.ledger.beginWait(f.h.correlationId, 'delivered', new HandoffBudget(6, f.clock));
+ f.ledger.beginWait(f.h.correlationId, 'delivered', new HandoffBudget(30, f.clock));
+ const ack = f.ledger.beginWait(f.h.correlationId, 'acknowledged', new HandoffBudget(30, f.clock));
+ const result = f.observe(new HandoffLedger(f.ledger.path, { clock: f.clock }));
+ assert.equal(result.state, 'delivered'); assert.equal(result.observation.injectionObserved, true);
+ assert.notEqual(result.ack.status, 'acknowledged'); assert.equal(result.wait.operationId, ack.wait.operationId);
+ assert.deepEqual(f.waits().slice(0, 2).map((w: any) => [w.status, w.reason]), [
+ ['failed', 'history_unavailable'], ['failed', 'history_unavailable'],
+ ]);
+ assert.equal(f.waits()[2].status, 'pending');
+});
+test('explicit operation timeout resolves the older cutoff without changing a newer pending wait', posix, t => {
+ const f = fixture(t);
+ const first = f.ledger.beginWait(f.h.correlationId, 'delivered', new HandoffBudget(6, f.clock));
+ f.advance(500);
+ const second = f.ledger.beginWait(f.h.correlationId, 'delivered', new HandoffBudget(6, f.clock));
+ f.advance(500);
+ const settled = f.ledger.recordWait(f.h.correlationId, 'timed_out_unknown', undefined, first.wait.operationId);
+ assert.equal(settled.wait.operationId, first.wait.operationId); assert.equal(settled.wait.status, 'timed_out_unknown');
+ assert.deepEqual(f.waits().map((w: any) => [w.operationId, w.status]), [
+ [first.wait.operationId, 'timed_out_unknown'], [second.wait.operationId, 'pending'],
+ ]);
+ const status = f.ledger.status(f.h.correlationId); assert.ok('handoff' in status);
+ assert.deepEqual(status.handoff.wait, second.wait);
+ const before = f.waits();
+ assert.throws(() => f.ledger.recordWait(f.h.correlationId, 'stopped', 'stopped_by_operator', first.wait.operationId), /handoff_wait_terminal/);
+ assert.deepEqual(f.waits(), before);
+});
+test('explicit wait selection rejects unknown IDs and cannot timeout a foreign monotonic clock', posix, t => {
+ const f = fixture(t);
+ const first = f.ledger.beginWait(f.h.correlationId, 'delivered', new HandoffBudget(6, f.clock));
+ f.advance(1000);
+ const foreign = new HandoffLedger(f.ledger.path, { clock: f.clock });
+ assert.throws(() => foreign.recordWait(f.h.correlationId, 'timed_out_unknown', undefined, first.wait.operationId), /receipt_order_unprovable/);
+ assert.throws(() => f.ledger.recordWait(f.h.correlationId, 'stopped', 'stopped_by_operator', 'not-a-uuid'), /invalid_wait_operation_id/);
+ assert.throws(() => f.ledger.recordWait(f.h.correlationId, 'stopped', 'stopped_by_operator', 'bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb'), /handoff_wait_unknown/);
+ assert.equal(f.waits()[0].status, 'pending');
+});
diff --git a/test/package-smoke.ts b/test/package-smoke.ts
index d8080e4..6c3eafb 100644
--- a/test/package-smoke.ts
+++ b/test/package-smoke.ts
@@ -24,7 +24,7 @@ try {
let hash;
for (let index = 0; index < 2; index++) {
const [packed] = JSON.parse(runNpm(['pack', '--ignore-scripts', '--json', '--pack-destination', task]));
- assert.deepEqual(packed.files.map((file: {path: string}) => file.path).sort(), ['UNICODE-LICENSE.txt', 'dist/casefold.js', 'dist/casefold.d.ts', 'dist/help.js', 'dist/help.d.ts', 'dist/output.js', 'dist/output.d.ts', 'CONTRIBUTING.md', 'LICENSE', 'PARITY.md', 'README.ja.md', 'README.ko.md', 'README.md', 'README.zh-CN.md', 'RELEASING.md', 'SECURITY.md', 'VALIDATION.md', 'docs/guide.md', 'docs/guide.ko.md', 'docs/guide.ja.md', 'docs/guide.zh-CN.md', 'docs/api.md', 'docs/design/remote-deployment.md', 'docs/design/ordinary-ssh-responses.md', 'docs/design/typescript-skill-compatibility.md', 'docs/shorthand.md', 'shorthand/sp.sh', 'shorthand/sp.ps1', 'dist/cli.js', 'dist/discovery.js', 'dist/diagnostics.js', 'dist/skill-metadata.js', 'dist/process.js', 'dist/protocol.js', 'dist/replies.js', 'dist/send.js', 'dist/ssh.js', 'dist/ssh-response.js', 'dist/windows.js', 'dist/writer.js', 'dist/index.js', 'dist/index.d.ts', 'dist/cli.d.ts', 'dist/discovery.d.ts', 'dist/diagnostics.d.ts', 'dist/skill-metadata.d.ts', 'dist/process.d.ts', 'dist/protocol.d.ts', 'dist/replies.d.ts', 'dist/send.d.ts', 'dist/ssh.d.ts', 'dist/ssh-response.d.ts', 'dist/windows.d.ts', 'dist/writer.d.ts', 'dist/updates.js', 'dist/updates.d.ts', 'package.json'].sort());
+ assert.deepEqual(packed.files.map((file: {path: string}) => file.path).sort(), ['UNICODE-LICENSE.txt', 'dist/casefold.js', 'dist/casefold.d.ts', 'dist/help.js', 'dist/help.d.ts', 'dist/output.js', 'dist/output.d.ts', 'CONTRIBUTING.md', 'LICENSE', 'PARITY.md', 'README.ja.md', 'README.ko.md', 'README.md', 'README.zh-CN.md', 'RELEASING.md', 'SECURITY.md', 'VALIDATION.md', 'docs/guide.md', 'docs/guide.ko.md', 'docs/guide.ja.md', 'docs/guide.zh-CN.md', 'docs/api.md', 'docs/design/remote-deployment.md', 'docs/design/ordinary-ssh-responses.md', 'docs/design/typescript-skill-compatibility.md', 'docs/shorthand.md', 'shorthand/sp.sh', 'shorthand/sp.ps1', 'dist/cli.js', 'dist/discovery.js', 'dist/diagnostics.js', 'dist/skill-metadata.js', 'dist/process.js', 'dist/protocol.js', 'dist/replies.js', 'dist/send.js', 'dist/ssh.js', 'dist/ssh-response.js', 'dist/windows.js', 'dist/writer.js', 'dist/index.js', 'dist/index.d.ts', 'dist/cli.d.ts', 'dist/discovery.d.ts', 'dist/diagnostics.d.ts', 'dist/skill-metadata.d.ts', 'dist/process.d.ts', 'dist/protocol.d.ts', 'dist/replies.d.ts', 'dist/send.d.ts', 'dist/ssh.d.ts', 'dist/ssh-response.d.ts', 'dist/windows.d.ts', 'dist/writer.d.ts', 'dist/updates.js', 'dist/updates.d.ts', 'dist/handoff.js', 'dist/handoff.d.ts', 'dist/handoff-ledger.js', 'dist/handoff-ledger.d.ts', 'dist/handoff-operations.js', 'dist/handoff-operations.d.ts', 'package.json'].sort());
const next = createHash('sha256').update(readFileSync(join(task, packed.filename))).digest('hex');
if (hash) assert.equal(next, hash, 'same build must produce identical tarball');
hash = next;
diff --git a/test/skill-guidance.test.ts b/test/skill-guidance.test.ts
index edbcc27..3096a5d 100644
--- a/test/skill-guidance.test.ts
+++ b/test/skill-guidance.test.ts
@@ -60,7 +60,7 @@ test('TS skill baseline list and guarded Codex dry-run examples never submit', t
const env = { ...process.env, HOME: root, USERPROFILE: root, CODEX_HOME: '', SESSION_PEER_CODEX_HOMES: '[]',
CLAUDE_CONFIG_DIR: join(root, '.claude'), ANTHROPIC_CONFIG_DIR: '' };
function invoke(args: string[]) {
- const result = spawnSync(process.execPath, [cli, ...args], { env, encoding: 'utf8', input: 'fixture message', timeout: 15000 });
+ const result = spawnSync(process.execPath, [cli, ...args], { env, encoding: 'utf8', input: args.includes('--message') && args[args.indexOf('--message') + 1] === '-' ? 'fixture message' : undefined, timeout: 15000 });
assert.ifError(result.error); assert.equal(result.signal, null);
return result;
}
diff --git a/test/transport.test.ts b/test/transport.test.ts
index 0783d83..a9c6ad6 100644
--- a/test/transport.test.ts
+++ b/test/transport.test.ts
@@ -8,6 +8,7 @@ import { createServer } from 'node:net';
import { once } from 'node:events';
import { fileURLToPath } from 'node:url';
import { probeLock, inspectWriter } from '../dist/writer.js';
+import { send } from '../dist/send.js';
import { reply, envelope } from '../dist/protocol.js';
const cli = fileURLToPath(new URL('../dist/cli.js', import.meta.url));
@@ -352,3 +353,24 @@ test('Windows --remote-bin doubles every PowerShell single-quote variant', async
const decoded = Buffer.from(readFileSync(calls, 'utf8').trim().split(' ').at(-1)!, 'base64').toString('utf16le');
assert.equal(decoded, "& 'C:\\a''\u2018\u2018\u2019\u2019\u201a\u201a\u201b\u201b;calc;#' --version; if ($null -eq $LASTEXITCODE) { exit 1 }; exit $LASTEXITCODE");
});
+
+test('opt-in handoff context stays bound to selected generation and rechecks storage before fencing', async t => {
+ const {path} = setup(t), home = join(path,'home'); database(home); await holder(t,path,home);
+ const owner = await inspectWriter(home,id); const crypto = await import('node:crypto'); const expected = crypto.createHash('sha256').update(owner.identity).digest('hex');
+ let effects = 0;
+ await assert.rejects(send({ to:`codex:${id}`,home,codexBin:process.execPath,message:'fixture',hooks:{
+ resolved(context) {assert.equal(context.generation,expected);writeFileSync(join(home,'config.toml'),'sqlite_home = "/fixture/relocated"');},
+ beforeEffect() {effects++;}
+ }}), /unsupported_codex_sqlite_home/);
+ assert.equal(effects,0);
+});
+
+test('ordinary SSH results cannot opt themselves into handoff or claim unsolicited ACK', async t=>{
+ const {path,env}=setup(t); const fake=join(path,'ssh'),log=join(path,'ssh-count-\'"\\\n\u2028\u2029');writeFileSync(log,'');
+ const h={schemaVersion:1,correlationId:id,ledgerEpoch:id,state:'acknowledged',submission:{status:'submitted'},observation:{status:'not_requested',injectionObserved:false},ack:{status:'acknowledged',assurance:'token_possession',receivedAtUtcMs:1,late:false},wait:{for:'none',status:'not_requested'},targetGeneration:'fixture',decisionOwner:'sender_operator',retry:{allowed:false,reason:'receiver_dedup_unavailable'},nextActions:['reconcile']};
+ // Fixture values are data, never part of the executable JavaScript source.
+ const config={log,handoff:{...h,targetGeneration:'\');throw new Error("fixture data executed");//\u2028\u2029'}};
+ writeFileSync(fake+'.json',JSON.stringify(config),{mode:0o600});
+ writeFileSync(fake,`#!${process.execPath}\n${sshConfigUser}if(process.argv.at(-1).includes('--version')){console.log('session-peer 0.3.6 (typescript)');process.exit(0);}const fs=require('node:fs');const config=JSON.parse(fs.readFileSync(__filename+'.json','utf8'));let input='';process.stdin.on('data',p=>input+=p);process.stdin.on('end',()=>{fs.appendFileSync(config.log,'1');console.log(JSON.stringify({schemaVersion:1,command:'send',host:'fixture',ok:true,status:'posted',submitted:true,consumptionConfirmed:false,dryRun:false,target:{agent:'claude',pid:123,name:null},remoteMarker:'kept',handoff:config.handoff,handoffQuery:{status:'unknown'},handoffWarning:'fixture-only'}));});`,{mode:0o700});
+ const r=await invoke(['send','--host','fixture','--to','123','--message','fixture','--no-from','--no-reply-to'],{...env,PATH:path+delimiter+(env.PATH??'')});assert.equal(r.code,0);assert.equal(r.status,'posted');assert.equal(r.submitted,true);assert.equal(r.target.name,null);assert.equal(r.remoteMarker,'kept');assert.equal('handoff' in r,false);assert.equal('handoffQuery' in r,false);assert.equal('handoffWarning' in r,false);assert.equal(readFileSync(log,'utf8'),'1');
+});