Files
CASAN/packages/casan-harness/scripts/python/harness_hash.py
T
thanhnvandClaude Opus 4.8 8450f8ca1a feat(install): Plan-21 hybrid global install + casan init (pin+verify)
Adopt CASAN like a normal tool (codegraph-style): install the harness ONCE
per machine, then `casan init` per project writes CONFIG ONLY — the harness
is no longer copied into every repo.

- install.sh / install.ps1: global bootstrap (curl|sh / irm|iex or local
  source). Installs harness to $CASAN_HOME/versions/<ver>, writes a `casan`
  launcher that resolves the shared harness + the current project's .specify,
  and records a gate-code integrity hash. CASAN_NO_PATH_LINK for tests.
- harness_hash.py: deterministic content hash over gate code (scripts/bash,
  scripts/python, security, level5) — the pin+verify anchor.
- casan-init.py: `casan init` writes .casan/{config,version.lock,agentic.env},
  .specify/ marker, and the Plan-20 client hooks — no harness copy. `verify`
  recomputes the harness hash LIVE and compares to the project pin (drift/
  tamper -> rc 3), preserving the Plan-16 trusted-gates guarantee off-repo.
- bin/casan: new `init` and `verify-harness` commands.
- hybrid-install-tests.sh: 21/21 (install, config-only init, no-copy, pin,
  verify ok, tamper drift, bridge runs against project state via global harness).
- docs: CASAN_INSTALL_HYBRID.md + Plan-21.

The path model (casan-paths.sh) already separated harness/state/domain roots,
so this is installer + init, not a core rewrite. Remote dist tarball, real
Windows run, and signed .harness-hash are the documented next steps.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 21:26:54 +07:00

106 lines
3.7 KiB
Python
Executable File

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""Deterministic harness integrity hash (Plan-21 hybrid install pin+verify).
Computes a stable content hash over the CASAN gate code so a globally-installed
harness can be PINNED by a project (`.casan/version.lock`) and VERIFIED at runtime.
If the resolved global harness drifts or is tampered relative to the pin, the
project can fail closed — preserving the "gates are trusted, not arbitrary code"
guarantee from Plan-16 even when the harness lives outside the repo.
Only GATE-relevant trees are hashed (the code that makes security/governance
decisions), never runtime state/logs:
scripts/bash scripts/python security level5
stdlib-only, deterministic (sorted paths), text-mode agnostic (hashes raw bytes).
Usage:
harness_hash.py compute <harness_root> # prints "<algo>:<hex>"
harness_hash.py manifest <harness_root> # prints JSON {file: sha256}
harness_hash.py verify <harness_root> <expected> # exit 0 match / 3 drift
"""
from __future__ import annotations
import hashlib
import json
import os
import sys
ALGO = "sha256"
HASHED_SUBTREES = ("scripts/bash", "scripts/python", "security", "level5")
SKIP_DIR_NAMES = {"__pycache__", ".git", "node_modules"}
SKIP_SUFFIXES = (".pyc", ".pyo", ".log", ".tmp", ".DS_Store")
# Within level5, only policy/config, not regenerated runtime artifacts.
SKIP_BASENAMES = {"policy-manifest.json", "policy-manifest.sig", "project-registry.json"}
def _iter_files(root):
for sub in HASHED_SUBTREES:
base = os.path.join(root, sub)
if not os.path.isdir(base):
continue
for dirpath, dirnames, filenames in os.walk(base):
dirnames[:] = sorted(d for d in dirnames if d not in SKIP_DIR_NAMES)
for name in sorted(filenames):
if name in SKIP_BASENAMES:
continue
if any(name.endswith(s) for s in SKIP_SUFFIXES):
continue
full = os.path.join(dirpath, name)
rel = os.path.relpath(full, root).replace(os.sep, "/")
yield rel, full
def manifest(root):
out = {}
for rel, full in _iter_files(root):
try:
with open(full, "rb") as fh:
out[rel] = hashlib.sha256(fh.read()).hexdigest()
except (OSError, IOError):
out[rel] = "UNREADABLE"
return out
def compute(root):
m = manifest(root)
joiner = "\n".join("%s:%s" % (rel, m[rel]) for rel in sorted(m))
digest = hashlib.sha256(joiner.encode("utf-8")).hexdigest()
return "%s:%s" % (ALGO, digest)
def main(argv=None):
argv = argv if argv is not None else sys.argv[1:]
if len(argv) < 2:
sys.stderr.write("usage: harness_hash.py <compute|manifest|verify> <harness_root> [expected]\n")
return 64
cmd, root = argv[0], argv[1]
if not os.path.isdir(root):
sys.stderr.write("harness_hash: not a directory: %s\n" % root)
return 66
if cmd == "compute":
print(compute(root))
return 0
if cmd == "manifest":
print(json.dumps(manifest(root), ensure_ascii=False, indent=2, sort_keys=True))
return 0
if cmd == "verify":
if len(argv) < 3:
sys.stderr.write("harness_hash: verify needs an expected hash\n")
return 64
actual = compute(root)
expected = argv[2].strip()
if actual == expected:
print("HARNESS_INTEGRITY_OK %s" % actual)
return 0
sys.stderr.write("HARNESS_INTEGRITY_DRIFT expected=%s actual=%s\n" % (expected, actual))
return 3
sys.stderr.write("harness_hash: unknown command %s\n" % cmd)
return 64
if __name__ == "__main__":
sys.exit(main())