Repository navigation
feat(handoff): add fenced local receipt, queue and wait runtime candidate - #290
Conversation
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 7f38b63290
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| text = read_message(args) | ||
| binding, generation, _ = handoff_binding(args, text) | ||
| epoch, record = ledger.prepare(binding, generation, native_context=handoff_native_context(args)) |
There was a problem hiding this comment.
Validate the body before reserving a prepared intent
When the private file is empty/whitespace-only or contains between 1,000,001 and 4,000,000 ASCII characters, read_message accepts it and prepare permanently reserves a successful intent, but every subsequent send with that correlation ID fails the check_message validation before reaching ledger.prepare. Because the binding requires the same payload digest and there is no deletion or fence eviction, this produces an unusable intent that permanently consumes ledger quota. Apply the normal message validation before committing the prepared record.
Useful? React with 👍 / 👎.
| supported = (goal == "acknowledged" and record["generation"] is not None | ||
| and record["capability"] is not None and not record["capability"]["revoked"]) | ||
| wait = {"for": goal, "status": initial_status or ("pending" if supported else "unsupported"), |
There was a problem hiding this comment.
Satisfy later waits from an existing manual acknowledgement
When a correlation-only Claude submission is acknowledged through handoff confirm without first minting a capability, the record correctly enters the acknowledged state, but a subsequent handoff wait --wait-for acknowledged is initialized as unsupported because supported requires a capability and ignores record["ack"]. The command therefore exits 1 while returning a handoff whose current state and ACK are already acknowledged; an existing valid ACK should make the new wait immediately satisfied regardless of whether its assurance came from a capability.
Useful? React with 👍 / 👎.
| if (before.st_dev, before.st_ino, before.st_size, before.st_mtime_ns) != (info.st_dev, info.st_ino, info.st_size, info.st_mtime_ns): | ||
| raise handoff_error("unsafe_message_file") | ||
| raw = stream.read(4 * MAX_MESSAGE_CHARS + 1) | ||
| final = os.fstat(stream.fileno()) | ||
| if (info.st_size, info.st_mtime_ns) != (final.st_size, final.st_mtime_ns) or len(raw) > 4 * MAX_MESSAGE_CHARS: |
There was a problem hiding this comment.
Recheck owner-only permissions on the opened message file
If the message file is changed from mode 0600 to group/world-readable between handoff_private_stat and either fstat, these comparisons still accept it because they omit st_mode, st_uid, and st_ctime; chmod does not need to change size or mtime. The handoff then reads and submits a file that no longer satisfies the advertised owner-only requirement. Validate the opened and final descriptor as a private owner-owned regular file, rather than checking only the pre-open pathname snapshot.
Useful? React with 👍 / 👎.
| if action == "prepare": | ||
| sub.add_argument("--to", required=True) | ||
| sub.add_argument("--message-file", required=True) | ||
| sub.add_argument("--codex-home") | ||
| sub.set_defaults(message=None, message_option=None, b64=None, host=[], device=None, target_generation=None) |
There was a problem hiding this comment.
Allow inactive Codex homes during preparation
For an intentionally inactive Codex thread, ordinary sending requires --codex-home together with --allow-inactive-codex-home, but the handoff prepare parser exposes only the former. Consequently resolve_codex_home always sees the opt-in as false and rejects preparation, so users cannot create the prepared Codex context that a later send with the supported inactive-home flag must match. Add the same explicit inactive-home option to the prepare command.
Useful? React with 👍 / 👎.
| record.update(phase="quarantined", submission="unknown", native=None, ack=None, pendingReceipt=None) | ||
| if record["capability"]: | ||
| record["capability"]["revoked"] = True | ||
| for wait in record["waits"]: | ||
| if wait["status"] == "pending": | ||
| wait.update(status="failed", reason="history_unavailable") |
There was a problem hiding this comment.
Clear satisfied waits when quarantining restored history
When the restored ledger contains an acknowledged intent whose wait is already satisfied, quarantine clears the ACK and native submission facts but leaves that wait unchanged because only pending waits are rewritten. The resulting record loads successfully, yet handoff_public rejects it because a satisfied acknowledged wait no longer has an ACK, so status on this known quarantined record returns a generic invalid-handoff failure instead of the required retained unknown state. Rewrite satisfied waits to a history-unavailable terminal state when removing their supporting evidence.
Useful? React with 👍 / 👎.
Summary
Add an opt-in Handoff v1 runtime candidate: a same-machine POSIX Claude receipt route, correlation-only local Codex submission, and single-destination POSIX source-streamed SSH correlation-only Claude/Codex submission, with bounded message stdin. This is runtime implementation, not another design-only PR. #181 remains open because the remaining acceptance gates below are not implemented.
Related: #181. Frozen design PR #261 and prerequisites #283–#289 are now merged through main
75cfb4f4a1778cce664ae1d6dc3c14d6edcf6b22. Final candidate7f38b63290199a8928beb7220278869ddcee4d88integrates that exact main, preserving both argument parsers, selected Codex storage guard, peer framing, native generation checks, connection-bound SSH identity, wake-chain safeguards, passive shorthand and protected setup. The user authorized normal protected merge and version preparation, not publication or operations.Companion documentation coordination: session-peer-skill PR #30. Published Python 1.0.4 and its existing skill snapshot do not implement this new handoff runtime or new framing contract; no runtime release or global skill installation is part of this PR.
Type
Checklist
Verified against
Final integrated candidate
7f38b63290199a8928beb7220278869ddcee4d88after all prerequisite merges:-Iimports/version, both parser contracts, setup/wake/handoff flags, passive shorthand resources, empty-home refusal, initialized empty-home JSON list and pip check pass. No model/native session is started.2c91632576c631a31b13811fdb3236177f36804a629c7adec1e84a6dfd5ed760; sdist:ac95838fa1af4c298e2655c23c4ef5820eb4d3dc6d6c8f857e80d561e6c848e1. These are test artifacts, not published release files or a version bump.7f38b63290199a8928beb7220278869ddcee4d88: all 28 checks including the release gate succeeded in CI run 37891144424. The PR is ready for review under normal protections; GitHub's review/comment APIs have returned no automatic review findings at the reporting snapshot. Earlier-head results below are historical, not final acceptance.a12a37bfailed one Ubuntu 3.13 positive early-receipt fixture, which used a six-second total leaving only one second for setup/effect. Its recipient timed out and the sender conservatively refused; the original inner reason was not captured, so this is not a uniquely proven CI root cause. Final7f38b63changes only tests: positive fixtures request ten seconds (five-second effect budget), all waits remain bounded, fixed stage/native-error diagnostics omit body/capability, and an actual expired one-second-cutoff test proves refusal before socket creation. All generation/early-receipt/privacy assertions and production deadlines remain unchanged. Both positive paths passed five repeated runs (10 tests, 3.272 seconds).Earlier implementation evidence:
Exact local candidate
8ec6cf98a357f72df787c3d4fec5fede3224a57c; macOS arm64/Python 3.14.8, owned temporary SQLite databases, inboxes, pipes, streamed fake SSH and fake CLI processes only. No actual agent/model send, installation or service change. Included #286 follow-up and its already-reviewed owned restarted-inbox and unsupported Codex generation regression prerequisites. An initial combined test failed because the restarted-inbox prerequisite fixture was absent; including it fixed the confirmed test setup error. Final8ec6cf9adds only the approved missing generation regression to the independently reviewed runtime head80f0acc.80f0acc: 182 pass, 21.484 seconds. At exact final head, affected generation suites: 21 pass, 0.930 seconds.python3 -m unittest discover -s tests/docsat exact final head: 54 pass, 2.131 seconds.--check,git diff --check, clean branch and frozen hashes: pass.80f0accin a private packaging-only environment: 837 tests, 807 passed and 30 explicitly skipped, 72.142 seconds; exit 0. The final test-only addition was subsequently rerun in the affected suites, not attributed to that earlier full result. Asyncio slow-callback diagnostics were observed, not ResourceWarning failures. Optional Relay/MCP dependencies and platform/live integrations are not inferred from skipped tests.e808df9848af7a708b84b8722798b02a5248c5aereceived three bounded read-only reviews: 160 focused tests, an independent 29 remote tests, and another independent 70 remote/backend/process tests all passed. Subsequent changes preserve local SSH-self limits with two regression tests and integrate independently approved feat(identity): pin Claude sends to observed process generations #286 changes. These reviews do not approve issue closure, release, live agents or unsupported routes. No external model CLI or actual model/agent test was used.80f0accfollow-up received root and independent read-only scoped acceptance, including independently run 55 and 35 selected tests. The final8ec6cf9approved test-only addition changes no runtime or frozen source.02d84c533946b4903884ff8fcb21e06274b582b2passed all 24 CI jobs, including macOS Python 3.9 clock fixtures. Its test-only slow partial-frame correction accepts expected BrokenPipe closure without masking aggregate deadline renewal. New-head CI/review must complete; previous results are not current-head acceptance.8ec6cf98a357f72df787c3d4fec5fede3224a57c: all 24 current-head checks succeeded, including the release gate, in CI run 37876295009. This is CI acceptance, not a release or live-agent/remote-host test.Darwin uses typed
mach_absolute_time/mach_timebase_infothroughout sender/journal/collector, avoiding CPython 3.9's process-local monotonic epoch. Native adapter duration translation samples native first/shared second and does not renew the budget; continuity is namespaced. Source evidence: CPython 3.9.25 commit0bbaf5de9744ae1acea3e2c9ad2257d1cc68e847,Python/pytime.c. Local Python 3.9 is unavailable; actual macOS 3.9 acceptance comes from CI, not a local claim.Positive protected-handler fixtures use an owned 0700 wrapper delegating to the test interpreter; this does not certify unsafe hosted toolcache permissions. Production script/interpreter protections remain unchanged.
Frozen document SHA-256:
46a953010c49214bd8f969412f26713384fbe09a0ba78ebf0fef75da40d0d889.Frozen fixture SHA-256:
963b94e78f510c2535e405813558275d512a5bfe6bbd99ffdf0994b4d2e6e441.Synthetic frozen-contract tests check schema/vectors, not real engine acceptance.
Notes / remaining acceptance gates
#181 remains open. Codex ACK and body-free precisely correlated injection observation, Windows bounded native pipes/process cleanup, paired devices, installed reverse-SSH receipt bootstrap and opt-in fanout are not implemented. Source-streamed SSH correlation-only submission is implemented with owned offline fixtures, not live host/native-agent acceptance. Public remote no-effect preparation tooling remains incomplete. Required unsupported evidence refuses before effect. Canonical thread/home/writer context is not atomic native incarnation or consumption proof; #284's effective native configuration/shared-daemon proof boundary remains open.
Cross-boot retention review, restoration/reconciliation CLI and stale collector lifecycle tooling remain. Known restoration has an explicit quarantine API; unmarked rollback/cloning cannot be reliably detected. Native filesystem/process identity/fsync calls cannot be forcibly cancelled: bounded socket/process budgets are not universal hard-real-time guarantees.
Receipt authority is token possession, not model identity or tool approval. The intended recipient's native storage may retain it; same-user readers may acquire that authority outside our control. No transcript polling, turn-completion ACK inference, automatic reply/retry/fallback, operational change, version bump, tag or publishing.