personal memory agent
0

Configure Feed

Select the types of activity you want to include in your feed.

fix(sandbox): reconcile recorded sandbox-profile state and close the code vocabulary

status now reconciles recorded intent metadata against live state for scout, spl, spb, and spp, making the previously unreachable fingerprint and identity residuals genuinely reachable while staying read-only and network-free.

SPB instance_id mismatch surfaces the named spb_instance_mismatch error code instead of collapsing into generic payload_invalid.

A malformed same-run intent is rejected as intent_malformed before any prepare side effect.

Envelope serialization enforces closed capability names and residual codes, failing loudly on an undeclared value.

Removed unused supported_contract_payload(); disable capability help derives from manifest.APPLY_CAPABILITIES so the manifest stays the single source.

Widened SPB fault injection to each mutation boundary.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

+340 -23
+96 -4
solstone/think/sandbox_profile/capabilities.py
··· 5 5 6 6 from __future__ import annotations 7 7 8 + import hashlib 8 9 import json 9 10 import logging 10 11 from pathlib import Path ··· 19 20 ) 20 21 from solstone.think.link.ca import load_or_generate_ca 21 22 from solstone.think.link.paths import LinkState, ca_dir 23 + from solstone.think.providers.local_endpoint import ( 24 + confidential_fingerprint_provenance_block, 25 + ) 22 26 from solstone.think.sandbox_profile import envelope, intent, manifest 23 27 from solstone.think.services import scout, spl, spp 24 28 from solstone.think.services.spb_handoff import _BINDING_FIELDS, _binding_from_payload ··· 37 41 38 42 39 43 class PayloadValidationError(ValueError): 40 - def __init__(self, message: str) -> None: 44 + def __init__(self, message: str, *, error_code: str = "payload_invalid") -> None: 41 45 super().__init__(message) 42 46 self.message = message 47 + self.error_code = error_code 43 48 44 49 45 50 def _read_json_file(path: Path) -> Any: ··· 110 115 return state if isinstance(state, str) else None 111 116 112 117 118 + def _observed_at_apply( 119 + intent_payload: dict[str, Any] | None, name: str 120 + ) -> dict[str, Any]: 121 + if not isinstance(intent_payload, dict): 122 + return {} 123 + observed = intent_payload.get("observed_at_apply") 124 + if not isinstance(observed, dict): 125 + return {} 126 + block = observed.get(name) 127 + return block if isinstance(block, dict) else {} 128 + 129 + 130 + def _observed_string( 131 + intent_payload: dict[str, Any] | None, name: str, field: str 132 + ) -> str | None: 133 + value = _observed_at_apply(intent_payload, name).get(field) 134 + return value if isinstance(value, str) and value else None 135 + 136 + 113 137 def _is_applied_intent(state: str | None) -> bool: 114 138 return state in {intent.INTENT_APPLIED, intent.INTENT_APPLY_STARTED} 115 139 ··· 120 144 121 145 def _cap(name: str, state: str, *residuals: str) -> envelope.CapabilityEnvelope: 122 146 return envelope.CapabilityEnvelope(name, state, tuple(residuals)) 147 + 148 + 149 + def _secret_sha256(value: str) -> str: 150 + # scout.KEY_FINGERPRINT_FIELD is written from this same UTF-8 SHA-256 input. 151 + return hashlib.sha256(value.encode("utf-8")).hexdigest() 123 152 124 153 125 154 def _validate_exact_fields( ··· 173 202 if not isinstance(instance_id, str) or not instance_id: 174 203 raise PayloadValidationError("spb requires prepared runtime identity") 175 204 if payload.get("instance_id") != instance_id: 176 - raise PayloadValidationError("spb payload instance_id does not match runtime") 205 + raise PayloadValidationError( 206 + "spb payload field instance_id does not match runtime", 207 + error_code="spb_instance_mismatch", 208 + ) 177 209 try: 178 210 _binding_from_payload(payload) 179 211 except ValueError: ··· 232 264 state = _cap_intent_state(intent_payload, manifest.CAPABILITY_SCOUT) 233 265 block = config.get("services", {}).get("scout") 234 266 applied = isinstance(block, dict) and block.get("state") != "pending" 235 - has_key = bool(config.get("env", {}).get("GOOGLE_API_KEY")) 267 + key = config.get("env", {}).get("GOOGLE_API_KEY") 268 + has_key = isinstance(key, str) and bool(key) 236 269 complete = applied and has_key 237 270 if complete and state is None: 238 271 return _cap( ··· 243 276 manifest.CAPABILITY_SCOUT, envelope.CAP_DEGRADED, "intent_finalize_missing" 244 277 ) 245 278 if complete: 279 + live_fingerprint = _secret_sha256(key) 280 + stored_fingerprint = block.get(scout.KEY_FINGERPRINT_FIELD) 281 + recorded_fingerprint = _observed_string( 282 + intent_payload, manifest.CAPABILITY_SCOUT, scout.KEY_FINGERPRINT_FIELD 283 + ) 284 + expected = recorded_fingerprint or ( 285 + stored_fingerprint if isinstance(stored_fingerprint, str) else None 286 + ) 287 + if expected is not None and ( 288 + live_fingerprint != expected or stored_fingerprint != expected 289 + ): 290 + return _cap( 291 + manifest.CAPABILITY_SCOUT, 292 + envelope.CAP_DEGRADED, 293 + "scout_key_fingerprint_mismatch", 294 + ) 246 295 return _cap(manifest.CAPABILITY_SCOUT, envelope.CAP_READY) 247 296 if _is_disabled_intent(state) or state in {None, intent.INTENT_PREPARED}: 248 297 return _cap(manifest.CAPABILITY_SCOUT, envelope.CAP_NOT_APPLIED) ··· 260 309 ) -> envelope.CapabilityEnvelope: 261 310 state = _cap_intent_state(intent_payload, manifest.CAPABILITY_SPL) 262 311 posture = _posture(config) 312 + link_state = _read_link_state(journal_path) 263 313 token = _read_service_token(journal_path) 264 314 complete = posture == "spl" and token is not None 265 315 if complete and state is None: ··· 271 321 manifest.CAPABILITY_SPL, envelope.CAP_DEGRADED, "intent_finalize_missing" 272 322 ) 273 323 if complete: 324 + recorded_instance_id = _observed_string( 325 + intent_payload, manifest.CAPABILITY_SPL, "instance_id" 326 + ) 327 + live_instance_id = ( 328 + link_state.get("instance_id") if isinstance(link_state, dict) else None 329 + ) 330 + if ( 331 + recorded_instance_id is not None 332 + and live_instance_id != recorded_instance_id 333 + ): 334 + return _cap( 335 + manifest.CAPABILITY_SPL, 336 + envelope.CAP_DEGRADED, 337 + "spl_identity_missing", 338 + ) 274 339 return _cap(manifest.CAPABILITY_SPL, envelope.CAP_READY) 275 340 if posture == "spl" and token is None: 276 341 return _cap(manifest.CAPABILITY_SPL, envelope.CAP_DEGRADED, "spl_token_missing") ··· 304 369 return _cap( 305 370 manifest.CAPABILITY_SPB, envelope.CAP_DEGRADED, "intent_finalize_missing" 306 371 ) 372 + recorded_instance_id = _observed_string( 373 + intent_payload, manifest.CAPABILITY_SPB, "instance_id" 374 + ) 375 + live_instance_id = binding.get("instance_id") if isinstance(binding, dict) else None 376 + if ( 377 + binding is not None 378 + and recorded_instance_id is not None 379 + and live_instance_id != recorded_instance_id 380 + ): 381 + return _cap( 382 + manifest.CAPABILITY_SPB, envelope.CAP_DEGRADED, "spb_instance_mismatch" 383 + ) 307 384 if complete: 308 385 return _cap(manifest.CAPABILITY_SPB, envelope.CAP_READY) 309 386 if binding is None and _is_applied_intent(state): ··· 329 406 config: dict[str, Any], intent_payload: dict[str, Any] | None 330 407 ) -> envelope.CapabilityEnvelope: 331 408 state = _cap_intent_state(intent_payload, manifest.CAPABILITY_SPP) 332 - block = config.get("services", {}).get("confidential") 409 + block = confidential_fingerprint_provenance_block(config) 333 410 local = config.get("providers", {}).get("local", {}) 334 411 credential = local.get("credential") if isinstance(local, dict) else None 335 412 complete = isinstance(block, dict) and bool(credential) ··· 342 419 manifest.CAPABILITY_SPP, envelope.CAP_DEGRADED, "intent_finalize_missing" 343 420 ) 344 421 if complete: 422 + recorded_fingerprint = _observed_string( 423 + intent_payload, 424 + manifest.CAPABILITY_SPP, 425 + spp.CREDENTIAL_FINGERPRINT_FIELD, 426 + ) 427 + live_fingerprint = block.get(spp.CREDENTIAL_FINGERPRINT_FIELD) 428 + if ( 429 + recorded_fingerprint is not None 430 + and live_fingerprint != recorded_fingerprint 431 + ): 432 + return _cap( 433 + manifest.CAPABILITY_SPP, 434 + envelope.CAP_DEGRADED, 435 + "spp_credential_fingerprint_mismatch", 436 + ) 345 437 return _cap(manifest.CAPABILITY_SPP, envelope.CAP_READY) 346 438 if _is_disabled_intent(state) or state in {None, intent.INTENT_PREPARED}: 347 439 return _cap(manifest.CAPABILITY_SPP, envelope.CAP_NOT_APPLIED)
+40 -5
solstone/think/sandbox_profile/cli.py
··· 72 72 73 73 74 74 def _supported_disable_action() -> tuple[str, ...]: 75 - return ("Supported disable capabilities: all, scout, spl, spb, spp.",) 75 + return ( 76 + f"Supported disable capabilities: all, {', '.join(manifest.APPLY_CAPABILITIES)}.", 77 + ) 76 78 77 79 78 80 def _emit(result: envelope.Envelope, *, json_output: bool) -> NoReturn: ··· 161 163 ) 162 164 except intent.IntentError: 163 165 return _error( 164 - action=action, code="internal_error", message="intent is unreadable" 166 + action=action, 167 + code="intent_malformed", 168 + message="sandbox profile intent is malformed", 169 + run_id=ctx.run_id, 165 170 ) 166 171 caps = capabilities.observe_capabilities(ctx.journal_path, intent_payload) 167 172 return envelope.Envelope( ··· 328 333 ), 329 334 json_output=json_output, 330 335 ) 336 + except intent.IntentError: 337 + _emit( 338 + _error( 339 + action=action, 340 + code="intent_malformed", 341 + message="sandbox profile intent is malformed", 342 + run_id=ctx.run_id, 343 + ), 344 + json_output=json_output, 345 + ) 331 346 except Exception: 332 347 log.debug("sandbox profile prepare failed without payload details") 333 348 _emit( ··· 409 424 _emit( 410 425 _error( 411 426 action=action, 412 - code="internal_error", 413 - message="intent is unreadable", 427 + code="intent_malformed", 428 + message="sandbox profile intent is malformed", 414 429 run_id=ctx.run_id, 415 430 ), 416 431 json_output=json_output, ··· 423 438 capability, 424 439 payload, 425 440 ) 426 - except (PayloadReadError, capabilities.PayloadValidationError): 441 + except PayloadReadError: 427 442 _emit( 428 443 _error( 429 444 action=action, 430 445 code="payload_invalid", 431 446 message="payload is invalid", 447 + run_id=ctx.run_id, 448 + ), 449 + json_output=json_output, 450 + ) 451 + except capabilities.PayloadValidationError as exc: 452 + _emit( 453 + _error( 454 + action=action, 455 + code=exc.error_code, 456 + message=exc.message, 432 457 run_id=ctx.run_id, 433 458 ), 434 459 json_output=json_output, ··· 534 559 action=action, 535 560 code="intent_run_mismatch", 536 561 message="sandbox profile intent belongs to a different run", 562 + run_id=ctx.run_id, 563 + ), 564 + json_output=json_output, 565 + ) 566 + except intent.IntentError: 567 + _emit( 568 + _error( 569 + action=action, 570 + code="intent_malformed", 571 + message="sandbox profile intent is malformed", 537 572 run_id=ctx.run_id, 538 573 ), 539 574 json_output=json_output,
+9
solstone/think/sandbox_profile/envelope.py
··· 68 68 "sandbox_marker_bad_run_id", 69 69 "sandbox_marker_path_mismatch", 70 70 "intent_missing", 71 + "intent_malformed", 71 72 "intent_run_mismatch", 72 73 "payload_invalid", 74 + "spb_instance_mismatch", 73 75 "unknown_capability", 74 76 "unsupported_capability_action", 75 77 "internal_error", ··· 88 90 "sandbox_marker_bad_run_id": "Use a canonical UUID run_id.", 89 91 "sandbox_marker_path_mismatch": "Point SOLSTONE_JOURNAL at the marker's canonical journal path.", 90 92 "intent_missing": "Run prepare first.", 93 + "intent_malformed": "Use the owning run or create a fresh sandbox.", 91 94 "intent_run_mismatch": "Use the owning run or create a fresh sandbox.", 92 95 "payload_invalid": "Send one valid JSON object on stdin for the selected capability.", 96 + "spb_instance_mismatch": "Send a hosted backup binding for the prepared runtime instance_id.", 93 97 "unknown_capability": "Use one of the supported capabilities.", 94 98 "unsupported_capability_action": "Use one of the supported capabilities for this action.", 95 99 "internal_error": "Inspect logs and retry in a fresh sandbox.", ··· 103 107 residuals: tuple[str, ...] = () 104 108 105 109 def to_json(self) -> dict[str, object]: 110 + if self.name not in manifest.CAPABILITY_ORDER: 111 + raise ValueError(f"unsupported capability name: {self.name!r}") 106 112 if self.state not in CAPABILITY_STATES: 107 113 raise ValueError(f"unsupported capability state: {self.state!r}") 114 + for residual in self.residuals: 115 + if residual not in RESIDUAL_CODES: 116 + raise ValueError(f"unsupported residual code: {residual!r}") 108 117 return { 109 118 "name": self.name, 110 119 "state": self.state,
+5 -4
solstone/think/sandbox_profile/intent.py
··· 97 97 payload = load_intent(journal_path) 98 98 if payload is None: 99 99 raise FileNotFoundError("sandbox profile intent is missing") 100 + return _validate_intent_payload(payload, run_id) 101 + 102 + 103 + def _validate_intent_payload(payload: dict[str, Any], run_id: str) -> dict[str, Any]: 100 104 existing = payload.get("run_id") 101 105 if existing != run_id: 102 106 raise IntentRunMismatch(str(existing) if isinstance(existing, str) else "") ··· 112 116 def ensure_prepared(journal_path: str | Path, run_id: str) -> dict[str, Any]: 113 117 existing = load_intent(journal_path) 114 118 if existing is not None: 115 - if existing.get("run_id") != run_id: 116 - prior = existing.get("run_id") 117 - raise IntentRunMismatch(str(prior) if isinstance(prior, str) else "") 118 - return existing 119 + return _validate_intent_payload(existing, run_id) 119 120 payload = _default_intent(run_id) 120 121 _write(intent_path(journal_path), payload) 121 122 return payload
-8
solstone/think/sandbox_profile/manifest.py
··· 67 67 journal_name=f"Synthetic sandbox {slug}", 68 68 home_label=f"sandbox-{slug}", 69 69 ) 70 - 71 - 72 - def supported_contract_payload() -> dict[str, object]: 73 - return { 74 - "profile": PROFILE, 75 - "contract_version": CONTRACT_VERSION, 76 - "capabilities": list(CAPABILITY_ORDER), 77 - }
+50
tests/sandbox_profile/test_apply.py
··· 93 93 assert status.exit_code == 1 94 94 assert cap_status["state"] == "degraded" 95 95 assert residual in cap_status["residuals"] 96 + 97 + 98 + @pytest.mark.parametrize( 99 + ("fail_on_replace", "residual"), 100 + [ 101 + (2, "spb_binding_missing"), 102 + (3, "spb_binding_missing"), 103 + (4, "spb_binding_missing"), 104 + (5, "spb_backup_config_incomplete"), 105 + (6, "spb_backup_config_incomplete"), 106 + (7, "intent_finalize_missing"), 107 + ], 108 + ) 109 + def test_spb_apply_fault_boundaries_are_named_and_retry_converges( 110 + tmp_path, 111 + monkeypatch, 112 + fail_on_replace: int, 113 + residual: str, 114 + ) -> None: 115 + journal = sandbox_journal(tmp_path, monkeypatch) 116 + prepare_ok(journal) 117 + payload = spb_payload(journal) 118 + real_replace = __import__("os").replace 119 + calls = {"count": 0} 120 + 121 + def flaky_replace(src, dst): 122 + calls["count"] += 1 123 + if calls["count"] == fail_on_replace: 124 + raise OSError("forced commit failure") 125 + real_replace(src, dst) 126 + 127 + monkeypatch.setattr("solstone.think.journal_io.atomic.os.replace", flaky_replace) 128 + 129 + failed = invoke(["apply", "spb", "--json"], input_text=json.dumps(payload)) 130 + status = invoke(["status", "--json"]) 131 + status_body = output_json(status) 132 + spb = next(cap for cap in status_body["capabilities"] if cap["name"] == "spb") 133 + 134 + assert failed.exit_code == 2 135 + assert status.exit_code == 1 136 + assert spb["state"] == "degraded" 137 + assert residual in spb["residuals"] 138 + 139 + monkeypatch.setattr("solstone.think.journal_io.atomic.os.replace", real_replace) 140 + retry = invoke(["apply", "spb", "--json"], input_text=json.dumps(payload)) 141 + retry_body = output_json(retry) 142 + retry_spb = next(cap for cap in retry_body["capabilities"] if cap["name"] == "spb") 143 + 144 + assert retry.exit_code == 0 145 + assert retry_spb["state"] == "ready"
+14 -1
tests/sandbox_profile/test_envelope.py
··· 5 5 6 6 import json 7 7 8 - from solstone.think.sandbox_profile import envelope 8 + import pytest 9 + 10 + from solstone.think.sandbox_profile import envelope, manifest 9 11 from tests.sandbox_profile import invoke, output_json, prepare_ok, sandbox_journal 10 12 11 13 ··· 51 53 "cleanup_failed": 3, 52 54 } 53 55 assert len(set(envelope.EXIT_BY_STATE.values())) == len(envelope.EXIT_BY_STATE) 56 + 57 + 58 + def test_capability_serializer_enforces_closed_vocabulary() -> None: 59 + with pytest.raises(ValueError, match="unsupported capability name"): 60 + envelope.CapabilityEnvelope("unknown", envelope.CAP_READY).to_json() 61 + with pytest.raises(ValueError, match="unsupported residual code"): 62 + envelope.CapabilityEnvelope( 63 + manifest.CAPABILITY_SCOUT, 64 + envelope.CAP_DEGRADED, 65 + ("typo_residual",), 66 + ).to_json() 54 67 55 68 56 69 def test_cli_exit_codes_cover_ok_degraded_error_and_cleanup_failed(
+3 -1
tests/sandbox_profile/test_payloads.py
··· 62 62 body = output_json(result) 63 63 64 64 assert result.exit_code == 2 65 - assert body["error"]["code"] == "payload_invalid" 65 + assert body["error"]["code"] == "spb_instance_mismatch" 66 + assert "instance_id" in body["error"]["message"] 67 + assert "not-the-runtime-instance" not in result.output 66 68 assert not (journal / "backup" / "hosted" / "binding.json").exists() 67 69 68 70
+34
tests/sandbox_profile/test_prepare.py
··· 3 3 4 4 from __future__ import annotations 5 5 6 + import json 7 + 6 8 from solstone.think.sandbox_profile import manifest 7 9 from tests.sandbox_profile import invoke, output_json, read_json, sandbox_journal 8 10 ··· 44 46 assert body["state"] == "ok" 45 47 assert not (journal / "config" / "journal.json").exists() 46 48 assert not (journal / "health" / "sandbox-profile" / "intent.json").exists() 49 + 50 + 51 + def test_prepare_refuses_malformed_same_run_intent_before_side_effects( 52 + tmp_path, monkeypatch 53 + ) -> None: 54 + journal = sandbox_journal(tmp_path, monkeypatch) 55 + intent_path = journal / "health" / "sandbox-profile" / "intent.json" 56 + intent_path.parent.mkdir(parents=True) 57 + intent_path.write_text( 58 + json.dumps( 59 + { 60 + "kind": "wrong", 61 + "contract_version": manifest.CONTRACT_VERSION, 62 + "run_id": "86d9eb6c-d64e-4ae5-b29e-524ddf57a013", 63 + "profile": manifest.PROFILE, 64 + }, 65 + indent=2, 66 + ) 67 + + "\n", 68 + encoding="utf-8", 69 + ) 70 + before = intent_path.read_text("utf-8") 71 + 72 + result = invoke(["prepare", "--json"]) 73 + body = output_json(result) 74 + 75 + assert result.exit_code == 2 76 + assert body["error"]["code"] == "intent_malformed" 77 + assert body["run_id"] == "86d9eb6c-d64e-4ae5-b29e-524ddf57a013" 78 + assert intent_path.read_text("utf-8") == before 79 + assert not (journal / "config" / "journal.json").exists() 80 + assert not (journal / "link").exists()
+89
tests/sandbox_profile/test_status.py
··· 9 9 invoke, 10 10 output_json, 11 11 prepare_ok, 12 + read_json, 12 13 sandbox_journal, 13 14 scout_payload, 15 + spb_payload, 16 + spl_payload, 17 + spp_payload, 14 18 ) 15 19 16 20 ··· 67 71 assert result.exit_code == 2 68 72 assert body["run_id"] == "86d9eb6c-d64e-4ae5-b29e-524ddf57a013" 69 73 assert body["error"]["code"] == "intent_run_mismatch" 74 + 75 + 76 + def _capability(body: dict[str, object], name: str) -> dict[str, object]: 77 + capabilities = body["capabilities"] 78 + assert isinstance(capabilities, list) 79 + return next(cap for cap in capabilities if cap["name"] == name) 80 + 81 + 82 + def _assert_degraded_residual(result, name: str, residual: str) -> None: 83 + body = output_json(result) 84 + cap = _capability(body, name) 85 + assert result.exit_code == 1 86 + assert body["state"] == "degraded" 87 + assert cap["state"] == "degraded" 88 + assert residual in cap["residuals"] 89 + 90 + 91 + def test_status_reconciles_scout_key_fingerprint(tmp_path, monkeypatch) -> None: 92 + journal = sandbox_journal(tmp_path, monkeypatch) 93 + prepare_ok(journal) 94 + result = invoke( 95 + ["apply", "scout", "--json"], input_text=json.dumps(scout_payload()) 96 + ) 97 + assert result.exit_code == 0 98 + config_path = journal / "config" / "journal.json" 99 + config = read_json(config_path) 100 + config["env"]["GOOGLE_API_KEY"] = "different-google-key" 101 + config_path.write_text(json.dumps(config, indent=2) + "\n", encoding="utf-8") 102 + 103 + status = invoke(["status", "--json"]) 104 + 105 + _assert_degraded_residual(status, "scout", "scout_key_fingerprint_mismatch") 106 + 107 + 108 + def test_status_reconciles_spl_identity(tmp_path, monkeypatch) -> None: 109 + journal = sandbox_journal(tmp_path, monkeypatch) 110 + prepare_ok(journal) 111 + monkeypatch.setattr( 112 + "solstone.think.services.spl.enroll_home", 113 + lambda *args, **kwargs: "service-token", 114 + ) 115 + result = invoke(["apply", "spl", "--json"], input_text=json.dumps(spl_payload())) 116 + assert result.exit_code == 0 117 + state_path = journal / "link" / "state.json" 118 + state = read_json(state_path) 119 + state["instance_id"] = "did:key:mismatched" 120 + state_path.write_text(json.dumps(state, indent=2) + "\n", encoding="utf-8") 121 + 122 + status = invoke(["status", "--json"]) 123 + 124 + _assert_degraded_residual(status, "spl", "spl_identity_missing") 125 + 126 + 127 + def test_status_reconciles_spb_binding_instance(tmp_path, monkeypatch) -> None: 128 + journal = sandbox_journal(tmp_path, monkeypatch) 129 + prepare_ok(journal) 130 + result = invoke( 131 + ["apply", "spb", "--json"], input_text=json.dumps(spb_payload(journal)) 132 + ) 133 + assert result.exit_code == 0 134 + binding_path = journal / "backup" / "hosted" / "binding.json" 135 + binding = read_json(binding_path) 136 + binding["instance_id"] = "did:key:mismatched" 137 + binding_path.write_text(json.dumps(binding, indent=2) + "\n", encoding="utf-8") 138 + 139 + status = invoke(["status", "--json"]) 140 + 141 + _assert_degraded_residual(status, "spb", "spb_instance_mismatch") 142 + 143 + 144 + def test_status_reconciles_spp_credential_fingerprint(tmp_path, monkeypatch) -> None: 145 + journal = sandbox_journal(tmp_path, monkeypatch) 146 + prepare_ok(journal) 147 + result = invoke(["apply", "spp", "--json"], input_text=json.dumps(spp_payload())) 148 + assert result.exit_code == 0 149 + config_path = journal / "config" / "journal.json" 150 + config = read_json(config_path) 151 + config["services"]["confidential"]["credential_fingerprint_sha256"] = ( 152 + "mismatched-fingerprint" 153 + ) 154 + config_path.write_text(json.dumps(config, indent=2) + "\n", encoding="utf-8") 155 + 156 + status = invoke(["status", "--json"]) 157 + 158 + _assert_degraded_residual(status, "spp", "spp_credential_fingerprint_mismatch")