chore: consolidate new foundation and archive v1 (#1495)

This commit is contained in:
2026-09-07 12:32:57 -05:00
3511 changed files with 727899 additions and 10 deletions
@@ -0,0 +1,193 @@
#!/usr/bin/env python3
"""Enforcement-side version-coupling gate (issue #869, Point-1 card C4).
Root cause this exists to guard against (#828 version skew, restated from
the C1 activation probe in ``lease-activation-probe.ts``): the lease
broker's ENFORCEMENT half (this toolkit — ``launch-runtime.py``,
``mutator-gate.py``, ``revoke-lease.py``) and its ACTIVATION half
(``execLeaseGatedRuntime()`` in ``launch.ts``, which chains the gated
runtime through ``launch-runtime.py`` and injects ``MOSAIC_LEASE_*``) ship
on different channels — an npm package and a framework/CLI reseed. C1 gave
the activation half a versioned, machine-checkable identity
(``LEASE_ACTIVATION_CAPABILITY``, printed by the CLI's hidden
``mosaic __lease-capability`` subcommand). That identity is inert on its
own: nothing yet asserted that ENFORCEMENT actually requires the version
ACTIVATION advertises. This module is that assertion, owned by the
enforcement side.
``EXPECTED_ACTIVATION_CAPABILITY`` below is this toolkit's own contract
declaration — bump it only when this toolkit's launch/gate seam starts
requiring a different activation contract (new env vars it depends on,
changed chaining behavior, etc.), independent of any package semver, for
the same reason C1's constant is: #828 happened precisely because a
version number that should have moved did not.
This module never talks to a real broker or a real installed CLI in its
own tests — both the probe's command resolution and its ``run`` transport
are injectable so tests can drive every branch with fakes/stubs (see
``version_coupling_unittest.py``).
"""
from __future__ import annotations
import json
import os
import shlex
import shutil
import subprocess
from collections.abc import Callable, Mapping
from typing import Final, TypedDict
class ActivationCapability(TypedDict):
name: str
version: int
# ENFORCEMENT-side expected activation contract. OWNED by this toolkit (the
# enforcement half). Mirrors — but is deliberately a SEPARATE constant from
# — `LEASE_ACTIVATION_CAPABILITY` in
# `packages/mosaic/src/commands/lease-activation-probe.ts` (the activation
# half's own declaration of what it implements). The two are compared at
# runtime by `assert_activation_capability_matches()`; drift between them is
# exactly the version-skew failure mode #828/#869 exist to catch, and must
# FAIL LOUD, never a silent pass and never a dead (always-true) gate.
EXPECTED_ACTIVATION_CAPABILITY: Final[ActivationCapability] = {
"name": "lease-runtime-activation",
"version": 1,
}
# Matches `LEASE_CAPABILITY_PROBE_COMMAND` in lease-activation-probe.ts —
# the hidden CLI subcommand that prints the activation half's advertised
# capability as compact JSON.
LEASE_CAPABILITY_PROBE_COMMAND: Final = "__lease-capability"
# Budget for the out-of-process `mosaic __lease-capability` probe. The CLI
# is a Node program whose cold start alone measures 2.2-2.3s on a mid-range
# workstation (sb-it-1-dt, 2026-08-13), so a 2s budget made every launch on
# such hosts fail closed with the #869 skew message even though the
# capability matched. The timeout only bounds the pathological hang case —
# the happy path returns as soon as the probe exits — so a generous budget
# costs nothing on healthy hosts.
PROBE_TIMEOUT_SECONDS: Final = 10.0
# Override hook: a full shell-style command line (parsed with `shlex.split`)
# to run INSTEAD of resolving `mosaic` on PATH and appending the probe
# subcommand. Real deployments should never need this — `mosaic` is on PATH
# whenever a runtime was launched via `mosaic <cmd>` in the first place, the
# only real caller of this seam. It exists for integration tests that spawn
# `launch-runtime.py` directly (never through the real CLI) to supply a
# fake/stub CLI probe, matching the existing convention of those tests
# supplying a fake broker and a fake runtime binary rather than depending on
# host state.
MOSAIC_COMMAND_OVERRIDE_VAR: Final = "MOSAIC_LEASE_VERSION_PROBE_COMMAND"
class VersionCouplingError(Exception):
"""Raised when the activation capability is absent, unreadable, or does
not match what enforcement expects. Callers MUST fail loud on this
(non-zero exit, clear actionable stderr) — never swallow it into a
silent pass, and never let its absence be treated as compatible."""
def _resolve_probe_command(environ: Mapping[str, str]) -> list[str] | None:
override = environ.get(MOSAIC_COMMAND_OVERRIDE_VAR)
if override:
parsed = shlex.split(override)
return parsed or None
# Resolve against the PROVIDED environment's PATH, not the ambient
# os.environ. Before this, a test passing a hermetic environ still
# resolved (and spawned) the host's real `mosaic` — masked only on hosts
# where the real probe happened to exceed the old 2s timeout. No PATH in
# the provided environment means nothing is resolvable (fail-closed),
# matching the probe's overall contract.
resolved = shutil.which("mosaic", path=environ.get("PATH", ""))
if resolved is None:
return None
return [resolved, LEASE_CAPABILITY_PROBE_COMMAND]
def default_probe_activation_capability(
environ: Mapping[str, str] | None = None,
*,
run: Callable[..., subprocess.CompletedProcess[str]] = subprocess.run,
) -> ActivationCapability | None:
"""Real capability lookup: resolves and executes the CLI's hidden
``__lease-capability`` probe subcommand out-of-process (the same
mechanism `defaultCapabilityProbe()` in lease-activation-probe.ts uses
from the activation side) and parses its JSON stdout. Any failure to
resolve a command, spawn it, have it exit zero, or produce a well-shaped
``{name, version}`` JSON object is treated as NO capability (``None``)
— fail-closed, never a fabricated/guessed capability.
"""
source_environment = os.environ if environ is None else environ
command = _resolve_probe_command(source_environment)
if command is None:
return None
try:
completed = run(
command,
capture_output=True,
text=True,
timeout=PROBE_TIMEOUT_SECONDS,
check=False,
)
except (OSError, subprocess.TimeoutExpired, ValueError):
return None
if completed.returncode != 0:
return None
try:
parsed = json.loads(completed.stdout)
except json.JSONDecodeError:
return None
if (
not isinstance(parsed, dict)
or not isinstance(parsed.get("name"), str)
or not isinstance(parsed.get("version"), int)
or isinstance(parsed.get("version"), bool)
):
return None
return {"name": parsed["name"], "version": parsed["version"]}
def format_mismatch_message(
activation: ActivationCapability | None,
expected: ActivationCapability,
) -> str:
"""Actionable, non-silent remediation message for either failure shape:
absent/unreadable capability, or a present-but-incompatible one."""
if activation is None:
return (
"Mosaic lease activation capability unreadable: enforcement "
f"expects '{expected['name']}' v{expected['version']} but the "
f"CLI's `mosaic {LEASE_CAPABILITY_PROBE_COMMAND}` probe produced "
"no usable result (mosaic not on PATH, non-zero exit, or "
"malformed output) — framework/CLI version skew; upgrade both "
"as one unit; see #869."
)
if activation["name"] != expected["name"]:
return (
f"activation capability name '{activation['name']}' != "
f"enforcement expects '{expected['name']}' — framework/CLI "
"version skew; upgrade both as one unit; see #869"
)
return (
f"activation capability v{activation['version']} != enforcement "
f"expects v{expected['version']} — framework/CLI version skew; "
"upgrade both as one unit; see #869"
)
def assert_activation_capability_matches(
activation: ActivationCapability | None,
expected: ActivationCapability = EXPECTED_ACTIVATION_CAPABILITY,
) -> None:
"""Raise `VersionCouplingError` unless `activation` is present AND its
`name`/`version` exactly match `expected`. Absence is treated the same
as a mismatch — never a silent pass."""
if (
activation is None
or activation.get("name") != expected["name"]
or activation.get("version") != expected["version"]
):
raise VersionCouplingError(format_mismatch_message(activation, expected))
@@ -0,0 +1,436 @@
#!/usr/bin/env python3
"""Fail CI when production code launches Claude/Pi outside the lease gate."""
from __future__ import annotations
import argparse
import json
import re
import shlex
import sys
from pathlib import Path
from typing import Final, NamedTuple, Sequence
SCANNED_ROOTS: Final = ("packages", "apps", "plugins", "tools")
SCANNED_SUFFIXES: Final = {
".bash",
".cjs",
".js",
".json",
".mjs",
".py",
".sh",
".ts",
".tsx",
".yaml",
".yml",
".zsh",
}
SKIPPED_DIRECTORIES: Final = {
".git",
".next",
".turbo",
"coverage",
"dist",
"node_modules",
}
DANGEROUS_PRIMITIVE: Final = "--dangerously-" + "skip-permissions"
CHOKE_POINT_SUFFIX: Final = "framework/tools/lease-broker/launch-runtime.py"
SHELL_SUFFIXES: Final = {".bash", ".sh", ".zsh"}
# A launch line is gated only when it CALLS the common wrapper in command
# position, invokes the TypeScript adapter, or constructs/runs a `mosaic`
# runtime command. A marker in a comment, string argument, echo, or unrelated
# variable can never satisfy these invocation-shaped patterns.
GATED_PATTERNS: Final = (
re.compile(r"(?:^\s*|=>\s*)execLeaseGatedRuntime\s*\("),
re.compile(
r"\bexecRuntime\s*\(\s*[\"']python3[\"']\s*,\s*"
r"\[\s*launcher\s*,.*[\"']--runtime[\"']\s*,\s*runtime\s*,\s*[\"']--[\"']"
),
re.compile(
r"(?:^|[;&|]\s*|\bexec\s+|\b(?:LAUNCH_COMMAND|launch_cmd)\s*=\s*\(?|\becho\s+[\"'])"
r"mosaic\s+(?:yolo\s+)?(?:claude|pi|claudex|[\"']?\$\{?runtime\}?[\"']?|[\"']?\$MOSAIC_AGENT_RUNTIME[\"']?)\b"
),
re.compile(r"\[\s*[\"']mosaic[\"']\s*,\s*(?:[\"']yolo[\"']\s*,\s*)?(?:runtime|[\"'](?:claude|pi|claudex)[\"'])"),
)
DIRECT_PATTERNS: Final = (
# Shell/process command forms, including here-doc command examples that an
# operator could execute verbatim.
re.compile(r"^\s*(?:claude|pi)(?:\s|$)"),
re.compile(r"(?:^|[;&|]\s*|\bexec\s+|\bcommand\s+)(?:claude|pi)\s+(?:-p\b|--dangerously\b|--print\b)"),
re.compile(r"\bexec\s+(?:claude|pi)(?:\s|$)"),
re.compile(r"\bexec\s+(?:/[^\s/]+)+/(?:claude|pi)(?:\s|$)"),
re.compile(r"\bexec\s+[\"']?\$(?:\{?runtime\}?|MOSAIC_AGENT_RUNTIME)\b"),
# JS/TS and Python process APIs with a literal runtime binary.
re.compile(
r"\b(?:spawn|spawnSync|exec|execSync|execFile|execFileSync|execv|execvp|execvpe|Popen|run|call|system|check_call|check_output)\s*\(\s*(?:\[\s*)?[\"'](?:claude|pi)(?:[\"']|\s)"
),
re.compile(
r"\b(?:spawn|spawnSync|exec|execSync|execFile|execFileSync|execv|execvp|execvpe|Popen|run|call|system|check_call|check_output)\s*\(\s*(?:\[\s*)?[\"'](?:/[^\"'/]+)+/(?:claude|pi)[\"']"
),
# Launch-command arrays and the prior @mosaicstack/coord dynamic default.
re.compile(r"\b(?:spawn|spawnSync|exec|execSync|execFile|execFileSync)\s*\(\s*runtime\b"),
re.compile(r"\b(?:command|launchCommand|LAUNCH_COMMAND)\s*=\s*(?:\(|\[)\s*[\"']?(?:claude|pi)\b"),
re.compile(r"\b(?:command|launchCommand|LAUNCH_COMMAND)\s*=\s*(?:\(|\[)\s*[\"']?\$(?:\{?runtime\}?|MOSAIC_AGENT_RUNTIME)\b"),
re.compile(r"\breturn\s*\[\s*runtime\s*,\s*[\"'](?:-p|--dangerously)"),
re.compile(r"\$\(\s*(?:claude|pi)(?:\s|$)"),
re.compile(r"\beval\s+[\"'](?:claude|pi)(?:\s|[\"'])"),
)
RUNTIME_ASSIGNMENT: Final = re.compile(
r"^\s*([A-Za-z_][A-Za-z0-9_]*)\s*=\s*[\"']?(?:claude|pi)(?:\s|[\"']|[;&|]|$)"
)
TYPESCRIPT_WRAPPER_INVOCATION: Final = re.compile(
r"(?m)^\s*execRuntime\s*\(\s*[\"']python3[\"']\s*,\s*"
r"\[\s*launcher\s*,[^\]]*[\"']--runtime[\"']\s*,\s*runtime\s*,\s*"
r"[\"']--[\"']\s*,\s*runtime(?:\s*,|\s*\])",
re.DOTALL,
)
class LaunchSite(NamedTuple):
path: Path
line_number: int
line: str
classification: str
def is_test_path(path: Path) -> bool:
name = path.name.lower()
return (
"__tests__" in path.parts
or ".spec." in name
or ".test." in name
or name.endswith("_unittest.py")
or name.startswith("test-")
or name.startswith("test_")
)
def strip_comments(path: Path, line: str, in_block_comment: bool = False) -> tuple[str, bool]:
suffix = path.suffix.lower()
hash_comments = suffix in {".bash", ".py", ".sh", ".yaml", ".yml", ".zsh"}
slash_comments = suffix in {".cjs", ".js", ".mjs", ".ts", ".tsx"}
output: list[str] = []
quote: str | None = None
escaped = False
index = 0
while index < len(line):
if in_block_comment:
end = line.find("*/", index)
if end < 0:
return "".join(output), True
in_block_comment = False
index = end + 2
continue
character = line[index]
following = line[index + 1] if index + 1 < len(line) else ""
if quote is not None:
output.append(character)
if escaped:
escaped = False
elif character == "\\":
escaped = True
elif character == quote:
quote = None
index += 1
continue
if character in {"'", '"', "`"}:
quote = character
output.append(character)
index += 1
continue
if slash_comments and character == "/" and following == "/":
break
if slash_comments and character == "/" and following == "*":
in_block_comment = True
index += 2
continue
if hash_comments and character == "#":
if suffix == ".py" or index == 0 or line[index - 1].isspace():
break
output.append(character)
index += 1
return "".join(output), in_block_comment
def is_choke_point(path: Path) -> bool:
return path.as_posix().endswith(CHOKE_POINT_SUFFIX)
def shell_commands(line: str) -> list[list[str]]:
candidate = line.rstrip().removesuffix("\\").rstrip()
try:
lexer = shlex.shlex(candidate, posix=True, punctuation_chars=";&|")
lexer.whitespace_split = True
lexer.commenters = ""
tokens = list(lexer)
except ValueError:
return []
commands: list[list[str]] = []
current: list[str] = []
for token in tokens:
if token and all(character in ";&|" for character in token):
if current:
commands.append(current)
current = []
else:
current.append(token)
if current:
commands.append(current)
return commands
def is_wrapper_invocation(path: Path, line: str) -> bool:
if path.suffix.lower() not in SHELL_SUFFIXES:
return False
separator = re.search(r"\s--(?:\s|$)", line)
if separator is None:
return False
# Everything after the wrapper separator is opaque runtime argv and may
# contain an open quote continued on later physical lines. Parse only the
# complete command-position prefix through the separator.
wrapper_prefix = line[: separator.end()]
for command in shell_commands(wrapper_prefix):
if command and command[0] == "exec":
command = command[1:]
if not command:
continue
if command[0] == "python3":
if len(command) < 2 or not command[1].endswith("launch-runtime.py"):
continue
arguments = command[2:]
elif command[0].endswith(("launch-runtime.py", "launch-runtime.sh")):
arguments = command[1:]
else:
continue
try:
runtime_index = arguments.index("--runtime")
separator_index = arguments.index("--")
except ValueError:
continue
if runtime_index + 1 < len(arguments) and runtime_index < separator_index:
return True
return False
def is_gated_line(path: Path, line: str) -> bool:
return is_wrapper_invocation(path, line) or any(
pattern.search(line) for pattern in GATED_PATTERNS
)
def shell_command_tokens(path: Path, line: str) -> list[str]:
if path.suffix.lower() not in SHELL_SUFFIXES:
return []
assignment = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*=")
case_arm = re.match(r"^\s*[^;&()]+\)\s*", line)
command_source = line[case_arm.end() :] if case_arm is not None else line
resolved: list[str] = []
for command in shell_commands(command_source):
index = 0
while index < len(command) and assignment.match(command[index]):
index += 1
# Resolve command position once for every literal and variable caller.
# Prefixes may be nested in either order (for example `exec env A=1`).
while index < len(command):
if command[index] in {"command", "exec", "nohup"}:
index += 1
continue
if command[index] == "env":
index += 1
while index < len(command) and (
command[index].startswith("-") or assignment.match(command[index])
):
index += 1
continue
break
if index < len(command):
resolved.append(command[index])
return resolved
def runtime_variable_reference(token: str, runtime_variables: set[str]) -> bool:
match = re.fullmatch(r"\$(?:([A-Za-z_][A-Za-z0-9_]*)|\{([A-Za-z_][A-Za-z0-9_]*)\})", token)
if match is None:
return False
return (match.group(1) or match.group(2)) in runtime_variables
def is_shell_direct_invocation(
path: Path, line: str, runtime_variables: set[str]
) -> bool:
return any(
Path(token).name in {"claude", "pi"}
or runtime_variable_reference(token, runtime_variables)
for token in shell_command_tokens(path, line)
)
def is_direct_line(path: Path, line: str, runtime_variables: set[str]) -> bool:
return is_shell_direct_invocation(path, line, runtime_variables) or any(
pattern.search(line) for pattern in DIRECT_PATTERNS
)
def executes_runtime_variable(
path: Path, line: str, runtime_variables: set[str]
) -> bool:
if any(
runtime_variable_reference(token, runtime_variables)
for token in shell_command_tokens(path, line)
):
return True
for variable in runtime_variables:
reference = rf"\$(?:{re.escape(variable)}|\{{{re.escape(variable)}\}})"
if re.search(rf"\beval\s+[\"']?{reference}", line):
return True
return False
def typescript_wrapper_lines(path: Path, source: str) -> set[int]:
if path.suffix.lower() not in {".js", ".mjs", ".ts", ".tsx"}:
return set()
return {
source.count("\n", 0, match.start()) + 1
for match in TYPESCRIPT_WRAPPER_INVOCATION.finditer(source)
}
def classify_text(path: Path, source: str) -> list[LaunchSite]:
sites: list[LaunchSite] = []
validated_typescript_wrappers = typescript_wrapper_lines(path, source)
runtime_variables: set[str] = set()
gated_continuation = False
in_block_comment = False
for line_number, physical_line in enumerate(source.splitlines(), start=1):
line, in_block_comment = strip_comments(path, physical_line, in_block_comment)
stripped = line.strip()
if not stripped:
gated_continuation = False
continue
assignment = RUNTIME_ASSIGNMENT.match(line)
if assignment is not None:
runtime_variables.add(assignment.group(1))
primitive_violation = DANGEROUS_PRIMITIVE in line and not is_choke_point(path)
line_is_direct = is_direct_line(path, line, runtime_variables) or executes_runtime_variable(
path, line, runtime_variables
)
line_is_gated = line_number in validated_typescript_wrappers or is_gated_line(path, line)
if primitive_violation:
sites.append(
LaunchSite(path, line_number, physical_line.rstrip(), "dangerous-primitive")
)
elif line_is_direct and not gated_continuation:
# Direct syntax always wins over a same-line marker. Only the command
# continuation of a previously validated wrapper may contain the raw
# runtime binary itself.
sites.append(LaunchSite(path, line_number, physical_line.rstrip(), "direct"))
elif line_is_gated:
sites.append(LaunchSite(path, line_number, physical_line.rstrip(), "gated"))
gated = line_is_gated or gated_continuation
gated_continuation = gated and line.rstrip().endswith("\\")
return sites
def scan_text(path: Path, source: str) -> list[LaunchSite]:
return [site for site in classify_text(path, source) if site.classification != "gated"]
def source_files(root: Path):
for relative_root in SCANNED_ROOTS:
search_root = root / relative_root
if not search_root.is_dir():
continue
for path in search_root.rglob("*"):
if not path.is_file() or path.suffix.lower() not in SCANNED_SUFFIXES:
continue
if any(part in SKIPPED_DIRECTORIES for part in path.parts):
continue
if is_test_path(path):
continue
yield path
def scan_repository(root: Path) -> list[LaunchSite]:
violations: list[LaunchSite] = []
for path in source_files(root):
try:
source = path.read_text(encoding="utf-8")
except UnicodeDecodeError:
violations.append(LaunchSite(path, 0, "non-UTF-8 source", "unscannable"))
continue
violations.extend(scan_text(path.relative_to(root), source))
return violations
def inventory_repository(root: Path) -> list[LaunchSite]:
inventory: list[LaunchSite] = []
for path in source_files(root):
try:
source = path.read_text(encoding="utf-8")
except UnicodeDecodeError:
inventory.append(LaunchSite(path.relative_to(root), 0, "non-UTF-8 source", "unscannable"))
continue
relative_path = path.relative_to(root)
inventory.extend(classify_text(relative_path, source))
return inventory
def format_violation(violation: LaunchSite) -> str:
return f"{violation.path}:{violation.line_number}: {violation.classification}: {violation.line.strip()}"
def main(argv: Sequence[str] | None = None) -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--root", type=Path, default=Path.cwd())
parser.add_argument("--json", action="store_true")
arguments = parser.parse_args(argv)
root = arguments.root.resolve()
inventory = inventory_repository(root)
violations = [site for site in inventory if site.classification != "gated"]
if arguments.json:
print(
json.dumps(
{
"gated": sum(site.classification == "gated" for site in inventory),
"total": len(inventory),
"sites": [
{
"path": str(site.path),
"line": site.line_number,
"classification": site.classification,
"source": site.line.strip(),
}
for site in inventory
],
},
sort_keys=True,
)
)
else:
for site in inventory:
print(format_violation(site))
print(
f"runtime launch inventory: "
f"{len(inventory) - len(violations)} gated/{len(inventory)} total"
)
if violations:
print("ungated consequential runtime launch detected", file=sys.stderr)
return 1
return 0
if __name__ == "__main__":
raise SystemExit(main())
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,222 @@
#!/usr/bin/env python3
"""Register a runtime parent with the lease broker, then exec without changing PID."""
from __future__ import annotations
import argparse
import json
import os
import socket
import sys
import time
from collections.abc import Callable, Mapping, Sequence
from datetime import datetime, timezone
from pathlib import Path
from typing import Final
from activation_version_gate import (
EXPECTED_ACTIVATION_CAPABILITY,
ActivationCapability,
VersionCouplingError,
assert_activation_capability_matches,
default_probe_activation_capability,
)
from lease_generation import initialize_runtime_generation
MAX_FRAME: Final = 64 * 1024
BROKER_TIMEOUT_SECONDS: Final = 1.5
CLAUDE_DANGEROUS_FLAG: Final = "--dangerously-skip-permissions"
# Distinct, non-overlapping exit code for the C4 version-coupling gate (see
# `activation_version_gate.py`) — deliberately different from the `1`
# (broker registration failed closed) and `64` (usage error) codes already
# owned by this script, so a version-skew denial is unambiguous in caller
# logs/tests and is never confused with a broker-availability failure.
EXIT_VERSION_SKEW: Final = 65
def broker_request(socket_path: Path, request: dict[str, object]) -> dict[str, object]:
payload = (json.dumps(request, separators=(",", ":")) + "\n").encode()
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 _self_starttime() -> str | None:
"""Field 22 of our own /proc stat — the anchor starttime the broker records.
Read past the comm field's parens, since a process name may contain them.
"""
try:
raw = Path(f"/proc/{os.getpid()}/stat").read_text()
return raw.rsplit(")", 1)[1].split()[19]
except (OSError, IndexError, ValueError):
return None
def _append_launch_record(environ: Mapping[str, str], record: dict[str, object]) -> None:
"""Append one NDJSON event to the #797 Runtime Session Ledger.
`fleet/run/sessions/` is operator-classified in framework-manifest.txt and is
already covered by test-upgrade-manifest-guard.sh, so an upgrade can neither
overwrite nor prune it. Files 0600 under a 0700 dir, matching what that guard
asserts.
Never raises: a launch must not be denied over bookkeeping. But it also never
fails silently — a missing record is exactly the kind of gap that made the
2026-08-06 MUTATOR_UNVERIFIED investigation cost a day.
"""
try:
mosaic_home = environ.get("MOSAIC_HOME") or str(Path.home() / ".config" / "mosaic")
directory = Path(mosaic_home) / "fleet" / "run" / "sessions"
directory.mkdir(parents=True, exist_ok=True)
os.chmod(directory, 0o700)
framed = {
"seq": time.time_ns() // 1_000_000,
"ts": datetime.now(timezone.utc).isoformat(),
**record,
}
path = directory / "events.ndjson"
descriptor = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_APPEND, 0o600)
with os.fdopen(descriptor, "w") as handle:
handle.write(json.dumps(framed, separators=(",", ":")) + "\n")
except (OSError, ValueError, TypeError) as error:
print(f"[mosaic] WARNING: launch record not written: {error}", file=sys.stderr)
def main(
argv: Sequence[str] | None = None,
*,
environ: Mapping[str, str] | None = None,
request: Callable[[Path, dict[str, object]], dict[str, object]] = broker_request,
execute: Callable[[str, list[str], dict[str, str]], object] = os.execvpe,
initialize_generation: Callable[[Path, int], None] = initialize_runtime_generation,
probe_activation_capability: Callable[
[Mapping[str, str]], ActivationCapability | None
] = default_probe_activation_capability,
expected_activation_capability: ActivationCapability = EXPECTED_ACTIVATION_CAPABILITY,
) -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--runtime", required=True, choices=("claude", "pi"))
parser.add_argument("--dangerous", action="store_true")
parser.add_argument("command", nargs=argparse.REMAINDER)
arguments = parser.parse_args(argv)
command = arguments.command
if command and command[0] == "--":
command = command[1:]
if not command:
print("lease-gated runtime command is required", file=sys.stderr)
return 64
if arguments.dangerous:
if arguments.runtime != "claude" or Path(command[0]).name != "claude":
print("dangerous mode is supported only for the Claude runtime", file=sys.stderr)
return 64
command = [command[0], CLAUDE_DANGEROUS_FLAG, *command[1:]]
source_environment = os.environ if environ is None else environ
# C4 version-coupling gate (#869 Point-1): before this ENFORCEMENT half
# chains into anything, assert that the ACTIVATION contract it is about
# to rely on (MOSAIC_LEASE_* injection, broker chaining) matches what
# this enforcement build expects. This is a build/deploy-defect check,
# not a broker-availability question, so it runs before — and
# independently of — broker registration below, and it FAILS LOUD: a
# clear stderr message plus a dedicated non-zero exit code, never a
# silent pass and never folded into the generic registration-failure
# branch.
try:
activation_capability = probe_activation_capability(source_environment)
assert_activation_capability_matches(
activation_capability,
expected_activation_capability,
)
except VersionCouplingError as version_error:
print(str(version_error), file=sys.stderr)
return EXIT_VERSION_SKEW
try:
socket_path = Path(source_environment["MOSAIC_LEASE_BROKER_SOCKET"])
generation = int(source_environment.get("MOSAIC_RUNTIME_GENERATION", "1"))
if generation < 0:
raise ValueError("invalid generation")
reply = request(
socket_path,
{
"action": "register_anchor",
"runtime_generation": generation,
},
)
session_id = reply.get("session_id")
if (
reply.get("ok") is not True
or not isinstance(session_id, str)
or len(session_id) != 64
or any(character not in "0123456789abcdef" for character in session_id)
):
raise ValueError("registration refused")
generation_file = socket_path.parent / f"generation-{session_id}.state"
initialize_generation(generation_file, generation)
except (KeyError, ValueError, OSError, json.JSONDecodeError):
print("Mosaic lease broker registration failed; runtime launch denied.", file=sys.stderr)
return 1
# Immutable launch record, half two. `mosaic` wrote `session.launch` with the
# config/provenance it knows; only this process knows the broker session id
# and the activation capability it just asserted. os.execvpe preserves the
# PID, so this PID is BOTH the anchor pid and the join key back to that
# record. Never fatal — bookkeeping must not deny a launch — but never
# silent either.
_append_launch_record(
source_environment,
{
"kind": "lease.register",
# Joins back to `mosaic`'s session.launch record. NOT pid: execRuntime()
# spawns rather than execs, so this process is a CHILD of mosaic with a
# different pid. This pid IS the broker anchor pid (os.execvpe below
# preserves it), which is a separate and still-useful fact.
"launch_id": source_environment.get("MOSAIC_LAUNCH_ID"),
"pid": os.getpid(),
"runtime": arguments.runtime,
"session_id": session_id,
"runtime_generation": generation,
"generation_file": str(generation_file),
"anchor_starttime": _self_starttime(),
"activation_capability": activation_capability,
"command": Path(command[0]).name,
},
)
environment = dict(source_environment)
environment["MOSAIC_LEASE_SESSION_ID"] = session_id
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:
print("Mosaic lease-gated runtime exec failed.", file=sys.stderr)
return 1
return 0
if __name__ == "__main__":
raise SystemExit(main())
@@ -0,0 +1,105 @@
#!/usr/bin/env python3
"""Private monotonic runtime-generation state shared by runtime hook processes."""
from __future__ import annotations
import fcntl
import os
import stat
from collections.abc import Mapping
from pathlib import Path
from typing import Final
MAX_GENERATION: Final = (1 << 63) - 1
MAX_GENERATION_BYTES: Final = 32
def parse_generation(value: object) -> int:
if not isinstance(value, str) or not value.isascii() or not value.isdigit():
raise ValueError("invalid runtime generation")
generation = int(value)
if generation < 0 or generation > MAX_GENERATION:
raise ValueError("invalid runtime generation")
return generation
def _validate_descriptor(descriptor: int) -> None:
metadata = os.fstat(descriptor)
if not stat.S_ISREG(metadata.st_mode):
raise ValueError("runtime generation state is not a regular file")
if metadata.st_uid != os.geteuid():
raise ValueError("runtime generation state has the wrong owner")
if stat.S_IMODE(metadata.st_mode) & 0o077:
raise ValueError("runtime generation state permissions are not private")
if metadata.st_size > MAX_GENERATION_BYTES:
raise ValueError("runtime generation state is oversized")
def _read_descriptor(descriptor: int) -> int:
os.lseek(descriptor, 0, os.SEEK_SET)
raw = os.read(descriptor, MAX_GENERATION_BYTES + 1)
if len(raw) > MAX_GENERATION_BYTES:
raise ValueError("runtime generation state is oversized")
try:
return parse_generation(raw.decode("ascii").strip())
except UnicodeDecodeError as exc:
raise ValueError("invalid runtime generation state") from exc
def _write_descriptor(descriptor: int, generation: int) -> None:
payload = f"{generation}\n".encode("ascii")
os.lseek(descriptor, 0, os.SEEK_SET)
os.ftruncate(descriptor, 0)
remaining = memoryview(payload)
while remaining:
written = os.write(descriptor, remaining)
if written <= 0:
raise OSError("runtime generation write made no progress")
remaining = remaining[written:]
os.fsync(descriptor)
def initialize_runtime_generation(path: Path, generation: int) -> None:
if generation < 0 or generation > MAX_GENERATION:
raise ValueError("invalid runtime generation")
descriptor = os.open(
path,
os.O_RDWR | os.O_CREAT | os.O_TRUNC | os.O_CLOEXEC | os.O_NOFOLLOW,
0o600,
)
try:
os.fchmod(descriptor, 0o600)
_validate_descriptor(descriptor)
_write_descriptor(descriptor, generation)
finally:
os.close(descriptor)
def read_runtime_generation(environ: Mapping[str, str]) -> int:
state_path = environ.get("MOSAIC_LEASE_GENERATION_FILE")
if not state_path:
return parse_generation(environ["MOSAIC_RUNTIME_GENERATION"])
descriptor = os.open(state_path, os.O_RDONLY | os.O_CLOEXEC | os.O_NOFOLLOW)
try:
_validate_descriptor(descriptor)
return _read_descriptor(descriptor)
finally:
os.close(descriptor)
def bump_runtime_generation(environ: Mapping[str, str]) -> int:
state_path = environ.get("MOSAIC_LEASE_GENERATION_FILE")
if not state_path:
raise ValueError("runtime generation file is required for same-PID rollover")
descriptor = os.open(state_path, os.O_RDWR | os.O_CLOEXEC | os.O_NOFOLLOW)
try:
fcntl.flock(descriptor, fcntl.LOCK_EX)
_validate_descriptor(descriptor)
current = _read_descriptor(descriptor)
if current >= MAX_GENERATION:
raise ValueError("runtime generation exhausted")
generation = current + 1
_write_descriptor(descriptor, generation)
return generation
finally:
os.close(descriptor)
@@ -0,0 +1,337 @@
#!/usr/bin/env python3
"""Lease promotion client — the half the enforcement toolkit never shipped.
The enforcement half (``daemon.py`` + ``mutator-gate.py``) ships and denies. The
promotion half has no production caller anywhere in the package: as of 0.0.48,
0.0.49 and 0.0.50-next.2207, ``begin_verification`` / ``observe_receipt`` /
``promote_lease`` are invoked only by ``broker-test-client.ts``, the acceptance
spec, unit tests, and two probes under ``docs/``. Consequence: **no lease on any
host can reach VERIFIED**, so every mutator is denied ``MUTATOR_UNVERIFIED`` by a
gate nothing can satisfy.
THE PROTOCOL (``daemon.py:578-754``)
------------------------------------
1. ``begin_verification`` — broker revokes, mints a challenge, and returns the
exact ``receipt`` text the MODEL must emit
2. *the model emits that text verbatim as its ENTIRE latest message*
3. the runtime adapter ships that message to the daemon-owned observer socket
4. ``observe_receipt`` -> ``PENDING_PROMOTION``
5. ``promote_lease`` -> ``VERIFIED``
THIS MODULE IMPLEMENTS 1, 4 AND 5 — NEVER 2
-------------------------------------------
Step 2 is the security property, not a formality. ``is_verbatim_receipt`` uses
``hmac.compare_digest`` against the exact minted string — explicitly "not a
transcript substring" (``receipt_challenge.py``). Promotion therefore requires a
live model that received the challenge in its context and echoed it exactly.
``receipt-observer-client.py`` will post ANY string as the latest assistant
message. A promotion client that posted its own receipt would satisfy the broker
while proving nothing — a gate-disabler indistinguishable from a working fix
unless someone looks for it. **This module never posts a receipt.** Emitting it
belongs to the runtime adapter, where a real model turn happens.
The construction binds the exact normative source bytes. ``h_source`` /
``h_payload`` are derived by the framework's own
``normative_fragments.build_payload`` rather than reimplemented: the broker
derives them the same way and any divergence yields ``PAYLOAD_BINDING_MISMATCH``.
There must be exactly one implementation.
WHAT THE BINDING DOES *NOT* PROVE
---------------------------------
It is tempting to read a VERIFIED lease as "this agent is running THIS law".
**It does not mean that**, and writing it down that way is how the belief spread.
The broker holds no reference copy of any normative source and never opens one;
it recomputes ``h_source`` / ``h_payload`` from the fragment bytes THIS CLIENT
sent and compares them to the binding THIS CLIENT sent (``daemon.py:602-616``).
Both sides of that comparison originate here, so it detects corruption in
transit and nothing else. What the binding actually asserts is "the client
claims these bytes, self-consistently".
Making it mean the stronger thing requires the broker to re-read the on-disk
sources itself, against a manifest the agent cannot rewrite — i.e. broker code
attestation under its own uid. Until then, do not cite a VERIFIED lease as
evidence of law integrity.
Usage
-----
lease_promote.py --begin # prints the receipt the MODEL must emit
lease_promote.py --complete <challenge> # after the adapter observed it
"""
from __future__ import annotations
import argparse
import base64
import hashlib
import json
import os
import socket
import sys
from pathlib import Path
from typing import 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 normative_fragments import NormativeFragment, build_payload # noqa: E402
MAX_FRAME: Final = 64 * 1024
BROKER_TIMEOUT_SECONDS: Final = 3.0
SCHEMA_VERSION: Final = 1
MANIFEST_VERSION: Final = 1
GENERATOR_VERSION: Final = "mosaic/lease_promote@1"
DEFAULT_TTL_SECONDS: Final = 3600
# Normative sources whose exact bytes bind the lease, in binding order. Order is
# load-bearing: ``h_source`` frames the resolved sequence, so reordering changes
# the derivation. Never fabricate a source that is not on disk.
FRAGMENT_SOURCES: Final = (
"CONSTITUTION.md",
"AGENTS.md",
"SOUL.md",
"USER.md",
"STANDARDS.md",
"TOOLS.md",
)
# Framework-owned sources, reconciled on every upgrade — `install.sh:76`
# FRAMEWORK_OWNED and `config/file-adapter.ts` FRAMEWORK_OWNED_FILES — plus the
# per-runtime contract shipped under `framework/runtime/<runtime>/`. A deployment
# missing one of these is broken, not minimal, so their absence is refused rather
# than silently dropped from the binding.
#
# SOUL.md and USER.md are deliberately excluded: install.sh does not seed them
# ("intentionally NOT seeded here — they are generated by `mosaic init`"), so a
# fresh install legitimately lacks both. TOOLS.md is user-seeded on first install
# only. Absence of those three is reported, not fatal.
REQUIRED_SOURCES: Final = frozenset({"CONSTITUTION.md", "AGENTS.md", "STANDARDS.md"})
class IncompleteBinding(RuntimeError):
"""A source that must bind this lease could not be read.
**Never downgrade this to a skip.** The broker recomputes the hashes from the
fragments it is sent, so an omitted fragment is internally consistent and
``PAYLOAD_BINDING_MISMATCH`` cannot fire — a partial law promotes exactly like
a complete one, and nothing downstream can tell the difference. Dropping an
unreadable source therefore does not degrade the binding, it forges a smaller
one. Fail here, where the omission is still visible.
"""
def mosaic_home() -> Path:
return Path(os.environ.get("MOSAIC_HOME") or Path.home() / ".config" / "mosaic")
def broker_socket() -> Path:
value = os.environ.get("MOSAIC_LEASE_BROKER_SOCKET")
if value:
return Path(value)
runtime_dir = os.environ.get("XDG_RUNTIME_DIR")
if runtime_dir:
return Path(runtime_dir) / "mosaic-lease" / "broker.sock"
return Path(f"/run/user/{os.getuid()}/mosaic-lease/broker.sock")
def session_identity() -> tuple[str, int, str]:
"""Session id, CURRENT generation, runtime.
The generation file wins over the env var, matching ``lease_generation.py``.
Sending a generation HIGHER than the broker's would revoke this session's own
authority (``daemon.py:342-344``), so this never guesses.
"""
session_id = os.environ["MOSAIC_LEASE_SESSION_ID"]
runtime = os.environ["MOSAIC_LEASE_RUNTIME"]
state_file = os.environ.get("MOSAIC_LEASE_GENERATION_FILE")
if state_file:
try:
return session_id, int(Path(state_file).read_text().strip()), runtime
except (OSError, ValueError):
pass
return session_id, int(os.environ["MOSAIC_RUNTIME_GENERATION"]), runtime
def build_construction(runtime: str) -> tuple[dict[str, object], object]:
"""Assemble the wire construction and derive its hashes with the sole builder."""
runtime_contract = f"runtime/{runtime}/RUNTIME.md"
sources = list(FRAGMENT_SOURCES) + [runtime_contract]
required = REQUIRED_SOURCES | {runtime_contract}
wire_fragments: list[dict[str, str]] = []
objects: list[NormativeFragment] = []
absent: list[str] = []
for source_id in sources:
try:
content = (mosaic_home() / source_id).read_bytes()
except FileNotFoundError:
# Genuinely not on disk. Legitimate only for operator-owned sources.
if source_id in required:
raise IncompleteBinding(
f"required normative source is absent: {source_id}"
) from None
absent.append(source_id)
continue
except OSError as exc:
# The path resolves but will not read — EACCES, EIO, EISDIR, ELOOP.
# That is an anomaly for EVERY source, optional ones included: an
# unreadable file is not an un-configured one, and treating it as
# absent is what lets a permission change quietly shrink the law.
raise IncompleteBinding(
f"normative source is present but unreadable: {source_id} "
f"({type(exc).__name__})"
) from exc
digest = hashlib.sha256(content).hexdigest()
wire_fragments.append(
{
"source_id": source_id,
"content_base64": base64.b64encode(content).decode("ascii"),
"expected_sha256": digest,
}
)
objects.append(NormativeFragment(source_id, content, digest))
if not wire_fragments:
raise IncompleteBinding("no normative sources found — refusing an empty binding")
# Absence is legitimate here but never invisible. The omission is already
# baked into h_source (the framed source sequence differs), but nothing
# compares h_source to an expected value, so this line is the only place a
# human learns the binding was narrower than the full set.
if absent:
print(
f"lease_promote: binding omits absent operator sources: {', '.join(absent)}",
file=sys.stderr,
)
result = build_payload(
manifest_version=MANIFEST_VERSION,
generator_version=GENERATOR_VERSION,
fragments=objects,
)
if result.injectionDecision != "ACCEPTED" or not result.promotion:
raise RuntimeError(f"construction refused locally: {result.source_reason}")
return (
{
"manifest_version": MANIFEST_VERSION,
"generator_version": GENERATOR_VERSION,
"fragments": wire_fragments,
},
result,
)
def broker_request(payload: dict[str, object]) -> dict[str, object]:
raw = (json.dumps(payload, separators=(",", ":")) + "\n").encode()
if len(raw) > MAX_FRAME:
raise ValueError(
f"request too large ({len(raw)} bytes); broker frame cap is {MAX_FRAME}"
)
response = bytearray()
with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as connection:
connection.settimeout(BROKER_TIMEOUT_SECONDS)
connection.connect(str(broker_socket()))
connection.sendall(raw)
connection.shutdown(socket.SHUT_WR)
while len(response) <= MAX_FRAME:
chunk = connection.recv(4096)
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 begin(
ttl_seconds: int = DEFAULT_TTL_SECONDS,
compaction_epoch: int = 0,
request_epoch: int = 0,
) -> dict[str, object]:
"""Step 1. Returns the broker reply, including the exact ``receipt`` text."""
session_id, generation, runtime = session_identity()
construction, derived = build_construction(runtime)
return broker_request(
{
"action": "begin_verification",
"session_id": session_id,
"runtime_generation": generation,
"runtime": runtime,
"ttl_seconds": ttl_seconds,
"binding": {
"compaction_epoch": compaction_epoch,
"request_epoch": request_epoch,
"h_source": derived.h_source,
"h_payload": derived.h_payload,
"schema_version": SCHEMA_VERSION,
},
"construction": construction,
}
)
def complete(challenge: str) -> dict[str, object]:
"""Steps 4-5. Assumes the model already emitted the receipt and the adapter
shipped it to the observer socket."""
session_id, generation, _ = session_identity()
observed = broker_request(
{
"action": "observe_receipt",
"session_id": session_id,
"runtime_generation": generation,
"receipt_challenge": challenge,
}
)
if observed.get("ok") is not True or observed.get("state") != "PENDING_PROMOTION":
return {"stage": "observe_receipt", **observed}
promoted = broker_request(
{
"action": "promote_lease",
"session_id": session_id,
"runtime_generation": generation,
"receipt_challenge": challenge,
}
)
return {"stage": "promote_lease", **promoted}
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(description="Mosaic lease promotion client.")
group = parser.add_mutually_exclusive_group(required=True)
group.add_argument(
"--begin",
action="store_true",
help="mint a challenge; prints the receipt the MODEL must emit verbatim",
)
group.add_argument(
"--complete",
metavar="CHALLENGE",
help="observe the emitted receipt and promote the lease",
)
parser.add_argument("--ttl-seconds", type=int, default=DEFAULT_TTL_SECONDS)
arguments = parser.parse_args(argv)
try:
if arguments.begin:
print(json.dumps(begin(ttl_seconds=arguments.ttl_seconds), indent=2))
else:
print(json.dumps(complete(arguments.complete), indent=2))
except KeyError as exc:
print(f"missing lease environment: {exc}; not a lease-gated session", file=sys.stderr)
return 2
except (OSError, ValueError, RuntimeError, json.JSONDecodeError) as exc:
print(f"{type(exc).__name__}: {exc}", file=sys.stderr)
return 2
return 0
if __name__ == "__main__":
raise SystemExit(main())
@@ -0,0 +1,166 @@
#!/usr/bin/env python3
"""Runtime-neutral whole mutator-class gate backed by the Mosaic lease broker."""
from __future__ import annotations
import argparse
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:
print(f"BLOCKED: Mosaic mutator gate denied this tool ({code}).", file=sys.stderr)
return 2
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:
raise ValueError("INVALID_GATE_INPUT")
value = json.loads(raw)
if not isinstance(value, dict):
raise ValueError("INVALID_GATE_INPUT")
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 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]:
payload = (json.dumps(request, separators=(",", ":")) + "\n").encode()
if len(payload) > MAX_FRAME:
raise ValueError("INVALID_GATE_INPUT")
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 main(
argv: Sequence[str] | None = None,
*,
environ: Mapping[str, str] | None = None,
stream: BinaryIO | None = None,
request: Callable[[Path, dict[str, object]], dict[str, object]] = broker_request,
resolve_generation: Callable[[Mapping[str, str]], int] = read_runtime_generation,
) -> 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:
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)
reply = request(
Path(socket_value),
{
"action": "authorize_tool",
"session_id": session_id,
"runtime_generation": generation,
"runtime": arguments.runtime,
"tool_name": tool_name,
},
)
except (KeyError, ValueError, OSError, json.JSONDecodeError):
return deny("GATE_UNAVAILABLE")
if reply.get("ok") is True and reply.get("decision") == "allow":
return 0
code = reply.get("code")
return deny(code if isinstance(code, str) else "MUTATOR_UNVERIFIED")
if __name__ == "__main__":
raise SystemExit(main())
@@ -0,0 +1,215 @@
#!/usr/bin/env python3
"""Fail-closed construction of verbatim-hashed normative fragments.
This is the single construction path for the Claude and Pi adapters. The
payload contains only versioned source metadata and exact validated source
bytes. ``h_payload`` is derived afterwards and is deliberately not representable
as a payload input, preventing cryptographic self-reference.
"""
from __future__ import annotations
import base64
import hashlib
import hmac
import struct
from collections.abc import Sequence
from typing import Final
MAX_FRAGMENT_BYTES: Final = 64 * 1024
HASH_DOMAIN_SEPARATOR: Final = b"MOSAIC/H_PAYLOAD/v1\x00"
SOURCE_DOMAIN_SEPARATOR: Final = b"MOSAIC/H_SOURCE/v1\x00"
_PAYLOAD_VERSION_LABEL: Final = b"MOSAIC/B_PAYLOAD/v1"
class NormativeFragment:
"""A source identity, its expected digest, and its exact resolved bytes."""
def __init__(self, source_id: str, content: bytes | None, expected_sha256: str) -> None:
self.source_id = source_id
self.content = content
self.expected_sha256 = expected_sha256
class ConstructionResult:
"""A source-admission decision and, only when admitted, derived payload values.
``promotion`` means the construction has produced the only values a later
receipt protocol may use to attempt promotion. It never performs broker
promotion itself. A REFUSED result has no payload/hash values, so it cannot
advance to that later protocol.
"""
def __init__(
self,
injection_decision: str,
promotion: bool,
source_reason: str | None,
b_payload: bytes | None,
h_payload: str | None,
h_source: str | None,
) -> None:
self.injectionDecision = injection_decision
self.promotion = promotion
self.source_reason = source_reason
self.b_payload = b_payload
self.h_payload = h_payload
self.h_source = h_source
def length_frame(parts: Sequence[bytes]) -> bytes:
"""Encode a finite ordered byte sequence with unambiguous 64-bit framing."""
framed = bytearray(struct.pack(">Q", len(parts)))
for part in parts:
if not isinstance(part, bytes):
raise TypeError("length framing requires bytes")
framed.extend(struct.pack(">Q", len(part)))
framed.extend(part)
return bytes(framed)
def _sha256_hex(payload: bytes) -> str:
return hashlib.sha256(payload).hexdigest()
def _valid_digest(value: object) -> bool:
return (
isinstance(value, str)
and len(value) == 64
and all(character in "0123456789abcdef" for character in value)
)
def _source_reason(fragment: NormativeFragment) -> str | None:
if not isinstance(fragment.source_id, str) or not fragment.source_id:
return "missing"
if fragment.content is None:
return "missing"
if not isinstance(fragment.content, bytes):
return "missing"
if len(fragment.content) > MAX_FRAGMENT_BYTES:
return "oversize"
if not _valid_digest(fragment.expected_sha256):
return "hash-mismatch"
actual = _sha256_hex(fragment.content)
if not hmac.compare_digest(actual, fragment.expected_sha256):
return "hash-mismatch"
return None
def _refused(reason: str) -> ConstructionResult:
return ConstructionResult(
injection_decision="REFUSED",
promotion=False,
source_reason=reason,
b_payload=None,
h_payload=None,
h_source=None,
)
def build_payload(
*,
manifest_version: int,
generator_version: str,
fragments: Sequence[NormativeFragment],
) -> ConstructionResult:
"""Construct B_payload then H_payload after fail-closed source validation.
The sequence order is caller-supplied resolved-source order and is encoded
directly. Changing source order, source identity, metadata, or any exact
fragment byte therefore changes the framed B_payload and its derived hash.
"""
if type(manifest_version) is not int or manifest_version < 0:
return _refused("missing")
if not isinstance(generator_version, str) or not generator_version:
return _refused("missing")
validated = list(fragments)
if not validated:
return _refused("missing")
for fragment in validated:
if not isinstance(fragment, NormativeFragment):
return _refused("missing")
reason = _source_reason(fragment)
if reason is not None:
return _refused(reason)
source_identities = [
length_frame([
fragment.source_id.encode("utf-8"),
fragment.expected_sha256.encode("ascii"),
])
for fragment in validated
]
h_source = _sha256_hex(SOURCE_DOMAIN_SEPARATOR + length_frame(source_identities))
payload_parts = [
_PAYLOAD_VERSION_LABEL,
str(manifest_version).encode("ascii"),
generator_version.encode("utf-8"),
*source_identities,
*(fragment.content for fragment in validated),
]
# h_payload is intentionally not an argument or field in payload_parts.
b_payload = length_frame(payload_parts)
h_payload = _sha256_hex(HASH_DOMAIN_SEPARATOR + length_frame([b_payload]))
return ConstructionResult(
injection_decision="ACCEPTED",
promotion=True,
source_reason=None,
b_payload=b_payload,
h_payload=h_payload,
h_source=h_source,
)
def build_payload_from_wire(value: object) -> ConstructionResult:
"""Decode a bounded broker request then invoke the sole payload builder.
Wire inputs contain only source bytes and their claimed source digests. The
authoritative ``build_payload`` implementation remains the only code that
admits those bytes and derives ``h_source``/``h_payload``.
"""
if not isinstance(value, dict) or set(value) != {
"manifest_version", "generator_version", "fragments"
}:
raise ValueError("invalid construction")
raw_fragments = value["fragments"]
if not isinstance(raw_fragments, list):
raise ValueError("invalid construction")
fragments: list[NormativeFragment] = []
for raw_fragment in raw_fragments:
if not isinstance(raw_fragment, dict) or set(raw_fragment) != {
"source_id", "content_base64", "expected_sha256"
}:
raise ValueError("invalid construction")
source_id = raw_fragment["source_id"]
encoded = raw_fragment["content_base64"]
expected_sha256 = raw_fragment["expected_sha256"]
if not isinstance(source_id, str) or not isinstance(encoded, str) or not isinstance(expected_sha256, str):
raise ValueError("invalid construction")
try:
content = base64.b64decode(encoded.encode("ascii"), validate=True)
except (UnicodeEncodeError, ValueError) as exc:
raise ValueError("invalid construction") from exc
fragments.append(NormativeFragment(source_id, content, expected_sha256))
return build_payload(
manifest_version=value["manifest_version"],
generator_version=value["generator_version"],
fragments=fragments,
)
def build_for_claude(**kwargs: object) -> ConstructionResult:
"""Claude adapter entrypoint; delegates to the sole shared constructor."""
return build_payload(**kwargs) # type: ignore[arg-type]
def build_for_pi(**kwargs: object) -> ConstructionResult:
"""Pi adapter entrypoint; delegates to the sole shared constructor."""
return build_payload(**kwargs) # type: ignore[arg-type]
@@ -0,0 +1,399 @@
#!/usr/bin/env python3
"""Claude UserPromptSubmit hook for operator-triggered lease promotion."""
from __future__ import annotations
import fcntl
import importlib.util
import json
import os
import secrets
import stat
import subprocess
import sys
import time
from collections.abc import Callable, Mapping
from pathlib import Path
from typing import Final, TextIO
_MODULE_DIRECTORY = str(Path(__file__).resolve().parent)
if _MODULE_DIRECTORY not in sys.path:
sys.path.insert(0, _MODULE_DIRECTORY)
from receipt_challenge import receipt_for # noqa: E402
_observer_spec = importlib.util.spec_from_file_location(
"mosaic_receipt_observer_client", Path(__file__).resolve().with_name("receipt-observer-client.py")
)
if _observer_spec is None or _observer_spec.loader is None:
raise RuntimeError("unable to load receipt observer client")
_observer_module = importlib.util.module_from_spec(_observer_spec)
_observer_spec.loader.exec_module(_observer_module)
observer_request = _observer_module.observer_request
MAX_FRAME: Final = 64 * 1024
PENDING_MAX_AGE_SECONDS: Final = 60 * 60
PROMOTER_TIMEOUT_SECONDS: Final = 10.0
PROMOTION_PROMPT: Final = "/mosaic-promote"
PROMOTER: Final = Path(__file__).resolve().with_name("lease_promote.py")
PENDING_DIRECTORY: Final = "mosaic-lease"
AUTHORIZATION_DIRECTORY: Final = "authorizations"
AUTHORIZATION_TTL_SECONDS: Final = 60
LEASE_TTL_SECONDS: Final = 60 * 60
LOCK_FILE: Final = "promotion.lock"
RESULT_FILE: Final = "last-result.json"
EXPECTED_BEGIN_KEYS: Final = frozenset(
{"ok", "state", "receipt_challenge", "receipt", "binding"}
)
EXPECTED_BINDING_KEYS: Final = frozenset(
{
"compaction_epoch",
"request_epoch",
"h_source",
"h_payload",
"runtime_generation",
"schema_version",
}
)
class PromotionAlreadyInProgress(RuntimeError):
pass
def reject_duplicate_json_keys(pairs: list[tuple[str, object]]) -> dict[str, object]:
value: dict[str, object] = {}
for key, item in pairs:
if key in value:
raise ValueError("duplicate promoter JSON key")
value[key] = item
return value
def read_hook_input(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 UserPromptSubmit input")
value = json.loads(raw, object_pairs_hook=reject_duplicate_json_keys)
if not isinstance(value, dict):
raise ValueError("invalid UserPromptSubmit input")
return value
def emit_context(stream: TextIO, message: str) -> None:
json.dump(
{
"hookSpecificOutput": {
"hookEventName": "UserPromptSubmit",
"additionalContext": message,
}
},
stream,
separators=(",", ":"),
)
stream.write("\n")
def session_pending_name(environ: Mapping[str, str]) -> tuple[Path, str]:
runtime_dir = Path(environ["XDG_RUNTIME_DIR"])
session_id = environ["MOSAIC_LEASE_SESSION_ID"]
if not runtime_dir.is_absolute():
raise ValueError("XDG_RUNTIME_DIR must be absolute")
if len(session_id) != 64 or any(character not in "0123456789abcdef" for character in session_id):
raise ValueError("invalid lease session id")
return runtime_dir, f"pending-{session_id}"
def open_pending_directory(runtime_dir: Path) -> int:
directory_flags = (
os.O_RDONLY
| getattr(os, "O_CLOEXEC", 0)
| getattr(os, "O_DIRECTORY", 0)
| getattr(os, "O_NOFOLLOW", 0)
)
runtime_descriptor = os.open(runtime_dir, directory_flags)
try:
runtime_metadata = os.fstat(runtime_descriptor)
if (
not stat.S_ISDIR(runtime_metadata.st_mode)
or runtime_metadata.st_uid != os.getuid()
or stat.S_IMODE(runtime_metadata.st_mode) != 0o700
):
raise ValueError("unsafe XDG runtime directory")
try:
os.mkdir(PENDING_DIRECTORY, mode=0o700, dir_fd=runtime_descriptor)
except FileExistsError:
pass
descriptor = os.open(PENDING_DIRECTORY, directory_flags, dir_fd=runtime_descriptor)
finally:
os.close(runtime_descriptor)
metadata = os.fstat(descriptor)
if (
not stat.S_ISDIR(metadata.st_mode)
or metadata.st_uid != os.getuid()
or stat.S_IMODE(metadata.st_mode) != 0o700
):
os.close(descriptor)
raise ValueError("unsafe promotion pending directory")
return descriptor
def acquire_lock(directory_descriptor: int) -> int:
flags = (
os.O_RDWR
| os.O_CREAT
| getattr(os, "O_CLOEXEC", 0)
| getattr(os, "O_NOFOLLOW", 0)
)
descriptor = os.open(LOCK_FILE, flags, 0o600, dir_fd=directory_descriptor)
metadata = os.fstat(descriptor)
if (
not stat.S_ISREG(metadata.st_mode)
or metadata.st_uid != os.getuid()
or stat.S_IMODE(metadata.st_mode) != 0o600
):
os.close(descriptor)
raise ValueError("unsafe promotion lock file")
try:
fcntl.flock(descriptor, fcntl.LOCK_EX | fcntl.LOCK_NB)
except BlockingIOError as error:
os.close(descriptor)
raise PromotionAlreadyInProgress() from error
return descriptor
def sweep_stale_pending(directory_descriptor: int, current_time: float) -> None:
cutoff = current_time - PENDING_MAX_AGE_SECONDS
removed = False
with os.scandir(directory_descriptor) as entries:
for candidate in entries:
if not (
candidate.name.startswith("pending-")
or candidate.name.startswith(".pending-")
):
continue
try:
metadata = candidate.stat(follow_symlinks=False)
if metadata.st_mtime < cutoff and not stat.S_ISDIR(metadata.st_mode):
os.unlink(candidate.name, dir_fd=directory_descriptor)
removed = True
except FileNotFoundError:
continue
if removed:
os.fsync(directory_descriptor)
def consume_authorization(directory_descriptor: int, session_id: str, wall_clock: float) -> str | None:
flags = os.O_RDONLY | getattr(os, "O_CLOEXEC", 0) | getattr(os, "O_DIRECTORY", 0) | getattr(os, "O_NOFOLLOW", 0)
try:
authorization_descriptor = os.open(AUTHORIZATION_DIRECTORY, flags, dir_fd=directory_descriptor)
except FileNotFoundError:
return None
try:
metadata = os.fstat(authorization_descriptor)
if not stat.S_ISDIR(metadata.st_mode) or metadata.st_uid != os.getuid() or stat.S_IMODE(metadata.st_mode) != 0o700:
raise ValueError("unsafe promotion authorization directory")
name = f"{session_id}.auth"
try:
descriptor = os.open(name, os.O_RDONLY | getattr(os, "O_CLOEXEC", 0) | getattr(os, "O_NOFOLLOW", 0), dir_fd=authorization_descriptor)
except FileNotFoundError:
return None
try:
token_metadata = os.fstat(descriptor)
if not stat.S_ISREG(token_metadata.st_mode) or token_metadata.st_uid != os.getuid() or stat.S_IMODE(token_metadata.st_mode) != 0o600 or token_metadata.st_size <= 0 or token_metadata.st_size > MAX_FRAME:
raise ValueError("unsafe promotion authorization")
raw = os.read(descriptor, MAX_FRAME + 1)
finally:
os.close(descriptor)
os.unlink(name, dir_fd=authorization_descriptor)
os.fsync(authorization_descriptor)
token = json.loads(raw, object_pairs_hook=reject_duplicate_json_keys)
if not isinstance(token, dict) or set(token) != {"nonce", "seat", "session_id", "expires_at", "ts"}:
return None
nonce = token.get("nonce")
expires_at = token.get("expires_at")
issued_at = token.get("ts")
if token.get("session_id") != session_id or not isinstance(token.get("seat"), str) or not isinstance(nonce, str) or len(nonce) != 64 or any(char not in "0123456789abcdef" for char in nonce) or type(expires_at) not in (int, float) or type(issued_at) not in (int, float) or expires_at <= wall_clock or expires_at > issued_at + AUTHORIZATION_TTL_SECONDS:
return None
return nonce
finally:
os.close(authorization_descriptor)
def write_result(directory_descriptor: int, attempt_id: str, verified: bool, reason: str | None, session_id: str, wall_clock: float) -> None:
temporary = f".{RESULT_FILE}.tmp-{secrets.token_hex(8)}"
descriptor = os.open(temporary, os.O_WRONLY | os.O_CREAT | os.O_EXCL | getattr(os, "O_CLOEXEC", 0) | getattr(os, "O_NOFOLLOW", 0), 0o600, dir_fd=directory_descriptor)
try:
os.fchmod(descriptor, 0o600)
with os.fdopen(descriptor, "w", encoding="utf-8", closefd=False) as stream:
json.dump({"attempt_id": attempt_id, "expires_at_wallclock": wall_clock + LEASE_TTL_SECONDS if verified else None, "reason": reason, "session_id": session_id, "ts": wall_clock, "verified": verified}, stream, separators=(",", ":"), sort_keys=True)
stream.flush(); os.fsync(stream.fileno())
os.replace(temporary, RESULT_FILE, src_dir_fd=directory_descriptor, dst_dir_fd=directory_descriptor)
os.fsync(directory_descriptor)
finally:
os.close(descriptor)
def write_pending(directory_descriptor: int, name: str, challenge: str) -> None:
temporary = f".{name}.tmp-{secrets.token_hex(8)}"
flags = (
os.O_WRONLY
| os.O_CREAT
| os.O_EXCL
| getattr(os, "O_CLOEXEC", 0)
| getattr(os, "O_NOFOLLOW", 0)
)
descriptor = os.open(temporary, flags, 0o600, dir_fd=directory_descriptor)
try:
os.fchmod(descriptor, 0o600)
with os.fdopen(descriptor, "w", encoding="utf-8", closefd=False) as stream:
stream.write(challenge)
stream.flush()
os.fsync(stream.fileno())
os.replace(
temporary,
name,
src_dir_fd=directory_descriptor,
dst_dir_fd=directory_descriptor,
)
os.fsync(directory_descriptor)
except Exception:
try:
os.unlink(temporary, dir_fd=directory_descriptor)
except FileNotFoundError:
pass
raise
finally:
os.close(descriptor)
def parse_begin_reply(
completed: subprocess.CompletedProcess[str],
) -> tuple[str, dict[str, object] | None]:
if completed.returncode != 0:
return f"PROMOTER_EXIT_{completed.returncode}", None
try:
value = json.loads(
completed.stdout,
object_pairs_hook=reject_duplicate_json_keys,
)
except (json.JSONDecodeError, RecursionError, TypeError, ValueError):
return "INVALID_PROMOTER_REPLY", None
if not isinstance(value, dict):
return "INVALID_PROMOTER_REPLY", None
if value.get("ok") is False and set(value) == {"ok", "code"}:
code = value.get("code")
return code if isinstance(code, str) and code else "PROMOTION_BEGIN_REFUSED", value
if set(value) != EXPECTED_BEGIN_KEYS or value.get("ok") is not True:
return "INVALID_PROMOTER_REPLY", None
if value.get("state") != "PENDING_VERIFICATION":
return "INVALID_PROMOTER_REPLY", None
challenge = value.get("receipt_challenge")
receipt = value.get("receipt")
binding = value.get("binding")
if (
not isinstance(challenge, str)
or len(challenge) != 64
or any(character not in "0123456789abcdef" for character in challenge)
or not isinstance(receipt, str)
or not isinstance(binding, dict)
or set(binding) != EXPECTED_BINDING_KEYS
):
return "INVALID_PROMOTER_REPLY", None
integer_fields = (
"compaction_epoch",
"request_epoch",
"runtime_generation",
"schema_version",
)
if any(type(binding.get(field)) is not int or binding[field] < 0 for field in integer_fields):
return "INVALID_PROMOTER_REPLY", None
if not all(
isinstance(binding.get(field), str)
and len(binding[field]) == 64
and all(character in "0123456789abcdef" for character in binding[field])
for field in ("h_source", "h_payload")
):
return "INVALID_PROMOTER_REPLY", None
if not secrets.compare_digest(
receipt.encode("utf-8"),
receipt_for(challenge, binding).encode("utf-8"),
):
return "INVALID_PROMOTER_REPLY", None
return "", value
def main(
*,
environ: Mapping[str, str] | None = None,
stdin: object | None = None,
stdout: TextIO | None = None,
stderr: TextIO | None = None,
run: Callable[..., subprocess.CompletedProcess[str]] = subprocess.run,
now: Callable[[], float] = time.time,
) -> int:
source_environment = os.environ if environ is None else environ
input_stream = sys.stdin if stdin is None else stdin
output_stream = sys.stdout if stdout is None else stdout
error_stream = sys.stderr if stderr is None else stderr
try:
hook_input = read_hook_input(input_stream)
except (OSError, RecursionError, ValueError, json.JSONDecodeError) as error:
print(f"Mosaic promotion trigger ignored invalid hook input: {error}", file=error_stream)
return 0
if hook_input.get("prompt") != PROMOTION_PROMPT:
return 0
directory_descriptor: int | None = None
lock_descriptor: int | None = None
try:
runtime_dir, pending_name = session_pending_name(source_environment)
session_id = source_environment["MOSAIC_LEASE_SESSION_ID"]
directory_descriptor = open_pending_directory(runtime_dir)
lock_descriptor = acquire_lock(directory_descriptor)
wall_clock = now()
nonce = consume_authorization(directory_descriptor, session_id, wall_clock)
if nonce is None:
write_result(directory_descriptor, "0" * 64, False, "NOT_AUTHORIZED", session_id, wall_clock)
print("Mosaic promotion denied: NOT_AUTHORIZED.", file=error_stream)
return 0
sweep_stale_pending(directory_descriptor, wall_clock)
completed = run([sys.executable, "-I", "-S", "-B", str(PROMOTER), "--begin"], check=False, capture_output=True, text=True, env=dict(source_environment), timeout=PROMOTER_TIMEOUT_SECONDS)
code, reply = parse_begin_reply(completed)
if code or reply is None:
write_result(directory_descriptor, nonce, False, code or "PROMOTION_BEGIN_FAILED", session_id, now())
return 0
challenge = str(reply["receipt_challenge"])
observation = observer_request(
Path(source_environment["MOSAIC_RECEIPT_OBSERVER_SOCKET"]),
{"action": "record_runtime_observation", "session_id": session_id, "runtime_generation": int(source_environment["MOSAIC_RUNTIME_GENERATION"]), "runtime": "claude", "latest_assistant_message": reply["receipt"]},
)
if set(observation) != {"ok"} or observation.get("ok") is not True:
write_result(directory_descriptor, challenge, False, "OBSERVATION_REJECTED", session_id, now())
return 0
completion = run([sys.executable, "-I", "-S", "-B", str(PROMOTER), "--complete", challenge], check=False, capture_output=True, text=True, env=dict(source_environment), timeout=PROMOTER_TIMEOUT_SECONDS)
try:
outcome = json.loads(completion.stdout, object_pairs_hook=reject_duplicate_json_keys)
except (json.JSONDecodeError, ValueError):
outcome = None
if completion.returncode == 0 and isinstance(outcome, dict) and outcome.get("stage") == "promote_lease" and outcome.get("ok") is True and outcome.get("state") == "VERIFIED":
write_result(directory_descriptor, challenge, True, None, session_id, now())
else:
reason = outcome.get("code") if isinstance(outcome, dict) and isinstance(outcome.get("code"), str) else "PROMOTION_INCOMPLETE"
write_result(directory_descriptor, challenge, False, reason, session_id, now())
except PromotionAlreadyInProgress:
print("Mosaic promotion denied: PROMOTION_ALREADY_IN_PROGRESS.", file=error_stream)
except (KeyError, OSError, RecursionError, ValueError, subprocess.SubprocessError) as error:
print(f"Mosaic promotion begin failed: {type(error).__name__}: {error}", file=error_stream)
finally:
if lock_descriptor is not None:
os.close(lock_descriptor)
if directory_descriptor is not None:
os.close(directory_descriptor)
return 0
if __name__ == "__main__":
raise SystemExit(main())
@@ -0,0 +1,361 @@
#!/usr/bin/env python3
"""Claude Stop hook that completes a pending operator-triggered promotion."""
from __future__ import annotations
import fcntl
import json
import os
import secrets
import stat
import subprocess
import sys
import time
from collections.abc import Callable, Mapping
from pathlib import Path
from typing import Final, NamedTuple, TextIO
MAX_FRAME: Final = 64 * 1024
PROMOTER_TIMEOUT_SECONDS: Final = 10.0
LEASE_TTL_SECONDS: Final = 60 * 60
PROMOTER: Final = Path(__file__).resolve().with_name("lease_promote.py")
PENDING_DIRECTORY: Final = "mosaic-lease"
LOCK_FILE: Final = "promotion.lock"
RESULT_FILE: Final = "last-result.json"
TERMINAL_FAILURE_CODES: Final = frozenset(
{
"RECEIPT_REPLAY",
"RECEIPT_MISMATCH",
"INVALID_LEASE_TRANSITION",
"PROMOTION_TOKEN_INVALID",
}
)
class PendingChallenge(NamedTuple):
value: str
device: int
inode: int
def reject_duplicate_json_keys(pairs: list[tuple[str, object]]) -> dict[str, object]:
value: dict[str, object] = {}
for key, item in pairs:
if key in value:
raise ValueError("duplicate promoter JSON key")
value[key] = item
return value
def session_pending_name(environ: Mapping[str, str]) -> tuple[Path, str]:
runtime_dir = Path(environ["XDG_RUNTIME_DIR"])
session_id = environ["MOSAIC_LEASE_SESSION_ID"]
if not runtime_dir.is_absolute():
raise ValueError("XDG_RUNTIME_DIR must be absolute")
if len(session_id) != 64 or any(character not in "0123456789abcdef" for character in session_id):
raise ValueError("invalid lease session id")
return runtime_dir, f"pending-{session_id}"
def open_pending_directory(runtime_dir: Path) -> int | None:
directory_flags = (
os.O_RDONLY
| getattr(os, "O_CLOEXEC", 0)
| getattr(os, "O_DIRECTORY", 0)
| getattr(os, "O_NOFOLLOW", 0)
)
try:
runtime_descriptor = os.open(runtime_dir, directory_flags)
except FileNotFoundError:
return None
try:
runtime_metadata = os.fstat(runtime_descriptor)
if (
not stat.S_ISDIR(runtime_metadata.st_mode)
or runtime_metadata.st_uid != os.getuid()
or stat.S_IMODE(runtime_metadata.st_mode) != 0o700
):
raise ValueError("unsafe XDG runtime directory")
try:
descriptor = os.open(PENDING_DIRECTORY, directory_flags, dir_fd=runtime_descriptor)
except FileNotFoundError:
return None
finally:
os.close(runtime_descriptor)
metadata = os.fstat(descriptor)
if (
not stat.S_ISDIR(metadata.st_mode)
or metadata.st_uid != os.getuid()
or stat.S_IMODE(metadata.st_mode) != 0o700
):
os.close(descriptor)
raise ValueError("unsafe promotion pending directory")
return descriptor
def acquire_lock(directory_descriptor: int) -> int:
flags = (
os.O_RDWR
| os.O_CREAT
| getattr(os, "O_CLOEXEC", 0)
| getattr(os, "O_NOFOLLOW", 0)
)
descriptor = os.open(LOCK_FILE, flags, 0o600, dir_fd=directory_descriptor)
metadata = os.fstat(descriptor)
if (
not stat.S_ISREG(metadata.st_mode)
or metadata.st_uid != os.getuid()
or stat.S_IMODE(metadata.st_mode) != 0o600
):
os.close(descriptor)
raise ValueError("unsafe promotion lock file")
try:
fcntl.flock(descriptor, fcntl.LOCK_EX | fcntl.LOCK_NB)
except BlockingIOError:
os.close(descriptor)
raise
return descriptor
def read_pending(directory_descriptor: int, name: str) -> PendingChallenge | None:
flags = os.O_RDONLY | getattr(os, "O_CLOEXEC", 0) | getattr(os, "O_NOFOLLOW", 0)
try:
descriptor = os.open(name, flags, dir_fd=directory_descriptor)
except FileNotFoundError:
return None
try:
metadata = os.fstat(descriptor)
if (
not stat.S_ISREG(metadata.st_mode)
or metadata.st_uid != os.getuid()
or stat.S_IMODE(metadata.st_mode) != 0o600
or metadata.st_size <= 0
or metadata.st_size > MAX_FRAME
):
raise ValueError("unsafe promotion pending file")
raw = os.read(descriptor, MAX_FRAME + 1)
finally:
os.close(descriptor)
if len(raw) > MAX_FRAME:
raise ValueError("oversized promotion challenge")
challenge = raw.decode("utf-8")
if (
len(challenge) != 64
or any(character not in "0123456789abcdef" for character in challenge)
):
raise ValueError("invalid promotion challenge")
return PendingChallenge(challenge, metadata.st_dev, metadata.st_ino)
def write_result(
directory_descriptor: int,
attempt_id: str,
verified: bool,
reason: str | None,
session_id: str,
wall_clock: float,
) -> None:
result = {
"attempt_id": attempt_id,
"expires_at_wallclock": wall_clock + LEASE_TTL_SECONDS if verified else None,
"reason": reason,
"session_id": session_id,
"ts": wall_clock,
"verified": verified,
}
temporary = f".{RESULT_FILE}.tmp-{secrets.token_hex(8)}"
flags = (
os.O_WRONLY
| os.O_CREAT
| os.O_EXCL
| getattr(os, "O_CLOEXEC", 0)
| getattr(os, "O_NOFOLLOW", 0)
)
descriptor = os.open(temporary, flags, 0o600, dir_fd=directory_descriptor)
try:
os.fchmod(descriptor, 0o600)
with os.fdopen(descriptor, "w", encoding="utf-8", closefd=False) as stream:
json.dump(result, stream, separators=(",", ":"), sort_keys=True)
stream.flush()
os.fsync(stream.fileno())
os.replace(
temporary,
RESULT_FILE,
src_dir_fd=directory_descriptor,
dst_dir_fd=directory_descriptor,
)
os.fsync(directory_descriptor)
except Exception:
try:
os.unlink(temporary, dir_fd=directory_descriptor)
except FileNotFoundError:
pass
raise
finally:
os.close(descriptor)
def delete_pending_if_unchanged(
directory_descriptor: int,
name: str,
pending: PendingChallenge,
error_stream: TextIO,
) -> None:
quarantine = f".{name}.delete-{secrets.token_hex(8)}"
try:
os.rename(
name,
quarantine,
src_dir_fd=directory_descriptor,
dst_dir_fd=directory_descriptor,
)
except FileNotFoundError:
return
except OSError as error:
print(f"Mosaic promotion could not quarantine pending file: {error}", file=error_stream)
return
try:
moved = os.stat(
quarantine,
dir_fd=directory_descriptor,
follow_symlinks=False,
)
if (moved.st_dev, moved.st_ino) == (pending.device, pending.inode):
os.unlink(quarantine, dir_fd=directory_descriptor)
os.fsync(directory_descriptor)
return
print("Mosaic promotion pending file changed; preserving replacement.", file=error_stream)
try:
os.link(
quarantine,
name,
src_dir_fd=directory_descriptor,
dst_dir_fd=directory_descriptor,
follow_symlinks=False,
)
except FileExistsError:
print(
f"Mosaic promotion preserved replacement as {quarantine}.",
file=error_stream,
)
else:
os.unlink(quarantine, dir_fd=directory_descriptor)
os.fsync(directory_descriptor)
except OSError as error:
print(f"Mosaic promotion could not resolve pending file: {error}", file=error_stream)
def parse_reply(completed: subprocess.CompletedProcess[str]) -> dict[str, object] | None:
if completed.returncode != 0:
return None
try:
value = json.loads(
completed.stdout,
object_pairs_hook=reject_duplicate_json_keys,
)
except (json.JSONDecodeError, RecursionError, TypeError, ValueError):
return None
if not isinstance(value, dict):
return None
if set(value) == {"stage", "ok", "state"}:
if (
value.get("stage") == "promote_lease"
and value.get("ok") is True
and value.get("state") == "VERIFIED"
):
return value
return None
if set(value) == {"stage", "ok", "code"}:
if (
value.get("stage") in {"observe_receipt", "promote_lease"}
and value.get("ok") is False
and isinstance(value.get("code"), str)
and value.get("code")
):
return value
return None
def main(
*,
environ: Mapping[str, str] | None = None,
stderr: TextIO | None = None,
run: Callable[..., subprocess.CompletedProcess[str]] = subprocess.run,
now: Callable[[], float] = time.time,
) -> int:
source_environment = os.environ if environ is None else environ
error_stream = sys.stderr if stderr is None else stderr
directory_descriptor: int | None = None
lock_descriptor: int | None = None
try:
runtime_dir, pending_name = session_pending_name(source_environment)
session_id = source_environment["MOSAIC_LEASE_SESSION_ID"]
directory_descriptor = open_pending_directory(runtime_dir)
if directory_descriptor is None:
return 0
try:
lock_descriptor = acquire_lock(directory_descriptor)
except (BlockingIOError, FileNotFoundError):
print("Mosaic promotion completion deferred: promotion is in progress.", file=error_stream)
return 0
pending = read_pending(directory_descriptor, pending_name)
if pending is None:
return 0
completed = run(
[
sys.executable,
"-I",
"-S",
"-B",
str(PROMOTER),
"--complete",
pending.value,
],
check=False,
capture_output=True,
text=True,
env=dict(source_environment),
timeout=PROMOTER_TIMEOUT_SECONDS,
)
reply = parse_reply(completed)
if reply is not None and reply.get("ok") is True:
write_result(directory_descriptor, pending.value, True, None, session_id, now())
delete_pending_if_unchanged(
directory_descriptor,
pending_name,
pending,
error_stream,
)
print("Mosaic lease promotion completed.", file=error_stream)
return 0
if reply is not None:
code = str(reply["code"])
print(f"Mosaic promotion incomplete: {code}.", file=error_stream)
if code in TERMINAL_FAILURE_CODES:
write_result(directory_descriptor, pending.value, False, code, session_id, now())
delete_pending_if_unchanged(
directory_descriptor,
pending_name,
pending,
error_stream,
)
else:
diagnostic = completed.stderr.strip() or f"promoter exit {completed.returncode}"
print(f"Mosaic promotion retryable failure: {diagnostic}.", file=error_stream)
except (KeyError, OSError, RecursionError, UnicodeError, ValueError, subprocess.SubprocessError) as error:
print(f"Mosaic promotion completion deferred: {type(error).__name__}: {error}", file=error_stream)
finally:
if lock_descriptor is not None:
os.close(lock_descriptor)
if directory_descriptor is not None:
os.close(directory_descriptor)
return 0
if __name__ == "__main__":
raise SystemExit(main())
@@ -0,0 +1,166 @@
#!/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
BENIGN_OBSERVATION_UNAVAILABLE_CODE: Final = "OBSERVATION_UNAVAILABLE"
def reject_duplicate_json_keys(pairs: list[tuple[str, object]]) -> dict[str, object]:
value: dict[str, object] = {}
for key, item in pairs:
if key in value:
raise ValueError("duplicate observer JSON key")
value[key] = item
return value
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, object_pairs_hook=reject_duplicate_json_keys)
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 response.count(b"\n") != 1 or not response.endswith(b"\n"):
raise ValueError("invalid observer reply")
value = json.loads(response[:-1], object_pairs_hook=reject_duplicate_json_keys)
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")
if "last_assistant_message" in source:
message = source["last_assistant_message"]
if not isinstance(message, str):
raise ValueError("invalid Claude observer input")
else:
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, RecursionError, ValueError, json.JSONDecodeError) as error:
print(f"Mosaic receipt observer refused: {error}", file=sys.stderr)
return 2
if set(reply) == {"ok"} and reply.get("ok") is True:
return 0
if (
set(reply) == {"ok", "code"}
and reply.get("ok") is False
and reply.get("code") == BENIGN_OBSERVATION_UNAVAILABLE_CODE
):
return 0
return 2
if __name__ == "__main__":
raise SystemExit(main())
@@ -0,0 +1,43 @@
#!/usr/bin/env python3
"""Broker-side construction and verification for one-time receipt challenges."""
from __future__ import annotations
import hashlib
import hmac
import struct
from typing import Final
LATEST_ASSISTANT_DOMAIN_SEPARATOR: Final = b"MOSAIC/H_LATEST_ASSISTANT/v1\x00"
def _length_frame(value: bytes) -> bytes:
return struct.pack(">Q", len(value)) + value
def receipt_for(challenge: str, binding: dict[str, object]) -> str:
"""Return the sole receipt text a model may copy for this broker cycle."""
return (
"MOSAIC-RECEIPT{"
f"challenge={challenge}; "
f"H_payload={binding['h_payload']}; "
f"gen={binding['runtime_generation']}; "
f"cep={binding['compaction_epoch']}"
"}"
)
def is_verbatim_receipt(message: str, challenge: str, binding: dict[str, object]) -> bool:
"""Require the exact one current-cycle receipt, not a transcript substring."""
expected = receipt_for(challenge, binding)
return hmac.compare_digest(message.encode("utf-8"), expected.encode("utf-8"))
def latest_assistant_digest(message: str) -> str:
"""Record the broker-computed digest of the exact observed assistant entry."""
encoded = message.encode("utf-8")
return hashlib.sha256(LATEST_ASSISTANT_DOMAIN_SEPARATOR + _length_frame(encoded)).hexdigest()
@@ -0,0 +1,133 @@
#!/usr/bin/env python3
"""Trusted latest-assistant-message observer boundary for receipt promotion.
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
import json
import os
import stat
from pathlib import Path
from typing import Protocol
class ReceiptObserver(Protocol):
def observe_latest_assistant_message(
self,
session_id: str,
runtime: str,
runtime_generation: int,
binding: dict[str, object],
) -> str | None: ...
class UnavailableReceiptObserver:
"""Fail-closed only for direct unit construction without daemon transport."""
def observe_latest_assistant_message(
self,
_session_id: str,
_runtime: str,
_runtime_generation: int,
_binding: dict[str, object],
) -> str | None:
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."""
def __init__(self) -> None:
self._messages: dict[tuple[str, int], str] = {}
def record_latest_assistant_message(
self, session_id: str, runtime_generation: int, message: str
) -> None:
self._messages[(session_id, 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_generation))
class FileTestReceiptObserver:
"""Private fixture-file observer for isolated out-of-process test drivers only."""
def __init__(self, path: Path) -> None:
self.path = path
def observe_latest_assistant_message(
self,
session_id: str,
_runtime: str,
runtime_generation: int,
_binding: dict[str, object],
) -> str | None:
flags = os.O_RDONLY | getattr(os, "O_CLOEXEC", 0) | getattr(os, "O_NOFOLLOW", 0)
try:
descriptor = os.open(self.path, flags)
except OSError:
return None
try:
metadata = os.fstat(descriptor)
if (
not stat.S_ISREG(metadata.st_mode)
or stat.S_IMODE(metadata.st_mode) != 0o600
or metadata.st_uid != os.geteuid()
or metadata.st_size > 64 * 1024
):
return None
raw = os.read(descriptor, 64 * 1024 + 1)
finally:
os.close(descriptor)
if len(raw) > 64 * 1024:
return None
try:
value = json.loads(raw)
except (json.JSONDecodeError, UnicodeDecodeError):
return None
if (
not isinstance(value, dict)
or set(value) != {"session_id", "runtime_generation", "latest_assistant_message"}
or value["session_id"] != session_id
or value["runtime_generation"] != runtime_generation
or not isinstance(value["latest_assistant_message"], str)
):
return None
return value["latest_assistant_message"]
@@ -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())
@@ -0,0 +1,116 @@
#!/usr/bin/env python3
"""Revoke a runtime lease from a compaction or lifecycle observer."""
from __future__ import annotations
import argparse
import json
import os
import socket
import sys
from collections.abc import Callable, Mapping, Sequence
from pathlib import Path
from typing import Final
from lease_generation import bump_runtime_generation, read_runtime_generation
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("invalid revoke request")
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 main(
argv: Sequence[str] | None = None,
*,
environ: Mapping[str, str] | None = None,
request: Callable[[Path, dict[str, object]], dict[str, object]] = broker_request,
) -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--runtime", required=True, choices=("claude", "pi"))
parser.add_argument("--reason", required=True)
parser.add_argument("--bump-generation", action="store_true")
arguments = parser.parse_args(argv)
source_environment = os.environ if environ is None else environ
# D29: a session that never held a lease has nothing to revoke, and that is a
# SUCCESS, not a failed revocation. The block below is deliberately fail-closed
# for a broker that is unreachable, which is right — but it cannot distinguish
# "the broker is down" from "there was never a lease", so a bare-launched
# session was denied every lifecycle transition, including compaction. Denying
# compaction protects nothing there; it converts a recoverable context limit
# into a lost session.
#
# Absence must be TOTAL to qualify. If exactly one variable is present the
# session is half-provisioned, which is real misconfiguration, and it still
# takes the fail-closed path below.
lease_variables = ("MOSAIC_LEASE_BROKER_SOCKET", "MOSAIC_LEASE_SESSION_ID")
present = [name for name in lease_variables if source_environment.get(name)]
if not present:
return 0
try:
if not arguments.reason or len(arguments.reason) > 128:
raise ValueError("invalid revoke reason")
socket_path = Path(source_environment["MOSAIC_LEASE_BROKER_SOCKET"])
session_id = source_environment["MOSAIC_LEASE_SESSION_ID"]
if (
len(session_id) != 64
or any(character not in "0123456789abcdef" for character in session_id)
):
raise ValueError("invalid broker session")
generation = (
bump_runtime_generation(source_environment)
if arguments.bump_generation
else read_runtime_generation(source_environment)
)
reply = request(
socket_path,
{
"action": "revoke_lease",
"session_id": session_id,
"runtime_generation": generation,
"reason": arguments.reason,
"runtime": arguments.runtime,
},
)
if reply.get("ok") is not True or reply.get("state") != "UNVERIFIED":
raise ValueError("revocation refused")
except (KeyError, ValueError, OSError, json.JSONDecodeError):
# A fired observer must remain fail-closed even if the broker transport
# is unavailable. Advancing the private local generation fences every
# later tool check; the broker revokes the old lease when it next sees
# that higher generation. Explicit rollover already advanced it above.
if not arguments.bump_generation:
try:
bump_runtime_generation(source_environment)
except (KeyError, ValueError, OSError):
pass
print("Mosaic lease revocation failed; lifecycle transition denied.", file=sys.stderr)
return 2
return 0
if __name__ == "__main__":
raise SystemExit(main())
@@ -0,0 +1,31 @@
#!/usr/bin/env bash
# Supervisor entry point for the Mosaic lease broker daemon (issue #869, C3).
#
# Resolves the broker socket path with the SAME precedence as
# `defaultLeaseBrokerSocket` in `packages/mosaic/src/commands/launch.ts`, so a
# gated runtime launched through that client always finds the socket this
# supervisor creates:
# 1. an explicit MOSAIC_LEASE_BROKER_SOCKET
# 2. "$XDG_RUNTIME_DIR/mosaic-lease/broker.sock"
# 3. "/run/user/<uid>/mosaic-lease/broker.sock"
#
# The state file is colocated next to the socket (same directory,
# "state.json"), mirroring how the broker already colocates its per-session
# generation files beside the socket.
#
# This script never installs, enables, or starts the systemd unit that calls
# it; it is only ever invoked BY that unit (or by a human/test harness that
# passes its own HOME/XDG_RUNTIME_DIR).
set -euo pipefail
SCRIPT_DIR=$(cd -- "$(dirname -- "$0")" && pwd)
if [ -n "${MOSAIC_LEASE_BROKER_SOCKET:-}" ]; then
SOCKET="$MOSAIC_LEASE_BROKER_SOCKET"
else
RUNTIME_DIR="${XDG_RUNTIME_DIR:-/run/user/$(id -u)}"
SOCKET="$RUNTIME_DIR/mosaic-lease/broker.sock"
fi
STATE="$(dirname -- "$SOCKET")/state.json"
exec python3 "$SCRIPT_DIR/daemon.py" --socket "$SOCKET" --state "$STATE"