fix(mosaic): wire constrained recovery runtime boundaries

This commit is contained in:
wjarvis mos-comms
2026-07-19 20:03:16 -05:00
parent f4beedc3e7
commit 95681510c1
14 changed files with 687 additions and 102 deletions

View File

@@ -15,6 +15,9 @@ Each connection carries exactly one UTF-8 JSON object followed by one newline, c
- `begin_verification`: authenticated identity, runtime (`claude` or `pi`), cycle `binding`, and a TTL no greater than 300 seconds. The broker revokes existing authority first, enters `PENDING_VERIFICATION`, and returns a single-use promotion token.
- `begin_recovery`: the constrained recovery entrypoint. It rejects caller-provided receipt/challenge fields and delegates to the same `begin_verification` transition, but reports `PENDING_DELIVERY` and marks the volatile cycle as recovery-owned.
- `complete_recovery`: authenticated identity only. It rejects caller-provided receipt/challenge fields, obtains the current recovery challenge only from broker state, and delegates to the same trusted-observer → evidence → consume → promote sequence. An observation failure revokes recovery authority; retry starts a fresh challenge.
The daemon owns a second protected production observer socket (mode `0600`) unless a private `--test-observer-file` fixture is selected. That transport accepts only the exact `record_runtime_observation` schema after kernel `SO_PEERCRED` plus the existing anchor/ancestry authentication; it validates the pending runtime/generation before storing one finalized assistant entry for the in-process `RuntimeReceiptObserver`. It is **not** a broker request action. Claude sends its latest assistant entry from the Stop-hook transport; Pi sends only finalized `message_end` assistant content. The public broker socket continues to reject request-supplied `latest_assistant_message` in begin, observe, and complete paths.
- `promote_lease`: authenticated identity plus the exact pending promotion token. The broker commits token consumption before making `VERIFIED` visible.
- `revoke_lease`: authenticated observer signal; deletes pending tokens and makes the session `UNVERIFIED` immediately. WI-3 Claude/Pi hooks send this existing action; `runtime` and bounded `reason` fields are diagnostic input only and never identity authority.
- `authorize_tool`: authenticated identity, runtime, and exact runtime-reported tool name. The broker returns an explicit allow/deny decision from the whole-class policy and current lease.

View File

@@ -1,13 +1,13 @@
#!/usr/bin/env python3
"""P6 constrained-recovery probe; BUILT ONLY and Mos-gated.
DO NOT self-fire. Under Mos authorization only:
DO NOT self-fire. Under Mos authorization only:
python3 -I -S -B docs/compaction-refresh/probes/p6_constrained_recovery.py
The default three isolated runs each start a fresh shipped daemon on a private
Unix socket and drive the shipped recovery *command* out of process. This
probe never resets broker state, replaces the observer seam, or reimplements
promotion: it exercises `recover-context.py` and the daemon's real socket.
The default three isolated runs launch the shipped daemon plus its production
observer transport on private sockets. The driver invokes the shipped recovery
command and adapter gate identity; it never resets broker state, mocks promote,
or taps a live model-output stream.
"""
from __future__ import annotations
@@ -31,8 +31,12 @@ HERE = Path(__file__).resolve().parent
REPOSITORY = HERE.parents[2]
TOOLS = REPOSITORY / "packages/mosaic/framework/tools/lease-broker"
DAEMON = TOOLS / "daemon.py"
GATE = TOOLS / "mutator-gate.py"
RECOVERY_COMMAND = TOOLS / "recover-context.py"
OBSERVER_CLIENT = TOOLS / "receipt-observer-client.py"
FRAGMENTS = TOOLS / "normative_fragments.py"
CLAUDE_SETTINGS = REPOSITORY / "packages/mosaic/framework/runtime/claude/settings.json"
PI_EXTENSION = REPOSITORY / "packages/mosaic/framework/runtime/pi/mosaic-extension.ts"
def load_shipped_fragments():
@@ -76,35 +80,72 @@ def wait_ready(process: subprocess.Popen[str], socket_path: Path) -> None:
raise TimeoutError("shipped daemon did not create private probe socket")
def recovery_command(arguments: list[str], environment: dict[str, str]) -> dict[str, object]:
def run_json(command: list[str], environment: dict[str, str], input_value: object | None = None) -> dict[str, object]:
completed = subprocess.run(
[sys.executable, "-I", "-S", "-B", str(RECOVERY_COMMAND), *arguments],
command,
input=None if input_value is None else json.dumps(input_value),
text=True,
capture_output=True,
env=environment,
check=False,
)
if not completed.stdout.endswith("\n"):
raise AssertionError(f"recovery command omitted framed result: {completed.stderr!r}")
raise AssertionError(f"command omitted framed result: {completed.stderr!r}")
reply = json.loads(completed.stdout)
if not isinstance(reply, dict):
raise AssertionError("recovery command result is not an object")
raise AssertionError("command result is not an object")
return reply
def run_once(index: int) -> None:
# Parity guard: this is an out-of-process driver of the shipped recovery
# entrypoint, not a shadow receipt/promotion implementation.
source = RECOVERY_COMMAND.read_text(encoding="utf-8")
if '"action": "begin_recovery"' not in source or '"action": "complete_recovery"' not in source:
def gate_recovery(runtime: str, phase: str, environment: dict[str, str]) -> None:
command = [sys.executable, "-I", "-S", "-B", str(GATE), "--runtime", runtime]
if runtime == "claude":
command.extend(["--recovery-command", str(RECOVERY_COMMAND)])
recovery_invocation = (
f"python3 {RECOVERY_COMMAND} begin --construction /tmp/p6.json "
"--compaction-epoch 1 --request-epoch 1"
if phase == "begin"
else f"python3 {RECOVERY_COMMAND} complete"
)
value = {"tool_name": "Bash", "tool_input": {"command": recovery_invocation}}
else:
value = {"tool_name": "mosaic_context_recover"}
completed = subprocess.run(command, input=json.dumps(value), text=True, capture_output=True, env=environment, check=False)
if completed.returncode != 0:
raise AssertionError(f"{runtime} recovery invocation remained gated: {completed.stderr!r}")
def record_production_observation(runtime: str, message: str, root: Path, environment: dict[str, str]) -> None:
command = [sys.executable, "-I", "-S", "-B", str(OBSERVER_CLIENT), "--runtime", runtime]
if runtime == "claude":
transcript = root / "claude-transcript.jsonl"
transcript.write_text(json.dumps({"message": {"role": "assistant", "content": message}}) + "\n", encoding="utf-8")
payload = {"transcript_path": str(transcript)}
command.append("--latest-entry")
else:
payload = {"latest_assistant_message": message}
completed = subprocess.run(command, input=json.dumps(payload), text=True, capture_output=True, env=environment, check=False)
if completed.returncode != 0:
raise AssertionError(f"{runtime} production observer transport refused: {completed.stderr!r}")
def run_once(index: int, runtime: str) -> None:
# Parity guard: drive the shipped command and the repaired adapter/observer
# bytes, not a shadow receipt or promotion implementation.
recovery_source = RECOVERY_COMMAND.read_text(encoding="utf-8")
if '"action": "begin_recovery"' not in recovery_source or '"action": "complete_recovery"' not in recovery_source:
raise AssertionError("P6 parity guard: recovery command no longer drives shipped broker entrypoints")
if "--recovery-command" not in CLAUDE_SETTINGS.read_text(encoding="utf-8"):
raise AssertionError("P6 parity guard: Claude recovery mapping is missing")
if "const RECOVERY_TOOL = 'mosaic_context_recover'" not in PI_EXTENSION.read_text(encoding="utf-8"):
raise AssertionError("P6 parity guard: Pi recovery tool mapping is missing")
fragments = load_shipped_fragments()
root = Path(tempfile.mkdtemp(prefix=f"mosaic-p6-recovery-{index}-"))
os.chmod(root, 0o700)
socket_path = root / "broker.sock"
observer_socket = root / "observer.sock"
state_path = root / "state.json"
observer_path = root / "test-observer.json"
construction_path = root / "construction.json"
content = b"P6 constrained recovery fixture\n"
construction = {
@@ -120,7 +161,7 @@ def run_once(index: int) -> None:
os.chmod(construction_path, 0o600)
process = subprocess.Popen(
[sys.executable, "-I", "-S", "-B", str(DAEMON), "--socket", str(socket_path),
"--state", str(state_path), "--test-observer-file", str(observer_path)],
"--state", str(state_path), "--observer-socket", str(observer_socket)],
stdin=subprocess.DEVNULL,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
@@ -135,7 +176,7 @@ def run_once(index: int) -> None:
built = fragments.build_payload_from_wire(construction)
normal = request(socket_path, {
"action": "begin_verification", "session_id": session_id, "runtime_generation": 1,
"runtime": "pi", "construction": construction,
"runtime": runtime, "construction": construction,
"binding": {"compaction_epoch": index, "request_epoch": index + 100,
"h_source": built.h_source, "h_payload": built.h_payload, "schema_version": 1},
})
@@ -146,13 +187,15 @@ def run_once(index: int) -> None:
environment = {
**os.environ,
"MOSAIC_LEASE_BROKER_SOCKET": str(socket_path),
"MOSAIC_RECEIPT_OBSERVER_SOCKET": str(observer_socket),
"MOSAIC_LEASE_SESSION_ID": session_id,
"MOSAIC_RUNTIME_GENERATION": "1",
"MOSAIC_LEASE_RUNTIME": "pi",
"MOSAIC_LEASE_RUNTIME": runtime,
}
recovery = recovery_command([
"begin", "--construction", str(construction_path), "--compaction-epoch", str(index + 10),
"--request-epoch", str(index + 110),
gate_recovery(runtime, "begin", environment)
recovery = run_json([
sys.executable, "-I", "-S", "-B", str(RECOVERY_COMMAND), "begin", "--construction", str(construction_path),
"--compaction-epoch", str(index + 10), "--request-epoch", str(index + 110),
], environment)
challenge = recovery.get("receipt_challenge")
receipt = recovery.get("receipt")
@@ -161,32 +204,26 @@ def run_once(index: int) -> None:
if challenge == normal_challenge:
raise AssertionError("recovery reused a normal-path challenge")
# C4: a normal-path receipt is not an admissible recovery receipt.
observer_path.write_text(json.dumps({
"session_id": session_id, "runtime_generation": 1, "latest_assistant_message": normal_receipt,
}), encoding="utf-8")
os.chmod(observer_path, 0o600)
refused = recovery_command(["complete"], environment)
# C4: production observer content is still exact-current-cycle only.
record_production_observation(runtime, normal_receipt, root, environment)
refused = run_json([sys.executable, "-I", "-S", "-B", str(RECOVERY_COMMAND), "complete"], environment)
if refused.get("ok") is not False or refused.get("code") != "RECEIPT_MISMATCH":
raise AssertionError(f"normal-path receipt replay was not refused: {refused!r}")
# Begin a new recovery cycle, then prove shipped evidence -> consume ->
# promote ordering and post-consumption non-repromotion.
recovery = recovery_command([
"begin", "--construction", str(construction_path), "--compaction-epoch", str(index + 20),
"--request-epoch", str(index + 120),
gate_recovery(runtime, "begin", environment)
recovery = run_json([
sys.executable, "-I", "-S", "-B", str(RECOVERY_COMMAND), "begin", "--construction", str(construction_path),
"--compaction-epoch", str(index + 20), "--request-epoch", str(index + 120),
], environment)
receipt = recovery.get("receipt")
if recovery.get("state") != "PENDING_DELIVERY" or not isinstance(receipt, str):
raise AssertionError(f"fresh recovery retry did not pend: {recovery!r}")
observer_path.write_text(json.dumps({
"session_id": session_id, "runtime_generation": 1, "latest_assistant_message": receipt,
}), encoding="utf-8")
os.chmod(observer_path, 0o600)
promoted = recovery_command(["complete"], environment)
record_production_observation(runtime, receipt, root, environment)
gate_recovery(runtime, "complete", environment)
promoted = run_json([sys.executable, "-I", "-S", "-B", str(RECOVERY_COMMAND), "complete"], environment)
if promoted.get("ok") is not True or promoted.get("state") != "VERIFIED":
raise AssertionError(f"recovery consume-before-promote failed: {promoted!r}")
replay = recovery_command(["complete"], environment)
replay = run_json([sys.executable, "-I", "-S", "-B", str(RECOVERY_COMMAND), "complete"], environment)
if replay.get("ok") is not False or replay.get("code") != "INVALID_LEASE_TRANSITION":
raise AssertionError(f"consumed recovery challenge re-promoted: {replay!r}")
finally:
@@ -206,8 +243,8 @@ def main() -> None:
arguments = parser.parse_args()
if arguments.runs != 3:
raise SystemExit("P6 requires exactly three isolated runs")
for index in range(arguments.runs):
run_once(index)
for index, runtime in enumerate(("pi", "claude", "pi")):
run_once(index, runtime)
print("P6 constrained recovery probe PASS: 3 isolated shipped recovery-command runs")

View File

@@ -31,7 +31,9 @@ The same check runs in the Mosaic package test suite and therefore in root CI. A
Clients must complete the request boundary before waiting for a reply. After sending the single JSON object and its terminating newline, the client **MUST half-close the socket's write side** (`shutdown(SHUT_WR)` in POSIX clients; `socket.end()` in Node) and only then await the response. Merely calling `write()` and waiting is invalid: the broker waits for EOF to enforce the exact-one-frame contract and fails closed at its one-second deadline. Do not replace `end()` with `write()` in client helpers. A delayed second frame remains malformed and is rejected.
`mosaic_context_recover` is the only unverified mutator class. Its durable `mosaic-context-refresh` skill is a thin wrapper over `tools/lease-broker/recover-context.py`: `begin` has the broker rebuild the validated `B_payload`/`H_payload`, revoke first, and mint a new `PENDING_DELIVERY` receipt challenge; `complete` accepts neither receipt text nor a challenge argument. It uses only the trusted `ReceiptObserver` seam for the exact latest assistant entry, commits evidence, consumes the broker-held challenge, and promotes VERIFIED last. A normal-path receipt cannot be replayed through recovery because each retry begins a distinct recovery cycle and recovery completion cannot receive caller-presented evidence.
`mosaic_context_recover` is the only unverified mutator class. Its durable `mosaic-context-refresh` skill is a thin wrapper over `tools/lease-broker/recover-context.py`: `begin` has the broker rebuild the validated `B_payload`/`H_payload`, revoke first, and mint a new `PENDING_DELIVERY` receipt challenge; `complete` accepts neither receipt text nor a challenge argument. Claude maps only the exact direct recovery executable/validated arguments to this exempt tool identity; ordinary `Bash` remains gated. Pi exposes only the `mosaic_context_recover` custom tool; ordinary `bash` and all other tools remain gated. A normal-path receipt cannot be replayed through recovery because each retry begins a distinct recovery cycle and recovery completion cannot receive caller-presented evidence.
Production daemon startup creates a separate private observer socket unless a test-only `--test-observer-file` fixture is selected. Claude's Stop hook sends its exact latest assistant entry and Pi's `message_end` handler sends only finalized assistant content to that authenticated transport; the broker public socket never accepts message text. This is byte-build and private out-of-process harness wiring only: do not activate it against a live daemon, live socket, systemd service, tmux session, or model-output stream outside the controlled integration procedure.
Receipt honesty is load-bearing: absent, malformed, prefix-truncated, and observable adapter-mutated terminal receipts do not promote. A tail-only case is non-promoting only where the concrete terminal payload is malformed or observably incomplete. A tail-preserving middle drop is **not receipt-detectable**; it is the disclosed T-C injection-contract residual deferred to WI-7 server-side evidence. The receipt remains a T-A delivery/liveness prerequisite, never a safety, obedience, or residency proof. The framework skill is source-resident and bridge-projected on install/upgrade; do not hand-create a live runtime symlink.

View File

@@ -1,9 +1,9 @@
# #833 constrained recovery command — build scratchpad
- **Objective:** Deliver WI-6 only: the single ungated constrained recovery command, durable `mosaic-context-refresh` wrapper skill, AC-1 observable partial-delivery and C4 replay evidence, and an unfired P6 probe.
- **Objective:** Deliver WI-6 plus Mos-ruled B1/B2 repair only: authenticated narrow recovery invocation in Claude/Pi, production receipt-observer transport, AC-1/C4 preservation, and an unfired repaired P6 probe.
- **Authority:** STEP-0 SHA-256 verified 4/4 against the supplied BUILD-BRIEF, SPEC-v5, ratification, and red-team records.
- **Base:** exact `07553ead337a70a9241f826d27571650262b289c`; new branch `feat/833-constrained-recovery-command`; merge-base assertion passed before any commit.
- **Constraints:** No rebase/pull during build; no live install/symlink or live broker/socket/tmux/systemd mutation; no self-review, PR, merge, push, or P6 fire. `docs/TASKS.md` is orchestrator-owned and will not be modified.
- **Constraints:** No rebase/pull during build; no live install/symlink or live broker/socket/tmux/systemd/model-stream activation; no self-review, PR, merge, push, or P6 fire. `docs/TASKS.md` is orchestrator-owned and will not be modified.
- **Plan:**
1. Add red-first unit tests against the recovery broker entrypoint for fresh recovery challenge, normal-receipt replay refusal, observable partial-delivery refusal, and the explicit middle-drop negative capability; commit the RED test and preserve its command output.
2. Implement the recovery command as a thin driver over shared WI-5 broker transitions and the trusted observer seam; it never accepts caller receipt text.
@@ -11,5 +11,5 @@
4. Build an unfired, default-3-run P6 standalone real-socket driver; it is not added to package scripts and will not be run.
5. Run targeted broker/mutator/receipt suites, lint, and format check; report head to `mosaic-100` and stop.
- **Risks:** The observer can only represent an exact latest message. Tail-preserving middle-drop is intentionally not claimed receipt-detectable (T-C residual deferred to WI-7 server evidence).
- **Evidence:** RED test committed at `3b5513bd6efad0c06b599fc759a66cff4286db04`; expected `UNKNOWN_ACTION` is logged in `/home/hermes/agent-work/reviews/833-wi6-red.log`. GREEN: `pnpm --filter @mosaicstack/mosaic run test:framework-shell` passed (deadline 2, normative 5, receipt 5, recovery 4, runtime tools 25, launch guard 12, inventory 14/14); `git diff --check`; Python `py_compile`; and a tmp-only execution of the real #824 `syncClaudeSkills` bridge projected the source skill (`WI-6 #824 tmp projection PASS`). P6 was built but not fired. The ordinary package Vitest/lint/typecheck/root-format commands cannot resolve their executables in this intentionally dependency-free fresh worktree (`node_modules` absent); no install/symlink workaround was used.
- **Evidence:** Original RED test committed at `3b5513bd6efad0c06b599fc759a66cff4286db04`; expected `UNKNOWN_ACTION` is logged in `/home/hermes/agent-work/reviews/833-wi6-red.log`. Repair RED is committed at `f4beedc3e7ac2e142dcbdeefb0e5ee40c20d9b86` and logged in `/home/hermes/agent-work/reviews/833-wi6-repair-red.log` before B1/B2 source. The repair's private out-of-process harness proves exact Claude recovery mapping while UNVERIFIED, non-recovery Bash denial, Pi's exact custom recovery-tool registration, production Pi `message_end` and Claude latest-entry observer promotion, and S1 triple rejection. P6 was rebuilt but remains unfired. The ordinary package Vitest/lint/typecheck/root-format commands cannot resolve their executables in this intentionally dependency-free fresh worktree (`node_modules` absent); no install/symlink workaround was used.
- **Budget:** No explicit task token cap was supplied; scope is fixed to WI-6 and no unrelated behavior will be added.

View File

@@ -38,7 +38,7 @@
"hooks": [
{
"type": "command",
"command": "python3 ~/.config/mosaic/tools/lease-broker/mutator-gate.py --runtime claude",
"command": "python3 ~/.config/mosaic/tools/lease-broker/mutator-gate.py --runtime claude --recovery-command ~/.config/mosaic/tools/lease-broker/recover-context.py",
"timeout": 3
}
]
@@ -79,6 +79,11 @@
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "python3 ~/.config/mosaic/tools/lease-broker/receipt-observer-client.py --runtime claude --latest-entry",
"timeout": 3
},
{
"type": "command",
"command": "~/.config/mosaic/tools/qa/reflect-stop-hook.sh",

View File

@@ -31,6 +31,14 @@ import { registerLeaseLifecycleHooks, type LeaseLifecyclePiApi } from './lease-l
const MOSAIC_HOME = process.env['MOSAIC_HOME'] ?? join(homedir(), '.config', 'mosaic');
const MUTATOR_GATE = join(MOSAIC_HOME, 'tools', 'lease-broker', 'mutator-gate.py');
const LEASE_REVOKER = join(MOSAIC_HOME, 'tools', 'lease-broker', 'revoke-lease.py');
const RECOVERY_COMMAND = join(MOSAIC_HOME, 'tools', 'lease-broker', 'recover-context.py');
const RECEIPT_OBSERVER_CLIENT = join(
MOSAIC_HOME,
'tools',
'lease-broker',
'receipt-observer-client.py',
);
const RECOVERY_TOOL = 'mosaic_context_recover';
// ---------------------------------------------------------------------------
// Helpers
@@ -135,6 +143,78 @@ function checkPiMutatorGate(toolName: string): { block: true; reason: string } |
};
}
function checkPiRecoveryGate(): { block: true; reason: string } | undefined {
return checkPiMutatorGate(RECOVERY_TOOL);
}
function assistantMessageText(message: unknown): string | undefined {
if (typeof message !== 'object' || message === null) return undefined;
const value = message as { role?: unknown; content?: unknown };
if (value.role !== 'assistant') return undefined;
if (typeof value.content === 'string') return value.content;
if (!Array.isArray(value.content)) return undefined;
const text: string[] = [];
for (const part of value.content) {
if (typeof part !== 'object' || part === null) return undefined;
const typed = part as { type?: unknown; text?: unknown };
if (typed.type !== 'text' || typeof typed.text !== 'string') return undefined;
text.push(typed.text);
}
return text.join('');
}
function recordPiMessageEnd(message: unknown): void {
const latestAssistantMessage = assistantMessageText(message);
if (latestAssistantMessage === undefined) return;
// This sends finalized Pi message_end content only to the daemon-owned
// authenticated observer transport, never to the public broker request API.
spawnSync('python3', [RECEIPT_OBSERVER_CLIENT, '--runtime', 'pi'], {
input: `${JSON.stringify({ latest_assistant_message: latestAssistantMessage })}\n`,
encoding: 'utf8',
timeout: 2_000,
env: process.env,
});
}
function runPiRecoveryCommand(params: {
phase: 'begin' | 'complete';
construction?: string;
compactionEpoch?: number;
requestEpoch?: number;
}): { content: Array<{ type: 'text'; text: string }> } {
const args = [RECOVERY_COMMAND, params.phase];
if (params.phase === 'begin') {
if (
typeof params.construction !== 'string' ||
!Number.isInteger(params.compactionEpoch) ||
!Number.isInteger(params.requestEpoch) ||
params.compactionEpoch < 0 ||
params.requestEpoch < 0
) {
return {
content: [
{ type: 'text', text: 'Recovery begin requires construction and non-negative epochs.' },
],
};
}
args.push(
'--construction',
params.construction,
'--compaction-epoch',
String(params.compactionEpoch),
'--request-epoch',
String(params.requestEpoch),
);
}
const result = spawnSync('python3', args, {
encoding: 'utf8',
timeout: 3_000,
env: process.env,
});
const output = result.status === 0 ? String(result.stdout ?? '') : String(result.stderr ?? '');
return { content: [{ type: 'text', text: output || 'Constrained recovery refused.' }] };
}
// ---------------------------------------------------------------------------
// Mission detection
// ---------------------------------------------------------------------------
@@ -287,6 +367,32 @@ export default function register(pi: ExtensionAPI) {
// class gate before execution. Broker/script failure blocks fail-closed.
pi.on('tool_call', async (event) => checkPiMutatorGate(event.toolName));
// Pi records only a finalized assistant entry at message_end. It never uses
// after_provider_response, which occurs before stream consumption.
pi.on('message_end', async (event) => {
recordPiMessageEnd((event as unknown as { message?: unknown }).message);
});
// The recovery custom tool is the only Pi invocation that maps to the
// broker's exempt RECOVERY_TOOL identity. It is not a Bash exception.
pi.registerTool({
name: RECOVERY_TOOL,
label: 'Mosaic Context Recovery',
description:
'Run the constrained broker-backed context recovery flow. This is the sole ungated mutator.',
parameters: Type.Object({
phase: Type.Union([Type.Literal('begin'), Type.Literal('complete')]),
construction: Type.Optional(Type.String()),
compactionEpoch: Type.Optional(Type.Integer({ minimum: 0 })),
requestEpoch: Type.Optional(Type.Integer({ minimum: 0 })),
}),
async execute(_toolCallId, params) {
const blocked = checkPiRecoveryGate();
if (blocked !== undefined) return { content: [{ type: 'text', text: blocked.reason }] };
return runPiRecoveryCommand(params);
},
});
// ── Session Start ─────────────────────────────────────────────────────
pi.on('session_start', async (_event, ctx) => {
sessionCwd = process.cwd();

View File

@@ -12,31 +12,40 @@ mutator remains behind the verified lease gate.
## Wrapper procedure
1. The runtime supplies the exact validated normative-fragment construction and the current
compaction/request epochs, then invokes:
compaction/request epochs.
- **Claude:** invoke only this direct command shape (no shell composition):
```bash
python3 "$MOSAIC_HOME/tools/lease-broker/recover-context.py" begin \
--construction "$MOSAIC_CONTEXT_REFRESH_CONSTRUCTION" \
--compaction-epoch "$MOSAIC_COMPACTION_EPOCH" \
--request-epoch "$MOSAIC_REQUEST_EPOCH"
```
```bash
python3 "$MOSAIC_HOME/tools/lease-broker/recover-context.py" begin \
--construction "$MOSAIC_CONTEXT_REFRESH_CONSTRUCTION" \
--compaction-epoch "$MOSAIC_COMPACTION_EPOCH" \
--request-epoch "$MOSAIC_REQUEST_EPOCH"
```
The command delegates to the shipped WI-5 broker transition: revoke first, build the canonical
`B_payload`/`H_payload`, enter `PENDING_DELIVERY`, and mint a fresh one-time challenge. It prints
Claude's all-tools gate maps this exact recovery executable and validated argument shape to
`mosaic_context_recover`; ordinary `Bash` remains gated.
- **Pi:** call the registered `mosaic_context_recover` tool with `phase: "begin"`,
`construction`, `compactionEpoch`, and `requestEpoch`. It is the exact broker-exempt tool name;
Pi `bash` and every other tool remain gated.
Both forms delegate to the shipped WI-5 broker transition: revoke first, build the canonical
`B_payload`/`H_payload`, enter `PENDING_DELIVERY`, and mint a fresh one-time challenge. They print
the terminal receipt envelope to deliver exactly as returned.
2. The current assistant message copies that one terminal receipt verbatim. It does not compute a
hash, add prose, quote a prior receipt, or present a caller-supplied receipt/challenge.
3. At `message_end`, the trusted runtime `ReceiptObserver` seam invokes:
3. The production trusted-observer transport records that finalized assistant entry before completion:
- **Claude** selects the latest assistant entry at its `Stop` hook.
- **Pi** records only finalized assistant content at `message_end` (never
`after_provider_response`).
```bash
python3 "$MOSAIC_HOME/tools/lease-broker/recover-context.py" complete
```
Completion supplies no receipt or challenge argument. The broker observes the exact latest
assistant entry, commits evidence, consumes its own fresh challenge, and promotes VERIFIED last.
If observation is absent, malformed, stale, or duplicated, recovery remains UNVERIFIED and a
retry begins a new cycle.
Then invoke completion with the same adapter form: Claude runs
`python3 "$MOSAIC_HOME/tools/lease-broker/recover-context.py" complete`; Pi calls
`mosaic_context_recover` with `phase: "complete"`. Completion supplies no receipt or challenge
argument. The broker observes the exact latest assistant entry, commits evidence, consumes its own
fresh challenge, and promotes VERIFIED last. If observation is absent, malformed, stale, or
duplicated, recovery remains UNVERIFIED and a retry begins a new cycle.
## Scope and honesty

View File

@@ -11,6 +11,7 @@ from concurrent.futures import ThreadPoolExecutor
import json
import os
import secrets
import select
import signal
import socket
import stat
@@ -28,7 +29,11 @@ if _MODULE_DIRECTORY not in sys.path:
sys.path.insert(0, _MODULE_DIRECTORY)
from normative_fragments import build_payload_from_wire
from receipt_challenge import is_verbatim_receipt, latest_assistant_digest, receipt_for
from receipt_observer import FileTestReceiptObserver, ReceiptObserver, UnavailableReceiptObserver
from receipt_observer import (
FileTestReceiptObserver,
ReceiptObserver,
RuntimeReceiptObserver,
)
MAX_FRAME: Final = 64 * 1024
MAX_STATE: Final = 4 * 1024 * 1024
@@ -302,7 +307,9 @@ class StateStore:
class Broker:
def __init__(self, store: StateStore, observer: ReceiptObserver | None = None) -> None:
self.store = store
self.observer: ReceiptObserver = observer if observer is not None else UnavailableReceiptObserver()
# Production construction always has a transport-capable observer. Test
# fixtures may inject their controlled observer explicitly.
self.observer: ReceiptObserver = observer if observer is not None else RuntimeReceiptObserver()
# Set only by begin_verification after its mandatory revoke-first fence.
# It is preserved if later cycle admission is refused; all other broker
# actions retain the normal snapshot rollback behavior.
@@ -390,6 +397,40 @@ class Broker:
raise BrokerFailure("STATE_INTEGRITY")
lease["state"] = LEASE_VERIFIED
def record_runtime_observation(
self, peer_pid: int, request: dict[str, object]
) -> dict[str, object]:
"""Record only an authenticated adapter's finalized assistant message.
This method is deliberately unreachable through ``Broker.handle`` and
its public broker socket. The production observer socket calls it after
SO_PEERCRED/ancestry authentication, preserving the S1 rule that a
broker request can never carry ``latest_assistant_message``.
"""
required = {
"action", "session_id", "runtime_generation", "runtime", "latest_assistant_message"
}
if set(request) != required or request.get("action") != "record_runtime_observation":
raise BrokerFailure("INVALID_OBSERVATION")
runtime = request.get("runtime")
message = request.get("latest_assistant_message")
if runtime not in READ_ONLY_TOOLS or not isinstance(message, str) or len(message.encode("utf-8")) > MAX_FRAME:
raise BrokerFailure("INVALID_OBSERVATION")
session_id, _ = self.authenticate(peer_pid, request)
generation = request["runtime_generation"]
lease = self.leases.get(session_id)
if (
not isinstance(lease, dict)
or lease.get("state") != LEASE_PENDING
or lease.get("runtime") != runtime
or lease.get("runtime_generation") != generation
or not isinstance(self.observer, RuntimeReceiptObserver)
):
raise BrokerFailure("OBSERVATION_UNAVAILABLE")
self.observer.record_latest_assistant_message(session_id, runtime, generation, message)
return {"ok": True}
def handle(self, peer: tuple[int, int, int], request: dict[str, object]) -> dict[str, object]:
if self.store.poisoned:
raise StateCommitUncertain()
@@ -783,14 +824,63 @@ def handle_connection(
return
def handle_runtime_observation_connection(
connection: socket.socket,
broker: Broker,
broker_lock: threading.Lock,
) -> None:
"""Serve the authenticated production observer transport, never the broker API."""
with connection:
read_deadline = time.monotonic() + READ_DEADLINE_SECONDS
try:
raw = connection.getsockopt(socket.SOL_SOCKET, socket.SO_PEERCRED, 12)
peer = struct.unpack("3i", raw)
request = read_frame(connection, read_deadline)
except BrokerFailure as exc:
reply = {"ok": False, "code": exc.code}
except OSError:
return
else:
acquired = broker_lock.acquire(timeout=HANDLE_QUEUE_TIMEOUT_SECONDS)
if not acquired:
reply = {"ok": False, "code": "BROKER_BUSY"}
else:
try:
try:
reply = broker.record_runtime_observation(peer[0], request)
except BrokerFailure as exc:
reply = {"ok": False, "code": exc.code}
finally:
broker_lock.release()
try:
connection.settimeout(SEND_TIMEOUT_SECONDS)
connection.sendall((json.dumps(reply, separators=(",", ":")) + "\n").encode())
except OSError:
return
def serve(
socket_path: Path,
state_path: Path,
observer: ReceiptObserver | None = None,
observer_socket_path: Path | None = None,
) -> None:
secure_parent(socket_path)
if socket_path.exists() or socket_path.is_symlink():
raise BrokerFailure("SOCKET_ALREADY_EXISTS")
runtime_observer = observer is None
if runtime_observer:
observer = RuntimeReceiptObserver()
observer_socket_path = observer_socket_path or socket_path.with_name("receipt-observer.sock")
if observer_socket_path == socket_path:
raise BrokerFailure("OBSERVER_SOCKET_CONFLICT")
secure_parent(observer_socket_path)
if observer_socket_path.exists() or observer_socket_path.is_symlink():
raise BrokerFailure("OBSERVER_SOCKET_ALREADY_EXISTS")
elif observer_socket_path is not None:
raise BrokerFailure("TEST_OBSERVER_SOCKET_CONFLICT")
store = StateStore(state_path)
broker = Broker(store, observer)
broker_lock = threading.Lock()
@@ -805,16 +895,28 @@ def serve(
server.bind(str(socket_path))
os.chmod(socket_path, 0o600)
owned = (socket_path.stat().st_dev, socket_path.stat().st_ino)
observer_server: socket.socket | None = None
observer_owned: tuple[int, int] | None = None
if runtime_observer and observer_socket_path is not None:
observer_server = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
observer_server.bind(str(observer_socket_path))
os.chmod(observer_socket_path, 0o600)
observer_owned = (observer_socket_path.stat().st_dev, observer_socket_path.stat().st_ino)
stopping = False
def stop(_signum: int, _frame: object) -> None:
nonlocal stopping
stopping = True
server.close()
if observer_server is not None:
observer_server.close()
def process_connection(connection: socket.socket) -> None:
def process_connection(connection: socket.socket, is_observer: bool) -> None:
try:
handle_connection(connection, broker, broker_lock)
if is_observer:
handle_runtime_observation_connection(connection, broker, broker_lock)
else:
handle_connection(connection, broker, broker_lock)
except Exception as exc:
with fatal_lock:
if not fatal_errors:
@@ -829,57 +931,73 @@ def serve(
signal.signal(signal.SIGTERM, stop)
signal.signal(signal.SIGINT, stop)
server.listen(MAX_IN_FLIGHT_CONNECTIONS)
server.settimeout(0.1)
server.setblocking(False)
if observer_server is not None:
observer_server.listen(MAX_IN_FLIGHT_CONNECTIONS)
observer_server.setblocking(False)
print("READY", flush=True)
try:
while not stopping:
failure = fatal_error()
if failure is not None:
raise failure
if not slots.acquire(timeout=0.1):
continue
listeners = [server, *([observer_server] if observer_server is not None else [])]
try:
connection, _ = server.accept()
except socket.timeout:
slots.release()
continue
except OSError:
slots.release()
failure = fatal_error()
if failure is not None:
raise failure
ready, _, _ = select.select(listeners, [], [], 0.1)
except (OSError, ValueError):
if stopping:
break
raise
try:
executor.submit(process_connection, connection)
except Exception:
slots.release()
connection.close()
raise
for listener in ready:
if not slots.acquire(blocking=False):
continue
try:
connection, _ = listener.accept()
except BlockingIOError:
slots.release()
continue
except OSError:
slots.release()
if stopping:
break
raise
try:
executor.submit(process_connection, connection, listener is observer_server)
except Exception:
slots.release()
connection.close()
raise
finally:
server.close()
if observer_server is not None:
observer_server.close()
executor.shutdown(wait=True)
try:
current = socket_path.stat()
if (current.st_dev, current.st_ino) == owned:
socket_path.unlink()
except FileNotFoundError:
pass
for path, inode in ((socket_path, owned), (observer_socket_path, observer_owned)):
if path is None or inode is None:
continue
try:
current = path.stat()
if (current.st_dev, current.st_ino) == inode:
path.unlink()
except FileNotFoundError:
pass
def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument("--socket", required=True, type=Path)
parser.add_argument("--state", required=True, type=Path)
parser.add_argument("--observer-socket", type=Path)
parser.add_argument("--test-observer-file", type=Path)
arguments = parser.parse_args()
if arguments.observer_socket is not None and arguments.test_observer_file is not None:
raise BrokerFailure("TEST_OBSERVER_SOCKET_CONFLICT")
observer = (
FileTestReceiptObserver(arguments.test_observer_file)
if arguments.test_observer_file is not None
else None
)
serve(arguments.socket, arguments.state, observer)
serve(arguments.socket, arguments.state, observer, arguments.observer_socket)
if __name__ == "__main__":

View File

@@ -97,6 +97,12 @@ def main(
environment["MOSAIC_RUNTIME_GENERATION"] = str(generation)
environment["MOSAIC_LEASE_GENERATION_FILE"] = str(generation_file)
environment["MOSAIC_LEASE_RUNTIME"] = arguments.runtime
# Matches daemon.py's production default; deployments using a distinct
# observer socket may set this authenticated transport path explicitly.
environment.setdefault(
"MOSAIC_RECEIPT_OBSERVER_SOCKET",
str(socket_path.with_name("receipt-observer.sock")),
)
try:
execute(command[0], command, environment)
except OSError:

View File

@@ -8,14 +8,21 @@ import json
import os
import socket
import sys
import shlex
from collections.abc import Callable, Mapping, Sequence
from pathlib import Path
from typing import BinaryIO, Final
# Isolated (`python -I`) adapter invocations must still import co-located
# framework modules; never depend on the caller's PYTHONPATH.
_MODULE_DIRECTORY = str(Path(__file__).resolve().parent)
if _MODULE_DIRECTORY not in sys.path:
sys.path.insert(0, _MODULE_DIRECTORY)
from lease_generation import read_runtime_generation
MAX_FRAME: Final = 64 * 1024
BROKER_TIMEOUT_SECONDS: Final = 1.5
RECOVERY_TOOL: Final = "mosaic_context_recover"
def deny(code: str) -> int:
@@ -23,7 +30,7 @@ def deny(code: str) -> int:
return 2
def read_tool_name(stream: BinaryIO | None = None) -> str:
def read_tool_request(stream: BinaryIO | None = None) -> dict[str, object]:
source = sys.stdin.buffer if stream is None else stream
raw = source.read(MAX_FRAME + 1)
if len(raw) > MAX_FRAME:
@@ -34,7 +41,53 @@ def read_tool_name(stream: BinaryIO | None = None) -> str:
tool_name = value.get("tool_name")
if not isinstance(tool_name, str) or not tool_name or len(tool_name) > 256:
raise ValueError("INVALID_GATE_INPUT")
return tool_name
return value
def read_tool_name(stream: BinaryIO | None = None) -> str:
"""Backward-compatible strict extraction for callers that need only the name."""
return str(read_tool_request(stream)["tool_name"])
def recovery_invocation_name(request: dict[str, object], recovery_command: Path | None) -> str:
"""Map only a direct Claude Bash invocation of the recovery executable.
The mapping is adapter-configured and remains broker-authenticated through
the ordinary authorization request. It never blesses Bash generally, shell
composition, a different executable, or unsupported recovery arguments.
"""
tool_name = request["tool_name"]
if tool_name != "Bash" or recovery_command is None:
return str(tool_name)
tool_input = request.get("tool_input")
if not isinstance(tool_input, dict) or set(tool_input) != {"command"}:
return str(tool_name)
command = tool_input.get("command")
if not isinstance(command, str) or not command:
return str(tool_name)
try:
argv = shlex.split(command, posix=True)
except ValueError:
return str(tool_name)
if len(argv) < 3 or argv[0] != "python3":
return str(tool_name)
try:
if Path(argv[1]).resolve(strict=False) != recovery_command.resolve(strict=False):
return str(tool_name)
except OSError:
return str(tool_name)
phase = argv[2]
if phase == "complete" and len(argv) == 3:
return RECOVERY_TOOL
if phase != "begin" or len(argv) != 9:
return str(tool_name)
expected_flags = {"--construction", "--compaction-epoch", "--request-epoch"}
supplied_flags = set(argv[3::2])
if supplied_flags != expected_flags or any(not value for value in argv[4::2]):
return str(tool_name)
return RECOVERY_TOOL
def broker_request(socket_path: Path, request: dict[str, object]) -> dict[str, object]:
@@ -70,11 +123,13 @@ def main(
) -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--runtime", required=True, choices=("claude", "pi"))
parser.add_argument("--recovery-command", type=Path)
arguments = parser.parse_args(argv)
source_environment = os.environ if environ is None else environ
try:
tool_name = read_tool_name(stream)
request_input = read_tool_request(stream)
tool_name = recovery_invocation_name(request_input, arguments.recovery_command)
socket_value = source_environment["MOSAIC_LEASE_BROKER_SOCKET"]
session_id = source_environment["MOSAIC_LEASE_SESSION_ID"]
generation = resolve_generation(source_environment)

View File

@@ -0,0 +1,143 @@
#!/usr/bin/env python3
"""Authenticated adapter-to-daemon transport for finalized assistant receipts.
This is not a broker request client. It writes only to the daemon-owned observer
socket, which authenticates SO_PEERCRED/ancestry before retaining a message for
the broker's ReceiptObserver seam.
"""
from __future__ import annotations
import argparse
import json
import os
import socket
import stat
import sys
from collections.abc import Mapping, Sequence
from pathlib import Path
from typing import Final
MAX_FRAME: Final = 64 * 1024
BROKER_TIMEOUT_SECONDS: Final = 1.5
MAX_TRANSCRIPT_BYTES: Final = 4 * 1024 * 1024
def read_json(stream: object) -> dict[str, object]:
raw = getattr(stream, "buffer", stream).read(MAX_FRAME + 1)
if not isinstance(raw, bytes) or len(raw) > MAX_FRAME:
raise ValueError("invalid observer input")
value = json.loads(raw)
if not isinstance(value, dict):
raise ValueError("invalid observer input")
return value
def assistant_text(entry: object) -> str | None:
if not isinstance(entry, dict):
return None
message = entry.get("message", entry)
if not isinstance(message, dict) or message.get("role") != "assistant":
return None
content = message.get("content")
if isinstance(content, str):
return content
if not isinstance(content, list):
return None
parts: list[str] = []
for item in content:
if not isinstance(item, dict) or item.get("type") != "text" or not isinstance(item.get("text"), str):
return None
parts.append(item["text"])
return "".join(parts)
def claude_latest_entry(value: dict[str, object]) -> str:
transcript_path = value.get("transcript_path")
if not isinstance(transcript_path, str) or not transcript_path:
raise ValueError("invalid Claude observer input")
path = Path(transcript_path)
flags = os.O_RDONLY | getattr(os, "O_CLOEXEC", 0) | getattr(os, "O_NOFOLLOW", 0)
descriptor = os.open(path, flags)
try:
metadata = os.fstat(descriptor)
if not stat.S_ISREG(metadata.st_mode) or metadata.st_size > MAX_TRANSCRIPT_BYTES:
raise ValueError("unsafe Claude transcript")
raw = os.read(descriptor, MAX_TRANSCRIPT_BYTES + 1)
finally:
os.close(descriptor)
if len(raw) > MAX_TRANSCRIPT_BYTES:
raise ValueError("oversized Claude transcript")
for line in reversed(raw.decode("utf-8").splitlines()):
try:
text = assistant_text(json.loads(line))
except json.JSONDecodeError as exc:
raise ValueError("invalid Claude transcript") from exc
if text is not None:
return text
raise ValueError("Claude transcript has no assistant entry")
def pi_message_end(value: dict[str, object]) -> str:
if set(value) != {"latest_assistant_message"} or not isinstance(value["latest_assistant_message"], str):
raise ValueError("invalid Pi observer input")
return value["latest_assistant_message"]
def observer_request(socket_path: Path, request: dict[str, object]) -> dict[str, object]:
payload = (json.dumps(request, separators=(",", ":")) + "\n").encode()
if len(payload) > MAX_FRAME:
raise ValueError("observer request too large")
response = bytearray()
with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as connection:
connection.settimeout(BROKER_TIMEOUT_SECONDS)
connection.connect(str(socket_path))
connection.sendall(payload)
connection.shutdown(socket.SHUT_WR)
while len(response) <= MAX_FRAME:
chunk = connection.recv(min(4096, MAX_FRAME + 1 - len(response)))
if not chunk:
break
response.extend(chunk)
if len(response) > MAX_FRAME or not response.endswith(b"\n"):
raise ValueError("invalid observer reply")
value = json.loads(response)
if not isinstance(value, dict):
raise ValueError("invalid observer reply")
return value
def main(argv: Sequence[str] | None = None, *, environ: Mapping[str, str] | None = None) -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--runtime", required=True, choices=("claude", "pi"))
parser.add_argument("--latest-entry", action="store_true")
arguments = parser.parse_args(argv)
source_environment = os.environ if environ is None else environ
try:
source = read_json(sys.stdin)
if arguments.runtime == "claude":
if not arguments.latest_entry:
raise ValueError("Claude observer requires --latest-entry")
message = claude_latest_entry(source)
else:
if arguments.latest_entry:
raise ValueError("Pi observer is message_end only")
message = pi_message_end(source)
if len(message.encode("utf-8")) > MAX_FRAME:
raise ValueError("assistant message too large")
reply = observer_request(Path(source_environment["MOSAIC_RECEIPT_OBSERVER_SOCKET"]), {
"action": "record_runtime_observation",
"session_id": source_environment["MOSAIC_LEASE_SESSION_ID"],
"runtime_generation": int(source_environment["MOSAIC_RUNTIME_GENERATION"]),
"runtime": arguments.runtime,
"latest_assistant_message": message,
})
except (KeyError, OSError, ValueError, json.JSONDecodeError) as error:
print(f"Mosaic receipt observer refused: {error}", file=sys.stderr)
return 2
return 0 if reply == {"ok": True} else 2
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -1,9 +1,11 @@
#!/usr/bin/env python3
"""Trusted latest-assistant-message observer boundary for receipt promotion.
Runtime adapters must implement ``observe_latest_assistant_message`` directly:
Claude selects the exact latest assistant entry and Pi selects ``message_end``.
The broker accepts no observed message through its request protocol.
Production adapters deliver finalized assistant content over the daemon-owned
observer socket after the daemon authenticates their peer against the broker's
kernel-anchored session identity. The broker request protocol never accepts
assistant-message content. Claude supplies its latest assistant entry; Pi
supplies finalized assistant content at ``message_end``.
"""
from __future__ import annotations
@@ -26,7 +28,7 @@ class ReceiptObserver(Protocol):
class UnavailableReceiptObserver:
"""Production-safe default until a runtime adapter injects an observer."""
"""Fail-closed only for direct unit construction without daemon transport."""
def observe_latest_assistant_message(
self,
@@ -38,6 +40,31 @@ class UnavailableReceiptObserver:
return None
class RuntimeReceiptObserver:
"""Daemon-owned production observer populated only by authenticated adapters."""
def __init__(self) -> None:
self._messages: dict[tuple[str, str, int], str] = {}
def record_latest_assistant_message(
self,
session_id: str,
runtime: str,
runtime_generation: int,
message: str,
) -> None:
self._messages[(session_id, runtime, runtime_generation)] = message
def observe_latest_assistant_message(
self,
session_id: str,
runtime: str,
runtime_generation: int,
_binding: dict[str, object],
) -> str | None:
return self._messages.get((session_id, runtime, runtime_generation))
class TestReceiptObserver:
"""Deterministic controlled observer used only by byte-build tests."""
@@ -60,7 +87,7 @@ class TestReceiptObserver:
class FileTestReceiptObserver:
"""Private fixture-file observer for isolated out-of-process test drivers."""
"""Private fixture-file observer for isolated out-of-process test drivers only."""
def __init__(self, path: Path) -> None:
self.path = path

View File

@@ -18,6 +18,11 @@ from collections.abc import Mapping, Sequence
from pathlib import Path
from typing import Final
# The out-of-process recovery command is intentionally runnable with `python
# -I`; locate its shipped construction module without caller-controlled paths.
_MODULE_DIRECTORY = str(Path(__file__).resolve().parent)
if _MODULE_DIRECTORY not in sys.path:
sys.path.insert(0, _MODULE_DIRECTORY)
from normative_fragments import build_payload_from_wire
MAX_FRAME: Final = 64 * 1024

View File

@@ -81,6 +81,8 @@ class RuntimeBoundaryFixture(unittest.TestCase):
except subprocess.TimeoutExpired:
child.kill()
child.wait()
if child.stdout is not None:
child.stdout.close()
self.temporary.cleanup()
def start_daemon(self, *, production_observer: bool) -> None:
@@ -145,6 +147,23 @@ class RecoveryRuntimeBoundaryTest(RuntimeBoundaryFixture):
check=False,
)
self.assertEqual(mapped.returncode, 0, mapped.stderr)
mapped_begin = subprocess.run(
[sys.executable, "-I", "-S", "-B", str(GATE), "--runtime", "claude", "--recovery-command", str(RECOVERY)],
input=json.dumps({
"tool_name": "Bash",
"tool_input": {
"command": (
f"python3 {RECOVERY} begin --construction /tmp/construction.json "
"--compaction-epoch 1 --request-epoch 1"
)
},
}),
text=True,
capture_output=True,
env={**environment, "MOSAIC_LEASE_RUNTIME": "claude"},
check=False,
)
self.assertEqual(mapped_begin.returncode, 0, mapped_begin.stderr)
ordinary_bash = subprocess.run(
[sys.executable, "-I", "-S", "-B", str(GATE), "--runtime", "claude", "--recovery-command", str(RECOVERY)],
input=json.dumps({"tool_name": "Bash", "tool_input": {"command": "echo not-recovery"}}),
@@ -180,14 +199,28 @@ class RecoveryRuntimeBoundaryTest(RuntimeBoundaryFixture):
# S1: production transport is not a broker request field. The public
# broker endpoint keeps rejecting caller-supplied assistant evidence.
rejected = request(self.socket, {
rejected_begin = request(self.socket, {
"action": "begin_recovery",
"session_id": session_id,
"runtime_generation": 1,
"receipt": receipt,
})
self.assertEqual(rejected_begin, {"ok": False, "code": "INVALID_RECOVERY_REQUEST"})
rejected_observe = request(self.socket, {
"action": "observe_receipt",
"session_id": session_id,
"runtime_generation": 1,
"receipt_challenge": cycle["receipt_challenge"],
"latest_assistant_message": receipt,
})
self.assertEqual(rejected, {"ok": False, "code": "INVALID_RECEIPT"})
self.assertEqual(rejected_observe, {"ok": False, "code": "INVALID_RECEIPT"})
rejected_complete = request(self.socket, {
"action": "complete_recovery",
"session_id": session_id,
"runtime_generation": 1,
"latest_assistant_message": receipt,
})
self.assertEqual(rejected_complete, {"ok": False, "code": "INVALID_RECOVERY_REQUEST"})
recorded = subprocess.run(
[sys.executable, "-I", "-S", "-B", str(OBSERVER_CLIENT), "--runtime", "pi"],
@@ -208,6 +241,42 @@ class RecoveryRuntimeBoundaryTest(RuntimeBoundaryFixture):
self.assertEqual(complete.returncode, 0, complete.stderr)
self.assertEqual(json.loads(complete.stdout).get("state"), "VERIFIED")
# The independent Claude transport selects one latest assistant entry
# from its hook transcript; it is not a Pi/message_end fallback.
claude_environment = {**environment, "MOSAIC_LEASE_RUNTIME": "claude"}
claude_begin = subprocess.run(
[sys.executable, "-I", "-S", "-B", str(RECOVERY), "begin", "--construction", str(self.construction()), "--compaction-epoch", "2", "--request-epoch", "2"],
text=True,
capture_output=True,
env=claude_environment,
check=False,
)
self.assertEqual(claude_begin.returncode, 0, claude_begin.stderr)
claude_receipt = json.loads(claude_begin.stdout)["receipt"]
transcript = self.root / "claude-transcript.jsonl"
transcript.write_text(
json.dumps({"message": {"role": "assistant", "content": claude_receipt}}) + "\n",
encoding="utf-8",
)
claude_recorded = subprocess.run(
[sys.executable, "-I", "-S", "-B", str(OBSERVER_CLIENT), "--runtime", "claude", "--latest-entry"],
input=json.dumps({"transcript_path": str(transcript)}),
text=True,
capture_output=True,
env=claude_environment,
check=False,
)
self.assertEqual(claude_recorded.returncode, 0, claude_recorded.stderr)
claude_complete = subprocess.run(
[sys.executable, "-I", "-S", "-B", str(RECOVERY), "complete"],
text=True,
capture_output=True,
env=claude_environment,
check=False,
)
self.assertEqual(claude_complete.returncode, 0, claude_complete.stderr)
self.assertEqual(json.loads(claude_complete.stdout).get("state"), "VERIFIED")
if __name__ == "__main__":
unittest.main()