diff --git a/dev/optimizer-evals/test_behavior_risk.py b/dev/optimizer-evals/test_behavior_risk.py new file mode 100644 index 0000000..41f9a3c --- /dev/null +++ b/dev/optimizer-evals/test_behavior_risk.py @@ -0,0 +1,540 @@ +from __future__ import annotations + +from copy import deepcopy +import json +import os +from pathlib import Path +import sys +import tempfile +import unittest +from unittest import mock + + +ROOT = Path(__file__).resolve().parents[2] +SCRIPTS_ROOT = ROOT / "runtime" / "skill-optimizer" / "scripts" +SCHEMA_PATH = SCRIPTS_ROOT / "schemas" / "behavior-risk-report.schema.json" +sys.path.insert(0, str(SCRIPTS_ROOT)) + +from core.behavior_risk import ( # noqa: E402 + BehaviorAuditError, + BehaviorCandidateChangedError, + BehaviorEvidenceState, + BehaviorLimitError, + BehaviorReportValidationError, + BehaviorScanLimits, + audit_behavior, + audit_behavior_risk, + report_from_dict, + validate_behavior_risk_report, +) +from core.canonical import digest_json, tree_digest as canonical_tree_digest # noqa: E402 +from core.policy import mandatory_capability_ids, mandatory_control_ids # noqa: E402 +from validators.contracts import _validate_schema # noqa: E402 + + +def _write_skill(root: Path, body: str = "Use deterministic arithmetic.\n") -> None: + root.mkdir(parents=True, exist_ok=True) + (root / "SKILL.md").write_text(f"# Demo\n{body}", encoding="utf-8") + + +def _schema_errors(document: dict) -> list[str]: + with SCHEMA_PATH.open("r", encoding="utf-8") as handle: + schema = json.load(handle) + return _validate_schema(document, schema, schema) + + +class BehaviorRiskAuditTests(unittest.TestCase): + def test_skill_and_python_behaviors_reuse_policy_risk_floors(self) -> None: + with tempfile.TemporaryDirectory() as raw: + root = Path(raw) + _write_skill( + root, + "Publish a message through an API using a token.\n" + "Delete stale files, then run a shell command.\n" + "Install the Skill, git commit it, and enable automatic routing.\n", + ) + scripts = root / "scripts" + scripts.mkdir() + (scripts / "actions.py").write_text( + "import os\n" + "import requests\n" + "import shutil\n" + "import subprocess\n" + "open(target, 'w').write('x')\n" + "requests.post('https://example.invalid/action')\n" + "subprocess.run('rm -rf generated', shell=True)\n" + "shutil.rmtree('cache')\n" + "os.getenv('API_TOKEN')\n", + encoding="utf-8", + ) + + report = audit_behavior_risk(root) + rule_ids = {finding.rule_id for finding in report.findings} + self.assertTrue( + { + "skill.external-write", + "skill.credential-access", + "skill.delete", + "skill.shell", + "skill.implicit-install", + "skill.commit", + "skill.auto-routing", + "python.local-write", + "python.unbounded-write-path", + "python.network", + "python.external-write", + "python.subprocess", + "python.shell", + "python.delete", + "python.environment-read", + "python.credential-read", + } + <= rule_ids + ) + self.assertEqual("R3", report.minimum_risk.value) + self.assertEqual( + report.mandatory_controls, + mandatory_control_ids(report.risk_findings), + ) + self.assertEqual( + report.mandatory_capabilities, + mandatory_capability_ids(report.risk_findings), + ) + self.assertTrue(report.requires_runtime_enforcement) + self.assertEqual(report.content_digest, report.report_digest) + for finding in report.findings: + self.assertEqual(finding.dimension, finding.risk_finding.dimension) + self.assertEqual(finding.level, finding.risk_finding.level) + self.assertTrue(finding.content_digest.startswith("sha256:")) + + def test_zero_findings_is_not_a_runtime_safety_claim(self) -> None: + with tempfile.TemporaryDirectory() as raw: + root = Path(raw) + _write_skill(root) + report = audit_behavior(root) + + self.assertEqual((), report.findings) + self.assertEqual((), report.unknowns) + self.assertEqual("R0", report.minimum_risk.value) + self.assertFalse(report.requires_runtime_enforcement) + self.assertFalse(report.has_sensitive_material) + self.assertNotIn("runtime_safe", report.to_dict()) + self.assertIn( + "static_analysis_does_not_prove_runtime_safety", + report.threat_model, + ) + + def test_unknown_and_runtime_enforcement_are_not_conflated(self) -> None: + with tempfile.TemporaryDirectory() as raw: + root = Path(raw) + _write_skill(root) + scripts = root / "scripts" + scripts.mkdir() + (scripts / "broken.py").write_text("def broken(:\n", encoding="utf-8") + (scripts / "dynamic.py").write_text("eval(source)\n", encoding="utf-8") + + report = audit_behavior_risk(root) + unknown = next( + finding + for finding in report.findings + if finding.rule_id == "python.syntax-unknown" + ) + dynamic = next( + finding + for finding in report.findings + if finding.rule_id == "python.dynamic-execution" + ) + self.assertIs(unknown.evidence_state, BehaviorEvidenceState.UNKNOWN) + self.assertIn(unknown.finding_id, report.unknowns) + self.assertIs( + dynamic.evidence_state, + BehaviorEvidenceState.REQUIRES_RUNTIME_ENFORCEMENT, + ) + self.assertNotIn(dynamic.finding_id, report.unknowns) + self.assertTrue(report.requires_runtime_enforcement) + + forged = deepcopy(report.to_dict()) + raw_finding = next( + item + for item in forged["findings"] + if item["rule_id"] == "python.dynamic-execution" + ) + raw_finding["requires_runtime_enforcement"] = False + finding_payload = dict(raw_finding) + finding_payload.pop("finding_id") + raw_finding["finding_id"] = ( + "behavior-" + + digest_json(finding_payload).removeprefix("sha256:")[:32] + ) + forged["content_digest"] = digest_json( + {key: value for key, value in forged.items() if key != "content_digest"} + ) + with self.assertRaisesRegex( + BehaviorReportValidationError, "closed rule semantics" + ): + report_from_dict(forged) + + def test_report_digest_and_derived_risk_relationships_are_semantic(self) -> None: + with tempfile.TemporaryDirectory() as raw: + root = Path(raw) + _write_skill(root, "Delete a file.\n") + report = audit_behavior_risk(root) + document = report.to_dict() + self.assertEqual([], _schema_errors(document)) + self.assertEqual(report, report_from_dict(document)) + + digest_tamper = deepcopy(document) + digest_tamper["scanned_bytes"] += 1 + self.assertEqual([], _schema_errors(digest_tamper)) + with self.assertRaisesRegex( + BehaviorReportValidationError, "digest mismatch" + ): + report_from_dict(digest_tamper) + + derived_tamper = deepcopy(document) + derived_tamper["minimum_risk"] = "R0" + derived_tamper["mandatory_controls"] = [] + derived_tamper["mandatory_capabilities"] = [] + derived_tamper["content_digest"] = digest_json( + { + key: value + for key, value in derived_tamper.items() + if key != "content_digest" + } + ) + self.assertEqual([], _schema_errors(derived_tamper)) + with self.assertRaisesRegex( + BehaviorReportValidationError, "minimum_risk" + ): + report_from_dict(derived_tamper) + + shape_tamper = deepcopy(document) + shape_tamper["eligible"] = True + self.assertTrue(_schema_errors(shape_tamper)) + with self.assertRaisesRegex( + BehaviorReportValidationError, "closed contract" + ): + report_from_dict(shape_tamper) + + def test_caller_empty_report_cannot_replace_current_byte_audit(self) -> None: + with tempfile.TemporaryDirectory() as safe_raw, tempfile.TemporaryDirectory() as risky_raw: + safe = Path(safe_raw) + risky = Path(risky_raw) + _write_skill(safe) + _write_skill(risky, "Publish a message and delete its source file.\n") + forged = audit_behavior_risk(safe).to_dict() + forged["candidate_digest"] = canonical_tree_digest(risky) + forged["content_digest"] = digest_json( + {key: value for key, value in forged.items() if key != "content_digest"} + ) + + # A self-consistent digest is not provenance or authority. + parsed = report_from_dict(forged) + self.assertEqual((), parsed.findings) + with self.assertRaisesRegex( + BehaviorReportValidationError, "current candidate bytes" + ): + validate_behavior_risk_report(parsed, candidate_root=risky) + + def test_secret_evidence_is_redacted_and_material_is_distinguished(self) -> None: + secret = "AKIAABCDEFGHIJKLMNOP" + with tempfile.TemporaryDirectory() as raw: + root = Path(raw) + _write_skill(root, "Read a credential from the environment.\n") + references = root / "references" + references.mkdir() + (references / "credentials.json").write_text( + json.dumps({"access_key": secret}), encoding="utf-8" + ) + + report = audit_behavior_risk(root) + material = [ + finding + for finding in report.findings + if finding.sensitive_material_bundled + ] + access = [ + finding + for finding in report.findings + if finding.rule_id == "skill.credential-access" + ] + self.assertTrue(material) + self.assertTrue(access) + self.assertTrue(report.has_sensitive_material) + self.assertFalse(access[0].sensitive_material_bundled) + self.assertNotIn(secret, json.dumps(report.to_dict(), sort_keys=True)) + + def test_runtime_credential_reference_is_not_bundled_literal_material(self) -> None: + with tempfile.TemporaryDirectory() as raw: + root = Path(raw) + _write_skill(root) + scripts = root / "scripts" + scripts.mkdir() + (scripts / "env_access.py").write_text( + "import os\n" + "TOKEN = os.environ.get('SERVICE_TOKEN')\n" + "PASSWORD = os.getenv('SERVICE_PASSWORD')\n", + encoding="utf-8", + ) + (scripts / "env_access.js").write_text( + "const TOKEN = process.env.SERVICE_TOKEN;\n", + encoding="utf-8", + ) + (scripts / "env_access.sh").write_text( + "TOKEN=${SERVICE_TOKEN}\nPASSWORD=$SERVICE_PASSWORD\n", + encoding="utf-8", + ) + + report = audit_behavior_risk(root) + rule_ids = {finding.rule_id for finding in report.findings} + self.assertIn("python.environment-read", rule_ids) + self.assertIn("python.credential-read", rule_ids) + self.assertFalse(report.has_sensitive_material) + self.assertFalse( + any( + finding.rule_id == "material.suspicious-secret" + for finding in report.findings + ) + ) + + for literal in ("redacted-token", "dummy", "fake", "example", "xxxx"): + with self.subTest(literal=literal), tempfile.TemporaryDirectory() as raw: + root = Path(raw) + _write_skill(root) + scripts = root / "scripts" + scripts.mkdir() + (scripts / "literal.py").write_text( + f"TOKEN = {literal!r}\n", + encoding="utf-8", + ) + report = audit_behavior_risk(root) + self.assertTrue(report.has_sensitive_material) + self.assertTrue( + any( + finding.rule_id == "material.suspicious-secret" + and finding.sensitive_material_bundled + for finding in report.findings + ) + ) + + def test_certificates_nested_sensitive_paths_and_sk_tokens_are_blocked(self) -> None: + live_token = "sk-abcdefghijklmnopqrstuvwxyz123456" + with tempfile.TemporaryDirectory() as raw: + root = Path(raw) + _write_skill(root) + assets = root / "assets" + assets.mkdir() + for suffix in (".crt", ".cer", ".cert"): + (assets / f"public{suffix}").write_text( + "certificate bytes\n", + encoding="utf-8", + ) + nested = root / "references" / "secrets" + nested.mkdir(parents=True) + (nested / "config.json").write_text("{}\n", encoding="utf-8") + (assets / "material.txt").write_text( + "-----BEGIN CERTIFICATE-----\n" + f"api_key = {live_token}\n", + encoding="utf-8", + ) + + report = audit_behavior_risk(root) + sensitive_paths = { + finding.path + for finding in report.findings + if finding.rule_id == "material.sensitive-path" + } + self.assertTrue( + { + "assets/public.crt", + "assets/public.cer", + "assets/public.cert", + "references/secrets/config.json", + } + <= sensitive_paths + ) + strong_material = [ + finding + for finding in report.findings + if finding.rule_id == "material.secret-pattern" + ] + self.assertGreaterEqual(len(strong_material), 2) + self.assertTrue(report.has_sensitive_material) + serialized = json.dumps(report.to_dict(), sort_keys=True) + self.assertNotIn(live_token, serialized) + self.assertNotIn("BEGIN CERTIFICATE", serialized) + + def test_python_sink_bypasses_cannot_look_like_r0(self) -> None: + attacks = { + "os-popen": ( + "import os\nos.popen('echo unsafe').read()\n", + {"python.subprocess", "python.shell"}, + ), + "os-open-write": ( + "import os\n" + "fd = os.open('out.bin', os.O_CREAT | os.O_WRONLY)\n" + "os.write(fd, b'x')\n", + {"python.local-write", "python.unbounded-write-path"}, + ), + "http-client": ( + "import http.client\n" + "client = http.client.HTTPSConnection('example.invalid')\n" + "client.request('GET', '/')\n", + {"python.network-import", "python.network"}, + ), + "smtp-sendmail": ( + "import smtplib\n" + "client = smtplib.SMTP('example.invalid')\n" + "client.sendmail('a@example.invalid', 'b@example.invalid', 'body')\n", + { + "python.network-import", + "python.network", + "python.external-write", + }, + ), + "sensitive-path-read": ( + "from pathlib import Path\nPath('.env').read_text()\n", + {"python.credential-read"}, + ), + "unknown-dispatch": ( + "client.perform_action()\n", + {"python.unanalyzed-call"}, + ), + } + for label, (source, expected_rules) in attacks.items(): + with self.subTest(label=label), tempfile.TemporaryDirectory() as raw: + root = Path(raw) + _write_skill(root) + scripts = root / "scripts" + scripts.mkdir() + (scripts / "attack.py").write_text(source, encoding="utf-8") + + report = audit_behavior_risk(root) + rule_ids = {finding.rule_id for finding in report.findings} + self.assertTrue(expected_rules <= rule_ids) + self.assertNotEqual("R0", report.minimum_risk.value) + if "python.unanalyzed-call" in expected_rules: + self.assertTrue(report.has_unknowns) + self.assertTrue(report.requires_runtime_enforcement) + + def test_extensionless_executable_script_cannot_bypass_behavior_scan(self) -> None: + with tempfile.TemporaryDirectory() as raw: + root = Path(raw) + _write_skill(root) + scripts = root / "scripts" + scripts.mkdir() + payload = scripts / "runner" + payload.write_text( + "rm -rf /tmp/victim\n" + "curl -X POST https://example.invalid/publish\n", + encoding="utf-8", + ) + payload.chmod(0o700) + + report = audit_behavior_risk(root) + rule_ids = {finding.rule_id for finding in report.findings} + self.assertTrue( + { + "script.delete", + "script.network", + "script.external-write", + "script.unparsed-unknown", + } + <= rule_ids + ) + self.assertNotEqual("R0", report.minimum_risk.value) + self.assertTrue(report.has_unknowns) + self.assertTrue(report.requires_runtime_enforcement) + + def test_closed_pure_python_calls_and_local_functions_remain_r0(self) -> None: + with tempfile.TemporaryDirectory() as raw: + root = Path(raw) + _write_skill(root) + scripts = root / "scripts" + scripts.mkdir() + (scripts / "pure.py").write_text( + "from pathlib import Path\n" + "def normalize(value: str) -> str:\n" + " return value.strip()\n" + "def render(value: str) -> str:\n" + " return normalize(value)\n" + "ROOT = Path('relative')\n", + encoding="utf-8", + ) + + report = audit_behavior_risk(root) + self.assertEqual((), report.findings) + self.assertEqual("R0", report.minimum_risk.value) + + def test_candidate_drift_is_detected_against_final_tree_digest(self) -> None: + with tempfile.TemporaryDirectory() as raw: + root = Path(raw) + _write_skill(root) + script = root / "scripts" / "action.py" + script.parent.mkdir() + script.write_text("print('one')\n", encoding="utf-8") + + def mutate_then_digest(path: Path) -> str: + script.write_text("print('two')\n", encoding="utf-8") + return canonical_tree_digest(path) + + with mock.patch( + "core.behavior_risk.tree_digest", side_effect=mutate_then_digest + ): + with self.assertRaises(BehaviorCandidateChangedError): + audit_behavior_risk(root) + + def test_symlink_special_escape_and_resource_limits_fail_closed(self) -> None: + with tempfile.TemporaryDirectory() as raw: + root = Path(raw) / "candidate" + _write_skill(root) + (root / "link").symlink_to(root / "SKILL.md") + with self.assertRaisesRegex(BehaviorAuditError, "symbolic links"): + audit_behavior_risk(root) + + if hasattr(os, "mkfifo"): + with tempfile.TemporaryDirectory() as raw: + root = Path(raw) + _write_skill(root) + os.mkfifo(root / "pipe") + with self.assertRaisesRegex(BehaviorAuditError, "special files"): + audit_behavior_risk(root) + + with tempfile.TemporaryDirectory() as raw: + parent = Path(raw) + root = parent / "candidate" + _write_skill(root) + escaped_spelling = Path(f"{root}/../candidate") + with self.assertRaisesRegex(BehaviorAuditError, "parent traversal"): + audit_behavior_risk(escaped_spelling) + + with tempfile.TemporaryDirectory() as raw: + root = Path(raw) + _write_skill(root) + (root / "extra.txt").write_text("x", encoding="utf-8") + with self.assertRaises(BehaviorLimitError): + audit_behavior_risk(root, limits=BehaviorScanLimits(max_files=1)) + with self.assertRaises(BehaviorLimitError): + audit_behavior_risk( + root, + limits=BehaviorScanLimits(max_file_bytes=4), + ) + with self.assertRaises(BehaviorLimitError): + audit_behavior_risk( + root, + limits=BehaviorScanLimits(max_total_bytes=4), + ) + + def test_stable_current_bytes_produce_stable_reports(self) -> None: + with tempfile.TemporaryDirectory() as raw: + root = Path(raw) + _write_skill(root, "Call an API over the network.\n") + first = audit_behavior_risk(root) + second = audit_behavior_risk(root) + self.assertEqual(first, second) + self.assertEqual(first.candidate_digest, canonical_tree_digest(root)) + self.assertIs(validate_behavior_risk_report(first), first) + + +if __name__ == "__main__": + unittest.main() diff --git a/dev/optimizer-evals/test_personal_install.py b/dev/optimizer-evals/test_personal_install.py new file mode 100644 index 0000000..291d695 --- /dev/null +++ b/dev/optimizer-evals/test_personal_install.py @@ -0,0 +1,772 @@ +from __future__ import annotations + +from copy import deepcopy +import inspect +import json +import os +from pathlib import Path +import sys +import tempfile +import unittest +from unittest.mock import patch + +WORKSPACE = Path(__file__).resolve().parents[2] +SCRIPTS_ROOT = WORKSPACE / "runtime" / "skill-optimizer" / "scripts" +sys.path.insert(0, str(SCRIPTS_ROOT)) + +from core.canonical import canonical_json, digest_json # noqa: E402 +from core.personal_install import ( # noqa: E402 + PERSONAL_INSTALL_ACTION, + PersonalInstallAuthorizationError, + PersonalInstallError, + PersonalInstallIntegrationError, + PersonalInstallJournalError, + PersonalInstallReplayError, + PersonalInstallRiskError, + PersonalInstallRollbackError, + PersonalInstallTargetError, + make_personal_install_decision_payload, + personal_install, + personal_install_authorization_target_digest, + rollback_personal_install, + validate_personal_install_receipt, + validate_personal_rollback_receipt, +) +import core.personal_install as personal_module # noqa: E402 +from core.workflow import ( # noqa: E402 + AuthorizationGrant, + WorkflowActor, + WorkflowEvent, + WorkflowEventType, + WorkflowState, +) +from core.workspace import TargetChangedError, path_digest # noqa: E402 +from validators.contracts import _validate_schema # noqa: E402 + + +def _digest(label: str) -> str: + return digest_json({"label": label}) + + +def _write_skill(path: Path, label: str) -> None: + path.mkdir(mode=0o700) + skill = path / "SKILL.md" + skill.write_text( + "---\n" + f"name: {path.name}\n" + f"description: A plain deterministic {label} greeting.\n" + "---\n" + f"# {label}\n" + "Return a greeting to the user.\n", + encoding="utf-8", + ) + skill.chmod(0o600) + + +class PersonalInstallTests(unittest.TestCase): + def setUp(self) -> None: + self.temporary = tempfile.TemporaryDirectory() + self.root = Path(self.temporary.name) + self.personal_root = self.root / "personal-skills" + self.personal_root.mkdir(mode=0o700) + self.candidate = self.root / "candidate-skill" + _write_skill(self.candidate, "new") + self.target = self.personal_root / "demo-skill" + self.workflow_id = "personal-install-workflow" + self.scope_digest = _digest("personal-scope") + self.roots_patch = patch.object( + personal_module, + "_configured_personal_roots", + return_value=(self.personal_root,), + ) + self.locality_patch = patch.object( + personal_module, + "_filesystem_locality", + return_value=("apfs", True), + ) + self.roots_patch.start() + self.locality_patch.start() + + def tearDown(self) -> None: + self.locality_patch.stop() + self.roots_patch.stop() + self.temporary.cleanup() + + def _workflow_path(self) -> Path: + return personal_module._fixed_workflow_log_path( # noqa: SLF001 + self.personal_root.resolve(strict=True), + self.workflow_id, + ) + + def _write_workflow(self, events: list[dict]) -> None: + """Private fixture stand-in for the missing trusted host adapter.""" + + control = self.personal_root / ".skill-optimizer-personal-install" + workflows = control / "workflows" + control.mkdir(mode=0o700, exist_ok=True) + control.chmod(0o700) + workflows.mkdir(mode=0o700, exist_ok=True) + workflows.chmod(0o700) + path = self._workflow_path() + path.write_text( + "".join(f"{canonical_json(event)}\n" for event in events), + encoding="utf-8", + ) + path.chmod(0o600) + + def _events( + self, + *, + expected_target_digest: str | None, + actions: tuple[str, ...] = ( + PERSONAL_INSTALL_ACTION, + "quality_unverified", + ), + grant_scope_digest: str | None = None, + grant_risk_digest: str | None = None, + grant_target_digest: str | None = None, + grant_status: str = "granted", + expires_at: str | None = "2099-01-01T00:00:00Z", + ) -> list[dict]: + payload = make_personal_install_decision_payload( + candidate=self.candidate, + target=self.target, + expected_target_digest=expected_target_digest, + scope_digest=self.scope_digest, + workflow_id=self.workflow_id, + ) + decision = WorkflowEvent.create( + sequence=0, + workflow_id=self.workflow_id, + event_type=WorkflowEventType.DECISION, + from_state=WorkflowState.INTAKE, + to_state=WorkflowState.INTAKE, + actor=WorkflowActor.USER, + payload=payload, + created_at="2026-07-26T00:00:00Z", + previous_event_digest=None, + ) + self._write_workflow([decision.to_dict()]) + authorization_target = personal_install_authorization_target_digest( + candidate=self.candidate, + target=self.target, + expected_target_digest=expected_target_digest, + scope_digest=self.scope_digest, + workflow_id=self.workflow_id, + ) + grant = AuthorizationGrant( + authorization_id="personal-install-grant", + kind="install", + status=grant_status, + actions=actions, + scope_digest=grant_scope_digest or self.scope_digest, + risk_digest=grant_risk_digest or payload["risk_commitment_digest"], + target_digest=grant_target_digest or authorization_target, + expires_at=expires_at, + ) + authorization = WorkflowEvent.create( + sequence=1, + workflow_id=self.workflow_id, + event_type=WorkflowEventType.AUTHORIZATION, + from_state=WorkflowState.INTAKE, + to_state=WorkflowState.INTAKE, + actor=WorkflowActor.USER, + payload={"grant": grant.to_dict()}, + created_at="2026-07-26T00:00:01Z", + previous_event_digest=decision.content_digest, + ) + events = [decision.to_dict(), authorization.to_dict()] + self._write_workflow(events) + return events + + def _install(self, expected_target_digest: str | None): + self._events(expected_target_digest=expected_target_digest) + return personal_install( + candidate=self.candidate, + target=self.target, + expected_target_digest=expected_target_digest, + scope_digest=self.scope_digest, + workflow_id=self.workflow_id, + ) + + def _journal_path(self, transaction_id: str) -> Path: + return ( + self.personal_root + / ".skill-optimizer-personal-install" + / "journals" + / f"{transaction_id}.json" + ) + + def _backup_path(self, transaction_id: str) -> Path: + return ( + self.personal_root + / ".skill-optimizer-personal-install" + / "backups" + / transaction_id + / "payload" + ) + + def _quarantine_path(self, transaction_id: str) -> Path: + return ( + self.personal_root + / ".skill-optimizer-personal-install" + / "quarantine" + / transaction_id + / "payload" + ) + + def test_absent_target_install_validates_and_rolls_back_to_absent(self) -> None: + candidate_digest = path_digest(self.candidate) + + receipt = self._install(None) + + self.assertEqual(receipt.status, "installed") + self.assertEqual(receipt.candidate_digest, candidate_digest) + self.assertEqual(receipt.staged_digest, candidate_digest) + self.assertEqual(receipt.post_install_digest, candidate_digest) + self.assertIsNone(receipt.pre_target_digest) + self.assertFalse(receipt.backup_present) + self.assertEqual( + validate_personal_install_receipt(receipt.to_dict()), receipt + ) + + rollback = rollback_personal_install(receipt.transaction_id) + + self.assertEqual(rollback.status, "rolled_back") + self.assertIsNone(rollback.restored_digest) + self.assertFalse(self.target.exists()) + self.assertEqual( + path_digest(self._quarantine_path(receipt.transaction_id)), + candidate_digest, + ) + self.assertEqual( + validate_personal_rollback_receipt(rollback.to_dict()), rollback + ) + with self.assertRaises(PersonalInstallReplayError): + rollback_personal_install(receipt.transaction_id) + + def test_existing_target_backup_install_and_exact_restore(self) -> None: + _write_skill(self.target, "old") + old_digest = path_digest(self.target) + + receipt = self._install(old_digest) + + self.assertEqual(receipt.pre_target_digest, old_digest) + self.assertEqual(receipt.backup_digest, old_digest) + self.assertTrue(receipt.backup_present) + self.assertEqual(path_digest(self._backup_path(receipt.transaction_id)), old_digest) + validate_personal_install_receipt(receipt) + + rollback = rollback_personal_install(receipt.transaction_id) + + self.assertEqual(rollback.restored_digest, old_digest) + self.assertEqual(path_digest(self.target), old_digest) + self.assertFalse(self._backup_path(receipt.transaction_id).exists()) + self.assertTrue(self._quarantine_path(receipt.transaction_id).exists()) + validate_personal_rollback_receipt(rollback) + + def test_rollback_rejects_unsafe_backup_metadata_with_unchanged_tree_digest(self) -> None: + for attack in ("shared-write-mode", "hard-link"): + with self.subTest(attack=attack): + original_target = self.target + self.target = self.personal_root / f"metadata-{attack}" + _write_skill(self.target, f"old-{attack}") + old_digest = path_digest(self.target) + receipt = self._install(old_digest) + backup = self._backup_path(receipt.transaction_id) + before_attack = path_digest(backup) + if attack == "shared-write-mode": + (backup / "SKILL.md").chmod(0o660) + else: + hardlink = self.root / f"shared-{receipt.transaction_id}" + os.link(backup / "SKILL.md", hardlink) + + self.assertEqual(path_digest(backup), before_attack) + with self.assertRaises(PersonalInstallRollbackError): + rollback_personal_install(receipt.transaction_id) + self.assertEqual(path_digest(self.target), receipt.post_install_digest) + self.target = original_target + + def test_public_install_reruns_audit_for_source_stage_and_installed_bytes(self) -> None: + self._events(expected_target_digest=None) + real_audit = personal_module.audit_behavior_risk + with patch.object( + personal_module, + "audit_behavior_risk", + wraps=real_audit, + ) as audit: + personal_install( + candidate=self.candidate, + target=self.target, + expected_target_digest=None, + scope_digest=self.scope_digest, + workflow_id=self.workflow_id, + ) + self.assertGreaterEqual(audit.call_count, 3) + + def test_api_has_no_caller_authority_validator_receipt_or_journal_root(self) -> None: + for callable_under_test in ( + personal_install, + personal_install_authorization_target_digest, + ): + parameters = inspect.signature(callable_under_test).parameters + for forbidden in ( + "behavior_report", + "validator", + "authority_receipt", + "authorization_grant", + "eligible", + "journal_root", + "personal", + "shared", + "automatic_routing", + "quality_unverified", + "workflow_events", + "workflow_log", + "workflow_log_path", + "workflow_handle", + "event_source", + ): + self.assertNotIn(forbidden, parameters) + with self.assertRaises(TypeError): + personal_install( + candidate=self.candidate, + target=self.target, + expected_target_digest=None, + scope_digest=self.scope_digest, + workflow_id=self.workflow_id, + workflow_events=(), + ) + with self.assertRaises(PersonalInstallJournalError): + rollback_personal_install({"transaction_id": "forged"}) # type: ignore[arg-type] + + def test_caller_event_array_cannot_replace_missing_fixed_trusted_log(self) -> None: + caller_events = self._events(expected_target_digest=None) + self._workflow_path().unlink() + + with self.assertRaises(TypeError): + personal_install( + candidate=self.candidate, + target=self.target, + expected_target_digest=None, + scope_digest=self.scope_digest, + workflow_id=self.workflow_id, + workflow_events=caller_events, + ) + with self.assertRaisesRegex( + PersonalInstallIntegrationError, + "trusted-workflow-event-source-adapter", + ): + personal_install_authorization_target_digest( + candidate=self.candidate, + target=self.target, + expected_target_digest=None, + scope_digest=self.scope_digest, + workflow_id=self.workflow_id, + ) + with self.assertRaisesRegex( + PersonalInstallIntegrationError, + "trusted-workflow-event-source-adapter", + ): + personal_install( + candidate=self.candidate, + target=self.target, + expected_target_digest=None, + scope_digest=self.scope_digest, + workflow_id=self.workflow_id, + ) + + self.assertFalse(self.target.exists()) + + def test_fixed_workflow_source_rejects_permissions_symlink_and_size(self) -> None: + self._events(expected_target_digest=None) + workflow_path = self._workflow_path() + workflow_path.chmod(0o644) + with self.assertRaisesRegex( + PersonalInstallIntegrationError, + "trusted-workflow-event-source-adapter", + ): + personal_install( + candidate=self.candidate, + target=self.target, + expected_target_digest=None, + scope_digest=self.scope_digest, + workflow_id=self.workflow_id, + ) + + self._events(expected_target_digest=None) + hardlink = self.root / "hard-linked-workflow.jsonl" + os.link(workflow_path, hardlink) + with self.assertRaisesRegex( + PersonalInstallIntegrationError, + "trusted-workflow-event-source-adapter", + ): + personal_install( + candidate=self.candidate, + target=self.target, + expected_target_digest=None, + scope_digest=self.scope_digest, + workflow_id=self.workflow_id, + ) + hardlink.unlink() + + self._events(expected_target_digest=None) + trusted_bytes = workflow_path.read_bytes() + alternate = self.root / "caller-events.jsonl" + alternate.write_bytes(trusted_bytes) + alternate.chmod(0o600) + workflow_path.unlink() + workflow_path.symlink_to(alternate) + with self.assertRaisesRegex( + PersonalInstallIntegrationError, + "trusted-workflow-event-source-adapter", + ): + personal_install( + candidate=self.candidate, + target=self.target, + expected_target_digest=None, + scope_digest=self.scope_digest, + workflow_id=self.workflow_id, + ) + workflow_path.unlink() + + self._events(expected_target_digest=None) + with patch.object( + personal_module, + "_MAX_WORKFLOW_LOG_BYTES", + 16, + ): + with self.assertRaisesRegex( + PersonalInstallIntegrationError, + "trusted-workflow-event-source-adapter", + ): + personal_install( + candidate=self.candidate, + target=self.target, + expected_target_digest=None, + scope_digest=self.scope_digest, + workflow_id=self.workflow_id, + ) + + self.assertFalse(self.target.exists()) + + def test_raw_decision_must_bind_quality_and_scope_facts_exactly(self) -> None: + payload = make_personal_install_decision_payload( + candidate=self.candidate, + target=self.target, + expected_target_digest=None, + scope_digest=self.scope_digest, + workflow_id=self.workflow_id, + ) + payload["quality_unverified"] = True + decision = WorkflowEvent.create( + sequence=0, + workflow_id=self.workflow_id, + event_type="decision", + from_state="intake", + to_state="intake", + actor="user", + payload=payload, + created_at="2026-07-26T00:00:00Z", + previous_event_digest=None, + ) + self._write_workflow([decision.to_dict()]) + with self.assertRaises(PersonalInstallAuthorizationError): + personal_install_authorization_target_digest( + candidate=self.candidate, + target=self.target, + expected_target_digest=None, + scope_digest=self.scope_digest, + workflow_id=self.workflow_id, + ) + + def test_wrong_scope_risk_target_actions_or_status_cannot_authorize(self) -> None: + cases = ( + {"grant_scope_digest": _digest("wrong-scope")}, + {"grant_risk_digest": _digest("wrong-risk")}, + {"grant_target_digest": _digest("wrong-target")}, + {"actions": (PERSONAL_INSTALL_ACTION,)}, + {"grant_status": "denied"}, + {"expires_at": "2000-01-01T00:00:00Z"}, + ) + for overrides in cases: + with self.subTest(overrides=overrides): + self._events(expected_target_digest=None, **overrides) + with self.assertRaises(PersonalInstallAuthorizationError): + personal_install( + candidate=self.candidate, + target=self.target, + expected_target_digest=None, + scope_digest=self.scope_digest, + workflow_id=self.workflow_id, + ) + self.assertFalse(self.target.exists()) + + def test_event_after_grant_invalidates_head_bound_authority(self) -> None: + events = self._events(expected_target_digest=None) + final = events[-1] + trailing = WorkflowEvent.create( + sequence=2, + workflow_id=self.workflow_id, + event_type="snapshot", + from_state="intake", + to_state="intake", + actor="optimizer", + payload={"kind": "unrelated"}, + created_at="2026-07-26T00:00:02Z", + previous_event_digest=final["content_digest"], + ) + self._write_workflow([*events, trailing.to_dict()]) + with self.assertRaises(PersonalInstallAuthorizationError): + personal_install( + candidate=self.candidate, + target=self.target, + expected_target_digest=None, + scope_digest=self.scope_digest, + workflow_id=self.workflow_id, + ) + + def test_current_byte_risk_unknown_or_runtime_enforcement_blocks_p1(self) -> None: + (self.candidate / "SKILL.md").write_text( + "---\nname: candidate-skill\n" + "description: Network publishing helper.\n---\n" + "Use curl to publish data over the network.\n", + encoding="utf-8", + ) + with self.assertRaises(PersonalInstallRiskError): + make_personal_install_decision_payload( + candidate=self.candidate, + target=self.target, + expected_target_digest=None, + scope_digest=self.scope_digest, + workflow_id=self.workflow_id, + ) + + def test_target_probe_rejects_unrecognized_nested_symlink_shared_and_managed(self) -> None: + outside = self.root / "outside" / "demo-skill" + outside.parent.mkdir(mode=0o700) + with self.assertRaises(PersonalInstallTargetError): + make_personal_install_decision_payload( + candidate=self.candidate, + target=outside, + expected_target_digest=None, + scope_digest=self.scope_digest, + workflow_id=self.workflow_id, + ) + + nested = self.personal_root / "nested" / "demo-skill" + nested.parent.mkdir(mode=0o700) + with self.assertRaises(PersonalInstallTargetError): + make_personal_install_decision_payload( + candidate=self.candidate, + target=nested, + expected_target_digest=None, + scope_digest=self.scope_digest, + workflow_id=self.workflow_id, + ) + + symlink = self.personal_root / "linked-skill" + symlink.symlink_to(self.candidate, target_is_directory=True) + with self.assertRaises(PersonalInstallTargetError): + make_personal_install_decision_payload( + candidate=self.candidate, + target=symlink, + expected_target_digest=None, + scope_digest=self.scope_digest, + workflow_id=self.workflow_id, + ) + symlink.unlink() + + self.personal_root.chmod(0o770) + with self.assertRaises(PersonalInstallTargetError): + make_personal_install_decision_payload( + candidate=self.candidate, + target=self.target, + expected_target_digest=None, + scope_digest=self.scope_digest, + workflow_id=self.workflow_id, + ) + self.personal_root.chmod(0o700) + + with patch.object( + personal_module, + "_filesystem_locality", + return_value=(None, None), + ): + with self.assertRaises(PersonalInstallTargetError): + make_personal_install_decision_payload( + candidate=self.candidate, + target=self.target, + expected_target_digest=None, + scope_digest=self.scope_digest, + workflow_id=self.workflow_id, + ) + + _write_skill(self.target, "hard-linked") + outside_link = self.root / "shared-skill-bytes" + os.link(self.target / "SKILL.md", outside_link) + with self.assertRaises(PersonalInstallTargetError): + make_personal_install_decision_payload( + candidate=self.candidate, + target=self.target, + expected_target_digest=path_digest(self.target), + scope_digest=self.scope_digest, + workflow_id=self.workflow_id, + ) + outside_link.unlink() + + (self.personal_root.parent / ".managed").write_text("managed", encoding="utf-8") + with self.assertRaises(PersonalInstallTargetError): + make_personal_install_decision_payload( + candidate=self.candidate, + target=self.target, + expected_target_digest=None, + scope_digest=self.scope_digest, + workflow_id=self.workflow_id, + ) + + def test_receipt_validator_does_not_recreate_missing_control_state(self) -> None: + receipt = self._install(None) + quarantine_root = ( + self.personal_root + / ".skill-optimizer-personal-install" + / "quarantine" + ) + quarantine_root.rmdir() + + with self.assertRaises(PersonalInstallJournalError): + validate_personal_install_receipt(receipt) + + self.assertFalse(quarantine_root.exists()) + + def test_target_drift_after_decision_fails_before_mutation(self) -> None: + _write_skill(self.target, "old") + expected = path_digest(self.target) + self._events(expected_target_digest=expected) + (self.target / "drift.txt").write_text("changed", encoding="utf-8") + with self.assertRaises(TargetChangedError): + personal_install( + candidate=self.candidate, + target=self.target, + expected_target_digest=expected, + scope_digest=self.scope_digest, + workflow_id=self.workflow_id, + ) + + def test_failed_install_restores_exact_pre_target_and_never_returns_success(self) -> None: + _write_skill(self.target, "old") + old_digest = path_digest(self.target) + self._events(expected_target_digest=old_digest) + real_rename = personal_module._atomic_rename_noreplace + + def fail_install_move(source: Path, destination: Path) -> None: + if ( + destination.name == self.target.name + and destination.parent == self.personal_root.resolve(strict=True) + and "staging" in source.parts + ): + raise OSError("injected install rename failure") + real_rename(source, destination) + + with patch.object( + personal_module, + "_atomic_rename_noreplace", + side_effect=fail_install_move, + ): + with self.assertRaises(PersonalInstallError): + personal_install( + candidate=self.candidate, + target=self.target, + expected_target_digest=old_digest, + scope_digest=self.scope_digest, + workflow_id=self.workflow_id, + ) + self.assertEqual(path_digest(self.target), old_digest) + journals = list( + ( + self.personal_root + / ".skill-optimizer-personal-install" + / "journals" + ).glob("*.json") + ) + self.assertEqual(len(journals), 1) + self.assertEqual(json.loads(journals[0].read_text())["state"], "failed_restored") + + def test_rollback_rejects_target_drift_backup_replacement_and_journal_tamper(self) -> None: + for attack in ("target", "backup", "journal"): + with self.subTest(attack=attack): + target = self.personal_root / f"demo-{attack}" + original_target = self.target + self.target = target + _write_skill(self.target, f"old-{attack}") + old_digest = path_digest(self.target) + receipt = self._install(old_digest) + if attack == "target": + (self.target / "drift.txt").write_text("drift", encoding="utf-8") + expected_error = PersonalInstallRollbackError + elif attack == "backup": + (self._backup_path(receipt.transaction_id) / "drift.txt").write_text( + "drift", encoding="utf-8" + ) + expected_error = PersonalInstallRollbackError + else: + journal_path = self._journal_path(receipt.transaction_id) + payload = json.loads(journal_path.read_text(encoding="utf-8")) + payload["quality_claim_status"] = "verified" + journal_path.write_text(json.dumps(payload) + "\n", encoding="utf-8") + expected_error = PersonalInstallJournalError + with self.assertRaises(expected_error): + rollback_personal_install(receipt.transaction_id) + self.target = original_target + + def test_forged_self_hashed_receipt_cannot_validate_or_trigger_rollback(self) -> None: + receipt = self._install(None) + forged = deepcopy(receipt.to_dict()) + forged["transaction_id"] = "pi-" + "0" * 64 + body = {key: value for key, value in forged.items() if key != "receipt_digest"} + forged["receipt_digest"] = digest_json(body) + with self.assertRaises(PersonalInstallJournalError): + validate_personal_install_receipt(forged) + with self.assertRaises(PersonalInstallJournalError): + rollback_personal_install(forged["transaction_id"]) + self.assertTrue(self.target.exists()) + + def test_receipt_schema_and_runtime_validator_share_positive_negative_corpus(self) -> None: + schema_path = ( + WORKSPACE + / "runtime" + / "skill-optimizer" + / "scripts" + / "schemas" + / "personal-install-receipt.schema.json" + ) + schema = json.loads(schema_path.read_text(encoding="utf-8")) + receipt = self._install(None) + install_mapping = receipt.to_dict() + self.assertEqual(_validate_schema(install_mapping, schema, schema), []) + validate_personal_install_receipt(install_mapping) + + extra = {**install_mapping, "eligible": True} + self.assertNotEqual(_validate_schema(extra, schema, schema), []) + with self.assertRaises(ValueError): + validate_personal_install_receipt(extra) + + relation = deepcopy(install_mapping) + relation["staged_digest"] = _digest("other-staged") + relation_body = { + key: value for key, value in relation.items() if key != "receipt_digest" + } + relation["receipt_digest"] = digest_json(relation_body) + # JSON Schema validates shape; the runtime must rebuild object relations. + self.assertEqual(_validate_schema(relation, schema, schema), []) + with self.assertRaises(ValueError): + validate_personal_install_receipt(relation) + + rollback = rollback_personal_install(receipt.transaction_id) + rollback_mapping = rollback.to_dict() + self.assertEqual(_validate_schema(rollback_mapping, schema, schema), []) + validate_personal_rollback_receipt(rollback_mapping) + + +if __name__ == "__main__": + unittest.main() diff --git a/dev/optimizer-evals/test_team_delivery.py b/dev/optimizer-evals/test_team_delivery.py new file mode 100644 index 0000000..bb21877 --- /dev/null +++ b/dev/optimizer-evals/test_team_delivery.py @@ -0,0 +1,964 @@ +"""P2 team-delivery trust, projection, and current-byte regressions.""" + +from __future__ import annotations + +from copy import deepcopy +import inspect +import json +import os +from pathlib import Path +import stat +import sys +import tempfile +import unittest +from unittest import mock + + +WORKSPACE = Path(__file__).resolve().parents[2] +SCRIPTS = WORKSPACE / "runtime" / "skill-optimizer" / "scripts" +if str(SCRIPTS) not in sys.path: + sys.path.insert(0, str(SCRIPTS)) + +from core.canonical import ( # noqa: E402 + AuthorizationKind, + AuthorizationStatus, + WorkflowState, + digest_json, +) +from core.workflow import ( # noqa: E402 + AuthorizationGrant, + WorkflowActor, + WorkflowEvent, + WorkflowEventType, +) +import packaging.team_delivery as team_delivery # noqa: E402 +from packaging.builder import PackagingError # noqa: E402 +from packaging.team_delivery import ( # noqa: E402 + QUALITY_UNVERIFIED_ACTION, + TEAM_DELIVERY_ACTION, + SensitiveMaterialError, + TeamDeliveryAuthorizationError, + TeamDeliveryIntegrationRequired, + TeamDeliveryIntegrityError, + UnsafeTeamOutputError, + build_team_delivery, + prepare_team_delivery_action_target, + validate_team_delivery_manifest, +) +from validators.contracts import _validate_schema # noqa: E402 + + +class TeamDeliveryTests(unittest.TestCase): + def _skill(self, root: Path, name: str = "demo") -> Path: + candidate = root / name + (candidate / "scripts").mkdir(parents=True) + (candidate / "references").mkdir() + (candidate / "SKILL.md").write_text( + "---\n" + f"name: {name}\n" + "description: Deterministic text formatting.\n" + "---\n\n" + "Return the supplied text.\n", + encoding="utf-8", + ) + (candidate / "scripts" / "format_text.py").write_text( + "def format_text(value: str) -> str:\n" + " return value.strip()\n", + encoding="utf-8", + ) + (candidate / "references" / "format.md").write_text( + "Use plain text.\n", encoding="utf-8" + ) + return candidate + + def _trusted_workflow_root(self, root: Path, host: str) -> Path: + workflow_root = root / f"trusted-{host}-workflows" + workflow_root.mkdir(mode=0o700, exist_ok=True) + workflow_root.chmod(0o700) + return workflow_root + + def _write_workflow_event( + self, + workflow_root: Path, + event: WorkflowEvent, + ) -> Path: + path = workflow_root / team_delivery._workflow_event_filename( + event.workflow_id + ) + path.write_text( + json.dumps(event.to_dict(), ensure_ascii=True, sort_keys=True) + "\n", + encoding="utf-8", + ) + path.chmod(0o600) + return path + + def _authority( + self, + candidate: Path, + output: Path, + *, + host: str = "codex", + quality_summary: dict[str, object] | None = None, + actions: tuple[str, ...] = ( + TEAM_DELIVERY_ACTION, + QUALITY_UNVERIFIED_ACTION, + ), + target_digest: str | None = None, + risk_digest: str | None = None, + workflow_id: str = "workflow-team-delivery", + ): + scope_digest = digest_json({"workflow_scope": "team-delivery"}) + target = prepare_team_delivery_action_target( + candidate, + output, + host=host, + scope_digest=scope_digest, + quality_summary=quality_summary, + ) + grant = AuthorizationGrant( + authorization_id="team-delivery-grant", + kind=AuthorizationKind.EXTERNAL_WRITE, + status=AuthorizationStatus.GRANTED, + actions=actions, + scope_digest=scope_digest, + risk_digest=risk_digest or target.risk_commitment_digest, + target_digest=target_digest or target.content_digest, + ) + event = WorkflowEvent.create( + sequence=0, + workflow_id=workflow_id, + event_type=WorkflowEventType.AUTHORIZATION, + from_state=WorkflowState.INTAKE, + to_state=WorkflowState.INTAKE, + actor=WorkflowActor.USER, + payload={"grant": grant.to_dict()}, + created_at="2026-07-26T12:00:00+08:00", + previous_event_digest=None, + ) + workflow_root = self._trusted_workflow_root(candidate.parent, host) + self._write_workflow_event(workflow_root, event) + return target, grant, event, scope_digest, workflow_root + + def _build( + self, + candidate: Path, + output: Path, + *, + host: str = "codex", + quality_summary: dict[str, object] | None = None, + ): + target, _grant, event, scope_digest, workflow_root = self._authority( + candidate, + output, + host=host, + quality_summary=quality_summary, + ) + root_function = "_codex_workflow_root" if host == "codex" else "_claude_workflow_root" + with mock.patch.object( + team_delivery, root_function, return_value=workflow_root + ): + manifest = build_team_delivery( + candidate, + output, + host=host, + workflow_id=event.workflow_id, + scope_digest=scope_digest, + risk_digest=target.risk_commitment_digest, + quality_summary=quality_summary, + expected_candidate_digest=target.candidate_digest, + ) + return manifest, target, event + + def _validate_manifest(self, manifest, **kwargs): + host = manifest.host if hasattr(manifest, "host") else manifest["host"] + output_root = ( + Path(manifest.output_root) + if hasattr(manifest, "output_root") + else Path(manifest["output_root"]) + ) + workflow_root = output_root.parent / f"trusted-{host}-workflows" + root_function = ( + "_codex_workflow_root" if host == "codex" else "_claude_workflow_root" + ) + with mock.patch.object( + team_delivery, root_function, return_value=workflow_root + ): + return validate_team_delivery_manifest(manifest, **kwargs) + + def test_builds_isolated_runtime_only_package_without_install(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + candidate = self._skill(root) + output = root / "approved-team-output" + output.mkdir() + + manifest, target, event = self._build(candidate, output) + + self.assertEqual(manifest.delivery_target, "P2_team_package") + self.assertEqual(manifest.invocation_mode, "explicit_only") + self.assertEqual(manifest.authority["action_target_digest"], target.content_digest) + self.assertEqual(manifest.authority["event_head_digest"], event.content_digest) + self.assertEqual( + set(path.name for path in output.iterdir()), + {"package", "package.manifest.json", "team-delivery-manifest.json"}, + ) + package = output / "package" + self.assertTrue((package / "SKILL.md").is_file()) + self.assertTrue((package / "scripts" / "format_text.py").is_file()) + self.assertTrue((package / "references" / "format.md").is_file()) + self.assertFalse((output / candidate.name).exists()) + self.assertFalse(manifest.rollback["install_performed"]) + self.assertFalse(manifest.rollback["host_activation_performed"]) + self.assertIn("install", manifest.claims["removed"]) + self.assertIn("host-activation", manifest.claims["removed"]) + persisted = json.loads( + (output / "team-delivery-manifest.json").read_text(encoding="utf-8") + ) + validated = self._validate_manifest( + persisted, + package_root=package, + builder_manifest_path=output / "package.manifest.json", + ) + self.assertEqual(validated.content_digest, manifest.content_digest) + + def test_generated_manifest_matches_the_closed_json_schema(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + candidate = self._skill(root) + output = root / "approved-team-output" + output.mkdir() + manifest, _target, _event = self._build(candidate, output) + schema = json.loads( + ( + SCRIPTS + / "schemas" + / "team-delivery-manifest.schema.json" + ).read_text(encoding="utf-8") + ) + + self.assertEqual( + [], + _validate_schema(manifest.to_dict(), schema, schema), + ) + + def test_excludes_research_eval_cache_tests_journal_backup_and_trace(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + candidate = self._skill(root) + additions = { + "references/research-notes.md": "notes\n", + "references/eval/results.md": "result\n", + "scripts/test_format.py": "VALUE = 1\n", + "scripts/journals/run.jsonl": "{}\n", + "assets/backups/old.txt": "old\n", + "assets/traces/run.trace": "trace\n", + "dev/debug.txt": "debug\n", + "README.md": "development only\n", + } + for relative, content in additions.items(): + path = candidate / relative + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(content, encoding="utf-8") + cache = candidate / "scripts" / "__pycache__" + cache.mkdir() + (cache / "format_text.pyc").write_bytes(b"cache") + output = root / "approved-team-output" + output.mkdir() + + manifest, _target, _event = self._build(candidate, output) + + package_paths = { + path.relative_to(output / "package").as_posix() + for path in (output / "package").rglob("*") + if path.is_file() + } + self.assertEqual( + package_paths, + {"SKILL.md", "references/format.md", "scripts/format_text.py"}, + ) + excluded = {item["path"] for item in manifest.projection["excluded_paths"]} + for relative in additions: + self.assertIn(relative, excluded) + self.assertIn("scripts/__pycache__/format_text.pyc", excluded) + self.assertEqual(manifest.behavior["minimum_risk"], "R2") + self.assertEqual(manifest.behavior["package_minimum_risk"], "R0") + + def test_known_sensitive_filename_and_secret_content_block_before_write(self) -> None: + cases = ( + ("token.txt", "ordinary text\n", "suspicious-sensitive-filename"), + ( + "scripts/config.py", + "API_KEY = 'live-value-that-must-not-ship'\n", + "credential-like-assignment", + ), + ( + "scripts/redacted.py", + "TOKEN = 'redacted-token'\n", + "credential-like-assignment", + ), + ( + "references/config.md", + "password: dummy-placeholder\n", + "credential-like-assignment", + ), + ( + "references/root.crt", + "certificate bytes\n", + "known-sensitive-path", + ), + ( + "references/trust.md", + "-----BEGIN CERTIFICATE-----\nMIIB\n-----END CERTIFICATE-----\n", + "certificate-material", + ), + ) + for relative, content, rule_id in cases: + with self.subTest(relative=relative), tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + candidate = self._skill(root) + path = candidate / relative + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(content, encoding="utf-8") + output = root / "approved-team-output" + output.mkdir() + + with self.assertRaises(SensitiveMaterialError) as raised: + prepare_team_delivery_action_target( + candidate, + output, + host="codex", + scope_digest=digest_json({"scope": relative}), + ) + + self.assertTrue( + any( + item["rule_id"] == rule_id + for item in raised.exception.findings + ) + ) + self.assertEqual(tuple(output.iterdir()), ()) + self.assertNotIn("live-value-that-must-not-ship", str(raised.exception)) + + def test_runtime_credential_access_is_not_mislabeled_as_bundled_secret(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + candidate = self._skill(root) + (candidate / "scripts" / "format_text.py").write_text( + "import os\nTOKEN = os.environ.get('SERVICE_TOKEN')\n", + encoding="utf-8", + ) + output = root / "approved-team-output" + output.mkdir() + + with self.assertRaises(TeamDeliveryIntegrationRequired) as raised: + prepare_team_delivery_action_target( + candidate, + output, + host="codex", + scope_digest=digest_json({"scope": "credential-access"}), + ) + + self.assertIn( + "process-plan-module-result-graph-adapter", + raised.exception.integration_requests, + ) + self.assertNotIsInstance(raised.exception, SensitiveMaterialError) + + def test_wrong_target_missing_quality_action_and_plain_bool_cannot_authorize(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + candidate = self._skill(root) + for attack in ("wrong-target", "missing-quality-action"): + with self.subTest(attack=attack): + output = root / f"output-{attack}" + output.mkdir() + actions = ( + (TEAM_DELIVERY_ACTION,) + if attack == "missing-quality-action" + else (TEAM_DELIVERY_ACTION, QUALITY_UNVERIFIED_ACTION) + ) + target, _grant, event, scope_digest, workflow_root = self._authority( + candidate, + output, + actions=actions, + target_digest=( + digest_json({"forged": True}) + if attack == "wrong-target" + else None + ), + workflow_id=f"workflow-{attack}", + ) + with mock.patch.object( + team_delivery, + "_codex_workflow_root", + return_value=workflow_root, + ): + with self.assertRaises(TeamDeliveryAuthorizationError): + build_team_delivery( + candidate, + output, + host="codex", + workflow_id=event.workflow_id, + scope_digest=scope_digest, + risk_digest=target.risk_commitment_digest, + ) + self.assertEqual(tuple(output.iterdir()), ()) + + output = root / "output-bool" + output.mkdir() + with self.assertRaises(TypeError): + prepare_team_delivery_action_target( + candidate, + output, + host="codex", + scope_digest=digest_json({"scope": "bool"}), + frozen_scope=True, # type: ignore[call-arg] + ) + + def test_public_build_has_no_caller_sources_and_missing_adapter_blocks(self) -> None: + parameters = inspect.signature(build_team_delivery).parameters + for forbidden in ( + "workflow_events", + "authorization_event_digest", + "frozen_scope", + "workflow_path", + "workflow_handle", + ): + self.assertNotIn(forbidden, parameters) + + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + candidate = self._skill(root) + output = root / "approved-team-output" + output.mkdir() + scope_digest = digest_json({"scope": "missing-trusted-source"}) + target = prepare_team_delivery_action_target( + candidate, + output, + host="codex", + scope_digest=scope_digest, + ) + with mock.patch.object( + team_delivery, + "_codex_workflow_root", + return_value=root / "missing-workflow-root", + ): + with self.assertRaises(TeamDeliveryIntegrationRequired) as raised: + build_team_delivery( + candidate, + output, + host="codex", + workflow_id="missing-workflow", + scope_digest=scope_digest, + risk_digest=target.risk_commitment_digest, + ) + self.assertIn( + "trusted-workflow-event-source-adapter", + raised.exception.integration_requests, + ) + self.assertEqual(tuple(output.iterdir()), ()) + + def test_source_drift_during_builder_is_detected_and_artifacts_removed(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + candidate = self._skill(root) + output = root / "approved-team-output" + output.mkdir() + target, _grant, event, scope_digest, workflow_root = self._authority( + candidate, output + ) + real_builder = team_delivery.build_distribution + + def mutate_after_build(*args, **kwargs): + result = real_builder(*args, **kwargs) + (candidate / "references" / "format.md").write_text( + "changed during projection\n", encoding="utf-8" + ) + return result + + with ( + mock.patch.object( + team_delivery, + "_codex_workflow_root", + return_value=workflow_root, + ), + mock.patch.object( + team_delivery, + "build_distribution", + side_effect=mutate_after_build, + ), + ): + with self.assertRaises(TeamDeliveryIntegrityError): + build_team_delivery( + candidate, + output, + host="codex", + workflow_id=event.workflow_id, + scope_digest=scope_digest, + risk_digest=target.risk_commitment_digest, + ) + + self.assertEqual(tuple(output.iterdir()), ()) + + def test_cleanup_never_deletes_a_concurrently_created_foreign_package(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + candidate = self._skill(root) + output = root / "approved-team-output" + output.mkdir() + target, _grant, event, scope_digest, workflow_root = self._authority( + candidate, output + ) + real_builder = team_delivery.build_distribution + + def create_foreign_then_build(*args, **kwargs): + foreign = output / "package" + foreign.mkdir() + (foreign / "foreign-marker.txt").write_text( + "foreign\n", encoding="utf-8" + ) + return real_builder(*args, **kwargs) + + with ( + mock.patch.object( + team_delivery, + "_codex_workflow_root", + return_value=workflow_root, + ), + mock.patch.object( + team_delivery, + "build_distribution", + side_effect=create_foreign_then_build, + ), + ): + with self.assertRaises(PackagingError): + build_team_delivery( + candidate, + output, + host="codex", + workflow_id=event.workflow_id, + scope_digest=scope_digest, + risk_digest=target.risk_commitment_digest, + ) + + self.assertEqual( + (output / "package" / "foreign-marker.txt").read_text( + encoding="utf-8" + ), + "foreign\n", + ) + + def test_cleanup_refuses_replaced_output_inode_and_leaves_both_trees(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + candidate = self._skill(root) + output = root / "approved-team-output" + output.mkdir() + target, _grant, event, scope_digest, workflow_root = self._authority( + candidate, output + ) + displaced = root / "displaced-team-output" + real_builder = team_delivery.build_distribution + + def replace_output_after_build(*args, **kwargs): + result = real_builder(*args, **kwargs) + output.rename(displaced) + output.mkdir() + (output / "package").mkdir() + (output / "package" / "foreign-marker.txt").write_text( + "foreign\n", encoding="utf-8" + ) + return result + + with ( + mock.patch.object( + team_delivery, + "_codex_workflow_root", + return_value=workflow_root, + ), + mock.patch.object( + team_delivery, + "build_distribution", + side_effect=replace_output_after_build, + ), + ): + with self.assertRaises(TeamDeliveryIntegrityError): + build_team_delivery( + candidate, + output, + host="codex", + workflow_id=event.workflow_id, + scope_digest=scope_digest, + risk_digest=target.risk_commitment_digest, + ) + + self.assertTrue((displaced / "package" / "SKILL.md").is_file()) + self.assertTrue((displaced / "package.manifest.json").is_file()) + self.assertEqual( + (output / "package" / "foreign-marker.txt").read_text( + encoding="utf-8" + ), + "foreign\n", + ) + + def test_cleanup_refuses_replaced_parent_and_leaves_foreign_paths(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + workspace = root / "workspace" + workspace.mkdir(mode=0o700) + workspace.chmod(0o700) + candidate = self._skill(workspace) + output = workspace / "approved-team-output" + output.mkdir() + target, _grant, event, scope_digest, workflow_root = self._authority( + candidate, output + ) + displaced = root / "displaced-workspace" + real_builder = team_delivery.build_distribution + + def replace_parent_after_build(*args, **kwargs): + result = real_builder(*args, **kwargs) + workspace.rename(displaced) + workspace.mkdir() + replacement_output = workspace / "approved-team-output" + replacement_output.mkdir() + (replacement_output / "package").mkdir() + (replacement_output / "package" / "foreign-marker.txt").write_text( + "foreign\n", encoding="utf-8" + ) + return result + + with ( + mock.patch.object( + team_delivery, + "_codex_workflow_root", + return_value=workflow_root, + ), + mock.patch.object( + team_delivery, + "build_distribution", + side_effect=replace_parent_after_build, + ), + ): + with self.assertRaises(TeamDeliveryIntegrityError): + build_team_delivery( + candidate, + output, + host="codex", + workflow_id=event.workflow_id, + scope_digest=scope_digest, + risk_digest=target.risk_commitment_digest, + ) + + self.assertTrue( + ( + displaced + / "approved-team-output" + / "package" + / "SKILL.md" + ).is_file() + ) + self.assertEqual( + ( + workspace + / "approved-team-output" + / "package" + / "foreign-marker.txt" + ).read_text(encoding="utf-8"), + "foreign\n", + ) + + def test_caller_scope_mapping_is_not_an_authority_input(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + candidate = self._skill(root) + output = root / "approved-team-output" + output.mkdir() + + target = prepare_team_delivery_action_target( + candidate, + output, + host="codex", + scope_digest=digest_json({"scope": "fixed-explicit-only"}), + ) + self.assertFalse(target.automatic_routing_requested) + self.assertFalse(target.automatic_routing_effective) + with self.assertRaises(TypeError): + prepare_team_delivery_action_target( + candidate, + output, + host="codex", + scope_digest=digest_json({"scope": "forged-automatic"}), + frozen_scope={ # type: ignore[call-arg] + "automatic_routing": True, + "quality_unverified": False, + }, + ) + + def test_explicit_invocation_removes_automatic_routing_without_shared_trigger(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + candidate = self._skill(root) + output = root / "approved-team-output" + output.mkdir() + + manifest, _target, _event = self._build(candidate, output) + + self.assertEqual(manifest.automatic_routing["claim_status"], "removed") + self.assertFalse(manifest.automatic_routing["scope_requested"]) + self.assertFalse(manifest.automatic_routing["shared_trigger_derived"]) + self.assertEqual(manifest.automatic_routing["required_evidence"], []) + self.assertIn("automatic-routing", manifest.claims["removed"]) + + def test_caller_verified_quality_claim_is_downgraded_and_compatibility_unknown(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + candidate = self._skill(root) + output = root / "approved-team-output" + output.mkdir() + quality = { + "candidate_digest": None, + "verified_claims": ["quality-gate-passed"], + "unverified_claims": ["routing-quality"], + "blocked_claims": [], + "removed_claims": [], + "formal_outcome": "unverified", + } + # project_untrusted_quality accepts an omitted binding or the real + # candidate digest. Remove the explicit null for the ordinary + # caller-authored summary exercised here. + quality.pop("candidate_digest") + + manifest, _target, _event = self._build( + candidate, output, quality_summary=quality + ) + + self.assertEqual(manifest.quality_claim_projection["verified_claims"], []) + self.assertIn( + "quality-gate-passed", + manifest.quality_claim_projection["blocked_claims"], + ) + self.assertEqual(manifest.compatibility["status"], "unknown") + self.assertIsNone(manifest.compatibility["evidence_digest"]) + self.assertNotIn("host-compatibility", manifest.claims["verified"]) + + def test_manifest_and_package_byte_tampering_fail_runtime_verification(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + candidate = self._skill(root) + output = root / "approved-team-output" + output.mkdir() + manifest, _target, _event = self._build(candidate, output) + + forged = deepcopy(manifest.to_dict()) + forged["compatibility"]["status"] = "verified" + forged["content_digest"] = digest_json( + {key: value for key, value in forged.items() if key != "content_digest"} + ) + schema = json.loads( + ( + SCRIPTS / "schemas" / "team-delivery-manifest.schema.json" + ).read_text(encoding="utf-8") + ) + self.assertTrue(_validate_schema(forged, schema, schema)) + with self.assertRaises(ValueError): + self._validate_manifest(forged) + + claims_attack = json.loads( + (output / "team-delivery-manifest.json").read_text( + encoding="utf-8" + ) + ) + for status, claim in ( + ("blocked", "host-compatibility"), + ("removed", "automatic-routing"), + ("removed", "formal-adoption"), + ): + claims_attack["claims"][status].remove(claim) + claims_attack["claims"]["verified"].append(claim) + claims_attack["claims"]["verified"].sort() + claims_attack["content_digest"] = digest_json( + { + key: value + for key, value in claims_attack.items() + if key != "content_digest" + } + ) + self.assertTrue(_validate_schema(claims_attack, schema, schema)) + with mock.patch.object( + team_delivery, + "verify_distribution", + side_effect=AssertionError("byte verifier must not run"), + ): + with self.assertRaises(ValueError): + self._validate_manifest(claims_attack) + + semantic_tamper = deepcopy(manifest.to_dict()) + semantic_tamper["package"]["file_count"] += 1 + semantic_tamper["content_digest"] = digest_json( + { + key: value + for key, value in semantic_tamper.items() + if key != "content_digest" + } + ) + self.assertEqual([], _validate_schema(semantic_tamper, schema, schema)) + with self.assertRaises(ValueError): + self._validate_manifest(semantic_tamper) + + selected = output / "package" / "references" / "format.md" + selected.chmod(stat.S_IMODE(selected.stat().st_mode) | stat.S_IWUSR) + selected.write_text("tampered\n", encoding="utf-8") + with self.assertRaises(TeamDeliveryIntegrityError): + self._validate_manifest( + manifest, + package_root=output / "package", + builder_manifest_path=output / "package.manifest.json", + ) + + def test_manifest_risk_authority_and_integration_requests_cannot_be_self_minted(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + candidate = self._skill(root) + output = root / "approved-team-output" + output.mkdir() + manifest, _target, _event = self._build(candidate, output) + + risk_attack = deepcopy(manifest.to_dict()) + risk_attack["behavior"]["mandatory_controls"] = [ + "caller-selected-control" + ] + risk_attack["content_digest"] = digest_json( + { + key: value + for key, value in risk_attack.items() + if key != "content_digest" + } + ) + with self.assertRaises(ValueError): + self._validate_manifest(risk_attack) + + request_attack = deepcopy(manifest.to_dict()) + request_attack["integration_requests"].append("caller-selected-request") + request_attack["integration_requests"].sort() + request_attack["content_digest"] = digest_json( + { + key: value + for key, value in request_attack.items() + if key != "content_digest" + } + ) + with self.assertRaises(ValueError): + self._validate_manifest(request_attack) + + authority_attack = deepcopy(manifest.to_dict()) + quality = authority_attack["quality_claim_projection"] + quality["integration_requests"].append("caller-selected-request") + quality["integration_requests"].sort() + quality["content_digest"] = digest_json( + {key: value for key, value in quality.items() if key != "content_digest"} + ) + target = authority_attack["authority"]["action_target"] + target["quality_projection_digest"] = quality["content_digest"] + target["content_digest"] = digest_json( + {key: value for key, value in target.items() if key != "content_digest"} + ) + authority_attack["authority"]["action_target_digest"] = target[ + "content_digest" + ] + authority_attack["integration_requests"].append( + "caller-selected-request" + ) + authority_attack["integration_requests"].sort() + authority_attack["content_digest"] = digest_json( + { + key: value + for key, value in authority_attack.items() + if key != "content_digest" + } + ) + with self.assertRaises(TeamDeliveryAuthorizationError): + self._validate_manifest(authority_attack) + + def test_current_byte_validator_rejects_extra_sibling_and_hardlinked_manifest(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + candidate = self._skill(root) + output = root / "approved-team-output" + output.mkdir() + manifest, _target, _event = self._build(candidate, output) + + extra = output / "foreign-sibling.txt" + extra.write_text("foreign\n", encoding="utf-8") + with self.assertRaises(TeamDeliveryIntegrityError): + self._validate_manifest(manifest) + extra.unlink() + + hardlink = root / "builder-manifest-hardlink.json" + try: + os.link(output / "package.manifest.json", hardlink) + except OSError: + self.skipTest("hard links are unavailable") + with self.assertRaises(TeamDeliveryIntegrityError): + self._validate_manifest(manifest) + + def test_output_root_cannot_be_host_skill_root_or_existing_directory(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + candidate = self._skill(root) + host_root = root / ".codex" / "skills" + host_root.mkdir(parents=True) + with self.assertRaises(UnsafeTeamOutputError): + prepare_team_delivery_action_target( + candidate, + host_root, + host="codex", + scope_digest=digest_json({"scope": "host-root"}), + ) + + output = root / "existing-output" + output.mkdir() + (output / "keep.txt").write_text("existing\n", encoding="utf-8") + with self.assertRaises(UnsafeTeamOutputError): + prepare_team_delivery_action_target( + candidate, + output, + host="codex", + scope_digest=digest_json({"scope": "existing"}), + ) + self.assertEqual((output / "keep.txt").read_text(encoding="utf-8"), "existing\n") + + def test_output_root_and_parent_must_be_current_user_private_writers(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + candidate = self._skill(root) + + unsafe_parent = root / "unsafe-parent" + unsafe_parent.mkdir() + unsafe_parent.chmod(0o777) + output = unsafe_parent / "output" + output.mkdir() + with self.assertRaises(UnsafeTeamOutputError): + prepare_team_delivery_action_target( + candidate, + output, + host="codex", + scope_digest=digest_json({"scope": "unsafe-parent"}), + ) + + safe_parent = root / "safe-parent" + safe_parent.mkdir(mode=0o700) + unsafe_output = safe_parent / "output" + unsafe_output.mkdir() + unsafe_output.chmod(0o770) + with self.assertRaises(UnsafeTeamOutputError): + prepare_team_delivery_action_target( + candidate, + unsafe_output, + host="codex", + scope_digest=digest_json({"scope": "unsafe-output"}), + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/dev/schema-tests/test_delivery_contracts.py b/dev/schema-tests/test_delivery_contracts.py new file mode 100644 index 0000000..e2e6747 --- /dev/null +++ b/dev/schema-tests/test_delivery_contracts.py @@ -0,0 +1,380 @@ +from __future__ import annotations + +from copy import deepcopy +from dataclasses import replace +import inspect +import json +from pathlib import Path +import stat +import sys +import tempfile +import unittest +from unittest import mock + + +WORKSPACE = Path(__file__).resolve().parents[2] +SCRIPTS = WORKSPACE / "runtime" / "skill-optimizer" / "scripts" +SCHEMAS = SCRIPTS / "schemas" +sys.path.insert(0, str(SCRIPTS)) + +from core.behavior_risk import ( # noqa: E402 + BehaviorReportValidationError, + audit_behavior_risk, + report_from_dict, +) +from core.canonical import digest_json # noqa: E402 +from core.delivery import ( # noqa: E402 + DeliveryError, + DeliveryTarget, + build_behavior_and_delivery_summary, + evaluate_delivery_eligibility, +) +from core.workflow import ( # noqa: E402 + AuthorizationGrant, + WorkflowActor, + WorkflowEvent, + WorkflowEventType, + WorkflowState, +) +import packaging.team_delivery as team_delivery # noqa: E402 +from packaging.team_delivery import ( # noqa: E402 + TeamDeliveryIntegrityError, + build_team_delivery, + prepare_team_delivery_action_target, + validate_team_delivery_manifest, +) +from validators.contracts import _validate_schema # noqa: E402 + + +def _load_schema(filename: str) -> dict: + return json.loads((SCHEMAS / filename).read_text(encoding="utf-8")) + + +def _schema_errors(filename: str, document: dict) -> list[str]: + schema = _load_schema(filename) + return _validate_schema(document, schema, schema) + + +def _write_safe_skill(root: Path) -> Path: + candidate = root / "candidate" + candidate.mkdir() + (candidate / "SKILL.md").write_text( + "---\n" + "name: quiet-summary\n" + "description: Summarize supplied prose without side effects.\n" + "---\n" + "# Quiet summary\n\n" + "Return a concise summary of the supplied prose.\n", + encoding="utf-8", + ) + return candidate + + +def _authorization_event( + *, + workflow_id: str, + scope_digest: str, + risk_digest: str, + target_digest: str, + kind: str, + action: str, +) -> WorkflowEvent: + grant = AuthorizationGrant( + authorization_id=f"{workflow_id}-authorization", + kind=kind, + status="granted", + actions=(action, "quality_unverified"), + scope_digest=scope_digest, + risk_digest=risk_digest, + target_digest=target_digest, + expires_at="2099-01-01T00:00:00Z", + ) + return WorkflowEvent.create( + sequence=0, + workflow_id=workflow_id, + event_type=WorkflowEventType.AUTHORIZATION, + from_state=WorkflowState.INTAKE, + to_state=WorkflowState.INTAKE, + actor=WorkflowActor.USER, + payload={"grant": grant.to_dict()}, + created_at="2026-07-26T00:00:00+00:00", + previous_event_digest=None, + ) + + +class BehaviorSchemaRuntimeParityTests(unittest.TestCase): + def test_behavior_positive_fixture_passes_schema_and_runtime(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + candidate = _write_safe_skill(Path(temporary)) + report = audit_behavior_risk(candidate).to_dict() + self.assertEqual([], _schema_errors("behavior-risk-report.schema.json", report)) + self.assertEqual(report, report_from_dict(report).to_dict()) + + def test_behavior_closed_negative_fixture_hits_both_surfaces(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + candidate = _write_safe_skill(Path(temporary)) + report = audit_behavior_risk(candidate).to_dict() + report["runtime_safe"] = True + self.assertTrue(_schema_errors("behavior-risk-report.schema.json", report)) + with self.assertRaises(BehaviorReportValidationError): + report_from_dict(report) + + def test_behavior_semantic_digest_and_rck_are_rebuilt_at_runtime(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + candidate = _write_safe_skill(Path(temporary)) + report = audit_behavior_risk(candidate).to_dict() + tampered = deepcopy(report) + tampered["mandatory_controls"] = ["caller-selected-control"] + unsigned = {key: value for key, value in tampered.items() if key != "content_digest"} + tampered["content_digest"] = digest_json(unsigned) + # Shape validation is intentionally not treated as semantic proof. + self.assertEqual( + [], _schema_errors("behavior-risk-report.schema.json", tampered) + ) + with self.assertRaisesRegex( + BehaviorReportValidationError, "mandatory_controls" + ): + report_from_dict(tampered) + + +class DeliveryTrustBoundaryTests(unittest.TestCase): + def _eligible_personal(self, root: Path, *, quality_summary=None): + candidate = _write_safe_skill(root) + personal_root = root / ".codex" / "skills" + personal_root.mkdir(parents=True) + target = personal_root / "quiet-summary" + report = audit_behavior_risk(candidate) + scope_digest = digest_json({"scope": "personal-test"}) + eligibility = evaluate_delivery_eligibility( + candidate, + delivery_target=DeliveryTarget.PERSONAL_INSTALL, + target=target, + approved_root=personal_root, + scope_digest=scope_digest, + workflow_id="delivery-personal", + host="codex", + quality_summary=quality_summary, + ) + return eligibility, report + + def test_generic_evaluator_blocks_without_trusted_workflow_source(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + eligibility, report = self._eligible_personal(Path(temporary)) + self.assertFalse(eligibility.eligible) + self.assertTrue(eligibility.quality_unverified) + self.assertEqual(report.minimum_risk, eligibility.minimum_risk) + self.assertEqual(report.mandatory_controls, eligibility.mandatory_controls) + self.assertEqual((), eligibility.quality_projection.verified_claims) + self.assertIn( + "trusted-workflow-event-source-adapter", + eligibility.integration_requests, + ) + self.assertNotIn( + "workflow_events", + inspect.signature(evaluate_delivery_eligibility).parameters, + ) + + def test_caller_verified_and_eligible_fields_never_upgrade_claims(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + eligibility, _ = self._eligible_personal( + Path(temporary), + quality_summary={ + "verified_claims": ["formal-adoption"], + "unverified_claims": [], + "blocked_claims": [], + "removed_claims": [], + "eligible": True, + }, + ) + self.assertFalse(eligibility.eligible) + self.assertEqual((), eligibility.quality_projection.verified_claims) + self.assertIn( + "formal-adoption", eligibility.quality_projection.blocked_claims + ) + self.assertIn( + "reject-caller-authored-verified-claims", + eligibility.integration_requests, + ) + + def test_required_claim_and_automatic_routing_fail_closed(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + candidate = _write_safe_skill(root) + output = root / "approved-team-output" + output.mkdir() + report = audit_behavior_risk(candidate) + scope_digest = digest_json({"scope": "team-test"}) + eligibility = evaluate_delivery_eligibility( + candidate, + delivery_target=DeliveryTarget.TEAM_PACKAGE, + target=output, + approved_root=output, + scope_digest=scope_digest, + workflow_id="delivery-team", + host="codex", + required_claims=("formal-adoption",), + ) + self.assertFalse(eligibility.eligible) + self.assertIn( + "gate1-quality-raw-graph-adapter", + eligibility.integration_requests, + ) + self.assertIn( + "automatic-routing-scope-adapter", + eligibility.integration_requests, + ) + + def test_forged_typed_eligibility_and_receipt_projection_are_rejected(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + eligibility, report = self._eligible_personal(Path(temporary)) + with self.assertRaisesRegex(ValueError, "eligible=true"): + replace(eligibility, eligible=True, reasons=()) + with self.assertRaisesRegex(DeliveryError, "receipt projection"): + build_behavior_and_delivery_summary( + report, + eligibility, + delivery_receipt={"status": "installed", "forged": True}, + ) + + def test_delivery_rejects_candidate_root_symlink(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + candidate = _write_safe_skill(root) + link = root / "candidate-link" + link.symlink_to(candidate, target_is_directory=True) + personal_root = root / ".codex" / "skills" + personal_root.mkdir(parents=True) + with self.assertRaisesRegex(DeliveryError, "symbolic link"): + evaluate_delivery_eligibility( + link, + delivery_target=DeliveryTarget.PERSONAL_INSTALL, + target=personal_root / "quiet-summary", + approved_root=personal_root, + scope_digest=digest_json({"scope": "symlink"}), + ) + + +class TeamManifestSchemaRuntimeParityTests(unittest.TestCase): + def _build_manifest(self, root: Path): + candidate = _write_safe_skill(root) + output = root / "approved-team-output" + output.mkdir() + scope_digest = digest_json({"workflow_scope": "explicit-team"}) + target = prepare_team_delivery_action_target( + candidate, + output, + host="codex", + scope_digest=scope_digest, + ) + event = _authorization_event( + workflow_id="schema-team-delivery", + scope_digest=scope_digest, + risk_digest=target.risk_commitment_digest, + target_digest=target.content_digest, + kind="external_write", + action="team_delivery", + ) + workflow_root = root / "trusted-codex-workflows" + workflow_root.mkdir(mode=0o700) + workflow_root.chmod(0o700) + workflow_path = workflow_root / team_delivery._workflow_event_filename( + event.workflow_id + ) + workflow_path.write_text( + json.dumps(event.to_dict(), ensure_ascii=True, sort_keys=True) + "\n", + encoding="utf-8", + ) + workflow_path.chmod(0o600) + with mock.patch.object( + team_delivery, "_codex_workflow_root", return_value=workflow_root + ): + manifest = build_team_delivery( + candidate, + output, + host="codex", + workflow_id=event.workflow_id, + scope_digest=scope_digest, + risk_digest=target.risk_commitment_digest, + expected_candidate_digest=target.candidate_digest, + ) + return manifest, output + + def _validate_manifest(self, manifest): + output_root = ( + Path(manifest.output_root) + if hasattr(manifest, "output_root") + else Path(manifest["output_root"]) + ) + workflow_root = output_root.parent / "trusted-codex-workflows" + with mock.patch.object( + team_delivery, "_codex_workflow_root", return_value=workflow_root + ): + return validate_team_delivery_manifest(manifest) + + def test_team_manifest_positive_fixture_passes_schema_and_current_bytes(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + manifest, _output = self._build_manifest(Path(temporary)) + document = manifest.to_dict() + self.assertEqual( + [], _schema_errors("team-delivery-manifest.schema.json", document) + ) + self.assertEqual( + manifest.content_digest, + self._validate_manifest(document).content_digest, + ) + + def test_team_manifest_negative_fixture_hits_schema_and_runtime(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + manifest, _output = self._build_manifest(Path(temporary)) + forged = deepcopy(manifest.to_dict()) + forged["compatibility"]["status"] = "verified" + forged["content_digest"] = digest_json( + {key: value for key, value in forged.items() if key != "content_digest"} + ) + self.assertTrue( + _schema_errors("team-delivery-manifest.schema.json", forged) + ) + with self.assertRaises(ValueError): + self._validate_manifest(forged) + + def test_team_manifest_runtime_rechecks_package_bytes(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + manifest, output = self._build_manifest(Path(temporary)) + skill = output / "package" / "SKILL.md" + skill.chmod(stat.S_IMODE(skill.stat().st_mode) | stat.S_IWUSR) + skill.write_text("tampered\n", encoding="utf-8") + with self.assertRaises(TeamDeliveryIntegrityError): + self._validate_manifest(manifest.to_dict()) + + def test_current_byte_risk_cannot_be_lowered_by_delivery_target(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + candidate = root / "candidate" + candidate.mkdir() + (candidate / "SKILL.md").write_text( + "---\nname: remote-writer\ndescription: Publish a report.\n---\n" + "# Remote writer\n\nPublish and upload the report through a network API.\n", + encoding="utf-8", + ) + personal_root = root / ".codex" / "skills" + personal_root.mkdir(parents=True) + target = personal_root / "remote-writer" + report = audit_behavior_risk(candidate) + eligibility = evaluate_delivery_eligibility( + candidate, + delivery_target=DeliveryTarget.PERSONAL_INSTALL, + target=target, + approved_root=personal_root, + scope_digest=digest_json({"scope": "risk-test"}), + ) + self.assertGreaterEqual(report.minimum_risk.severity, 2) + self.assertEqual(report.minimum_risk, eligibility.minimum_risk) + self.assertEqual(report.mandatory_controls, eligibility.mandatory_controls) + self.assertEqual( + report.mandatory_capabilities, eligibility.mandatory_capabilities + ) + self.assertFalse(eligibility.eligible) + + +if __name__ == "__main__": + unittest.main() diff --git a/runtime/skill-optimizer/scripts/core/behavior_risk.py b/runtime/skill-optimizer/scripts/core/behavior_risk.py new file mode 100644 index 0000000..d1112ad --- /dev/null +++ b/runtime/skill-optimizer/scripts/core/behavior_risk.py @@ -0,0 +1,2229 @@ +"""Current-byte behavior risk audit for Skill candidates. + +The scanner deliberately produces evidence, not a runtime-safety certificate. +It reads a candidate through no-follow file descriptors, hashes exactly the +bytes it inspected, and compares that snapshot with a fresh tree digest before +returning. Delivery and installation callers must run this audit themselves; +a caller-authored report is never authorization for a side effect. +""" + +from __future__ import annotations + +import ast +from dataclasses import dataclass +import errno +import os +from pathlib import Path, PurePosixPath +import re +import stat +from typing import Any, Iterable, Mapping, Sequence + +from .canonical import ( + ClosedStrEnum, + RiskDimension, + RiskLevel, + digest_bytes, + digest_json, + tree_digest, +) +from .policy import ( + RISK_DIMENSION_MINIMUM_LEVEL, + RiskFinding, + mandatory_capability_ids, + mandatory_control_ids, + maximum_risk, +) + + +BEHAVIOR_RISK_SCHEMA_VERSION = "1.0.0" +_DIGEST_RE = re.compile(r"sha256:[0-9a-f]{64}\Z") +_FINDING_ID_RE = re.compile(r"behavior-[0-9a-f]{32}\Z") + + +class BehaviorRiskError(ValueError): + """Base error for behavior audit and report validation failures.""" + + +class BehaviorAuditError(BehaviorRiskError): + """The candidate cannot be safely and completely inspected.""" + + +class BehaviorCandidateChangedError(BehaviorAuditError): + """The candidate changed while its current bytes were being inspected.""" + + +class BehaviorLimitError(BehaviorAuditError): + """The candidate exceeds a frozen scanner resource limit.""" + + +class BehaviorReportValidationError(BehaviorRiskError): + """A BehaviorRiskReport violates its closed semantic contract.""" + + +class BehaviorEvidenceState(ClosedStrEnum): + """How static evidence supports a finding. + + ``REQUIRES_RUNTIME_ENFORCEMENT`` is intentionally distinct from + ``UNKNOWN``: the scanner observed a construct whose safety property can + only be enforced while it runs. The boolean on :class:`BehaviorFinding` + also lets observed or inferred behaviors require runtime controls. + """ + + OBSERVED = "observed" + INFERRED = "inferred" + UNKNOWN = "unknown" + REQUIRES_RUNTIME_ENFORCEMENT = "requires_runtime_enforcement" + + +@dataclass(frozen=True) +class BehaviorScanLimits: + """Hard resource bounds applied before an audit can return a report.""" + + max_files: int = 2048 + max_total_bytes: int = 32 * 1024 * 1024 + max_file_bytes: int = 4 * 1024 * 1024 + max_depth: int = 32 + max_path_bytes: int = 1024 + max_ast_nodes: int = 100_000 + + def __post_init__(self) -> None: + for field_name in ( + "max_files", + "max_total_bytes", + "max_file_bytes", + "max_depth", + "max_path_bytes", + "max_ast_nodes", + ): + value = getattr(self, field_name) + if isinstance(value, bool) or not isinstance(value, int) or value <= 0: + raise ValueError(f"{field_name} must be a positive integer") + + +@dataclass(frozen=True) +class _RuleSpec: + dimension: RiskDimension + evidence_state: BehaviorEvidenceState + runtime_enforcement: bool + sensitive_material: bool = False + + +def _rule( + dimension: RiskDimension, + state: BehaviorEvidenceState, + runtime: bool, + *, + sensitive: bool = False, +) -> _RuleSpec: + return _RuleSpec(dimension, state, runtime, sensitive) + + +_RULES: Mapping[str, _RuleSpec] = { + "skill.local-write": _rule( + RiskDimension.LOCAL_MUTATION, BehaviorEvidenceState.INFERRED, True + ), + "skill.external-write": _rule( + RiskDimension.EXTERNAL_WRITE, BehaviorEvidenceState.INFERRED, True + ), + "skill.delete": _rule( + RiskDimension.IRREVERSIBLE_CHANGE, BehaviorEvidenceState.INFERRED, True + ), + "skill.network": _rule( + RiskDimension.REAL_EXTERNAL_DEPENDENCY, + BehaviorEvidenceState.INFERRED, + True, + ), + "skill.credential-access": _rule( + RiskDimension.CREDENTIAL_OR_SENSITIVE_DATA, + BehaviorEvidenceState.INFERRED, + True, + ), + "skill.sensitive-retention": _rule( + RiskDimension.CREDENTIAL_OR_SENSITIVE_DATA, + BehaviorEvidenceState.INFERRED, + True, + ), + "skill.commit": _rule( + RiskDimension.LOCAL_MUTATION, BehaviorEvidenceState.INFERRED, True + ), + "skill.auto-routing": _rule( + RiskDimension.SHARED_TRIGGER, BehaviorEvidenceState.INFERRED, True + ), + "skill.shell": _rule( + RiskDimension.HIGH_IMPACT_EVALUATION, + BehaviorEvidenceState.INFERRED, + True, + ), + "skill.implicit-install": _rule( + RiskDimension.LOCAL_MUTATION, BehaviorEvidenceState.INFERRED, True + ), + "python.local-write": _rule( + RiskDimension.LOCAL_MUTATION, BehaviorEvidenceState.OBSERVED, True + ), + "python.delete": _rule( + RiskDimension.IRREVERSIBLE_CHANGE, + BehaviorEvidenceState.OBSERVED, + True, + ), + "python.network": _rule( + RiskDimension.REAL_EXTERNAL_DEPENDENCY, + BehaviorEvidenceState.OBSERVED, + True, + ), + "python.network-import": _rule( + RiskDimension.REAL_EXTERNAL_DEPENDENCY, + BehaviorEvidenceState.INFERRED, + True, + ), + "python.external-write": _rule( + RiskDimension.EXTERNAL_WRITE, BehaviorEvidenceState.OBSERVED, True + ), + "python.subprocess": _rule( + RiskDimension.HIGH_IMPACT_EVALUATION, + BehaviorEvidenceState.OBSERVED, + True, + ), + "python.subprocess-import": _rule( + RiskDimension.HIGH_IMPACT_EVALUATION, + BehaviorEvidenceState.INFERRED, + True, + ), + "python.shell": _rule( + RiskDimension.HIGH_IMPACT_EVALUATION, + BehaviorEvidenceState.OBSERVED, + True, + ), + "python.environment-read": _rule( + RiskDimension.CREDENTIAL_OR_SENSITIVE_DATA, + BehaviorEvidenceState.INFERRED, + True, + ), + "python.credential-read": _rule( + RiskDimension.CREDENTIAL_OR_SENSITIVE_DATA, + BehaviorEvidenceState.OBSERVED, + True, + ), + "python.credential-import": _rule( + RiskDimension.CREDENTIAL_OR_SENSITIVE_DATA, + BehaviorEvidenceState.INFERRED, + True, + ), + "python.implicit-install": _rule( + RiskDimension.LOCAL_MUTATION, BehaviorEvidenceState.OBSERVED, True + ), + "python.unbounded-write-path": _rule( + RiskDimension.EXTERNAL_WRITE, BehaviorEvidenceState.UNKNOWN, True + ), + "python.dynamic-execution": _rule( + RiskDimension.HIGH_IMPACT_EVALUATION, + BehaviorEvidenceState.REQUIRES_RUNTIME_ENFORCEMENT, + True, + ), + "python.syntax-unknown": _rule( + RiskDimension.HIGH_IMPACT_EVALUATION, + BehaviorEvidenceState.UNKNOWN, + True, + ), + "python.unanalyzed-call": _rule( + RiskDimension.HIGH_IMPACT_EVALUATION, + BehaviorEvidenceState.UNKNOWN, + True, + ), + "script.local-write": _rule( + RiskDimension.LOCAL_MUTATION, BehaviorEvidenceState.OBSERVED, True + ), + "script.delete": _rule( + RiskDimension.IRREVERSIBLE_CHANGE, + BehaviorEvidenceState.OBSERVED, + True, + ), + "script.network": _rule( + RiskDimension.REAL_EXTERNAL_DEPENDENCY, + BehaviorEvidenceState.OBSERVED, + True, + ), + "script.external-write": _rule( + RiskDimension.EXTERNAL_WRITE, BehaviorEvidenceState.OBSERVED, True + ), + "script.shell": _rule( + RiskDimension.HIGH_IMPACT_EVALUATION, + BehaviorEvidenceState.OBSERVED, + True, + ), + "script.environment-read": _rule( + RiskDimension.CREDENTIAL_OR_SENSITIVE_DATA, + BehaviorEvidenceState.INFERRED, + True, + ), + "script.implicit-install": _rule( + RiskDimension.LOCAL_MUTATION, BehaviorEvidenceState.OBSERVED, True + ), + "script.unparsed-unknown": _rule( + RiskDimension.HIGH_IMPACT_EVALUATION, + BehaviorEvidenceState.UNKNOWN, + True, + ), + "script.binary-unknown": _rule( + RiskDimension.HIGH_IMPACT_EVALUATION, + BehaviorEvidenceState.UNKNOWN, + True, + ), + "material.sensitive-path": _rule( + RiskDimension.CREDENTIAL_OR_SENSITIVE_DATA, + BehaviorEvidenceState.OBSERVED, + False, + sensitive=True, + ), + "material.secret-pattern": _rule( + RiskDimension.CREDENTIAL_OR_SENSITIVE_DATA, + BehaviorEvidenceState.OBSERVED, + False, + sensitive=True, + ), + "material.suspicious-secret": _rule( + RiskDimension.CREDENTIAL_OR_SENSITIVE_DATA, + BehaviorEvidenceState.UNKNOWN, + False, + sensitive=True, + ), + "material.executable-content": _rule( + RiskDimension.HIGH_IMPACT_EVALUATION, + BehaviorEvidenceState.OBSERVED, + True, + ), + "material.development": _rule( + RiskDimension.HIGH_IMPACT_EVALUATION, + BehaviorEvidenceState.OBSERVED, + False, + ), + "material.volatile-interface": _rule( + RiskDimension.REAL_EXTERNAL_DEPENDENCY, + BehaviorEvidenceState.INFERRED, + True, + ), +} + + +def _require_digest(value: object, field_name: str) -> str: + if not isinstance(value, str) or _DIGEST_RE.fullmatch(value) is None: + raise BehaviorReportValidationError( + f"{field_name} must be a lowercase sha256 digest" + ) + return value + + +def _validate_relative_path(value: object) -> str: + if not isinstance(value, str) or not value or "\x00" in value or "\\" in value: + raise BehaviorReportValidationError("finding path must be a POSIX relative path") + path = PurePosixPath(value) + if path.is_absolute() or value in {".", ".."} or ".." in path.parts: + raise BehaviorReportValidationError("finding path escapes the candidate root") + if path.as_posix() != value: + raise BehaviorReportValidationError("finding path is not normalized") + return value + + +def _finding_payload( + *, + dimension: RiskDimension, + level: RiskLevel, + evidence_state: BehaviorEvidenceState, + requires_runtime_enforcement: bool, + sensitive_material_bundled: bool, + path: str, + line: int, + rule_id: str, + content_digest: str, +) -> dict[str, Any]: + return { + "dimension": dimension.value, + "level": level.value, + "evidence_state": evidence_state.value, + "requires_runtime_enforcement": requires_runtime_enforcement, + "sensitive_material_bundled": sensitive_material_bundled, + "path": path, + "line": line, + "rule_id": rule_id, + "content_digest": content_digest, + } + + +def _finding_id(payload: Mapping[str, Any]) -> str: + return f"behavior-{digest_json(payload).removeprefix('sha256:')[:32]}" + + +@dataclass(frozen=True) +class BehaviorFinding: + """One redacted, policy-backed observation from the current bytes.""" + + finding_id: str + dimension: RiskDimension + level: RiskLevel + evidence_state: BehaviorEvidenceState + requires_runtime_enforcement: bool + sensitive_material_bundled: bool + path: str + line: int + rule_id: str + content_digest: str + + def __post_init__(self) -> None: + object.__setattr__(self, "dimension", RiskDimension(self.dimension)) + object.__setattr__(self, "level", RiskLevel(self.level)) + object.__setattr__( + self, "evidence_state", BehaviorEvidenceState(self.evidence_state) + ) + if not isinstance(self.requires_runtime_enforcement, bool): + raise BehaviorReportValidationError( + "requires_runtime_enforcement must be boolean" + ) + if not isinstance(self.sensitive_material_bundled, bool): + raise BehaviorReportValidationError( + "sensitive_material_bundled must be boolean" + ) + _validate_relative_path(self.path) + if isinstance(self.line, bool) or not isinstance(self.line, int) or self.line < 1: + raise BehaviorReportValidationError("finding line must be a positive integer") + if self.rule_id not in _RULES: + raise BehaviorReportValidationError(f"unknown behavior rule: {self.rule_id}") + _require_digest(self.content_digest, "finding.content_digest") + if not isinstance(self.finding_id, str) or _FINDING_ID_RE.fullmatch( + self.finding_id + ) is None: + raise BehaviorReportValidationError("finding_id is invalid") + + spec = _RULES[self.rule_id] + expected_level = RISK_DIMENSION_MINIMUM_LEVEL[spec.dimension] + if ( + self.dimension is not spec.dimension + or self.level is not expected_level + or self.evidence_state is not spec.evidence_state + or self.requires_runtime_enforcement is not spec.runtime_enforcement + or self.sensitive_material_bundled is not spec.sensitive_material + ): + raise BehaviorReportValidationError( + f"finding does not match the closed rule semantics: {self.rule_id}" + ) + if ( + self.evidence_state + is BehaviorEvidenceState.REQUIRES_RUNTIME_ENFORCEMENT + and not self.requires_runtime_enforcement + ): + raise BehaviorReportValidationError( + "runtime-enforcement evidence cannot disable runtime enforcement" + ) + + payload = _finding_payload( + dimension=self.dimension, + level=self.level, + evidence_state=self.evidence_state, + requires_runtime_enforcement=self.requires_runtime_enforcement, + sensitive_material_bundled=self.sensitive_material_bundled, + path=self.path, + line=self.line, + rule_id=self.rule_id, + content_digest=self.content_digest, + ) + if self.finding_id != _finding_id(payload): + raise BehaviorReportValidationError("finding_id does not match finding bytes") + + # Reuse the optimizer's only risk policy and its minimum-level check. + self.risk_finding + + @classmethod + def create( + cls, + *, + path: str, + line: int, + rule_id: str, + content_digest: str, + ) -> "BehaviorFinding": + if rule_id not in _RULES: + raise BehaviorReportValidationError(f"unknown behavior rule: {rule_id}") + spec = _RULES[rule_id] + level = RISK_DIMENSION_MINIMUM_LEVEL[spec.dimension] + payload = _finding_payload( + dimension=spec.dimension, + level=level, + evidence_state=spec.evidence_state, + requires_runtime_enforcement=spec.runtime_enforcement, + sensitive_material_bundled=spec.sensitive_material, + path=path, + line=line, + rule_id=rule_id, + content_digest=content_digest, + ) + return cls(finding_id=_finding_id(payload), **payload) + + @property + def risk_finding(self) -> RiskFinding: + return RiskFinding( + self.finding_id, + self.dimension, + self.level, + ( + f"path:{self.path}", + f"line:{self.line}", + f"rule:{self.rule_id}", + f"content:{self.content_digest}", + f"state:{self.evidence_state.value}", + ), + ) + + def to_dict(self) -> dict[str, Any]: + return { + "finding_id": self.finding_id, + **_finding_payload( + dimension=self.dimension, + level=self.level, + evidence_state=self.evidence_state, + requires_runtime_enforcement=self.requires_runtime_enforcement, + sensitive_material_bundled=self.sensitive_material_bundled, + path=self.path, + line=self.line, + rule_id=self.rule_id, + content_digest=self.content_digest, + ), + } + + +_REPORT_FIELDS = frozenset( + { + "schema_version", + "candidate_digest", + "findings", + "unknowns", + "minimum_risk", + "mandatory_controls", + "mandatory_capabilities", + "scanned_files", + "scanned_bytes", + "threat_model", + "content_digest", + } +) +_FINDING_FIELDS = frozenset( + { + "finding_id", + "dimension", + "level", + "evidence_state", + "requires_runtime_enforcement", + "sensitive_material_bundled", + "path", + "line", + "rule_id", + "content_digest", + } +) + + +@dataclass(frozen=True) +class BehaviorRiskReport: + """Frozen audit result bound to one complete candidate tree digest.""" + + schema_version: str + candidate_digest: str + findings: tuple[BehaviorFinding, ...] + unknowns: tuple[str, ...] + minimum_risk: RiskLevel + mandatory_controls: tuple[str, ...] + mandatory_capabilities: tuple[str, ...] + scanned_files: int + scanned_bytes: int + threat_model: tuple[str, ...] + content_digest: str + + def __post_init__(self) -> None: + object.__setattr__(self, "findings", tuple(self.findings)) + object.__setattr__(self, "unknowns", tuple(self.unknowns)) + object.__setattr__(self, "minimum_risk", RiskLevel(self.minimum_risk)) + object.__setattr__(self, "mandatory_controls", tuple(self.mandatory_controls)) + object.__setattr__( + self, "mandatory_capabilities", tuple(self.mandatory_capabilities) + ) + object.__setattr__(self, "threat_model", tuple(self.threat_model)) + _validate_report(self) + + @property + def report_digest(self) -> str: + return self.content_digest + + @property + def risk_findings(self) -> tuple[RiskFinding, ...]: + return tuple(finding.risk_finding for finding in self.findings) + + @property + def has_unknowns(self) -> bool: + return bool(self.unknowns) + + @property + def requires_runtime_enforcement(self) -> bool: + return any( + finding.requires_runtime_enforcement for finding in self.findings + ) + + @property + def has_sensitive_material(self) -> bool: + return any(finding.sensitive_material_bundled for finding in self.findings) + + def _unsigned_dict(self) -> dict[str, Any]: + return { + "schema_version": self.schema_version, + "candidate_digest": self.candidate_digest, + "findings": [finding.to_dict() for finding in self.findings], + "unknowns": list(self.unknowns), + "minimum_risk": self.minimum_risk.value, + "mandatory_controls": list(self.mandatory_controls), + "mandatory_capabilities": list(self.mandatory_capabilities), + "scanned_files": self.scanned_files, + "scanned_bytes": self.scanned_bytes, + "threat_model": list(self.threat_model), + } + + def to_dict(self) -> dict[str, Any]: + return {**self._unsigned_dict(), "content_digest": self.content_digest} + + +def _validate_text_tuple(values: tuple[str, ...], field_name: str) -> None: + if any(not isinstance(item, str) or not item for item in values): + raise BehaviorReportValidationError(f"{field_name} must contain non-empty strings") + if tuple(sorted(set(values))) != values: + raise BehaviorReportValidationError(f"{field_name} must be sorted and unique") + + +def _validate_report(report: BehaviorRiskReport) -> None: + if report.schema_version != BEHAVIOR_RISK_SCHEMA_VERSION: + raise BehaviorReportValidationError("unsupported behavior report schema_version") + _require_digest(report.candidate_digest, "candidate_digest") + _require_digest(report.content_digest, "content_digest") + if any(not isinstance(item, BehaviorFinding) for item in report.findings): + raise BehaviorReportValidationError("findings must contain BehaviorFinding values") + finding_keys = tuple( + (item.path, item.line, item.rule_id, item.finding_id) for item in report.findings + ) + if tuple(sorted(finding_keys)) != finding_keys: + raise BehaviorReportValidationError("findings must use canonical order") + finding_ids = tuple(item.finding_id for item in report.findings) + if len(set(finding_ids)) != len(finding_ids): + raise BehaviorReportValidationError("finding IDs must be unique") + + expected_unknowns = tuple( + sorted( + finding.finding_id + for finding in report.findings + if finding.evidence_state is BehaviorEvidenceState.UNKNOWN + ) + ) + if report.unknowns != expected_unknowns: + raise BehaviorReportValidationError( + "unknowns must be derived from unknown behavior findings" + ) + _validate_text_tuple(report.unknowns, "unknowns") + + risk_findings = report.risk_findings + if report.minimum_risk is not maximum_risk(risk_findings): + raise BehaviorReportValidationError("minimum_risk is not derived from findings") + expected_controls = mandatory_control_ids(risk_findings) + if report.mandatory_controls != expected_controls: + raise BehaviorReportValidationError( + "mandatory_controls are not derived from findings" + ) + expected_capabilities = mandatory_capability_ids(risk_findings) + if report.mandatory_capabilities != expected_capabilities: + raise BehaviorReportValidationError( + "mandatory_capabilities are not derived from findings" + ) + _validate_text_tuple(report.mandatory_controls, "mandatory_controls") + _validate_text_tuple(report.mandatory_capabilities, "mandatory_capabilities") + + for field_name in ("scanned_files", "scanned_bytes"): + value = getattr(report, field_name) + if isinstance(value, bool) or not isinstance(value, int) or value < 0: + raise BehaviorReportValidationError( + f"{field_name} must be a non-negative integer" + ) + if report.scanned_files < 1: + raise BehaviorReportValidationError( + "scanned_files must include the required SKILL.md" + ) + if report.scanned_files < len({finding.path for finding in report.findings}): + raise BehaviorReportValidationError( + "scanned_files cannot be smaller than finding path coverage" + ) + _validate_text_tuple(report.threat_model, "threat_model") + if not report.threat_model: + raise BehaviorReportValidationError( + "threat_model must state the static audit limitations" + ) + if report.threat_model != _THREAT_MODEL: + raise BehaviorReportValidationError("threat_model does not match scanner limits") + if digest_json(report._unsigned_dict()) != report.content_digest: + raise BehaviorReportValidationError("behavior report digest mismatch") + + +def _finding_from_dict(value: Mapping[str, Any]) -> BehaviorFinding: + if set(value) != _FINDING_FIELDS: + raise BehaviorReportValidationError( + "behavior finding fields do not match the closed contract" + ) + try: + return BehaviorFinding( + finding_id=value["finding_id"], + dimension=value["dimension"], + level=value["level"], + evidence_state=value["evidence_state"], + requires_runtime_enforcement=value["requires_runtime_enforcement"], + sensitive_material_bundled=value["sensitive_material_bundled"], + path=value["path"], + line=value["line"], + rule_id=value["rule_id"], + content_digest=value["content_digest"], + ) + except (KeyError, TypeError, ValueError) as exc: + if isinstance(exc, BehaviorReportValidationError): + raise + raise BehaviorReportValidationError(f"invalid behavior finding: {exc}") from exc + + +def report_from_dict(value: Mapping[str, Any]) -> BehaviorRiskReport: + """Parse a closed report mapping and rebuild every local relationship. + + This validates self-consistency, not current filesystem provenance. Pass a + ``candidate_root`` to :func:`validate_behavior_risk_report` when current + bytes must be established. + """ + + if not isinstance(value, Mapping) or set(value) != _REPORT_FIELDS: + raise BehaviorReportValidationError( + "behavior report fields do not match the closed contract" + ) + raw_findings = value.get("findings") + if isinstance(raw_findings, (str, bytes)) or not isinstance( + raw_findings, Sequence + ): + raise BehaviorReportValidationError("findings must be an array") + for field_name in ( + "unknowns", + "mandatory_controls", + "mandatory_capabilities", + "threat_model", + ): + raw = value.get(field_name) + if isinstance(raw, (str, bytes)) or not isinstance(raw, Sequence): + raise BehaviorReportValidationError(f"{field_name} must be an array") + try: + return BehaviorRiskReport( + schema_version=value["schema_version"], + candidate_digest=value["candidate_digest"], + findings=tuple(_finding_from_dict(item) for item in raw_findings), + unknowns=tuple(value["unknowns"]), + minimum_risk=value["minimum_risk"], + mandatory_controls=tuple(value["mandatory_controls"]), + mandatory_capabilities=tuple(value["mandatory_capabilities"]), + scanned_files=value["scanned_files"], + scanned_bytes=value["scanned_bytes"], + threat_model=tuple(value["threat_model"]), + content_digest=value["content_digest"], + ) + except (KeyError, TypeError, ValueError) as exc: + if isinstance(exc, BehaviorReportValidationError): + raise + raise BehaviorReportValidationError(f"invalid behavior report: {exc}") from exc + + +def validate_behavior_risk_report( + value: BehaviorRiskReport | Mapping[str, Any], + *, + candidate_root: str | os.PathLike[str] | None = None, + limits: BehaviorScanLimits | None = None, +) -> BehaviorRiskReport: + """Validate a report and optionally close it against current candidate bytes.""" + + if isinstance(value, BehaviorRiskReport): + _validate_report(value) + report = value + elif isinstance(value, Mapping): + report = report_from_dict(value) + else: + raise TypeError("value must be a BehaviorRiskReport or mapping") + if candidate_root is not None: + current = audit_behavior_risk(candidate_root, limits=limits) + if current.to_dict() != report.to_dict(): + raise BehaviorReportValidationError( + "behavior report does not match the current candidate bytes" + ) + return report + + +@dataclass(frozen=True) +class _FileSnapshot: + path: str + data: bytes + mode: int + + +def _stable_metadata(metadata: os.stat_result) -> tuple[int, int, int, int, int, int]: + return ( + metadata.st_dev, + metadata.st_ino, + metadata.st_mode, + metadata.st_size, + metadata.st_mtime_ns, + metadata.st_ctime_ns, + ) + + +def _read_all(descriptor: int, *, limit: int, path: str) -> bytes: + chunks: list[bytes] = [] + size = 0 + while True: + chunk = os.read(descriptor, min(1024 * 1024, limit + 1 - size)) + if not chunk: + break + chunks.append(chunk) + size += len(chunk) + if size > limit: + raise BehaviorLimitError(f"candidate file exceeds max_file_bytes: {path}") + return b"".join(chunks) + + +def _open_flags(*, directory: bool = False) -> int: + if not hasattr(os, "O_NOFOLLOW"): + raise BehaviorAuditError("the host cannot provide no-follow file opens") + # O_NONBLOCK prevents a hostile FIFO/device entry from hanging before its + # fstat-based special-file rejection. It has no effect on regular files. + flags = ( + os.O_RDONLY + | os.O_NOFOLLOW + | getattr(os, "O_CLOEXEC", 0) + | getattr(os, "O_NONBLOCK", 0) + ) + if directory: + flags |= getattr(os, "O_DIRECTORY", 0) + return flags + + +def _snapshot_tree( + root_descriptor: int, + *, + limits: BehaviorScanLimits, +) -> tuple[str, tuple[_FileSnapshot, ...], int]: + records: list[dict[str, Any]] = [] + files: list[_FileSnapshot] = [] + total_bytes = 0 + entry_count = 0 + + def visit(directory_fd: int, relative: str, depth: int) -> None: + nonlocal total_bytes, entry_count + if depth > limits.max_depth: + raise BehaviorLimitError("candidate exceeds max_depth") + before_directory = os.fstat(directory_fd) + if not stat.S_ISDIR(before_directory.st_mode): + raise BehaviorAuditError(f"candidate entry is not a directory: {relative}") + records.append( + { + "path": relative, + "type": "directory", + "executable": bool(before_directory.st_mode & 0o111), + } + ) + try: + names = sorted(os.listdir(directory_fd)) + except OSError as exc: + raise BehaviorAuditError(f"cannot list candidate directory: {relative}") from exc + for name in names: + if not isinstance(name, str) or not name or name in {".", ".."}: + raise BehaviorAuditError("candidate contains an invalid directory entry") + try: + encoded_name = name.encode("utf-8") + except UnicodeEncodeError as exc: + raise BehaviorAuditError("candidate paths must be valid UTF-8") from exc + if b"/" in encoded_name or b"\x00" in encoded_name: + raise BehaviorAuditError("candidate contains an invalid path component") + child_relative = name if relative == "." else f"{relative}/{name}" + if len(child_relative.encode("utf-8")) > limits.max_path_bytes: + raise BehaviorLimitError("candidate path exceeds max_path_bytes") + entry_count += 1 + if entry_count > limits.max_files: + raise BehaviorLimitError("candidate exceeds max_files") + try: + descriptor = os.open( + name, + _open_flags(), + dir_fd=directory_fd, + ) + except OSError as exc: + if exc.errno in {errno.ELOOP, errno.EMLINK}: + raise BehaviorAuditError( + f"symbolic links are not allowed: {child_relative}" + ) from exc + raise BehaviorAuditError( + f"cannot safely open candidate entry: {child_relative}" + ) from exc + try: + before = os.fstat(descriptor) + entry_type = stat.S_IFMT(before.st_mode) + if stat.S_ISDIR(entry_type): + visit(descriptor, child_relative, depth + 1) + elif stat.S_ISREG(entry_type): + if before.st_size > limits.max_file_bytes: + raise BehaviorLimitError( + f"candidate file exceeds max_file_bytes: {child_relative}" + ) + data = _read_all( + descriptor, + limit=limits.max_file_bytes, + path=child_relative, + ) + after = os.fstat(descriptor) + if _stable_metadata(before) != _stable_metadata(after): + raise BehaviorCandidateChangedError( + f"candidate file changed during audit: {child_relative}" + ) + if len(data) != after.st_size: + raise BehaviorCandidateChangedError( + f"candidate file size changed during audit: {child_relative}" + ) + total_bytes += len(data) + if total_bytes > limits.max_total_bytes: + raise BehaviorLimitError("candidate exceeds max_total_bytes") + content_digest = digest_bytes(data) + records.append( + { + "path": child_relative, + "type": "file", + "size": len(data), + "executable": bool(after.st_mode & 0o111), + "content_digest": content_digest, + } + ) + files.append(_FileSnapshot(child_relative, data, after.st_mode)) + else: + raise BehaviorAuditError( + f"special files are not allowed: {child_relative}" + ) + finally: + os.close(descriptor) + try: + after_names = sorted(os.listdir(directory_fd)) + except OSError as exc: + raise BehaviorCandidateChangedError( + f"candidate directory changed during audit: {relative}" + ) from exc + after_directory = os.fstat(directory_fd) + if names != after_names or _stable_metadata(before_directory) != _stable_metadata( + after_directory + ): + raise BehaviorCandidateChangedError( + f"candidate directory changed during audit: {relative}" + ) + + visit(root_descriptor, ".", 0) + records.sort(key=lambda record: record["path"]) + files.sort(key=lambda item: item.path) + return digest_json(records), tuple(files), total_bytes + + +def _line_digest(data: bytes, line: int) -> str: + lines = data.splitlines(keepends=True) + if not lines: + return digest_bytes(data) + index = min(max(line - 1, 0), len(lines) - 1) + return digest_bytes(lines[index]) + + +def _add_finding( + findings: dict[str, BehaviorFinding], + *, + snapshot: _FileSnapshot, + line: int, + rule_id: str, +) -> None: + finding = BehaviorFinding.create( + path=snapshot.path, + line=max(1, line), + rule_id=rule_id, + content_digest=_line_digest(snapshot.data, line), + ) + findings[finding.finding_id] = finding + + +_SKILL_PATTERNS: tuple[tuple[str, re.Pattern[str]], ...] = ( + ( + "skill.delete", + re.compile( + r"\b(?:delete|remove|unlink|erase|purge|rm\s+-[a-z]*r)\b|删除|移除|清空|销毁", + re.IGNORECASE, + ), + ), + ( + "skill.sensitive-retention", + re.compile( + r"\b(?:retain|store|save|log|archive)\b.{0,40}" + r"\b(?:credential|secret|password|token|api[ _-]?key)\b|" + r"(?:保留|存储|记录|归档).{0,20}(?:凭据|密钥|密码|令牌|敏感)", + re.IGNORECASE, + ), + ), + ( + "skill.credential-access", + re.compile( + r"\b(?:credential|secret|password|token|api[ _-]?key|keychain|keyring)\b|" + r"凭据|密钥|密码|令牌|环境变量", + re.IGNORECASE, + ), + ), + ( + "skill.auto-routing", + re.compile( + r"\b(?:automatic(?:ally)?\s+(?:route|routing|trigger)|auto[ _-]?route|" + r"shared\s+trigger|global\s+trigger)\b|自动路由|自动触发|共享触发", + re.IGNORECASE, + ), + ), + ( + "skill.external-write", + re.compile( + r"\b(?:publish|upload|deploy|send\s+(?:(?:a\s+)?message|email)|post\s+to|" + r"push\s+to|write\s+to\s+(?:slack|calendar|github|jira|notion|api)|" + r"update\s+(?:a\s+)?remote|(?:create|update|delete)\s+(?:an?\s+)?" + r"(?:event|issue|ticket|calendar\s+entry))\b|" + r"发布|上传|部署|发送(?:消息|邮件)|写入外部|更新远程", + re.IGNORECASE, + ), + ), + ( + "skill.network", + re.compile( + r"\b(?:network|http[s]?\s+request|call\s+(?:an?\s+)?api|fetch|" + r"download|curl|wget|webhook)\b|联网|网络请求|调用接口|下载", + re.IGNORECASE, + ), + ), + ( + "skill.shell", + re.compile( + r"\b(?:shell|bash|zsh|powershell|terminal|subprocess|os\.system)\b|" + r"命令行|终端命令|执行脚本", + re.IGNORECASE, + ), + ), + ( + "skill.implicit-install", + re.compile( + r"\b(?:pip|pip3|npm|pnpm|yarn|brew|apt(?:-get)?)\s+install\b|" + r"\binstall\s+(?:the\s+)?skill\b|安装依赖|安装技能", + re.IGNORECASE, + ), + ), + ( + "skill.commit", + re.compile(r"\bgit\s+(?:commit|add)\b|\bcommit\s+changes\b|提交代码", re.IGNORECASE), + ), + ( + "skill.local-write", + re.compile( + r"\b(?:write|create|edit|modify|save|rename|copy|move)\b.{0,28}" + r"\b(?:file|directory|folder|artifact|document)\b|写入文件|创建文件|修改文件|保存文件", + re.IGNORECASE, + ), + ), +) + + +_SCRIPT_PATTERNS: tuple[tuple[str, re.Pattern[str]], ...] = ( + ( + "script.delete", + re.compile( + r"\b(?:rm\s+(?:-[a-z]*r[a-z]*|--recursive)|unlink|rmdir|Remove-Item)\b", + re.IGNORECASE, + ), + ), + ( + "script.external-write", + re.compile( + r"\b(?:git\s+push|curl\b[^\n]*(?:-X\s*)?(?:POST|PUT|PATCH|DELETE)|" + r"axios\.(?:post|put|patch|delete)|fetch\([^\n]*method\s*:)\b", + re.IGNORECASE, + ), + ), + ( + "script.network", + re.compile(r"\b(?:curl|wget|fetch\s*\(|axios\.|Invoke-WebRequest|https?://)", re.IGNORECASE), + ), + ( + "script.implicit-install", + re.compile(r"\b(?:pip3?|npm|pnpm|yarn|brew|apt(?:-get)?)\s+install\b", re.IGNORECASE), + ), + ( + "script.environment-read", + re.compile(r"(?:\$\{?[A-Z][A-Z0-9_]*\}?|process\.env|ENV\[)", re.IGNORECASE), + ), + ( + "script.shell", + re.compile(r"\b(?:eval|exec|sudo|child_process|Start-Process)\b|`[^`]+`|\$\(", re.IGNORECASE), + ), + ( + "script.local-write", + re.compile(r"(?:^|\s)(?:>|>>)(?:\s|[^&])|\b(?:writeFile|mkdir|touch|cp|mv)\b", re.IGNORECASE), + ), +) + + +_STRONG_SECRET_PATTERNS: tuple[re.Pattern[bytes], ...] = ( + re.compile(br"-----BEGIN [A-Z0-9 ]*PRIVATE KEY-----"), + re.compile(br"-----BEGIN CERTIFICATE-----"), + re.compile(br"\bAKIA[0-9A-Z]{16}\b"), + re.compile(br"\b(?:gh[pousr]_[A-Za-z0-9]{30,}|github_pat_[A-Za-z0-9_]{40,})\b"), + re.compile(br"\bxox[baprs]-[A-Za-z0-9-]{20,}\b"), + re.compile(br"\bsk-(?:proj-)?[A-Za-z0-9_-]{20,}\b"), +) +_SUSPICIOUS_SECRET_PATTERN = re.compile( + br"(?i)\b(?:api[_-]?key|access[_-]?token|auth[_-]?token|token|password|secret)\b" + br"\s*[:=]\s*['\"]?[^\s'\",}{]{8,}" +) +_PLACEHOLDER_SECRET_LITERAL_PATTERN = re.compile( + br"(?ix)" + br"\b(?:api[_-]?key|access[_-]?token|auth[_-]?token|token|password|secret)\b" + br"\s*[:=]\s*" + br"(?P['\"])" + br"(?:?|x{4,})" + br"(?P=quote)" +) +_RUNTIME_SECRET_REFERENCE_PATTERN = re.compile( + br"(?ix)^(?:" + br"os\s*\.\s*environ(?:\s*\.\s*get\s*\(|\s*\[)|" + br"os\s*\.\s*getenv\s*\(|" + br"getenv\s*\(|" + br"process\s*\.\s*env(?:\s*\.|\s*\[)|" + br"\$\{[a-z_][a-z0-9_]*\}|" + br"\$[a-z_][a-z0-9_]*" + br")" +) +_CREDENTIAL_NAME_RE = re.compile( + r"(?:^|[._-])(?:credential|credentials|secret|secrets|token|tokens|password|" + r"passwords|private[_-]?key|auth)(?:$|[._-])", + re.IGNORECASE, +) +_EXACT_SENSITIVE_NAMES = frozenset( + { + ".env", + "credentials.json", + "secrets.json", + "id_rsa", + "id_ed25519", + } +) +_SENSITIVE_SUFFIXES = ( + ".key", + ".pem", + ".p12", + ".pfx", + ".crt", + ".cer", + ".cert", +) +_DEVELOPMENT_PARTS = frozenset( + { + ".git", + ".pytest_cache", + "__pycache__", + "backup", + "backups", + "cache", + "dev", + "eval", + "evals", + "journal", + "journals", + "research", + "test", + "tests", + "trace", + "traces", + } +) +_SCRIPT_SUFFIXES = frozenset( + {".sh", ".bash", ".zsh", ".fish", ".js", ".mjs", ".cjs", ".ts", ".rb", ".pl", ".ps1"} +) +_VOLATILE_INTERFACE_RE = re.compile( + r"https?://(?:api\.|[^\s/]+/api/)(?!v\d+(?:/|\b))[^\s)\]}>'\"]+|" + r"\b(?:latest|unstable|nightly)\s+(?:api|endpoint|interface)\b", + re.IGNORECASE, +) + + +def _path_component_is_sensitive(component: str) -> bool: + normalized = component.casefold() + return ( + normalized in _EXACT_SENSITIVE_NAMES + or normalized.startswith(".env.") + or normalized.endswith(_SENSITIVE_SUFFIXES) + or _CREDENTIAL_NAME_RE.search(normalized) is not None + ) + + +def _path_text_is_sensitive(value: str) -> bool: + path = PurePosixPath(value.replace("\\", "/")) + return any(_path_component_is_sensitive(component) for component in path.parts) + + +def _suspicious_match_is_runtime_reference( + data: bytes, + match: re.Match[bytes], +) -> bool: + """Return true when an assignment reads a runtime source, not literal bytes.""" + + line_end = data.find(b"\n", match.start()) + if line_end < 0: + line_end = len(data) + fragment = data[match.start() : line_end] + separator = re.search(br"[:=]", fragment) + if separator is None: + return False + right_hand_side = fragment[separator.end() :].lstrip() + return _RUNTIME_SECRET_REFERENCE_PATTERN.match(right_hand_side) is not None + + +def _scan_material( + snapshot: _FileSnapshot, + findings: dict[str, BehaviorFinding], +) -> None: + path = PurePosixPath(snapshot.path) + lower_parts = {part.casefold() for part in path.parts} + sensitive_path = any(_path_component_is_sensitive(part) for part in path.parts) + if sensitive_path: + _add_finding( + findings, + snapshot=snapshot, + line=1, + rule_id="material.sensitive-path", + ) + if lower_parts & _DEVELOPMENT_PARTS: + _add_finding( + findings, + snapshot=snapshot, + line=1, + rule_id="material.development", + ) + if ({"references", "assets"} & lower_parts) and ( + bool(snapshot.mode & 0o111) + or path.suffix.casefold() in _SCRIPT_SUFFIXES + or snapshot.data.startswith(b"#!") + ): + _add_finding( + findings, + snapshot=snapshot, + line=1, + rule_id="material.executable-content", + ) + + for pattern in _STRONG_SECRET_PATTERNS: + for match in pattern.finditer(snapshot.data): + _add_finding( + findings, + snapshot=snapshot, + line=snapshot.data.count(b"\n", 0, match.start()) + 1, + rule_id="material.secret-pattern", + ) + for match in _SUSPICIOUS_SECRET_PATTERN.finditer(snapshot.data): + if _suspicious_match_is_runtime_reference(snapshot.data, match): + continue + _add_finding( + findings, + snapshot=snapshot, + line=snapshot.data.count(b"\n", 0, match.start()) + 1, + rule_id="material.suspicious-secret", + ) + for match in _PLACEHOLDER_SECRET_LITERAL_PATTERN.finditer(snapshot.data): + _add_finding( + findings, + snapshot=snapshot, + line=snapshot.data.count(b"\n", 0, match.start()) + 1, + rule_id="material.suspicious-secret", + ) + + try: + text = snapshot.data.decode("utf-8") + except UnicodeDecodeError: + return + for match in _VOLATILE_INTERFACE_RE.finditer(text): + line = text.count("\n", 0, match.start()) + 1 + _add_finding( + findings, + snapshot=snapshot, + line=line, + rule_id="material.volatile-interface", + ) + + +def _scan_skill_markdown( + snapshot: _FileSnapshot, + findings: dict[str, BehaviorFinding], +) -> None: + try: + text = snapshot.data.decode("utf-8") + except UnicodeDecodeError as exc: + raise BehaviorAuditError("SKILL.md must be valid UTF-8") from exc + for line_number, line in enumerate(text.splitlines(), 1): + for rule_id, pattern in _SKILL_PATTERNS: + if pattern.search(line): + _add_finding( + findings, + snapshot=snapshot, + line=line_number, + rule_id=rule_id, + ) + + +def _dotted_name(node: ast.AST) -> str | None: + if isinstance(node, ast.Name): + return node.id + if isinstance(node, ast.Attribute): + prefix = _dotted_name(node.value) + return f"{prefix}.{node.attr}" if prefix else node.attr + return None + + +def _literal_text(node: ast.AST | None) -> str | None: + if isinstance(node, ast.Constant) and isinstance(node.value, str): + return node.value + return None + + +def _command_text(node: ast.AST | None) -> str | None: + literal = _literal_text(node) + if literal is not None: + return literal + if isinstance(node, (ast.List, ast.Tuple)): + parts = [_literal_text(item) for item in node.elts] + if all(item is not None for item in parts): + return " ".join(item for item in parts if item is not None) + return None + + +def _path_expression_text(node: ast.AST | None) -> str | None: + value = _literal_text(node) + if value is not None: + return value + if isinstance(node, ast.Call): + constructor = _dotted_name(node.func) or "" + if constructor.rpartition(".")[2] in {"Path", "PurePath", "PurePosixPath"}: + return _literal_text(node.args[0]) if node.args else None + return None + + +def _write_target_is_bounded(node: ast.AST | None) -> bool: + value = _path_expression_text(node) + if value is None or not value: + return False + path = PurePosixPath(value.replace("\\", "/")) + return not path.is_absolute() and ".." not in path.parts + + +_PURE_BUILTIN_CALLS = frozenset( + { + "abs", + "all", + "any", + "bin", + "bool", + "bytes", + "chr", + "dict", + "divmod", + "enumerate", + "float", + "format", + "frozenset", + "hash", + "hex", + "int", + "isinstance", + "issubclass", + "len", + "list", + "max", + "min", + "oct", + "ord", + "pow", + "range", + "repr", + "reversed", + "round", + "set", + "sorted", + "str", + "sum", + "tuple", + "zip", + } +) +_PURE_STRING_METHODS = frozenset( + { + "capitalize", + "casefold", + "center", + "count", + "encode", + "endswith", + "expandtabs", + "find", + "format", + "format_map", + "index", + "isalnum", + "isalpha", + "isascii", + "isdecimal", + "isdigit", + "isidentifier", + "islower", + "isnumeric", + "isprintable", + "isspace", + "istitle", + "isupper", + "join", + "ljust", + "lower", + "lstrip", + "partition", + "removeprefix", + "removesuffix", + "replace", + "rfind", + "rindex", + "rjust", + "rpartition", + "rsplit", + "rstrip", + "split", + "splitlines", + "startswith", + "strip", + "swapcase", + "title", + "translate", + "upper", + "zfill", + } +) +_STRING_RETURNING_METHODS = frozenset( + { + "capitalize", + "casefold", + "center", + "expandtabs", + "format", + "format_map", + "join", + "ljust", + "lower", + "lstrip", + "removeprefix", + "removesuffix", + "replace", + "rjust", + "rstrip", + "strip", + "swapcase", + "title", + "translate", + "upper", + "zfill", + } +) +_PATH_CONSTRUCTOR_SUFFIXES = frozenset( + {"Path", "PurePath", "PurePosixPath", "PureWindowsPath"} +) +_PATH_CONSTRUCTOR_NAMES = frozenset( + { + "pathlib.Path", + "pathlib.PurePath", + "pathlib.PurePosixPath", + "pathlib.PureWindowsPath", + } +) +_WRITE_OPEN_FLAGS = frozenset( + {"O_APPEND", "O_CREAT", "O_RDWR", "O_TRUNC", "O_WRONLY"} +) + + +@dataclass +class _PythonScopeFacts: + bound_names: set[str] + string_names: set[str] + path_names: set[str] + + +def _assigned_names(node: ast.AST) -> set[str]: + if isinstance(node, ast.Name): + return {node.id} + if isinstance(node, (ast.Tuple, ast.List)): + return { + name + for item in node.elts + for name in _assigned_names(item) + } + return set() + + +def _annotation_suffix(node: ast.AST | None) -> str: + return (_dotted_name(node) or "").rpartition(".")[2] + + +def _contains_write_open_flag(node: ast.AST | None) -> bool: + if node is None: + return False + for item in ast.walk(node): + name = _dotted_name(item) + if name and name.rpartition(".")[2] in _WRITE_OPEN_FLAGS: + return True + return False + + +class _PythonBehaviorVisitor(ast.NodeVisitor): + def __init__( + self, + snapshot: _FileSnapshot, + findings: dict[str, BehaviorFinding], + limits: BehaviorScanLimits, + local_functions: frozenset[str], + ) -> None: + self.snapshot = snapshot + self.findings = findings + self.limits = limits + self.aliases: dict[str, str] = {} + self.local_functions = local_functions + self.scopes = [_PythonScopeFacts(set(), set(), set())] + self.nodes = 0 + + def generic_visit(self, node: ast.AST) -> None: + self.nodes += 1 + if self.nodes > self.limits.max_ast_nodes: + raise BehaviorLimitError( + f"Python AST exceeds max_ast_nodes: {self.snapshot.path}" + ) + super().generic_visit(node) + + def _add(self, node: ast.AST, rule_id: str) -> None: + _add_finding( + self.findings, + snapshot=self.snapshot, + line=getattr(node, "lineno", 1), + rule_id=rule_id, + ) + + def _resolved_name(self, node: ast.AST) -> str: + name = _dotted_name(node) or "" + head, separator, tail = name.partition(".") + replacement = self.aliases.get(head) + return f"{replacement}{separator}{tail}" if replacement else name + + def _bind( + self, + names: Iterable[str], + *, + string_value: bool = False, + path_value: bool = False, + ) -> None: + scope = self.scopes[-1] + for name in names: + scope.bound_names.add(name) + scope.string_names.discard(name) + scope.path_names.discard(name) + if string_value: + scope.string_names.add(name) + if path_value: + scope.path_names.add(name) + + def _name_has_fact(self, name: str, field_name: str) -> bool: + for scope in reversed(self.scopes): + if name in scope.bound_names: + return name in getattr(scope, field_name) + return False + + def _name_is_bound(self, name: str) -> bool: + return any(name in scope.bound_names for scope in reversed(self.scopes)) + + def _is_path_constructor(self, node: ast.AST) -> bool: + if not isinstance(node, ast.Call): + return False + if isinstance(node.func, ast.Name) and self._name_is_bound(node.func.id): + return False + name = self._resolved_name(node.func) + return name in _PATH_CONSTRUCTOR_NAMES + + def _is_path_expression(self, node: ast.AST | None) -> bool: + if isinstance(node, ast.Name): + return self._name_has_fact(node.id, "path_names") + if node is not None and self._is_path_constructor(node): + return True + if isinstance(node, ast.BinOp) and isinstance(node.op, ast.Div): + return self._is_path_expression(node.left) + return False + + def _is_string_expression(self, node: ast.AST | None) -> bool: + if isinstance(node, (ast.Constant, ast.JoinedStr)): + return isinstance(node, ast.JoinedStr) or isinstance(node.value, str) + if isinstance(node, ast.Name): + return self._name_has_fact(node.id, "string_names") + if isinstance(node, ast.Call): + name = self._resolved_name(node.func) + if name in {"str", "builtins.str"}: + return True + if isinstance(node.func, ast.Attribute): + return ( + node.func.attr in _STRING_RETURNING_METHODS + and self._is_string_expression(node.func.value) + ) + return False + + def _is_pure_call(self, node: ast.Call, name: str) -> bool: + if isinstance(node.func, ast.Name): + if node.func.id in self.local_functions: + return True + if ( + not self._name_is_bound(node.func.id) + and ( + name in _PURE_BUILTIN_CALLS + or name.removeprefix("builtins.") in _PURE_BUILTIN_CALLS + ) + ): + return True + if self._is_path_constructor(node): + return True + if isinstance(node.func, ast.Attribute): + return ( + node.func.attr in _PURE_STRING_METHODS + and self._is_string_expression(node.func.value) + ) + return False + + def _visit_function(self, node: ast.FunctionDef | ast.AsyncFunctionDef) -> None: + arguments = ( + tuple(node.args.posonlyargs) + + tuple(node.args.args) + + tuple(node.args.kwonlyargs) + ) + scope = _PythonScopeFacts(set(), set(), set()) + for argument in arguments: + scope.bound_names.add(argument.arg) + annotation = _annotation_suffix(argument.annotation) + if annotation == "str": + scope.string_names.add(argument.arg) + elif annotation in _PATH_CONSTRUCTOR_SUFFIXES: + scope.path_names.add(argument.arg) + if node.args.vararg is not None: + scope.bound_names.add(node.args.vararg.arg) + if node.args.kwarg is not None: + scope.bound_names.add(node.args.kwarg.arg) + self.scopes.append(scope) + try: + self.generic_visit(node) + finally: + self.scopes.pop() + + def visit_FunctionDef(self, node: ast.FunctionDef) -> None: + self._bind((node.name,)) + self._visit_function(node) + + def visit_AsyncFunctionDef(self, node: ast.AsyncFunctionDef) -> None: + self._bind((node.name,)) + self._visit_function(node) + + def visit_Lambda(self, node: ast.Lambda) -> None: + names = { + argument.arg + for argument in ( + tuple(node.args.posonlyargs) + + tuple(node.args.args) + + tuple(node.args.kwonlyargs) + ) + } + self.scopes.append(_PythonScopeFacts(names, set(), set())) + try: + self.generic_visit(node) + finally: + self.scopes.pop() + + def visit_Assign(self, node: ast.Assign) -> None: + names = { + name + for target in node.targets + for name in _assigned_names(target) + } + self._bind( + names, + string_value=self._is_string_expression(node.value), + path_value=self._is_path_expression(node.value), + ) + self.generic_visit(node) + + def visit_AnnAssign(self, node: ast.AnnAssign) -> None: + names = _assigned_names(node.target) + annotation = _annotation_suffix(node.annotation) + self._bind( + names, + string_value=annotation == "str" or self._is_string_expression(node.value), + path_value=( + annotation in _PATH_CONSTRUCTOR_SUFFIXES + or self._is_path_expression(node.value) + ), + ) + self.generic_visit(node) + + def visit_Import(self, node: ast.Import) -> None: + for alias in node.names: + bound = alias.asname or alias.name.partition(".")[0] + self.aliases[bound] = alias.name + root = alias.name.partition(".")[0] + if root in { + "aiohttp", + "http", + "httpx", + "requests", + "smtplib", + "socket", + "urllib", + }: + self._add(node, "python.network-import") + elif root == "subprocess": + self._add(node, "python.subprocess-import") + elif root in {"keyring", "boto3", "dotenv"}: + self._add(node, "python.credential-import") + self.generic_visit(node) + + def visit_ImportFrom(self, node: ast.ImportFrom) -> None: + module = node.module or "" + for alias in node.names: + if alias.name == "*": + continue + self.aliases[alias.asname or alias.name] = f"{module}.{alias.name}" + root = module.partition(".")[0] + if root in { + "aiohttp", + "http", + "httpx", + "requests", + "smtplib", + "socket", + "urllib", + }: + self._add(node, "python.network-import") + elif root == "subprocess": + self._add(node, "python.subprocess-import") + elif root in {"keyring", "boto3", "dotenv"}: + self._add(node, "python.credential-import") + self.generic_visit(node) + + def visit_Subscript(self, node: ast.Subscript) -> None: + name = self._resolved_name(node.value) + if name in {"os.environ", "environ"} or name.endswith(".environ"): + self._add(node, "python.environment-read") + key = _literal_text(node.slice) + if key and re.search( + r"(?:TOKEN|SECRET|PASSWORD|CREDENTIAL|API[_-]?KEY|AUTH)", + key, + re.IGNORECASE, + ): + self._add(node, "python.credential-read") + self.generic_visit(node) + + def visit_Call(self, node: ast.Call) -> None: + name = self._resolved_name(node.func) + suffix = name.rpartition(".")[2] + positional_target = node.args[0] if node.args else None + analyzed = False + + write_call = False + if name in {"open", "builtins.open", "io.open"}: + analyzed = True + mode_node = node.args[1] if len(node.args) > 1 else None + for keyword in node.keywords: + if keyword.arg == "mode": + mode_node = keyword.value + mode = _literal_text(mode_node) + if mode is not None and any(flag in mode for flag in "wax+"): + write_call = True + elif mode_node is not None and mode is None: + self._add(node, "python.unbounded-write-path") + path_text = _path_expression_text(positional_target) + if path_text and _path_text_is_sensitive(path_text): + self._add(node, "python.credential-read") + elif ( + suffix == "open" + and isinstance(node.func, ast.Attribute) + and self._is_path_expression(node.func.value) + ): + analyzed = True + # Bound Path.open takes mode as its first argument; the receiver is + # the mutation target. Unknown receivers remain unbounded. + mode_node = node.args[0] if node.args else None + for keyword in node.keywords: + if keyword.arg == "mode": + mode_node = keyword.value + mode = _literal_text(mode_node) + if mode is not None and any(flag in mode for flag in "wax+"): + write_call = True + elif mode_node is not None and mode is None: + self._add(node, "python.unbounded-write-path") + positional_target = ( + node.func.value if isinstance(node.func, ast.Attribute) else None + ) + path_text = _path_expression_text(positional_target) + if path_text and _path_text_is_sensitive(path_text): + self._add(node, "python.credential-read") + elif suffix in { + "write_text", + "write_bytes", + "mkdir", + "touch", + }: + analyzed = True + write_call = True + positional_target = node.func.value if isinstance(node.func, ast.Attribute) else positional_target + elif name in { + "os.mkdir", + "os.makedirs", + "os.rename", + "os.replace", + "shutil.copy", + "shutil.copy2", + "shutil.copyfile", + "shutil.copytree", + "shutil.move", + }: + analyzed = True + write_call = True + elif suffix in {"rename", "replace"} and isinstance(node.func, ast.Attribute): + # Only recognize a generic rename/replace receiver when it is an + # explicit Path construction; ``str.replace`` and similar methods + # are not filesystem writes. + if _path_expression_text(node.func.value) is not None: + analyzed = True + write_call = True + positional_target = node.func.value + + if write_call: + analyzed = True + self._add(node, "python.local-write") + if not _write_target_is_bounded(positional_target): + self._add(node, "python.unbounded-write-path") + + if name == "os.open": + analyzed = True + flags_node = node.args[1] if len(node.args) > 1 else None + for keyword in node.keywords: + if keyword.arg == "flags": + flags_node = keyword.value + if _contains_write_open_flag(flags_node): + self._add(node, "python.local-write") + if not _write_target_is_bounded(positional_target): + self._add(node, "python.unbounded-write-path") + else: + # Descriptor access can later feed an unobserved write or + # external device; keep it unknown when no write flag is seen. + self._add(node, "python.unanalyzed-call") + + if name == "os.write": + analyzed = True + self._add(node, "python.local-write") + self._add(node, "python.unbounded-write-path") + + delete_target = positional_target + if suffix in {"unlink", "rmdir"} and isinstance(node.func, ast.Attribute): + delete_target = node.func.value + if suffix in {"unlink", "rmdir"} or name in { + "os.unlink", + "os.remove", + "os.rmdir", + "shutil.rmtree", + }: + analyzed = True + self._add(node, "python.delete") + if not _write_target_is_bounded(delete_target): + self._add(node, "python.unbounded-write-path") + + network_roots = ( + "aiohttp.", + "http.", + "httpx.", + "requests.", + "smtplib.", + "socket.", + "urllib.", + ) + if name.startswith(network_roots) or name in {"urlopen", "create_connection"}: + analyzed = True + self._add(node, "python.network") + if suffix.casefold() in {"post", "put", "patch", "delete"}: + self._add(node, "python.external-write") + + if suffix.casefold() in { + "chat_postmessage", + "create_event", + "deploy", + "publish", + "send", + "sendmail", + "send_message", + "update_event", + "upload", + "upload_file", + }: + analyzed = True + self._add(node, "python.external-write") + self._add(node, "python.network") + + if name in { + "subprocess.run", + "subprocess.call", + "subprocess.check_call", + "subprocess.check_output", + "subprocess.Popen", + "run", + "call", + "check_call", + "check_output", + "Popen", + "os.popen", + "os.system", + }: + analyzed = True + self._add(node, "python.subprocess") + shell_enabled = name in {"os.popen", "os.system"} + for keyword in node.keywords: + if keyword.arg == "shell" and isinstance(keyword.value, ast.Constant): + shell_enabled = keyword.value.value is True + if shell_enabled: + self._add(node, "python.shell") + command = _command_text(positional_target) + if command is not None: + if re.search(r"(?:^|\s)(?:rm|rmdir|unlink)(?:\s|$)", command): + self._add(node, "python.delete") + if re.search(r"(?:^|\s)(?:curl|wget)(?:\s|$)", command): + self._add(node, "python.network") + if re.search(r"(?:^|\s)git\s+push(?:\s|$)", command): + self._add(node, "python.external-write") + if re.search( + r"(?:^|\s)(?:pip3?|npm|pnpm|yarn|brew|apt(?:-get)?)\s+install(?:\s|$)", + command, + ): + self._add(node, "python.implicit-install") + + if name in {"eval", "exec", "builtins.eval", "builtins.exec", "__import__"} or name.startswith( + "importlib." + ): + analyzed = True + self._add(node, "python.dynamic-execution") + + if name in {"os.getenv", "getenv"} or name.endswith("environ.get"): + analyzed = True + self._add(node, "python.environment-read") + key = _literal_text(positional_target) + if key and re.search( + r"(?:TOKEN|SECRET|PASSWORD|CREDENTIAL|API[_-]?KEY|AUTH)", + key, + re.IGNORECASE, + ): + self._add(node, "python.credential-read") + if name in { + "keyring.get_password", + "get_password", + "boto3.client", + "dotenv.load_dotenv", + "load_dotenv", + }: + analyzed = True + self._add(node, "python.credential-read") + + if ( + suffix in {"read_bytes", "read_text"} + and isinstance(node.func, ast.Attribute) + and self._is_path_expression(node.func.value) + ): + path_text = _path_expression_text(node.func.value) + if path_text and _path_text_is_sensitive(path_text): + analyzed = True + self._add(node, "python.credential-read") + + if name in {"print", "builtins.print"}: + analyzed = True + file_target = next( + (keyword.value for keyword in node.keywords if keyword.arg == "file"), + None, + ) + if file_target is not None: + self._add(node, "python.local-write") + self._add(node, "python.unbounded-write-path") + + if not analyzed and not self._is_pure_call(node, name): + self._add(node, "python.unanalyzed-call") + + self.generic_visit(node) + + +def _scan_python( + snapshot: _FileSnapshot, + findings: dict[str, BehaviorFinding], + limits: BehaviorScanLimits, +) -> None: + try: + source = snapshot.data.decode("utf-8") + except UnicodeDecodeError: + _add_finding( + findings, + snapshot=snapshot, + line=1, + rule_id="script.binary-unknown", + ) + return + try: + tree = ast.parse(source, filename=snapshot.path) + except (SyntaxError, ValueError, MemoryError): + _add_finding( + findings, + snapshot=snapshot, + line=1, + rule_id="python.syntax-unknown", + ) + return + local_functions = frozenset( + item.name + for item in ast.walk(tree) + if isinstance(item, (ast.FunctionDef, ast.AsyncFunctionDef)) + ) + _PythonBehaviorVisitor( + snapshot, + findings, + limits, + local_functions, + ).visit(tree) + + +def _scan_other_script( + snapshot: _FileSnapshot, + findings: dict[str, BehaviorFinding], +) -> None: + try: + text = snapshot.data.decode("utf-8") + except UnicodeDecodeError: + _add_finding( + findings, + snapshot=snapshot, + line=1, + rule_id="script.binary-unknown", + ) + return + for line_number, line in enumerate(text.splitlines(), 1): + for rule_id, pattern in _SCRIPT_PATTERNS: + if pattern.search(line): + _add_finding( + findings, + snapshot=snapshot, + line=line_number, + rule_id=rule_id, + ) + # Regexes identify known constructs but cannot parse the complete behavior + # of every non-Python language. Preserve that residual uncertainty even + # when one or more concrete findings were observed. + _add_finding( + findings, + snapshot=snapshot, + line=1, + rule_id="script.unparsed-unknown", + ) + + +_THREAT_MODEL = tuple( + sorted( + { + "dynamic_inputs_and_host_side_effects_require_runtime_controls", + "parent_directory_replacement_is_detected_at_audit_end_not_locked", + "static_analysis_does_not_prove_runtime_safety", + } + ) +) + + +def _build_report( + *, + candidate_digest: str, + findings: Iterable[BehaviorFinding], + scanned_files: int, + scanned_bytes: int, +) -> BehaviorRiskReport: + ordered_findings = tuple( + sorted(findings, key=lambda item: (item.path, item.line, item.rule_id, item.finding_id)) + ) + risk_findings = tuple(item.risk_finding for item in ordered_findings) + unknowns = tuple( + sorted( + item.finding_id + for item in ordered_findings + if item.evidence_state is BehaviorEvidenceState.UNKNOWN + ) + ) + unsigned = { + "schema_version": BEHAVIOR_RISK_SCHEMA_VERSION, + "candidate_digest": candidate_digest, + "findings": [item.to_dict() for item in ordered_findings], + "unknowns": list(unknowns), + "minimum_risk": maximum_risk(risk_findings).value, + "mandatory_controls": list(mandatory_control_ids(risk_findings)), + "mandatory_capabilities": list(mandatory_capability_ids(risk_findings)), + "scanned_files": scanned_files, + "scanned_bytes": scanned_bytes, + "threat_model": list(_THREAT_MODEL), + } + return BehaviorRiskReport( + schema_version=unsigned["schema_version"], + candidate_digest=unsigned["candidate_digest"], + findings=ordered_findings, + unknowns=unknowns, + minimum_risk=unsigned["minimum_risk"], + mandatory_controls=tuple(unsigned["mandatory_controls"]), + mandatory_capabilities=tuple(unsigned["mandatory_capabilities"]), + scanned_files=scanned_files, + scanned_bytes=scanned_bytes, + threat_model=_THREAT_MODEL, + content_digest=digest_json(unsigned), + ) + + +def audit_behavior_risk( + candidate_root: str | os.PathLike[str], + *, + limits: BehaviorScanLimits | None = None, +) -> BehaviorRiskReport: + """Audit the current bytes of one Skill candidate and return a frozen report. + + The root must be an absolute, normalized directory. Symlinks, special + files, path escapes, resource-limit violations, and any observed drift fail + the audit rather than being converted into a low-risk result. + """ + + selected_limits = limits or BehaviorScanLimits() + if not isinstance(selected_limits, BehaviorScanLimits): + raise TypeError("limits must be a BehaviorScanLimits value") + raw_path = os.fspath(candidate_root) + if not isinstance(raw_path, str) or not raw_path or "\x00" in raw_path: + raise BehaviorAuditError("candidate_root must be a non-empty filesystem path") + supplied = Path(raw_path).expanduser() + if not supplied.is_absolute(): + raise BehaviorAuditError("candidate_root must be absolute") + if ".." in supplied.parts: + raise BehaviorAuditError("candidate_root cannot contain parent traversal") + root = Path(os.path.normpath(supplied)) + try: + root_metadata = root.lstat() + except OSError as exc: + raise BehaviorAuditError("candidate_root is unavailable") from exc + if stat.S_ISLNK(root_metadata.st_mode): + raise BehaviorAuditError("candidate_root cannot be a symbolic link") + if not stat.S_ISDIR(root_metadata.st_mode): + raise BehaviorAuditError("candidate_root must be a directory") + + try: + root_descriptor = os.open(root, _open_flags(directory=True)) + except OSError as exc: + raise BehaviorAuditError("candidate_root cannot be safely opened") from exc + try: + opened_root = os.fstat(root_descriptor) + if _stable_metadata(root_metadata) != _stable_metadata(opened_root): + raise BehaviorCandidateChangedError( + "candidate_root changed while it was being opened" + ) + candidate_digest, snapshots, total_bytes = _snapshot_tree( + root_descriptor, + limits=selected_limits, + ) + by_path = {item.path: item for item in snapshots} + skill_snapshot = by_path.get("SKILL.md") + if skill_snapshot is None: + raise BehaviorAuditError("candidate root must contain a regular SKILL.md") + + findings: dict[str, BehaviorFinding] = {} + for snapshot in snapshots: + _scan_material(snapshot, findings) + path = PurePosixPath(snapshot.path) + lower_parts = tuple(part.casefold() for part in path.parts) + if snapshot.path == "SKILL.md": + _scan_skill_markdown(snapshot, findings) + if lower_parts and lower_parts[0] == "scripts": + if path.suffix.casefold() == ".py": + _scan_python(snapshot, findings, selected_limits) + elif ( + path.suffix.casefold() in _SCRIPT_SUFFIXES + or snapshot.data.startswith(b"#!") + or bool(snapshot.mode & 0o111) + ): + _scan_other_script(snapshot, findings) + + try: + current_metadata = root.lstat() + except OSError as exc: + raise BehaviorCandidateChangedError( + "candidate_root disappeared during audit" + ) from exc + if _stable_metadata(current_metadata) != _stable_metadata(opened_root): + raise BehaviorCandidateChangedError( + "candidate_root changed during audit" + ) + try: + final_digest = tree_digest(root) + except (OSError, ValueError) as exc: + raise BehaviorCandidateChangedError( + "candidate tree became unsafe during audit" + ) from exc + if final_digest != candidate_digest: + raise BehaviorCandidateChangedError( + "candidate bytes changed during behavior audit" + ) + final_metadata = root.lstat() + if _stable_metadata(final_metadata) != _stable_metadata(opened_root): + raise BehaviorCandidateChangedError( + "candidate_root changed during final digest" + ) + finally: + os.close(root_descriptor) + + return _build_report( + candidate_digest=candidate_digest, + findings=findings.values(), + scanned_files=len(snapshots), + scanned_bytes=total_bytes, + ) + + +def audit_behavior( + candidate_root: str | os.PathLike[str], + *, + limits: BehaviorScanLimits | None = None, +) -> BehaviorRiskReport: + """Compatibility spelling for :func:`audit_behavior_risk`.""" + + return audit_behavior_risk(candidate_root, limits=limits) + + +__all__ = [ + "BEHAVIOR_RISK_SCHEMA_VERSION", + "BehaviorAuditError", + "BehaviorCandidateChangedError", + "BehaviorEvidenceState", + "BehaviorFinding", + "BehaviorLimitError", + "BehaviorReportValidationError", + "BehaviorRiskError", + "BehaviorRiskReport", + "BehaviorScanLimits", + "audit_behavior", + "audit_behavior_risk", + "report_from_dict", + "validate_behavior_risk_report", +] diff --git a/runtime/skill-optimizer/scripts/core/delivery.py b/runtime/skill-optimizer/scripts/core/delivery.py new file mode 100644 index 0000000..a992a0c --- /dev/null +++ b/runtime/skill-optimizer/scripts/core/delivery.py @@ -0,0 +1,664 @@ +"""Fail-closed delivery eligibility derived from current candidate bytes. + +This module deliberately does not provide a trust-upgrade hook for quality, +capability, or control mappings. It can approve the narrow standalone case +where no such claim is required and a raw workflow chain contains an exact +user authorization. Everything else remains blocked until the shared +evidence-graph adapters can rebuild the relevant facts. +""" + +from __future__ import annotations + +from collections.abc import Iterable, Mapping, Sequence +from dataclasses import InitVar, dataclass +import os +from pathlib import Path +import re +import stat +from typing import Any + +from .behavior_risk import ( + BehaviorRiskReport, + audit_behavior_risk, + validate_behavior_risk_report, +) +from .canonical import ( + ClosedStrEnum, + RiskLevel, + digest_json, +) +from .process_plan import ProcessCapabilityError, validate_process_capability_facts + + +DELIVERY_ELIGIBILITY_OBJECT_VERSION = "skill-optimizer.delivery-eligibility/1" +QUALITY_PROJECTION_OBJECT_VERSION = "skill-optimizer.quality-projection/1" +BEHAVIOR_AND_DELIVERY_SUMMARY_OBJECT_VERSION = ( + "skill-optimizer.behavior-and-delivery-summary/1" +) +DELIVERY_AUTHORIZATION_TARGET_OBJECT_VERSION = ( + "skill-optimizer.delivery-authorization-target/1" +) + +_DIGEST_RE = re.compile(r"^sha256:[0-9a-f]{64}$") +_SUMMARY_FACTORY_TOKEN = object() + + +class DeliveryError(RuntimeError): + """Delivery evidence or target facts are invalid.""" + + +class DeliveryTarget(ClosedStrEnum): + PERSONAL_INSTALL = "P1_personal_install" + TEAM_PACKAGE = "P2_team_package" + TRANSACTIONAL_INSTALL = "P3_transactional_install" + + +@dataclass(frozen=True) +class QualityProjection: + """Display-only projection of an untrusted quality summary. + + The standalone Track C boundary has no Gate 1 raw-graph adapter. A caller + can therefore never cause an item to enter ``verified_claims`` here. + """ + + candidate_digest: str + verified_claims: tuple[str, ...] + unverified_claims: tuple[str, ...] + blocked_claims: tuple[str, ...] + removed_claims: tuple[str, ...] + integration_requests: tuple[str, ...] + object_version: str = QUALITY_PROJECTION_OBJECT_VERSION + + def __post_init__(self) -> None: + if self.object_version != QUALITY_PROJECTION_OBJECT_VERSION: + raise ValueError("unsupported quality projection object_version") + _require_digest(self.candidate_digest, "quality candidate_digest") + for field_name in ( + "verified_claims", + "unverified_claims", + "blocked_claims", + "removed_claims", + "integration_requests", + ): + object.__setattr__( + self, + field_name, + _closed_strings(getattr(self, field_name), field_name), + ) + if self.verified_claims: + raise ValueError( + "standalone delivery cannot upgrade caller claims to verified" + ) + claim_sets = ( + set(self.unverified_claims), + set(self.blocked_claims), + set(self.removed_claims), + ) + if any(left & right for index, left in enumerate(claim_sets) for right in claim_sets[index + 1 :]): + raise ValueError("quality claim states must not overlap") + + def body(self) -> dict[str, Any]: + return { + "object_version": self.object_version, + "candidate_digest": self.candidate_digest, + "verified_claims": list(self.verified_claims), + "unverified_claims": list(self.unverified_claims), + "blocked_claims": list(self.blocked_claims), + "removed_claims": list(self.removed_claims), + "integration_requests": list(self.integration_requests), + } + + @property + def content_digest(self) -> str: + return digest_json(self.body()) + + def to_dict(self) -> dict[str, Any]: + return {**self.body(), "content_digest": self.content_digest} + + +@dataclass(frozen=True) +class DeliveryEligibility: + delivery_target: DeliveryTarget + candidate: str + candidate_digest: str + behavior_report_digest: str + risk_commitment_digest: str + minimum_risk: RiskLevel + mandatory_controls: tuple[str, ...] + mandatory_capabilities: tuple[str, ...] + target: str + authorization_target_digest: str + workflow_id: str | None + workflow_event_head_digest: str | None + authorization_grant_digest: str | None + quality_projection: QualityProjection + quality_unverified: bool + eligible: bool + reasons: tuple[str, ...] + warnings: tuple[str, ...] + integration_requests: tuple[str, ...] + object_version: str = DELIVERY_ELIGIBILITY_OBJECT_VERSION + + def __post_init__(self) -> None: + if self.object_version != DELIVERY_ELIGIBILITY_OBJECT_VERSION: + raise ValueError("unsupported delivery eligibility object_version") + object.__setattr__(self, "delivery_target", DeliveryTarget(self.delivery_target)) + object.__setattr__(self, "minimum_risk", RiskLevel(self.minimum_risk)) + for value, field_name in ( + (self.candidate_digest, "candidate_digest"), + (self.behavior_report_digest, "behavior_report_digest"), + (self.risk_commitment_digest, "risk_commitment_digest"), + (self.authorization_target_digest, "authorization_target_digest"), + ): + _require_digest(value, field_name) + for value, field_name in ( + (self.workflow_event_head_digest, "workflow_event_head_digest"), + (self.authorization_grant_digest, "authorization_grant_digest"), + ): + if value is not None: + _require_digest(value, field_name) + for field_name in ( + "mandatory_controls", + "mandatory_capabilities", + "reasons", + "warnings", + "integration_requests", + ): + object.__setattr__( + self, + field_name, + _closed_strings(getattr(self, field_name), field_name), + ) + if not isinstance(self.quality_projection, QualityProjection): + raise ValueError("quality_projection must be a QualityProjection") + if self.quality_projection.candidate_digest != self.candidate_digest: + raise ValueError("quality projection is bound to different candidate bytes") + if self.quality_unverified is not True: + raise ValueError("standalone delivery must retain quality_unverified") + if not isinstance(self.eligible, bool): + raise ValueError("eligible must be boolean") + if self.eligible: + raise ValueError( + "standalone delivery cannot mint eligible=true without a trusted adapter" + ) + if self.authorization_grant_digest is not None or self.workflow_event_head_digest is not None: + raise ValueError( + "standalone delivery cannot attach caller-authored authorization evidence" + ) + if "trusted workflow event source is unavailable" not in self.reasons: + raise ValueError("blocked delivery must state the missing workflow authority") + if "trusted-workflow-event-source-adapter" not in self.integration_requests: + raise ValueError("blocked delivery must request a trusted workflow source") + if "quality-unverified" not in self.quality_projection.unverified_claims: + raise ValueError("quality_unverified must be represented in the claim projection") + + def body(self) -> dict[str, Any]: + return { + "object_version": self.object_version, + "delivery_target": self.delivery_target.value, + "candidate": self.candidate, + "candidate_digest": self.candidate_digest, + "behavior_report_digest": self.behavior_report_digest, + "risk_commitment_digest": self.risk_commitment_digest, + "minimum_risk": self.minimum_risk.value, + "mandatory_controls": list(self.mandatory_controls), + "mandatory_capabilities": list(self.mandatory_capabilities), + "target": self.target, + "authorization_target_digest": self.authorization_target_digest, + "workflow_id": self.workflow_id, + "workflow_event_head_digest": self.workflow_event_head_digest, + "authorization_grant_digest": self.authorization_grant_digest, + "quality_projection": self.quality_projection.to_dict(), + "quality_unverified": self.quality_unverified, + "eligible": self.eligible, + "reasons": list(self.reasons), + "warnings": list(self.warnings), + "integration_requests": list(self.integration_requests), + } + + @property + def content_digest(self) -> str: + return digest_json(self.body()) + + def to_dict(self) -> dict[str, Any]: + return {**self.body(), "content_digest": self.content_digest} + +@dataclass(frozen=True) +class BehaviorAndDeliverySummary: + behavior_findings: tuple[Mapping[str, Any], ...] + minimum_risk: RiskLevel + mandatory_controls: tuple[str, ...] + unknowns: tuple[str, ...] + delivery_target: DeliveryTarget + delivery_eligibility: DeliveryEligibility + delivery_receipt: Mapping[str, Any] | None = None + rollback_receipt: Mapping[str, Any] | None = None + object_version: str = BEHAVIOR_AND_DELIVERY_SUMMARY_OBJECT_VERSION + _factory_token: InitVar[object | None] = None + + def __post_init__(self, _factory_token: object | None) -> None: + if _factory_token is not _SUMMARY_FACTORY_TOKEN: + raise ValueError( + "behavior/delivery summaries must be rebuilt from current evidence" + ) + if self.object_version != BEHAVIOR_AND_DELIVERY_SUMMARY_OBJECT_VERSION: + raise ValueError("unsupported behavior/delivery summary object_version") + object.__setattr__(self, "minimum_risk", RiskLevel(self.minimum_risk)) + object.__setattr__(self, "delivery_target", DeliveryTarget(self.delivery_target)) + object.__setattr__(self, "behavior_findings", tuple(dict(item) for item in self.behavior_findings)) + object.__setattr__(self, "mandatory_controls", _closed_strings(self.mandatory_controls, "mandatory_controls")) + object.__setattr__(self, "unknowns", _closed_strings(self.unknowns, "unknowns")) + if not isinstance(self.delivery_eligibility, DeliveryEligibility): + raise ValueError("delivery_eligibility must be a typed blocked result") + if self.delivery_eligibility.eligible: + raise ValueError("standalone summary cannot contain eligible=true") + if self.delivery_receipt is not None or self.rollback_receipt is not None: + raise ValueError( + "standalone summary receipt wiring requires a typed integration adapter" + ) + + def body(self) -> dict[str, Any]: + return { + "object_version": self.object_version, + "behavior_findings": [dict(item) for item in self.behavior_findings], + "minimum_risk": self.minimum_risk.value, + "mandatory_controls": list(self.mandatory_controls), + "unknowns": list(self.unknowns), + "delivery_target": self.delivery_target.value, + "delivery_eligibility": self.delivery_eligibility.to_dict(), + "delivery_receipt": ( + dict(self.delivery_receipt) if self.delivery_receipt is not None else None + ), + "rollback_receipt": ( + dict(self.rollback_receipt) if self.rollback_receipt is not None else None + ), + } + + @property + def content_digest(self) -> str: + return digest_json(self.body()) + + def to_dict(self) -> dict[str, Any]: + return {**self.body(), "content_digest": self.content_digest} + + +def _require_digest(value: object, field_name: str) -> str: + if not isinstance(value, str) or _DIGEST_RE.fullmatch(value) is None: + raise ValueError(f"{field_name} must be a sha256 digest") + return value + + +def _closed_strings(value: Iterable[str], field_name: str) -> tuple[str, ...]: + if isinstance(value, (str, bytes)): + raise ValueError(f"{field_name} must be an array") + normalized = tuple(value) + if any(not isinstance(item, str) or not item.strip() for item in normalized): + raise ValueError(f"{field_name} must contain non-empty strings") + if len(set(normalized)) != len(normalized): + raise ValueError(f"{field_name} must not contain duplicates") + return tuple(sorted(normalized)) + + +def _claim_values(value: object, field_name: str) -> tuple[str, ...]: + if value is None: + return () + if isinstance(value, (str, bytes)) or not isinstance(value, Sequence): + raise DeliveryError(f"quality {field_name} must be an array") + return _closed_strings(value, f"quality.{field_name}") + + +def project_untrusted_quality( + candidate_digest: str, + value: Mapping[str, Any] | None, +) -> QualityProjection: + """Project display facts without trusting any caller-authored verified state.""" + + _require_digest(candidate_digest, "candidate_digest") + if value is None: + return QualityProjection( + candidate_digest=candidate_digest, + verified_claims=(), + unverified_claims=("quality-unverified",), + blocked_claims=(), + removed_claims=(), + integration_requests=("gate1-quality-raw-graph-adapter",), + ) + if not isinstance(value, Mapping): + raise DeliveryError("quality evidence must be an object") + claimed_candidate = value.get("candidate_digest") + if claimed_candidate is not None and claimed_candidate != candidate_digest: + raise DeliveryError("quality evidence is bound to different candidate bytes") + caller_verified = set(_claim_values(value.get("verified_claims"), "verified_claims")) + unverified = set(_claim_values(value.get("unverified_claims"), "unverified_claims")) + blocked = set(_claim_values(value.get("blocked_claims"), "blocked_claims")) + removed = set(_claim_values(value.get("removed_claims"), "removed_claims")) + requests = {"gate1-quality-raw-graph-adapter"} + if caller_verified: + blocked.update(caller_verified) + requests.add("reject-caller-authored-verified-claims") + formal_outcome = value.get("formal_outcome") + if formal_outcome not in {None, "unverified", "blocked", "removed"}: + blocked.add("formal-adoption") + requests.add("gate1-formal-adoption-evidence") + unverified.add("quality-unverified") + unverified.difference_update(blocked | removed) + removed.difference_update(blocked) + return QualityProjection( + candidate_digest=candidate_digest, + verified_claims=(), + unverified_claims=tuple(unverified), + blocked_claims=tuple(blocked), + removed_claims=tuple(removed), + integration_requests=tuple(requests), + ) + + +def behavior_risk_commitment_digest(report: BehaviorRiskReport) -> str: + """Bind authorization to the report and all derived R/C/K facts.""" + + validate_behavior_risk_report(report) + return digest_json( + { + "object_version": "skill-optimizer.behavior-risk-commitment/1", + "behavior_report_digest": report.content_digest, + "candidate_digest": report.candidate_digest, + "minimum_risk": report.minimum_risk.value, + "mandatory_controls": list(report.mandatory_controls), + "mandatory_capabilities": list(report.mandatory_capabilities), + "unknowns": list(report.unknowns), + "requires_runtime_enforcement": report.requires_runtime_enforcement, + "sensitive_material_bundled": report.has_sensitive_material, + } + ) + + +def delivery_authorization_target_digest( + *, + delivery_target: DeliveryTarget | str, + candidate_digest: str, + behavior_report_digest: str, + target: str | os.PathLike[str], + expected_target_digest: str | None, + host: str | None, + quality_unverified: bool, +) -> str: + """Return the exact, non-caller-selectable delivery authorization identity.""" + + normalized_target = DeliveryTarget(delivery_target) + _require_digest(candidate_digest, "candidate_digest") + _require_digest(behavior_report_digest, "behavior_report_digest") + if expected_target_digest is not None: + _require_digest(expected_target_digest, "expected_target_digest") + if not isinstance(quality_unverified, bool): + raise ValueError("quality_unverified must be boolean") + exact_target = _canonical_target_path(target) + return digest_json( + { + "object_version": DELIVERY_AUTHORIZATION_TARGET_OBJECT_VERSION, + "delivery_target": normalized_target.value, + "candidate_digest": candidate_digest, + "behavior_report_digest": behavior_report_digest, + "target": str(exact_target), + "expected_target_digest": expected_target_digest, + "host": host, + "quality_unverified": quality_unverified, + } + ) + + +def _canonical_target_path(path: str | os.PathLike[str]) -> Path: + supplied = Path(path).expanduser() + if os.path.lexists(supplied) and supplied.is_symlink(): + raise DeliveryError("delivery target cannot be a symbolic link") + resolved = supplied.resolve(strict=False) + if resolved == resolved.parent or not resolved.name: + raise DeliveryError("delivery target cannot be a filesystem root") + parent = resolved.parent.resolve(strict=True) + metadata = parent.lstat() + if not stat.S_ISDIR(metadata.st_mode) or stat.S_ISLNK(metadata.st_mode): + raise DeliveryError("delivery target parent must be a real directory") + return parent / resolved.name + + +def _target_errors( + delivery_target: DeliveryTarget, + candidate: Path, + target: Path, + approved_root: str | os.PathLike[str] | None, +) -> list[str]: + errors: list[str] = [] + if approved_root is None: + return ["approved delivery root is required"] + root_input = Path(approved_root).expanduser() + if root_input.is_symlink(): + return ["approved delivery root cannot be a symbolic link"] + try: + root = root_input.resolve(strict=True) + except FileNotFoundError: + return ["approved delivery root does not exist"] + metadata = root.lstat() + if not stat.S_ISDIR(metadata.st_mode) or stat.S_ISLNK(metadata.st_mode): + return ["approved delivery root must be a real directory"] + if delivery_target is DeliveryTarget.PERSONAL_INSTALL and target.parent != root: + errors.append("P1 target must be a direct child of the approved personal root") + if delivery_target is DeliveryTarget.TEAM_PACKAGE and target != root: + errors.append("P2 target must be the exact approved isolated output root") + if target == candidate or target.is_relative_to(candidate) or candidate.is_relative_to(target): + errors.append("delivery target and candidate must not overlap") + return errors + + +def _raw_plan_evidence_errors( + *, + report: BehaviorRiskReport, + process_plan: Mapping[str, Any] | None, + capability_resolution: Mapping[str, Any] | None, + module_results: Iterable[Mapping[str, Any]] | None, +) -> tuple[list[str], list[str]]: + """Validate what can be rebuilt, then refuse to treat self-hashes as authority.""" + + reasons: list[str] = [] + requests: list[str] = [] + required_controls = set(report.mandatory_controls) + required_capabilities = set(report.mandatory_capabilities) + if process_plan is not None: + supplied = process_plan.get("content_digest") + if not isinstance(supplied, str) or digest_json( + {key: item for key, item in process_plan.items() if key != "content_digest"} + ) != supplied: + reasons.append("ProcessPlan digest is invalid") + if process_plan.get("risk_level") != report.minimum_risk.value: + reasons.append("ProcessPlan risk level does not match current behavior") + if capability_resolution is not None: + try: + validate_process_capability_facts( + capability_resolution, + risk_findings=report.risk_findings, + require_verified_chain=False, + ) + except (ProcessCapabilityError, TypeError, ValueError) as exc: + reasons.append(f"capability resolution is invalid: {exc}") + if module_results is not None: + normalized = tuple(module_results) + if any(not isinstance(item, Mapping) for item in normalized): + reasons.append("module results must be raw objects") + if normalized: + requests.append("process-module-result-graph-adapter") + if required_controls: + reasons.append("mandatory controls lack trusted raw execution evidence") + requests.append("process-plan-module-result-graph-adapter") + if required_capabilities: + reasons.append("mandatory capabilities lack a trusted current host probe chain") + requests.append("capability-probe-registry-adapter") + return reasons, requests + + +def evaluate_delivery_eligibility( + candidate_root: str | os.PathLike[str], + *, + delivery_target: DeliveryTarget | str, + target: str | os.PathLike[str], + approved_root: str | os.PathLike[str] | None, + scope_digest: str, + workflow_id: str | None = None, + expected_target_digest: str | None = None, + host: str | None = None, + quality_summary: Mapping[str, Any] | None = None, + required_claims: Iterable[str] = (), + process_plan: Mapping[str, Any] | None = None, + capability_resolution: Mapping[str, Any] | None = None, + module_results: Iterable[Mapping[str, Any]] | None = None, +) -> DeliveryEligibility: + """Rebuild current-byte eligibility and fail closed at missing trust adapters.""" + + normalized_target = DeliveryTarget(delivery_target) + _require_digest(scope_digest, "scope_digest") + if expected_target_digest is not None: + _require_digest(expected_target_digest, "expected_target_digest") + supplied_candidate = Path(candidate_root).expanduser() + if not supplied_candidate.is_absolute(): + raise DeliveryError("candidate root must be absolute") + if os.path.lexists(supplied_candidate) and supplied_candidate.is_symlink(): + raise DeliveryError("candidate root cannot be a symbolic link") + candidate = supplied_candidate.resolve(strict=True) + report = audit_behavior_risk(candidate) + validate_behavior_risk_report(report) + exact_target = _canonical_target_path(target) + projection = project_untrusted_quality(report.candidate_digest, quality_summary) + risk_commitment = behavior_risk_commitment_digest(report) + quality_unverified = True + authorization_target = delivery_authorization_target_digest( + delivery_target=normalized_target, + candidate_digest=report.candidate_digest, + behavior_report_digest=report.content_digest, + target=exact_target, + expected_target_digest=expected_target_digest, + host=host, + quality_unverified=quality_unverified, + ) + + reasons = _target_errors( + normalized_target, candidate, exact_target, approved_root + ) + warnings = [ + "quality is unverified; delivery does not prove host activation or routing", + ] + integration_requests = list(projection.integration_requests) + + if report.has_unknowns: + reasons.append("behavior report contains unresolved unknowns") + if report.requires_runtime_enforcement: + reasons.append("behavior report requires unverified runtime enforcement") + if normalized_target is DeliveryTarget.PERSONAL_INSTALL and report.minimum_risk.severity >= RiskLevel.R2.severity: + reasons.append("P1 cannot carry R2/R3 behavior; use P3") + if report.has_sensitive_material: + reasons.append("candidate contains sensitive material") + if normalized_target is DeliveryTarget.TRANSACTIONAL_INSTALL: + reasons.append("P3 eligibility is owned by core.install integration") + integration_requests.append("p3-transactional-install-decision-adapter") + + requested_claims = _closed_strings(required_claims, "required_claims") + if requested_claims: + reasons.append("required quality claims are not verified by Gate 1") + integration_requests.append("gate1-quality-raw-graph-adapter") + reasons.append("frozen automatic-routing scope is unavailable") + integration_requests.extend( + ( + "gate1-automatic-routing-claim-adapter", + "automatic-routing-scope-adapter", + "adjacent-skill-regression-evidence", + "false-trigger-gate-evidence", + ) + ) + + raw_reasons, raw_requests = _raw_plan_evidence_errors( + report=report, + process_plan=process_plan, + capability_resolution=capability_resolution, + module_results=module_results, + ) + reasons.extend(raw_reasons) + integration_requests.extend(raw_requests) + + grant_digest = None + event_head = None + # Track C cannot safely accept a caller-authored event array, validator, + # receipt, or marker as user authority. Host integration must resolve the + # workflow ID to a fixed, library-owned event source and then rebuild this + # result. P1/P2 mutation APIs implement their own fixed-root loaders; this + # generic display evaluator deliberately remains blocked until the shared + # adapter exists. + reasons.append("trusted workflow event source is unavailable") + integration_requests.append("trusted-workflow-event-source-adapter") + + return DeliveryEligibility( + delivery_target=normalized_target, + candidate=str(candidate), + candidate_digest=report.candidate_digest, + behavior_report_digest=report.content_digest, + risk_commitment_digest=risk_commitment, + minimum_risk=report.minimum_risk, + mandatory_controls=tuple(report.mandatory_controls), + mandatory_capabilities=tuple(report.mandatory_capabilities), + target=str(exact_target), + authorization_target_digest=authorization_target, + workflow_id=workflow_id, + workflow_event_head_digest=event_head, + authorization_grant_digest=grant_digest, + quality_projection=projection, + quality_unverified=quality_unverified, + eligible=not reasons, + reasons=tuple(sorted(set(reasons))), + warnings=tuple(warnings), + integration_requests=tuple(sorted(set(integration_requests))), + ) + + +def build_behavior_and_delivery_summary( + report: BehaviorRiskReport, + eligibility: DeliveryEligibility, + *, + delivery_receipt: Mapping[str, Any] | None = None, + rollback_receipt: Mapping[str, Any] | None = None, +) -> BehaviorAndDeliverySummary: + validate_behavior_risk_report(report) + if report.candidate_digest != eligibility.candidate_digest: + raise DeliveryError("behavior and delivery records bind different candidate bytes") + if report.content_digest != eligibility.behavior_report_digest: + raise DeliveryError("delivery eligibility binds another behavior report") + if eligibility.minimum_risk is not report.minimum_risk: + raise DeliveryError("delivery eligibility lowers or changes current risk") + if eligibility.mandatory_controls != tuple(report.mandatory_controls): + raise DeliveryError("delivery eligibility changes mandatory controls") + if eligibility.mandatory_capabilities != tuple(report.mandatory_capabilities): + raise DeliveryError("delivery eligibility changes mandatory capabilities") + if eligibility.risk_commitment_digest != behavior_risk_commitment_digest(report): + raise DeliveryError("delivery eligibility binds another risk commitment") + if delivery_receipt is not None or rollback_receipt is not None: + raise DeliveryError( + "receipt projection requires target-specific current-byte integration" + ) + return BehaviorAndDeliverySummary( + behavior_findings=tuple(item.to_dict() for item in report.findings), + minimum_risk=report.minimum_risk, + mandatory_controls=tuple(report.mandatory_controls), + unknowns=tuple(report.unknowns), + delivery_target=eligibility.delivery_target, + delivery_eligibility=eligibility, + delivery_receipt=delivery_receipt, + rollback_receipt=rollback_receipt, + _factory_token=_SUMMARY_FACTORY_TOKEN, + ) + + +__all__ = [ + "DeliveryEligibility", + "DeliveryError", + "DeliveryTarget", + "QualityProjection", + "behavior_risk_commitment_digest", + "build_behavior_and_delivery_summary", + "delivery_authorization_target_digest", + "evaluate_delivery_eligibility", + "project_untrusted_quality", +] diff --git a/runtime/skill-optimizer/scripts/core/personal_install.py b/runtime/skill-optimizer/scripts/core/personal_install.py new file mode 100644 index 0000000..e1bd757 --- /dev/null +++ b/runtime/skill-optimizer/scripts/core/personal_install.py @@ -0,0 +1,2460 @@ +"""Narrow P1 personal installation with raw-event authority and safe rollback. + +This module intentionally does not reuse the P3 durability claims in +``core.install``. P1 is limited to direct children of library-recognized, +user-owned personal Skill roots on a local filesystem. It serializes writers, +uses compare-and-swap and atomic no-replace renames, and keeps a tamper-evident +journal under a fixed root derived from the personal Skill root. + +The journal prevents ordinary receipt injection and accidental replay. It is +not a signature, does not defend against a malicious actor running as the same +OS user, and does not provide crash recovery. Targets requiring those +properties must use P3. +""" + +from __future__ import annotations + +from collections.abc import Mapping +from contextlib import contextmanager +import ctypes +from dataclasses import dataclass +import errno +import fcntl +import json +import os +from pathlib import Path +import re +import secrets +import stat +import subprocess +import sys +from typing import Any, Iterator + +from .behavior_risk import ( + BehaviorRiskReport, + audit_behavior_risk, + validate_behavior_risk_report, +) +from .canonical import ( + AuthorizationKind, + AuthorizationStatus, + RiskLevel, + canonical_json, + digest_json, +) +from .delivery import behavior_risk_commitment_digest +from .filesystem import fsync_directory +from .workflow import ( + AuthorizationGrant, + WorkflowActor, + WorkflowEvent, + WorkflowEventType, + recover_events, +) +from .workspace import ( + CandidateChangedError, + TargetChangedError, + optional_path_digest, + path_digest, +) + + +PERSONAL_INSTALL_ACTION = "personal_install" +QUALITY_UNVERIFIED_ACTION = "quality_unverified" +PERSONAL_INSTALL_DECISION_OBJECT_VERSION = ( + "skill-optimizer.personal-install-decision/1" +) +PERSONAL_INSTALL_AUTHORIZATION_TARGET_OBJECT_VERSION = ( + "skill-optimizer.personal-install-authorization-target/1" +) +PERSONAL_TARGET_OBSERVATION_OBJECT_VERSION = ( + "skill-optimizer.personal-target-observation/1" +) +PERSONAL_INSTALL_RECEIPT_OBJECT_VERSION = ( + "skill-optimizer.personal-install-receipt/1" +) +PERSONAL_ROLLBACK_RECEIPT_OBJECT_VERSION = ( + "skill-optimizer.personal-rollback-receipt/1" +) +PERSONAL_INSTALL_JOURNAL_OBJECT_VERSION = ( + "skill-optimizer.personal-install-journal/1" +) + +_CONTROL_DIRECTORY_NAME = ".skill-optimizer-personal-install" +_DIGEST_RE = re.compile(r"^sha256:[0-9a-f]{64}$") +_TRANSACTION_RE = re.compile(r"^pi-[0-9a-f]{64}$") +_TARGET_NAME_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$") +_DARWIN_RENAME_EXCL = 0x00000004 +_LINUX_RENAME_NOREPLACE = 0x00000001 +_LINUX_AT_FDCWD = -100 +_MAX_JOURNAL_BYTES = 256 * 1024 +_MAX_WORKFLOW_LOG_BYTES = 4 * 1024 * 1024 +_MAX_WORKFLOW_EVENT_BYTES = 256 * 1024 +_MAX_WORKFLOW_EVENTS = 4096 +_P1_LIMITATIONS = ( + "no-host-activation-proof", + "no-automatic-routing-claim", + "no-crash-recovery", + "no-p3-durable-journal", + "quality-unverified", + "same-user-malicious-actor-out-of-scope", +) +_MANAGED_MARKERS = ( + ".managed", + ".organization-managed", + ".enterprise-managed", + "managed.json", + "managed-settings.json", + "organization.json", + "enterprise.json", +) +_DARWIN_MOUNT_LINE_RE = re.compile( + r"^.+ on (?P.+) \((?P[^)]*)\)$" +) +_NETWORK_FILESYSTEMS = frozenset( + { + "9p", + "afpfs", + "autofs", + "cifs", + "fuse.sshfs", + "nfs", + "nfs4", + "smb2", + "smb3", + "smbfs", + "sshfs", + "webdav", + } +) +_LOCAL_LINUX_FILESYSTEMS = frozenset( + { + "btrfs", + "devtmpfs", + "ext2", + "ext3", + "ext4", + "f2fs", + "overlay", + "overlayfs", + "ramfs", + "tmpfs", + "ufs", + "xfs", + "zfs", + } +) + + +class PersonalInstallError(RuntimeError): + """Base class for fail-closed P1 installation errors.""" + + +class PersonalInstallIntegrationError(PersonalInstallError): + """The shared workflow adapter cannot prove required authority facts.""" + + +class PersonalInstallTargetError(PersonalInstallError): + """The exact target is not a safe P1 personal target.""" + + +class PersonalInstallRiskError(PersonalInstallError): + """Current candidate behavior requires P3 or remains unknown.""" + + +class PersonalInstallAuthorizationError(PersonalInstallError): + """The raw workflow chain lacks one exact, current user grant.""" + + +class PersonalInstallJournalError(PersonalInstallError): + """The fixed library-owned transaction journal is invalid.""" + + +class PersonalInstallReplayError(PersonalInstallJournalError): + """A terminal or non-installable transaction was replayed.""" + + +class PersonalInstallRollbackError(PersonalInstallError): + """Rollback could not prove the target/backup transaction relations.""" + + +class PersonalInstallAmbiguousState(PersonalInstallError): + """P1 could not prove either the pre-install or installed state.""" + + +def _require_digest( + value: object, + field_name: str, + *, + nullable: bool = False, +) -> str | None: + if value is None and nullable: + return None + if not isinstance(value, str) or _DIGEST_RE.fullmatch(value) is None: + raise ValueError(f"{field_name} must be a sha256 digest") + return value + + +def _require_transaction_id(value: object) -> str: + if not isinstance(value, str) or _TRANSACTION_RE.fullmatch(value) is None: + raise PersonalInstallJournalError( + "rollback requires one opaque personal-install transaction ID" + ) + return value + + +def _configured_personal_roots() -> tuple[Path, ...]: + """Return the library-owned set of personal Skill roots. + + This is deliberately a zero-argument function rather than a public install + parameter. Tests may replace it in-process; production callers cannot use + an ``approved_root`` argument to bless an arbitrary shared directory. + """ + + home = Path.home().expanduser().resolve(strict=True) + return (home / ".codex" / "skills", home / ".claude" / "skills") + + +def _mode_is_private(metadata: os.stat_result) -> bool: + return not stat.S_IMODE(metadata.st_mode) & (stat.S_IWGRP | stat.S_IWOTH) + + +def _require_real_owned_directory(path: Path, field_name: str) -> os.stat_result: + try: + metadata = path.lstat() + except FileNotFoundError as exc: + raise PersonalInstallTargetError(f"{field_name} does not exist") from exc + if stat.S_ISLNK(metadata.st_mode) or not stat.S_ISDIR(metadata.st_mode): + raise PersonalInstallTargetError(f"{field_name} must be a real directory") + if metadata.st_uid != os.getuid(): + raise PersonalInstallTargetError(f"{field_name} is not owned by the current user") + if not _mode_is_private(metadata): + raise PersonalInstallTargetError( + f"{field_name} is writable by another OS principal" + ) + return metadata + + +def _decode_mount_path(value: str) -> str: + return ( + value.replace(r"\040", " ") + .replace(r"\011", "\t") + .replace(r"\012", "\n") + .replace(r"\134", "\\") + ) + + +def _filesystem_locality(path: Path) -> tuple[str | None, bool | None]: + """Return ``(filesystem_type, is_local)`` without trusting caller flags.""" + + resolved = path.resolve(strict=True) + if sys.platform == "darwin": + try: + result = subprocess.run( + ("/sbin/mount",), + check=True, + capture_output=True, + text=True, + timeout=5, + ) + except (OSError, subprocess.SubprocessError): + return None, None + matches: list[tuple[int, str, tuple[str, ...]]] = [] + for line in result.stdout.splitlines(): + match = _DARWIN_MOUNT_LINE_RE.fullmatch(line) + if match is None: + continue + mount = Path(_decode_mount_path(match.group("mount"))) + try: + if resolved != mount and not resolved.is_relative_to(mount): + continue + except (OSError, ValueError): + continue + options = tuple( + item.strip().lower() + for item in match.group("options").split(",") + if item.strip() + ) + filesystem_type = options[0] if options else "" + matches.append((len(str(mount)), filesystem_type, options)) + if not matches: + return None, None + _, filesystem_type, options = max(matches, key=lambda item: item[0]) + return filesystem_type or None, "local" in options + + if sys.platform.startswith("linux"): + try: + lines = Path("/proc/self/mountinfo").read_text(encoding="utf-8").splitlines() + except OSError: + return None, None + matches: list[tuple[int, str]] = [] + for line in lines: + fields = line.split() + if "-" not in fields: + continue + separator = fields.index("-") + if separator + 1 >= len(fields) or len(fields) <= 4: + continue + mount = Path(_decode_mount_path(fields[4])) + try: + if resolved != mount and not resolved.is_relative_to(mount): + continue + except (OSError, ValueError): + continue + matches.append((len(str(mount)), fields[separator + 1].lower())) + if not matches: + return None, None + _, filesystem_type = max(matches, key=lambda item: item[0]) + if filesystem_type in _NETWORK_FILESYSTEMS: + return filesystem_type, False + if filesystem_type in _LOCAL_LINUX_FILESYSTEMS: + return filesystem_type, True + return filesystem_type, None + + return None, None + + +def _safe_target_tree(path: Path) -> None: + """Reject links, special files, foreign owners, and shared-writable bytes.""" + + pending = [path] + while pending: + current = pending.pop() + metadata = current.lstat() + if stat.S_ISLNK(metadata.st_mode): + raise PersonalInstallTargetError( + f"personal target contains a symbolic link: {current}" + ) + if metadata.st_uid != os.getuid(): + raise PersonalInstallTargetError( + f"personal target contains foreign-owned bytes: {current}" + ) + if not _mode_is_private(metadata): + raise PersonalInstallTargetError( + f"personal target contains shared-writable bytes: {current}" + ) + if stat.S_ISDIR(metadata.st_mode): + with os.scandir(current) as entries: + pending.extend(Path(entry.path) for entry in entries) + elif stat.S_ISREG(metadata.st_mode): + if metadata.st_nlink != 1: + raise PersonalInstallTargetError( + f"personal target contains a shared hard-linked file: {current}" + ) + else: + raise PersonalInstallTargetError( + f"personal target contains a special filesystem entry: {current}" + ) + + +def _canonical_candidate(path: str | os.PathLike[str]) -> Path: + supplied = Path(path).expanduser() + if not supplied.is_absolute(): + raise PersonalInstallTargetError("candidate path must be absolute") + if os.path.lexists(supplied) and supplied.is_symlink(): + raise PersonalInstallTargetError("candidate root cannot be a symbolic link") + try: + resolved = supplied.resolve(strict=True) + except FileNotFoundError as exc: + raise PersonalInstallTargetError("candidate root does not exist") from exc + metadata = resolved.lstat() + if not stat.S_ISDIR(metadata.st_mode): + raise PersonalInstallTargetError("personal Skill candidate must be a directory") + skill_file = resolved / "SKILL.md" + if not skill_file.is_file() or skill_file.is_symlink(): + raise PersonalInstallTargetError("personal Skill candidate requires a regular SKILL.md") + return resolved + + +def _recognized_target( + path: str | os.PathLike[str], +) -> tuple[Path, Path, os.stat_result]: + supplied = Path(path).expanduser() + if not supplied.is_absolute(): + raise PersonalInstallTargetError("personal target path must be absolute") + if not _TARGET_NAME_RE.fullmatch(supplied.name): + raise PersonalInstallTargetError("personal target name is not a safe Skill name") + if supplied.name == _CONTROL_DIRECTORY_NAME: + raise PersonalInstallTargetError("personal target collides with the control root") + if os.path.lexists(supplied) and supplied.is_symlink(): + raise PersonalInstallTargetError("personal target cannot be a symbolic link") + try: + supplied_parent = supplied.parent.resolve(strict=True) + except FileNotFoundError as exc: + raise PersonalInstallTargetError( + "personal target parent does not exist" + ) from exc + exact_supplied = supplied_parent / supplied.name + + matches: list[tuple[Path, os.stat_result]] = [] + for raw_root in _configured_personal_roots(): + root_input = Path(raw_root).expanduser() + try: + root_metadata = _require_real_owned_directory( + root_input, "personal Skill root" + ) + root = root_input.resolve(strict=True) + except (FileNotFoundError, PersonalInstallTargetError): + continue + if exact_supplied.parent == root and exact_supplied == root / supplied.name: + matches.append((root, root_metadata)) + if len(matches) != 1: + raise PersonalInstallTargetError( + "P1 target must be a direct child of one recognized personal Skill root" + ) + root, root_metadata = matches[0] + exact_target = root / supplied.name + + parent = root.parent + _require_real_owned_directory(parent, "personal Skill root parent") + for marker_name in _MANAGED_MARKERS: + if os.path.lexists(root / marker_name) or os.path.lexists(parent / marker_name): + raise PersonalInstallTargetError( + "personal Skill root carries an organization/managed-directory signal" + ) + return root, exact_target, root_metadata + + +@dataclass(frozen=True) +class PersonalTargetObservation: + personal_root: str + target: str + target_exists: bool + target_digest: str | None + root_identity_digest: str + direct_child: bool + user_owned: bool + local_filesystem: bool + filesystem_type: str + single_writer_control: str + recoverable: bool + organization_managed: bool + crash_recovery_supported: bool + object_version: str = PERSONAL_TARGET_OBSERVATION_OBJECT_VERSION + + def __post_init__(self) -> None: + if self.object_version != PERSONAL_TARGET_OBSERVATION_OBJECT_VERSION: + raise ValueError("unsupported personal target observation object_version") + _require_digest(self.target_digest, "target_digest", nullable=True) + _require_digest(self.root_identity_digest, "root_identity_digest") + if not all( + isinstance(value, bool) + for value in ( + self.target_exists, + self.direct_child, + self.user_owned, + self.local_filesystem, + self.recoverable, + self.organization_managed, + self.crash_recovery_supported, + ) + ): + raise ValueError("personal target observation flags must be boolean") + + def body(self) -> dict[str, Any]: + return { + "object_version": self.object_version, + "personal_root": self.personal_root, + "target": self.target, + "target_exists": self.target_exists, + "target_digest": self.target_digest, + "root_identity_digest": self.root_identity_digest, + "direct_child": self.direct_child, + "user_owned": self.user_owned, + "local_filesystem": self.local_filesystem, + "filesystem_type": self.filesystem_type, + "single_writer_control": self.single_writer_control, + "recoverable": self.recoverable, + "organization_managed": self.organization_managed, + "crash_recovery_supported": self.crash_recovery_supported, + } + + @property + def content_digest(self) -> str: + return digest_json(self.body()) + + def to_dict(self) -> dict[str, Any]: + return {**self.body(), "content_digest": self.content_digest} + + +def observe_personal_target( + target: str | os.PathLike[str], +) -> PersonalTargetObservation: + """Probe personal/local/direct-child facts from the current filesystem.""" + + root, exact_target, root_metadata = _recognized_target(target) + filesystem_type, is_local = _filesystem_locality(root) + if filesystem_type is None or is_local is None: + raise PersonalInstallTargetError( + "personal target filesystem locality is unknown; P3 is required" + ) + if not is_local: + raise PersonalInstallTargetError( + "personal target is not on a local filesystem; P3 is required" + ) + + target_exists = os.path.lexists(exact_target) + target_digest: str | None = None + if target_exists: + target_metadata = exact_target.lstat() + if stat.S_ISLNK(target_metadata.st_mode) or not stat.S_ISDIR( + target_metadata.st_mode + ): + raise PersonalInstallTargetError( + "existing personal target must be a real directory" + ) + _safe_target_tree(exact_target) + target_digest = path_digest(exact_target) + + root_identity = digest_json( + { + "object_version": "skill-optimizer.personal-root-identity/1", + "path": str(root), + "device": root_metadata.st_dev, + "inode": root_metadata.st_ino, + "owner": root_metadata.st_uid, + } + ) + return PersonalTargetObservation( + personal_root=str(root), + target=str(exact_target), + target_exists=target_exists, + target_digest=target_digest, + root_identity_digest=root_identity, + direct_child=True, + user_owned=True, + local_filesystem=True, + filesystem_type=filesystem_type, + single_writer_control="library-root-lock-plus-cas-and-noreplace", + recoverable=True, + organization_managed=False, + crash_recovery_supported=False, + ) + + +@dataclass(frozen=True) +class _Preparation: + candidate: Path + target: Path + personal_root: Path + expected_target_digest: str | None + scope_digest: str + report: BehaviorRiskReport + risk_commitment_digest: str + observation: PersonalTargetObservation + decision_payload: Mapping[str, Any] + + +def _p1_behavior_report(candidate: Path) -> BehaviorRiskReport: + report = audit_behavior_risk(candidate) + validate_behavior_risk_report(report) + observed_digest = path_digest(candidate) + if report.candidate_digest != observed_digest: + raise CandidateChangedError( + "behavior audit does not bind the candidate's current bytes" + ) + if report.has_unknowns: + raise PersonalInstallRiskError( + "P1 refuses unresolved behavior unknowns; use P3" + ) + if report.requires_runtime_enforcement: + raise PersonalInstallRiskError( + "P1 cannot prove required runtime enforcement; use P3" + ) + if report.has_sensitive_material: + raise PersonalInstallRiskError( + "P1 refuses candidates containing sensitive material" + ) + if report.minimum_risk.severity >= RiskLevel.R2.severity: + raise PersonalInstallRiskError("R2/R3 behavior requires P3 installation") + if tuple(report.mandatory_capabilities): + raise PersonalInstallRiskError( + "P1 cannot prove mandatory host capabilities; use P3" + ) + return report + + +def _decision_payload( + *, + workflow_id: str, + candidate: Path, + report: BehaviorRiskReport, + observation: PersonalTargetObservation, + scope_digest: str, + risk_commitment_digest: str, +) -> dict[str, Any]: + return { + "object_version": PERSONAL_INSTALL_DECISION_OBJECT_VERSION, + "kind": "personal_install_decision", + "action": PERSONAL_INSTALL_ACTION, + "delivery_target": "P1_personal_install", + "workflow_id": workflow_id, + "candidate": str(candidate), + "candidate_digest": report.candidate_digest, + "behavior_report_digest": report.content_digest, + "risk_commitment_digest": risk_commitment_digest, + "personal_root": observation.personal_root, + "target": observation.target, + "expected_target_digest": observation.target_digest, + "target_observation_digest": observation.content_digest, + "scope_digest": scope_digest, + "quality_claim_status": "unverified", + "quality_unverified": "accepted", + "automatic_routing": "excluded", + "crash_recovery": "not_required", + } + + +def _prepare( + *, + candidate: str | os.PathLike[str], + target: str | os.PathLike[str], + expected_target_digest: str | None, + scope_digest: str, + workflow_id: str, +) -> _Preparation: + _require_digest(expected_target_digest, "expected_target_digest", nullable=True) + _require_digest(scope_digest, "scope_digest") + if not isinstance(workflow_id, str) or not workflow_id: + raise ValueError("workflow_id is required") + candidate_path = _canonical_candidate(candidate) + report = _p1_behavior_report(candidate_path) + observation = observe_personal_target(target) + if observation.target_digest != expected_target_digest: + raise TargetChangedError( + "personal target changed since the approved CAS observation" + ) + target_path = Path(observation.target) + root = Path(observation.personal_root) + if ( + target_path == candidate_path + or target_path.is_relative_to(candidate_path) + or candidate_path.is_relative_to(target_path) + ): + raise PersonalInstallTargetError("candidate and target must not overlap") + risk_commitment = behavior_risk_commitment_digest(report) + payload = _decision_payload( + workflow_id=workflow_id, + candidate=candidate_path, + report=report, + observation=observation, + scope_digest=scope_digest, + risk_commitment_digest=risk_commitment, + ) + return _Preparation( + candidate=candidate_path, + target=target_path, + personal_root=root, + expected_target_digest=expected_target_digest, + scope_digest=scope_digest, + report=report, + risk_commitment_digest=risk_commitment, + observation=observation, + decision_payload=payload, + ) + + +def make_personal_install_decision_payload( + *, + candidate: str | os.PathLike[str], + target: str | os.PathLike[str], + expected_target_digest: str | None, + scope_digest: str, + workflow_id: str, +) -> dict[str, Any]: + """Build the exact payload a user decision event must contain. + + The returned mapping is only authoring material. ``personal_install`` + never accepts it as authority and reconstructs it from current bytes. + """ + + return dict( + _prepare( + candidate=candidate, + target=target, + expected_target_digest=expected_target_digest, + scope_digest=scope_digest, + workflow_id=workflow_id, + ).decision_payload + ) + + +def _recover_exact_decision( + preparation: _Preparation, + *, + workflow_id: str, + workflow_events: tuple[WorkflowEvent | Mapping[str, Any], ...], +) -> tuple[WorkflowEvent, str]: + if not workflow_events: + raise PersonalInstallIntegrationError( + "raw workflow events ending in a user personal-install decision are required" + ) + try: + recovery = recover_events(workflow_events) + except (TypeError, ValueError, RuntimeError) as exc: + raise PersonalInstallAuthorizationError( + f"invalid personal-install workflow chain: {exc}" + ) from exc + if recovery.workflow_id != workflow_id: + raise PersonalInstallAuthorizationError("personal-install workflow ID mismatch") + decision = recovery.events[-1] + if ( + decision.event_type is not WorkflowEventType.DECISION + or decision.actor is not WorkflowActor.USER + or dict(decision.payload) != dict(preparation.decision_payload) + ): + raise PersonalInstallAuthorizationError( + "workflow head is not the exact current-byte user P1 decision" + ) + if recovery.last_event_digest != decision.content_digest: + raise PersonalInstallAuthorizationError("personal-install decision head mismatch") + return decision, decision.content_digest + + +def _authorization_target_digest( + preparation: _Preparation, + *, + workflow_id: str, + decision_event_head_digest: str, +) -> str: + _require_digest(decision_event_head_digest, "decision_event_head_digest") + return digest_json( + { + "object_version": PERSONAL_INSTALL_AUTHORIZATION_TARGET_OBJECT_VERSION, + "action": PERSONAL_INSTALL_ACTION, + "workflow_id": workflow_id, + "candidate": str(preparation.candidate), + "candidate_digest": preparation.report.candidate_digest, + "behavior_report_digest": preparation.report.content_digest, + "risk_commitment_digest": preparation.risk_commitment_digest, + "personal_root": str(preparation.personal_root), + "target": str(preparation.target), + "expected_target_digest": preparation.expected_target_digest, + "target_observation_digest": preparation.observation.content_digest, + "scope_digest": preparation.scope_digest, + "quality_claim_status": "unverified", + "quality_unverified": "accepted", + "decision_event_head_digest": decision_event_head_digest, + } + ) + + +def personal_install_authorization_target_digest( + *, + candidate: str | os.PathLike[str], + target: str | os.PathLike[str], + expected_target_digest: str | None, + scope_digest: str, + workflow_id: str, +) -> str: + """Rebuild a grant target from the fixed trusted log's user decision.""" + + preparation = _prepare( + candidate=candidate, + target=target, + expected_target_digest=expected_target_digest, + scope_digest=scope_digest, + workflow_id=workflow_id, + ) + records = _load_fixed_workflow_events(preparation.personal_root, workflow_id) + _, decision_head = _recover_exact_decision( + preparation, + workflow_id=workflow_id, + workflow_events=records, + ) + return _authorization_target_digest( + preparation, + workflow_id=workflow_id, + decision_event_head_digest=decision_head, + ) + + +@dataclass(frozen=True) +class _Authority: + decision_event_digest: str + authorization_event_digest: str + workflow_event_head_digest: str + grant_digest: str + authorization_target_digest: str + + +def _verify_install_authority( + preparation: _Preparation, + *, + workflow_id: str, + workflow_events: tuple[WorkflowEvent | Mapping[str, Any], ...], +) -> _Authority: + if len(workflow_events) < 2: + raise PersonalInstallIntegrationError( + "raw user decision and INSTALL authorization events are required" + ) + try: + recovery = recover_events(workflow_events) + except (TypeError, ValueError, RuntimeError) as exc: + raise PersonalInstallAuthorizationError( + f"invalid personal-install workflow chain: {exc}" + ) from exc + if recovery.workflow_id != workflow_id: + raise PersonalInstallAuthorizationError("personal-install workflow ID mismatch") + authorization_event = recovery.events[-1] + if ( + authorization_event.event_type is not WorkflowEventType.AUTHORIZATION + or authorization_event.actor is not WorkflowActor.USER + ): + raise PersonalInstallAuthorizationError( + "workflow head must be the exact user INSTALL grant event" + ) + + decision, decision_head = _recover_exact_decision( + preparation, + workflow_id=workflow_id, + workflow_events=workflow_events[:-1], + ) + if authorization_event.previous_event_digest != decision.content_digest: + raise PersonalInstallAuthorizationError( + "INSTALL grant is not adjacent to the bound user decision" + ) + authorization_target = _authorization_target_digest( + preparation, + workflow_id=workflow_id, + decision_event_head_digest=decision_head, + ) + try: + grant = AuthorizationGrant.from_dict(authorization_event.payload["grant"]) + except (KeyError, TypeError, ValueError) as exc: + raise PersonalInstallAuthorizationError("invalid user INSTALL grant") from exc + if grant.kind is not AuthorizationKind.INSTALL: + raise PersonalInstallAuthorizationError("P1 requires an INSTALL grant") + if grant.status is not AuthorizationStatus.GRANTED: + raise PersonalInstallAuthorizationError("P1 INSTALL grant is not granted") + if set(grant.actions) != {PERSONAL_INSTALL_ACTION, QUALITY_UNVERIFIED_ACTION}: + raise PersonalInstallAuthorizationError( + "P1 grant must bind only personal_install and quality_unverified" + ) + if grant.scope_digest != preparation.scope_digest: + raise PersonalInstallAuthorizationError("P1 grant scope digest mismatch") + if grant.risk_digest != preparation.risk_commitment_digest: + raise PersonalInstallAuthorizationError( + "P1 grant risk digest is not the current behavior commitment" + ) + if grant.target_digest != authorization_target: + raise PersonalInstallAuthorizationError("P1 grant target digest mismatch") + if grant.is_expired(): + raise PersonalInstallAuthorizationError("P1 INSTALL grant is expired") + matching = [ + current + for current in recovery.authorization_grants + if current.authorization_id == grant.authorization_id + and current.to_dict() == grant.to_dict() + ] + if len(matching) != 1: + raise PersonalInstallAuthorizationError( + "P1 INSTALL grant is not current in the recovered workflow" + ) + if recovery.last_event_digest != authorization_event.content_digest: + raise PersonalInstallAuthorizationError("P1 authorization event head mismatch") + return _Authority( + decision_event_digest=decision.content_digest, + authorization_event_digest=authorization_event.content_digest, + workflow_event_head_digest=authorization_event.content_digest, + grant_digest=grant.content_digest, + authorization_target_digest=authorization_target, + ) + + +def _control_root(personal_root: Path) -> Path: + return personal_root / _CONTROL_DIRECTORY_NAME + + +def _workflow_log_key(workflow_id: str) -> str: + if ( + not isinstance(workflow_id, str) + or not workflow_id + or len(workflow_id.encode("utf-8")) > 1024 + or "\x00" in workflow_id + ): + raise ValueError("workflow_id must be non-empty bounded text") + return digest_json( + { + "object_version": "skill-optimizer.personal-workflow-log-key/1", + "workflow_id": workflow_id, + } + ).removeprefix("sha256:") + + +def _fixed_workflow_log_path(personal_root: Path, workflow_id: str) -> Path: + """Derive the sole accepted workflow source; never accept a caller path.""" + + return ( + _control_root(personal_root) + / "workflows" + / f"{_workflow_log_key(workflow_id)}.jsonl" + ) + + +def _ensure_private_directory(path: Path) -> None: + if os.path.lexists(path): + metadata = path.lstat() + if stat.S_ISLNK(metadata.st_mode) or not stat.S_ISDIR(metadata.st_mode): + raise PersonalInstallJournalError(f"unsafe P1 control path: {path}") + if ( + metadata.st_uid != os.getuid() + or stat.S_IMODE(metadata.st_mode) & (stat.S_IRWXG | stat.S_IRWXO) + ): + raise PersonalInstallJournalError(f"non-private P1 control path: {path}") + return + try: + os.mkdir(path, 0o700) + except FileExistsError: + _ensure_private_directory(path) + return + fsync_directory(path.parent) + + +def _ensure_control_tree(personal_root: Path) -> Path: + control = _control_root(personal_root) + _ensure_private_directory(control) + for name in ("workflows", "journals", "backups", "staging", "quarantine"): + _ensure_private_directory(control / name) + return control + + +def _existing_workflow_root(personal_root: Path) -> Path: + control = _control_root(personal_root) + workflow_root = control / "workflows" + try: + for path in (control, workflow_root): + if not os.path.lexists(path): + raise PersonalInstallJournalError( + "fixed personal workflow hierarchy is missing" + ) + _ensure_private_directory(path) + except PersonalInstallJournalError as exc: + raise PersonalInstallIntegrationError( + "trusted-workflow-event-source-adapter required: " + str(exc) + ) from exc + return workflow_root + + +def _load_fixed_workflow_events( + personal_root: Path, + workflow_id: str, +) -> tuple[WorkflowEvent, ...]: + """Load a private, fixed-path raw JSONL chain through a no-follow fd.""" + + try: + workflow_root = _existing_workflow_root(personal_root) + path = _fixed_workflow_log_path(personal_root, workflow_id) + if path.parent != workflow_root: + raise PersonalInstallJournalError("fixed workflow path escaped its root") + flags = os.O_RDONLY | getattr(os, "O_CLOEXEC", 0) + flags |= getattr(os, "O_NOFOLLOW", 0) + descriptor = os.open(path, flags) + try: + before = os.fstat(descriptor) + if ( + not stat.S_ISREG(before.st_mode) + or before.st_uid != os.getuid() + or before.st_nlink != 1 + or stat.S_IMODE(before.st_mode) & (stat.S_IRWXG | stat.S_IRWXO) + or before.st_size <= 0 + or before.st_size > _MAX_WORKFLOW_LOG_BYTES + ): + raise PersonalInstallJournalError( + "fixed workflow log is not a private bounded regular file" + ) + chunks: list[bytes] = [] + total = 0 + while True: + chunk = os.read( + descriptor, + min(64 * 1024, _MAX_WORKFLOW_LOG_BYTES + 1 - total), + ) + if not chunk: + break + chunks.append(chunk) + total += len(chunk) + if total > _MAX_WORKFLOW_LOG_BYTES: + raise PersonalInstallJournalError( + "fixed workflow log exceeds the byte limit" + ) + after = os.fstat(descriptor) + finally: + os.close(descriptor) + if ( + (before.st_dev, before.st_ino, before.st_size, before.st_mtime_ns) + != (after.st_dev, after.st_ino, after.st_size, after.st_mtime_ns) + or total != before.st_size + ): + raise PersonalInstallJournalError( + "fixed workflow log changed while being read" + ) + path_metadata = path.lstat() + if ( + stat.S_ISLNK(path_metadata.st_mode) + or path_metadata.st_nlink != 1 + or (path_metadata.st_dev, path_metadata.st_ino) + != (before.st_dev, before.st_ino) + ): + raise PersonalInstallJournalError( + "fixed workflow log pathname changed while being read" + ) + data = b"".join(chunks) + if not data.endswith(b"\n"): + raise PersonalInstallJournalError("fixed workflow log is unterminated") + lines = data.splitlines(keepends=True) + if not lines or len(lines) > _MAX_WORKFLOW_EVENTS: + raise PersonalInstallJournalError( + "fixed workflow log has an invalid event count" + ) + records: list[Mapping[str, Any]] = [] + for index, raw_line in enumerate(lines): + if ( + not raw_line.endswith(b"\n") + or len(raw_line) > _MAX_WORKFLOW_EVENT_BYTES + or not raw_line.strip() + ): + raise PersonalInstallJournalError( + f"fixed workflow event {index} violates line bounds" + ) + try: + record = json.loads(raw_line.decode("utf-8")) + except (UnicodeDecodeError, json.JSONDecodeError) as exc: + raise PersonalInstallJournalError( + f"fixed workflow event {index} is invalid JSON" + ) from exc + if not isinstance(record, Mapping): + raise PersonalInstallJournalError( + f"fixed workflow event {index} must be an object" + ) + records.append(record) + recovery = recover_events(records) + if recovery.workflow_id != workflow_id: + raise PersonalInstallJournalError( + "fixed workflow log does not match its workflow ID" + ) + return recovery.events + except PersonalInstallIntegrationError: + raise + except (OSError, TypeError, ValueError, RuntimeError) as exc: + raise PersonalInstallIntegrationError( + "trusted-workflow-event-source-adapter required: " + str(exc) + ) from exc + + +def _existing_control_tree(personal_root: Path) -> Path: + control = _control_root(personal_root) + for path in ( + control, + control / "workflows", + control / "journals", + control / "backups", + control / "staging", + control / "quarantine", + ): + if not os.path.lexists(path): + raise PersonalInstallJournalError("P1 control tree is incomplete") + _ensure_private_directory(path) + return control + + +@contextmanager +def _personal_root_lock( + personal_root: Path, + *, + create: bool = True, +) -> Iterator[Path]: + control = ( + _ensure_control_tree(personal_root) + if create + else _existing_control_tree(personal_root) + ) + lock_path = control / "writer.lock" + flags = os.O_RDWR | getattr(os, "O_CLOEXEC", 0) + if create: + flags |= os.O_CREAT + flags |= getattr(os, "O_NOFOLLOW", 0) + try: + descriptor = os.open(lock_path, flags, 0o600) + except FileNotFoundError as exc: + raise PersonalInstallJournalError("P1 writer lock is unavailable") from exc + try: + metadata = os.fstat(descriptor) + if ( + not stat.S_ISREG(metadata.st_mode) + or metadata.st_uid != os.getuid() + or metadata.st_nlink != 1 + or stat.S_IMODE(metadata.st_mode) & (stat.S_IRWXG | stat.S_IRWXO) + ): + raise PersonalInstallJournalError("P1 writer lock is not private") + fcntl.flock(descriptor, fcntl.LOCK_EX) + yield control + finally: + try: + fcntl.flock(descriptor, fcntl.LOCK_UN) + finally: + os.close(descriptor) + + +def _write_all(descriptor: int, data: bytes) -> None: + remaining = memoryview(data) + while remaining: + written = os.write(descriptor, remaining) + if written <= 0: + raise PersonalInstallJournalError("short P1 journal write") + remaining = remaining[written:] + + +def _journal_body(payload: Mapping[str, Any]) -> dict[str, Any]: + return {key: value for key, value in payload.items() if key != "journal_digest"} + + +def _with_journal_digest(payload: Mapping[str, Any]) -> dict[str, Any]: + body = _journal_body(payload) + return {**body, "journal_digest": digest_json(body)} + + +def _journal_bytes(payload: Mapping[str, Any]) -> bytes: + return (canonical_json(payload) + "\n").encode("utf-8") + + +def _create_journal(path: Path, payload: Mapping[str, Any]) -> dict[str, Any]: + complete = _with_journal_digest(payload) + data = _journal_bytes(complete) + flags = os.O_WRONLY | os.O_CREAT | os.O_EXCL | getattr(os, "O_CLOEXEC", 0) + flags |= getattr(os, "O_NOFOLLOW", 0) + descriptor = os.open(path, flags, 0o600) + try: + _write_all(descriptor, data) + os.fsync(descriptor) + finally: + os.close(descriptor) + fsync_directory(path.parent) + return complete + + +def _replace_journal(path: Path, payload: Mapping[str, Any]) -> dict[str, Any]: + complete = _with_journal_digest(payload) + data = _journal_bytes(complete) + temporary = path.parent / f".{path.name}.{secrets.token_hex(16)}.tmp" + flags = os.O_WRONLY | os.O_CREAT | os.O_EXCL | getattr(os, "O_CLOEXEC", 0) + flags |= getattr(os, "O_NOFOLLOW", 0) + descriptor = os.open(temporary, flags, 0o600) + try: + _write_all(descriptor, data) + os.fsync(descriptor) + finally: + os.close(descriptor) + try: + os.replace(temporary, path) + fsync_directory(path.parent) + except BaseException: + if os.path.lexists(temporary): + temporary.unlink() + raise + return complete + + +_JOURNAL_KEYS = frozenset( + { + "object_version", + "transaction_id", + "state", + "personal_root", + "root_identity_digest", + "target", + "candidate_digest", + "staged_digest", + "behavior_report_digest", + "minimum_risk", + "scope_digest", + "risk_commitment_digest", + "target_observation_digest", + "authorization_target_digest", + "decision_event_digest", + "authorization_event_digest", + "workflow_event_head_digest", + "authorization_grant_digest", + "expected_target_digest", + "pre_target_digest", + "installed_digest", + "backup_relative_path", + "backup_digest", + "quarantine_relative_path", + "quarantine_digest", + "restored_digest", + "quality_claim_status", + "limitations", + "journal_digest", + } +) +_JOURNAL_STATES = frozenset( + { + "prepared", + "backup_moved", + "installed", + "rollback_started", + "rolled_back", + "failed_restored", + "failed_ambiguous", + } +) + + +def _validate_journal(payload: Mapping[str, Any]) -> dict[str, Any]: + if set(payload) != _JOURNAL_KEYS: + raise PersonalInstallJournalError("P1 journal fields do not match the closed record") + normalized = dict(payload) + if normalized["object_version"] != PERSONAL_INSTALL_JOURNAL_OBJECT_VERSION: + raise PersonalInstallJournalError("unsupported P1 journal object_version") + _require_transaction_id(normalized["transaction_id"]) + if normalized["state"] not in _JOURNAL_STATES: + raise PersonalInstallJournalError("invalid P1 journal transaction state") + for field_name in ( + "root_identity_digest", + "candidate_digest", + "staged_digest", + "behavior_report_digest", + "scope_digest", + "risk_commitment_digest", + "target_observation_digest", + "authorization_target_digest", + "decision_event_digest", + "authorization_event_digest", + "workflow_event_head_digest", + "authorization_grant_digest", + ): + _require_digest(normalized[field_name], field_name) + for field_name in ( + "expected_target_digest", + "pre_target_digest", + "installed_digest", + "backup_digest", + "quarantine_digest", + "restored_digest", + ): + _require_digest(normalized[field_name], field_name, nullable=True) + if normalized["expected_target_digest"] != normalized["pre_target_digest"]: + raise PersonalInstallJournalError("P1 journal pre/CAS digest relation mismatch") + if normalized["candidate_digest"] != normalized["staged_digest"]: + raise PersonalInstallJournalError("P1 journal candidate/staged relation mismatch") + if normalized["installed_digest"] is not None and ( + normalized["installed_digest"] != normalized["candidate_digest"] + ): + raise PersonalInstallJournalError("P1 journal installed/candidate relation mismatch") + if normalized["pre_target_digest"] is None: + if ( + normalized["backup_relative_path"] is not None + or normalized["backup_digest"] is not None + ): + raise PersonalInstallJournalError("new-target P1 journal cannot claim a backup") + elif normalized["backup_digest"] not in {None, normalized["pre_target_digest"]}: + raise PersonalInstallJournalError("P1 journal backup/pre-target relation mismatch") + if normalized["quality_claim_status"] != "unverified": + raise PersonalInstallJournalError("P1 journal cannot claim verified quality") + if normalized["limitations"] != list(_P1_LIMITATIONS): + raise PersonalInstallJournalError("P1 journal limitations were changed") + if normalized["workflow_event_head_digest"] != normalized["authorization_event_digest"]: + raise PersonalInstallJournalError( + "P1 journal event-head/authorization relation mismatch" + ) + if normalized["minimum_risk"] not in { + RiskLevel.R0.value, + RiskLevel.R1.value, + }: + raise PersonalInstallJournalError("P1 journal contains non-P1 risk") + if normalized["state"] in { + "installed", + "rollback_started", + "rolled_back", + }: + if normalized["installed_digest"] != normalized["candidate_digest"]: + raise PersonalInstallJournalError( + "installed P1 journal lacks the exact candidate digest" + ) + if normalized["pre_target_digest"] is not None and ( + normalized["backup_digest"] != normalized["pre_target_digest"] + ): + raise PersonalInstallJournalError( + "installed P1 journal lacks the exact backup digest" + ) + if normalized["state"] == "rolled_back": + if ( + normalized["quarantine_digest"] != normalized["installed_digest"] + or normalized["restored_digest"] != normalized["pre_target_digest"] + ): + raise PersonalInstallJournalError( + "terminal P1 rollback relations are invalid" + ) + expected_digest = digest_json(_journal_body(normalized)) + if normalized["journal_digest"] != expected_digest: + raise PersonalInstallJournalError("P1 journal digest mismatch") + for field_name in ("personal_root", "target"): + if not isinstance(normalized[field_name], str) or not normalized[field_name]: + raise PersonalInstallJournalError(f"P1 journal {field_name} is invalid") + for field_name in ("backup_relative_path", "quarantine_relative_path"): + value = normalized[field_name] + if value is not None and (not isinstance(value, str) or not value): + raise PersonalInstallJournalError(f"P1 journal {field_name} is invalid") + return normalized + + +def _load_journal(path: Path) -> dict[str, Any]: + flags = os.O_RDONLY | getattr(os, "O_CLOEXEC", 0) | getattr(os, "O_NOFOLLOW", 0) + try: + descriptor = os.open(path, flags) + except (FileNotFoundError, OSError) as exc: + raise PersonalInstallJournalError("P1 transaction journal is unavailable") from exc + try: + metadata = os.fstat(descriptor) + if ( + not stat.S_ISREG(metadata.st_mode) + or metadata.st_uid != os.getuid() + or metadata.st_nlink != 1 + or stat.S_IMODE(metadata.st_mode) & (stat.S_IRWXG | stat.S_IRWXO) + or metadata.st_size > _MAX_JOURNAL_BYTES + ): + raise PersonalInstallJournalError("P1 journal file is unsafe") + data = b"" + while len(data) <= _MAX_JOURNAL_BYTES: + chunk = os.read(descriptor, min(64 * 1024, _MAX_JOURNAL_BYTES + 1 - len(data))) + if not chunk: + break + data += chunk + if len(data) > _MAX_JOURNAL_BYTES: + raise PersonalInstallJournalError("P1 journal exceeds the size limit") + finally: + os.close(descriptor) + if not data.endswith(b"\n"): + raise PersonalInstallJournalError("P1 journal is unterminated") + try: + value = json.loads(data) + except json.JSONDecodeError as exc: + raise PersonalInstallJournalError("P1 journal is not valid JSON") from exc + if not isinstance(value, Mapping): + raise PersonalInstallJournalError("P1 journal must be an object") + return _validate_journal(value) + + +def _atomic_rename_noreplace(source: Path, destination: Path) -> None: + libc = ctypes.CDLL(None, use_errno=True) + source_bytes = os.fsencode(source) + destination_bytes = os.fsencode(destination) + ctypes.set_errno(0) + try: + if sys.platform == "darwin": + rename = libc.renamex_np + rename.argtypes = [ctypes.c_char_p, ctypes.c_char_p, ctypes.c_uint] + rename.restype = ctypes.c_int + result = rename(source_bytes, destination_bytes, _DARWIN_RENAME_EXCL) + elif sys.platform.startswith("linux"): + rename = libc.renameat2 + rename.argtypes = [ + ctypes.c_int, + ctypes.c_char_p, + ctypes.c_int, + ctypes.c_char_p, + ctypes.c_uint, + ] + rename.restype = ctypes.c_int + result = rename( + getattr(os, "AT_FDCWD", _LINUX_AT_FDCWD), + source_bytes, + getattr(os, "AT_FDCWD", _LINUX_AT_FDCWD), + destination_bytes, + _LINUX_RENAME_NOREPLACE, + ) + else: + raise PersonalInstallTargetError( + "platform lacks atomic no-replace rename; P3 is required" + ) + except AttributeError as exc: + raise PersonalInstallTargetError( + "platform lacks atomic no-replace rename; P3 is required" + ) from exc + if result == 0: + return + error = ctypes.get_errno() + if error in {errno.EEXIST, errno.ENOTEMPTY}: + raise FileExistsError(error, os.strerror(error), str(destination)) + if error in { + errno.ENOSYS, + getattr(errno, "ENOTSUP", errno.EOPNOTSUPP), + errno.EOPNOTSUPP, + }: + raise PersonalInstallTargetError( + "filesystem lacks atomic no-replace rename; P3 is required" + ) from OSError(error, os.strerror(error), str(destination)) + raise OSError(error, os.strerror(error), str(destination)) + + +def _write_private_file(source: Path, destination: Path) -> None: + source_flags = os.O_RDONLY | getattr(os, "O_CLOEXEC", 0) + source_flags |= getattr(os, "O_NOFOLLOW", 0) + source_descriptor = os.open(source, source_flags) + try: + source_metadata = os.fstat(source_descriptor) + if not stat.S_ISREG(source_metadata.st_mode): + raise PersonalInstallTargetError("candidate contains a non-regular file") + mode = 0o700 if source_metadata.st_mode & 0o111 else 0o600 + destination_flags = os.O_WRONLY | os.O_CREAT | os.O_EXCL + destination_flags |= getattr(os, "O_CLOEXEC", 0) + destination_flags |= getattr(os, "O_NOFOLLOW", 0) + destination_descriptor = os.open(destination, destination_flags, mode) + try: + while True: + chunk = os.read(source_descriptor, 1024 * 1024) + if not chunk: + break + _write_all(destination_descriptor, chunk) + os.fsync(destination_descriptor) + finally: + os.close(destination_descriptor) + finally: + os.close(source_descriptor) + + +def _copy_private_tree(source: Path, destination: Path) -> None: + source_metadata = source.lstat() + if stat.S_ISLNK(source_metadata.st_mode) or not stat.S_ISDIR(source_metadata.st_mode): + raise PersonalInstallTargetError("candidate root must remain a real directory") + os.mkdir(destination, 0o700) + for entry in sorted(os.scandir(source), key=lambda item: item.name): + source_child = Path(entry.path) + destination_child = destination / entry.name + if entry.is_symlink(): + raise PersonalInstallTargetError("candidate contains a symbolic link") + if entry.is_dir(follow_symlinks=False): + _copy_private_tree(source_child, destination_child) + elif entry.is_file(follow_symlinks=False): + _write_private_file(source_child, destination_child) + else: + raise PersonalInstallTargetError("candidate contains a special file") + fsync_directory(destination) + + +def _new_transaction_paths(control: Path, transaction_id: str) -> dict[str, Path]: + paths = { + "journal": control / "journals" / f"{transaction_id}.json", + "backup_root": control / "backups" / transaction_id, + "staging_root": control / "staging" / transaction_id, + "quarantine_root": control / "quarantine" / transaction_id, + } + for key in ("backup_root", "staging_root"): + os.mkdir(paths[key], 0o700) + fsync_directory(paths[key].parent) + return { + **paths, + "backup": paths["backup_root"] / "payload", + "staging": paths["staging_root"] / "payload", + "quarantine": paths["quarantine_root"] / "payload", + } + + +def _relative_control_path(control: Path, path: Path) -> str: + return path.relative_to(control).as_posix() + + +def _validate_receipt_target_paths(personal_root: object, target: object) -> None: + if not isinstance(personal_root, str) or not isinstance(target, str): + raise ValueError("personal receipt root and target must be strings") + root_path = Path(personal_root) + target_path = Path(target) + if ( + not root_path.is_absolute() + or not target_path.is_absolute() + or target_path.parent != root_path + or target_path != root_path / target_path.name + or _TARGET_NAME_RE.fullmatch(target_path.name) is None + or target_path.name == _CONTROL_DIRECTORY_NAME + ): + raise ValueError("personal receipt target/root relation is invalid") + + +def _base_journal( + preparation: _Preparation, + authority: _Authority, + *, + transaction_id: str, + staged_digest: str, + control: Path, + backup: Path, +) -> dict[str, Any]: + pre_digest = preparation.expected_target_digest + return { + "object_version": PERSONAL_INSTALL_JOURNAL_OBJECT_VERSION, + "transaction_id": transaction_id, + "state": "prepared", + "personal_root": str(preparation.personal_root), + "root_identity_digest": preparation.observation.root_identity_digest, + "target": str(preparation.target), + "candidate_digest": preparation.report.candidate_digest, + "staged_digest": staged_digest, + "behavior_report_digest": preparation.report.content_digest, + "minimum_risk": preparation.report.minimum_risk.value, + "scope_digest": preparation.scope_digest, + "risk_commitment_digest": preparation.risk_commitment_digest, + "target_observation_digest": preparation.observation.content_digest, + "authorization_target_digest": authority.authorization_target_digest, + "decision_event_digest": authority.decision_event_digest, + "authorization_event_digest": authority.authorization_event_digest, + "workflow_event_head_digest": authority.workflow_event_head_digest, + "authorization_grant_digest": authority.grant_digest, + "expected_target_digest": pre_digest, + "pre_target_digest": pre_digest, + "installed_digest": None, + "backup_relative_path": ( + _relative_control_path(control, backup) if pre_digest is not None else None + ), + "backup_digest": None, + "quarantine_relative_path": None, + "quarantine_digest": None, + "restored_digest": None, + "quality_claim_status": "unverified", + "limitations": list(_P1_LIMITATIONS), + } + + +@dataclass(frozen=True) +class PersonalInstallReceipt: + transaction_id: str + status: str + personal_root: str + target: str + candidate_digest: str + staged_digest: str + behavior_report_digest: str + minimum_risk: str + scope_digest: str + risk_commitment_digest: str + target_observation_digest: str + authorization_target_digest: str + decision_event_digest: str + authorization_event_digest: str + workflow_event_head_digest: str + authorization_grant_digest: str + expected_target_digest: str | None + pre_target_digest: str | None + post_install_digest: str + backup_digest: str | None + backup_present: bool + journal_digest: str + quality_claim_status: str = "unverified" + limitations: tuple[str, ...] = _P1_LIMITATIONS + object_version: str = PERSONAL_INSTALL_RECEIPT_OBJECT_VERSION + + def __post_init__(self) -> None: + if self.object_version != PERSONAL_INSTALL_RECEIPT_OBJECT_VERSION: + raise ValueError("unsupported personal install receipt object_version") + _require_transaction_id(self.transaction_id) + if self.status != "installed": + raise ValueError("personal install receipt status must be installed") + for field_name in ( + "candidate_digest", + "staged_digest", + "behavior_report_digest", + "scope_digest", + "risk_commitment_digest", + "target_observation_digest", + "authorization_target_digest", + "decision_event_digest", + "authorization_event_digest", + "workflow_event_head_digest", + "authorization_grant_digest", + "post_install_digest", + "journal_digest", + ): + _require_digest(getattr(self, field_name), field_name) + for field_name in ( + "expected_target_digest", + "pre_target_digest", + "backup_digest", + ): + _require_digest(getattr(self, field_name), field_name, nullable=True) + if self.minimum_risk not in {RiskLevel.R0.value, RiskLevel.R1.value}: + raise ValueError("personal install receipt risk is not P1-compatible") + if self.expected_target_digest != self.pre_target_digest: + raise ValueError("personal install receipt pre/CAS relation mismatch") + if not ( + self.candidate_digest + == self.staged_digest + == self.post_install_digest + ): + raise ValueError( + "personal install receipt candidate/staged/post relation mismatch" + ) + if self.workflow_event_head_digest != self.authorization_event_digest: + raise ValueError( + "personal install receipt event-head/authorization relation mismatch" + ) + if not isinstance(self.backup_present, bool): + raise ValueError("personal install receipt backup_present must be boolean") + expected_backup = self.pre_target_digest is not None + if self.backup_present is not expected_backup: + raise ValueError("personal install receipt backup presence mismatch") + if expected_backup and self.backup_digest != self.pre_target_digest: + raise ValueError("personal install receipt backup/pre relation mismatch") + if not expected_backup and self.backup_digest is not None: + raise ValueError("new-target personal install cannot claim a backup") + if self.quality_claim_status != "unverified": + raise ValueError("personal install receipt cannot claim verified quality") + if tuple(self.limitations) != _P1_LIMITATIONS: + raise ValueError("personal install receipt limitations were changed") + _validate_receipt_target_paths(self.personal_root, self.target) + + def body(self) -> dict[str, Any]: + return { + "object_version": self.object_version, + "receipt_kind": "install", + "transaction_id": self.transaction_id, + "status": self.status, + "personal_root": self.personal_root, + "target": self.target, + "candidate_digest": self.candidate_digest, + "staged_digest": self.staged_digest, + "behavior_report_digest": self.behavior_report_digest, + "minimum_risk": self.minimum_risk, + "scope_digest": self.scope_digest, + "risk_commitment_digest": self.risk_commitment_digest, + "target_observation_digest": self.target_observation_digest, + "authorization_target_digest": self.authorization_target_digest, + "decision_event_digest": self.decision_event_digest, + "authorization_event_digest": self.authorization_event_digest, + "workflow_event_head_digest": self.workflow_event_head_digest, + "authorization_grant_digest": self.authorization_grant_digest, + "expected_target_digest": self.expected_target_digest, + "pre_target_digest": self.pre_target_digest, + "post_install_digest": self.post_install_digest, + "backup_digest": self.backup_digest, + "backup_present": self.backup_present, + "journal_digest": self.journal_digest, + "quality_claim_status": self.quality_claim_status, + "limitations": list(self.limitations), + } + + @property + def receipt_digest(self) -> str: + return digest_json(self.body()) + + def to_dict(self) -> dict[str, Any]: + return {**self.body(), "receipt_digest": self.receipt_digest} + + @classmethod + def from_dict(cls, value: Mapping[str, Any]) -> "PersonalInstallReceipt": + expected = { + "object_version", + "receipt_kind", + "transaction_id", + "status", + "personal_root", + "target", + "candidate_digest", + "staged_digest", + "behavior_report_digest", + "minimum_risk", + "scope_digest", + "risk_commitment_digest", + "target_observation_digest", + "authorization_target_digest", + "decision_event_digest", + "authorization_event_digest", + "workflow_event_head_digest", + "authorization_grant_digest", + "expected_target_digest", + "pre_target_digest", + "post_install_digest", + "backup_digest", + "backup_present", + "journal_digest", + "quality_claim_status", + "limitations", + "receipt_digest", + } + if not isinstance(value, Mapping) or set(value) != expected: + raise ValueError("personal install receipt fields do not match the closed schema") + if value["receipt_kind"] != "install": + raise ValueError("personal install receipt kind mismatch") + if not isinstance(value["limitations"], list): + raise ValueError("personal install receipt limitations must be an array") + receipt = cls( + transaction_id=value["transaction_id"], + status=value["status"], + personal_root=value["personal_root"], + target=value["target"], + candidate_digest=value["candidate_digest"], + staged_digest=value["staged_digest"], + behavior_report_digest=value["behavior_report_digest"], + minimum_risk=value["minimum_risk"], + scope_digest=value["scope_digest"], + risk_commitment_digest=value["risk_commitment_digest"], + target_observation_digest=value["target_observation_digest"], + authorization_target_digest=value["authorization_target_digest"], + decision_event_digest=value["decision_event_digest"], + authorization_event_digest=value["authorization_event_digest"], + workflow_event_head_digest=value["workflow_event_head_digest"], + authorization_grant_digest=value["authorization_grant_digest"], + expected_target_digest=value["expected_target_digest"], + pre_target_digest=value["pre_target_digest"], + post_install_digest=value["post_install_digest"], + backup_digest=value["backup_digest"], + backup_present=value["backup_present"], + journal_digest=value["journal_digest"], + quality_claim_status=value["quality_claim_status"], + limitations=tuple(value["limitations"]), + object_version=value["object_version"], + ) + _require_digest(value["receipt_digest"], "receipt_digest") + if value["receipt_digest"] != receipt.receipt_digest: + raise ValueError("personal install receipt digest mismatch") + return receipt + + +@dataclass(frozen=True) +class PersonalRollbackReceipt: + transaction_id: str + status: str + personal_root: str + target: str + installed_digest: str + quarantine_digest: str + restored_digest: str | None + journal_digest: str + quality_claim_status: str = "unverified" + limitations: tuple[str, ...] = _P1_LIMITATIONS + object_version: str = PERSONAL_ROLLBACK_RECEIPT_OBJECT_VERSION + + def __post_init__(self) -> None: + if self.object_version != PERSONAL_ROLLBACK_RECEIPT_OBJECT_VERSION: + raise ValueError("unsupported personal rollback receipt object_version") + _require_transaction_id(self.transaction_id) + if self.status != "rolled_back": + raise ValueError("personal rollback receipt status must be rolled_back") + for field_name in ( + "installed_digest", + "quarantine_digest", + "journal_digest", + ): + _require_digest(getattr(self, field_name), field_name) + _require_digest(self.restored_digest, "restored_digest", nullable=True) + if self.installed_digest != self.quarantine_digest: + raise ValueError( + "personal rollback receipt installed/quarantine relation mismatch" + ) + if self.quality_claim_status != "unverified": + raise ValueError("personal rollback receipt cannot claim verified quality") + if tuple(self.limitations) != _P1_LIMITATIONS: + raise ValueError("personal rollback receipt limitations were changed") + _validate_receipt_target_paths(self.personal_root, self.target) + + def body(self) -> dict[str, Any]: + return { + "object_version": self.object_version, + "receipt_kind": "rollback", + "transaction_id": self.transaction_id, + "status": self.status, + "personal_root": self.personal_root, + "target": self.target, + "installed_digest": self.installed_digest, + "quarantine_digest": self.quarantine_digest, + "restored_digest": self.restored_digest, + "journal_digest": self.journal_digest, + "quality_claim_status": self.quality_claim_status, + "limitations": list(self.limitations), + } + + @property + def receipt_digest(self) -> str: + return digest_json(self.body()) + + def to_dict(self) -> dict[str, Any]: + return {**self.body(), "receipt_digest": self.receipt_digest} + + @classmethod + def from_dict(cls, value: Mapping[str, Any]) -> "PersonalRollbackReceipt": + expected = { + "object_version", + "receipt_kind", + "transaction_id", + "status", + "personal_root", + "target", + "installed_digest", + "quarantine_digest", + "restored_digest", + "journal_digest", + "quality_claim_status", + "limitations", + "receipt_digest", + } + if not isinstance(value, Mapping) or set(value) != expected: + raise ValueError("personal rollback receipt fields do not match the closed schema") + if value["receipt_kind"] != "rollback": + raise ValueError("personal rollback receipt kind mismatch") + if not isinstance(value["limitations"], list): + raise ValueError("personal rollback receipt limitations must be an array") + receipt = cls( + transaction_id=value["transaction_id"], + status=value["status"], + personal_root=value["personal_root"], + target=value["target"], + installed_digest=value["installed_digest"], + quarantine_digest=value["quarantine_digest"], + restored_digest=value["restored_digest"], + journal_digest=value["journal_digest"], + quality_claim_status=value["quality_claim_status"], + limitations=tuple(value["limitations"]), + object_version=value["object_version"], + ) + _require_digest(value["receipt_digest"], "receipt_digest") + if value["receipt_digest"] != receipt.receipt_digest: + raise ValueError("personal rollback receipt digest mismatch") + return receipt + + +def _receipt_from_journal(journal: Mapping[str, Any]) -> PersonalInstallReceipt: + if journal["state"] != "installed" or journal["installed_digest"] is None: + raise PersonalInstallJournalError("P1 success receipt requires installed state") + return PersonalInstallReceipt( + transaction_id=journal["transaction_id"], + status="installed", + personal_root=journal["personal_root"], + target=journal["target"], + candidate_digest=journal["candidate_digest"], + staged_digest=journal["staged_digest"], + behavior_report_digest=journal["behavior_report_digest"], + minimum_risk=journal["minimum_risk"], + scope_digest=journal["scope_digest"], + risk_commitment_digest=journal["risk_commitment_digest"], + target_observation_digest=journal["target_observation_digest"], + authorization_target_digest=journal["authorization_target_digest"], + decision_event_digest=journal["decision_event_digest"], + authorization_event_digest=journal["authorization_event_digest"], + workflow_event_head_digest=journal["workflow_event_head_digest"], + authorization_grant_digest=journal["authorization_grant_digest"], + expected_target_digest=journal["expected_target_digest"], + pre_target_digest=journal["pre_target_digest"], + post_install_digest=journal["installed_digest"], + backup_digest=journal["backup_digest"], + backup_present=journal["backup_digest"] is not None, + journal_digest=journal["journal_digest"], + ) + + +def _same_preparation(left: _Preparation, right: _Preparation) -> bool: + return ( + left.candidate == right.candidate + and left.target == right.target + and left.personal_root == right.personal_root + and left.expected_target_digest == right.expected_target_digest + and left.scope_digest == right.scope_digest + and left.report.to_dict() == right.report.to_dict() + and left.risk_commitment_digest == right.risk_commitment_digest + and left.observation.to_dict() == right.observation.to_dict() + and dict(left.decision_payload) == dict(right.decision_payload) + ) + + +def _reports_match(left: BehaviorRiskReport, right: BehaviorRiskReport) -> bool: + return left.to_dict() == right.to_dict() + + +def _restore_failed_install( + *, + preparation: _Preparation, + journal_path: Path, + journal: dict[str, Any], + backup: Path, + quarantine: Path, + installed: bool, + backup_moved: bool, +) -> None: + target = preparation.target + if installed: + current = optional_path_digest(target) + if current != preparation.report.candidate_digest: + raise PersonalInstallAmbiguousState( + "installed target drifted before failure restoration" + ) + if os.path.lexists(quarantine.parent): + raise PersonalInstallAmbiguousState( + "transaction quarantine path unexpectedly exists" + ) + os.mkdir(quarantine.parent, 0o700) + fsync_directory(quarantine.parent.parent) + _atomic_rename_noreplace(target, quarantine) + journal["quarantine_relative_path"] = _relative_control_path( + _control_root(preparation.personal_root), quarantine + ) + journal["quarantine_digest"] = path_digest(quarantine) + if backup_moved: + if os.path.lexists(target): + raise PersonalInstallAmbiguousState( + "target reappeared before failure restoration" + ) + control = _control_root(preparation.personal_root) + transaction_id = journal["transaction_id"] + try: + _validate_private_transaction_payload( + control=control, + transaction_id=transaction_id, + kind="backups", + payload=backup, + ) + except PersonalInstallError as exc: + raise PersonalInstallAmbiguousState( + "backup metadata/path is unsafe for failure restoration" + ) from exc + if ( + journal["backup_relative_path"] + != _relative_control_path(control, backup) + or journal["backup_digest"] != preparation.expected_target_digest + ): + raise PersonalInstallAmbiguousState( + "backup journal relationship changed before failure restoration" + ) + if path_digest(backup) != preparation.expected_target_digest: + raise PersonalInstallAmbiguousState("backup changed before failure restoration") + _atomic_rename_noreplace(backup, target) + restored = optional_path_digest(target) + if restored != preparation.expected_target_digest: + raise PersonalInstallAmbiguousState( + "failed P1 transaction did not restore the exact pre-install state" + ) + journal["state"] = "failed_restored" + journal["restored_digest"] = restored + _replace_journal(journal_path, journal) + + +def personal_install( + *, + candidate: str | os.PathLike[str], + target: str | os.PathLike[str], + expected_target_digest: str | None, + scope_digest: str, + workflow_id: str, +) -> PersonalInstallReceipt: + """Install one current-byte candidate under narrow P1 constraints. + + Authority is reconstructed exclusively from the fixed private workflow + JSONL derived from the recognized personal root and ``workflow_id``. No + caller-provided event array/path/handle, report, validator, authority + receipt, target observation, install-grant object, journal root, or + eligibility boolean is accepted. + """ + + preparation = _prepare( + candidate=candidate, + target=target, + expected_target_digest=expected_target_digest, + scope_digest=scope_digest, + workflow_id=workflow_id, + ) + records = _load_fixed_workflow_events(preparation.personal_root, workflow_id) + authority = _verify_install_authority( + preparation, + workflow_id=workflow_id, + workflow_events=records, + ) + with _personal_root_lock(preparation.personal_root) as control: + locked = _prepare( + candidate=candidate, + target=target, + expected_target_digest=expected_target_digest, + scope_digest=scope_digest, + workflow_id=workflow_id, + ) + if not _same_preparation(preparation, locked): + raise TargetChangedError( + "candidate, behavior, or target facts changed before P1 lock acquisition" + ) + locked_records = _load_fixed_workflow_events( + locked.personal_root, + workflow_id, + ) + locked_authority = _verify_install_authority( + locked, + workflow_id=workflow_id, + workflow_events=locked_records, + ) + if locked_authority != authority: + raise PersonalInstallAuthorizationError( + "P1 workflow authority changed before mutation" + ) + + transaction_id = f"pi-{secrets.token_hex(32)}" + paths = _new_transaction_paths(control, transaction_id) + _copy_private_tree(locked.candidate, paths["staging"]) + staged_digest = path_digest(paths["staging"]) + if ( + staged_digest != locked.report.candidate_digest + or path_digest(locked.candidate) != locked.report.candidate_digest + ): + raise CandidateChangedError("candidate changed while staging P1 install") + staged_report = _p1_behavior_report(paths["staging"]) + if not _reports_match(locked.report, staged_report): + raise CandidateChangedError( + "staged candidate behavior differs from the authorized audit" + ) + + journal = _base_journal( + locked, + authority, + transaction_id=transaction_id, + staged_digest=staged_digest, + control=control, + backup=paths["backup"], + ) + journal = _create_journal(paths["journal"], journal) + installed = False + backup_moved = False + try: + observed_target = optional_path_digest(locked.target) + if observed_target != locked.expected_target_digest: + raise TargetChangedError("P1 target changed before backup CAS") + if observed_target is not None: + _atomic_rename_noreplace(locked.target, paths["backup"]) + backup_moved = True + backup_digest = path_digest(paths["backup"]) + if backup_digest != observed_target: + raise PersonalInstallAmbiguousState( + "P1 backup does not match the pre-install target" + ) + journal["state"] = "backup_moved" + journal["backup_digest"] = backup_digest + journal = _replace_journal(paths["journal"], journal) + if os.path.lexists(locked.target): + raise TargetChangedError("P1 target appeared before install rename") + _atomic_rename_noreplace(paths["staging"], locked.target) + installed = True + post_digest = path_digest(locked.target) + if post_digest != locked.report.candidate_digest: + raise CandidateChangedError("installed P1 bytes differ from the candidate") + installed_report = _p1_behavior_report(locked.target) + if not _reports_match(locked.report, installed_report): + raise CandidateChangedError( + "installed candidate behavior differs from the authorized audit" + ) + journal["state"] = "installed" + journal["installed_digest"] = post_digest + journal = _replace_journal(paths["journal"], journal) + return _receipt_from_journal(journal) + except Exception as original: + try: + _restore_failed_install( + preparation=locked, + journal_path=paths["journal"], + journal=journal, + backup=paths["backup"], + quarantine=paths["quarantine"], + installed=installed, + backup_moved=backup_moved, + ) + except Exception as restore_error: + try: + journal["state"] = "failed_ambiguous" + _replace_journal(paths["journal"], journal) + except Exception: + pass + raise PersonalInstallAmbiguousState( + f"P1 install failed and exact restoration is unproven: {restore_error}" + ) from original + if isinstance(original, PersonalInstallError): + raise + raise PersonalInstallError(f"P1 install failed: {original}") from original + + +def _find_transaction_journal(transaction_id: str) -> tuple[Path, Path, dict[str, Any]]: + matches: list[tuple[Path, Path]] = [] + for raw_root in _configured_personal_roots(): + root_input = Path(raw_root).expanduser() + if not os.path.lexists(root_input): + continue + try: + _require_real_owned_directory(root_input, "personal Skill root") + root = root_input.resolve(strict=True) + except PersonalInstallTargetError: + continue + journal_path = _control_root(root) / "journals" / f"{transaction_id}.json" + if os.path.lexists(journal_path): + matches.append((root, journal_path)) + if len(matches) != 1: + raise PersonalInstallJournalError( + "P1 transaction ID does not resolve to exactly one library journal" + ) + root, journal_path = matches[0] + journal = _load_journal(journal_path) + if journal["transaction_id"] != transaction_id: + raise PersonalInstallJournalError("P1 journal transaction ID mismatch") + if journal["personal_root"] != str(root): + raise PersonalInstallJournalError("P1 journal personal-root mismatch") + return root, journal_path, journal + + +def _exact_journal_payload_path( + control: Path, + transaction_id: str, + kind: str, +) -> Path: + if kind not in {"backups", "quarantine"}: + raise ValueError("unsupported P1 transaction payload kind") + return control / kind / transaction_id / "payload" + + +def _validate_private_transaction_payload( + *, + control: Path, + transaction_id: str, + kind: str, + payload: Path, +) -> Path: + """Rebuild and validate one transaction-owned private payload path.""" + + _require_transaction_id(transaction_id) + expected = _exact_journal_payload_path(control, transaction_id, kind) + if ( + payload != expected + or expected.parent.parent != control / kind + or expected.parent != control / kind / transaction_id + ): + raise PersonalInstallJournalError( + f"P1 {kind} payload escaped its fixed transaction root" + ) + for directory in (control, control / kind, expected.parent): + _ensure_private_directory(directory) + if not os.path.lexists(expected): + raise PersonalInstallJournalError(f"P1 {kind} payload is unavailable") + _safe_target_tree(expected) + return expected + + +def rollback_personal_install(transaction_id: str) -> PersonalRollbackReceipt: + """Rollback one P1 transaction by opaque ID, never by caller receipt data.""" + + normalized_id = _require_transaction_id(transaction_id) + root, journal_path, initial = _find_transaction_journal(normalized_id) + with _personal_root_lock(root, create=False) as control: + journal = _load_journal(journal_path) + if journal != initial: + raise PersonalInstallJournalError( + "P1 journal changed before rollback lock acquisition" + ) + if journal["state"] != "installed": + raise PersonalInstallReplayError( + f"P1 transaction state {journal['state']!r} cannot be rolled back" + ) + target = Path(journal["target"]) + if target.parent != root or target != root / target.name: + raise PersonalInstallJournalError("P1 journal target/root relation mismatch") + observation = observe_personal_target(target) + if observation.personal_root != str(root): + raise PersonalInstallJournalError("P1 rollback target root changed") + if observation.root_identity_digest != journal["root_identity_digest"]: + raise PersonalInstallJournalError("P1 personal-root identity changed") + installed_digest = journal["installed_digest"] + if installed_digest is None or observation.target_digest != installed_digest: + raise PersonalInstallRollbackError( + "current personal target does not match the installed transaction bytes" + ) + + backup = _exact_journal_payload_path( + control, normalized_id, "backups" + ) + expected_backup_relative = _relative_control_path(control, backup) + pre_digest = journal["pre_target_digest"] + if pre_digest is None: + if ( + journal["backup_relative_path"] is not None + or journal["backup_digest"] is not None + ): + raise PersonalInstallJournalError( + "new-target transaction unexpectedly claims a backup" + ) + if os.path.lexists(backup): + raise PersonalInstallJournalError( + "new-target transaction has an unexpected backup payload" + ) + else: + try: + _validate_private_transaction_payload( + control=control, + transaction_id=normalized_id, + kind="backups", + payload=backup, + ) + except PersonalInstallError as exc: + raise PersonalInstallRollbackError( + "P1 backup metadata/path is unsafe for restoration" + ) from exc + if ( + journal["backup_relative_path"] != expected_backup_relative + or journal["backup_digest"] != pre_digest + or path_digest(backup) != pre_digest + ): + raise PersonalInstallRollbackError( + "P1 backup path/bytes do not match the transaction" + ) + + quarantine = _exact_journal_payload_path( + control, normalized_id, "quarantine" + ) + if os.path.lexists(quarantine.parent): + raise PersonalInstallReplayError( + "P1 transaction quarantine already exists" + ) + os.mkdir(quarantine.parent, 0o700) + fsync_directory(quarantine.parent.parent) + journal["state"] = "rollback_started" + journal["quarantine_relative_path"] = _relative_control_path( + control, quarantine + ) + journal = _replace_journal(journal_path, journal) + + try: + _atomic_rename_noreplace(target, quarantine) + quarantine_digest = path_digest(quarantine) + if quarantine_digest != installed_digest: + raise PersonalInstallAmbiguousState( + "quarantined P1 bytes differ from installed bytes" + ) + if pre_digest is not None: + if os.path.lexists(target): + raise PersonalInstallAmbiguousState( + "P1 target reappeared before backup restoration" + ) + _atomic_rename_noreplace(backup, target) + restored_digest = optional_path_digest(target) + if restored_digest != pre_digest: + raise PersonalInstallAmbiguousState( + "P1 rollback did not restore the exact pre-install state" + ) + except Exception as exc: + if not os.path.lexists(target) and os.path.lexists(quarantine): + try: + _atomic_rename_noreplace(quarantine, target) + except Exception: + pass + raise PersonalInstallAmbiguousState( + "P1 rollback stopped in a non-replayable state; use audited manual recovery" + ) from exc + + journal["state"] = "rolled_back" + journal["quarantine_digest"] = quarantine_digest + journal["restored_digest"] = restored_digest + journal = _replace_journal(journal_path, journal) + return PersonalRollbackReceipt( + transaction_id=normalized_id, + status="rolled_back", + personal_root=str(root), + target=str(target), + installed_digest=installed_digest, + quarantine_digest=quarantine_digest, + restored_digest=restored_digest, + journal_digest=journal["journal_digest"], + ) + + +def _rollback_receipt_from_journal( + journal: Mapping[str, Any], +) -> PersonalRollbackReceipt: + if ( + journal["state"] != "rolled_back" + or journal["installed_digest"] is None + or journal["quarantine_digest"] is None + ): + raise PersonalInstallJournalError( + "P1 rollback receipt requires a terminal rolled_back journal" + ) + return PersonalRollbackReceipt( + transaction_id=journal["transaction_id"], + status="rolled_back", + personal_root=journal["personal_root"], + target=journal["target"], + installed_digest=journal["installed_digest"], + quarantine_digest=journal["quarantine_digest"], + restored_digest=journal["restored_digest"], + journal_digest=journal["journal_digest"], + ) + + +def _coerce_install_receipt( + value: PersonalInstallReceipt | Mapping[str, Any], +) -> PersonalInstallReceipt: + if isinstance(value, PersonalInstallReceipt): + # Reconstruct so dataclass instances created through unusual mechanisms + # cannot skip the same closed-field validation as mappings. + return PersonalInstallReceipt.from_dict(value.to_dict()) + if not isinstance(value, Mapping): + raise ValueError("personal install receipt must be a typed receipt or object") + return PersonalInstallReceipt.from_dict(value) + + +def _coerce_rollback_receipt( + value: PersonalRollbackReceipt | Mapping[str, Any], +) -> PersonalRollbackReceipt: + if isinstance(value, PersonalRollbackReceipt): + return PersonalRollbackReceipt.from_dict(value.to_dict()) + if not isinstance(value, Mapping): + raise ValueError("personal rollback receipt must be a typed receipt or object") + return PersonalRollbackReceipt.from_dict(value) + + +def validate_personal_install_receipt( + value: PersonalInstallReceipt | Mapping[str, Any], +) -> PersonalInstallReceipt: + """Reload fixed journal/current bytes and validate an install receipt. + + Shape, self-digest, or a caller-created dataclass is never sufficient. A + valid result requires the exact library journal to remain in ``installed`` + state, the target to retain the installed bytes and behavior report, and + any backup to retain the exact pre-install bytes at the derived path. + """ + + receipt = _coerce_install_receipt(value) + root, journal_path, initial = _find_transaction_journal(receipt.transaction_id) + with _personal_root_lock(root, create=False) as control: + journal = _load_journal(journal_path) + if journal != initial: + raise PersonalInstallJournalError( + "P1 journal changed before receipt-validation lock acquisition" + ) + expected_receipt = _receipt_from_journal(journal) + if expected_receipt.to_dict() != receipt.to_dict(): + raise PersonalInstallJournalError( + "personal install receipt differs from the fixed transaction journal" + ) + target = Path(journal["target"]) + observation = observe_personal_target(target) + if ( + observation.personal_root != str(root) + or observation.root_identity_digest != journal["root_identity_digest"] + or observation.target_digest != receipt.post_install_digest + ): + raise PersonalInstallJournalError( + "personal install receipt differs from current target bytes/root" + ) + current_report = _p1_behavior_report(target) + if ( + current_report.content_digest != receipt.behavior_report_digest + or current_report.candidate_digest != receipt.post_install_digest + or behavior_risk_commitment_digest(current_report) + != receipt.risk_commitment_digest + ): + raise PersonalInstallJournalError( + "personal install receipt differs from the current behavior audit" + ) + + backup = _exact_journal_payload_path( + control, receipt.transaction_id, "backups" + ) + if receipt.pre_target_digest is None: + if os.path.lexists(backup): + raise PersonalInstallJournalError( + "new-target P1 receipt has an unexpected backup payload" + ) + else: + try: + _validate_private_transaction_payload( + control=control, + transaction_id=receipt.transaction_id, + kind="backups", + payload=backup, + ) + except PersonalInstallError as exc: + raise PersonalInstallJournalError( + "personal install backup metadata/path is unsafe" + ) from exc + if ( + journal["backup_relative_path"] + != _relative_control_path(control, backup) + or path_digest(backup) != receipt.pre_target_digest + ): + raise PersonalInstallJournalError( + "personal install backup no longer matches the receipt" + ) + return receipt + + +def validate_personal_rollback_receipt( + value: PersonalRollbackReceipt | Mapping[str, Any], +) -> PersonalRollbackReceipt: + """Reload terminal journal/quarantine/restored bytes for a rollback receipt.""" + + receipt = _coerce_rollback_receipt(value) + root, journal_path, initial = _find_transaction_journal(receipt.transaction_id) + with _personal_root_lock(root, create=False) as control: + journal = _load_journal(journal_path) + if journal != initial: + raise PersonalInstallJournalError( + "P1 journal changed before rollback-receipt validation" + ) + expected_receipt = _rollback_receipt_from_journal(journal) + if expected_receipt.to_dict() != receipt.to_dict(): + raise PersonalInstallJournalError( + "personal rollback receipt differs from the fixed terminal journal" + ) + target = Path(journal["target"]) + observation = observe_personal_target(target) + if ( + observation.personal_root != str(root) + or observation.root_identity_digest != journal["root_identity_digest"] + or observation.target_digest != receipt.restored_digest + ): + raise PersonalInstallJournalError( + "personal rollback receipt differs from the restored target" + ) + quarantine = _exact_journal_payload_path( + control, receipt.transaction_id, "quarantine" + ) + if ( + journal["quarantine_relative_path"] + != _relative_control_path(control, quarantine) + or not os.path.lexists(quarantine) + or path_digest(quarantine) != receipt.quarantine_digest + ): + raise PersonalInstallJournalError( + "personal rollback quarantine differs from the terminal journal" + ) + quarantined_report = _p1_behavior_report(quarantine) + if ( + quarantined_report.content_digest != journal["behavior_report_digest"] + or quarantined_report.candidate_digest != receipt.installed_digest + or behavior_risk_commitment_digest(quarantined_report) + != journal["risk_commitment_digest"] + ): + raise PersonalInstallJournalError( + "personal rollback quarantine behavior differs from installed bytes" + ) + backup = _exact_journal_payload_path( + control, receipt.transaction_id, "backups" + ) + if os.path.lexists(backup): + raise PersonalInstallJournalError( + "terminal P1 rollback unexpectedly retains an active backup payload" + ) + return receipt + + +__all__ = [ + "PERSONAL_INSTALL_ACTION", + "PERSONAL_INSTALL_RECEIPT_OBJECT_VERSION", + "PERSONAL_ROLLBACK_RECEIPT_OBJECT_VERSION", + "PersonalInstallAmbiguousState", + "PersonalInstallAuthorizationError", + "PersonalInstallError", + "PersonalInstallIntegrationError", + "PersonalInstallJournalError", + "PersonalInstallReceipt", + "PersonalInstallReplayError", + "PersonalInstallRiskError", + "PersonalInstallRollbackError", + "PersonalInstallTargetError", + "PersonalRollbackReceipt", + "PersonalTargetObservation", + "make_personal_install_decision_payload", + "observe_personal_target", + "personal_install", + "personal_install_authorization_target_digest", + "rollback_personal_install", + "validate_personal_install_receipt", + "validate_personal_rollback_receipt", +] diff --git a/runtime/skill-optimizer/scripts/packaging/team_delivery.py b/runtime/skill-optimizer/scripts/packaging/team_delivery.py new file mode 100644 index 0000000..57cd47d --- /dev/null +++ b/runtime/skill-optimizer/scripts/packaging/team_delivery.py @@ -0,0 +1,2731 @@ +"""Fail-closed P2 team delivery for one current Skill candidate. + +The module produces an isolated, host-specific, read-only package. It never +installs or activates the package. A successful write requires a recovered +raw workflow chain whose current user ``external_write`` grant binds the exact +candidate, behavior report, runtime projection, output directory identity, +scope projection, and quality-unverified limitation. + +The existing package builder remains the byte/manifest authority. This layer +adds the narrower runtime projection, sensitive-material audit, workflow +authority, and a delivery manifest whose quality and compatibility claim caps +remain explicit. +""" + +from __future__ import annotations + +from collections.abc import Iterable, Mapping, Sequence +from contextlib import contextmanager +from dataclasses import dataclass +import errno +import fcntl +import json +import os +from pathlib import Path +import re +import shutil +import stat +from typing import Any, Iterator + +from core.behavior_risk import ( + BehaviorRiskReport, + audit_behavior_risk, + report_from_dict, + validate_behavior_risk_report, +) +from core.canonical import ( + AuthorizationKind, + GateResult, + RiskLevel, + digest_bytes, + digest_json, + tree_digest, +) +from core.delivery import ( + QualityProjection, + behavior_risk_commitment_digest, + project_untrusted_quality, +) +from core.filesystem import fsync_directory +from core.workflow import ( + AuthorizationGrant, + WorkflowActor, + WorkflowEventType, + evaluate_authorization_gate, + recover_events, +) + +from .builder import ( + DEFAULT_MAX_FILE_BYTES, + DEFAULT_MAX_TOTAL_BYTES, + PackagingError, + VerificationReceipt, + build_distribution, + verify_distribution, +) + + +TEAM_DELIVERY_SCHEMA_VERSION = "1.0.0" +TEAM_DELIVERY_MANIFEST_OBJECT_VERSION = ( + "skill-optimizer.team-delivery-manifest/1" +) +TEAM_DELIVERY_ACTION_TARGET_OBJECT_VERSION = ( + "skill-optimizer.team-delivery-action-target/1" +) +TEAM_DELIVERY_ID_OBJECT_VERSION = "skill-optimizer.team-delivery-id/1" +TEAM_DELIVERY_ACTION = "team_delivery" +QUALITY_UNVERIFIED_ACTION = "quality_unverified" + +_SUPPORTED_HOSTS = frozenset({"codex", "claude"}) +_RUNTIME_ROOT_FILES = frozenset({"SKILL.md", "VERSION", "LICENSE"}) +_RUNTIME_DIRECTORIES = { + "codex": frozenset({"agents", "references", "scripts", "assets"}), + "claude": frozenset({"references", "scripts", "assets"}), +} +_DEVELOPMENT_COMPONENTS = frozenset( + { + "research", + "researches", + "eval", + "evals", + "evaluation", + "evaluations", + "benchmark", + "benchmarks", + "cache", + "caches", + "__pycache__", + ".mypy_cache", + ".pytest_cache", + ".ruff_cache", + "test", + "tests", + "testing", + "journal", + "journals", + "backup", + "backups", + "dev", + "development", + "trace", + "traces", + "fixture", + "fixtures", + } +) +_DEVELOPMENT_WORDS = frozenset( + { + "research", + "eval", + "evaluation", + "benchmark", + "test", + "journal", + "backup", + "dev", + "development", + "trace", + } +) +_TRACE_SUFFIXES = frozenset({".log", ".trace", ".jsonl", ".pyc", ".pyo"}) +_SUSPICIOUS_DATA_SUFFIXES = frozenset( + { + ".txt", + ".md", + ".json", + ".yaml", + ".yml", + ".toml", + ".ini", + ".cfg", + ".conf", + ".properties", + ".csv", + } +) +_SUSPICIOUS_NAME_WORDS = frozenset( + { + "token", + "tokens", + "password", + "passwords", + "passwd", + "credential", + "credentials", + "secret", + "secrets", + "api-key", + "api_key", + "access-token", + "access_token", + "refresh-token", + "refresh_token", + "private-key", + "private_key", + "key", + "keys", + } +) +_BUILDER_SENSITIVE_NAMES = frozenset( + { + ".env", + ".netrc", + ".npmrc", + ".pypirc", + "credentials", + "credentials.json", + "id_dsa", + "id_ed25519", + "id_rsa", + "secrets", + "secrets.json", + } +) +_SENSITIVE_SUFFIXES = frozenset( + {".key", ".pem", ".p12", ".pfx", ".crt", ".cer", ".cert"} +) +_KNOWN_BINARY_SUFFIXES = frozenset( + { + ".png", + ".jpg", + ".jpeg", + ".gif", + ".webp", + ".ico", + ".woff", + ".woff2", + ".ttf", + ".otf", + } +) +_DIGEST_RE = re.compile(r"^sha256:[0-9a-f]{64}$") +_CLAIM_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9_.:/-]{0,127}$") +_SECRET_PATTERNS = ( + ( + "private-key-material", + re.compile(rb"-----BEGIN (?:[A-Z0-9 ]+ )?PRIVATE KEY-----"), + ), + ("certificate-material", re.compile(rb"-----BEGIN CERTIFICATE-----")), + ("aws-access-key", re.compile(rb"\b(?:AKIA|ASIA)[0-9A-Z]{16}\b")), + ( + "github-token", + re.compile(rb"\b(?:github_pat_[A-Za-z0-9_]{20,}|gh[pousr]_[A-Za-z0-9]{20,})\b"), + ), + ("slack-token", re.compile(rb"\bxox[baprs]-[A-Za-z0-9-]{16,}\b")), + ("openai-style-key", re.compile(rb"\bsk-[A-Za-z0-9_-]{20,}\b")), +) +_ASSIGNMENT_RE = re.compile( + r"(?i)\b(password|passwd|token|api[_-]?key|secret|credential)\b" + r"\s*[:=]\s*(?:" + r"(?P[\"'])(?P[^\"'\r\n]{8,})(?P=quote)" + r"|(?P[A-Za-z0-9_.:/+@-]{8,}))" +) +_CODE_SUFFIXES = frozenset( + {".py", ".js", ".mjs", ".cjs", ".ts", ".rb", ".pl", ".ps1", ".sh"} +) +_WORKFLOW_MAX_BYTES = 4 * 1024 * 1024 +_WORKFLOW_MAX_EVENTS = 4096 +_WORKFLOW_MAX_LINE_BYTES = 1024 * 1024 + +_FIXED_THREAT_MODEL = { + "same_user_malicious_actor_resistant": False, + "destination_parent_toctou_fully_eliminated": False, + "directory_lock": "advisory-flock-plus-inode-recheck", + "claim_cap": "current-local-structure-and-bytes-only", + "limitations": [ + "local-workflow-hash-chain-is-not-an-identity-signature", + "path-based-builder-cannot-eliminate-malicious-parent-replacement", + "static-audit-does-not-prove-absence-of-all-unknown-secrets", + "no-host-install-activation-routing-or-compatibility-proof", + ], +} +_FIXED_INTEGRATION_REQUESTS = ( + "gate1-frozen-scope-event-adapter", + "gate1-quality-raw-graph-adapter", + "real-host-compatibility-adapter", + "shared-cli-and-schema-registry-integration", +) + + +class TeamDeliveryError(PackagingError): + """Base error for a P2 artifact that must not be delivered.""" + + +class TeamDeliveryAuthorizationError(TeamDeliveryError): + """The raw workflow chain does not authorize this exact output.""" + + +class TeamDeliveryIntegrationRequired(TeamDeliveryError): + """A required trust adapter is unavailable in the standalone Track C tree.""" + + def __init__(self, message: str, integration_requests: Iterable[str]): + self.integration_requests = tuple(sorted(set(integration_requests))) + super().__init__(message) + + +class UnsafeTeamOutputError(TeamDeliveryError): + """The destination is not an isolated P2 output directory.""" + + +class SensitiveMaterialError(TeamDeliveryError): + """Selected or candidate material may contain a secret.""" + + def __init__(self, message: str, findings: Iterable[Mapping[str, Any]] = ()): + self.findings = tuple(dict(item) for item in findings) + super().__init__(message) + + +class TeamDeliveryIntegrityError(TeamDeliveryError): + """Current bytes do not match their frozen P2 evidence.""" + + +@dataclass(frozen=True) +class ProjectionEntry: + path: str + size: int + digest: str + executable: bool + + def to_dict(self) -> dict[str, Any]: + return { + "path": self.path, + "size": self.size, + "digest": self.digest, + "executable": self.executable, + } + + +@dataclass(frozen=True) +class ProjectionExclusion: + path: str + reason: str + + def to_dict(self) -> dict[str, str]: + return {"path": self.path, "reason": self.reason} + + +@dataclass(frozen=True) +class _ProjectionPlan: + selected_files: tuple[ProjectionEntry, ...] + excluded_paths: tuple[ProjectionExclusion, ...] + + @property + def content_digest(self) -> str: + return digest_json( + { + "object_version": "skill-optimizer.runtime-projection/1", + "selected_files": [item.to_dict() for item in self.selected_files], + "excluded_paths": [item.to_dict() for item in self.excluded_paths], + } + ) + + @property + def whitelist(self) -> tuple[str, ...]: + return tuple(item.path for item in self.selected_files) + + +@dataclass(frozen=True) +class _OwnedArtifact: + """One exact inode and digest proven to have been created by this build.""" + + path: Path + kind: str + output_identity_digest: str + device: int + inode: int + digest: str + + +@dataclass(frozen=True) +class TeamDeliveryActionTarget: + host: str + candidate_digest: str + behavior_report_digest: str + risk_commitment_digest: str + scope_digest: str + scope_projection_digest: str + quality_projection_digest: str + runtime_projection_digest: str + output_root: str + output_root_identity_digest: str + quality_unverified: bool + automatic_routing_requested: bool + automatic_routing_effective: bool + package_relative_path: str = "package" + builder_manifest_relative_path: str = "package.manifest.json" + team_manifest_relative_path: str = "team-delivery-manifest.json" + object_version: str = TEAM_DELIVERY_ACTION_TARGET_OBJECT_VERSION + + def __post_init__(self) -> None: + if self.object_version != TEAM_DELIVERY_ACTION_TARGET_OBJECT_VERSION: + raise ValueError("unsupported team delivery action target object_version") + if self.host not in _SUPPORTED_HOSTS: + raise ValueError("unsupported team delivery host") + for field_name in ( + "candidate_digest", + "behavior_report_digest", + "risk_commitment_digest", + "scope_digest", + "scope_projection_digest", + "quality_projection_digest", + "runtime_projection_digest", + "output_root_identity_digest", + ): + _require_digest(getattr(self, field_name), field_name) + if self.quality_unverified is not True: + raise ValueError("standalone P2 requires the quality-unverified limitation") + if self.automatic_routing_requested is not False: + raise ValueError( + "standalone P2 has no trusted automatic-routing scope adapter" + ) + if self.automatic_routing_effective is not False: + raise ValueError("standalone P2 cannot enable automatic routing") + output = Path(self.output_root) + if not output.is_absolute(): + raise ValueError("team delivery output root must be absolute") + for value, expected, field_name in ( + (self.package_relative_path, "package", "package_relative_path"), + ( + self.builder_manifest_relative_path, + "package.manifest.json", + "builder_manifest_relative_path", + ), + ( + self.team_manifest_relative_path, + "team-delivery-manifest.json", + "team_manifest_relative_path", + ), + ): + if value != expected: + raise ValueError(f"unsupported {field_name}") + + def body(self) -> dict[str, Any]: + return { + "object_version": self.object_version, + "host": self.host, + "candidate_digest": self.candidate_digest, + "behavior_report_digest": self.behavior_report_digest, + "risk_commitment_digest": self.risk_commitment_digest, + "scope_digest": self.scope_digest, + "scope_projection_digest": self.scope_projection_digest, + "quality_projection_digest": self.quality_projection_digest, + "runtime_projection_digest": self.runtime_projection_digest, + "output_root": self.output_root, + "output_root_identity_digest": self.output_root_identity_digest, + "quality_unverified": self.quality_unverified, + "automatic_routing_requested": self.automatic_routing_requested, + "automatic_routing_effective": self.automatic_routing_effective, + "package_relative_path": self.package_relative_path, + "builder_manifest_relative_path": self.builder_manifest_relative_path, + "team_manifest_relative_path": self.team_manifest_relative_path, + } + + @property + def content_digest(self) -> str: + return digest_json(self.body()) + + def to_dict(self) -> dict[str, Any]: + return {**self.body(), "content_digest": self.content_digest} + + @classmethod + def from_dict(cls, value: Mapping[str, Any]) -> "TeamDeliveryActionTarget": + expected = { + "object_version", + "host", + "candidate_digest", + "behavior_report_digest", + "risk_commitment_digest", + "scope_digest", + "scope_projection_digest", + "quality_projection_digest", + "runtime_projection_digest", + "output_root", + "output_root_identity_digest", + "quality_unverified", + "automatic_routing_requested", + "automatic_routing_effective", + "package_relative_path", + "builder_manifest_relative_path", + "team_manifest_relative_path", + "content_digest", + } + _require_closed_mapping(value, expected, "team delivery action target") + target = cls(**{key: value[key] for key in expected if key != "content_digest"}) + if value["content_digest"] != target.content_digest: + raise ValueError("team delivery action target content_digest mismatch") + return target + + +@dataclass(frozen=True) +class TeamDeliveryManifest: + delivery_id: str + host: str + source_candidate_digest: str + source_behavior_report_digest: str + source_risk_commitment_digest: str + source_scope_projection_digest: str + output_root: str + package_root: str + builder_manifest_path: str + team_manifest_path: str + authority: Mapping[str, Any] + behavior: Mapping[str, Any] + projection: Mapping[str, Any] + package: Mapping[str, Any] + claims: Mapping[str, Any] + quality_claim_projection: Mapping[str, Any] + compatibility: Mapping[str, Any] + automatic_routing: Mapping[str, Any] + change_summary: Mapping[str, Any] + rollback: Mapping[str, Any] + threat_model: Mapping[str, Any] + integration_requests: tuple[str, ...] + content_digest: str + delivery_target: str = "P2_team_package" + invocation_mode: str = "explicit_only" + schema_version: str = TEAM_DELIVERY_SCHEMA_VERSION + object_version: str = TEAM_DELIVERY_MANIFEST_OBJECT_VERSION + + def __post_init__(self) -> None: + for field_name in ( + "authority", + "behavior", + "projection", + "package", + "claims", + "quality_claim_projection", + "compatibility", + "automatic_routing", + "change_summary", + "rollback", + "threat_model", + ): + value = getattr(self, field_name) + if not isinstance(value, Mapping): + raise ValueError(f"{field_name} must be an object") + object.__setattr__(self, field_name, _json_mapping(value)) + object.__setattr__( + self, + "integration_requests", + _closed_strings(self.integration_requests, "integration_requests"), + ) + _validate_manifest_semantics(self) + + def body(self) -> dict[str, Any]: + return { + "object_version": self.object_version, + "schema_version": self.schema_version, + "delivery_id": self.delivery_id, + "delivery_target": self.delivery_target, + "host": self.host, + "invocation_mode": self.invocation_mode, + "source_candidate_digest": self.source_candidate_digest, + "source_behavior_report_digest": self.source_behavior_report_digest, + "source_risk_commitment_digest": self.source_risk_commitment_digest, + "source_scope_projection_digest": self.source_scope_projection_digest, + "output_root": self.output_root, + "package_root": self.package_root, + "builder_manifest_path": self.builder_manifest_path, + "team_manifest_path": self.team_manifest_path, + "authority": dict(self.authority), + "behavior": dict(self.behavior), + "projection": dict(self.projection), + "package": dict(self.package), + "claims": dict(self.claims), + "quality_claim_projection": dict(self.quality_claim_projection), + "compatibility": dict(self.compatibility), + "automatic_routing": dict(self.automatic_routing), + "change_summary": dict(self.change_summary), + "rollback": dict(self.rollback), + "threat_model": dict(self.threat_model), + "integration_requests": list(self.integration_requests), + } + + def to_dict(self) -> dict[str, Any]: + return {**self.body(), "content_digest": self.content_digest} + + @classmethod + def from_dict(cls, value: Mapping[str, Any]) -> "TeamDeliveryManifest": + expected = { + "object_version", + "schema_version", + "delivery_id", + "delivery_target", + "host", + "invocation_mode", + "source_candidate_digest", + "source_behavior_report_digest", + "source_risk_commitment_digest", + "source_scope_projection_digest", + "output_root", + "package_root", + "builder_manifest_path", + "team_manifest_path", + "authority", + "behavior", + "projection", + "package", + "claims", + "quality_claim_projection", + "compatibility", + "automatic_routing", + "change_summary", + "rollback", + "threat_model", + "integration_requests", + "content_digest", + } + _require_closed_mapping(value, expected, "team delivery manifest") + return cls(**dict(value)) + + +def _json_mapping(value: Mapping[str, Any]) -> dict[str, Any]: + return json.loads(json.dumps(dict(value), ensure_ascii=True, sort_keys=True)) + + +def _require_closed_mapping( + value: object, expected: set[str], field_name: str +) -> Mapping[str, Any]: + if not isinstance(value, Mapping) or set(value) != expected: + raise ValueError(f"{field_name} fields do not match the closed contract") + return value + + +def _require_digest(value: object, field_name: str) -> str: + if not isinstance(value, str) or _DIGEST_RE.fullmatch(value) is None: + raise ValueError(f"{field_name} must be a sha256 digest") + return value + + +def _closed_strings(value: Iterable[str], field_name: str) -> tuple[str, ...]: + if isinstance(value, (str, bytes)): + raise ValueError(f"{field_name} must be an array") + normalized = tuple(value) + if any(not isinstance(item, str) or not item.strip() for item in normalized): + raise ValueError(f"{field_name} must contain non-empty strings") + if len(set(normalized)) != len(normalized): + raise ValueError(f"{field_name} must not contain duplicates") + return tuple(sorted(normalized)) + + +def _claim_strings(value: object, field_name: str) -> tuple[str, ...]: + values = _closed_strings(value, field_name) # type: ignore[arg-type] + if any(_CLAIM_RE.fullmatch(item) is None for item in values): + raise ValueError(f"{field_name} contains an unsafe claim identifier") + return values + + +def _standalone_scope_projection() -> dict[str, Any]: + """Return the fixed claim cap used until Gate 1 supplies real scope facts.""" + + fixed = { + "object_version": "skill-optimizer.standalone-team-scope/1", + "invocation_mode": "explicit_only", + "automatic_routing": False, + "quality_unverified": True, + "source_status": "unverified", + } + return {**fixed, "source_digest": digest_json(fixed)} + + +def _candidate_root(candidate_root: str | os.PathLike[str]) -> Path: + supplied = Path(candidate_root).expanduser() + if supplied.is_symlink(): + raise TeamDeliveryError("candidate root cannot be a symbolic link") + candidate = supplied.resolve(strict=True) + metadata = candidate.lstat() + if not stat.S_ISDIR(metadata.st_mode) or stat.S_ISLNK(metadata.st_mode): + raise TeamDeliveryError("candidate root must be a real directory") + if not (candidate / "SKILL.md").is_file(): + raise TeamDeliveryError("candidate root must contain SKILL.md") + return candidate + + +def _known_skill_roots(extra: Iterable[str | os.PathLike[str]]) -> tuple[Path, ...]: + roots = { + (Path.home() / ".codex" / "skills").resolve(strict=False), + (Path.home() / ".claude" / "skills").resolve(strict=False), + (Path.home() / ".agents" / "skills").resolve(strict=False), + } + codex_home = os.environ.get("CODEX_HOME") + if codex_home: + roots.add((Path(codex_home).expanduser() / "skills").resolve(strict=False)) + for value in extra: + roots.add(Path(value).expanduser().resolve(strict=False)) + return tuple(sorted(roots, key=str)) + + +def _contains_host_skill_root_signature(path: Path) -> bool: + parts = tuple(part.casefold() for part in path.parts) + signatures = ((".codex", "skills"), (".claude", "skills"), (".agents", "skills")) + return any( + parts[index : index + 2] == signature + for signature in signatures + for index in range(max(0, len(parts) - 1)) + ) + + +def _is_relative_to(path: Path, parent: Path) -> bool: + try: + path.relative_to(parent) + except ValueError: + return False + return True + + +def _require_owned_unwritable_directory(path: Path, field_name: str) -> os.stat_result: + try: + metadata = path.lstat() + except OSError as exc: + raise UnsafeTeamOutputError(f"{field_name} is unavailable: {exc}") from exc + if not stat.S_ISDIR(metadata.st_mode) or stat.S_ISLNK(metadata.st_mode): + raise UnsafeTeamOutputError(f"{field_name} must be a real directory") + if metadata.st_uid != os.geteuid(): + raise UnsafeTeamOutputError(f"{field_name} must be owned by the current user") + if metadata.st_mode & (stat.S_IWGRP | stat.S_IWOTH): + raise UnsafeTeamOutputError( + f"{field_name} cannot be group- or world-writable" + ) + return metadata + + +def _validate_output_root( + output_root: str | os.PathLike[str], + candidate: Path, + *, + official_skill_roots: Iterable[str | os.PathLike[str]], + require_empty: bool = True, +) -> Path: + supplied = Path(output_root).expanduser() + if supplied.is_symlink(): + raise UnsafeTeamOutputError("team output root cannot be a symbolic link") + output = supplied.resolve(strict=True) + _require_owned_unwritable_directory(output.parent, "team output parent") + _require_owned_unwritable_directory(output, "team output root") + if output == output.parent or not output.name: + raise UnsafeTeamOutputError("team output root cannot be a filesystem root") + if output.name.casefold() == "skills": + raise UnsafeTeamOutputError("team output root cannot be named like a host Skill root") + if ( + output == candidate + or _is_relative_to(output, candidate) + or _is_relative_to(candidate, output) + ): + raise UnsafeTeamOutputError("team output root and candidate cannot overlap") + if _contains_host_skill_root_signature(output): + raise UnsafeTeamOutputError("team output root cannot be a host Skill root") + for root in _known_skill_roots(official_skill_roots): + if output == root or _is_relative_to(output, root): + raise UnsafeTeamOutputError("team output root cannot be inside a host Skill root") + if require_empty: + try: + entries = tuple(output.iterdir()) + except OSError as exc: + raise UnsafeTeamOutputError(f"cannot inspect team output root: {exc}") from exc + if entries: + raise UnsafeTeamOutputError("team output root must be empty and dedicated") + return output + + +def _output_identity(path: Path) -> str: + _require_owned_unwritable_directory(path.parent, "team output parent") + metadata = _require_owned_unwritable_directory(path, "team output root") + return digest_json( + { + "object_version": "skill-optimizer.output-root-identity/1", + "path": str(path), + "device": metadata.st_dev, + "inode": metadata.st_ino, + "mode": stat.S_IMODE(metadata.st_mode), + "owner": metadata.st_uid, + } + ) + + +@contextmanager +def _locked_output_root(path: Path) -> Iterator[str]: + flags = os.O_RDONLY | getattr(os, "O_CLOEXEC", 0) | getattr(os, "O_DIRECTORY", 0) + flags |= getattr(os, "O_NOFOLLOW", 0) + try: + descriptor = os.open(path, flags) + except OSError as exc: + raise UnsafeTeamOutputError(f"cannot open team output root safely: {exc}") from exc + try: + metadata = os.fstat(descriptor) + if not stat.S_ISDIR(metadata.st_mode): + raise UnsafeTeamOutputError("team output root changed before locking") + try: + fcntl.flock(descriptor, fcntl.LOCK_EX | fcntl.LOCK_NB) + except OSError as exc: + raise UnsafeTeamOutputError("team output root is in concurrent use") from exc + identity = _output_identity(path) + yield identity + if _output_identity(path) != identity: + raise UnsafeTeamOutputError("team output root changed during delivery") + finally: + try: + fcntl.flock(descriptor, fcntl.LOCK_UN) + finally: + os.close(descriptor) + + +def _relative_path(path: Path, candidate: Path) -> str: + relative = path.relative_to(candidate).as_posix() + if not relative or relative.startswith("/") or any( + part in {"", ".", ".."} for part in relative.split("/") + ): + raise TeamDeliveryError("candidate contains an unsafe relative path") + return relative + + +def _development_exclusion(relative: str) -> str | None: + parts = relative.split("/") + lowered = tuple(part.casefold() for part in parts) + if any(part in _DEVELOPMENT_COMPONENTS for part in lowered): + return "development_material" + basename = lowered[-1] + stem_words = { + item for item in re.split(r"[^a-z0-9]+", Path(basename).stem) if item + } + if stem_words & _DEVELOPMENT_WORDS: + return "development_material" + if basename.startswith("test_") or basename.endswith("_test.py"): + return "test_material" + if Path(basename).suffix in _TRACE_SUFFIXES: + return "development_trace_or_cache" + return None + + +def _selection_exclusion(relative: str, host: str) -> str | None: + parts = relative.split("/") + if len(parts) == 1: + if relative in _RUNTIME_ROOT_FILES: + return None + development = _development_exclusion(relative) + if development is not None: + return development + return "not_runtime_allowlisted" + top = parts[0] + development = _development_exclusion(relative) + if development is not None: + return development + if top not in _RUNTIME_DIRECTORIES[host]: + if top == "agents" and host == "claude": + return "host_specific_not_runtime" + return "not_runtime_allowlisted" + return None + + +def _sensitive_name_finding(relative: str) -> dict[str, Any] | None: + for component in relative.split("/"): + lowered = component.casefold() + suffix = Path(lowered).suffix + if ( + lowered in _BUILDER_SENSITIVE_NAMES + or lowered.startswith(".env.") + or suffix in _SENSITIVE_SUFFIXES + ): + return { + "path": relative, + "line": None, + "rule_id": "known-sensitive-path", + "content_digest": digest_bytes(relative.encode("utf-8")), + } + basename = relative.rsplit("/", 1)[-1].casefold() + suffix = Path(basename).suffix + stem = basename[: -len(suffix)] if suffix else basename + words = {stem, *re.split(r"[^a-z0-9_-]+", stem)} + if suffix in _SUSPICIOUS_DATA_SUFFIXES and words & _SUSPICIOUS_NAME_WORDS: + return { + "path": relative, + "line": None, + "rule_id": "suspicious-sensitive-filename", + "content_digest": digest_bytes(relative.encode("utf-8")), + } + return None + + +def _read_stable_file(path: Path, relative: str, maximum: int) -> tuple[bytes, int]: + flags = os.O_RDONLY | getattr(os, "O_CLOEXEC", 0) | getattr(os, "O_NOFOLLOW", 0) + try: + descriptor = os.open(path, flags) + except OSError as exc: + if exc.errno in {errno.ELOOP, errno.EMLINK}: + raise TeamDeliveryError(f"symbolic links are not allowed: {relative}") from exc + raise TeamDeliveryError(f"cannot snapshot candidate file {relative}: {exc}") from exc + try: + before = os.fstat(descriptor) + if not stat.S_ISREG(before.st_mode): + raise TeamDeliveryError(f"non-regular candidate entry: {relative}") + if before.st_size > maximum: + raise TeamDeliveryError(f"candidate file exceeds size limit: {relative}") + + def read_once() -> bytes: + os.lseek(descriptor, 0, os.SEEK_SET) + chunks: list[bytes] = [] + observed = 0 + while True: + chunk = os.read(descriptor, min(1024 * 1024, maximum - observed + 1)) + if not chunk: + break + chunks.append(chunk) + observed += len(chunk) + if observed > maximum: + raise TeamDeliveryError( + f"candidate file exceeds size limit: {relative}" + ) + return b"".join(chunks) + + first = read_once() + middle = os.fstat(descriptor) + second = read_once() + after = os.fstat(descriptor) + stable = lambda item: ( + item.st_dev, + item.st_ino, + item.st_mode, + item.st_size, + item.st_mtime_ns, + item.st_ctime_ns, + ) + if stable(before) != stable(middle) or stable(middle) != stable(after) or first != second: + raise TeamDeliveryIntegrityError( + f"candidate file changed during projection scan: {relative}" + ) + return first, stat.S_IMODE(after.st_mode) + finally: + os.close(descriptor) + + +def _looks_like_known_binary(data: bytes, suffix: str) -> bool: + if suffix in {".png"}: + return data.startswith(b"\x89PNG\r\n\x1a\n") + if suffix in {".jpg", ".jpeg"}: + return data.startswith(b"\xff\xd8\xff") + if suffix == ".gif": + return data.startswith((b"GIF87a", b"GIF89a")) + if suffix == ".webp": + return len(data) >= 12 and data[:4] == b"RIFF" and data[8:12] == b"WEBP" + if suffix == ".ico": + return data.startswith(b"\x00\x00\x01\x00") + if suffix in {".woff", ".woff2"}: + return data.startswith((b"wOFF", b"wOF2")) + if suffix in {".ttf", ".otf"}: + return data.startswith((b"\x00\x01\x00\x00", b"OTTO")) + return False + + +def _content_findings(relative: str, data: bytes) -> tuple[dict[str, Any], ...]: + findings: list[dict[str, Any]] = [] + for rule_id, pattern in _SECRET_PATTERNS: + match = pattern.search(data) + if match is not None: + findings.append( + { + "path": relative, + "line": data.count(b"\n", 0, match.start()) + 1, + "rule_id": rule_id, + "content_digest": digest_bytes(data), + } + ) + suffix = Path(relative).suffix.casefold() + try: + text = data.decode("utf-8") + except UnicodeDecodeError: + if suffix in _KNOWN_BINARY_SUFFIXES and _looks_like_known_binary(data, suffix): + return tuple(findings) + findings.append( + { + "path": relative, + "line": None, + "rule_id": "unclassified-binary-sensitive-material-unknown", + "content_digest": digest_bytes(data), + } + ) + return tuple(findings) + if "\x00" in text and not _looks_like_known_binary(data, suffix): + findings.append( + { + "path": relative, + "line": None, + "rule_id": "embedded-nul-sensitive-material-unknown", + "content_digest": digest_bytes(data), + } + ) + return tuple(findings) + for line_number, line in enumerate(text.splitlines(), 1): + for match in _ASSIGNMENT_RE.finditer(line): + # A bare expression in source code (for example + # ``TOKEN = os.environ.get(...)``) is runtime credential access, + # not bundled secret material. Quoted literals are always blocked, + # including values labelled redacted/example/dummy: caller prose is + # not evidence that sensitive bytes are absent. + if match.group("bare") is not None and suffix in _CODE_SUFFIXES: + continue + findings.append( + { + "path": relative, + "line": line_number, + "rule_id": "credential-like-assignment", + "content_digest": digest_bytes(data), + } + ) + unique: dict[tuple[str, int | None, str], dict[str, Any]] = {} + for finding in findings: + key = (finding["path"], finding["line"], finding["rule_id"]) + unique.setdefault(key, finding) + return tuple(unique[key] for key in sorted(unique)) + + +def _projection_plan( + candidate: Path, + host: str, + *, + max_file_bytes: int, + max_total_bytes: int, +) -> _ProjectionPlan: + if host not in _SUPPORTED_HOSTS: + raise ValueError(f"unsupported team delivery host: {host}") + selected: list[ProjectionEntry] = [] + excluded: list[ProjectionExclusion] = [] + sensitive: list[dict[str, Any]] = [] + total_bytes = 0 + try: + paths = tuple( + sorted(candidate.rglob("*"), key=lambda item: item.relative_to(candidate).as_posix()) + ) + except OSError as exc: + raise TeamDeliveryError(f"cannot enumerate candidate: {exc}") from exc + for path in paths: + relative = _relative_path(path, candidate) + try: + metadata = path.lstat() + except OSError as exc: + raise TeamDeliveryIntegrityError( + f"candidate changed during projection enumeration: {relative}: {exc}" + ) from exc + if stat.S_ISLNK(metadata.st_mode): + raise TeamDeliveryError(f"symbolic links are not allowed: {relative}") + if stat.S_ISDIR(metadata.st_mode): + continue + if not stat.S_ISREG(metadata.st_mode): + raise TeamDeliveryError(f"non-regular candidate entry: {relative}") + name_finding = _sensitive_name_finding(relative) + if name_finding is not None: + sensitive.append(name_finding) + continue + exclusion = _selection_exclusion(relative, host) + if exclusion is not None: + excluded.append(ProjectionExclusion(relative, exclusion)) + continue + data, mode = _read_stable_file(path, relative, max_file_bytes) + content_findings = _content_findings(relative, data) + if content_findings: + sensitive.extend(content_findings) + continue + total_bytes += len(data) + if total_bytes > max_total_bytes: + raise TeamDeliveryError("runtime projection exceeds total byte limit") + selected.append( + ProjectionEntry( + path=relative, + size=len(data), + digest=digest_bytes(data), + executable=bool(mode & 0o111), + ) + ) + if sensitive: + raise SensitiveMaterialError( + "candidate contains sensitive or unclassifiable material", + sensitive, + ) + if not any(item.path == "SKILL.md" for item in selected): + raise TeamDeliveryError("runtime projection must include SKILL.md") + return _ProjectionPlan(tuple(selected), tuple(excluded)) + + +def _behavior_preconditions( + report: BehaviorRiskReport, + plan: _ProjectionPlan, +) -> None: + validate_behavior_risk_report(report) + if report.has_sensitive_material: + findings = [ + { + "path": finding.path, + "line": finding.line, + "rule_id": finding.rule_id, + "content_digest": finding.content_digest, + } + for finding in report.findings + if finding.sensitive_material_bundled + ] + raise SensitiveMaterialError( + "behavior audit found bundled sensitive material", findings + ) + excluded = {item.path: item.reason for item in plan.excluded_paths} + removable_reasons = { + "development_material", + "test_material", + "development_trace_or_cache", + } + residual_findings = tuple( + finding + for finding in report.findings + if not ( + finding.rule_id == "material.development" + and excluded.get(finding.path) in removable_reasons + ) + ) + residual_unknowns = tuple( + finding for finding in residual_findings if finding.evidence_state.value == "unknown" + ) + residual_runtime = tuple( + finding for finding in residual_findings if finding.requires_runtime_enforcement + ) + requests: list[str] = [] + reasons: list[str] = [] + if residual_unknowns: + reasons.append("behavior audit contains unresolved unknowns") + requests.append("behavior-runtime-enforcement-adapter") + if residual_runtime: + reasons.append("runtime enforcement has not been evidenced") + requests.append("process-plan-module-result-graph-adapter") + if residual_findings: + reasons.append("mandatory controls lack trusted raw execution evidence") + requests.append("process-plan-module-result-graph-adapter") + if any( + finding.risk_finding.dimension.value != "local_mutation" + for finding in residual_findings + ): + reasons.append("mandatory capabilities lack a trusted current host probe") + requests.append("capability-probe-registry-adapter") + if reasons: + raise TeamDeliveryIntegrationRequired("; ".join(reasons), requests) + + +def _quality_projection(report: BehaviorRiskReport, value: Mapping[str, Any] | None): + projection = project_untrusted_quality(report.candidate_digest, value) + if projection.verified_claims: + raise TeamDeliveryIntegrityError( + "standalone quality projection unexpectedly contains verified claims" + ) + return projection + + +def _derive_action_target( + *, + report: BehaviorRiskReport, + plan: _ProjectionPlan, + output: Path, + host: str, + scope_digest: str, + normalized_scope: Mapping[str, Any], + quality_projection_digest: str, +) -> TeamDeliveryActionTarget: + _require_digest(scope_digest, "scope_digest") + return TeamDeliveryActionTarget( + host=host, + candidate_digest=report.candidate_digest, + behavior_report_digest=report.content_digest, + risk_commitment_digest=behavior_risk_commitment_digest(report), + scope_digest=scope_digest, + scope_projection_digest=digest_json(dict(normalized_scope)), + quality_projection_digest=quality_projection_digest, + runtime_projection_digest=plan.content_digest, + output_root=str(output), + output_root_identity_digest=_output_identity(output), + quality_unverified=True, + automatic_routing_requested=bool(normalized_scope["automatic_routing"]), + automatic_routing_effective=False, + ) + + +def prepare_team_delivery_action_target( + candidate_root: str | os.PathLike[str], + output_root: str | os.PathLike[str], + *, + host: str, + scope_digest: str, + quality_summary: Mapping[str, Any] | None = None, + expected_candidate_digest: str | None = None, + official_skill_roots: Iterable[str | os.PathLike[str]] = (), + max_file_bytes: int = DEFAULT_MAX_FILE_BYTES, + max_total_bytes: int = DEFAULT_MAX_TOTAL_BYTES, +) -> TeamDeliveryActionTarget: + """Read current facts and derive the target a user grant must bind. + + This function is read-only. The returned digest is not authority by + itself; ``build_team_delivery`` independently rebuilds it and requires one + matching current user event in a valid raw workflow chain. + """ + + candidate = _candidate_root(candidate_root) + output = _validate_output_root( + output_root, + candidate, + official_skill_roots=official_skill_roots, + ) + normalized_scope = _standalone_scope_projection() + report = audit_behavior_risk(candidate) + if expected_candidate_digest is not None: + _require_digest(expected_candidate_digest, "expected_candidate_digest") + if report.candidate_digest != expected_candidate_digest: + raise TeamDeliveryIntegrityError("candidate digest differs from expectation") + plan = _projection_plan( + candidate, + host, + max_file_bytes=max_file_bytes, + max_total_bytes=max_total_bytes, + ) + _behavior_preconditions(report, plan) + quality = _quality_projection(report, quality_summary) + return _derive_action_target( + report=report, + plan=plan, + output=output, + host=host, + scope_digest=scope_digest, + normalized_scope=normalized_scope, + quality_projection_digest=quality.content_digest, + ) + + +def _codex_workflow_root() -> Path: + return Path.home() / ".codex" / "skill-optimizer" / "workflows" + + +def _claude_workflow_root() -> Path: + return Path.home() / ".claude" / "skill-optimizer" / "workflows" + + +def _workflow_event_root(host: str) -> Path: + if host == "codex": + return _codex_workflow_root() + if host == "claude": + return _claude_workflow_root() + raise ValueError(f"unsupported team delivery host: {host}") + + +def _workflow_event_filename(workflow_id: str) -> str: + if ( + not isinstance(workflow_id, str) + or not workflow_id + or len(workflow_id.encode("utf-8")) > 1024 + or "\x00" in workflow_id + ): + raise TeamDeliveryAuthorizationError("workflow_id is invalid") + identity = digest_bytes(workflow_id.encode("utf-8")).removeprefix("sha256:") + return f"{identity}.jsonl" + + +def _require_private_control_directory(path: Path, field_name: str) -> os.stat_result: + try: + metadata = path.lstat() + except FileNotFoundError as exc: + raise TeamDeliveryIntegrationRequired( + f"{field_name} is unavailable", + ("trusted-workflow-event-source-adapter",), + ) from exc + except OSError as exc: + raise TeamDeliveryAuthorizationError( + f"cannot inspect {field_name}: {exc}" + ) from exc + if not stat.S_ISDIR(metadata.st_mode) or stat.S_ISLNK(metadata.st_mode): + raise TeamDeliveryAuthorizationError(f"{field_name} must be a real directory") + if metadata.st_uid != os.geteuid(): + raise TeamDeliveryAuthorizationError( + f"{field_name} must be owned by the current user" + ) + if metadata.st_mode & (stat.S_IRWXG | stat.S_IRWXO): + raise TeamDeliveryAuthorizationError( + f"{field_name} must not grant group or world permissions" + ) + return metadata + + +def _read_bounded_twice(descriptor: int, maximum: int) -> bytes: + def read_once() -> bytes: + os.lseek(descriptor, 0, os.SEEK_SET) + chunks: list[bytes] = [] + total = 0 + while True: + chunk = os.read(descriptor, min(1024 * 1024, maximum - total + 1)) + if not chunk: + break + chunks.append(chunk) + total += len(chunk) + if total > maximum: + raise TeamDeliveryAuthorizationError( + "trusted workflow event source exceeds its byte limit" + ) + return b"".join(chunks) + + first = read_once() + second = read_once() + if first != second: + raise TeamDeliveryAuthorizationError( + "trusted workflow event source changed while being read" + ) + return first + + +def _load_trusted_workflow_events( + host: str, + workflow_id: str, +) -> tuple[Mapping[str, Any], ...]: + """Load the fixed private workflow source; callers cannot supply a path.""" + + root_input = _workflow_event_root(host).expanduser() + if root_input.is_symlink(): + raise TeamDeliveryAuthorizationError( + "trusted workflow event root cannot be a symbolic link" + ) + try: + root = root_input.resolve(strict=True) + except FileNotFoundError as exc: + raise TeamDeliveryIntegrationRequired( + "trusted workflow event root is unavailable", + ("trusted-workflow-event-source-adapter",), + ) from exc + _require_private_control_directory(root.parent, "trusted workflow parent") + expected_root = _require_private_control_directory( + root, "trusted workflow event root" + ) + directory_flags = ( + os.O_RDONLY + | getattr(os, "O_CLOEXEC", 0) + | getattr(os, "O_DIRECTORY", 0) + | getattr(os, "O_NOFOLLOW", 0) + ) + try: + root_descriptor = os.open(root, directory_flags) + except OSError as exc: + raise TeamDeliveryAuthorizationError( + f"cannot open trusted workflow event root: {exc}" + ) from exc + filename = _workflow_event_filename(workflow_id) + try: + opened_root = os.fstat(root_descriptor) + if ( + opened_root.st_dev != expected_root.st_dev + or opened_root.st_ino != expected_root.st_ino + ): + raise TeamDeliveryAuthorizationError( + "trusted workflow event root changed while opening" + ) + flags = os.O_RDONLY | getattr(os, "O_CLOEXEC", 0) | getattr( + os, "O_NOFOLLOW", 0 + ) + try: + descriptor = os.open(filename, flags, dir_fd=root_descriptor) + except FileNotFoundError as exc: + raise TeamDeliveryIntegrationRequired( + "trusted workflow event source is unavailable", + ("trusted-workflow-event-source-adapter",), + ) from exc + except OSError as exc: + raise TeamDeliveryAuthorizationError( + f"cannot open trusted workflow event source: {exc}" + ) from exc + try: + before = os.fstat(descriptor) + if not stat.S_ISREG(before.st_mode): + raise TeamDeliveryAuthorizationError( + "trusted workflow event source must be a regular file" + ) + if before.st_uid != os.geteuid() or before.st_nlink != 1: + raise TeamDeliveryAuthorizationError( + "trusted workflow event source ownership or link count is unsafe" + ) + if before.st_mode & (stat.S_IRWXG | stat.S_IRWXO): + raise TeamDeliveryAuthorizationError( + "trusted workflow event source must be private" + ) + if before.st_size > _WORKFLOW_MAX_BYTES: + raise TeamDeliveryAuthorizationError( + "trusted workflow event source exceeds its byte limit" + ) + raw = _read_bounded_twice(descriptor, _WORKFLOW_MAX_BYTES) + after = os.fstat(descriptor) + stable = lambda item: ( + item.st_dev, + item.st_ino, + item.st_mode, + item.st_nlink, + item.st_size, + item.st_mtime_ns, + item.st_ctime_ns, + ) + if stable(before) != stable(after): + raise TeamDeliveryAuthorizationError( + "trusted workflow event source changed while being read" + ) + try: + current = os.stat( + filename, + dir_fd=root_descriptor, + follow_symlinks=False, + ) + except OSError as exc: + raise TeamDeliveryAuthorizationError( + "trusted workflow event source changed after reading" + ) from exc + if current.st_dev != after.st_dev or current.st_ino != after.st_ino: + raise TeamDeliveryAuthorizationError( + "trusted workflow event source was replaced after opening" + ) + finally: + os.close(descriptor) + current_root = root.lstat() + if ( + current_root.st_dev != opened_root.st_dev + or current_root.st_ino != opened_root.st_ino + ): + raise TeamDeliveryAuthorizationError( + "trusted workflow event root was replaced after reading" + ) + finally: + os.close(root_descriptor) + if not raw or not raw.endswith(b"\n"): + raise TeamDeliveryAuthorizationError( + "trusted workflow event source must be non-empty complete JSONL" + ) + lines = raw.splitlines() + if len(lines) > _WORKFLOW_MAX_EVENTS: + raise TeamDeliveryAuthorizationError( + "trusted workflow event source exceeds its event limit" + ) + events: list[Mapping[str, Any]] = [] + for index, line in enumerate(lines, 1): + if not line or len(line) > _WORKFLOW_MAX_LINE_BYTES: + raise TeamDeliveryAuthorizationError( + f"trusted workflow event line {index} is empty or oversized" + ) + try: + value = json.loads(line) + except (UnicodeDecodeError, json.JSONDecodeError) as exc: + raise TeamDeliveryAuthorizationError( + f"trusted workflow event line {index} is invalid JSON" + ) from exc + if not isinstance(value, Mapping): + raise TeamDeliveryAuthorizationError( + f"trusted workflow event line {index} must be an object" + ) + events.append(value) + return tuple(events) + + +def _recover_authority( + *, + host: str, + workflow_id: str, + scope_digest: str, + risk_digest: str, + target: TeamDeliveryActionTarget, +) -> dict[str, Any]: + if not isinstance(workflow_id, str) or not workflow_id: + raise TeamDeliveryAuthorizationError("workflow_id is required") + _require_digest(scope_digest, "scope_digest") + _require_digest(risk_digest, "risk_digest") + if scope_digest != target.scope_digest: + raise TeamDeliveryAuthorizationError("scope digest differs from action target") + if risk_digest != target.risk_commitment_digest: + raise TeamDeliveryAuthorizationError( + "risk digest must be the current behavior risk commitment" + ) + workflow_events = _load_trusted_workflow_events(host, workflow_id) + try: + recovery = recover_events(workflow_events) + except Exception as exc: + raise TeamDeliveryAuthorizationError( + f"invalid team delivery workflow authority chain: {exc}" + ) from exc + if recovery.workflow_id != workflow_id: + raise TeamDeliveryAuthorizationError("workflow ID mismatch") + authorization_event_digest = recovery.last_event_digest + if authorization_event_digest is None: + raise TeamDeliveryAuthorizationError( + "trusted workflow event source has no current event head" + ) + events = [ + event + for event in recovery.events + if event.content_digest == authorization_event_digest + and event.event_type is WorkflowEventType.AUTHORIZATION + and event.actor is WorkflowActor.USER + ] + if len(events) != 1: + raise TeamDeliveryAuthorizationError( + "exact user external-write authorization event is absent" + ) + try: + event_grant = AuthorizationGrant.from_dict(events[0].payload["grant"]) + except (KeyError, TypeError, ValueError) as exc: + raise TeamDeliveryAuthorizationError("authorization grant is invalid") from exc + current = [ + grant + for grant in recovery.authorization_grants + if grant.authorization_id == event_grant.authorization_id + ] + if len(current) != 1 or current[0].to_dict() != event_grant.to_dict(): + raise TeamDeliveryAuthorizationError( + "authorization event is not the current workflow grant" + ) + decision = evaluate_authorization_gate( + (AuthorizationKind.EXTERNAL_WRITE,), + (event_grant,), + scope_digest=scope_digest, + risk_digest=risk_digest, + target_digest=target.content_digest, + required_actions={ + AuthorizationKind.EXTERNAL_WRITE: ( + TEAM_DELIVERY_ACTION, + QUALITY_UNVERIFIED_ACTION, + ) + }, + ) + if decision.result is not GateResult.APPROVED: + raise TeamDeliveryAuthorizationError( + "exact team delivery authorization is not approved: " + f"{decision.result.value}" + ) + return { + "workflow_id": workflow_id, + "event_head_digest": recovery.last_event_digest, + "authorization_event_digest": events[0].content_digest, + "authorization_grant_digest": event_grant.content_digest, + "scope_digest": scope_digest, + "risk_digest": risk_digest, + "action_target": target.to_dict(), + "action_target_digest": target.content_digest, + "required_actions": [TEAM_DELIVERY_ACTION, QUALITY_UNVERIFIED_ACTION], + "trust_limit": ( + "local hash-chain evidence detects ordinary drift but is not a " + "signature or same-user adversary boundary" + ), + } + + +def _builder_manifest(path: Path) -> dict[str, Any]: + try: + value = json.loads(path.read_text(encoding="utf-8")) + except (OSError, UnicodeDecodeError, json.JSONDecodeError) as exc: + raise TeamDeliveryIntegrityError(f"cannot read builder manifest: {exc}") from exc + if not isinstance(value, dict): + raise TeamDeliveryIntegrityError("builder manifest must be an object") + return value + + +def _receipt_value(receipt: VerificationReceipt) -> dict[str, Any]: + return {**receipt.to_dict(), "receipt_digest": receipt.receipt_digest} + + +def _manifest_claims( + *, + quality: Mapping[str, Any], + automatic_requested: bool, +) -> dict[str, list[str]]: + verified = { + "behavior-audit-current-package-bytes", + "behavior-audit-current-source-bytes", + "builder-manifest-matches-current-package-bytes", + "runtime-only-allowlist-projection", + } + unverified = {"quality-unverified"} + unverified.update(f"quality:{item}" for item in quality["unverified_claims"]) + blocked = {"host-compatibility"} + blocked.update(f"quality:{item}" for item in quality["blocked_claims"]) + removed = { + "formal-adoption", + "host-activation", + "install", + } + removed.update(f"quality:{item}" for item in quality["removed_claims"]) + if automatic_requested: + blocked.add("automatic-routing") + else: + removed.add("automatic-routing") + return { + "verified": sorted(verified), + "unverified": sorted(unverified), + "blocked": sorted(blocked), + "removed": sorted(removed), + } + + +def _manifest_integration_requests( + *, quality: Mapping[str, Any], automatic_requested: bool +) -> tuple[str, ...]: + requests = set(quality["integration_requests"]) + requests.update( + { + "gate1-frozen-scope-event-adapter", + "real-host-compatibility-adapter", + "shared-cli-and-schema-registry-integration", + } + ) + if automatic_requested: + requests.update( + { + "gate1-automatic-routing-claim-adapter", + "automatic-routing-scope-adapter", + "adjacent-skill-regression-evidence", + "false-trigger-gate-evidence", + "real-host-auto-route-capability", + } + ) + return tuple(sorted(requests)) + + +def _manifest_from_build( + *, + action_target: TeamDeliveryActionTarget, + authority: Mapping[str, Any], + source_report: BehaviorRiskReport, + package_report: BehaviorRiskReport, + plan: _ProjectionPlan, + build_result: Any, + builder_manifest: Mapping[str, Any], + quality: Any, + output: Path, +) -> TeamDeliveryManifest: + receipt = _receipt_value(build_result.verification_receipt) + selected_files = builder_manifest.get("files") + if selected_files != [item.to_dict() for item in plan.selected_files]: + raise TeamDeliveryIntegrityError( + "builder selected bytes differ from the authorized runtime projection" + ) + delivery_id = digest_json( + { + "object_version": TEAM_DELIVERY_ID_OBJECT_VERSION, + "action_target_digest": action_target.content_digest, + "dist_digest": build_result.dist_digest, + } + ) + quality_value = quality.to_dict() + automatic_requested = action_target.automatic_routing_requested + integration_requests = _manifest_integration_requests( + quality=quality_value, + automatic_requested=automatic_requested, + ) + routing_required_evidence: list[str] = [] + if automatic_requested: + routing_required_evidence = [ + "trusted-frozen-automatic-routing-scope", + "routing-cases", + "adjacent-skill-regression", + "false-trigger-gate", + "real-host-auto-route-capability", + ] + body = { + "delivery_id": delivery_id, + "host": action_target.host, + "source_candidate_digest": source_report.candidate_digest, + "source_behavior_report_digest": source_report.content_digest, + "source_risk_commitment_digest": action_target.risk_commitment_digest, + "source_scope_projection_digest": action_target.scope_projection_digest, + "output_root": str(output), + "package_root": str(build_result.dist_root), + "builder_manifest_path": str(build_result.manifest_path), + "team_manifest_path": str(output / action_target.team_manifest_relative_path), + "authority": dict(authority), + "behavior": { + "source_report_digest": source_report.content_digest, + "package_report_digest": package_report.content_digest, + "minimum_risk": source_report.minimum_risk.value, + "package_minimum_risk": package_report.minimum_risk.value, + "mandatory_controls": list(source_report.mandatory_controls), + "mandatory_capabilities": list(source_report.mandatory_capabilities), + "unknowns": list(source_report.unknowns), + "requires_runtime_enforcement": source_report.requires_runtime_enforcement, + "sensitive_material_bundled": source_report.has_sensitive_material, + }, + "projection": { + "runtime_projection_digest": plan.content_digest, + "builder_source_projection_digest": build_result.source_projection_digest, + "selected_files": selected_files, + "excluded_paths": [item.to_dict() for item in plan.excluded_paths], + "source_rechecked_after_build": True, + "package_behavior_audited": True, + }, + "package": { + "dist_digest": build_result.dist_digest, + "builder_manifest_digest": build_result.manifest_digest, + "verification_receipt_digest": build_result.verification_receipt.receipt_digest, + "verification_receipt": receipt, + "file_count": build_result.file_count, + "total_bytes": build_result.total_bytes, + "read_only": ( + build_result.verification_receipt.tree_read_only + and build_result.verification_receipt.manifest_read_only + ), + }, + "claims": _manifest_claims( + quality=quality_value, automatic_requested=automatic_requested + ), + "quality_claim_projection": quality_value, + "compatibility": { + "host": action_target.host, + "status": "unknown", + "evidence_digest": None, + "reason": "real-host-adapter-validation-missing", + }, + "automatic_routing": { + "scope_requested": automatic_requested, + "scope_source_status": "unverified", + "effective": False, + "claim_status": "blocked" if automatic_requested else "removed", + "shared_trigger_derived": False, + "required_evidence": routing_required_evidence, + }, + "change_summary": { + "kind": "isolated-additive-package", + "selected_file_count": len(plan.selected_files), + "excluded_path_count": len(plan.excluded_paths), + "source_mutated": False, + "host_skill_root_mutated": False, + }, + "rollback": { + "install_performed": False, + "host_activation_performed": False, + "host_rollback_required": False, + "package_cleanup": "separate-authorized-action", + }, + "threat_model": { + "same_user_malicious_actor_resistant": False, + "destination_parent_toctou_fully_eliminated": False, + "directory_lock": "advisory-flock-plus-inode-recheck", + "claim_cap": "current-local-structure-and-bytes-only", + "limitations": [ + "local-workflow-hash-chain-is-not-an-identity-signature", + "path-based-builder-cannot-eliminate-malicious-parent-replacement", + "static-audit-does-not-prove-absence-of-all-unknown-secrets", + "no-host-install-activation-routing-or-compatibility-proof", + ], + }, + "integration_requests": integration_requests, + } + unsigned = { + "object_version": TEAM_DELIVERY_MANIFEST_OBJECT_VERSION, + "schema_version": TEAM_DELIVERY_SCHEMA_VERSION, + "delivery_target": "P2_team_package", + "invocation_mode": "explicit_only", + **body, + } + return TeamDeliveryManifest( + **body, + content_digest=digest_json(unsigned), + ) + + +def _team_manifest_bytes(value: Mapping[str, Any]) -> bytes: + return ( + json.dumps(value, ensure_ascii=True, indent=2, sort_keys=True) + "\n" + ).encode("utf-8") + + +def _exclusive_write_manifest(path: Path, value: Mapping[str, Any]) -> None: + data = _team_manifest_bytes(value) + flags = os.O_WRONLY | os.O_CREAT | os.O_EXCL | getattr(os, "O_CLOEXEC", 0) + flags |= getattr(os, "O_NOFOLLOW", 0) + try: + descriptor = os.open(path, flags, 0o444) + except OSError as exc: + raise TeamDeliveryIntegrityError( + f"cannot create team delivery manifest exclusively: {exc}" + ) from exc + try: + view = memoryview(data) + while view: + written = os.write(descriptor, view) + if written <= 0: + raise TeamDeliveryIntegrityError("short write for team delivery manifest") + view = view[written:] + os.fsync(descriptor) + finally: + os.close(descriptor) + fsync_directory(path.parent) + + +def _remove_owned_artifact(path: Path, output: Path) -> None: + if path.parent != output or path.name not in { + "package", + "package.manifest.json", + "team-delivery-manifest.json", + }: + raise TeamDeliveryIntegrityError("refusing to clean an unowned delivery path") + if not os.path.lexists(path): + return + if path.is_symlink() or not path.is_dir(): + try: + path.chmod(stat.S_IRUSR | stat.S_IWUSR, follow_symlinks=False) + except OSError: + pass + path.unlink(missing_ok=True) + return + paths = (path, *tuple(path.rglob("*"))) + for item in reversed(paths): + if item.is_symlink(): + continue + try: + mode = stat.S_IMODE(item.lstat().st_mode) + item.chmod(mode | stat.S_IWUSR | stat.S_IXUSR, follow_symlinks=False) + except OSError: + pass + shutil.rmtree(path) + + +def _artifact_digest(path: Path, kind: str) -> str: + if kind == "tree": + return tree_digest(path) + if kind == "file": + return digest_bytes(path.read_bytes()) + raise ValueError("unsupported owned artifact kind") + + +def _capture_owned_artifact( + path: Path, + output: Path, + output_identity_digest: str, + *, + kind: str, + expected_digest: str, +) -> _OwnedArtifact: + if _output_identity(output) != output_identity_digest: + raise TeamDeliveryIntegrityError( + "output root changed before artifact ownership could be recorded" + ) + if path.parent != output or path.name not in { + "package", + "package.manifest.json", + "team-delivery-manifest.json", + }: + raise TeamDeliveryIntegrityError("artifact path is outside transaction ownership") + try: + metadata = path.lstat() + except OSError as exc: + raise TeamDeliveryIntegrityError( + f"created artifact is unavailable for ownership capture: {path.name}" + ) from exc + if kind == "tree": + if not stat.S_ISDIR(metadata.st_mode) or stat.S_ISLNK(metadata.st_mode): + raise TeamDeliveryIntegrityError("created package is not a real directory") + elif kind == "file": + if ( + not stat.S_ISREG(metadata.st_mode) + or stat.S_ISLNK(metadata.st_mode) + or metadata.st_nlink != 1 + ): + raise TeamDeliveryIntegrityError( + "created control file is not an exclusive regular inode" + ) + else: + raise ValueError("unsupported owned artifact kind") + actual_digest = _artifact_digest(path, kind) + if actual_digest != expected_digest: + raise TeamDeliveryIntegrityError( + f"created artifact digest changed before ownership capture: {path.name}" + ) + return _OwnedArtifact( + path=path, + kind=kind, + output_identity_digest=output_identity_digest, + device=metadata.st_dev, + inode=metadata.st_ino, + digest=actual_digest, + ) + + +def _artifact_is_still_owned(artifact: _OwnedArtifact, output: Path) -> bool: + try: + if _output_identity(output) != artifact.output_identity_digest: + return False + metadata = artifact.path.lstat() + if metadata.st_dev != artifact.device or metadata.st_ino != artifact.inode: + return False + if artifact.kind == "tree": + if not stat.S_ISDIR(metadata.st_mode) or stat.S_ISLNK(metadata.st_mode): + return False + elif ( + not stat.S_ISREG(metadata.st_mode) + or stat.S_ISLNK(metadata.st_mode) + or metadata.st_nlink != 1 + ): + return False + return _artifact_digest(artifact.path, artifact.kind) == artifact.digest + except (OSError, ValueError): + return False + + +def _cleanup_owned_artifacts( + output: Path, + output_identity_digest: str, + artifacts: Iterable[_OwnedArtifact], +) -> None: + try: + if _output_identity(output) != output_identity_digest: + return + except (OSError, UnsafeTeamOutputError): + return + for artifact in reversed(tuple(artifacts)): + if artifact.output_identity_digest != output_identity_digest: + continue + if _artifact_is_still_owned(artifact, output): + _remove_owned_artifact(artifact.path, output) + + +def _validate_manifest_semantics(manifest: TeamDeliveryManifest) -> None: + if manifest.object_version != TEAM_DELIVERY_MANIFEST_OBJECT_VERSION: + raise ValueError("unsupported team delivery manifest object_version") + if manifest.schema_version != TEAM_DELIVERY_SCHEMA_VERSION: + raise ValueError("unsupported team delivery manifest schema_version") + if manifest.delivery_target != "P2_team_package": + raise ValueError("team delivery manifest has the wrong delivery target") + if manifest.host not in _SUPPORTED_HOSTS: + raise ValueError("team delivery manifest has an unsupported host") + if manifest.invocation_mode != "explicit_only": + raise ValueError("standalone team delivery must remain explicit-only") + for field_name in ( + "delivery_id", + "source_candidate_digest", + "source_behavior_report_digest", + "source_risk_commitment_digest", + "source_scope_projection_digest", + "content_digest", + ): + _require_digest(getattr(manifest, field_name), field_name) + output = Path(manifest.output_root) + package = Path(manifest.package_root) + builder_manifest = Path(manifest.builder_manifest_path) + team_manifest = Path(manifest.team_manifest_path) + if not all(path.is_absolute() for path in (output, package, builder_manifest, team_manifest)): + raise ValueError("team delivery paths must be absolute") + if package != output / "package": + raise ValueError("package must be the fixed direct child of output_root") + if builder_manifest != output / "package.manifest.json": + raise ValueError("builder manifest must be the fixed direct child of output_root") + if team_manifest != output / "team-delivery-manifest.json": + raise ValueError("team manifest must be the fixed direct child of output_root") + if output.name.casefold() == "skills" or _contains_host_skill_root_signature(output): + raise ValueError("team delivery output cannot be a host Skill root") + if any(output == root or _is_relative_to(output, root) for root in _known_skill_roots(())): + raise ValueError("team delivery output cannot be inside a host Skill root") + + authority = _require_closed_mapping( + manifest.authority, + { + "workflow_id", + "event_head_digest", + "authorization_event_digest", + "authorization_grant_digest", + "scope_digest", + "risk_digest", + "action_target", + "action_target_digest", + "required_actions", + "trust_limit", + }, + "authority", + ) + target = TeamDeliveryActionTarget.from_dict(authority["action_target"]) + if authority["action_target_digest"] != target.content_digest: + raise ValueError("authority action target digest mismatch") + for field_name in ( + "event_head_digest", + "authorization_event_digest", + "authorization_grant_digest", + "scope_digest", + "risk_digest", + "action_target_digest", + ): + _require_digest(authority[field_name], f"authority.{field_name}") + if authority["event_head_digest"] != authority["authorization_event_digest"]: + raise ValueError("authorization event must remain the manifest event head") + if authority["scope_digest"] != target.scope_digest: + raise ValueError("authority scope differs from action target") + if authority["risk_digest"] != target.risk_commitment_digest: + raise ValueError("authority risk differs from action target") + if authority["required_actions"] != [TEAM_DELIVERY_ACTION, QUALITY_UNVERIFIED_ACTION]: + raise ValueError("authority required actions are not closed") + if target.output_root != manifest.output_root or target.host != manifest.host: + raise ValueError("action target differs from delivery paths or host") + if target.candidate_digest != manifest.source_candidate_digest: + raise ValueError("action target candidate digest mismatch") + if target.behavior_report_digest != manifest.source_behavior_report_digest: + raise ValueError("action target behavior report mismatch") + if target.risk_commitment_digest != manifest.source_risk_commitment_digest: + raise ValueError("action target risk commitment mismatch") + if target.scope_projection_digest != manifest.source_scope_projection_digest: + raise ValueError("action target scope projection mismatch") + if target.scope_projection_digest != digest_json(_standalone_scope_projection()): + raise ValueError("action target does not use the fixed standalone scope projection") + + recovered_authority = _recover_authority( + host=manifest.host, + workflow_id=authority["workflow_id"], + scope_digest=authority["scope_digest"], + risk_digest=authority["risk_digest"], + target=target, + ) + if dict(authority) != recovered_authority: + raise ValueError("manifest authority is not the current fixed workflow authority") + + behavior = _require_closed_mapping( + manifest.behavior, + { + "source_report_digest", + "package_report_digest", + "minimum_risk", + "package_minimum_risk", + "mandatory_controls", + "mandatory_capabilities", + "unknowns", + "requires_runtime_enforcement", + "sensitive_material_bundled", + }, + "behavior", + ) + if behavior["source_report_digest"] != manifest.source_behavior_report_digest: + raise ValueError("behavior source report digest mismatch") + _require_digest(behavior["package_report_digest"], "behavior.package_report_digest") + source_risk = RiskLevel(behavior["minimum_risk"]) + package_risk = RiskLevel(behavior["package_minimum_risk"]) + if package_risk.severity > source_risk.severity: + raise ValueError("package risk cannot exceed the audited source without blocking") + controls = _closed_strings( + behavior["mandatory_controls"], "behavior.mandatory_controls" + ) + capabilities = _closed_strings( + behavior["mandatory_capabilities"], "behavior.mandatory_capabilities" + ) + unknowns = _closed_strings(behavior["unknowns"], "behavior.unknowns") + if behavior["requires_runtime_enforcement"] is not False: + raise ValueError("standalone P2 cannot claim unresolved runtime enforcement") + if behavior["sensitive_material_bundled"] is not False: + raise ValueError("team package cannot contain sensitive material") + rebuilt_risk_commitment = digest_json( + { + "object_version": "skill-optimizer.behavior-risk-commitment/1", + "behavior_report_digest": manifest.source_behavior_report_digest, + "candidate_digest": manifest.source_candidate_digest, + "minimum_risk": source_risk.value, + "mandatory_controls": list(controls), + "mandatory_capabilities": list(capabilities), + "unknowns": list(unknowns), + "requires_runtime_enforcement": behavior[ + "requires_runtime_enforcement" + ], + "sensitive_material_bundled": behavior[ + "sensitive_material_bundled" + ], + } + ) + if rebuilt_risk_commitment != target.risk_commitment_digest: + raise ValueError("manifest behavior projection differs from authorized risk facts") + + projection = _require_closed_mapping( + manifest.projection, + { + "runtime_projection_digest", + "builder_source_projection_digest", + "selected_files", + "excluded_paths", + "source_rechecked_after_build", + "package_behavior_audited", + }, + "projection", + ) + for field_name in ("runtime_projection_digest", "builder_source_projection_digest"): + _require_digest(projection[field_name], f"projection.{field_name}") + if projection["runtime_projection_digest"] != target.runtime_projection_digest: + raise ValueError("runtime projection differs from the authorized target") + if projection["source_rechecked_after_build"] is not True: + raise ValueError("source bytes were not rechecked after projection") + if projection["package_behavior_audited"] is not True: + raise ValueError("package behavior was not audited") + selected = _validate_selected_files(projection["selected_files"]) + excluded = _validate_exclusions(projection["excluded_paths"]) + rebuilt_projection = _ProjectionPlan(selected, excluded) + if rebuilt_projection.content_digest != projection["runtime_projection_digest"]: + raise ValueError("runtime projection digest mismatch") + + package_value = _require_closed_mapping( + manifest.package, + { + "dist_digest", + "builder_manifest_digest", + "verification_receipt_digest", + "verification_receipt", + "file_count", + "total_bytes", + "read_only", + }, + "package", + ) + for field_name in ( + "dist_digest", + "builder_manifest_digest", + "verification_receipt_digest", + ): + _require_digest(package_value[field_name], f"package.{field_name}") + receipt = package_value["verification_receipt"] + receipt_expected = { + "object_version", + "dist_root", + "manifest_path", + "dist_digest", + "manifest_digest", + "tree_digest", + "manifest_schema_version", + "source_projection_digest", + "file_count", + "total_bytes", + "tree_read_only", + "manifest_read_only", + "receipt_digest", + } + _require_closed_mapping(receipt, receipt_expected, "package.verification_receipt") + if receipt["dist_root"] != manifest.package_root: + raise ValueError("package receipt dist_root mismatch") + if receipt["manifest_path"] != manifest.builder_manifest_path: + raise ValueError("package receipt manifest_path mismatch") + if receipt["dist_digest"] != package_value["dist_digest"]: + raise ValueError("package receipt dist digest mismatch") + if receipt["manifest_digest"] != package_value["builder_manifest_digest"]: + raise ValueError("package receipt manifest digest mismatch") + receipt_digest = digest_json({key: receipt[key] for key in receipt if key != "receipt_digest"}) + if receipt["receipt_digest"] != receipt_digest: + raise ValueError("package verification receipt digest mismatch") + if package_value["verification_receipt_digest"] != receipt_digest: + raise ValueError("package receipt binding mismatch") + if receipt["file_count"] != len(selected) or package_value["file_count"] != len(selected): + raise ValueError("package file count mismatch") + if package_value["total_bytes"] != sum(item.size for item in selected): + raise ValueError("package total bytes mismatch") + if package_value["read_only"] is not True: + raise ValueError("team package must be read-only") + + quality = _validate_quality_projection(manifest.quality_claim_projection) + if quality["candidate_digest"] != manifest.source_candidate_digest: + raise ValueError("quality projection candidate digest mismatch") + if quality["content_digest"] != target.quality_projection_digest: + raise ValueError("quality projection differs from the authorized target") + + claims = _require_closed_mapping( + manifest.claims, + {"verified", "unverified", "blocked", "removed"}, + "claims", + ) + claim_sets = { + name: set(_claim_strings(claims[name], f"claims.{name}")) + for name in ("verified", "unverified", "blocked", "removed") + } + names = tuple(claim_sets) + if any( + claim_sets[left] & claim_sets[right] + for index, left in enumerate(names) + for right in names[index + 1 :] + ): + raise ValueError("claim states must not overlap") + expected_claims = _manifest_claims( + quality=quality, + automatic_requested=target.automatic_routing_requested, + ) + if dict(claims) != expected_claims: + raise ValueError("manifest claim states are not the exact derived projection") + compatibility = _require_closed_mapping( + manifest.compatibility, + {"host", "status", "evidence_digest", "reason"}, + "compatibility", + ) + if compatibility != { + "host": manifest.host, + "status": "unknown", + "evidence_digest": None, + "reason": "real-host-adapter-validation-missing", + }: + raise ValueError("compatibility must remain unknown without a real adapter") + + routing = _require_closed_mapping( + manifest.automatic_routing, + { + "scope_requested", + "scope_source_status", + "effective", + "claim_status", + "shared_trigger_derived", + "required_evidence", + }, + "automatic_routing", + ) + if routing["scope_requested"] != target.automatic_routing_requested: + raise ValueError("automatic routing scope differs from the target") + if routing["scope_source_status"] != "unverified" or routing["effective"] is not False: + raise ValueError("standalone automatic routing cannot be effective") + if routing["shared_trigger_derived"] is not False: + raise ValueError("standalone P2 cannot derive shared-trigger risk") + expected_status = "blocked" if routing["scope_requested"] else "removed" + if routing["claim_status"] != expected_status: + raise ValueError("automatic routing claim status is invalid") + required_routing_evidence = _closed_strings( + routing["required_evidence"], "automatic_routing.required_evidence" + ) + if bool(required_routing_evidence) != bool(routing["scope_requested"]): + raise ValueError("automatic routing evidence requirements are incomplete") + expected_requests = _manifest_integration_requests( + quality=quality, + automatic_requested=target.automatic_routing_requested, + ) + if manifest.integration_requests != expected_requests: + raise ValueError("manifest integration requests are not the exact derived set") + + change = _require_closed_mapping( + manifest.change_summary, + { + "kind", + "selected_file_count", + "excluded_path_count", + "source_mutated", + "host_skill_root_mutated", + }, + "change_summary", + ) + if change["kind"] != "isolated-additive-package": + raise ValueError("team delivery change kind is invalid") + if change["selected_file_count"] != len(selected): + raise ValueError("selected file change count mismatch") + if change["excluded_path_count"] != len(excluded): + raise ValueError("excluded path change count mismatch") + if change["source_mutated"] is not False or change["host_skill_root_mutated"] is not False: + raise ValueError("P2 cannot claim source or host root mutation") + + rollback = _require_closed_mapping( + manifest.rollback, + { + "install_performed", + "host_activation_performed", + "host_rollback_required", + "package_cleanup", + }, + "rollback", + ) + if rollback != { + "install_performed": False, + "host_activation_performed": False, + "host_rollback_required": False, + "package_cleanup": "separate-authorized-action", + }: + raise ValueError("team delivery rollback semantics are invalid") + + threat = _require_closed_mapping( + manifest.threat_model, + { + "same_user_malicious_actor_resistant", + "destination_parent_toctou_fully_eliminated", + "directory_lock", + "claim_cap", + "limitations", + }, + "threat_model", + ) + if threat["same_user_malicious_actor_resistant"] is not False: + raise ValueError("local workflow evidence cannot claim same-user resistance") + if threat["destination_parent_toctou_fully_eliminated"] is not False: + raise ValueError("path-based builder cannot claim complete TOCTOU elimination") + _closed_strings(threat["limitations"], "threat_model.limitations") + if manifest.content_digest != digest_json(manifest.body()): + raise ValueError("team delivery manifest content_digest mismatch") + expected_id = digest_json( + { + "object_version": TEAM_DELIVERY_ID_OBJECT_VERSION, + "action_target_digest": target.content_digest, + "dist_digest": package_value["dist_digest"], + } + ) + if manifest.delivery_id != expected_id: + raise ValueError("team delivery ID mismatch") + + +def _validate_selected_files(value: object) -> tuple[ProjectionEntry, ...]: + if isinstance(value, (str, bytes)) or not isinstance(value, Sequence) or not value: + raise ValueError("projection.selected_files must be a non-empty array") + result: list[ProjectionEntry] = [] + previous: str | None = None + for raw in value: + _require_closed_mapping(raw, {"path", "size", "digest", "executable"}, "selected file") + path = raw["path"] + if not isinstance(path, str) or not path or path.startswith("/") or "\\" in path: + raise ValueError("selected file path is unsafe") + if any(part in {"", ".", ".."} for part in path.split("/")): + raise ValueError("selected file path is unsafe") + if previous is not None and path <= previous: + raise ValueError("selected files must be unique and sorted") + size = raw["size"] + if isinstance(size, bool) or not isinstance(size, int) or size < 0: + raise ValueError("selected file size is invalid") + _require_digest(raw["digest"], "selected file digest") + if not isinstance(raw["executable"], bool): + raise ValueError("selected file executable must be boolean") + result.append(ProjectionEntry(path, size, raw["digest"], raw["executable"])) + previous = path + if not any(item.path == "SKILL.md" for item in result): + raise ValueError("selected files must contain SKILL.md") + return tuple(result) + + +def _validate_exclusions(value: object) -> tuple[ProjectionExclusion, ...]: + if isinstance(value, (str, bytes)) or not isinstance(value, Sequence): + raise ValueError("projection.excluded_paths must be an array") + result: list[ProjectionExclusion] = [] + previous: str | None = None + allowed_reasons = { + "development_material", + "test_material", + "development_trace_or_cache", + "host_specific_not_runtime", + "not_runtime_allowlisted", + } + for raw in value: + _require_closed_mapping(raw, {"path", "reason"}, "excluded path") + path = raw["path"] + reason = raw["reason"] + if not isinstance(path, str) or not path or path.startswith("/") or "\\" in path: + raise ValueError("excluded path is unsafe") + if any(part in {"", ".", ".."} for part in path.split("/")): + raise ValueError("excluded path is unsafe") + if reason not in allowed_reasons: + raise ValueError("excluded path reason is invalid") + if previous is not None and path <= previous: + raise ValueError("excluded paths must be unique and sorted") + result.append(ProjectionExclusion(path, reason)) + previous = path + return tuple(result) + + +def _validate_quality_projection(value: object) -> Mapping[str, Any]: + expected = { + "object_version", + "candidate_digest", + "verified_claims", + "unverified_claims", + "blocked_claims", + "removed_claims", + "integration_requests", + "content_digest", + } + quality = _require_closed_mapping(value, expected, "quality_claim_projection") + if quality["object_version"] != "skill-optimizer.quality-projection/1": + raise ValueError("unsupported quality projection object_version") + _require_digest(quality["candidate_digest"], "quality candidate_digest") + if quality["verified_claims"] != []: + raise ValueError("standalone P2 cannot contain verified quality claims") + states = { + name: set(_claim_strings(quality[name], f"quality.{name}")) + for name in ("unverified_claims", "blocked_claims", "removed_claims") + } + names = tuple(states) + if any( + states[left] & states[right] + for index, left in enumerate(names) + for right in names[index + 1 :] + ): + raise ValueError("quality claim states overlap") + _closed_strings(quality["integration_requests"], "quality.integration_requests") + expected_digest = digest_json( + {key: quality[key] for key in quality if key != "content_digest"} + ) + if quality["content_digest"] != expected_digest: + raise ValueError("quality projection content_digest mismatch") + return quality + + +def _require_safe_control_file(path: Path, field_name: str) -> None: + metadata = path.lstat() + if ( + not stat.S_ISREG(metadata.st_mode) + or stat.S_ISLNK(metadata.st_mode) + or metadata.st_nlink != 1 + ): + raise TeamDeliveryIntegrityError( + f"{field_name} must be one non-linked regular inode" + ) + if metadata.st_uid != os.geteuid(): + raise TeamDeliveryIntegrityError( + f"{field_name} must be owned by the current user" + ) + if metadata.st_mode & (stat.S_IWGRP | stat.S_IWOTH): + raise TeamDeliveryIntegrityError( + f"{field_name} cannot be group- or world-writable" + ) + + +def _reject_hardlinked_package_files(package_root: Path) -> None: + for path in sorted(package_root.rglob("*")): + metadata = path.lstat() + if stat.S_ISREG(metadata.st_mode) and metadata.st_nlink != 1: + raise TeamDeliveryIntegrityError( + "team package contains a hard-linked regular file" + ) + + +def validate_team_delivery_manifest( + value: TeamDeliveryManifest | Mapping[str, Any], + *, + package_root: str | os.PathLike[str] | None = None, + builder_manifest_path: str | os.PathLike[str] | None = None, +) -> TeamDeliveryManifest: + """Rebuild manifest semantics and optionally verify current package bytes.""" + + manifest = ( + value + if isinstance(value, TeamDeliveryManifest) + else TeamDeliveryManifest.from_dict(value) + ) + _validate_manifest_semantics(manifest) + if package_root is None and builder_manifest_path is None: + package_root = manifest.package_root + builder_manifest_path = manifest.builder_manifest_path + elif package_root is None or builder_manifest_path is None: + raise ValueError("package_root and builder_manifest_path must be supplied together") + try: + package_input = Path(package_root).expanduser() + builder_input = Path(builder_manifest_path).expanduser() + team_input = Path(manifest.team_manifest_path).expanduser() + if package_input.is_symlink() or builder_input.is_symlink() or team_input.is_symlink(): + raise TeamDeliveryIntegrityError( + "team delivery verification paths cannot be symbolic links" + ) + package_path = package_input.resolve(strict=True) + builder_path = builder_input.resolve(strict=True) + team_path = team_input.resolve(strict=True) + output_path = Path(manifest.output_root).expanduser().resolve(strict=True) + _require_owned_unwritable_directory( + output_path.parent, "team output parent" + ) + _require_owned_unwritable_directory(output_path, "team output root") + if ( + str(package_path) != manifest.package_root + or str(builder_path) != manifest.builder_manifest_path + or str(team_path) != manifest.team_manifest_path + ): + raise TeamDeliveryIntegrityError( + "manifest verification paths differ from the receipt" + ) + expected_output_entries = {package_path, builder_path, team_path} + if set(output_path.iterdir()) != expected_output_entries: + raise TeamDeliveryIntegrityError( + "team output root no longer contains exactly its three artifacts" + ) + _require_safe_control_file(builder_path, "builder manifest") + _require_safe_control_file(team_path, "team delivery manifest") + _reject_hardlinked_package_files(package_path) + target = TeamDeliveryActionTarget.from_dict(manifest.authority["action_target"]) + if _output_identity(output_path) != target.output_root_identity_digest: + raise TeamDeliveryIntegrityError("team output root identity changed") + if stat.S_IMODE(team_path.lstat().st_mode) & ( + stat.S_IWUSR | stat.S_IWGRP | stat.S_IWOTH + ): + raise TeamDeliveryIntegrityError("team delivery manifest is writable") + persisted = json.loads(team_path.read_text(encoding="utf-8")) + if persisted != manifest.to_dict(): + raise TeamDeliveryIntegrityError( + "persisted team manifest differs from supplied manifest" + ) + receipt = verify_distribution( + package_path, + builder_path, + expected_dist_digest=manifest.package["dist_digest"], + expected_manifest_digest=manifest.package["builder_manifest_digest"], + ) + if _receipt_value(receipt) != manifest.package["verification_receipt"]: + raise TeamDeliveryIntegrityError( + "current package verification receipt differs from team manifest" + ) + builder = _builder_manifest(builder_path) + if builder.get("files") != manifest.projection["selected_files"]: + raise TeamDeliveryIntegrityError( + "current builder manifest files differ from team manifest" + ) + if builder.get("source_projection_digest") != manifest.projection[ + "builder_source_projection_digest" + ]: + raise TeamDeliveryIntegrityError("builder source projection digest mismatch") + package_report = audit_behavior_risk(package_path) + validate_behavior_risk_report(package_report) + if package_report.candidate_digest != manifest.package["dist_digest"]: + raise TeamDeliveryIntegrityError( + "current package behavior audit does not bind the distribution" + ) + if package_report.content_digest != manifest.behavior["package_report_digest"]: + raise TeamDeliveryIntegrityError( + "current package behavior report differs from team manifest" + ) + if package_report.minimum_risk.value != manifest.behavior[ + "package_minimum_risk" + ]: + raise TeamDeliveryIntegrityError( + "current package risk projection differs from team manifest" + ) + package_plan = _projection_plan( + package_path, + manifest.host, + max_file_bytes=max( + DEFAULT_MAX_FILE_BYTES, + max(item["size"] for item in manifest.projection["selected_files"]), + ), + max_total_bytes=max( + DEFAULT_MAX_TOTAL_BYTES, + manifest.package["total_bytes"], + ), + ) + _behavior_preconditions(package_report, package_plan) + if package_plan.excluded_paths: + raise TeamDeliveryIntegrityError( + "current package contains content outside the runtime allowlist" + ) + if [item.to_dict() for item in package_plan.selected_files] != manifest.projection[ + "selected_files" + ]: + raise TeamDeliveryIntegrityError( + "current runtime projection differs from team manifest" + ) + return manifest + except TeamDeliveryIntegrityError: + raise + except Exception as exc: + raise TeamDeliveryIntegrityError( + f"team delivery current-byte verification failed: {exc}" + ) from exc + + +def build_team_delivery( + candidate_root: str | os.PathLike[str], + output_root: str | os.PathLike[str], + *, + host: str, + workflow_id: str, + scope_digest: str, + risk_digest: str, + quality_summary: Mapping[str, Any] | None = None, + expected_candidate_digest: str | None = None, + official_skill_roots: Iterable[str | os.PathLike[str]] = (), + max_file_bytes: int = DEFAULT_MAX_FILE_BYTES, + max_total_bytes: int = DEFAULT_MAX_TOTAL_BYTES, +) -> TeamDeliveryManifest: + """Build one isolated P2 package and return its current-byte manifest. + + The destination must be a pre-existing empty directory outside every known + host Skill root. No installation, activation, automatic-routing mutation, + or host compatibility assertion occurs. + """ + + candidate = _candidate_root(candidate_root) + frozen_official_roots = tuple(official_skill_roots) + output = _validate_output_root( + output_root, + candidate, + official_skill_roots=frozen_official_roots, + ) + normalized_scope = _standalone_scope_projection() + _require_digest(risk_digest, "risk_digest") + if expected_candidate_digest is not None: + _require_digest(expected_candidate_digest, "expected_candidate_digest") + + package_root = output / "package" + builder_manifest_path = output / "package.manifest.json" + team_manifest_path = output / "team-delivery-manifest.json" + + with _locked_output_root(output) as locked_identity: + _validate_output_root( + output, + candidate, + official_skill_roots=frozen_official_roots, + ) + source_report = audit_behavior_risk(candidate) + if ( + expected_candidate_digest is not None + and source_report.candidate_digest != expected_candidate_digest + ): + raise TeamDeliveryIntegrityError("candidate digest differs from expectation") + plan = _projection_plan( + candidate, + host, + max_file_bytes=max_file_bytes, + max_total_bytes=max_total_bytes, + ) + _behavior_preconditions(source_report, plan) + quality = _quality_projection(source_report, quality_summary) + action_target = _derive_action_target( + report=source_report, + plan=plan, + output=output, + host=host, + scope_digest=scope_digest, + normalized_scope=normalized_scope, + quality_projection_digest=quality.content_digest, + ) + if action_target.output_root_identity_digest != locked_identity: + raise UnsafeTeamOutputError("output root changed before authorization check") + authority = _recover_authority( + host=host, + workflow_id=workflow_id, + scope_digest=scope_digest, + risk_digest=risk_digest, + target=action_target, + ) + + # Re-run the complete current-byte input closure immediately before the + # builder snapshots files. This prevents a stale preparation report or + # caller-provided lower-risk report from becoming the package authority. + current_report = audit_behavior_risk(candidate) + current_plan = _projection_plan( + candidate, + host, + max_file_bytes=max_file_bytes, + max_total_bytes=max_total_bytes, + ) + _behavior_preconditions(current_report, current_plan) + if ( + current_report.content_digest != source_report.content_digest + or current_report.candidate_digest != source_report.candidate_digest + or current_plan.content_digest != plan.content_digest + ): + raise TeamDeliveryIntegrityError( + "candidate changed between authorization and package projection" + ) + + owned_artifacts: list[_OwnedArtifact] = [] + try: + build_result = build_distribution( + candidate, + package_root, + whitelist=plan.whitelist, + manifest_path=builder_manifest_path, + max_file_bytes=max_file_bytes, + max_total_bytes=max_total_bytes, + ) + owned_artifacts.append( + _capture_owned_artifact( + package_root, + output, + locked_identity, + kind="tree", + expected_digest=build_result.dist_digest, + ) + ) + owned_artifacts.append( + _capture_owned_artifact( + builder_manifest_path, + output, + locked_identity, + kind="file", + expected_digest=build_result.manifest_digest, + ) + ) + builder = _builder_manifest(builder_manifest_path) + if builder.get("files") != [item.to_dict() for item in plan.selected_files]: + raise TeamDeliveryIntegrityError( + "builder package differs from the pre-authorized projection" + ) + source_after = audit_behavior_risk(candidate) + if ( + source_after.candidate_digest != source_report.candidate_digest + or source_after.content_digest != source_report.content_digest + ): + raise TeamDeliveryIntegrityError( + "candidate changed while the team package was built" + ) + package_report = audit_behavior_risk(package_root) + validate_behavior_risk_report(package_report) + if package_report.candidate_digest != build_result.dist_digest: + raise TeamDeliveryIntegrityError( + "post-projection behavior audit does not bind package bytes" + ) + if package_report.has_sensitive_material or package_report.has_unknowns: + raise SensitiveMaterialError( + "post-projection behavior audit is sensitive or unresolved" + ) + if package_report.minimum_risk.severity > source_report.minimum_risk.severity: + raise TeamDeliveryIntegrityError( + "post-projection behavior risk exceeds the source report" + ) + package_plan = _projection_plan( + package_root, + host, + max_file_bytes=max_file_bytes, + max_total_bytes=max_total_bytes, + ) + _behavior_preconditions(package_report, package_plan) + if package_plan.selected_files != plan.selected_files or package_plan.excluded_paths: + raise TeamDeliveryIntegrityError( + "post-projection runtime allowlist does not close over package bytes" + ) + verification = verify_distribution( + package_root, + builder_manifest_path, + expected_dist_digest=build_result.dist_digest, + expected_manifest_digest=build_result.manifest_digest, + ) + if verification.receipt_digest != build_result.verification_receipt.receipt_digest: + raise TeamDeliveryIntegrityError( + "package verification receipt changed after behavior audit" + ) + manifest = _manifest_from_build( + action_target=action_target, + authority=authority, + source_report=source_report, + package_report=package_report, + plan=plan, + build_result=build_result, + builder_manifest=builder, + quality=quality, + output=output, + ) + _exclusive_write_manifest(team_manifest_path, manifest.to_dict()) + owned_artifacts.append( + _capture_owned_artifact( + team_manifest_path, + output, + locked_identity, + kind="file", + expected_digest=digest_bytes( + _team_manifest_bytes(manifest.to_dict()) + ), + ) + ) + persisted = json.loads(team_manifest_path.read_text(encoding="utf-8")) + validate_team_delivery_manifest( + persisted, + package_root=package_root, + builder_manifest_path=builder_manifest_path, + ) + allowed = {package_root, builder_manifest_path, team_manifest_path} + if set(output.iterdir()) != allowed: + raise TeamDeliveryIntegrityError( + "unexpected concurrent content appeared in team output root" + ) + return manifest + except Exception: + _cleanup_owned_artifacts(output, locked_identity, owned_artifacts) + raise + + +__all__ = [ + "QUALITY_UNVERIFIED_ACTION", + "SensitiveMaterialError", + "TEAM_DELIVERY_ACTION", + "TEAM_DELIVERY_ACTION_TARGET_OBJECT_VERSION", + "TEAM_DELIVERY_MANIFEST_OBJECT_VERSION", + "TEAM_DELIVERY_SCHEMA_VERSION", + "TeamDeliveryActionTarget", + "TeamDeliveryAuthorizationError", + "TeamDeliveryError", + "TeamDeliveryIntegrationRequired", + "TeamDeliveryIntegrityError", + "TeamDeliveryManifest", + "UnsafeTeamOutputError", + "build_team_delivery", + "prepare_team_delivery_action_target", + "validate_team_delivery_manifest", +] diff --git a/runtime/skill-optimizer/scripts/schemas/behavior-risk-report.schema.json b/runtime/skill-optimizer/scripts/schemas/behavior-risk-report.schema.json new file mode 100644 index 0000000..a025643 --- /dev/null +++ b/runtime/skill-optimizer/scripts/schemas/behavior-risk-report.schema.json @@ -0,0 +1,142 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://skill-optimizer.local/schemas/behavior-risk-report.schema.json", + "title": "Current-byte Skill behavior risk report", + "type": "object", + "additionalProperties": false, + "required": [ + "schema_version", + "candidate_digest", + "findings", + "unknowns", + "minimum_risk", + "mandatory_controls", + "mandatory_capabilities", + "scanned_files", + "scanned_bytes", + "threat_model", + "content_digest" + ], + "properties": { + "schema_version": {"const": "1.0.0"}, + "candidate_digest": {"$ref": "#/$defs/digest"}, + "findings": { + "type": "array", + "items": {"$ref": "#/$defs/finding"}, + "uniqueItems": true + }, + "unknowns": { + "type": "array", + "items": {"$ref": "#/$defs/findingId"}, + "uniqueItems": true + }, + "minimum_risk": {"enum": ["R0", "R1", "R2", "R3"]}, + "mandatory_controls": { + "type": "array", + "items": {"type": "string", "minLength": 1}, + "uniqueItems": true + }, + "mandatory_capabilities": { + "type": "array", + "items": {"type": "string", "minLength": 1}, + "uniqueItems": true + }, + "scanned_files": {"type": "integer", "minimum": 1}, + "scanned_bytes": {"type": "integer", "minimum": 0}, + "threat_model": { + "type": "array", + "minItems": 1, + "items": {"type": "string", "minLength": 1}, + "uniqueItems": true + }, + "content_digest": {"$ref": "#/$defs/digest"} + }, + "$defs": { + "digest": { + "type": "string", + "pattern": "^sha256:[0-9a-f]{64}$" + }, + "findingId": { + "type": "string", + "pattern": "^behavior-[0-9a-f]{32}$" + }, + "finding": { + "type": "object", + "additionalProperties": false, + "required": [ + "finding_id", + "dimension", + "level", + "evidence_state", + "requires_runtime_enforcement", + "sensitive_material_bundled", + "path", + "line", + "rule_id", + "content_digest" + ], + "properties": { + "finding_id": {"$ref": "#/$defs/findingId"}, + "dimension": { + "enum": [ + "local_mutation", + "external_write", + "irreversible_change", + "credential_or_sensitive_data", + "shared_trigger", + "real_external_dependency", + "high_impact_evaluation" + ] + }, + "level": {"enum": ["R0", "R1", "R2", "R3"]}, + "evidence_state": { + "enum": [ + "observed", + "inferred", + "unknown", + "requires_runtime_enforcement" + ] + }, + "requires_runtime_enforcement": {"type": "boolean"}, + "sensitive_material_bundled": {"type": "boolean"}, + "path": { + "type": "string", + "minLength": 1, + "pattern": "^(?!/)(?!.*(?:^|/)\\.\\.(?:/|$))[^\\\\\\u0000]+$" + }, + "line": {"type": "integer", "minimum": 1}, + "rule_id": { + "type": "string", + "pattern": "^[a-z][a-z0-9-]*(?:\\.[a-z][a-z0-9-]*)+$" + }, + "content_digest": {"$ref": "#/$defs/digest"} + }, + "allOf": [ + { + "if": { + "properties": { + "evidence_state": {"const": "requires_runtime_enforcement"} + }, + "required": ["evidence_state"] + }, + "then": { + "properties": { + "requires_runtime_enforcement": {"const": true} + } + } + }, + { + "if": { + "properties": {"sensitive_material_bundled": {"const": true}}, + "required": ["sensitive_material_bundled"] + }, + "then": { + "properties": { + "dimension": {"const": "credential_or_sensitive_data"} + } + } + } + ] + } + } +} diff --git a/runtime/skill-optimizer/scripts/schemas/personal-install-receipt.schema.json b/runtime/skill-optimizer/scripts/schemas/personal-install-receipt.schema.json new file mode 100644 index 0000000..05f1586 --- /dev/null +++ b/runtime/skill-optimizer/scripts/schemas/personal-install-receipt.schema.json @@ -0,0 +1,165 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://skill-optimizer.local/schemas/personal-install-receipt.schema.json", + "title": "P1 personal install or rollback receipt", + "oneOf": [ + {"$ref": "#/$defs/installReceipt"}, + {"$ref": "#/$defs/rollbackReceipt"} + ], + "$defs": { + "digest": { + "type": "string", + "pattern": "^sha256:[0-9a-f]{64}$" + }, + "nullableDigest": { + "anyOf": [ + {"$ref": "#/$defs/digest"}, + {"type": "null"} + ] + }, + "transactionId": { + "type": "string", + "pattern": "^pi-[0-9a-f]{64}$" + }, + "absolutePath": { + "type": "string", + "pattern": "^/.+$", + "minLength": 2 + }, + "limitations": { + "type": "array", + "const": [ + "no-host-activation-proof", + "no-automatic-routing-claim", + "no-crash-recovery", + "no-p3-durable-journal", + "quality-unverified", + "same-user-malicious-actor-out-of-scope" + ] + }, + "installReceipt": { + "type": "object", + "additionalProperties": false, + "required": [ + "object_version", + "receipt_kind", + "transaction_id", + "status", + "personal_root", + "target", + "candidate_digest", + "staged_digest", + "behavior_report_digest", + "minimum_risk", + "scope_digest", + "risk_commitment_digest", + "target_observation_digest", + "authorization_target_digest", + "decision_event_digest", + "authorization_event_digest", + "workflow_event_head_digest", + "authorization_grant_digest", + "expected_target_digest", + "pre_target_digest", + "post_install_digest", + "backup_digest", + "backup_present", + "journal_digest", + "quality_claim_status", + "limitations", + "receipt_digest" + ], + "properties": { + "object_version": { + "const": "skill-optimizer.personal-install-receipt/1" + }, + "receipt_kind": {"const": "install"}, + "transaction_id": {"$ref": "#/$defs/transactionId"}, + "status": {"const": "installed"}, + "personal_root": {"$ref": "#/$defs/absolutePath"}, + "target": {"$ref": "#/$defs/absolutePath"}, + "candidate_digest": {"$ref": "#/$defs/digest"}, + "staged_digest": {"$ref": "#/$defs/digest"}, + "behavior_report_digest": {"$ref": "#/$defs/digest"}, + "minimum_risk": {"enum": ["R0", "R1"]}, + "scope_digest": {"$ref": "#/$defs/digest"}, + "risk_commitment_digest": {"$ref": "#/$defs/digest"}, + "target_observation_digest": {"$ref": "#/$defs/digest"}, + "authorization_target_digest": {"$ref": "#/$defs/digest"}, + "decision_event_digest": {"$ref": "#/$defs/digest"}, + "authorization_event_digest": {"$ref": "#/$defs/digest"}, + "workflow_event_head_digest": {"$ref": "#/$defs/digest"}, + "authorization_grant_digest": {"$ref": "#/$defs/digest"}, + "expected_target_digest": {"$ref": "#/$defs/nullableDigest"}, + "pre_target_digest": {"$ref": "#/$defs/nullableDigest"}, + "post_install_digest": {"$ref": "#/$defs/digest"}, + "backup_digest": {"$ref": "#/$defs/nullableDigest"}, + "backup_present": {"type": "boolean"}, + "journal_digest": {"$ref": "#/$defs/digest"}, + "quality_claim_status": {"const": "unverified"}, + "limitations": {"$ref": "#/$defs/limitations"}, + "receipt_digest": {"$ref": "#/$defs/digest"} + }, + "allOf": [ + { + "if": { + "properties": { + "pre_target_digest": {"type": "null"} + }, + "required": ["pre_target_digest"] + }, + "then": { + "properties": { + "expected_target_digest": {"type": "null"}, + "backup_digest": {"type": "null"}, + "backup_present": {"const": false} + } + }, + "else": { + "properties": { + "expected_target_digest": {"$ref": "#/$defs/digest"}, + "backup_digest": {"$ref": "#/$defs/digest"}, + "backup_present": {"const": true} + } + } + } + ] + }, + "rollbackReceipt": { + "type": "object", + "additionalProperties": false, + "required": [ + "object_version", + "receipt_kind", + "transaction_id", + "status", + "personal_root", + "target", + "installed_digest", + "quarantine_digest", + "restored_digest", + "journal_digest", + "quality_claim_status", + "limitations", + "receipt_digest" + ], + "properties": { + "object_version": { + "const": "skill-optimizer.personal-rollback-receipt/1" + }, + "receipt_kind": {"const": "rollback"}, + "transaction_id": {"$ref": "#/$defs/transactionId"}, + "status": {"const": "rolled_back"}, + "personal_root": {"$ref": "#/$defs/absolutePath"}, + "target": {"$ref": "#/$defs/absolutePath"}, + "installed_digest": {"$ref": "#/$defs/digest"}, + "quarantine_digest": {"$ref": "#/$defs/digest"}, + "restored_digest": {"$ref": "#/$defs/nullableDigest"}, + "journal_digest": {"$ref": "#/$defs/digest"}, + "quality_claim_status": {"const": "unverified"}, + "limitations": {"$ref": "#/$defs/limitations"}, + "receipt_digest": {"$ref": "#/$defs/digest"} + } + } + } +} diff --git a/runtime/skill-optimizer/scripts/schemas/team-delivery-manifest.schema.json b/runtime/skill-optimizer/scripts/schemas/team-delivery-manifest.schema.json new file mode 100644 index 0000000..687130a --- /dev/null +++ b/runtime/skill-optimizer/scripts/schemas/team-delivery-manifest.schema.json @@ -0,0 +1,843 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://skill-optimizer.local/schemas/team-delivery-manifest.schema.json", + "title": "Skill Optimizer P2 Team Delivery Manifest", + "type": "object", + "additionalProperties": false, + "required": [ + "object_version", + "schema_version", + "delivery_id", + "delivery_target", + "host", + "invocation_mode", + "source_candidate_digest", + "source_behavior_report_digest", + "source_risk_commitment_digest", + "source_scope_projection_digest", + "output_root", + "package_root", + "builder_manifest_path", + "team_manifest_path", + "authority", + "behavior", + "projection", + "package", + "claims", + "quality_claim_projection", + "compatibility", + "automatic_routing", + "change_summary", + "rollback", + "threat_model", + "integration_requests", + "content_digest" + ], + "properties": { + "object_version": { + "const": "skill-optimizer.team-delivery-manifest/1" + }, + "schema_version": { + "const": "1.0.0" + }, + "delivery_id": { + "$ref": "#/$defs/digest" + }, + "delivery_target": { + "const": "P2_team_package" + }, + "host": { + "enum": [ + "codex", + "claude" + ] + }, + "invocation_mode": { + "const": "explicit_only" + }, + "source_candidate_digest": { + "$ref": "#/$defs/digest" + }, + "source_behavior_report_digest": { + "$ref": "#/$defs/digest" + }, + "source_risk_commitment_digest": { + "$ref": "#/$defs/digest" + }, + "source_scope_projection_digest": { + "$ref": "#/$defs/digest" + }, + "output_root": { + "$ref": "#/$defs/absolute_path" + }, + "package_root": { + "$ref": "#/$defs/absolute_path" + }, + "builder_manifest_path": { + "$ref": "#/$defs/absolute_path" + }, + "team_manifest_path": { + "$ref": "#/$defs/absolute_path" + }, + "authority": { + "$ref": "#/$defs/authority" + }, + "behavior": { + "$ref": "#/$defs/behavior" + }, + "projection": { + "$ref": "#/$defs/projection" + }, + "package": { + "$ref": "#/$defs/package" + }, + "claims": { + "$ref": "#/$defs/claims" + }, + "quality_claim_projection": { + "$ref": "#/$defs/quality_projection" + }, + "compatibility": { + "$ref": "#/$defs/compatibility" + }, + "automatic_routing": { + "$ref": "#/$defs/automatic_routing" + }, + "change_summary": { + "$ref": "#/$defs/change_summary" + }, + "rollback": { + "$ref": "#/$defs/rollback" + }, + "threat_model": { + "$ref": "#/$defs/threat_model" + }, + "integration_requests": { + "$ref": "#/$defs/nonempty_unique_strings" + }, + "content_digest": { + "$ref": "#/$defs/digest" + } + }, + "$defs": { + "digest": { + "type": "string", + "pattern": "^sha256:[0-9a-f]{64}$" + }, + "absolute_path": { + "type": "string", + "minLength": 2, + "pattern": "^/.*$" + }, + "relative_path": { + "type": "string", + "minLength": 1, + "pattern": "^(?!/)(?!.*(?:^|/)\\.\\.(?:/|$))(?!.*\\\\).+$" + }, + "claim_id": { + "type": "string", + "pattern": "^[A-Za-z0-9][A-Za-z0-9_.:/-]{0,127}$" + }, + "claim_array": { + "type": "array", + "uniqueItems": true, + "items": { + "$ref": "#/$defs/claim_id" + } + }, + "unique_strings": { + "type": "array", + "uniqueItems": true, + "items": { + "type": "string", + "minLength": 1 + } + }, + "nonempty_unique_strings": { + "type": "array", + "minItems": 1, + "uniqueItems": true, + "items": { + "type": "string", + "minLength": 1 + } + }, + "action_target": { + "type": "object", + "additionalProperties": false, + "required": [ + "object_version", + "host", + "candidate_digest", + "behavior_report_digest", + "risk_commitment_digest", + "scope_digest", + "scope_projection_digest", + "quality_projection_digest", + "runtime_projection_digest", + "output_root", + "output_root_identity_digest", + "quality_unverified", + "automatic_routing_requested", + "automatic_routing_effective", + "package_relative_path", + "builder_manifest_relative_path", + "team_manifest_relative_path", + "content_digest" + ], + "properties": { + "object_version": { + "const": "skill-optimizer.team-delivery-action-target/1" + }, + "host": { + "enum": [ + "codex", + "claude" + ] + }, + "candidate_digest": { + "$ref": "#/$defs/digest" + }, + "behavior_report_digest": { + "$ref": "#/$defs/digest" + }, + "risk_commitment_digest": { + "$ref": "#/$defs/digest" + }, + "scope_digest": { + "$ref": "#/$defs/digest" + }, + "scope_projection_digest": { + "$ref": "#/$defs/digest" + }, + "quality_projection_digest": { + "$ref": "#/$defs/digest" + }, + "runtime_projection_digest": { + "$ref": "#/$defs/digest" + }, + "output_root": { + "$ref": "#/$defs/absolute_path" + }, + "output_root_identity_digest": { + "$ref": "#/$defs/digest" + }, + "quality_unverified": { + "const": true + }, + "automatic_routing_requested": { + "const": false + }, + "automatic_routing_effective": { + "const": false + }, + "package_relative_path": { + "const": "package" + }, + "builder_manifest_relative_path": { + "const": "package.manifest.json" + }, + "team_manifest_relative_path": { + "const": "team-delivery-manifest.json" + }, + "content_digest": { + "$ref": "#/$defs/digest" + } + } + }, + "authority": { + "type": "object", + "additionalProperties": false, + "required": [ + "workflow_id", + "event_head_digest", + "authorization_event_digest", + "authorization_grant_digest", + "scope_digest", + "risk_digest", + "action_target", + "action_target_digest", + "required_actions", + "trust_limit" + ], + "properties": { + "workflow_id": { + "type": "string", + "minLength": 1 + }, + "event_head_digest": { + "$ref": "#/$defs/digest" + }, + "authorization_event_digest": { + "$ref": "#/$defs/digest" + }, + "authorization_grant_digest": { + "$ref": "#/$defs/digest" + }, + "scope_digest": { + "$ref": "#/$defs/digest" + }, + "risk_digest": { + "$ref": "#/$defs/digest" + }, + "action_target": { + "$ref": "#/$defs/action_target" + }, + "action_target_digest": { + "$ref": "#/$defs/digest" + }, + "required_actions": { + "const": [ + "team_delivery", + "quality_unverified" + ] + }, + "trust_limit": { + "const": "local hash-chain evidence detects ordinary drift but is not a signature or same-user adversary boundary" + } + } + }, + "behavior": { + "type": "object", + "additionalProperties": false, + "required": [ + "source_report_digest", + "package_report_digest", + "minimum_risk", + "package_minimum_risk", + "mandatory_controls", + "mandatory_capabilities", + "unknowns", + "requires_runtime_enforcement", + "sensitive_material_bundled" + ], + "properties": { + "source_report_digest": { + "$ref": "#/$defs/digest" + }, + "package_report_digest": { + "$ref": "#/$defs/digest" + }, + "minimum_risk": { + "enum": [ + "R0", + "R1", + "R2", + "R3" + ] + }, + "package_minimum_risk": { + "enum": [ + "R0", + "R1", + "R2", + "R3" + ] + }, + "mandatory_controls": { + "$ref": "#/$defs/unique_strings" + }, + "mandatory_capabilities": { + "$ref": "#/$defs/unique_strings" + }, + "unknowns": { + "type": "array", + "uniqueItems": true, + "items": { + "type": "string", + "pattern": "^behavior-[0-9a-f]{32}$" + } + }, + "requires_runtime_enforcement": { + "const": false + }, + "sensitive_material_bundled": { + "const": false + } + } + }, + "selected_file": { + "type": "object", + "additionalProperties": false, + "required": [ + "path", + "size", + "digest", + "executable" + ], + "properties": { + "path": { + "$ref": "#/$defs/relative_path" + }, + "size": { + "type": "integer", + "minimum": 0 + }, + "digest": { + "$ref": "#/$defs/digest" + }, + "executable": { + "type": "boolean" + } + } + }, + "excluded_path": { + "type": "object", + "additionalProperties": false, + "required": [ + "path", + "reason" + ], + "properties": { + "path": { + "$ref": "#/$defs/relative_path" + }, + "reason": { + "enum": [ + "development_material", + "test_material", + "development_trace_or_cache", + "host_specific_not_runtime", + "not_runtime_allowlisted" + ] + } + } + }, + "projection": { + "type": "object", + "additionalProperties": false, + "required": [ + "runtime_projection_digest", + "builder_source_projection_digest", + "selected_files", + "excluded_paths", + "source_rechecked_after_build", + "package_behavior_audited" + ], + "properties": { + "runtime_projection_digest": { + "$ref": "#/$defs/digest" + }, + "builder_source_projection_digest": { + "$ref": "#/$defs/digest" + }, + "selected_files": { + "type": "array", + "minItems": 1, + "uniqueItems": true, + "contains": { + "type": "object", + "properties": { + "path": { + "const": "SKILL.md" + } + }, + "required": [ + "path" + ] + }, + "minContains": 1, + "maxContains": 1, + "items": { + "$ref": "#/$defs/selected_file" + } + }, + "excluded_paths": { + "type": "array", + "uniqueItems": true, + "items": { + "$ref": "#/$defs/excluded_path" + } + }, + "source_rechecked_after_build": { + "const": true + }, + "package_behavior_audited": { + "const": true + } + } + }, + "verification_receipt": { + "type": "object", + "additionalProperties": false, + "required": [ + "object_version", + "dist_root", + "manifest_path", + "dist_digest", + "manifest_digest", + "tree_digest", + "manifest_schema_version", + "source_projection_digest", + "file_count", + "total_bytes", + "tree_read_only", + "manifest_read_only", + "receipt_digest" + ], + "properties": { + "object_version": { + "const": "skill-optimizer.distribution-verification-receipt/v1" + }, + "dist_root": { + "$ref": "#/$defs/absolute_path" + }, + "manifest_path": { + "$ref": "#/$defs/absolute_path" + }, + "dist_digest": { + "$ref": "#/$defs/digest" + }, + "manifest_digest": { + "$ref": "#/$defs/digest" + }, + "tree_digest": { + "$ref": "#/$defs/digest" + }, + "manifest_schema_version": { + "const": "1.1" + }, + "source_projection_digest": { + "$ref": "#/$defs/digest" + }, + "file_count": { + "type": "integer", + "minimum": 1 + }, + "total_bytes": { + "type": "integer", + "minimum": 1 + }, + "tree_read_only": { + "const": true + }, + "manifest_read_only": { + "const": true + }, + "receipt_digest": { + "$ref": "#/$defs/digest" + } + } + }, + "package": { + "type": "object", + "additionalProperties": false, + "required": [ + "dist_digest", + "builder_manifest_digest", + "verification_receipt_digest", + "verification_receipt", + "file_count", + "total_bytes", + "read_only" + ], + "properties": { + "dist_digest": { + "$ref": "#/$defs/digest" + }, + "builder_manifest_digest": { + "$ref": "#/$defs/digest" + }, + "verification_receipt_digest": { + "$ref": "#/$defs/digest" + }, + "verification_receipt": { + "$ref": "#/$defs/verification_receipt" + }, + "file_count": { + "type": "integer", + "minimum": 1 + }, + "total_bytes": { + "type": "integer", + "minimum": 1 + }, + "read_only": { + "const": true + } + } + }, + "claims": { + "type": "object", + "additionalProperties": false, + "required": [ + "verified", + "unverified", + "blocked", + "removed" + ], + "properties": { + "verified": { + "const": [ + "behavior-audit-current-package-bytes", + "behavior-audit-current-source-bytes", + "builder-manifest-matches-current-package-bytes", + "runtime-only-allowlist-projection" + ] + }, + "unverified": { + "allOf": [ + { + "$ref": "#/$defs/claim_array" + }, + { + "contains": { + "const": "quality-unverified" + }, + "minContains": 1, + "maxContains": 1 + } + ] + }, + "blocked": { + "allOf": [ + { + "$ref": "#/$defs/claim_array" + }, + { + "contains": { + "const": "host-compatibility" + }, + "minContains": 1, + "maxContains": 1 + } + ] + }, + "removed": { + "allOf": [ + { + "$ref": "#/$defs/claim_array" + }, + { + "contains": { + "const": "install" + }, + "minContains": 1, + "maxContains": 1 + }, + { + "contains": { + "const": "host-activation" + }, + "minContains": 1, + "maxContains": 1 + }, + { + "contains": { + "const": "formal-adoption" + }, + "minContains": 1, + "maxContains": 1 + }, + { + "contains": { + "const": "automatic-routing" + }, + "minContains": 1, + "maxContains": 1 + } + ] + } + } + }, + "quality_projection": { + "type": "object", + "additionalProperties": false, + "required": [ + "object_version", + "candidate_digest", + "verified_claims", + "unverified_claims", + "blocked_claims", + "removed_claims", + "integration_requests", + "content_digest" + ], + "properties": { + "object_version": { + "const": "skill-optimizer.quality-projection/1" + }, + "candidate_digest": { + "$ref": "#/$defs/digest" + }, + "verified_claims": { + "type": "array", + "maxItems": 0 + }, + "unverified_claims": { + "$ref": "#/$defs/claim_array" + }, + "blocked_claims": { + "$ref": "#/$defs/claim_array" + }, + "removed_claims": { + "$ref": "#/$defs/claim_array" + }, + "integration_requests": { + "$ref": "#/$defs/nonempty_unique_strings" + }, + "content_digest": { + "$ref": "#/$defs/digest" + } + } + }, + "compatibility": { + "type": "object", + "additionalProperties": false, + "required": [ + "host", + "status", + "evidence_digest", + "reason" + ], + "properties": { + "host": { + "enum": [ + "codex", + "claude" + ] + }, + "status": { + "const": "unknown" + }, + "evidence_digest": { + "type": "null" + }, + "reason": { + "const": "real-host-adapter-validation-missing" + } + } + }, + "automatic_routing": { + "type": "object", + "additionalProperties": false, + "required": [ + "scope_requested", + "scope_source_status", + "effective", + "claim_status", + "shared_trigger_derived", + "required_evidence" + ], + "properties": { + "scope_requested": { + "const": false + }, + "scope_source_status": { + "const": "unverified" + }, + "effective": { + "const": false + }, + "claim_status": { + "const": "removed" + }, + "shared_trigger_derived": { + "const": false + }, + "required_evidence": { + "allOf": [ + { + "$ref": "#/$defs/unique_strings" + }, + { + "maxItems": 0 + } + ] + } + } + }, + "change_summary": { + "type": "object", + "additionalProperties": false, + "required": [ + "kind", + "selected_file_count", + "excluded_path_count", + "source_mutated", + "host_skill_root_mutated" + ], + "properties": { + "kind": { + "const": "isolated-additive-package" + }, + "selected_file_count": { + "type": "integer", + "minimum": 1 + }, + "excluded_path_count": { + "type": "integer", + "minimum": 0 + }, + "source_mutated": { + "const": false + }, + "host_skill_root_mutated": { + "const": false + } + } + }, + "rollback": { + "type": "object", + "additionalProperties": false, + "required": [ + "install_performed", + "host_activation_performed", + "host_rollback_required", + "package_cleanup" + ], + "properties": { + "install_performed": { + "const": false + }, + "host_activation_performed": { + "const": false + }, + "host_rollback_required": { + "const": false + }, + "package_cleanup": { + "const": "separate-authorized-action" + } + } + }, + "threat_model": { + "type": "object", + "additionalProperties": false, + "required": [ + "same_user_malicious_actor_resistant", + "destination_parent_toctou_fully_eliminated", + "directory_lock", + "claim_cap", + "limitations" + ], + "properties": { + "same_user_malicious_actor_resistant": { + "const": false + }, + "destination_parent_toctou_fully_eliminated": { + "const": false + }, + "directory_lock": { + "const": "advisory-flock-plus-inode-recheck" + }, + "claim_cap": { + "const": "current-local-structure-and-bytes-only" + }, + "limitations": { + "type": "array", + "minItems": 4, + "uniqueItems": true, + "items": { + "enum": [ + "local-workflow-hash-chain-is-not-an-identity-signature", + "path-based-builder-cannot-eliminate-malicious-parent-replacement", + "static-audit-does-not-prove-absence-of-all-unknown-secrets", + "no-host-install-activation-routing-or-compatibility-proof" + ] + } + } + } + } + } +}