This commit was merged in pull request #846.
This commit is contained in:
@@ -8,6 +8,7 @@ import {
|
||||
mkdirSync,
|
||||
mkdtempSync,
|
||||
readlinkSync,
|
||||
readFileSync,
|
||||
rmSync,
|
||||
symlinkSync,
|
||||
writeFileSync,
|
||||
@@ -26,6 +27,9 @@ import {
|
||||
const LEGACY_SYNC_SCRIPT = fileURLToPath(
|
||||
new URL('../../framework/tools/_scripts/mosaic-sync-skills', import.meta.url),
|
||||
);
|
||||
const CONTEXT_REFRESH_SOURCE_SKILL = fileURLToPath(
|
||||
new URL('../../framework/skills/mosaic-context-refresh/SKILL.md', import.meta.url),
|
||||
);
|
||||
|
||||
describe('Claude skill bridge', () => {
|
||||
let root: string;
|
||||
@@ -360,6 +364,25 @@ describe('Claude skill bridge', () => {
|
||||
});
|
||||
|
||||
describe('syncClaudeSkills', () => {
|
||||
it('projects the durable context-refresh skill through the #824 bridge in a tmp root', () => {
|
||||
expect(existsSync(CONTEXT_REFRESH_SOURCE_SKILL)).toBe(true);
|
||||
const source = readFileSync(CONTEXT_REFRESH_SOURCE_SKILL, 'utf8');
|
||||
expect(source).toContain('recover-context.py');
|
||||
expect(source).toContain('middle drop is not represented as receipt-detectable');
|
||||
const skillDir = createSkill('mosaic-context-refresh');
|
||||
writeFileSync(join(skillDir, 'SKILL.md'), source);
|
||||
|
||||
const result = syncClaudeSkills(paths);
|
||||
|
||||
expect(result).toEqual({
|
||||
registered: ['mosaic-context-refresh'],
|
||||
repaired: [],
|
||||
unchanged: [],
|
||||
conflicts: [],
|
||||
});
|
||||
expectCorrectLink('mosaic-context-refresh');
|
||||
});
|
||||
|
||||
it('generically creates every missing canonical link and repairs managed broken links', () => {
|
||||
createSkill('added-after-setup');
|
||||
createSkill('another-new-skill');
|
||||
|
||||
@@ -78,6 +78,7 @@ const PROBE_PATHS = [
|
||||
'install.sh',
|
||||
'framework-manifest.txt',
|
||||
'guides/E2E-DELIVERY.md',
|
||||
'skills/mosaic-context-refresh/SKILL.md',
|
||||
'tools/git/pr-create.sh',
|
||||
'tools/_lib/manifest.sh',
|
||||
'defaults/SOUL.md',
|
||||
@@ -97,6 +98,7 @@ const PROBE_PATHS = [
|
||||
'memory/note.md',
|
||||
'sources/skills/x.md',
|
||||
'credentials/c.json',
|
||||
'skills-local/custom/SKILL.md',
|
||||
'tools/_lib/credentials.json',
|
||||
'fleet/roster.yaml',
|
||||
'fleet/roster.json',
|
||||
|
||||
@@ -304,6 +304,11 @@ describe('manifest completeness against shipped framework tree', () => {
|
||||
expect(misclassified).toEqual([]);
|
||||
});
|
||||
|
||||
it('shipped canonical skills are framework-owned while local skills are operator-owned', () => {
|
||||
expect(resolveOwnership(manifest, 'skills/mosaic-context-refresh/SKILL.md')).toBe('framework');
|
||||
expect(resolveOwnership(manifest, 'skills-local/custom/SKILL.md')).toBe('operator');
|
||||
});
|
||||
|
||||
it('the operator-owned surface from #791 resolves to operator', () => {
|
||||
const operatorPaths = [
|
||||
'agents/coder0.conf',
|
||||
@@ -313,6 +318,7 @@ describe('manifest completeness against shipped framework tree', () => {
|
||||
'SOUL.local.md',
|
||||
'USER.local.md',
|
||||
'STANDARDS.local.md',
|
||||
'skills-local/custom/SKILL.md',
|
||||
'tools/_lib/credentials.json',
|
||||
'fleet/roster.yaml',
|
||||
'fleet/roster.json',
|
||||
|
||||
171
packages/mosaic/src/lease-broker/context_recovery_unittest.py
Normal file
171
packages/mosaic/src/lease-broker/context_recovery_unittest.py
Normal file
@@ -0,0 +1,171 @@
|
||||
#!/usr/bin/env python3
|
||||
"""RED-first WI-6 contracts against the shipped constrained recovery entrypoint."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
import hashlib
|
||||
import importlib.util
|
||||
import os
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
TOOLS = Path(__file__).parents[2] / "framework/tools/lease-broker"
|
||||
DAEMON_PATH = TOOLS / "daemon.py"
|
||||
FRAGMENTS_PATH = TOOLS / "normative_fragments.py"
|
||||
OBSERVER_PATH = TOOLS / "receipt_observer.py"
|
||||
|
||||
|
||||
def load_module(name: str, path: Path):
|
||||
assert path.is_file(), f"shipped module is missing: {path}"
|
||||
spec = importlib.util.spec_from_file_location(name, path)
|
||||
if spec is None or spec.loader is None:
|
||||
raise RuntimeError(f"unable to load {name}")
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
DAEMON = load_module("lease_broker_recovery_daemon", DAEMON_PATH)
|
||||
FRAGMENTS = load_module("lease_broker_recovery_fragments", FRAGMENTS_PATH)
|
||||
OBSERVER = load_module("lease_broker_recovery_observer", OBSERVER_PATH)
|
||||
|
||||
|
||||
class RecoveryFixture(unittest.TestCase):
|
||||
def setUp(self) -> None:
|
||||
self.temporary = tempfile.TemporaryDirectory()
|
||||
root = Path(self.temporary.name)
|
||||
os.chmod(root, 0o700)
|
||||
self.peer = (os.getpid(), os.getuid(), os.getgid())
|
||||
self.observer = OBSERVER.TestReceiptObserver()
|
||||
self.broker = DAEMON.Broker(DAEMON.StateStore(root / "state.json"), observer=self.observer)
|
||||
registered = self.broker.handle(self.peer, {
|
||||
"action": "register_anchor",
|
||||
"runtime_generation": 11,
|
||||
})
|
||||
self.session_id = registered["session_id"]
|
||||
|
||||
def tearDown(self) -> None:
|
||||
self.temporary.cleanup()
|
||||
|
||||
def construction_and_binding(self) -> tuple[dict[str, object], dict[str, object]]:
|
||||
content = b"Mosaic recovery authority\n"
|
||||
construction = {
|
||||
"manifest_version": 1,
|
||||
"generator_version": "wi6-recovery-test",
|
||||
"fragments": [{
|
||||
"source_id": "authority/recovery",
|
||||
"content_base64": base64.b64encode(content).decode("ascii"),
|
||||
"expected_sha256": hashlib.sha256(content).hexdigest(),
|
||||
}],
|
||||
}
|
||||
built = FRAGMENTS.build_payload_from_wire(construction)
|
||||
self.assertEqual(built.injectionDecision, "ACCEPTED")
|
||||
self.assertTrue(built.promotion)
|
||||
return construction, {
|
||||
"compaction_epoch": 17,
|
||||
"request_epoch": 23,
|
||||
"h_source": built.h_source,
|
||||
"h_payload": built.h_payload,
|
||||
"schema_version": 1,
|
||||
}
|
||||
|
||||
def begin_normal(self) -> dict[str, object]:
|
||||
construction, binding = self.construction_and_binding()
|
||||
return self.broker.handle(self.peer, {
|
||||
"action": "begin_verification",
|
||||
"session_id": self.session_id,
|
||||
"runtime_generation": 11,
|
||||
"runtime": "pi",
|
||||
"binding": binding,
|
||||
"construction": construction,
|
||||
})
|
||||
|
||||
def begin_recovery(self) -> dict[str, object]:
|
||||
construction, binding = self.construction_and_binding()
|
||||
return self.broker.handle(self.peer, {
|
||||
"action": "begin_recovery",
|
||||
"session_id": self.session_id,
|
||||
"runtime_generation": 11,
|
||||
"runtime": "pi",
|
||||
"binding": binding,
|
||||
"construction": construction,
|
||||
})
|
||||
|
||||
def complete_recovery(self) -> dict[str, object]:
|
||||
return self.broker.handle(self.peer, {
|
||||
"action": "complete_recovery",
|
||||
"session_id": self.session_id,
|
||||
"runtime_generation": 11,
|
||||
})
|
||||
|
||||
def assert_recovery_refused_unverified(self, code: str) -> None:
|
||||
with self.assertRaisesRegex(DAEMON.BrokerFailure, code):
|
||||
self.complete_recovery()
|
||||
self.assertEqual(self.broker.leases[self.session_id]["state"], DAEMON.LEASE_UNVERIFIED)
|
||||
denied = self.broker.handle(self.peer, {
|
||||
"action": "authorize_tool",
|
||||
"session_id": self.session_id,
|
||||
"runtime_generation": 11,
|
||||
"runtime": "pi",
|
||||
"tool_name": "bash",
|
||||
})
|
||||
self.assertEqual(denied["decision"], "deny")
|
||||
|
||||
|
||||
class ConstrainedRecoveryContractTest(RecoveryFixture):
|
||||
def test_recovery_mints_a_fresh_challenge_distinct_from_normal_path(self) -> None:
|
||||
normal = self.begin_normal()
|
||||
recovery = self.begin_recovery()
|
||||
|
||||
self.assertEqual(recovery["state"], "PENDING_DELIVERY")
|
||||
self.assertNotEqual(normal["receipt_challenge"], recovery["receipt_challenge"])
|
||||
self.assertIn(recovery["receipt_challenge"], recovery["receipt"])
|
||||
|
||||
def test_c4_normal_path_receipt_cannot_be_replayed_through_recovery(self) -> None:
|
||||
normal = self.begin_normal()
|
||||
recovery = self.begin_recovery()
|
||||
self.assertNotEqual(normal["receipt_challenge"], recovery["receipt_challenge"])
|
||||
|
||||
self.observer.record_latest_assistant_message(self.session_id, 11, normal["receipt"])
|
||||
self.assert_recovery_refused_unverified("RECEIPT_MISMATCH")
|
||||
|
||||
def test_t27_observable_partial_delivery_variants_never_promote(self) -> None:
|
||||
variants = {
|
||||
"absent": None,
|
||||
"malformed": "MOSAIC-RECEIPT{malformed}",
|
||||
"prefix-truncated": None,
|
||||
"observable-adapter-mutation": None,
|
||||
# Tail-only is represented only by this concrete malformed/incomplete
|
||||
# delivery. It is not a category-wide tail-only detection claim.
|
||||
"tail-only-malformed": "H_payload=tail-only",
|
||||
}
|
||||
for name, observed in variants.items():
|
||||
with self.subTest(name=name):
|
||||
recovery = self.begin_recovery()
|
||||
if name == "prefix-truncated":
|
||||
observed = recovery["receipt"][:-1]
|
||||
elif name == "observable-adapter-mutation":
|
||||
observed = recovery["receipt"].replace("H_payload=", "H_payload=0", 1)
|
||||
if observed is not None:
|
||||
self.observer.record_latest_assistant_message(self.session_id, 11, observed)
|
||||
self.assert_recovery_refused_unverified(
|
||||
"RECEIPT_OBSERVATION_UNAVAILABLE" if observed is None else "RECEIPT_MISMATCH"
|
||||
)
|
||||
|
||||
def test_negative_capability_tail_preserving_middle_drop_is_not_receipt_detectable(self) -> None:
|
||||
recovery = self.begin_recovery()
|
||||
|
||||
# The observer seam receives the exact terminal message, not the delivered
|
||||
# payload bytes. A middle drop that preserves this tail is therefore T-C
|
||||
# and deliberately NOT represented as receipt-detectable; WI-7 server-side
|
||||
# evidence owns that residual. This is not an assertion that it is caught.
|
||||
self.observer.record_latest_assistant_message(self.session_id, 11, recovery["receipt"])
|
||||
promoted = self.complete_recovery()
|
||||
self.assertEqual(promoted["state"], DAEMON.LEASE_VERIFIED)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,64 @@
|
||||
#!/usr/bin/env python3
|
||||
"""RED-first framework-firewall and portability contracts for shipped skills."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
import re
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
MOSAIC = Path(__file__).parents[2]
|
||||
SKILLS = MOSAIC / "framework/skills"
|
||||
REFRESH_SKILL = SKILLS / "mosaic-context-refresh/SKILL.md"
|
||||
GATE_PATH = MOSAIC / "framework/tools/lease-broker/mutator-gate.py"
|
||||
OPERATOR_HOME = re.compile(r"/home/[^/\s]+/")
|
||||
RECOVERY_PLACEHOLDER = "/absolute/path/to/mosaic/tools/lease-broker/recover-context.py"
|
||||
CONSTRUCTION_PLACEHOLDER = "/absolute/path/to/mosaic-context-refresh-construction.json"
|
||||
|
||||
|
||||
def load_gate():
|
||||
spec = importlib.util.spec_from_file_location("framework_skill_portability_gate", GATE_PATH)
|
||||
if spec is None or spec.loader is None:
|
||||
raise RuntimeError("unable to load mutator gate")
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
GATE = load_gate()
|
||||
|
||||
|
||||
class FrameworkSkillPortabilityTest(unittest.TestCase):
|
||||
def test_shipped_framework_skills_contain_no_operator_home_path(self) -> None:
|
||||
offenders = [
|
||||
str(path.relative_to(SKILLS))
|
||||
for path in SKILLS.rglob("SKILL.md")
|
||||
if OPERATOR_HOME.search(path.read_text(encoding="utf-8"))
|
||||
]
|
||||
self.assertEqual(offenders, [])
|
||||
|
||||
def test_shipped_recovery_template_resolves_to_a_literal_install_path_and_admits(self) -> None:
|
||||
source = REFRESH_SKILL.read_text(encoding="utf-8")
|
||||
self.assertIn(RECOVERY_PLACEHOLDER, source)
|
||||
self.assertIn(CONSTRUCTION_PLACEHOLDER, source)
|
||||
resolved_recovery = "/opt/mosaic/tools/lease-broker/recover-context.py"
|
||||
resolved_construction = "/opt/mosaic/recovery/construction.json"
|
||||
rendered = source.replace(RECOVERY_PLACEHOLDER, resolved_recovery).replace(
|
||||
CONSTRUCTION_PLACEHOLDER, resolved_construction
|
||||
)
|
||||
match = re.search(r"```bash\s*\n\s*(.*?)\n\s*```", rendered, flags=re.DOTALL)
|
||||
self.assertIsNotNone(match)
|
||||
command = match.group(1) if match is not None else ""
|
||||
self.assertEqual(
|
||||
GATE.recovery_invocation_name(
|
||||
{"tool_name": "Bash", "tool_input": {"command": command}},
|
||||
Path(resolved_recovery),
|
||||
),
|
||||
GATE.RECOVERY_TOOL,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,218 @@
|
||||
#!/usr/bin/env python3
|
||||
"""RED-first adversarial Claude B1 gate contracts.
|
||||
|
||||
This private harness drives the shipped gate and daemon out of process. It
|
||||
never contacts a live broker/runtime and executes a shell payload only after a
|
||||
regression has already (incorrectly) received the recovery exemption.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import socket
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import time
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
TOOLS = Path(__file__).parents[2] / "framework/tools/lease-broker"
|
||||
FRAMEWORK = Path(__file__).parents[2] / "framework"
|
||||
DAEMON = TOOLS / "daemon.py"
|
||||
GATE = TOOLS / "mutator-gate.py"
|
||||
RECOVERY = TOOLS / "recover-context.py"
|
||||
SKILL = FRAMEWORK / "skills/mosaic-context-refresh/SKILL.md"
|
||||
|
||||
|
||||
def request(socket_path: Path, value: dict[str, object]) -> dict[str, object]:
|
||||
with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as connection:
|
||||
connection.settimeout(3.0)
|
||||
connection.connect(str(socket_path))
|
||||
connection.sendall((json.dumps(value, separators=(",", ":")) + "\n").encode())
|
||||
connection.shutdown(socket.SHUT_WR)
|
||||
response = bytearray()
|
||||
while True:
|
||||
chunk = connection.recv(4096)
|
||||
if not chunk:
|
||||
break
|
||||
response.extend(chunk)
|
||||
if not response.endswith(b"\n") or response.count(b"\n") != 1:
|
||||
raise AssertionError(f"unframed broker response: {bytes(response)!r}")
|
||||
reply = json.loads(response[:-1])
|
||||
if not isinstance(reply, dict):
|
||||
raise AssertionError("broker response is not an object")
|
||||
return reply
|
||||
|
||||
|
||||
def wait_ready(process: subprocess.Popen[str], socket_path: Path) -> None:
|
||||
deadline = time.monotonic() + 5.0
|
||||
while time.monotonic() < deadline:
|
||||
if socket_path.exists():
|
||||
return
|
||||
if process.poll() is not None:
|
||||
output = process.stdout.read() if process.stdout is not None else ""
|
||||
raise RuntimeError(f"daemon exited before READY: {output}")
|
||||
time.sleep(0.02)
|
||||
raise TimeoutError("daemon did not create private socket")
|
||||
|
||||
|
||||
class ClaudeRecoveryGateAdversarialTest(unittest.TestCase):
|
||||
def setUp(self) -> None:
|
||||
self.temporary = tempfile.TemporaryDirectory()
|
||||
self.root = Path(self.temporary.name)
|
||||
os.chmod(self.root, 0o700)
|
||||
self.socket = self.root / "broker.sock"
|
||||
self.daemon = subprocess.Popen(
|
||||
[sys.executable, "-I", "-S", "-B", str(DAEMON), "--socket", str(self.socket), "--state", str(self.root / "state.json")],
|
||||
stdin=subprocess.DEVNULL,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
text=True,
|
||||
)
|
||||
wait_ready(self.daemon, self.socket)
|
||||
registered = request(self.socket, {"action": "register_anchor", "runtime_generation": 1})
|
||||
self.session_id = registered["session_id"]
|
||||
self.environment = {
|
||||
**os.environ,
|
||||
"MOSAIC_LEASE_BROKER_SOCKET": str(self.socket),
|
||||
"MOSAIC_LEASE_SESSION_ID": self.session_id,
|
||||
"MOSAIC_RUNTIME_GENERATION": "1",
|
||||
"MOSAIC_LEASE_RUNTIME": "claude",
|
||||
}
|
||||
# This private path is only a gate-classifier identity; if a regression
|
||||
# blesses a payload, bash invokes no installed/live recovery command.
|
||||
self.recovery_path = self.root / "recover-context.py"
|
||||
self.canonical = (
|
||||
f"python3 {self.recovery_path} begin "
|
||||
f"--construction {self.root / 'construction.json'} --compaction-epoch 0 --request-epoch 0"
|
||||
)
|
||||
|
||||
def tearDown(self) -> None:
|
||||
if self.daemon.poll() is None:
|
||||
self.daemon.terminate()
|
||||
try:
|
||||
self.daemon.wait(timeout=3.0)
|
||||
except subprocess.TimeoutExpired:
|
||||
self.daemon.kill()
|
||||
self.daemon.wait()
|
||||
if self.daemon.stdout is not None:
|
||||
self.daemon.stdout.close()
|
||||
self.temporary.cleanup()
|
||||
|
||||
def gate(
|
||||
self, command: str, recovery_command: Path | str | None = None
|
||||
) -> subprocess.CompletedProcess[str]:
|
||||
configured_recovery = self.recovery_path if recovery_command is None else recovery_command
|
||||
return subprocess.run(
|
||||
[
|
||||
sys.executable,
|
||||
"-I",
|
||||
"-S",
|
||||
"-B",
|
||||
str(GATE),
|
||||
"--runtime",
|
||||
"claude",
|
||||
"--recovery-command",
|
||||
str(configured_recovery),
|
||||
],
|
||||
input=json.dumps({"tool_name": "Bash", "tool_input": {"command": command}}),
|
||||
text=True,
|
||||
capture_output=True,
|
||||
env=self.environment,
|
||||
check=False,
|
||||
)
|
||||
|
||||
def command_for(self, position: str, value: str) -> str:
|
||||
argv = [
|
||||
"python3",
|
||||
str(self.recovery_path),
|
||||
"begin",
|
||||
"--construction",
|
||||
str(self.root / "construction.json"),
|
||||
"--compaction-epoch",
|
||||
"0",
|
||||
"--request-epoch",
|
||||
"0",
|
||||
]
|
||||
positions = {
|
||||
"executable": 0,
|
||||
"path": 1,
|
||||
"phase": 2,
|
||||
"construction_flag": 3,
|
||||
"construction": 4,
|
||||
"compaction_epoch_flag": 5,
|
||||
"compaction_epoch": 6,
|
||||
"request_epoch_flag": 7,
|
||||
"request_epoch": 8,
|
||||
}
|
||||
try:
|
||||
argv[positions[position]] = value
|
||||
except KeyError as exc:
|
||||
raise AssertionError(f"unknown argv position {position}") from exc
|
||||
return " ".join(argv)
|
||||
|
||||
def test_canonical_literal_and_shipped_skill_invocation_are_ungated(self) -> None:
|
||||
self.assertEqual(self.gate(self.canonical).returncode, 0)
|
||||
source = SKILL.read_text(encoding="utf-8")
|
||||
recovery_placeholder = "/absolute/path/to/mosaic/tools/lease-broker/recover-context.py"
|
||||
construction_placeholder = "/absolute/path/to/mosaic-context-refresh-construction.json"
|
||||
self.assertIn(recovery_placeholder, source)
|
||||
self.assertIn(construction_placeholder, source)
|
||||
resolved_recovery = "/opt/mosaic/tools/lease-broker/recover-context.py"
|
||||
rendered = source.replace(recovery_placeholder, resolved_recovery).replace(
|
||||
construction_placeholder, "/opt/mosaic/recovery/construction.json"
|
||||
)
|
||||
match = re.search(r"```bash\s*\n\s*(.*?)\n\s*```", rendered, flags=re.DOTALL)
|
||||
self.assertIsNotNone(match)
|
||||
shipped = match.group(1) if match is not None else ""
|
||||
self.assertEqual(self.gate(shipped, resolved_recovery).returncode, 0)
|
||||
|
||||
def test_every_shell_active_vector_in_every_argv_position_falls_through_to_bash_deny(self) -> None:
|
||||
positions = (
|
||||
"executable",
|
||||
"path",
|
||||
"phase",
|
||||
"construction_flag",
|
||||
"construction",
|
||||
"compaction_epoch_flag",
|
||||
"compaction_epoch",
|
||||
"request_epoch_flag",
|
||||
"request_epoch",
|
||||
)
|
||||
marker = self.root / "PWNED"
|
||||
marker_vector = f"$(touch${{IFS}}{marker})"
|
||||
vectors = {
|
||||
"command-substitution": marker_vector,
|
||||
"backtick": f"`touch${{IFS}}{marker}`",
|
||||
"process-substitution": f"<(touch${{IFS}}{marker})",
|
||||
"parameter-expansion": "${IFS}",
|
||||
"home-expansion": "${HOME}",
|
||||
"arithmetic-expansion": "$((1+1))",
|
||||
"brace-expansion": "{a,b}",
|
||||
"tilde-expansion": "~",
|
||||
"glob": "*",
|
||||
"redirection": ">",
|
||||
"semicolon": f";touch${{IFS}}{marker}",
|
||||
"and": f"&&touch${{IFS}}{marker}",
|
||||
"pipe": f"|touch${{IFS}}{marker}",
|
||||
"embedded-newline": "literal\nnext",
|
||||
"quoting-trick": "'literal'",
|
||||
}
|
||||
for position in positions:
|
||||
for kind, vector in vectors.items():
|
||||
with self.subTest(position=position, kind=kind):
|
||||
marker.unlink(missing_ok=True)
|
||||
command = self.command_for(position, vector)
|
||||
gated = self.gate(command)
|
||||
if gated.returncode == 0 and kind in {"command-substitution", "backtick", "process-substitution", "semicolon", "and", "pipe"}:
|
||||
subprocess.run(["bash", "-c", command], cwd=self.root, env=self.environment, check=False)
|
||||
self.assertEqual(gated.returncode, 2, f"unexpected recovery exemption: {command!r}")
|
||||
self.assertFalse(marker.exists(), f"shell payload executed: {command!r}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
282
packages/mosaic/src/lease-broker/recovery_runtime_unittest.py
Normal file
282
packages/mosaic/src/lease-broker/recovery_runtime_unittest.py
Normal file
@@ -0,0 +1,282 @@
|
||||
#!/usr/bin/env python3
|
||||
"""RED-first B1/B2 runtime-boundary contracts for constrained recovery.
|
||||
|
||||
Every runtime process in these tests is a fresh child against a private daemon
|
||||
and Unix sockets. They do not activate a live Mosaic daemon or model stream.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import socket
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import time
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
TOOLS = Path(__file__).parents[2] / "framework/tools/lease-broker"
|
||||
FRAMEWORK = Path(__file__).parents[2] / "framework"
|
||||
DAEMON = TOOLS / "daemon.py"
|
||||
GATE = TOOLS / "mutator-gate.py"
|
||||
RECOVERY = TOOLS / "recover-context.py"
|
||||
OBSERVER_CLIENT = TOOLS / "receipt-observer-client.py"
|
||||
CLAUDE_SETTINGS = FRAMEWORK / "runtime/claude/settings.json"
|
||||
PI_EXTENSION = FRAMEWORK / "runtime/pi/mosaic-extension.ts"
|
||||
|
||||
|
||||
def request(socket_path: Path, value: dict[str, object]) -> dict[str, object]:
|
||||
with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as connection:
|
||||
connection.settimeout(3.0)
|
||||
connection.connect(str(socket_path))
|
||||
connection.sendall((json.dumps(value, separators=(",", ":")) + "\n").encode())
|
||||
connection.shutdown(socket.SHUT_WR)
|
||||
response = bytearray()
|
||||
while True:
|
||||
chunk = connection.recv(4096)
|
||||
if not chunk:
|
||||
break
|
||||
response.extend(chunk)
|
||||
if not response.endswith(b"\n") or response.count(b"\n") != 1:
|
||||
raise AssertionError(f"unframed broker response: {bytes(response)!r}")
|
||||
reply = json.loads(response[:-1])
|
||||
if not isinstance(reply, dict):
|
||||
raise AssertionError("broker response is not an object")
|
||||
return reply
|
||||
|
||||
|
||||
def wait_ready(process: subprocess.Popen[str], socket_path: Path) -> None:
|
||||
deadline = time.monotonic() + 5.0
|
||||
while time.monotonic() < deadline:
|
||||
if socket_path.exists():
|
||||
return
|
||||
if process.poll() is not None:
|
||||
output = process.stdout.read() if process.stdout is not None else ""
|
||||
raise RuntimeError(f"daemon exited before READY: {output}")
|
||||
time.sleep(0.02)
|
||||
raise TimeoutError("daemon did not create private broker socket")
|
||||
|
||||
|
||||
class RuntimeBoundaryFixture(unittest.TestCase):
|
||||
def setUp(self) -> None:
|
||||
self.temporary = tempfile.TemporaryDirectory()
|
||||
self.root = Path(self.temporary.name)
|
||||
os.chmod(self.root, 0o700)
|
||||
self.socket = self.root / "broker.sock"
|
||||
self.observer_socket = self.root / "observer.sock"
|
||||
self.state = self.root / "state.json"
|
||||
self.children: list[subprocess.Popen[str]] = []
|
||||
|
||||
def tearDown(self) -> None:
|
||||
for child in self.children:
|
||||
if child.poll() is None:
|
||||
child.terminate()
|
||||
try:
|
||||
child.wait(timeout=3.0)
|
||||
except subprocess.TimeoutExpired:
|
||||
child.kill()
|
||||
child.wait()
|
||||
if child.stdout is not None:
|
||||
child.stdout.close()
|
||||
self.temporary.cleanup()
|
||||
|
||||
def start_daemon(self, *, production_observer: bool) -> None:
|
||||
arguments = [sys.executable, "-I", "-S", "-B", str(DAEMON), "--socket", str(self.socket), "--state", str(self.state)]
|
||||
if production_observer:
|
||||
arguments.extend(["--observer-socket", str(self.observer_socket)])
|
||||
process = subprocess.Popen(
|
||||
arguments,
|
||||
stdin=subprocess.DEVNULL,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
text=True,
|
||||
)
|
||||
self.children.append(process)
|
||||
wait_ready(process, self.socket)
|
||||
|
||||
def register(self) -> str:
|
||||
reply = request(self.socket, {"action": "register_anchor", "runtime_generation": 1})
|
||||
session_id = reply.get("session_id")
|
||||
self.assertTrue(reply.get("ok"))
|
||||
self.assertIsInstance(session_id, str)
|
||||
return session_id
|
||||
|
||||
def environment(self, session_id: str) -> dict[str, str]:
|
||||
return {
|
||||
**os.environ,
|
||||
"MOSAIC_LEASE_BROKER_SOCKET": str(self.socket),
|
||||
"MOSAIC_RECEIPT_OBSERVER_SOCKET": str(self.observer_socket),
|
||||
"MOSAIC_LEASE_SESSION_ID": session_id,
|
||||
"MOSAIC_RUNTIME_GENERATION": "1",
|
||||
"MOSAIC_LEASE_RUNTIME": "pi",
|
||||
}
|
||||
|
||||
def construction(self) -> Path:
|
||||
content = b"WI-6 B2 production observer fixture\n"
|
||||
path = self.root / "construction.json"
|
||||
path.write_text(json.dumps({
|
||||
"manifest_version": 1,
|
||||
"generator_version": "wi6-repair-runtime-boundary",
|
||||
"fragments": [{
|
||||
"source_id": "authority/wi6-repair",
|
||||
"content_base64": base64.b64encode(content).decode("ascii"),
|
||||
"expected_sha256": hashlib.sha256(content).hexdigest(),
|
||||
}],
|
||||
}), encoding="utf-8")
|
||||
os.chmod(path, 0o600)
|
||||
return path
|
||||
|
||||
|
||||
class RecoveryRuntimeBoundaryTest(RuntimeBoundaryFixture):
|
||||
def test_b1_claude_exact_recovery_command_is_invocable_unverified_but_bash_is_not(self) -> None:
|
||||
self.start_daemon(production_observer=False)
|
||||
session_id = self.register()
|
||||
environment = self.environment(session_id)
|
||||
recovery_command = f"python3 {RECOVERY} complete"
|
||||
mapped = subprocess.run(
|
||||
[sys.executable, "-I", "-S", "-B", str(GATE), "--runtime", "claude", "--recovery-command", str(RECOVERY)],
|
||||
input=json.dumps({"tool_name": "Bash", "tool_input": {"command": recovery_command}}),
|
||||
text=True,
|
||||
capture_output=True,
|
||||
env={**environment, "MOSAIC_LEASE_RUNTIME": "claude"},
|
||||
check=False,
|
||||
)
|
||||
self.assertEqual(mapped.returncode, 0, mapped.stderr)
|
||||
mapped_begin = subprocess.run(
|
||||
[sys.executable, "-I", "-S", "-B", str(GATE), "--runtime", "claude", "--recovery-command", str(RECOVERY)],
|
||||
input=json.dumps({
|
||||
"tool_name": "Bash",
|
||||
"tool_input": {
|
||||
"command": (
|
||||
f"python3 {RECOVERY} begin --construction /tmp/construction.json "
|
||||
"--compaction-epoch 1 --request-epoch 1"
|
||||
)
|
||||
},
|
||||
}),
|
||||
text=True,
|
||||
capture_output=True,
|
||||
env={**environment, "MOSAIC_LEASE_RUNTIME": "claude"},
|
||||
check=False,
|
||||
)
|
||||
self.assertEqual(mapped_begin.returncode, 0, mapped_begin.stderr)
|
||||
ordinary_bash = subprocess.run(
|
||||
[sys.executable, "-I", "-S", "-B", str(GATE), "--runtime", "claude", "--recovery-command", str(RECOVERY)],
|
||||
input=json.dumps({"tool_name": "Bash", "tool_input": {"command": "echo not-recovery"}}),
|
||||
text=True,
|
||||
capture_output=True,
|
||||
env={**environment, "MOSAIC_LEASE_RUNTIME": "claude"},
|
||||
check=False,
|
||||
)
|
||||
self.assertEqual(ordinary_bash.returncode, 2)
|
||||
|
||||
def test_b1_pi_registers_only_the_broker_exempt_recovery_tool(self) -> None:
|
||||
extension = PI_EXTENSION.read_text(encoding="utf-8")
|
||||
self.assertIn("const RECOVERY_TOOL = 'mosaic_context_recover'", extension)
|
||||
self.assertIn("name: RECOVERY_TOOL", extension)
|
||||
self.assertIn("checkPiMutatorGate(RECOVERY_TOOL)", extension)
|
||||
self.assertNotIn("toolName === 'bash' ? RECOVERY_TOOL", extension)
|
||||
|
||||
def test_b2_production_observer_promotes_over_private_transport_and_rejects_broker_supplied_message(self) -> None:
|
||||
self.start_daemon(production_observer=True)
|
||||
session_id = self.register()
|
||||
environment = self.environment(session_id)
|
||||
begin = subprocess.run(
|
||||
[sys.executable, "-I", "-S", "-B", str(RECOVERY), "begin", "--construction", str(self.construction()), "--compaction-epoch", "1", "--request-epoch", "1"],
|
||||
text=True,
|
||||
capture_output=True,
|
||||
env=environment,
|
||||
check=False,
|
||||
)
|
||||
self.assertEqual(begin.returncode, 0, begin.stderr)
|
||||
cycle = json.loads(begin.stdout)
|
||||
receipt = cycle.get("receipt")
|
||||
self.assertIsInstance(receipt, str)
|
||||
|
||||
# S1: production transport is not a broker request field. The public
|
||||
# broker endpoint keeps rejecting caller-supplied assistant evidence.
|
||||
rejected_begin = request(self.socket, {
|
||||
"action": "begin_recovery",
|
||||
"session_id": session_id,
|
||||
"runtime_generation": 1,
|
||||
"receipt": receipt,
|
||||
})
|
||||
self.assertEqual(rejected_begin, {"ok": False, "code": "INVALID_RECOVERY_REQUEST"})
|
||||
rejected_observe = request(self.socket, {
|
||||
"action": "observe_receipt",
|
||||
"session_id": session_id,
|
||||
"runtime_generation": 1,
|
||||
"receipt_challenge": cycle["receipt_challenge"],
|
||||
"latest_assistant_message": receipt,
|
||||
})
|
||||
self.assertEqual(rejected_observe, {"ok": False, "code": "INVALID_RECEIPT"})
|
||||
rejected_complete = request(self.socket, {
|
||||
"action": "complete_recovery",
|
||||
"session_id": session_id,
|
||||
"runtime_generation": 1,
|
||||
"latest_assistant_message": receipt,
|
||||
})
|
||||
self.assertEqual(rejected_complete, {"ok": False, "code": "INVALID_RECOVERY_REQUEST"})
|
||||
|
||||
recorded = subprocess.run(
|
||||
[sys.executable, "-I", "-S", "-B", str(OBSERVER_CLIENT), "--runtime", "pi"],
|
||||
input=json.dumps({"latest_assistant_message": receipt}),
|
||||
text=True,
|
||||
capture_output=True,
|
||||
env=environment,
|
||||
check=False,
|
||||
)
|
||||
self.assertEqual(recorded.returncode, 0, recorded.stderr)
|
||||
complete = subprocess.run(
|
||||
[sys.executable, "-I", "-S", "-B", str(RECOVERY), "complete"],
|
||||
text=True,
|
||||
capture_output=True,
|
||||
env=environment,
|
||||
check=False,
|
||||
)
|
||||
self.assertEqual(complete.returncode, 0, complete.stderr)
|
||||
self.assertEqual(json.loads(complete.stdout).get("state"), "VERIFIED")
|
||||
|
||||
# The independent Claude transport selects one latest assistant entry
|
||||
# from its hook transcript; it is not a Pi/message_end fallback.
|
||||
claude_environment = {**environment, "MOSAIC_LEASE_RUNTIME": "claude"}
|
||||
claude_begin = subprocess.run(
|
||||
[sys.executable, "-I", "-S", "-B", str(RECOVERY), "begin", "--construction", str(self.construction()), "--compaction-epoch", "2", "--request-epoch", "2"],
|
||||
text=True,
|
||||
capture_output=True,
|
||||
env=claude_environment,
|
||||
check=False,
|
||||
)
|
||||
self.assertEqual(claude_begin.returncode, 0, claude_begin.stderr)
|
||||
claude_receipt = json.loads(claude_begin.stdout)["receipt"]
|
||||
transcript = self.root / "claude-transcript.jsonl"
|
||||
transcript.write_text(
|
||||
json.dumps({"message": {"role": "assistant", "content": claude_receipt}}) + "\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
claude_recorded = subprocess.run(
|
||||
[sys.executable, "-I", "-S", "-B", str(OBSERVER_CLIENT), "--runtime", "claude", "--latest-entry"],
|
||||
input=json.dumps({"transcript_path": str(transcript)}),
|
||||
text=True,
|
||||
capture_output=True,
|
||||
env=claude_environment,
|
||||
check=False,
|
||||
)
|
||||
self.assertEqual(claude_recorded.returncode, 0, claude_recorded.stderr)
|
||||
claude_complete = subprocess.run(
|
||||
[sys.executable, "-I", "-S", "-B", str(RECOVERY), "complete"],
|
||||
text=True,
|
||||
capture_output=True,
|
||||
env=claude_environment,
|
||||
check=False,
|
||||
)
|
||||
self.assertEqual(claude_complete.returncode, 0, claude_complete.stderr)
|
||||
self.assertEqual(json.loads(claude_complete.stdout).get("state"), "VERIFIED")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user