WI-6 (#833): constrained recovery command + mosaic-context-refresh skill wrapper (#846)
All checks were successful
ci/woodpecker/push/publish Pipeline was successful
ci/woodpecker/push/ci Pipeline was successful

This commit was merged in pull request #846.
This commit is contained in:
2026-07-20 03:33:00 +00:00
parent 07553ead33
commit 2509eb7646
23 changed files with 1847 additions and 43 deletions

View File

@@ -35,6 +35,8 @@ CONTRIBUTING.md
defaults/**
examples/**
guides/**
# Shipped framework subtree — canonical skills are upgrade-reconciled.
skills/**
install.sh
install.ps1
LICENSE
@@ -67,6 +69,9 @@ policy/**
memory/**
sources/**
credentials/**
# Operator-authored/customized skills live separately from canonical skills/ and
# must remain structurally unprunable even as skills/** is framework-owned.
skills-local/**
# Secret-bearing operator file INSIDE the framework-owned tools/ subtree.
# Listed explicitly so the deny-wins rule carves it out of tools/**.
tools/_lib/credentials.json

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

@@ -0,0 +1,65 @@
---
name: mosaic-context-refresh
description: Run the constrained Mosaic context-recovery flow after compaction or directive-loss. This is a thin wrapper over the broker-backed recovery command; it never treats a receipt as a safety or residency proof.
---
# mosaic-context-refresh
Use this only after compaction, session resume, or confirmed directive drift. It invokes the
**single ungated mutator**, `tools/lease-broker/recover-context.py`; every other consequential
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.
- **Claude:** invoke only this direct command shape (no shell composition):
```bash
python3 /absolute/path/to/mosaic/tools/lease-broker/recover-context.py begin --construction /absolute/path/to/mosaic-context-refresh-construction.json --compaction-epoch 0 --request-epoch 0
```
This is a literal argv template: replace the recover-context.py path and construction JSON path
with the literal absolute paths for your install, then replace each epoch with literal decimal
digits. Do not use variables, quoting, globs, redirects,
shell operators, substitutions, or line continuations. Claude's all-tools gate maps only this
fully literal recovery 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. 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`).
Then invoke completion with the same adapter form: Claude runs
`python3 /absolute/path/to/mosaic/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
- A receipt from the normal verification path cannot be replayed through recovery: recovery mints a
distinct current challenge and does not accept caller-provided receipt text as evidence.
- Observable absent, malformed, prefix-truncated, and adapter-mutated terminal receipts do not
promote. “Tail-only” is non-promoting only when the delivered terminal bytes are concretely
malformed or incomplete.
- **Negative capability:** a tail-preserving middle drop is not represented as receipt-detectable.
It is a T-C injection-contract residual deferred to WI-7 server-side evidence; do not claim this
skill or receipt catches it.
- The receipt is a T-A delivery/liveness prerequisite only. It never proves obedience, comprehension,
durable residency, or safety; the whole mutator-class gate and server-side branch protection retain
those roles.
This source-resident skill is projected by the Mosaic skill bridge after framework install/upgrade.
Do not create a live symlink manually.

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()
@@ -400,7 +441,7 @@ class Broker:
response = self._handle(peer, request)
if self.store.value != previous:
self.store.commit()
if request.get("action") == "promote_lease":
if request.get("action") in {"promote_lease", "complete_recovery"}:
session_id = request.get("session_id")
if not isinstance(session_id, str):
raise BrokerFailure("INVALID_IDENTITY")
@@ -469,6 +510,71 @@ class Broker:
raise BrokerFailure("TOKEN_REPLAY")
del self.store.tokens()[token_value]
return {"ok": True}
if action == "begin_recovery":
# Recovery is the one ungated mutator, but it is not a second
# receipt protocol. It delegates to this exact normal-path
# transition, then marks its volatile pending cycle so completion
# can obtain the broker-minted challenge internally. Caller-supplied
# receipt text is never an input to recovery.
if any(field in request for field in ("receipt", "latest_assistant_message", "receipt_challenge")):
raise BrokerFailure("INVALID_RECOVERY_REQUEST")
normal_request = dict(request)
normal_request["action"] = "begin_verification"
response = self._handle(peer, normal_request)
session_id = response.get("session_id")
if not isinstance(session_id, str):
# begin_verification deliberately does not return identity;
# recover it only after its authenticated shared transition.
candidate = request.get("session_id")
if not isinstance(candidate, str):
raise BrokerFailure("INVALID_IDENTITY")
session_id = candidate
lease = self.leases.get(session_id)
if not isinstance(lease, dict) or lease.get("state") != LEASE_PENDING:
raise BrokerFailure("STATE_INTEGRITY")
lease["cycle_kind"] = "recovery"
response["state"] = "PENDING_DELIVERY"
return response
if action == "complete_recovery":
if any(field in request for field in ("receipt", "latest_assistant_message", "receipt_challenge")):
raise BrokerFailure("INVALID_RECOVERY_REQUEST")
session_id, _ = self.authenticate(peer_pid, request)
lease = self.leases.get(session_id)
challenge = lease.get("receipt_challenge") if isinstance(lease, dict) else None
if (
not isinstance(lease, dict)
or lease.get("cycle_kind") != "recovery"
or lease.get("state") != LEASE_PENDING
or not isinstance(challenge, str)
):
raise BrokerFailure("INVALID_LEASE_TRANSITION")
try:
# Reuse the shipped observe -> evidence commit -> consume ->
# promote transition. The recovery caller supplies neither a
# normal-path receipt nor a challenge; the trusted observer
# and current broker cycle remain the sole evidence authority.
observed_request = {
"action": "observe_receipt",
"session_id": session_id,
"runtime_generation": request["runtime_generation"],
"receipt_challenge": challenge,
}
self._handle(peer, observed_request)
return self._handle(peer, {
"action": "promote_lease",
"session_id": session_id,
"runtime_generation": request["runtime_generation"],
"receipt_challenge": challenge,
})
except Exception:
# A malformed, absent, or stale observed receipt must leave
# no pending recovery capability or live lease. A retry mints
# a new challenge through the shared begin transition.
self.revoke_session_authority(session_id)
self._rejected_cycle_fence = (
copy.deepcopy(self.store.value), copy.deepcopy(self.leases)
)
raise
if action == "begin_verification":
session_id, _ = self.authenticate(peer_pid, request)
runtime = request.get("runtime")
@@ -718,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()
@@ -740,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:
@@ -764,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,23 @@ import json
import os
import socket
import sys
import re
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"
_LITERAL_ABSOLUTE_PATH: Final = re.compile(r"/[A-Za-z0-9._/-]+\Z")
_SHELL_ACTIVE: Final = frozenset("$`~*?[]{}<>;|&" + '"' + "'" + "\\" + "\n\r\t")
def deny(code: str) -> int:
@@ -23,7 +32,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 +43,61 @@ 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 byte-literal Claude recovery argv to ``RECOVERY_TOOL``.
Claude's Bash tool evaluates its raw command with a real shell. Therefore
the gate never attempts a second shell parser: any quote, expansion,
redirection, operator, glob, newline, or non-space whitespace is refused
before tokenizing. The remaining plain-space split is an exact argv proof,
not a best-effort interpretation of shell syntax.
"""
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
or any(character in _SHELL_ACTIVE for character in command)
or command.startswith(" ")
or command.endswith(" ")
or " " in command
):
return str(tool_name)
argv = command.split(" ")
if " ".join(argv) != command or len(argv) < 3 or argv[0] != "python3":
return str(tool_name)
if argv[1] != str(recovery_command):
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)
if argv[3::2] != ["--construction", "--compaction-epoch", "--request-epoch"]:
return str(tool_name)
construction, compaction_epoch, request_epoch = argv[4::2]
if (
_LITERAL_ABSOLUTE_PATH.fullmatch(construction) is None
or not compaction_epoch.isdecimal()
or not request_epoch.isdecimal()
):
return str(tool_name)
return RECOVERY_TOOL
def broker_request(socket_path: Path, request: dict[str, object]) -> dict[str, object]:
@@ -70,11 +133,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

@@ -0,0 +1,151 @@
#!/usr/bin/env python3
"""Constrained recovery command: the sole ungated Mosaic mutator.
This is deliberately a thin client of the broker's recovery entrypoint. It
never accepts receipt text or a caller-provided challenge: the broker mints the
fresh challenge, delivers its exact receipt envelope, and later asks the
trusted ReceiptObserver seam to observe that same pending cycle.
"""
from __future__ import annotations
import argparse
import json
import os
import socket
import sys
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
BROKER_TIMEOUT_SECONDS: Final = 1.5
def broker_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("recovery 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 broker reply")
value = json.loads(response)
if not isinstance(value, dict):
raise ValueError("invalid broker reply")
return value
def load_construction(path: Path) -> dict[str, object]:
raw = path.read_bytes()
if len(raw) > MAX_FRAME:
raise ValueError("construction exceeds broker frame limit")
value = json.loads(raw)
if not isinstance(value, dict):
raise ValueError("construction must be an object")
return value
def identity(environ: Mapping[str, str]) -> tuple[Path, str, int, str]:
socket_path = Path(environ["MOSAIC_LEASE_BROKER_SOCKET"])
session_id = environ["MOSAIC_LEASE_SESSION_ID"]
generation = int(environ["MOSAIC_RUNTIME_GENERATION"])
runtime = environ["MOSAIC_LEASE_RUNTIME"]
if generation < 0 or runtime not in {"claude", "pi"}:
raise ValueError("invalid runtime identity")
return socket_path, session_id, generation, runtime
def begin(
construction_path: Path,
compaction_epoch: int,
request_epoch: int,
environ: Mapping[str, str],
) -> dict[str, object]:
if compaction_epoch < 0 or request_epoch < 0:
raise ValueError("epochs must be non-negative")
construction = load_construction(construction_path)
# Invoke the shared WI-5 construction before asking the broker to repeat
# its authoritative admission/build. No digest or receipt enters via CLI.
built = build_payload_from_wire(construction)
if (
built.injectionDecision != "ACCEPTED"
or not built.promotion
or not isinstance(built.h_source, str)
or not isinstance(built.h_payload, str)
):
raise ValueError("payload construction refused")
socket_path, session_id, generation, runtime = identity(environ)
return broker_request(socket_path, {
"action": "begin_recovery",
"session_id": session_id,
"runtime_generation": generation,
"runtime": runtime,
"binding": {
"compaction_epoch": compaction_epoch,
"request_epoch": request_epoch,
"h_source": built.h_source,
"h_payload": built.h_payload,
"schema_version": 1,
},
"construction": construction,
})
def complete(environ: Mapping[str, str]) -> dict[str, object]:
socket_path, session_id, generation, _runtime = identity(environ)
# No receipt or challenge argument exists: recovery completion can only use
# the broker's current recovery cycle and its trusted observer seam.
return broker_request(socket_path, {
"action": "complete_recovery",
"session_id": session_id,
"runtime_generation": generation,
})
def main(argv: Sequence[str] | None = None, *, environ: Mapping[str, str] | None = None) -> int:
parser = argparse.ArgumentParser(description=__doc__)
subcommands = parser.add_subparsers(dest="phase", required=True)
begin_parser = subcommands.add_parser("begin", help="mint and deliver a fresh recovery receipt")
begin_parser.add_argument("--construction", required=True, type=Path)
begin_parser.add_argument("--compaction-epoch", required=True, type=int)
begin_parser.add_argument("--request-epoch", required=True, type=int)
subcommands.add_parser("complete", help="observe and promote only the current recovery receipt")
arguments = parser.parse_args(argv)
source_environment = os.environ if environ is None else environ
try:
reply = (
begin(
arguments.construction,
arguments.compaction_epoch,
arguments.request_epoch,
source_environment,
)
if arguments.phase == "begin"
else complete(source_environment)
)
except (KeyError, OSError, ValueError, json.JSONDecodeError) as error:
print(f"Mosaic constrained recovery refused: {error}", file=sys.stderr)
return 2
print(json.dumps(reply, separators=(",", ":")))
return 0 if reply.get("ok") is True else 2
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -25,7 +25,7 @@
"lint": "eslint src",
"typecheck": "tsc --noEmit",
"test": "vitest run --passWithNoTests && pnpm run test:framework-shell",
"test:framework-shell": "python3 src/lease-broker/daemon_deadline_unittest.py && python3 src/lease-broker/normative_fragments_unittest.py && python3 src/lease-broker/receipt_challenge_unittest.py && python3 src/mutator-gate/runtime_tools_unittest.py && python3 src/mutator-gate/runtime_launch_guard_unittest.py && python3 framework/tools/lease-broker/check-runtime-launches.py --root ../.. && bash framework/tools/codex/test-pr-diff-context.sh"
"test:framework-shell": "python3 src/lease-broker/daemon_deadline_unittest.py && python3 src/lease-broker/normative_fragments_unittest.py && python3 src/lease-broker/receipt_challenge_unittest.py && python3 src/lease-broker/context_recovery_unittest.py && python3 src/lease-broker/recovery_runtime_unittest.py && python3 src/lease-broker/recovery_b1_adversarial_unittest.py && python3 src/lease-broker/framework_skill_portability_unittest.py && python3 src/mutator-gate/runtime_tools_unittest.py && python3 src/mutator-gate/runtime_launch_guard_unittest.py && python3 framework/tools/lease-broker/check-runtime-launches.py --root ../.. && bash framework/tools/codex/test-pr-diff-context.sh"
},
"dependencies": {
"@mosaicstack/brain": "workspace:*",

View File

@@ -8,6 +8,7 @@ import {
mkdirSync,
mkdtempSync,
readlinkSync,
readFileSync,
rmSync,
symlinkSync,
writeFileSync,
@@ -26,6 +27,9 @@ import {
const LEGACY_SYNC_SCRIPT = fileURLToPath(
new URL('../../framework/tools/_scripts/mosaic-sync-skills', import.meta.url),
);
const CONTEXT_REFRESH_SOURCE_SKILL = fileURLToPath(
new URL('../../framework/skills/mosaic-context-refresh/SKILL.md', import.meta.url),
);
describe('Claude skill bridge', () => {
let root: string;
@@ -360,6 +364,25 @@ describe('Claude skill bridge', () => {
});
describe('syncClaudeSkills', () => {
it('projects the durable context-refresh skill through the #824 bridge in a tmp root', () => {
expect(existsSync(CONTEXT_REFRESH_SOURCE_SKILL)).toBe(true);
const source = readFileSync(CONTEXT_REFRESH_SOURCE_SKILL, 'utf8');
expect(source).toContain('recover-context.py');
expect(source).toContain('middle drop is not represented as receipt-detectable');
const skillDir = createSkill('mosaic-context-refresh');
writeFileSync(join(skillDir, 'SKILL.md'), source);
const result = syncClaudeSkills(paths);
expect(result).toEqual({
registered: ['mosaic-context-refresh'],
repaired: [],
unchanged: [],
conflicts: [],
});
expectCorrectLink('mosaic-context-refresh');
});
it('generically creates every missing canonical link and repairs managed broken links', () => {
createSkill('added-after-setup');
createSkill('another-new-skill');

View File

@@ -78,6 +78,7 @@ const PROBE_PATHS = [
'install.sh',
'framework-manifest.txt',
'guides/E2E-DELIVERY.md',
'skills/mosaic-context-refresh/SKILL.md',
'tools/git/pr-create.sh',
'tools/_lib/manifest.sh',
'defaults/SOUL.md',
@@ -97,6 +98,7 @@ const PROBE_PATHS = [
'memory/note.md',
'sources/skills/x.md',
'credentials/c.json',
'skills-local/custom/SKILL.md',
'tools/_lib/credentials.json',
'fleet/roster.yaml',
'fleet/roster.json',

View File

@@ -304,6 +304,11 @@ describe('manifest completeness against shipped framework tree', () => {
expect(misclassified).toEqual([]);
});
it('shipped canonical skills are framework-owned while local skills are operator-owned', () => {
expect(resolveOwnership(manifest, 'skills/mosaic-context-refresh/SKILL.md')).toBe('framework');
expect(resolveOwnership(manifest, 'skills-local/custom/SKILL.md')).toBe('operator');
});
it('the operator-owned surface from #791 resolves to operator', () => {
const operatorPaths = [
'agents/coder0.conf',
@@ -313,6 +318,7 @@ describe('manifest completeness against shipped framework tree', () => {
'SOUL.local.md',
'USER.local.md',
'STANDARDS.local.md',
'skills-local/custom/SKILL.md',
'tools/_lib/credentials.json',
'fleet/roster.yaml',
'fleet/roster.json',

View File

@@ -0,0 +1,171 @@
#!/usr/bin/env python3
"""RED-first WI-6 contracts against the shipped constrained recovery entrypoint."""
from __future__ import annotations
import base64
import hashlib
import importlib.util
import os
import tempfile
import unittest
from pathlib import Path
TOOLS = Path(__file__).parents[2] / "framework/tools/lease-broker"
DAEMON_PATH = TOOLS / "daemon.py"
FRAGMENTS_PATH = TOOLS / "normative_fragments.py"
OBSERVER_PATH = TOOLS / "receipt_observer.py"
def load_module(name: str, path: Path):
assert path.is_file(), f"shipped module is missing: {path}"
spec = importlib.util.spec_from_file_location(name, path)
if spec is None or spec.loader is None:
raise RuntimeError(f"unable to load {name}")
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
DAEMON = load_module("lease_broker_recovery_daemon", DAEMON_PATH)
FRAGMENTS = load_module("lease_broker_recovery_fragments", FRAGMENTS_PATH)
OBSERVER = load_module("lease_broker_recovery_observer", OBSERVER_PATH)
class RecoveryFixture(unittest.TestCase):
def setUp(self) -> None:
self.temporary = tempfile.TemporaryDirectory()
root = Path(self.temporary.name)
os.chmod(root, 0o700)
self.peer = (os.getpid(), os.getuid(), os.getgid())
self.observer = OBSERVER.TestReceiptObserver()
self.broker = DAEMON.Broker(DAEMON.StateStore(root / "state.json"), observer=self.observer)
registered = self.broker.handle(self.peer, {
"action": "register_anchor",
"runtime_generation": 11,
})
self.session_id = registered["session_id"]
def tearDown(self) -> None:
self.temporary.cleanup()
def construction_and_binding(self) -> tuple[dict[str, object], dict[str, object]]:
content = b"Mosaic recovery authority\n"
construction = {
"manifest_version": 1,
"generator_version": "wi6-recovery-test",
"fragments": [{
"source_id": "authority/recovery",
"content_base64": base64.b64encode(content).decode("ascii"),
"expected_sha256": hashlib.sha256(content).hexdigest(),
}],
}
built = FRAGMENTS.build_payload_from_wire(construction)
self.assertEqual(built.injectionDecision, "ACCEPTED")
self.assertTrue(built.promotion)
return construction, {
"compaction_epoch": 17,
"request_epoch": 23,
"h_source": built.h_source,
"h_payload": built.h_payload,
"schema_version": 1,
}
def begin_normal(self) -> dict[str, object]:
construction, binding = self.construction_and_binding()
return self.broker.handle(self.peer, {
"action": "begin_verification",
"session_id": self.session_id,
"runtime_generation": 11,
"runtime": "pi",
"binding": binding,
"construction": construction,
})
def begin_recovery(self) -> dict[str, object]:
construction, binding = self.construction_and_binding()
return self.broker.handle(self.peer, {
"action": "begin_recovery",
"session_id": self.session_id,
"runtime_generation": 11,
"runtime": "pi",
"binding": binding,
"construction": construction,
})
def complete_recovery(self) -> dict[str, object]:
return self.broker.handle(self.peer, {
"action": "complete_recovery",
"session_id": self.session_id,
"runtime_generation": 11,
})
def assert_recovery_refused_unverified(self, code: str) -> None:
with self.assertRaisesRegex(DAEMON.BrokerFailure, code):
self.complete_recovery()
self.assertEqual(self.broker.leases[self.session_id]["state"], DAEMON.LEASE_UNVERIFIED)
denied = self.broker.handle(self.peer, {
"action": "authorize_tool",
"session_id": self.session_id,
"runtime_generation": 11,
"runtime": "pi",
"tool_name": "bash",
})
self.assertEqual(denied["decision"], "deny")
class ConstrainedRecoveryContractTest(RecoveryFixture):
def test_recovery_mints_a_fresh_challenge_distinct_from_normal_path(self) -> None:
normal = self.begin_normal()
recovery = self.begin_recovery()
self.assertEqual(recovery["state"], "PENDING_DELIVERY")
self.assertNotEqual(normal["receipt_challenge"], recovery["receipt_challenge"])
self.assertIn(recovery["receipt_challenge"], recovery["receipt"])
def test_c4_normal_path_receipt_cannot_be_replayed_through_recovery(self) -> None:
normal = self.begin_normal()
recovery = self.begin_recovery()
self.assertNotEqual(normal["receipt_challenge"], recovery["receipt_challenge"])
self.observer.record_latest_assistant_message(self.session_id, 11, normal["receipt"])
self.assert_recovery_refused_unverified("RECEIPT_MISMATCH")
def test_t27_observable_partial_delivery_variants_never_promote(self) -> None:
variants = {
"absent": None,
"malformed": "MOSAIC-RECEIPT{malformed}",
"prefix-truncated": None,
"observable-adapter-mutation": None,
# Tail-only is represented only by this concrete malformed/incomplete
# delivery. It is not a category-wide tail-only detection claim.
"tail-only-malformed": "H_payload=tail-only",
}
for name, observed in variants.items():
with self.subTest(name=name):
recovery = self.begin_recovery()
if name == "prefix-truncated":
observed = recovery["receipt"][:-1]
elif name == "observable-adapter-mutation":
observed = recovery["receipt"].replace("H_payload=", "H_payload=0", 1)
if observed is not None:
self.observer.record_latest_assistant_message(self.session_id, 11, observed)
self.assert_recovery_refused_unverified(
"RECEIPT_OBSERVATION_UNAVAILABLE" if observed is None else "RECEIPT_MISMATCH"
)
def test_negative_capability_tail_preserving_middle_drop_is_not_receipt_detectable(self) -> None:
recovery = self.begin_recovery()
# The observer seam receives the exact terminal message, not the delivered
# payload bytes. A middle drop that preserves this tail is therefore T-C
# and deliberately NOT represented as receipt-detectable; WI-7 server-side
# evidence owns that residual. This is not an assertion that it is caught.
self.observer.record_latest_assistant_message(self.session_id, 11, recovery["receipt"])
promoted = self.complete_recovery()
self.assertEqual(promoted["state"], DAEMON.LEASE_VERIFIED)
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,64 @@
#!/usr/bin/env python3
"""RED-first framework-firewall and portability contracts for shipped skills."""
from __future__ import annotations
import importlib.util
import re
import unittest
from pathlib import Path
MOSAIC = Path(__file__).parents[2]
SKILLS = MOSAIC / "framework/skills"
REFRESH_SKILL = SKILLS / "mosaic-context-refresh/SKILL.md"
GATE_PATH = MOSAIC / "framework/tools/lease-broker/mutator-gate.py"
OPERATOR_HOME = re.compile(r"/home/[^/\s]+/")
RECOVERY_PLACEHOLDER = "/absolute/path/to/mosaic/tools/lease-broker/recover-context.py"
CONSTRUCTION_PLACEHOLDER = "/absolute/path/to/mosaic-context-refresh-construction.json"
def load_gate():
spec = importlib.util.spec_from_file_location("framework_skill_portability_gate", GATE_PATH)
if spec is None or spec.loader is None:
raise RuntimeError("unable to load mutator gate")
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
GATE = load_gate()
class FrameworkSkillPortabilityTest(unittest.TestCase):
def test_shipped_framework_skills_contain_no_operator_home_path(self) -> None:
offenders = [
str(path.relative_to(SKILLS))
for path in SKILLS.rglob("SKILL.md")
if OPERATOR_HOME.search(path.read_text(encoding="utf-8"))
]
self.assertEqual(offenders, [])
def test_shipped_recovery_template_resolves_to_a_literal_install_path_and_admits(self) -> None:
source = REFRESH_SKILL.read_text(encoding="utf-8")
self.assertIn(RECOVERY_PLACEHOLDER, source)
self.assertIn(CONSTRUCTION_PLACEHOLDER, source)
resolved_recovery = "/opt/mosaic/tools/lease-broker/recover-context.py"
resolved_construction = "/opt/mosaic/recovery/construction.json"
rendered = source.replace(RECOVERY_PLACEHOLDER, resolved_recovery).replace(
CONSTRUCTION_PLACEHOLDER, resolved_construction
)
match = re.search(r"```bash\s*\n\s*(.*?)\n\s*```", rendered, flags=re.DOTALL)
self.assertIsNotNone(match)
command = match.group(1) if match is not None else ""
self.assertEqual(
GATE.recovery_invocation_name(
{"tool_name": "Bash", "tool_input": {"command": command}},
Path(resolved_recovery),
),
GATE.RECOVERY_TOOL,
)
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,218 @@
#!/usr/bin/env python3
"""RED-first adversarial Claude B1 gate contracts.
This private harness drives the shipped gate and daemon out of process. It
never contacts a live broker/runtime and executes a shell payload only after a
regression has already (incorrectly) received the recovery exemption.
"""
from __future__ import annotations
import json
import os
import re
import socket
import subprocess
import sys
import tempfile
import time
import unittest
from pathlib import Path
TOOLS = Path(__file__).parents[2] / "framework/tools/lease-broker"
FRAMEWORK = Path(__file__).parents[2] / "framework"
DAEMON = TOOLS / "daemon.py"
GATE = TOOLS / "mutator-gate.py"
RECOVERY = TOOLS / "recover-context.py"
SKILL = FRAMEWORK / "skills/mosaic-context-refresh/SKILL.md"
def request(socket_path: Path, value: dict[str, object]) -> dict[str, object]:
with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as connection:
connection.settimeout(3.0)
connection.connect(str(socket_path))
connection.sendall((json.dumps(value, separators=(",", ":")) + "\n").encode())
connection.shutdown(socket.SHUT_WR)
response = bytearray()
while True:
chunk = connection.recv(4096)
if not chunk:
break
response.extend(chunk)
if not response.endswith(b"\n") or response.count(b"\n") != 1:
raise AssertionError(f"unframed broker response: {bytes(response)!r}")
reply = json.loads(response[:-1])
if not isinstance(reply, dict):
raise AssertionError("broker response is not an object")
return reply
def wait_ready(process: subprocess.Popen[str], socket_path: Path) -> None:
deadline = time.monotonic() + 5.0
while time.monotonic() < deadline:
if socket_path.exists():
return
if process.poll() is not None:
output = process.stdout.read() if process.stdout is not None else ""
raise RuntimeError(f"daemon exited before READY: {output}")
time.sleep(0.02)
raise TimeoutError("daemon did not create private socket")
class ClaudeRecoveryGateAdversarialTest(unittest.TestCase):
def setUp(self) -> None:
self.temporary = tempfile.TemporaryDirectory()
self.root = Path(self.temporary.name)
os.chmod(self.root, 0o700)
self.socket = self.root / "broker.sock"
self.daemon = subprocess.Popen(
[sys.executable, "-I", "-S", "-B", str(DAEMON), "--socket", str(self.socket), "--state", str(self.root / "state.json")],
stdin=subprocess.DEVNULL,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
)
wait_ready(self.daemon, self.socket)
registered = request(self.socket, {"action": "register_anchor", "runtime_generation": 1})
self.session_id = registered["session_id"]
self.environment = {
**os.environ,
"MOSAIC_LEASE_BROKER_SOCKET": str(self.socket),
"MOSAIC_LEASE_SESSION_ID": self.session_id,
"MOSAIC_RUNTIME_GENERATION": "1",
"MOSAIC_LEASE_RUNTIME": "claude",
}
# This private path is only a gate-classifier identity; if a regression
# blesses a payload, bash invokes no installed/live recovery command.
self.recovery_path = self.root / "recover-context.py"
self.canonical = (
f"python3 {self.recovery_path} begin "
f"--construction {self.root / 'construction.json'} --compaction-epoch 0 --request-epoch 0"
)
def tearDown(self) -> None:
if self.daemon.poll() is None:
self.daemon.terminate()
try:
self.daemon.wait(timeout=3.0)
except subprocess.TimeoutExpired:
self.daemon.kill()
self.daemon.wait()
if self.daemon.stdout is not None:
self.daemon.stdout.close()
self.temporary.cleanup()
def gate(
self, command: str, recovery_command: Path | str | None = None
) -> subprocess.CompletedProcess[str]:
configured_recovery = self.recovery_path if recovery_command is None else recovery_command
return subprocess.run(
[
sys.executable,
"-I",
"-S",
"-B",
str(GATE),
"--runtime",
"claude",
"--recovery-command",
str(configured_recovery),
],
input=json.dumps({"tool_name": "Bash", "tool_input": {"command": command}}),
text=True,
capture_output=True,
env=self.environment,
check=False,
)
def command_for(self, position: str, value: str) -> str:
argv = [
"python3",
str(self.recovery_path),
"begin",
"--construction",
str(self.root / "construction.json"),
"--compaction-epoch",
"0",
"--request-epoch",
"0",
]
positions = {
"executable": 0,
"path": 1,
"phase": 2,
"construction_flag": 3,
"construction": 4,
"compaction_epoch_flag": 5,
"compaction_epoch": 6,
"request_epoch_flag": 7,
"request_epoch": 8,
}
try:
argv[positions[position]] = value
except KeyError as exc:
raise AssertionError(f"unknown argv position {position}") from exc
return " ".join(argv)
def test_canonical_literal_and_shipped_skill_invocation_are_ungated(self) -> None:
self.assertEqual(self.gate(self.canonical).returncode, 0)
source = SKILL.read_text(encoding="utf-8")
recovery_placeholder = "/absolute/path/to/mosaic/tools/lease-broker/recover-context.py"
construction_placeholder = "/absolute/path/to/mosaic-context-refresh-construction.json"
self.assertIn(recovery_placeholder, source)
self.assertIn(construction_placeholder, source)
resolved_recovery = "/opt/mosaic/tools/lease-broker/recover-context.py"
rendered = source.replace(recovery_placeholder, resolved_recovery).replace(
construction_placeholder, "/opt/mosaic/recovery/construction.json"
)
match = re.search(r"```bash\s*\n\s*(.*?)\n\s*```", rendered, flags=re.DOTALL)
self.assertIsNotNone(match)
shipped = match.group(1) if match is not None else ""
self.assertEqual(self.gate(shipped, resolved_recovery).returncode, 0)
def test_every_shell_active_vector_in_every_argv_position_falls_through_to_bash_deny(self) -> None:
positions = (
"executable",
"path",
"phase",
"construction_flag",
"construction",
"compaction_epoch_flag",
"compaction_epoch",
"request_epoch_flag",
"request_epoch",
)
marker = self.root / "PWNED"
marker_vector = f"$(touch${{IFS}}{marker})"
vectors = {
"command-substitution": marker_vector,
"backtick": f"`touch${{IFS}}{marker}`",
"process-substitution": f"<(touch${{IFS}}{marker})",
"parameter-expansion": "${IFS}",
"home-expansion": "${HOME}",
"arithmetic-expansion": "$((1+1))",
"brace-expansion": "{a,b}",
"tilde-expansion": "~",
"glob": "*",
"redirection": ">",
"semicolon": f";touch${{IFS}}{marker}",
"and": f"&&touch${{IFS}}{marker}",
"pipe": f"|touch${{IFS}}{marker}",
"embedded-newline": "literal\nnext",
"quoting-trick": "'literal'",
}
for position in positions:
for kind, vector in vectors.items():
with self.subTest(position=position, kind=kind):
marker.unlink(missing_ok=True)
command = self.command_for(position, vector)
gated = self.gate(command)
if gated.returncode == 0 and kind in {"command-substitution", "backtick", "process-substitution", "semicolon", "and", "pipe"}:
subprocess.run(["bash", "-c", command], cwd=self.root, env=self.environment, check=False)
self.assertEqual(gated.returncode, 2, f"unexpected recovery exemption: {command!r}")
self.assertFalse(marker.exists(), f"shell payload executed: {command!r}")
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,282 @@
#!/usr/bin/env python3
"""RED-first B1/B2 runtime-boundary contracts for constrained recovery.
Every runtime process in these tests is a fresh child against a private daemon
and Unix sockets. They do not activate a live Mosaic daemon or model stream.
"""
from __future__ import annotations
import base64
import hashlib
import json
import os
import socket
import subprocess
import sys
import tempfile
import time
import unittest
from pathlib import Path
TOOLS = Path(__file__).parents[2] / "framework/tools/lease-broker"
FRAMEWORK = Path(__file__).parents[2] / "framework"
DAEMON = TOOLS / "daemon.py"
GATE = TOOLS / "mutator-gate.py"
RECOVERY = TOOLS / "recover-context.py"
OBSERVER_CLIENT = TOOLS / "receipt-observer-client.py"
CLAUDE_SETTINGS = FRAMEWORK / "runtime/claude/settings.json"
PI_EXTENSION = FRAMEWORK / "runtime/pi/mosaic-extension.ts"
def request(socket_path: Path, value: dict[str, object]) -> dict[str, object]:
with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as connection:
connection.settimeout(3.0)
connection.connect(str(socket_path))
connection.sendall((json.dumps(value, separators=(",", ":")) + "\n").encode())
connection.shutdown(socket.SHUT_WR)
response = bytearray()
while True:
chunk = connection.recv(4096)
if not chunk:
break
response.extend(chunk)
if not response.endswith(b"\n") or response.count(b"\n") != 1:
raise AssertionError(f"unframed broker response: {bytes(response)!r}")
reply = json.loads(response[:-1])
if not isinstance(reply, dict):
raise AssertionError("broker response is not an object")
return reply
def wait_ready(process: subprocess.Popen[str], socket_path: Path) -> None:
deadline = time.monotonic() + 5.0
while time.monotonic() < deadline:
if socket_path.exists():
return
if process.poll() is not None:
output = process.stdout.read() if process.stdout is not None else ""
raise RuntimeError(f"daemon exited before READY: {output}")
time.sleep(0.02)
raise TimeoutError("daemon did not create private broker socket")
class RuntimeBoundaryFixture(unittest.TestCase):
def setUp(self) -> None:
self.temporary = tempfile.TemporaryDirectory()
self.root = Path(self.temporary.name)
os.chmod(self.root, 0o700)
self.socket = self.root / "broker.sock"
self.observer_socket = self.root / "observer.sock"
self.state = self.root / "state.json"
self.children: list[subprocess.Popen[str]] = []
def tearDown(self) -> None:
for child in self.children:
if child.poll() is None:
child.terminate()
try:
child.wait(timeout=3.0)
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:
arguments = [sys.executable, "-I", "-S", "-B", str(DAEMON), "--socket", str(self.socket), "--state", str(self.state)]
if production_observer:
arguments.extend(["--observer-socket", str(self.observer_socket)])
process = subprocess.Popen(
arguments,
stdin=subprocess.DEVNULL,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
)
self.children.append(process)
wait_ready(process, self.socket)
def register(self) -> str:
reply = request(self.socket, {"action": "register_anchor", "runtime_generation": 1})
session_id = reply.get("session_id")
self.assertTrue(reply.get("ok"))
self.assertIsInstance(session_id, str)
return session_id
def environment(self, session_id: str) -> dict[str, str]:
return {
**os.environ,
"MOSAIC_LEASE_BROKER_SOCKET": str(self.socket),
"MOSAIC_RECEIPT_OBSERVER_SOCKET": str(self.observer_socket),
"MOSAIC_LEASE_SESSION_ID": session_id,
"MOSAIC_RUNTIME_GENERATION": "1",
"MOSAIC_LEASE_RUNTIME": "pi",
}
def construction(self) -> Path:
content = b"WI-6 B2 production observer fixture\n"
path = self.root / "construction.json"
path.write_text(json.dumps({
"manifest_version": 1,
"generator_version": "wi6-repair-runtime-boundary",
"fragments": [{
"source_id": "authority/wi6-repair",
"content_base64": base64.b64encode(content).decode("ascii"),
"expected_sha256": hashlib.sha256(content).hexdigest(),
}],
}), encoding="utf-8")
os.chmod(path, 0o600)
return path
class RecoveryRuntimeBoundaryTest(RuntimeBoundaryFixture):
def test_b1_claude_exact_recovery_command_is_invocable_unverified_but_bash_is_not(self) -> None:
self.start_daemon(production_observer=False)
session_id = self.register()
environment = self.environment(session_id)
recovery_command = f"python3 {RECOVERY} complete"
mapped = subprocess.run(
[sys.executable, "-I", "-S", "-B", str(GATE), "--runtime", "claude", "--recovery-command", str(RECOVERY)],
input=json.dumps({"tool_name": "Bash", "tool_input": {"command": recovery_command}}),
text=True,
capture_output=True,
env={**environment, "MOSAIC_LEASE_RUNTIME": "claude"},
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"}}),
text=True,
capture_output=True,
env={**environment, "MOSAIC_LEASE_RUNTIME": "claude"},
check=False,
)
self.assertEqual(ordinary_bash.returncode, 2)
def test_b1_pi_registers_only_the_broker_exempt_recovery_tool(self) -> None:
extension = PI_EXTENSION.read_text(encoding="utf-8")
self.assertIn("const RECOVERY_TOOL = 'mosaic_context_recover'", extension)
self.assertIn("name: RECOVERY_TOOL", extension)
self.assertIn("checkPiMutatorGate(RECOVERY_TOOL)", extension)
self.assertNotIn("toolName === 'bash' ? RECOVERY_TOOL", extension)
def test_b2_production_observer_promotes_over_private_transport_and_rejects_broker_supplied_message(self) -> None:
self.start_daemon(production_observer=True)
session_id = self.register()
environment = self.environment(session_id)
begin = subprocess.run(
[sys.executable, "-I", "-S", "-B", str(RECOVERY), "begin", "--construction", str(self.construction()), "--compaction-epoch", "1", "--request-epoch", "1"],
text=True,
capture_output=True,
env=environment,
check=False,
)
self.assertEqual(begin.returncode, 0, begin.stderr)
cycle = json.loads(begin.stdout)
receipt = cycle.get("receipt")
self.assertIsInstance(receipt, str)
# S1: production transport is not a broker request field. The public
# broker endpoint keeps rejecting caller-supplied assistant evidence.
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_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"],
input=json.dumps({"latest_assistant_message": receipt}),
text=True,
capture_output=True,
env=environment,
check=False,
)
self.assertEqual(recorded.returncode, 0, recorded.stderr)
complete = subprocess.run(
[sys.executable, "-I", "-S", "-B", str(RECOVERY), "complete"],
text=True,
capture_output=True,
env=environment,
check=False,
)
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()