@mc-and-his-agents/loom-installer 0.1.21 → 0.1.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/package.json +1 -1
  2. package/payload/manifest.json +62 -62
  3. package/payload/plugin/loom/skills/shared/scripts/loom_check.py +21 -0
  4. package/payload/plugin/loom/skills/shared/scripts/loom_init.py +48 -40
  5. package/payload/plugin/loom/skills/shared/scripts/runtime_state.py +17 -0
  6. package/payload/skills/loom-adopt/.loom-runtime/shared/scripts/loom_check.py +21 -0
  7. package/payload/skills/loom-adopt/.loom-runtime/shared/scripts/loom_init.py +48 -40
  8. package/payload/skills/loom-adopt/.loom-runtime/shared/scripts/runtime_state.py +17 -0
  9. package/payload/skills/loom-handoff/.loom-runtime/shared/scripts/loom_check.py +21 -0
  10. package/payload/skills/loom-handoff/.loom-runtime/shared/scripts/loom_init.py +48 -40
  11. package/payload/skills/loom-handoff/.loom-runtime/shared/scripts/runtime_state.py +17 -0
  12. package/payload/skills/loom-init/.loom-runtime/shared/scripts/loom_check.py +21 -0
  13. package/payload/skills/loom-init/.loom-runtime/shared/scripts/loom_init.py +48 -40
  14. package/payload/skills/loom-init/.loom-runtime/shared/scripts/runtime_state.py +17 -0
  15. package/payload/skills/loom-merge-ready/.loom-runtime/shared/scripts/loom_check.py +21 -0
  16. package/payload/skills/loom-merge-ready/.loom-runtime/shared/scripts/loom_init.py +48 -40
  17. package/payload/skills/loom-merge-ready/.loom-runtime/shared/scripts/runtime_state.py +17 -0
  18. package/payload/skills/loom-pre-review/.loom-runtime/shared/scripts/loom_check.py +21 -0
  19. package/payload/skills/loom-pre-review/.loom-runtime/shared/scripts/loom_init.py +48 -40
  20. package/payload/skills/loom-pre-review/.loom-runtime/shared/scripts/runtime_state.py +17 -0
  21. package/payload/skills/loom-resume/.loom-runtime/shared/scripts/loom_check.py +21 -0
  22. package/payload/skills/loom-resume/.loom-runtime/shared/scripts/loom_init.py +48 -40
  23. package/payload/skills/loom-resume/.loom-runtime/shared/scripts/runtime_state.py +17 -0
  24. package/payload/skills/loom-retire/.loom-runtime/shared/scripts/loom_check.py +21 -0
  25. package/payload/skills/loom-retire/.loom-runtime/shared/scripts/loom_init.py +48 -40
  26. package/payload/skills/loom-retire/.loom-runtime/shared/scripts/runtime_state.py +17 -0
  27. package/payload/skills/loom-review/.loom-runtime/shared/scripts/loom_check.py +21 -0
  28. package/payload/skills/loom-review/.loom-runtime/shared/scripts/loom_init.py +48 -40
  29. package/payload/skills/loom-review/.loom-runtime/shared/scripts/runtime_state.py +17 -0
  30. package/payload/skills/loom-spec-review/.loom-runtime/shared/scripts/loom_check.py +21 -0
  31. package/payload/skills/loom-spec-review/.loom-runtime/shared/scripts/loom_init.py +48 -40
  32. package/payload/skills/loom-spec-review/.loom-runtime/shared/scripts/runtime_state.py +17 -0
@@ -4,6 +4,7 @@
4
4
  from __future__ import annotations
5
5
 
6
6
  import json
7
+ import hashlib
7
8
  import os
8
9
  from pathlib import Path
9
10
  from typing import Any
@@ -85,6 +86,14 @@ def detect_carrier(caller_file: str) -> str | None:
85
86
  return None
86
87
 
87
88
 
89
+ def sha256_file(path: Path) -> str:
90
+ digest = hashlib.sha256()
91
+ with path.open("rb") as handle:
92
+ for chunk in iter(lambda: handle.read(1024 * 1024), b""):
93
+ digest.update(chunk)
94
+ return digest.hexdigest()
95
+
96
+
88
97
  def _default_scene_for_carrier(carrier: str) -> str:
89
98
  if carrier == "repo-local-wrapper":
90
99
  return "repo-local-demo"
@@ -284,6 +293,14 @@ def _validate_bootstrapped_runtime(caller_file: str) -> tuple[dict[str, Any], li
284
293
  runtime_file = runtime_root / Path(relative).name
285
294
  if not runtime_file.exists():
286
295
  errors.append(f"bootstrapped runtime file is missing: `{relative}`")
296
+ continue
297
+ expected_hash = artifact.get("sha256")
298
+ if not isinstance(expected_hash, str) or not expected_hash.strip():
299
+ errors.append(f"bootstrap runtime artifact `{relative}` must declare sha256 provenance")
300
+ continue
301
+ actual_hash = sha256_file(runtime_file)
302
+ if actual_hash != expected_hash:
303
+ errors.append(f"bootstrap runtime artifact `{relative}` sha256 drifted")
287
304
 
288
305
  status = "pass" if not errors else "block"
289
306
  summary = (
@@ -3711,6 +3711,27 @@ def check_daily_execution_cli(root: Path) -> list[Failure]:
3711
3711
  elif payload.get("result") != "block":
3712
3712
  failures.append(Failure("daily-execution-cli", "`bootstrapped runtime-state` must block when the bootstrap manifest drifts"))
3713
3713
 
3714
+ hash_drift_bootstrap = tmp_root / "hash-drift-bootstrapped-target"
3715
+ shutil.copytree(example_target, hash_drift_bootstrap)
3716
+ manifest_path = hash_drift_bootstrap / ".loom" / "bootstrap" / "manifest.json"
3717
+ manifest = load_json_file(manifest_path)
3718
+ if isinstance(manifest, dict):
3719
+ artifacts = manifest.get("artifacts")
3720
+ if isinstance(artifacts, list):
3721
+ for artifact in artifacts:
3722
+ if isinstance(artifact, dict) and artifact.get("path") == ".loom/bin/runtime_state.py":
3723
+ artifact["sha256"] = "0" * 64
3724
+ manifest_path.write_text(json.dumps(manifest, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
3725
+ payload, error = load_command_json(
3726
+ root,
3727
+ ["python3", ".loom/bin/loom_init.py", "runtime-state", "--target", "."],
3728
+ cwd=hash_drift_bootstrap,
3729
+ )
3730
+ if error:
3731
+ failures.append(Failure("daily-execution-cli", f"`bootstrapped runtime-state` provenance hash drift failed unexpectedly: {error}"))
3732
+ elif payload.get("result") != "block":
3733
+ failures.append(Failure("daily-execution-cli", "`bootstrapped runtime-state` must block when runtime provenance hashes drift"))
3734
+
3714
3735
  if shutil.which("git") is not None:
3715
3736
  with tempfile.TemporaryDirectory(prefix="loom-check-installed-pre-merge-") as tmp:
3716
3737
  tmp_root = Path(tmp)
@@ -4,6 +4,7 @@
4
4
  from __future__ import annotations
5
5
 
6
6
  import argparse
7
+ import hashlib
7
8
  import json
8
9
  import os
9
10
  import re
@@ -27,6 +28,17 @@ TOOL_VERSION = "1.3.0"
27
28
  CONTRACT_VERSION = "1.3.0"
28
29
  WORK_ITEM_ID = "INIT-0001"
29
30
 
31
+ RUNTIME_ARTIFACT_SOURCES = {
32
+ ".loom/bin/loom_init.py": RUNTIME_SOURCE,
33
+ ".loom/bin/fact_chain_support.py": FACT_CHAIN_RUNTIME_SOURCE,
34
+ ".loom/bin/governance_surface.py": GOVERNANCE_RUNTIME_SOURCE,
35
+ ".loom/bin/loom_flow.py": FLOW_RUNTIME_SOURCE,
36
+ ".loom/bin/loom_status.py": STATUS_RUNTIME_SOURCE,
37
+ ".loom/bin/runtime_paths.py": "skills/shared/scripts/runtime_paths.py",
38
+ ".loom/bin/runtime_state.py": "skills/shared/scripts/runtime_state.py",
39
+ ".loom/bin/loom_check.py": CHECK_RUNTIME_SOURCE,
40
+ }
41
+
30
42
  ROOT_BOUNDARY_FILES = (
31
43
  "AGENTS.md",
32
44
  "WORKFLOW.md",
@@ -816,46 +828,14 @@ def initial_artifacts(target_root: Path, install_pr_template: bool, adoption_pat
816
828
  "kind": "capability-map",
817
829
  "source": "generated",
818
830
  },
819
- {
820
- "path": ".loom/bin/loom_init.py",
821
- "kind": "loom-tool",
822
- "source": RUNTIME_SOURCE,
823
- },
824
- {
825
- "path": ".loom/bin/fact_chain_support.py",
826
- "kind": "loom-tool-support",
827
- "source": FACT_CHAIN_RUNTIME_SOURCE,
828
- },
829
- {
830
- "path": ".loom/bin/governance_surface.py",
831
- "kind": "loom-tool-support",
832
- "source": GOVERNANCE_RUNTIME_SOURCE,
833
- },
834
- {
835
- "path": ".loom/bin/loom_flow.py",
836
- "kind": "loom-tool",
837
- "source": FLOW_RUNTIME_SOURCE,
838
- },
839
- {
840
- "path": ".loom/bin/loom_status.py",
841
- "kind": "loom-tool",
842
- "source": STATUS_RUNTIME_SOURCE,
843
- },
844
- {
845
- "path": ".loom/bin/runtime_paths.py",
846
- "kind": "loom-tool-support",
847
- "source": "skills/shared/scripts/runtime_paths.py",
848
- },
849
- {
850
- "path": ".loom/bin/runtime_state.py",
851
- "kind": "loom-tool-support",
852
- "source": "skills/shared/scripts/runtime_state.py",
853
- },
854
- {
855
- "path": ".loom/bin/loom_check.py",
856
- "kind": "loom-tool",
857
- "source": CHECK_RUNTIME_SOURCE,
858
- },
831
+ runtime_artifact(".loom/bin/loom_init.py", "loom-tool", RUNTIME_SOURCE),
832
+ runtime_artifact(".loom/bin/fact_chain_support.py", "loom-tool-support", FACT_CHAIN_RUNTIME_SOURCE),
833
+ runtime_artifact(".loom/bin/governance_surface.py", "loom-tool-support", GOVERNANCE_RUNTIME_SOURCE),
834
+ runtime_artifact(".loom/bin/loom_flow.py", "loom-tool", FLOW_RUNTIME_SOURCE),
835
+ runtime_artifact(".loom/bin/loom_status.py", "loom-tool", STATUS_RUNTIME_SOURCE),
836
+ runtime_artifact(".loom/bin/runtime_paths.py", "loom-tool-support", "skills/shared/scripts/runtime_paths.py"),
837
+ runtime_artifact(".loom/bin/runtime_state.py", "loom-tool-support", "skills/shared/scripts/runtime_state.py"),
838
+ runtime_artifact(".loom/bin/loom_check.py", "loom-tool", CHECK_RUNTIME_SOURCE),
859
839
  ]
860
840
  if uses_attach_only_path(adoption_path):
861
841
  artifacts.extend(
@@ -1362,6 +1342,34 @@ def copy_file(source: Path, target: Path, force: bool) -> bool:
1362
1342
  return write_text(target, content, force=force)
1363
1343
 
1364
1344
 
1345
+ def sha256_file(path: Path) -> str:
1346
+ digest = hashlib.sha256()
1347
+ with path.open("rb") as handle:
1348
+ for chunk in iter(lambda: handle.read(1024 * 1024), b""):
1349
+ digest.update(chunk)
1350
+ return digest.hexdigest()
1351
+
1352
+
1353
+ def runtime_artifact(path: str, kind: str, source: str) -> dict[str, str]:
1354
+ runtime_sources = {
1355
+ ".loom/bin/loom_init.py": Path(__file__),
1356
+ ".loom/bin/fact_chain_support.py": Path(__file__).with_name("fact_chain_support.py"),
1357
+ ".loom/bin/governance_surface.py": Path(__file__).with_name("governance_surface.py"),
1358
+ ".loom/bin/loom_flow.py": Path(__file__).with_name("loom_flow.py"),
1359
+ ".loom/bin/loom_status.py": Path(__file__).with_name("loom_status.py"),
1360
+ ".loom/bin/runtime_paths.py": Path(__file__).with_name("runtime_paths.py"),
1361
+ ".loom/bin/runtime_state.py": Path(__file__).with_name("runtime_state.py"),
1362
+ ".loom/bin/loom_check.py": Path(__file__).with_name("loom_check.py"),
1363
+ }
1364
+ source_path = runtime_sources[path]
1365
+ return {
1366
+ "path": path,
1367
+ "kind": kind,
1368
+ "source": source,
1369
+ "sha256": sha256_file(source_path),
1370
+ }
1371
+
1372
+
1365
1373
  def manifest_payload(result: dict[str, object]) -> dict[str, object]:
1366
1374
  return {
1367
1375
  "schema_version": "loom-bootstrap-manifest/v1",
@@ -4,6 +4,7 @@
4
4
  from __future__ import annotations
5
5
 
6
6
  import json
7
+ import hashlib
7
8
  import os
8
9
  from pathlib import Path
9
10
  from typing import Any
@@ -85,6 +86,14 @@ def detect_carrier(caller_file: str) -> str | None:
85
86
  return None
86
87
 
87
88
 
89
+ def sha256_file(path: Path) -> str:
90
+ digest = hashlib.sha256()
91
+ with path.open("rb") as handle:
92
+ for chunk in iter(lambda: handle.read(1024 * 1024), b""):
93
+ digest.update(chunk)
94
+ return digest.hexdigest()
95
+
96
+
88
97
  def _default_scene_for_carrier(carrier: str) -> str:
89
98
  if carrier == "repo-local-wrapper":
90
99
  return "repo-local-demo"
@@ -284,6 +293,14 @@ def _validate_bootstrapped_runtime(caller_file: str) -> tuple[dict[str, Any], li
284
293
  runtime_file = runtime_root / Path(relative).name
285
294
  if not runtime_file.exists():
286
295
  errors.append(f"bootstrapped runtime file is missing: `{relative}`")
296
+ continue
297
+ expected_hash = artifact.get("sha256")
298
+ if not isinstance(expected_hash, str) or not expected_hash.strip():
299
+ errors.append(f"bootstrap runtime artifact `{relative}` must declare sha256 provenance")
300
+ continue
301
+ actual_hash = sha256_file(runtime_file)
302
+ if actual_hash != expected_hash:
303
+ errors.append(f"bootstrap runtime artifact `{relative}` sha256 drifted")
287
304
 
288
305
  status = "pass" if not errors else "block"
289
306
  summary = (
@@ -3711,6 +3711,27 @@ def check_daily_execution_cli(root: Path) -> list[Failure]:
3711
3711
  elif payload.get("result") != "block":
3712
3712
  failures.append(Failure("daily-execution-cli", "`bootstrapped runtime-state` must block when the bootstrap manifest drifts"))
3713
3713
 
3714
+ hash_drift_bootstrap = tmp_root / "hash-drift-bootstrapped-target"
3715
+ shutil.copytree(example_target, hash_drift_bootstrap)
3716
+ manifest_path = hash_drift_bootstrap / ".loom" / "bootstrap" / "manifest.json"
3717
+ manifest = load_json_file(manifest_path)
3718
+ if isinstance(manifest, dict):
3719
+ artifacts = manifest.get("artifacts")
3720
+ if isinstance(artifacts, list):
3721
+ for artifact in artifacts:
3722
+ if isinstance(artifact, dict) and artifact.get("path") == ".loom/bin/runtime_state.py":
3723
+ artifact["sha256"] = "0" * 64
3724
+ manifest_path.write_text(json.dumps(manifest, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
3725
+ payload, error = load_command_json(
3726
+ root,
3727
+ ["python3", ".loom/bin/loom_init.py", "runtime-state", "--target", "."],
3728
+ cwd=hash_drift_bootstrap,
3729
+ )
3730
+ if error:
3731
+ failures.append(Failure("daily-execution-cli", f"`bootstrapped runtime-state` provenance hash drift failed unexpectedly: {error}"))
3732
+ elif payload.get("result") != "block":
3733
+ failures.append(Failure("daily-execution-cli", "`bootstrapped runtime-state` must block when runtime provenance hashes drift"))
3734
+
3714
3735
  if shutil.which("git") is not None:
3715
3736
  with tempfile.TemporaryDirectory(prefix="loom-check-installed-pre-merge-") as tmp:
3716
3737
  tmp_root = Path(tmp)
@@ -4,6 +4,7 @@
4
4
  from __future__ import annotations
5
5
 
6
6
  import argparse
7
+ import hashlib
7
8
  import json
8
9
  import os
9
10
  import re
@@ -27,6 +28,17 @@ TOOL_VERSION = "1.3.0"
27
28
  CONTRACT_VERSION = "1.3.0"
28
29
  WORK_ITEM_ID = "INIT-0001"
29
30
 
31
+ RUNTIME_ARTIFACT_SOURCES = {
32
+ ".loom/bin/loom_init.py": RUNTIME_SOURCE,
33
+ ".loom/bin/fact_chain_support.py": FACT_CHAIN_RUNTIME_SOURCE,
34
+ ".loom/bin/governance_surface.py": GOVERNANCE_RUNTIME_SOURCE,
35
+ ".loom/bin/loom_flow.py": FLOW_RUNTIME_SOURCE,
36
+ ".loom/bin/loom_status.py": STATUS_RUNTIME_SOURCE,
37
+ ".loom/bin/runtime_paths.py": "skills/shared/scripts/runtime_paths.py",
38
+ ".loom/bin/runtime_state.py": "skills/shared/scripts/runtime_state.py",
39
+ ".loom/bin/loom_check.py": CHECK_RUNTIME_SOURCE,
40
+ }
41
+
30
42
  ROOT_BOUNDARY_FILES = (
31
43
  "AGENTS.md",
32
44
  "WORKFLOW.md",
@@ -816,46 +828,14 @@ def initial_artifacts(target_root: Path, install_pr_template: bool, adoption_pat
816
828
  "kind": "capability-map",
817
829
  "source": "generated",
818
830
  },
819
- {
820
- "path": ".loom/bin/loom_init.py",
821
- "kind": "loom-tool",
822
- "source": RUNTIME_SOURCE,
823
- },
824
- {
825
- "path": ".loom/bin/fact_chain_support.py",
826
- "kind": "loom-tool-support",
827
- "source": FACT_CHAIN_RUNTIME_SOURCE,
828
- },
829
- {
830
- "path": ".loom/bin/governance_surface.py",
831
- "kind": "loom-tool-support",
832
- "source": GOVERNANCE_RUNTIME_SOURCE,
833
- },
834
- {
835
- "path": ".loom/bin/loom_flow.py",
836
- "kind": "loom-tool",
837
- "source": FLOW_RUNTIME_SOURCE,
838
- },
839
- {
840
- "path": ".loom/bin/loom_status.py",
841
- "kind": "loom-tool",
842
- "source": STATUS_RUNTIME_SOURCE,
843
- },
844
- {
845
- "path": ".loom/bin/runtime_paths.py",
846
- "kind": "loom-tool-support",
847
- "source": "skills/shared/scripts/runtime_paths.py",
848
- },
849
- {
850
- "path": ".loom/bin/runtime_state.py",
851
- "kind": "loom-tool-support",
852
- "source": "skills/shared/scripts/runtime_state.py",
853
- },
854
- {
855
- "path": ".loom/bin/loom_check.py",
856
- "kind": "loom-tool",
857
- "source": CHECK_RUNTIME_SOURCE,
858
- },
831
+ runtime_artifact(".loom/bin/loom_init.py", "loom-tool", RUNTIME_SOURCE),
832
+ runtime_artifact(".loom/bin/fact_chain_support.py", "loom-tool-support", FACT_CHAIN_RUNTIME_SOURCE),
833
+ runtime_artifact(".loom/bin/governance_surface.py", "loom-tool-support", GOVERNANCE_RUNTIME_SOURCE),
834
+ runtime_artifact(".loom/bin/loom_flow.py", "loom-tool", FLOW_RUNTIME_SOURCE),
835
+ runtime_artifact(".loom/bin/loom_status.py", "loom-tool", STATUS_RUNTIME_SOURCE),
836
+ runtime_artifact(".loom/bin/runtime_paths.py", "loom-tool-support", "skills/shared/scripts/runtime_paths.py"),
837
+ runtime_artifact(".loom/bin/runtime_state.py", "loom-tool-support", "skills/shared/scripts/runtime_state.py"),
838
+ runtime_artifact(".loom/bin/loom_check.py", "loom-tool", CHECK_RUNTIME_SOURCE),
859
839
  ]
860
840
  if uses_attach_only_path(adoption_path):
861
841
  artifacts.extend(
@@ -1362,6 +1342,34 @@ def copy_file(source: Path, target: Path, force: bool) -> bool:
1362
1342
  return write_text(target, content, force=force)
1363
1343
 
1364
1344
 
1345
+ def sha256_file(path: Path) -> str:
1346
+ digest = hashlib.sha256()
1347
+ with path.open("rb") as handle:
1348
+ for chunk in iter(lambda: handle.read(1024 * 1024), b""):
1349
+ digest.update(chunk)
1350
+ return digest.hexdigest()
1351
+
1352
+
1353
+ def runtime_artifact(path: str, kind: str, source: str) -> dict[str, str]:
1354
+ runtime_sources = {
1355
+ ".loom/bin/loom_init.py": Path(__file__),
1356
+ ".loom/bin/fact_chain_support.py": Path(__file__).with_name("fact_chain_support.py"),
1357
+ ".loom/bin/governance_surface.py": Path(__file__).with_name("governance_surface.py"),
1358
+ ".loom/bin/loom_flow.py": Path(__file__).with_name("loom_flow.py"),
1359
+ ".loom/bin/loom_status.py": Path(__file__).with_name("loom_status.py"),
1360
+ ".loom/bin/runtime_paths.py": Path(__file__).with_name("runtime_paths.py"),
1361
+ ".loom/bin/runtime_state.py": Path(__file__).with_name("runtime_state.py"),
1362
+ ".loom/bin/loom_check.py": Path(__file__).with_name("loom_check.py"),
1363
+ }
1364
+ source_path = runtime_sources[path]
1365
+ return {
1366
+ "path": path,
1367
+ "kind": kind,
1368
+ "source": source,
1369
+ "sha256": sha256_file(source_path),
1370
+ }
1371
+
1372
+
1365
1373
  def manifest_payload(result: dict[str, object]) -> dict[str, object]:
1366
1374
  return {
1367
1375
  "schema_version": "loom-bootstrap-manifest/v1",
@@ -4,6 +4,7 @@
4
4
  from __future__ import annotations
5
5
 
6
6
  import json
7
+ import hashlib
7
8
  import os
8
9
  from pathlib import Path
9
10
  from typing import Any
@@ -85,6 +86,14 @@ def detect_carrier(caller_file: str) -> str | None:
85
86
  return None
86
87
 
87
88
 
89
+ def sha256_file(path: Path) -> str:
90
+ digest = hashlib.sha256()
91
+ with path.open("rb") as handle:
92
+ for chunk in iter(lambda: handle.read(1024 * 1024), b""):
93
+ digest.update(chunk)
94
+ return digest.hexdigest()
95
+
96
+
88
97
  def _default_scene_for_carrier(carrier: str) -> str:
89
98
  if carrier == "repo-local-wrapper":
90
99
  return "repo-local-demo"
@@ -284,6 +293,14 @@ def _validate_bootstrapped_runtime(caller_file: str) -> tuple[dict[str, Any], li
284
293
  runtime_file = runtime_root / Path(relative).name
285
294
  if not runtime_file.exists():
286
295
  errors.append(f"bootstrapped runtime file is missing: `{relative}`")
296
+ continue
297
+ expected_hash = artifact.get("sha256")
298
+ if not isinstance(expected_hash, str) or not expected_hash.strip():
299
+ errors.append(f"bootstrap runtime artifact `{relative}` must declare sha256 provenance")
300
+ continue
301
+ actual_hash = sha256_file(runtime_file)
302
+ if actual_hash != expected_hash:
303
+ errors.append(f"bootstrap runtime artifact `{relative}` sha256 drifted")
287
304
 
288
305
  status = "pass" if not errors else "block"
289
306
  summary = (
@@ -3711,6 +3711,27 @@ def check_daily_execution_cli(root: Path) -> list[Failure]:
3711
3711
  elif payload.get("result") != "block":
3712
3712
  failures.append(Failure("daily-execution-cli", "`bootstrapped runtime-state` must block when the bootstrap manifest drifts"))
3713
3713
 
3714
+ hash_drift_bootstrap = tmp_root / "hash-drift-bootstrapped-target"
3715
+ shutil.copytree(example_target, hash_drift_bootstrap)
3716
+ manifest_path = hash_drift_bootstrap / ".loom" / "bootstrap" / "manifest.json"
3717
+ manifest = load_json_file(manifest_path)
3718
+ if isinstance(manifest, dict):
3719
+ artifacts = manifest.get("artifacts")
3720
+ if isinstance(artifacts, list):
3721
+ for artifact in artifacts:
3722
+ if isinstance(artifact, dict) and artifact.get("path") == ".loom/bin/runtime_state.py":
3723
+ artifact["sha256"] = "0" * 64
3724
+ manifest_path.write_text(json.dumps(manifest, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
3725
+ payload, error = load_command_json(
3726
+ root,
3727
+ ["python3", ".loom/bin/loom_init.py", "runtime-state", "--target", "."],
3728
+ cwd=hash_drift_bootstrap,
3729
+ )
3730
+ if error:
3731
+ failures.append(Failure("daily-execution-cli", f"`bootstrapped runtime-state` provenance hash drift failed unexpectedly: {error}"))
3732
+ elif payload.get("result") != "block":
3733
+ failures.append(Failure("daily-execution-cli", "`bootstrapped runtime-state` must block when runtime provenance hashes drift"))
3734
+
3714
3735
  if shutil.which("git") is not None:
3715
3736
  with tempfile.TemporaryDirectory(prefix="loom-check-installed-pre-merge-") as tmp:
3716
3737
  tmp_root = Path(tmp)
@@ -4,6 +4,7 @@
4
4
  from __future__ import annotations
5
5
 
6
6
  import argparse
7
+ import hashlib
7
8
  import json
8
9
  import os
9
10
  import re
@@ -27,6 +28,17 @@ TOOL_VERSION = "1.3.0"
27
28
  CONTRACT_VERSION = "1.3.0"
28
29
  WORK_ITEM_ID = "INIT-0001"
29
30
 
31
+ RUNTIME_ARTIFACT_SOURCES = {
32
+ ".loom/bin/loom_init.py": RUNTIME_SOURCE,
33
+ ".loom/bin/fact_chain_support.py": FACT_CHAIN_RUNTIME_SOURCE,
34
+ ".loom/bin/governance_surface.py": GOVERNANCE_RUNTIME_SOURCE,
35
+ ".loom/bin/loom_flow.py": FLOW_RUNTIME_SOURCE,
36
+ ".loom/bin/loom_status.py": STATUS_RUNTIME_SOURCE,
37
+ ".loom/bin/runtime_paths.py": "skills/shared/scripts/runtime_paths.py",
38
+ ".loom/bin/runtime_state.py": "skills/shared/scripts/runtime_state.py",
39
+ ".loom/bin/loom_check.py": CHECK_RUNTIME_SOURCE,
40
+ }
41
+
30
42
  ROOT_BOUNDARY_FILES = (
31
43
  "AGENTS.md",
32
44
  "WORKFLOW.md",
@@ -816,46 +828,14 @@ def initial_artifacts(target_root: Path, install_pr_template: bool, adoption_pat
816
828
  "kind": "capability-map",
817
829
  "source": "generated",
818
830
  },
819
- {
820
- "path": ".loom/bin/loom_init.py",
821
- "kind": "loom-tool",
822
- "source": RUNTIME_SOURCE,
823
- },
824
- {
825
- "path": ".loom/bin/fact_chain_support.py",
826
- "kind": "loom-tool-support",
827
- "source": FACT_CHAIN_RUNTIME_SOURCE,
828
- },
829
- {
830
- "path": ".loom/bin/governance_surface.py",
831
- "kind": "loom-tool-support",
832
- "source": GOVERNANCE_RUNTIME_SOURCE,
833
- },
834
- {
835
- "path": ".loom/bin/loom_flow.py",
836
- "kind": "loom-tool",
837
- "source": FLOW_RUNTIME_SOURCE,
838
- },
839
- {
840
- "path": ".loom/bin/loom_status.py",
841
- "kind": "loom-tool",
842
- "source": STATUS_RUNTIME_SOURCE,
843
- },
844
- {
845
- "path": ".loom/bin/runtime_paths.py",
846
- "kind": "loom-tool-support",
847
- "source": "skills/shared/scripts/runtime_paths.py",
848
- },
849
- {
850
- "path": ".loom/bin/runtime_state.py",
851
- "kind": "loom-tool-support",
852
- "source": "skills/shared/scripts/runtime_state.py",
853
- },
854
- {
855
- "path": ".loom/bin/loom_check.py",
856
- "kind": "loom-tool",
857
- "source": CHECK_RUNTIME_SOURCE,
858
- },
831
+ runtime_artifact(".loom/bin/loom_init.py", "loom-tool", RUNTIME_SOURCE),
832
+ runtime_artifact(".loom/bin/fact_chain_support.py", "loom-tool-support", FACT_CHAIN_RUNTIME_SOURCE),
833
+ runtime_artifact(".loom/bin/governance_surface.py", "loom-tool-support", GOVERNANCE_RUNTIME_SOURCE),
834
+ runtime_artifact(".loom/bin/loom_flow.py", "loom-tool", FLOW_RUNTIME_SOURCE),
835
+ runtime_artifact(".loom/bin/loom_status.py", "loom-tool", STATUS_RUNTIME_SOURCE),
836
+ runtime_artifact(".loom/bin/runtime_paths.py", "loom-tool-support", "skills/shared/scripts/runtime_paths.py"),
837
+ runtime_artifact(".loom/bin/runtime_state.py", "loom-tool-support", "skills/shared/scripts/runtime_state.py"),
838
+ runtime_artifact(".loom/bin/loom_check.py", "loom-tool", CHECK_RUNTIME_SOURCE),
859
839
  ]
860
840
  if uses_attach_only_path(adoption_path):
861
841
  artifacts.extend(
@@ -1362,6 +1342,34 @@ def copy_file(source: Path, target: Path, force: bool) -> bool:
1362
1342
  return write_text(target, content, force=force)
1363
1343
 
1364
1344
 
1345
+ def sha256_file(path: Path) -> str:
1346
+ digest = hashlib.sha256()
1347
+ with path.open("rb") as handle:
1348
+ for chunk in iter(lambda: handle.read(1024 * 1024), b""):
1349
+ digest.update(chunk)
1350
+ return digest.hexdigest()
1351
+
1352
+
1353
+ def runtime_artifact(path: str, kind: str, source: str) -> dict[str, str]:
1354
+ runtime_sources = {
1355
+ ".loom/bin/loom_init.py": Path(__file__),
1356
+ ".loom/bin/fact_chain_support.py": Path(__file__).with_name("fact_chain_support.py"),
1357
+ ".loom/bin/governance_surface.py": Path(__file__).with_name("governance_surface.py"),
1358
+ ".loom/bin/loom_flow.py": Path(__file__).with_name("loom_flow.py"),
1359
+ ".loom/bin/loom_status.py": Path(__file__).with_name("loom_status.py"),
1360
+ ".loom/bin/runtime_paths.py": Path(__file__).with_name("runtime_paths.py"),
1361
+ ".loom/bin/runtime_state.py": Path(__file__).with_name("runtime_state.py"),
1362
+ ".loom/bin/loom_check.py": Path(__file__).with_name("loom_check.py"),
1363
+ }
1364
+ source_path = runtime_sources[path]
1365
+ return {
1366
+ "path": path,
1367
+ "kind": kind,
1368
+ "source": source,
1369
+ "sha256": sha256_file(source_path),
1370
+ }
1371
+
1372
+
1365
1373
  def manifest_payload(result: dict[str, object]) -> dict[str, object]:
1366
1374
  return {
1367
1375
  "schema_version": "loom-bootstrap-manifest/v1",
@@ -4,6 +4,7 @@
4
4
  from __future__ import annotations
5
5
 
6
6
  import json
7
+ import hashlib
7
8
  import os
8
9
  from pathlib import Path
9
10
  from typing import Any
@@ -85,6 +86,14 @@ def detect_carrier(caller_file: str) -> str | None:
85
86
  return None
86
87
 
87
88
 
89
+ def sha256_file(path: Path) -> str:
90
+ digest = hashlib.sha256()
91
+ with path.open("rb") as handle:
92
+ for chunk in iter(lambda: handle.read(1024 * 1024), b""):
93
+ digest.update(chunk)
94
+ return digest.hexdigest()
95
+
96
+
88
97
  def _default_scene_for_carrier(carrier: str) -> str:
89
98
  if carrier == "repo-local-wrapper":
90
99
  return "repo-local-demo"
@@ -284,6 +293,14 @@ def _validate_bootstrapped_runtime(caller_file: str) -> tuple[dict[str, Any], li
284
293
  runtime_file = runtime_root / Path(relative).name
285
294
  if not runtime_file.exists():
286
295
  errors.append(f"bootstrapped runtime file is missing: `{relative}`")
296
+ continue
297
+ expected_hash = artifact.get("sha256")
298
+ if not isinstance(expected_hash, str) or not expected_hash.strip():
299
+ errors.append(f"bootstrap runtime artifact `{relative}` must declare sha256 provenance")
300
+ continue
301
+ actual_hash = sha256_file(runtime_file)
302
+ if actual_hash != expected_hash:
303
+ errors.append(f"bootstrap runtime artifact `{relative}` sha256 drifted")
287
304
 
288
305
  status = "pass" if not errors else "block"
289
306
  summary = (
@@ -3711,6 +3711,27 @@ def check_daily_execution_cli(root: Path) -> list[Failure]:
3711
3711
  elif payload.get("result") != "block":
3712
3712
  failures.append(Failure("daily-execution-cli", "`bootstrapped runtime-state` must block when the bootstrap manifest drifts"))
3713
3713
 
3714
+ hash_drift_bootstrap = tmp_root / "hash-drift-bootstrapped-target"
3715
+ shutil.copytree(example_target, hash_drift_bootstrap)
3716
+ manifest_path = hash_drift_bootstrap / ".loom" / "bootstrap" / "manifest.json"
3717
+ manifest = load_json_file(manifest_path)
3718
+ if isinstance(manifest, dict):
3719
+ artifacts = manifest.get("artifacts")
3720
+ if isinstance(artifacts, list):
3721
+ for artifact in artifacts:
3722
+ if isinstance(artifact, dict) and artifact.get("path") == ".loom/bin/runtime_state.py":
3723
+ artifact["sha256"] = "0" * 64
3724
+ manifest_path.write_text(json.dumps(manifest, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
3725
+ payload, error = load_command_json(
3726
+ root,
3727
+ ["python3", ".loom/bin/loom_init.py", "runtime-state", "--target", "."],
3728
+ cwd=hash_drift_bootstrap,
3729
+ )
3730
+ if error:
3731
+ failures.append(Failure("daily-execution-cli", f"`bootstrapped runtime-state` provenance hash drift failed unexpectedly: {error}"))
3732
+ elif payload.get("result") != "block":
3733
+ failures.append(Failure("daily-execution-cli", "`bootstrapped runtime-state` must block when runtime provenance hashes drift"))
3734
+
3714
3735
  if shutil.which("git") is not None:
3715
3736
  with tempfile.TemporaryDirectory(prefix="loom-check-installed-pre-merge-") as tmp:
3716
3737
  tmp_root = Path(tmp)