fix(mosaic): make recovery skill portable
Some checks failed
ci/woodpecker/pr/ci Pipeline failed

This commit is contained in:
wjarvis mos-comms
2026-07-19 21:08:45 -05:00
parent bfc904b3c2
commit 3277faadec
3 changed files with 23 additions and 21 deletions

View File

@@ -16,11 +16,12 @@ mutator remains behind the verified lease gate.
- **Claude:** invoke only this direct command shape (no shell composition):
```bash
python3 /home/hermes/.config/mosaic/tools/lease-broker/recover-context.py begin --construction /absolute/path/to/mosaic-context-refresh-construction.json --compaction-epoch 0 --request-epoch 0
python3 /absolute/path/to/mosaic/tools/lease-broker/recover-context.py begin --construction /absolute/path/to/mosaic-context-refresh-construction.json --compaction-epoch 0 --request-epoch 0
```
This is a literal argv template: replace the construction path with a literal absolute path and
replace each epoch with literal decimal digits. Do not use variables, quoting, globs, redirects,
This is a literal argv template: replace the recover-context.py path and construction JSON path
with the literal absolute paths for your install, then replace each epoch with literal decimal
digits. Do not use variables, quoting, globs, redirects,
shell operators, substitutions, or line continuations. Claude's all-tools gate maps only this
fully literal recovery shape to `mosaic_context_recover`; ordinary `Bash` remains gated.
@@ -40,7 +41,7 @@ mutator remains behind the verified lease gate.
`after_provider_response`).
Then invoke completion with the same adapter form: Claude runs
`python3 /home/hermes/.config/mosaic/tools/lease-broker/recover-context.py complete`; Pi calls
`python3 /absolute/path/to/mosaic/tools/lease-broker/recover-context.py complete`; Pi calls
`mosaic_context_recover` with `phase: "complete"`. Completion supplies no receipt or challenge
argument. The broker observes the exact latest assistant entry, commits evidence, consumes its own
fresh challenge, and promotes VERIFIED last. If observation is absent, malformed, stale, or

View File

@@ -45,12 +45,12 @@ class FrameworkSkillPortabilityTest(unittest.TestCase):
self.assertIn(CONSTRUCTION_PLACEHOLDER, source)
resolved_recovery = "/opt/mosaic/tools/lease-broker/recover-context.py"
resolved_construction = "/opt/mosaic/recovery/construction.json"
command = (
source.replace(RECOVERY_PLACEHOLDER, resolved_recovery)
.replace(CONSTRUCTION_PLACEHOLDER, resolved_construction)
.split("```bash\n", 1)[1]
.split("\n```", 1)[0]
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}},

View File

@@ -10,6 +10,7 @@ from __future__ import annotations
import json
import os
import re
import socket
import subprocess
import sys
@@ -156,19 +157,19 @@ class ClaudeRecoveryGateAdversarialTest(unittest.TestCase):
def test_canonical_literal_and_shipped_skill_invocation_are_ungated(self) -> None:
self.assertEqual(self.gate(self.canonical).returncode, 0)
shipped = (
"python3 /home/hermes/.config/mosaic/tools/lease-broker/recover-context.py begin "
"--construction /absolute/path/to/mosaic-context-refresh-construction.json "
"--compaction-epoch 0 --request-epoch 0"
)
self.assertIn(shipped, SKILL.read_text(encoding="utf-8"))
self.assertEqual(
self.gate(
shipped,
"/home/hermes/.config/mosaic/tools/lease-broker/recover-context.py",
).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 = (