feat: add production Core runtime modes
This commit is contained in:
@@ -1,10 +1,10 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""`casan init` / `casan verify-harness` (Plan-21 hybrid adoption).
|
||||
"""`casan init` / `casan verify-harness` (Plan-21 production adoption).
|
||||
|
||||
Adopt CASAN into an EXISTING project by writing only per-project config — the
|
||||
shared harness stays under $CASAN_HOME and is NOT copied into the repo. This is
|
||||
the codegraph-style flow: global install once, then `casan init` per project.
|
||||
Adopt CASAN into an EXISTING project using an explicit runtime contract:
|
||||
managed mode pins the shared Core under $CASAN_HOME; vendored mode installs the
|
||||
same production-only Core under `.casan/runtime/casan-core`.
|
||||
|
||||
What init writes into the target repo:
|
||||
.casan/config.json project id, enforcement/integration mode, clients
|
||||
@@ -13,6 +13,8 @@ What init writes into the target repo:
|
||||
.specify/ runtime state root marker (logs/traces/admissions)
|
||||
.claude/settings.json Plan-20 Claude Code hooks (--client claude|all)
|
||||
.codex/hooks.json+config Plan-20 Codex hooks (--client codex|all)
|
||||
.casan/runtime/casan-core
|
||||
optional self-contained Core (--runtime vendored)
|
||||
|
||||
`verify` recomputes the resolved harness gate-code hash and compares it to
|
||||
version.lock — the pin+VERIFY half. Drift/tamper of the global harness relative
|
||||
@@ -107,6 +109,8 @@ def _render_init(result):
|
||||
("Location", result["target"]),
|
||||
("Level", "%s (%s)" % (
|
||||
result["target_level_name"].capitalize(), result["target_level"])),
|
||||
("Runtime", "%s — %s" % (
|
||||
result["runtime_mode"].capitalize(), result["runtime_path"])),
|
||||
("Mode", result["enforcement_mode"]),
|
||||
("Clients", _client_names(result["clients"])),
|
||||
("Files", "%d created or updated" % len(result["created"])),
|
||||
@@ -149,6 +153,10 @@ def _render_level(result):
|
||||
result.get("project_target_level_name"),
|
||||
result.get("project_target_level")))
|
||||
if result.get("project_target_level") else "not initialized"),
|
||||
("Runtime", ("%s — %s" % (
|
||||
result.get("project_runtime_mode"),
|
||||
result.get("project_runtime_path")))
|
||||
if result.get("project_target_level") else "not initialized"),
|
||||
("Status", str(result.get("project_level_status") or "unknown")),
|
||||
])
|
||||
print()
|
||||
@@ -270,7 +278,10 @@ def _copy_tree_missing(src_dir, dst_dir, target, created):
|
||||
|
||||
|
||||
def resolve_harness(explicit):
|
||||
for cand in (explicit, os.environ.get("CASAN_HARNESS_ROOT")):
|
||||
for cand in (
|
||||
explicit,
|
||||
os.environ.get("CASAN_HARNESS_ROOT"),
|
||||
os.environ.get("CASAN_GLOBAL_HARNESS_ROOT")):
|
||||
if cand and os.path.isdir(os.path.join(cand, "scripts", "bash")):
|
||||
return os.path.abspath(cand)
|
||||
install = os.environ.get("CASAN_INSTALL_ROOT")
|
||||
@@ -290,8 +301,9 @@ def install_root(harness):
|
||||
|
||||
|
||||
def harness_version(harness):
|
||||
for p in (os.path.join(install_root(harness), "VERSION"),
|
||||
os.path.join(harness, "..", "..", "VERSION")):
|
||||
local_root = os.path.abspath(os.path.join(harness, "..", ".."))
|
||||
for p in (os.path.join(local_root, "VERSION"),
|
||||
os.path.join(install_root(harness), "VERSION")):
|
||||
try:
|
||||
with open(p, "r", encoding="utf-8") as fh:
|
||||
v = fh.read().strip()
|
||||
@@ -317,17 +329,38 @@ def compute_harness_hash(harness):
|
||||
"""For PINNING at init: use the value recorded at install time if present
|
||||
(it equals a live compute of the same files), else compute live. Verify must
|
||||
NOT use this — it must call compute_live() to detect drift."""
|
||||
recorded = os.path.join(install_root(harness), ".harness-hash")
|
||||
try:
|
||||
with open(recorded, "r", encoding="utf-8") as fh:
|
||||
v = fh.read().strip()
|
||||
if v:
|
||||
return v, "recorded"
|
||||
except (OSError, IOError):
|
||||
pass
|
||||
local_root = os.path.abspath(os.path.join(harness, "..", ".."))
|
||||
for recorded in (
|
||||
os.path.join(local_root, ".harness-hash"),
|
||||
os.path.join(install_root(harness), ".harness-hash")):
|
||||
try:
|
||||
with open(recorded, "r", encoding="utf-8") as fh:
|
||||
v = fh.read().strip()
|
||||
if v:
|
||||
return v, "recorded"
|
||||
except (OSError, IOError):
|
||||
continue
|
||||
return compute_live(harness)
|
||||
|
||||
|
||||
def resolve_project_harness(target, explicit=None):
|
||||
lock = _load_json_or(
|
||||
os.path.join(target, ".casan", "version.lock"), {})
|
||||
runtime_path = lock.get("runtime_path")
|
||||
if lock.get("runtime_mode") == "vendored" and isinstance(runtime_path, str):
|
||||
root = (
|
||||
None if os.path.isabs(runtime_path)
|
||||
else _safe_project_path(target, runtime_path)
|
||||
)
|
||||
if not root:
|
||||
return None
|
||||
candidate = os.path.join(root, "packages", "casan-harness")
|
||||
if os.path.isdir(os.path.join(candidate, "scripts", "bash")):
|
||||
return os.path.abspath(candidate)
|
||||
return None
|
||||
return resolve_harness(explicit)
|
||||
|
||||
|
||||
def _write(path, text, backups):
|
||||
with owner_writable(path):
|
||||
if os.path.exists(path):
|
||||
@@ -366,8 +399,10 @@ def _safe_project_path(target, relative):
|
||||
if not normalized or os.path.isabs(str(relative)):
|
||||
return None
|
||||
candidate = os.path.abspath(os.path.join(target, *normalized.split("/")))
|
||||
target_real = os.path.realpath(target)
|
||||
candidate_real = os.path.realpath(candidate)
|
||||
try:
|
||||
if os.path.commonpath((target, candidate)) != target:
|
||||
if os.path.commonpath((target_real, candidate_real)) != target_real:
|
||||
return None
|
||||
except ValueError:
|
||||
return None
|
||||
@@ -387,6 +422,110 @@ def _file_sha256(path):
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
def _install_vendored_core(target, source_harness):
|
||||
"""Atomically install the production Core runtime inside one project."""
|
||||
source_root = os.path.abspath(os.path.join(source_harness, "..", ".."))
|
||||
copier = os.path.join(source_root, "scripts", "copy-runtime.py")
|
||||
source_bin = os.path.join(source_root, "bin", "casan")
|
||||
source_version = os.path.join(source_root, "VERSION")
|
||||
for required in (source_bin, source_version):
|
||||
if not os.path.isfile(required):
|
||||
raise RuntimeError(
|
||||
"vendored runtime source is incomplete; missing %s" % required)
|
||||
|
||||
destination = _safe_project_path(
|
||||
target, ".casan/runtime/casan-core")
|
||||
if not destination:
|
||||
raise RuntimeError(
|
||||
"vendored runtime path escapes the project through a symlink")
|
||||
parent = os.path.dirname(destination)
|
||||
os.makedirs(parent, exist_ok=True)
|
||||
staging = tempfile.mkdtemp(prefix=".casan-core-", dir=parent)
|
||||
try:
|
||||
if os.path.isfile(copier):
|
||||
result = subprocess.run(
|
||||
[
|
||||
sys.executable,
|
||||
copier,
|
||||
"--source-root", source_root,
|
||||
"--destination-root", staging,
|
||||
"--component", "harness",
|
||||
"--clean",
|
||||
],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
text=True,
|
||||
check=False,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
raise RuntimeError(
|
||||
result.stderr.strip() or result.stdout.strip() or
|
||||
"production runtime copy failed")
|
||||
else:
|
||||
shutil.copytree(
|
||||
source_harness,
|
||||
os.path.join(staging, "packages", "casan-harness"))
|
||||
vendored_harness = os.path.join(
|
||||
staging, "packages", "casan-harness")
|
||||
forbidden = [
|
||||
relative for relative in (
|
||||
"tests",
|
||||
"level5",
|
||||
"scripts/bash/ci-harness-gate.sh",
|
||||
"scripts/bash/security-gate.sh",
|
||||
"scripts/bash/dashboard-server.py",
|
||||
)
|
||||
if os.path.exists(os.path.join(
|
||||
vendored_harness, *relative.split("/")))
|
||||
]
|
||||
if forbidden:
|
||||
raise RuntimeError(
|
||||
"source is not a production runtime; forbidden paths: %s" %
|
||||
", ".join(forbidden))
|
||||
os.makedirs(os.path.join(staging, "bin"), exist_ok=True)
|
||||
shutil.copy2(source_bin, os.path.join(staging, "bin", "casan"))
|
||||
shutil.copy2(source_version, os.path.join(staging, "VERSION"))
|
||||
with open(os.path.join(staging, ".casan-level"), "w",
|
||||
encoding="utf-8") as fh:
|
||||
fh.write("core\n")
|
||||
vendored_hash, hash_source = compute_live(vendored_harness)
|
||||
if hash_source == "error":
|
||||
raise RuntimeError(
|
||||
"cannot verify staged vendored Core: %s" % vendored_hash)
|
||||
with open(os.path.join(staging, ".harness-hash"), "w",
|
||||
encoding="utf-8") as fh:
|
||||
fh.write(vendored_hash + "\n")
|
||||
if os.path.isdir(destination) and not os.path.islink(destination):
|
||||
shutil.rmtree(destination)
|
||||
elif os.path.exists(destination) or os.path.islink(destination):
|
||||
os.unlink(destination)
|
||||
os.replace(staging, destination)
|
||||
staging = None
|
||||
finally:
|
||||
if staging and os.path.isdir(staging):
|
||||
shutil.rmtree(staging)
|
||||
|
||||
file_count = sum(
|
||||
len(files) for _root, _dirs, files in os.walk(destination))
|
||||
return destination, file_count
|
||||
|
||||
|
||||
def _remove_vendored_core(target):
|
||||
destination = _safe_project_path(
|
||||
target, ".casan/runtime/casan-core")
|
||||
if not destination:
|
||||
return False
|
||||
if not os.path.exists(destination) and not os.path.islink(destination):
|
||||
return False
|
||||
with owner_writable(destination):
|
||||
if os.path.isdir(destination) and not os.path.islink(destination):
|
||||
shutil.rmtree(destination)
|
||||
else:
|
||||
os.unlink(destination)
|
||||
_prune_empty_parents(destination, target)
|
||||
return True
|
||||
|
||||
|
||||
def _backup_once(path, backups):
|
||||
if backups is None:
|
||||
return
|
||||
@@ -831,8 +970,38 @@ def cmd_init(args):
|
||||
except ValueError as error:
|
||||
sys.stderr.write("casan init: %s\n" % error)
|
||||
return 64
|
||||
version = harness_version(harness)
|
||||
hhash, hsource = compute_harness_hash(harness)
|
||||
|
||||
previous_config = _load_json_or(
|
||||
os.path.join(target, ".casan", "config.json"), {})
|
||||
previous_runtime_mode = previous_config.get("runtime_mode")
|
||||
runtime_mode = (
|
||||
args.runtime or
|
||||
(previous_runtime_mode
|
||||
if previous_runtime_mode in ("managed", "vendored") else "managed")
|
||||
)
|
||||
runtime_removed = False
|
||||
runtime_files = 0
|
||||
if runtime_mode == "vendored":
|
||||
try:
|
||||
runtime_root, runtime_files = _install_vendored_core(
|
||||
target, harness)
|
||||
except (OSError, RuntimeError) as error:
|
||||
sys.stderr.write(
|
||||
"casan init: cannot install vendored Core runtime: %s\n" %
|
||||
error)
|
||||
return 1
|
||||
active_harness = os.path.join(
|
||||
runtime_root, "packages", "casan-harness")
|
||||
runtime_path = os.path.relpath(
|
||||
runtime_root, target).replace(os.sep, "/")
|
||||
else:
|
||||
runtime_removed = _remove_vendored_core(target)
|
||||
active_harness = harness
|
||||
runtime_root = install_root(harness)
|
||||
runtime_path = runtime_root
|
||||
|
||||
version = harness_version(active_harness)
|
||||
hhash, hsource = compute_harness_hash(active_harness)
|
||||
created = []
|
||||
backups = []
|
||||
previous_manifest = _load_json_or(
|
||||
@@ -889,7 +1058,11 @@ def cmd_init(args):
|
||||
"vscode-native-copilot": "unsupported_global_interception",
|
||||
},
|
||||
"harness_version": version,
|
||||
"adoption_model": "hybrid-global",
|
||||
"adoption_model": (
|
||||
"managed-global" if runtime_mode == "managed"
|
||||
else "vendored-project"),
|
||||
"runtime_mode": runtime_mode,
|
||||
"runtime_path": runtime_path,
|
||||
"target_level": lvl,
|
||||
"target_level_name": lvl_name,
|
||||
}
|
||||
@@ -903,7 +1076,9 @@ def cmd_init(args):
|
||||
"harness_hash": hhash,
|
||||
"hash_algo": "sha256",
|
||||
"hash_source": hsource,
|
||||
"install_root": install_root(harness),
|
||||
"install_root": runtime_root,
|
||||
"runtime_mode": runtime_mode,
|
||||
"runtime_path": runtime_path,
|
||||
"recorded_at": now_iso(),
|
||||
}
|
||||
p = os.path.join(cfg_dir, "version.lock")
|
||||
@@ -924,7 +1099,7 @@ def cmd_init(args):
|
||||
_write(p, "\n".join(env_lines), backups); created_add(p)
|
||||
|
||||
# Stable project-local bootstrap. It loads the config above, resolves the
|
||||
# global harness and verifies version.lock before dispatching an adapter.
|
||||
# managed/vendored Core and verifies version.lock before dispatching.
|
||||
bootstrap_source = os.path.join(devkit_root(), "templates", "project",
|
||||
"casan-hook.py")
|
||||
bootstrap_target = os.path.join(cfg_dir, "casan-hook.py")
|
||||
@@ -1067,6 +1242,10 @@ def cmd_init(args):
|
||||
"target_level_name": lvl_name,
|
||||
"harness_version": version,
|
||||
"harness_hash": hhash,
|
||||
"runtime_mode": runtime_mode,
|
||||
"runtime_path": runtime_path,
|
||||
"runtime_files": runtime_files,
|
||||
"runtime_removed": runtime_removed,
|
||||
"enforcement_mode": args.mode,
|
||||
"clients": clients,
|
||||
"created": created,
|
||||
@@ -1076,8 +1255,11 @@ def cmd_init(args):
|
||||
"level_extras": level_extras,
|
||||
"level_removed": level_removed,
|
||||
"level_retained": level_retained,
|
||||
"note": ("harness NOT copied into repo (hybrid model); selected client hooks "
|
||||
"MERGED and unselected CASAN hooks removed; run `casan doctor`"),
|
||||
"note": (
|
||||
"managed runtime is referenced by version/hash lock"
|
||||
if runtime_mode == "managed" else
|
||||
"production-only Core runtime vendored under .casan/runtime/casan-core"
|
||||
),
|
||||
}
|
||||
_emit_json_or_human(args, result, _render_init)
|
||||
if legacy_migration.get("manual_review"):
|
||||
@@ -1096,11 +1278,11 @@ def cmd_init(args):
|
||||
|
||||
|
||||
def cmd_verify(args):
|
||||
harness = resolve_harness(args.harness)
|
||||
if not harness:
|
||||
sys.stderr.write("casan verify-harness: cannot locate the harness.\n")
|
||||
return 1
|
||||
target = os.path.abspath(args.target or os.getcwd())
|
||||
harness = resolve_project_harness(target, args.harness)
|
||||
if not harness:
|
||||
sys.stderr.write("casan verify-harness: cannot locate the project runtime.\n")
|
||||
return 1
|
||||
lock_path = os.path.join(target, ".casan", "version.lock")
|
||||
if not os.path.exists(lock_path):
|
||||
sys.stderr.write("casan verify-harness: no .casan/version.lock (run `casan init` first).\n")
|
||||
@@ -1120,8 +1302,8 @@ def cmd_verify(args):
|
||||
}
|
||||
_emit_json_or_human(args, result, _render_verify)
|
||||
if not ok:
|
||||
sys.stderr.write("HARNESS_INTEGRITY_DRIFT — the resolved harness does not match the "
|
||||
"project pin. The global harness changed or was tampered.\n")
|
||||
sys.stderr.write("HARNESS_INTEGRITY_DRIFT — the resolved Core runtime does not match "
|
||||
"the project pin. The runtime changed or was tampered.\n")
|
||||
return 3
|
||||
return 0
|
||||
|
||||
@@ -1144,6 +1326,8 @@ def cmd_level(args):
|
||||
"installed_level": installed,
|
||||
"project_target_level": tl,
|
||||
"project_target_level_name": cfg.get("target_level_name"),
|
||||
"project_runtime_mode": cfg.get("runtime_mode", "managed"),
|
||||
"project_runtime_path": cfg.get("runtime_path"),
|
||||
"project_level_status": status_map.get(tl, "unknown") if tl else None,
|
||||
"levels": {
|
||||
"1 core": "implemented — harness + gates + CLI",
|
||||
@@ -1217,7 +1401,7 @@ def cmd_doctor(args):
|
||||
else:
|
||||
clients = config.get("clients") or []
|
||||
|
||||
harness = resolve_harness(args.harness)
|
||||
harness = resolve_project_harness(target, args.harness)
|
||||
lock = _load_json_or(os.path.join(target, ".casan", "version.lock"), {})
|
||||
expected = lock.get("harness_hash")
|
||||
actual, source = compute_live(harness) if harness else ("unavailable", "error")
|
||||
@@ -1502,6 +1686,8 @@ def cmd_uninstall(args):
|
||||
|
||||
removed, retained = _remove_owned_project_artifacts(
|
||||
target, owned_files, owned_file_hashes)
|
||||
if _remove_vendored_core(target):
|
||||
removed.append(".casan/runtime/casan-core")
|
||||
|
||||
for relative in (
|
||||
".casan/config.json",
|
||||
@@ -1567,6 +1753,11 @@ def main(argv=None):
|
||||
"(default auto: install when `code` is available)"))
|
||||
pi.add_argument("--level", default="core",
|
||||
help="packaging level to adopt: 1|core (default), 2|devkit, 3|platform (preview), 4|enterprise (refused)")
|
||||
pi.add_argument(
|
||||
"--runtime", choices=["managed", "vendored"],
|
||||
help=("Core runtime placement: managed uses the pinned global install "
|
||||
"(default for new adoption); vendored copies a production-only "
|
||||
"Core into the project; re-init preserves the current mode"))
|
||||
pi.add_argument("--mode", choices=["observe", "enforce"], default="enforce",
|
||||
help="agentic policy mode (default: enforce; use observe for a telemetry-only pilot)")
|
||||
pi.add_argument("--integration-mode", dest="integration_mode",
|
||||
|
||||
Reference in New Issue
Block a user