feat: prepare pinned Custodian worker bundle and required input admission
Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a15374-f59f-7a31-86d2-d23b5d2ed181
This commit is contained in:
parent
0319fe8be8
commit
fed119a17c
18 changed files with 1058 additions and 2 deletions
81
src/activity_core/custodian_inputs.py
Normal file
81
src/activity_core/custodian_inputs.py
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
"""Fail-closed admission for trusted-owner fleet snapshots and freshness receipts."""
|
||||
import hashlib
|
||||
import json
|
||||
import stat
|
||||
from datetime import UTC, datetime
|
||||
from pathlib import Path, PurePosixPath
|
||||
|
||||
|
||||
def _read(path):
|
||||
path = Path(path)
|
||||
if any(p.is_symlink() for p in [path, *path.parents]) or not stat.S_ISREG(path.stat().st_mode) or path.stat().st_size > 8 * 1024 * 1024:
|
||||
raise ValueError("unsafe fleet evidence path or size")
|
||||
return path.read_bytes()
|
||||
|
||||
|
||||
def admit(fleet, policy_path, receipts_path, repos, now=None):
|
||||
"""Files/receipts are protected owner inputs, not signatures or remote verification."""
|
||||
fleet = Path(fleet)
|
||||
policy = json.loads(_read(policy_path))
|
||||
if policy.get("schema") != "activity-core.custodian-input-policy.v1" or set(policy["repositories"]) != set(repos):
|
||||
raise ValueError("fleet policy repository mismatch")
|
||||
raw = _read(fleet / "manifest.json")
|
||||
if _read(fleet / "COMPLETE").decode().strip() != hashlib.sha256(raw).hexdigest():
|
||||
raise ValueError("incomplete fleet snapshot")
|
||||
manifest = json.loads(raw)
|
||||
records = manifest.get("repositories", [])
|
||||
if manifest.get("schema") != "repo-manager.fleet-input.v1" or len(records) != len(repos) or {r["slug"] for r in records} != set(repos):
|
||||
raise ValueError("fleet snapshot repository mismatch")
|
||||
if {p.name for p in fleet.iterdir()} != set(repos) | {"manifest.json", "COMPLETE"}:
|
||||
raise ValueError("snapshot contains unexpected repository or file")
|
||||
receipts = json.loads(_read(receipts_path))
|
||||
now = now or datetime.now(UTC)
|
||||
expected_files = set()
|
||||
total = 0
|
||||
for record in records:
|
||||
valid = False
|
||||
for receipt in receipts:
|
||||
if receipt.get("schema") != "repo-manager.freshness-receipt.v1" or receipt.get("repo_slug") != record["slug"] or receipt.get("forge_commit") != record["commit"]:
|
||||
continue
|
||||
hub = receipt.get("hub", {})
|
||||
if hub.get("verified_primary") is not True or hub.get("instance_role") != "primary" or hub.get("instance_label") != policy["expected_hub_instance"]:
|
||||
continue
|
||||
if receipt.get("outcome") not in {"applied", "noop"}:
|
||||
continue
|
||||
noop = receipt.get("outcome") == "noop" and receipt.get("reason") == "forge_head_matches_last_success"
|
||||
bound = all(receipt.get(key) == record["commit"] for key in ("expected_commit", "derived_commit", "projected_commit"))
|
||||
# Native issuer emits this reason only after HEAD == last successful commit.
|
||||
# Receipts are protected owner evidence; this is not signature verification.
|
||||
bound = bound or noop
|
||||
if not bound:
|
||||
continue
|
||||
observed = datetime.fromisoformat(receipt["finished_at"])
|
||||
if observed.tzinfo is None:
|
||||
continue
|
||||
age = (now - observed).total_seconds()
|
||||
if -policy["max_future_skew_seconds"] <= age <= policy["max_observation_age_seconds"]:
|
||||
valid = True
|
||||
if not valid:
|
||||
raise ValueError("missing accepted fresh Forge receipt")
|
||||
for entry in record["files"]:
|
||||
relative = PurePosixPath(entry["path"])
|
||||
if relative.is_absolute() or ".." in relative.parts or relative.parts[0] not in {"workplans", "decisions"} or relative.suffix != ".md":
|
||||
raise ValueError("unsafe snapshot record")
|
||||
path = fleet / record["slug"] / relative
|
||||
data = _read(path)
|
||||
total += len(data)
|
||||
if total > policy["max_snapshot_bytes"]:
|
||||
raise ValueError("snapshot size policy exceeded")
|
||||
if len(data) != entry["bytes"] or hashlib.sha256(data).hexdigest() != entry["sha256"]:
|
||||
raise ValueError("snapshot record hash mismatch")
|
||||
expected_files.add(path)
|
||||
actual_files = set()
|
||||
for slug in repos:
|
||||
for path in (fleet / slug).rglob("*"):
|
||||
if path.is_symlink():
|
||||
raise ValueError("snapshot symlink refused")
|
||||
if path.is_file():
|
||||
actual_files.add(path)
|
||||
if actual_files != expected_files or total > policy["max_snapshot_bytes"]:
|
||||
raise ValueError("snapshot content outside manifest or size policy")
|
||||
return hashlib.sha256(raw).hexdigest()
|
||||
|
|
@ -89,6 +89,10 @@ def build_command(operation: str, params: dict) -> tuple[list[str], Path, Path,
|
|||
raise ValueError("explicit canonical security-flow repository list required")
|
||||
for repo in repos:
|
||||
_path(str(fleet / repo / "workplans"), directory=True)
|
||||
from activity_core.custodian_inputs import admit
|
||||
policy_path = _path(os.environ.get("ACTIVITY_CORE_CUSTODIAN_INPUT_POLICY"))
|
||||
receipts_path = _path(os.environ.get("ACTIVITY_CORE_CUSTODIAN_FRESHNESS_RECEIPTS"))
|
||||
admit(fleet, policy_path, receipts_path, repos)
|
||||
hub = os.environ.get("ACTIVITY_CORE_CUSTODIAN_HUB", "")
|
||||
parsed = urlsplit(hub)
|
||||
if parsed.scheme not in {"http", "https"} or not parsed.hostname or parsed.username or parsed.password or parsed.query or parsed.fragment or parsed.path not in {"", "/"}:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue