Home
Design notes

Generated enforcement adapters

Secret-blind Claude, Codex, and OpenCode hook adapters backed by supervisor-owned verification.

The canonical evaluator in internal/enforcement is provider-neutral. The internal/adapters package is the narrow provider edge: it renders current Claude command hooks, Codex command hooks, and an OpenCode plugin, and every generated command invokes one absolute installed radioactive_ralph path. Adapters normalize provider payloads locally and send only this finite tuple to the supervisor:

  • adapter: claude, codex, or opencode
  • event: PostToolUse or Stop
  • Ralph's opaque managed session id

Tool arguments, tool results, transcripts, prompts, environment snapshots, and provider credentials never cross the hook IPC boundary. An ordinary session without RALPH_MANAGED_SESSION_ID is a no-op and does not even read hook stdin. A provider launch that is not managed also strips stale inherited Ralph hook coordinates, so a contaminated service environment cannot accidentally enroll an ordinary turn. A launch with only one of the managed session or endpoint coordinates is rejected before the provider subprocess starts. A managed malformed event, missing executable, missing supervisor, protocol mismatch, or rejected verification fails closed with static output. Claude uses its exit-2/plain-stderr block contract, Codex uses its exit-0/structured-JSON decision contract, and the Ralph-managed OpenCode launcher uses a finite JSON status plus exit 2. Input and environment values are never echoed.

Completion authority

PostToolUse refreshes the live worker/session heartbeat. Stop resolves the opaque session to its current owner-guarded store claim. The first Stop writes a finite pending verdict, starts orchestrator-owned verification, and blocks the Stop immediately; it never parks the provider's hook process behind a long test suite. A later Stop is allowed only after every task in the turn has a durable passed verdict. PostToolUse deletes prior verdicts, so checkout progress always forces a fresh check. Pending work is deduplicated in-process and safely restarted after a supervisor restart.

The hook check does not mark a task done. After the provider exits, bounded authoritative evidence returns through the normal runner and VerifyAndCompleteAs repeats the check and records the verdict. That second check closes the hook-to-process-exit race. A native-fanout turn must pass every assigned task.

Adapter v1 manages only tasks with explicit mechanical acceptance. A malformed acceptance document, an empty object, or an object without a non-empty command or file predicate is not mechanical acceptance and cannot enable the hook gate. A judgment-only task needs bounded assistant evidence that is not available at the common secret-blind hook boundary, so the orchestrator deliberately omits the managed marker for that turn. Treating Stop, process exit, or hook success as judgment evidence would violate the completion contract; trapping the turn in an infinite Stop loop would violate the never-block contract. A mixed native fanout is therefore entirely unmanaged rather than partially gated.

Installation boundary

radioactive_ralph adapters install --target <directory> builds a content-addressed release, atomically switches <directory>/current, and then atomically records that verified non-secret directory as Ralph's active adapter target. Managed launches discover a custom install without an extra shell environment dependency. An explicit RALPH_ADAPTER_ROOT remains the deployment override and takes precedence; a malformed or symlinked active selector fails closed instead of silently falling back. The bundle contains:

  • bin/radioactive_ralph
  • claude-hooks.json
  • codex-hooks.toml
  • opencode-plugin.js
  • manifest.json

It also owns a private runtime parent outside the immutable release. Every managed OpenCode launch receives a unique 0700 home/config pair below that parent, and Ralph removes the pair after provider and final-Stop completion. Concurrent workers therefore never share writable config, package, or cache state. Cleanup restores traversal on launch-private directories without following symlinks and fails provider completion if removal cannot be proven, so copied credentials cannot be silently stranded. OpenCode may bootstrap package/cache state there, but Ralph rejects every documented config, plugin, command, agent, mode, skill, and compatible global-skill discovery entry point before each launch. Ralph disables project config and supplies exactly the reviewed generated plugin through the finite environment config; ordinary unmanaged OpenCode runs remain transparent. The managed launcher places the real provider in its own process group and reclaims every non-zombie member before submitting final Stop. Ralph owns the provider output pipes and waits for them to close after that proof. A provider that exits while a background tool keeps running therefore cannot mutate the checkout during or after acceptance; cleanup uncertainty fails closed without a Stop request. Native Windows rejects managed OpenCode coordinates before starting the provider; this completion-authority path is supported on macOS, Linux, and WSL.

The installer writes files under a private directory, syncs them before publication, verifies an existing content-addressed release byte-for-byte through no-follow regular-file handles, and keeps prior releases for rollback. Generated commands point to <directory>/current/bin/radioactive_ralph, not a bare command on inherited PATH. This directly prevents the service/minimal-environment failure PostToolUse hook exited with code 127.

Installing a bundle does not edit live Claude, Codex, or OpenCode user configuration. Deployment must first feature-probe the installed provider versions, merge the reviewed fragment without deleting unrelated hooks, replay the parity/canary fixtures, and only then enable it. OpenCode's plugin API does not provide process-level completion authority. The current managed-adapter support guarantee is OpenCode 1.18.18, enforced by the explicit live version probe; the older 1.18.3 parser capture is historical protocol evidence only. session.idle is a notification, and OpenCode 1.18.18 can log a rejected plugin hook while opencode run still exits zero. The generated plugin therefore reports only PostToolUse progress. For a managed turn Ralph starts the real OpenCode CLI through an absolute, content-verified launcher in its own process group and adds a random, per-launch process scope inherited by tools. After the provider exits, Ralph keeps the leader waitable while it reclaims both the original process group and any scope member that escaped with setpgid(2), setsid(2), or reparenting. Darwin audit tokens and Linux pidfds make each kill stable against PID reuse. Ralph never serializes or includes scope values in its hook payloads, errors, logs, or generated artifacts, and Stop is not submitted until the scope is proven empty. Ralph preserves any genuine provider nonzero or signal exit, and only after provider success synchronously submits the finite Stop event. A missing or failed progress-hook process is therefore non-fatal to the provider tool call; it cannot replace or weaken the launcher's fail-closed final check. A normal verification_started or verification_pending reply is polled for at most 12 minutes, covering the supervisor's bounded 10-minute acceptance check without turning pending work into a false provider failure. The wrapper emits only a static progress line while it waits. The first line is synchronous before the first Stop RPC; later lines recur at one third of the resolved provider stall lease. Managed OpenCode fails closed when that interval would be below 100 milliseconds (a stall lease below 300 milliseconds), bounding heartbeat volume under the provider's raw PTY evidence cap. Unavailable, timed-out, malformed, or failed verification returns the static OpenCode JSON protocol and exit 2. The managed launch adds only its exact launch-private home and config directories to the containment write allowances OpenCode needs for package bootstrap; it never retains the unmanaged real-state allowance or grants the adapter/release root. HOME and every XDG state root resolve inside that private home. Ralph copies only the documented, bounded regular XDG_DATA_HOME/opencode/auth.json credential file into the ephemeral data root with mode 0600 so an ordinarily authenticated OpenCode remains authenticated; credential bytes never enter generated artifacts, arguments, logs, or finite failure output. Cached skills, model state, and global config do not enter the managed turn. No-tool runs take this same launcher finalization path, and sanitized PATH cannot produce the former bare-hook code-127 failure. Ralph's supervisor/reaper remains the load-bearing recovery authority.

The Linux CI leg also re-applies the guarded versioning, strict-decoding, coordinate, acceptance, ownership, live-binding, deduplication, and progress invalidation defects one at a time in isolated source trees. Each mutation must land at its exact source line, compile, and make its named regression test fail for the expected assertion before the ordinary suite can be considered meaningful.