422 lines
18 KiB
Python
422 lines
18 KiB
Python
#!/usr/bin/env python3
|
|
"""Generate and independently rebuild Leo's deterministic identity version manifest.
|
|
|
|
The version manifest is an operator-facing projection over the stricter identity
|
|
compiler. It names the runtime, Git commit, individual skills, database
|
|
version, compiled identity, and session boundary without turning generated
|
|
views or session history into provenance.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import re
|
|
import sys
|
|
from pathlib import Path, PurePosixPath
|
|
from typing import Any
|
|
|
|
if __package__ in {None, ""}:
|
|
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
|
|
|
from scripts import leo_behavior_manifest as behavior
|
|
from scripts import leo_identity_manifest as identity
|
|
|
|
SCHEMA = "livingip.leoIdentityVersionManifest.v1"
|
|
DEFAULT_PROFILE = Path("fixtures/working-leo/leo-identity-v1/profile-template")
|
|
DEFAULT_HERMES_ROOT = Path("fixtures/working-leo/leo-identity-v1/hermes-runtime")
|
|
DEFAULT_DEPLOYMENT_ROOT = Path(".")
|
|
DEFAULT_SOURCE_ROOT = Path(".")
|
|
DEFAULT_DATABASE_FINGERPRINT = Path("fixtures/working-leo/leo-identity-v1/leo-database-fingerprint-v1.json")
|
|
DEFAULT_CONSTITUTION = Path("fixtures/working-leo/leo-identity-v1/leo-constitution-v1.json")
|
|
DEFAULT_DATABASE_IDENTITY = Path("fixtures/working-leo/leo-identity-v1/leo-database-identity-v1.json")
|
|
HEX_64 = re.compile(r"^[0-9a-f]{64}$")
|
|
|
|
|
|
class IdentityVersionManifestError(identity.IdentityManifestError):
|
|
"""The identity version receipt is incomplete, inconsistent, or drifted."""
|
|
|
|
|
|
def _require(condition: bool, message: str) -> None:
|
|
if not condition:
|
|
raise IdentityVersionManifestError(message)
|
|
|
|
|
|
def expected_authority_contract() -> dict[str, str]:
|
|
return {
|
|
"durable_knowledge_authority": "approved_postgres_rows_with_bound_evidence",
|
|
"compiled_identity_role": "derived_view_from_versioned_inputs_not_authority",
|
|
"session_history_role": "temporary_or_persisted_context_not_provenance",
|
|
}
|
|
|
|
|
|
def _runtime_version(identity_manifest: dict[str, Any]) -> dict[str, Any]:
|
|
core = identity_manifest["identity_inputs"]
|
|
runtime = core["runtime"]
|
|
model = core["model"]
|
|
return {
|
|
"source_commit": core["source_commit"],
|
|
"identity_runtime_sha256": runtime["identity_runtime_sha256"],
|
|
"teleo_infrastructure": {
|
|
"git_head": runtime["teleo_git_head"],
|
|
"identity_source_sha256": runtime["teleo_source_sha256"],
|
|
},
|
|
"hermes": {
|
|
"git_head": runtime["hermes_git_head"],
|
|
"source_sha256": runtime["hermes_source_sha256"],
|
|
},
|
|
"python": runtime["python"],
|
|
"model": {
|
|
"provider": model["provider"],
|
|
"default_model": model["default_model"],
|
|
"model_section_sha256": model["model_section_sha256"],
|
|
"hosted_weights": model["hosted_weights"],
|
|
"actual_model_required_in_turn_receipt": model["actual_model_required_in_turn_receipt"],
|
|
},
|
|
}
|
|
|
|
|
|
def _canonical_database_version(identity_manifest: dict[str, Any]) -> dict[str, Any]:
|
|
core = identity_manifest["identity_inputs"]
|
|
database = core["canonical_database"]
|
|
source = core["identity_sources"]["database_derived_identity"]
|
|
provenance = source["provenance"]
|
|
return {
|
|
"authority": database["authority"],
|
|
"canonical_authority_granted": database["canonical_authority_granted"],
|
|
"database": database["database"],
|
|
"database_user": database["database_user"],
|
|
"system_identifier": database["system_identifier"],
|
|
"fingerprint_sha256": database["fingerprint_sha256"],
|
|
"table_rows_sha256": database["table_rows_sha256"],
|
|
"structure_sha256": database["structure_sha256"],
|
|
"table_count": database["table_count"],
|
|
"total_rows": database["total_rows"],
|
|
"identity_records_sha256": source["semantic_sha256"],
|
|
"identity_record_count": source["record_count"],
|
|
"source_mode": provenance["mode"],
|
|
"same_transaction_as_fingerprint": provenance["same_transaction_as_fingerprint"],
|
|
"export_receipt_sha256": provenance["export_receipt_sha256"],
|
|
}
|
|
|
|
|
|
def _compiled_identity_version(identity_manifest: dict[str, Any]) -> dict[str, Any]:
|
|
return {
|
|
"role": "derived_view_from_versioned_inputs_not_authority",
|
|
"identity_inputs_sha256": identity_manifest["identity_inputs_sha256"],
|
|
"identity_manifest_sha256": identity_manifest["manifest_sha256"],
|
|
"views": identity_manifest["compiled_views"],
|
|
}
|
|
|
|
|
|
def _skill_versions(behavior_manifest: dict[str, Any], identity_manifest: dict[str, Any]) -> dict[str, Any]:
|
|
content = behavior_manifest["components"]["procedural_skills"]["content"]
|
|
identity.validate_content_manifest(content, label="procedural skills")
|
|
groups: dict[str, list[dict[str, Any]]] = {}
|
|
for item in content["files"]:
|
|
parts = PurePosixPath(item["path"]).parts
|
|
_require(
|
|
len(parts) >= 3 and parts[0] == "skills" and bool(parts[1]),
|
|
"procedural skill file is not inside a named skill directory",
|
|
)
|
|
relative_path = PurePosixPath(*parts[2:]).as_posix()
|
|
groups.setdefault(parts[1], []).append(
|
|
{
|
|
"path": relative_path,
|
|
"bytes": item["bytes"],
|
|
"mode": item["mode"],
|
|
"sha256": item["sha256"],
|
|
}
|
|
)
|
|
_require(bool(groups), "at least one named skill is required")
|
|
skills = []
|
|
for name in sorted(groups):
|
|
files = sorted(groups[name], key=lambda item: item["path"])
|
|
skills.append(
|
|
{
|
|
"name": name,
|
|
"files": files,
|
|
"file_count": len(files),
|
|
"total_bytes": sum(item["bytes"] for item in files),
|
|
"content_sha256": behavior.canonical_sha256(files),
|
|
}
|
|
)
|
|
result = {
|
|
"content_sha256": content["sha256"],
|
|
"file_count": content["file_count"],
|
|
"total_bytes": content["total_bytes"],
|
|
"skills": skills,
|
|
}
|
|
expected = identity_manifest["identity_inputs"]["skills"]
|
|
_require(result["content_sha256"] == expected["content_sha256"], "skill aggregate hash drift detected")
|
|
_require(result["file_count"] == expected["file_count"], "skill aggregate file count drift detected")
|
|
return result
|
|
|
|
|
|
def _validate_skill_versions(value: Any, identity_manifest: dict[str, Any]) -> None:
|
|
_require(isinstance(value, dict), "skill versions are missing")
|
|
_require(
|
|
set(value) == {"content_sha256", "file_count", "total_bytes", "skills"},
|
|
"skill version fields are invalid",
|
|
)
|
|
_require(isinstance(value.get("skills"), list) and bool(value["skills"]), "named skills are missing")
|
|
names: list[str] = []
|
|
reconstructed_files: list[dict[str, Any]] = []
|
|
total_bytes = 0
|
|
for skill in value["skills"]:
|
|
_require(
|
|
isinstance(skill, dict) and set(skill) == {"name", "files", "file_count", "total_bytes", "content_sha256"},
|
|
"named skill fields are invalid",
|
|
)
|
|
name = skill.get("name")
|
|
files = skill.get("files")
|
|
_require(
|
|
isinstance(name, str) and bool(name) and PurePosixPath(name).parts == (name,) and name not in {".", ".."},
|
|
"skill name is invalid",
|
|
)
|
|
_require(isinstance(files, list) and bool(files), f"skill {name} has no files")
|
|
names.append(name)
|
|
paths: list[str] = []
|
|
skill_bytes = 0
|
|
for item in files:
|
|
_require(
|
|
isinstance(item, dict) and set(item) == {"path", "bytes", "mode", "sha256"},
|
|
f"skill {name} file fields are invalid",
|
|
)
|
|
path = item.get("path")
|
|
size = item.get("bytes")
|
|
mode = item.get("mode")
|
|
_require(
|
|
isinstance(path, str)
|
|
and bool(path)
|
|
and not path.startswith("/")
|
|
and ".." not in PurePosixPath(path).parts,
|
|
f"skill {name} path is invalid",
|
|
)
|
|
_require(isinstance(size, int) and not isinstance(size, bool) and size >= 0, "skill byte size is invalid")
|
|
_require(isinstance(mode, str) and bool(re.fullmatch(r"0o[0-7]{3}", mode)), "skill mode is invalid")
|
|
_require(
|
|
isinstance(item.get("sha256"), str) and bool(HEX_64.fullmatch(item["sha256"])), "skill hash is invalid"
|
|
)
|
|
paths.append(path)
|
|
skill_bytes += size
|
|
reconstructed_files.append({**item, "path": f"skills/{name}/{path}"})
|
|
_require(paths == sorted(paths) and len(paths) == len(set(paths)), f"skill {name} paths are not sorted")
|
|
_require(skill.get("file_count") == len(files), f"skill {name} file count is invalid")
|
|
_require(skill.get("total_bytes") == skill_bytes, f"skill {name} byte count is invalid")
|
|
_require(skill.get("content_sha256") == behavior.canonical_sha256(files), f"skill {name} hash drift detected")
|
|
total_bytes += skill_bytes
|
|
_require(names == sorted(names) and len(names) == len(set(names)), "skill names are not unique and sorted")
|
|
reconstructed_files.sort(key=lambda item: item["path"])
|
|
aggregate = behavior.canonical_sha256({"files": reconstructed_files, "missing": [], "symlinks": []})
|
|
_require(value.get("content_sha256") == aggregate, "skill aggregate content drift detected")
|
|
_require(value.get("file_count") == len(reconstructed_files), "skill aggregate file count is invalid")
|
|
_require(value.get("total_bytes") == total_bytes, "skill aggregate byte count is invalid")
|
|
expected = identity_manifest["identity_inputs"]["skills"]
|
|
_require(value["content_sha256"] == expected["content_sha256"], "skills are not bound to the identity manifest")
|
|
_require(value["file_count"] == expected["file_count"], "skill count is not bound to the identity manifest")
|
|
|
|
|
|
def assemble_version_manifest(
|
|
*, behavior_manifest: dict[str, Any], identity_manifest: dict[str, Any]
|
|
) -> dict[str, Any]:
|
|
identity.validate_behavior_manifest(behavior_manifest)
|
|
identity.validate_identity_manifest(identity_manifest)
|
|
core = identity_manifest["identity_inputs"]
|
|
_require(
|
|
behavior_manifest["identity_runtime_sha256"] == core["runtime"]["identity_runtime_sha256"],
|
|
"behavior runtime is not bound to the identity manifest",
|
|
)
|
|
_require(
|
|
behavior_manifest["teleo_infrastructure_runtime"]["git_head"] == core["source_commit"],
|
|
"behavior source commit is not bound to the identity manifest",
|
|
)
|
|
stable = {
|
|
"schema": SCHEMA,
|
|
"identity_name": "Leo",
|
|
"runtime_version": _runtime_version(identity_manifest),
|
|
"skill_versions": _skill_versions(behavior_manifest, identity_manifest),
|
|
"canonical_database_version": _canonical_database_version(identity_manifest),
|
|
"compiled_identity_version": _compiled_identity_version(identity_manifest),
|
|
"session_boundary": core["session_boundary"],
|
|
"authority_contract": expected_authority_contract(),
|
|
"identity_manifest": identity_manifest,
|
|
}
|
|
return {**stable, "manifest_sha256": behavior.canonical_sha256(stable)}
|
|
|
|
|
|
def build_version_manifest(
|
|
*,
|
|
profile: Path,
|
|
hermes_root: Path,
|
|
deployment_root: Path,
|
|
source_root: Path,
|
|
database_fingerprint: Path,
|
|
constitution: Path,
|
|
database_identity: Path,
|
|
) -> dict[str, Any]:
|
|
behavior_manifest = behavior.build_manifest(profile, hermes_root, deployment_root)
|
|
identity_manifest = identity.build_identity_manifest(
|
|
behavior_manifest=behavior_manifest,
|
|
database_fingerprint_path=database_fingerprint,
|
|
constitution_path=constitution,
|
|
database_identity_path=database_identity,
|
|
source_root=source_root,
|
|
)
|
|
identity.verify_bound_sources(identity_manifest, source_root)
|
|
return assemble_version_manifest(
|
|
behavior_manifest=behavior_manifest,
|
|
identity_manifest=identity_manifest,
|
|
)
|
|
|
|
|
|
def validate_version_manifest(value: dict[str, Any]) -> None:
|
|
_require(
|
|
set(value)
|
|
== {
|
|
"schema",
|
|
"identity_name",
|
|
"runtime_version",
|
|
"skill_versions",
|
|
"canonical_database_version",
|
|
"compiled_identity_version",
|
|
"session_boundary",
|
|
"authority_contract",
|
|
"identity_manifest",
|
|
"manifest_sha256",
|
|
},
|
|
"identity version manifest fields are invalid",
|
|
)
|
|
_require(value.get("schema") == SCHEMA, "unsupported identity version manifest schema")
|
|
_require(value.get("identity_name") == "Leo", "identity version must describe Leo")
|
|
expected_hash = value.get("manifest_sha256")
|
|
_require(isinstance(expected_hash, str) and bool(HEX_64.fullmatch(expected_hash)), "manifest hash is invalid")
|
|
stable = {key: item for key, item in value.items() if key != "manifest_sha256"}
|
|
_require(behavior.canonical_sha256(stable) == expected_hash, "identity version manifest hash drift detected")
|
|
embedded = value.get("identity_manifest")
|
|
_require(isinstance(embedded, dict), "embedded identity manifest is missing")
|
|
identity.validate_identity_manifest(embedded)
|
|
core = embedded["identity_inputs"]
|
|
_require(value.get("runtime_version") == _runtime_version(embedded), "runtime version projection drift detected")
|
|
_validate_skill_versions(value.get("skill_versions"), embedded)
|
|
_require(
|
|
value.get("canonical_database_version") == _canonical_database_version(embedded),
|
|
"canonical database version projection drift detected",
|
|
)
|
|
_require(
|
|
value.get("compiled_identity_version") == _compiled_identity_version(embedded),
|
|
"compiled identity version projection drift detected",
|
|
)
|
|
_require(value.get("session_boundary") == core["session_boundary"], "session boundary projection drift detected")
|
|
_require(
|
|
value.get("authority_contract") == expected_authority_contract(), "identity authority contract drift detected"
|
|
)
|
|
|
|
|
|
def verify_against_inputs(
|
|
value: dict[str, Any],
|
|
*,
|
|
profile: Path,
|
|
hermes_root: Path,
|
|
deployment_root: Path,
|
|
source_root: Path,
|
|
database_fingerprint: Path,
|
|
constitution: Path,
|
|
database_identity: Path,
|
|
) -> None:
|
|
validate_version_manifest(value)
|
|
rebuilt = build_version_manifest(
|
|
profile=profile,
|
|
hermes_root=hermes_root,
|
|
deployment_root=deployment_root,
|
|
source_root=source_root,
|
|
database_fingerprint=database_fingerprint,
|
|
constitution=constitution,
|
|
database_identity=database_identity,
|
|
)
|
|
_require(rebuilt == value, "identity version manifest does not match the supplied versioned inputs")
|
|
|
|
|
|
def write_json(path: Path, value: dict[str, Any]) -> None:
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
path.write_text(json.dumps(value, indent=2, sort_keys=True) + "\n", encoding="utf-8")
|
|
|
|
|
|
def summary(value: dict[str, Any]) -> dict[str, Any]:
|
|
return {
|
|
"status": "ok",
|
|
"manifest_sha256": value["manifest_sha256"],
|
|
"source_commit": value["runtime_version"]["source_commit"],
|
|
"skills": [item["name"] for item in value["skill_versions"]["skills"]],
|
|
"canonical_database": {
|
|
"authority": value["canonical_database_version"]["authority"],
|
|
"canonical_authority_granted": value["canonical_database_version"]["canonical_authority_granted"],
|
|
"fingerprint_sha256": value["canonical_database_version"]["fingerprint_sha256"],
|
|
},
|
|
"compiled_identity_sha256": value["compiled_identity_version"]["identity_inputs_sha256"],
|
|
"session_boundary": value["session_boundary"]["classification"],
|
|
}
|
|
|
|
|
|
def _add_input_arguments(parser: argparse.ArgumentParser) -> None:
|
|
parser.add_argument("--profile", type=Path, default=DEFAULT_PROFILE)
|
|
parser.add_argument("--hermes-root", type=Path, default=DEFAULT_HERMES_ROOT)
|
|
parser.add_argument("--deployment-root", type=Path, default=DEFAULT_DEPLOYMENT_ROOT)
|
|
parser.add_argument("--source-root", type=Path, default=DEFAULT_SOURCE_ROOT)
|
|
parser.add_argument("--database-fingerprint", type=Path, default=DEFAULT_DATABASE_FINGERPRINT)
|
|
parser.add_argument("--constitution", type=Path, default=DEFAULT_CONSTITUTION)
|
|
parser.add_argument("--database-identity", type=Path, default=DEFAULT_DATABASE_IDENTITY)
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
generate = subparsers.add_parser("generate", help="generate a deterministic identity version manifest")
|
|
_add_input_arguments(generate)
|
|
generate.add_argument("--output", type=Path, required=True)
|
|
verify = subparsers.add_parser("verify", help="rebuild and compare a manifest from its versioned inputs")
|
|
verify.add_argument("--manifest", type=Path, required=True)
|
|
_add_input_arguments(verify)
|
|
args = parser.parse_args()
|
|
try:
|
|
if args.command == "generate":
|
|
manifest = build_version_manifest(
|
|
profile=args.profile,
|
|
hermes_root=args.hermes_root,
|
|
deployment_root=args.deployment_root,
|
|
source_root=args.source_root,
|
|
database_fingerprint=args.database_fingerprint,
|
|
constitution=args.constitution,
|
|
database_identity=args.database_identity,
|
|
)
|
|
write_json(args.output, manifest)
|
|
else:
|
|
manifest = identity.load_json(args.manifest, "identity version manifest")
|
|
verify_against_inputs(
|
|
manifest,
|
|
profile=args.profile,
|
|
hermes_root=args.hermes_root,
|
|
deployment_root=args.deployment_root,
|
|
source_root=args.source_root,
|
|
database_fingerprint=args.database_fingerprint,
|
|
constitution=args.constitution,
|
|
database_identity=args.database_identity,
|
|
)
|
|
print(json.dumps(summary(manifest), sort_keys=True))
|
|
return 0
|
|
except (identity.IdentityManifestError, OSError, ValueError) as exc:
|
|
print(
|
|
json.dumps(
|
|
{
|
|
"status": "error",
|
|
"error": "identity_version_manifest_invalid",
|
|
"message": str(exc),
|
|
},
|
|
sort_keys=True,
|
|
)
|
|
)
|
|
return 2
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|