@hunter-harness/workflow-harness 0.2.39 → 0.2.41
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.
- package/harness/bundles/general/claude-code/.harness-build.json +1 -1
- package/harness/bundles/general/claude-code/harness-archive/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-archive/checklist.md +3 -0
- package/harness/bundles/general/claude-code/harness-archive/templates/render-summary.mjs +22 -0
- package/harness/bundles/general/claude-code/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-plan/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-plan/reference.md +16 -16
- package/harness/bundles/general/claude-code/harness-review/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-run/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-run/reference.md +1 -1
- package/harness/bundles/general/claude-code/harness-submit/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-sync/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-test/SKILL.md +3 -1
- package/harness/bundles/general/claude-code/protocols/archive-report-protocol.md +10 -0
- package/harness/bundles/general/claude-code/protocols/ledger-protocol.md +12 -0
- package/harness/bundles/general/claude-code/protocols/powershell-protocol.md +21 -0
- package/harness/bundles/general/claude-code/protocols/state-layout-protocol.md +11 -0
- package/harness/bundles/general/claude-code/scripts/harness_archive.py +413 -2
- package/harness/bundles/general/claude-code/scripts/harness_deploy.py +10 -51
- package/harness/bundles/general/claude-code/scripts/harness_environment.py +556 -39
- package/harness/bundles/general/claude-code/scripts/harness_gate.py +145 -0
- package/harness/bundles/general/claude-code/scripts/harness_ledger.py +581 -55
- package/harness/bundles/general/claude-code/scripts/harness_plan_finalize.py +85 -6
- package/harness/bundles/general/claude-code/scripts/harness_service.py +291 -22
- package/harness/bundles/general/claude-code/scripts/harness_test_runner.py +405 -25
- package/harness/bundles/general/codebuddy/.harness-build.json +1 -1
- package/harness/bundles/general/codebuddy/harness-archive/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-archive/checklist.md +3 -0
- package/harness/bundles/general/codebuddy/harness-archive/templates/render-summary.mjs +22 -0
- package/harness/bundles/general/codebuddy/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-plan/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-plan/reference.md +16 -16
- package/harness/bundles/general/codebuddy/harness-review/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-run/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-run/reference.md +1 -1
- package/harness/bundles/general/codebuddy/harness-submit/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-sync/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-test/SKILL.md +3 -1
- package/harness/bundles/general/codebuddy/protocols/archive-report-protocol.md +10 -0
- package/harness/bundles/general/codebuddy/protocols/ledger-protocol.md +12 -0
- package/harness/bundles/general/codebuddy/protocols/powershell-protocol.md +21 -0
- package/harness/bundles/general/codebuddy/protocols/state-layout-protocol.md +11 -0
- package/harness/bundles/general/codebuddy/scripts/harness_archive.py +413 -2
- package/harness/bundles/general/codebuddy/scripts/harness_deploy.py +10 -51
- package/harness/bundles/general/codebuddy/scripts/harness_environment.py +556 -39
- package/harness/bundles/general/codebuddy/scripts/harness_gate.py +145 -0
- package/harness/bundles/general/codebuddy/scripts/harness_ledger.py +581 -55
- package/harness/bundles/general/codebuddy/scripts/harness_plan_finalize.py +85 -6
- package/harness/bundles/general/codebuddy/scripts/harness_service.py +291 -22
- package/harness/bundles/general/codebuddy/scripts/harness_test_runner.py +405 -25
- package/harness/bundles/general/codex/.harness-build.json +1 -1
- package/harness/bundles/general/codex/harness-archive/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-archive/checklist.md +3 -0
- package/harness/bundles/general/codex/harness-archive/templates/render-summary.mjs +22 -0
- package/harness/bundles/general/codex/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-plan/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-plan/reference.md +16 -16
- package/harness/bundles/general/codex/harness-review/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-run/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-run/reference.md +1 -1
- package/harness/bundles/general/codex/harness-submit/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-sync/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-test/SKILL.md +3 -1
- package/harness/bundles/general/codex/protocols/archive-report-protocol.md +10 -0
- package/harness/bundles/general/codex/protocols/ledger-protocol.md +12 -0
- package/harness/bundles/general/codex/protocols/powershell-protocol.md +21 -0
- package/harness/bundles/general/codex/protocols/state-layout-protocol.md +11 -0
- package/harness/bundles/general/codex/scripts/harness_archive.py +413 -2
- package/harness/bundles/general/codex/scripts/harness_deploy.py +10 -51
- package/harness/bundles/general/codex/scripts/harness_environment.py +556 -39
- package/harness/bundles/general/codex/scripts/harness_gate.py +145 -0
- package/harness/bundles/general/codex/scripts/harness_ledger.py +581 -55
- package/harness/bundles/general/codex/scripts/harness_plan_finalize.py +85 -6
- package/harness/bundles/general/codex/scripts/harness_service.py +291 -22
- package/harness/bundles/general/codex/scripts/harness_test_runner.py +405 -25
- package/harness/bundles/general/cursor/.harness-build.json +1 -1
- package/harness/bundles/general/cursor/harness-archive/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-archive/checklist.md +3 -0
- package/harness/bundles/general/cursor/harness-archive/templates/render-summary.mjs +22 -0
- package/harness/bundles/general/cursor/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-plan/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-plan/reference.md +16 -16
- package/harness/bundles/general/cursor/harness-review/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-run/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-run/reference.md +1 -1
- package/harness/bundles/general/cursor/harness-submit/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-sync/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-test/SKILL.md +3 -1
- package/harness/bundles/general/cursor/protocols/archive-report-protocol.md +10 -0
- package/harness/bundles/general/cursor/protocols/ledger-protocol.md +12 -0
- package/harness/bundles/general/cursor/protocols/powershell-protocol.md +21 -0
- package/harness/bundles/general/cursor/protocols/state-layout-protocol.md +11 -0
- package/harness/bundles/general/cursor/scripts/harness_archive.py +413 -2
- package/harness/bundles/general/cursor/scripts/harness_deploy.py +10 -51
- package/harness/bundles/general/cursor/scripts/harness_environment.py +556 -39
- package/harness/bundles/general/cursor/scripts/harness_gate.py +145 -0
- package/harness/bundles/general/cursor/scripts/harness_ledger.py +581 -55
- package/harness/bundles/general/cursor/scripts/harness_plan_finalize.py +85 -6
- package/harness/bundles/general/cursor/scripts/harness_service.py +291 -22
- package/harness/bundles/general/cursor/scripts/harness_test_runner.py +405 -25
- package/harness/bundles/java/claude-code/.harness-build.json +1 -1
- package/harness/bundles/java/claude-code/harness-apidoc/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-archive/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-archive/checklist.md +3 -0
- package/harness/bundles/java/claude-code/harness-archive/templates/render-summary.mjs +22 -0
- package/harness/bundles/java/claude-code/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-package/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-plan/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-plan/reference.md +16 -16
- package/harness/bundles/java/claude-code/harness-review/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-run/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-submit/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-sync/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-test/SKILL.md +3 -1
- package/harness/bundles/java/claude-code/protocols/archive-report-protocol.md +10 -0
- package/harness/bundles/java/claude-code/protocols/ledger-protocol.md +12 -0
- package/harness/bundles/java/claude-code/protocols/powershell-protocol.md +21 -0
- package/harness/bundles/java/claude-code/protocols/state-layout-protocol.md +11 -0
- package/harness/bundles/java/claude-code/scripts/harness_archive.py +413 -2
- package/harness/bundles/java/claude-code/scripts/harness_deploy.py +10 -51
- package/harness/bundles/java/claude-code/scripts/harness_environment.py +556 -39
- package/harness/bundles/java/claude-code/scripts/harness_gate.py +145 -0
- package/harness/bundles/java/claude-code/scripts/harness_ledger.py +581 -55
- package/harness/bundles/java/claude-code/scripts/harness_plan_finalize.py +85 -6
- package/harness/bundles/java/claude-code/scripts/harness_service.py +291 -22
- package/harness/bundles/java/claude-code/scripts/harness_test_runner.py +405 -25
- package/harness/bundles/java/codebuddy/.harness-build.json +1 -1
- package/harness/bundles/java/codebuddy/harness-apidoc/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-archive/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-archive/checklist.md +3 -0
- package/harness/bundles/java/codebuddy/harness-archive/templates/render-summary.mjs +22 -0
- package/harness/bundles/java/codebuddy/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-package/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-plan/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-plan/reference.md +16 -16
- package/harness/bundles/java/codebuddy/harness-review/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-run/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-submit/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-sync/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-test/SKILL.md +3 -1
- package/harness/bundles/java/codebuddy/protocols/archive-report-protocol.md +10 -0
- package/harness/bundles/java/codebuddy/protocols/ledger-protocol.md +12 -0
- package/harness/bundles/java/codebuddy/protocols/powershell-protocol.md +21 -0
- package/harness/bundles/java/codebuddy/protocols/state-layout-protocol.md +11 -0
- package/harness/bundles/java/codebuddy/scripts/harness_archive.py +413 -2
- package/harness/bundles/java/codebuddy/scripts/harness_deploy.py +10 -51
- package/harness/bundles/java/codebuddy/scripts/harness_environment.py +556 -39
- package/harness/bundles/java/codebuddy/scripts/harness_gate.py +145 -0
- package/harness/bundles/java/codebuddy/scripts/harness_ledger.py +581 -55
- package/harness/bundles/java/codebuddy/scripts/harness_plan_finalize.py +85 -6
- package/harness/bundles/java/codebuddy/scripts/harness_service.py +291 -22
- package/harness/bundles/java/codebuddy/scripts/harness_test_runner.py +405 -25
- package/harness/bundles/java/codex/.harness-build.json +1 -1
- package/harness/bundles/java/codex/harness-apidoc/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-archive/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-archive/checklist.md +3 -0
- package/harness/bundles/java/codex/harness-archive/templates/render-summary.mjs +22 -0
- package/harness/bundles/java/codex/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-package/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-plan/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-plan/reference.md +16 -16
- package/harness/bundles/java/codex/harness-review/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-run/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-submit/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-sync/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-test/SKILL.md +3 -1
- package/harness/bundles/java/codex/protocols/archive-report-protocol.md +10 -0
- package/harness/bundles/java/codex/protocols/ledger-protocol.md +12 -0
- package/harness/bundles/java/codex/protocols/powershell-protocol.md +21 -0
- package/harness/bundles/java/codex/protocols/state-layout-protocol.md +11 -0
- package/harness/bundles/java/codex/scripts/harness_archive.py +413 -2
- package/harness/bundles/java/codex/scripts/harness_deploy.py +10 -51
- package/harness/bundles/java/codex/scripts/harness_environment.py +556 -39
- package/harness/bundles/java/codex/scripts/harness_gate.py +145 -0
- package/harness/bundles/java/codex/scripts/harness_ledger.py +581 -55
- package/harness/bundles/java/codex/scripts/harness_plan_finalize.py +85 -6
- package/harness/bundles/java/codex/scripts/harness_service.py +291 -22
- package/harness/bundles/java/codex/scripts/harness_test_runner.py +405 -25
- package/harness/bundles/java/cursor/.harness-build.json +1 -1
- package/harness/bundles/java/cursor/harness-apidoc/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-archive/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-archive/checklist.md +3 -0
- package/harness/bundles/java/cursor/harness-archive/templates/render-summary.mjs +22 -0
- package/harness/bundles/java/cursor/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-package/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-plan/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-plan/reference.md +16 -16
- package/harness/bundles/java/cursor/harness-review/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-run/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-submit/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-sync/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-test/SKILL.md +3 -1
- package/harness/bundles/java/cursor/protocols/archive-report-protocol.md +10 -0
- package/harness/bundles/java/cursor/protocols/ledger-protocol.md +12 -0
- package/harness/bundles/java/cursor/protocols/powershell-protocol.md +21 -0
- package/harness/bundles/java/cursor/protocols/state-layout-protocol.md +11 -0
- package/harness/bundles/java/cursor/scripts/harness_archive.py +413 -2
- package/harness/bundles/java/cursor/scripts/harness_deploy.py +10 -51
- package/harness/bundles/java/cursor/scripts/harness_environment.py +556 -39
- package/harness/bundles/java/cursor/scripts/harness_gate.py +145 -0
- package/harness/bundles/java/cursor/scripts/harness_ledger.py +581 -55
- package/harness/bundles/java/cursor/scripts/harness_plan_finalize.py +85 -6
- package/harness/bundles/java/cursor/scripts/harness_service.py +291 -22
- package/harness/bundles/java/cursor/scripts/harness_test_runner.py +405 -25
- package/harness/manifests/general/claude-code.json +29 -29
- package/harness/manifests/general/codebuddy.json +29 -29
- package/harness/manifests/general/codex.json +29 -29
- package/harness/manifests/general/cursor.json +29 -29
- package/harness/manifests/java/claude-code.json +30 -30
- package/harness/manifests/java/codebuddy.json +30 -30
- package/harness/manifests/java/codex.json +30 -30
- package/harness/manifests/java/cursor.json +30 -30
- package/hunter-workflow-family.json +4 -4
- package/package.json +1 -1
|
@@ -12,10 +12,13 @@ import argparse
|
|
|
12
12
|
import datetime as dt
|
|
13
13
|
import hashlib
|
|
14
14
|
import json
|
|
15
|
+
import os
|
|
15
16
|
import sys
|
|
16
17
|
from pathlib import Path
|
|
17
18
|
from typing import Any
|
|
18
19
|
|
|
20
|
+
import harness_service as hservice
|
|
21
|
+
|
|
19
22
|
|
|
20
23
|
if hasattr(sys.stdout, "reconfigure"):
|
|
21
24
|
sys.stdout.reconfigure(encoding="utf-8")
|
|
@@ -45,6 +48,17 @@ FINGERPRINT_CANDIDATES = (
|
|
|
45
48
|
"compose.yaml",
|
|
46
49
|
)
|
|
47
50
|
|
|
51
|
+
REQUIRED_CONTENT_EVIDENCE_FIELDS = (
|
|
52
|
+
"instanceId",
|
|
53
|
+
"instanceStartedAt",
|
|
54
|
+
"migrationHead",
|
|
55
|
+
"seedVersion",
|
|
56
|
+
"apiBuildIdentity",
|
|
57
|
+
"redisPing",
|
|
58
|
+
"databaseIndex",
|
|
59
|
+
"isolationPrefixHash",
|
|
60
|
+
)
|
|
61
|
+
|
|
48
62
|
|
|
49
63
|
def now_iso() -> str:
|
|
50
64
|
return dt.datetime.now().astimezone().isoformat(timespec="seconds")
|
|
@@ -128,6 +142,141 @@ def compute_environment_hash(
|
|
|
128
142
|
return "sha256:" + _sha256_bytes(payload)
|
|
129
143
|
|
|
130
144
|
|
|
145
|
+
def _canonical_content_evidence(raw: dict[str, Any]) -> dict[str, Any]:
|
|
146
|
+
content = {
|
|
147
|
+
key: str(raw.get(key) or "").strip()
|
|
148
|
+
for key in (
|
|
149
|
+
"instanceId",
|
|
150
|
+
"instanceStartedAt",
|
|
151
|
+
"migrationHead",
|
|
152
|
+
"seedVersion",
|
|
153
|
+
"apiBuildIdentity",
|
|
154
|
+
"redisPing",
|
|
155
|
+
"databaseIndex",
|
|
156
|
+
"isolationPrefixHash",
|
|
157
|
+
)
|
|
158
|
+
if str(raw.get(key) or "").strip()
|
|
159
|
+
}
|
|
160
|
+
canaries_raw = raw.get("canaries")
|
|
161
|
+
canaries: list[dict[str, str]] = []
|
|
162
|
+
if isinstance(canaries_raw, list):
|
|
163
|
+
for index, item in enumerate(canaries_raw):
|
|
164
|
+
if not isinstance(item, dict):
|
|
165
|
+
raise ValueError(f"content canaries[{index}] must be an object")
|
|
166
|
+
name = str(item.get("name") or "").strip()
|
|
167
|
+
status = str(item.get("status") or "").strip().upper()
|
|
168
|
+
identity = str(item.get("identity") or "").strip()
|
|
169
|
+
if not name or status not in {"PASS", "FAIL", "UNKNOWN"}:
|
|
170
|
+
raise ValueError(
|
|
171
|
+
f"content canaries[{index}] requires name and PASS/FAIL/UNKNOWN status"
|
|
172
|
+
)
|
|
173
|
+
canonical = {"name": name, "status": status}
|
|
174
|
+
if identity:
|
|
175
|
+
canonical["identity"] = identity
|
|
176
|
+
canaries.append(canonical)
|
|
177
|
+
content["canaries"] = sorted(
|
|
178
|
+
canaries,
|
|
179
|
+
key=lambda item: (item["name"], item.get("identity", "")),
|
|
180
|
+
)
|
|
181
|
+
return content
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def create_environment_receipt(
|
|
185
|
+
project: Path,
|
|
186
|
+
*,
|
|
187
|
+
stack_id: str,
|
|
188
|
+
content_evidence: dict[str, Any],
|
|
189
|
+
environment_values: dict[str, str] | None = None,
|
|
190
|
+
powershell: dict[str, str] | None = None,
|
|
191
|
+
) -> dict[str, Any]:
|
|
192
|
+
"""Create a typed, secret-free receipt for reusable environment contents."""
|
|
193
|
+
content = _canonical_content_evidence(content_evidence)
|
|
194
|
+
canonical_content = json.dumps(
|
|
195
|
+
content,
|
|
196
|
+
ensure_ascii=False,
|
|
197
|
+
sort_keys=True,
|
|
198
|
+
separators=(",", ":"),
|
|
199
|
+
).encode("utf-8")
|
|
200
|
+
fields = {
|
|
201
|
+
str(name): "sha256:" + _sha256_bytes(str(value).encode("utf-8"))
|
|
202
|
+
for name, value in sorted((environment_values or {}).items())
|
|
203
|
+
if str(name).strip() and value is not None
|
|
204
|
+
}
|
|
205
|
+
shell = {
|
|
206
|
+
key: str((powershell or {}).get(key) or "").strip()
|
|
207
|
+
for key in ("edition", "version")
|
|
208
|
+
if str((powershell or {}).get(key) or "").strip()
|
|
209
|
+
}
|
|
210
|
+
receipt = {
|
|
211
|
+
"schemaVersion": 1,
|
|
212
|
+
"stackId": stack_id,
|
|
213
|
+
"preparedAt": now_iso(),
|
|
214
|
+
"environmentHash": compute_environment_hash(project),
|
|
215
|
+
"content": content,
|
|
216
|
+
"contentFingerprint": "sha256:" + _sha256_bytes(canonical_content),
|
|
217
|
+
"environmentFields": fields,
|
|
218
|
+
"powershell": shell,
|
|
219
|
+
}
|
|
220
|
+
identity_payload = json.dumps(
|
|
221
|
+
{
|
|
222
|
+
key: value
|
|
223
|
+
for key, value in receipt.items()
|
|
224
|
+
if key not in {"preparedAt", "receiptId"}
|
|
225
|
+
},
|
|
226
|
+
ensure_ascii=False,
|
|
227
|
+
sort_keys=True,
|
|
228
|
+
separators=(",", ":"),
|
|
229
|
+
).encode("utf-8")
|
|
230
|
+
receipt["receiptId"] = "sha256:" + _sha256_bytes(identity_payload)
|
|
231
|
+
return receipt
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def resolve_verification_environment(
|
|
235
|
+
receipt: dict[str, Any],
|
|
236
|
+
*,
|
|
237
|
+
required_fields: list[str],
|
|
238
|
+
source_environment: dict[str, str] | None = None,
|
|
239
|
+
) -> dict[str, Any]:
|
|
240
|
+
"""Close a verification's dynamic environment over a typed receipt."""
|
|
241
|
+
source = source_environment if source_environment is not None else dict(os.environ)
|
|
242
|
+
identities = receipt.get("environmentFields")
|
|
243
|
+
if receipt.get("schemaVersion") != 1 or not isinstance(identities, dict):
|
|
244
|
+
return {
|
|
245
|
+
"ok": False,
|
|
246
|
+
"code": "VERIFICATION_ENVIRONMENT_INCOMPLETE",
|
|
247
|
+
"message": "environment receipt is missing typed environmentFields",
|
|
248
|
+
"missing": sorted(set(required_fields)),
|
|
249
|
+
"changed": [],
|
|
250
|
+
}
|
|
251
|
+
required = sorted({str(item).strip() for item in required_fields if str(item).strip()})
|
|
252
|
+
missing = [
|
|
253
|
+
field
|
|
254
|
+
for field in required
|
|
255
|
+
if field not in identities or field not in source or source[field] == ""
|
|
256
|
+
]
|
|
257
|
+
changed = [
|
|
258
|
+
field
|
|
259
|
+
for field in required
|
|
260
|
+
if field not in missing
|
|
261
|
+
and identities.get(field)
|
|
262
|
+
!= "sha256:" + _sha256_bytes(str(source[field]).encode("utf-8"))
|
|
263
|
+
]
|
|
264
|
+
if missing or changed:
|
|
265
|
+
return {
|
|
266
|
+
"ok": False,
|
|
267
|
+
"code": "VERIFICATION_ENVIRONMENT_INCOMPLETE",
|
|
268
|
+
"message": "required dynamic environment fields are missing or changed",
|
|
269
|
+
"missing": missing,
|
|
270
|
+
"changed": changed,
|
|
271
|
+
}
|
|
272
|
+
return {
|
|
273
|
+
"ok": True,
|
|
274
|
+
"code": "VERIFICATION_ENVIRONMENT_READY",
|
|
275
|
+
"environment": {field: str(source[field]) for field in required},
|
|
276
|
+
"receiptId": receipt.get("receiptId"),
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
|
|
131
280
|
def _lease_path(lease_root: Path, stack_id: str) -> Path:
|
|
132
281
|
safe = "".join(ch if ch.isalnum() or ch in "-_." else "_" for ch in stack_id)
|
|
133
282
|
return lease_root / f"{safe}.json"
|
|
@@ -147,12 +296,137 @@ def list_leases(lease_root: Path) -> list[dict[str, Any]]:
|
|
|
147
296
|
return out
|
|
148
297
|
|
|
149
298
|
|
|
299
|
+
def _parse_expiry(value: Any) -> dt.datetime | None:
|
|
300
|
+
if not isinstance(value, str) or not value.strip():
|
|
301
|
+
return None
|
|
302
|
+
try:
|
|
303
|
+
parsed = dt.datetime.fromisoformat(value.strip().replace("Z", "+00:00"))
|
|
304
|
+
except ValueError:
|
|
305
|
+
return None
|
|
306
|
+
if parsed.tzinfo is None:
|
|
307
|
+
return parsed.replace(tzinfo=dt.timezone.utc)
|
|
308
|
+
return parsed.astimezone(dt.timezone.utc)
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def classify_leases(lease_root: Path) -> list[dict[str, Any]]:
|
|
312
|
+
"""Classify leases without mutating or terminating any owner process."""
|
|
313
|
+
results: list[dict[str, Any]] = []
|
|
314
|
+
if not lease_root.is_dir():
|
|
315
|
+
return results
|
|
316
|
+
now = dt.datetime.now(dt.timezone.utc)
|
|
317
|
+
for path in sorted(lease_root.glob("*.json")):
|
|
318
|
+
try:
|
|
319
|
+
lease = _read_json(path)
|
|
320
|
+
except (OSError, json.JSONDecodeError) as exc:
|
|
321
|
+
results.append(
|
|
322
|
+
{
|
|
323
|
+
"stackId": path.stem,
|
|
324
|
+
"classification": "LEASE_UNREADABLE",
|
|
325
|
+
"message": str(exc),
|
|
326
|
+
"sourcePath": str(path),
|
|
327
|
+
}
|
|
328
|
+
)
|
|
329
|
+
continue
|
|
330
|
+
if not isinstance(lease, dict):
|
|
331
|
+
continue
|
|
332
|
+
item = {
|
|
333
|
+
"stackId": str(lease.get("stackId") or path.stem),
|
|
334
|
+
"changeId": lease.get("changeId"),
|
|
335
|
+
"expiresAt": lease.get("expiresAt"),
|
|
336
|
+
"sourcePath": str(path),
|
|
337
|
+
}
|
|
338
|
+
expires_at = _parse_expiry(lease.get("expiresAt"))
|
|
339
|
+
if expires_at is None:
|
|
340
|
+
item["classification"] = "LEASE_EXPIRY_INVALID"
|
|
341
|
+
results.append(item)
|
|
342
|
+
continue
|
|
343
|
+
if now < expires_at:
|
|
344
|
+
item["classification"] = (
|
|
345
|
+
"STALE_CONTENT"
|
|
346
|
+
if lease.get("status") == "STALE_CONTENT"
|
|
347
|
+
else "ACTIVE"
|
|
348
|
+
)
|
|
349
|
+
results.append(item)
|
|
350
|
+
continue
|
|
351
|
+
owner = lease.get("owner")
|
|
352
|
+
if not isinstance(owner, dict) or any(
|
|
353
|
+
not owner.get(field) for field in ("pid", "executable", "startedAt")
|
|
354
|
+
):
|
|
355
|
+
item["classification"] = "OWNER_IDENTITY_INCOMPLETE"
|
|
356
|
+
results.append(item)
|
|
357
|
+
continue
|
|
358
|
+
try:
|
|
359
|
+
owner_pid = int(owner["pid"])
|
|
360
|
+
except (TypeError, ValueError):
|
|
361
|
+
item["classification"] = "OWNER_IDENTITY_INCOMPLETE"
|
|
362
|
+
results.append(item)
|
|
363
|
+
continue
|
|
364
|
+
if not hservice.is_pid_alive(owner_pid):
|
|
365
|
+
item["classification"] = "RECLAIMABLE"
|
|
366
|
+
results.append(item)
|
|
367
|
+
continue
|
|
368
|
+
identity = hservice.verify_process_identity(
|
|
369
|
+
{
|
|
370
|
+
"pid": owner_pid,
|
|
371
|
+
"startedAt": owner["startedAt"],
|
|
372
|
+
"processIdentity": {"executable": owner["executable"]},
|
|
373
|
+
}
|
|
374
|
+
)
|
|
375
|
+
item["classification"] = (
|
|
376
|
+
"OWNER_ACTIVE"
|
|
377
|
+
if identity is True
|
|
378
|
+
else (
|
|
379
|
+
"OWNER_IDENTITY_UNCONFIRMED"
|
|
380
|
+
if identity is None
|
|
381
|
+
else "OWNER_IDENTITY_MISMATCH"
|
|
382
|
+
)
|
|
383
|
+
)
|
|
384
|
+
results.append(item)
|
|
385
|
+
return results
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
def reap_expired_leases(lease_root: Path) -> dict[str, Any]:
|
|
389
|
+
"""Remove only expired leases whose recorded owner is definitely gone."""
|
|
390
|
+
leases = classify_leases(lease_root)
|
|
391
|
+
reaped: list[str] = []
|
|
392
|
+
for item in leases:
|
|
393
|
+
if item.get("classification") != "RECLAIMABLE":
|
|
394
|
+
continue
|
|
395
|
+
path = Path(str(item["sourcePath"]))
|
|
396
|
+
try:
|
|
397
|
+
path.unlink()
|
|
398
|
+
except FileNotFoundError:
|
|
399
|
+
pass
|
|
400
|
+
reaped.append(str(item["stackId"]))
|
|
401
|
+
receipt = {
|
|
402
|
+
"schemaVersion": 1,
|
|
403
|
+
"action": "environment-lease-reap",
|
|
404
|
+
"completedAt": now_iso(),
|
|
405
|
+
"reaped": reaped,
|
|
406
|
+
"leases": leases,
|
|
407
|
+
}
|
|
408
|
+
receipt_path = (
|
|
409
|
+
lease_root
|
|
410
|
+
/ "reap-receipts"
|
|
411
|
+
/ f"{dt.datetime.now().strftime('%Y%m%dT%H%M%S')}-{os.getpid()}.json"
|
|
412
|
+
)
|
|
413
|
+
_write_json(receipt_path, receipt)
|
|
414
|
+
return {
|
|
415
|
+
"ok": True,
|
|
416
|
+
"code": "LEASE_REAP_COMPLETE",
|
|
417
|
+
"reaped": reaped,
|
|
418
|
+
"leases": leases,
|
|
419
|
+
"receiptPath": str(receipt_path),
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
|
|
150
423
|
def require_writable_lease(
|
|
151
424
|
project: Path,
|
|
152
425
|
*,
|
|
153
426
|
change_id: str,
|
|
154
427
|
stack_id: str,
|
|
155
428
|
lease_root: Path | None = None,
|
|
429
|
+
environment_receipt: dict[str, Any] | None = None,
|
|
156
430
|
) -> dict[str, Any]:
|
|
157
431
|
"""Gate: a writable stack may be used only with an active matching lease."""
|
|
158
432
|
root = lease_root or default_lease_root(project)
|
|
@@ -190,28 +464,8 @@ def require_writable_lease(
|
|
|
190
464
|
# Review Y2: expired leases must not remain writable.
|
|
191
465
|
expires_raw = lease.get("expiresAt")
|
|
192
466
|
if isinstance(expires_raw, str) and expires_raw.strip():
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
expires_at = dt.datetime.fromisoformat(raw)
|
|
196
|
-
# Normalize both sides to UTC so naive/aware/offset mixes compare safely.
|
|
197
|
-
if expires_at.tzinfo is None:
|
|
198
|
-
expires_at = expires_at.replace(tzinfo=dt.timezone.utc)
|
|
199
|
-
else:
|
|
200
|
-
expires_at = expires_at.astimezone(dt.timezone.utc)
|
|
201
|
-
now_utc = dt.datetime.now(dt.timezone.utc)
|
|
202
|
-
if now_utc >= expires_at:
|
|
203
|
-
return {
|
|
204
|
-
"ok": False,
|
|
205
|
-
"code": "ENVIRONMENT_LEASE_EXPIRED",
|
|
206
|
-
"message": (
|
|
207
|
-
f"lease for stack={stack_id} expired at {expires_raw}; "
|
|
208
|
-
"re-acquire before writable use"
|
|
209
|
-
),
|
|
210
|
-
"changeId": change_id,
|
|
211
|
-
"stackId": stack_id,
|
|
212
|
-
"expiresAt": expires_raw,
|
|
213
|
-
}
|
|
214
|
-
except ValueError:
|
|
467
|
+
expires_at = _parse_expiry(expires_raw)
|
|
468
|
+
if expires_at is None:
|
|
215
469
|
return {
|
|
216
470
|
"ok": False,
|
|
217
471
|
"code": "ENVIRONMENT_LEASE_REQUIRED",
|
|
@@ -219,6 +473,62 @@ def require_writable_lease(
|
|
|
219
473
|
"changeId": change_id,
|
|
220
474
|
"stackId": stack_id,
|
|
221
475
|
}
|
|
476
|
+
if dt.datetime.now(dt.timezone.utc) >= expires_at:
|
|
477
|
+
return {
|
|
478
|
+
"ok": False,
|
|
479
|
+
"code": "ENVIRONMENT_LEASE_EXPIRED",
|
|
480
|
+
"message": (
|
|
481
|
+
f"lease for stack={stack_id} expired at {expires_raw}; "
|
|
482
|
+
"re-acquire before writable use"
|
|
483
|
+
),
|
|
484
|
+
"changeId": change_id,
|
|
485
|
+
"stackId": stack_id,
|
|
486
|
+
"expiresAt": expires_raw,
|
|
487
|
+
}
|
|
488
|
+
if lease.get("status") == "STALE_CONTENT":
|
|
489
|
+
return {
|
|
490
|
+
"ok": False,
|
|
491
|
+
"code": "STALE_CONTENT",
|
|
492
|
+
"message": "lease content was previously marked stale; prepare again",
|
|
493
|
+
"lease": lease,
|
|
494
|
+
}
|
|
495
|
+
expected_content = str(lease.get("contentFingerprint") or "").strip()
|
|
496
|
+
if expected_content:
|
|
497
|
+
if not isinstance(environment_receipt, dict):
|
|
498
|
+
return {
|
|
499
|
+
"ok": False,
|
|
500
|
+
"code": "ENVIRONMENT_CONTENT_RECEIPT_REQUIRED",
|
|
501
|
+
"message": "current typed environment receipt is required before reuse",
|
|
502
|
+
"changeId": change_id,
|
|
503
|
+
"stackId": stack_id,
|
|
504
|
+
}
|
|
505
|
+
observed_content = str(
|
|
506
|
+
environment_receipt.get("contentFingerprint") or ""
|
|
507
|
+
).strip()
|
|
508
|
+
canaries = (
|
|
509
|
+
environment_receipt.get("content", {}).get("canaries")
|
|
510
|
+
if isinstance(environment_receipt.get("content"), dict)
|
|
511
|
+
else None
|
|
512
|
+
)
|
|
513
|
+
canaries_ok = isinstance(canaries, list) and all(
|
|
514
|
+
isinstance(item, dict) and item.get("status") == "PASS"
|
|
515
|
+
for item in canaries
|
|
516
|
+
)
|
|
517
|
+
if observed_content != expected_content or not canaries_ok:
|
|
518
|
+
lease["status"] = "STALE_CONTENT"
|
|
519
|
+
lease["staleAt"] = now_iso()
|
|
520
|
+
lease["expectedContentFingerprint"] = expected_content
|
|
521
|
+
lease["observedContentFingerprint"] = observed_content
|
|
522
|
+
_write_json(path, lease)
|
|
523
|
+
return {
|
|
524
|
+
"ok": False,
|
|
525
|
+
"code": "STALE_CONTENT",
|
|
526
|
+
"message": "environment content fingerprint or canary changed",
|
|
527
|
+
"expectedContentFingerprint": expected_content,
|
|
528
|
+
"observedContentFingerprint": observed_content,
|
|
529
|
+
}
|
|
530
|
+
lease["heartbeatAt"] = now_iso()
|
|
531
|
+
_write_json(path, lease)
|
|
222
532
|
return {"ok": True, "code": "LEASE_HELD", "lease": lease}
|
|
223
533
|
|
|
224
534
|
|
|
@@ -231,6 +541,7 @@ def acquire_lease(
|
|
|
231
541
|
lease_root: Path | None = None,
|
|
232
542
|
writable_volumes: list[str] | None = None,
|
|
233
543
|
ttl_seconds: int = 3600,
|
|
544
|
+
environment_receipt: dict[str, Any] | None = None,
|
|
234
545
|
) -> dict[str, Any]:
|
|
235
546
|
"""Acquire a lease; reject cross-change sharing of the same writable volume."""
|
|
236
547
|
root = lease_root or default_lease_root(project)
|
|
@@ -284,16 +595,55 @@ def acquire_lease(
|
|
|
284
595
|
|
|
285
596
|
started = dt.datetime.now().astimezone()
|
|
286
597
|
expires = started + dt.timedelta(seconds=max(60, int(ttl_seconds)))
|
|
598
|
+
if environment_receipt is not None:
|
|
599
|
+
if (
|
|
600
|
+
environment_receipt.get("schemaVersion") != 1
|
|
601
|
+
or environment_receipt.get("stackId") != stack_id
|
|
602
|
+
or not str(environment_receipt.get("contentFingerprint") or "").strip()
|
|
603
|
+
):
|
|
604
|
+
return {
|
|
605
|
+
"ok": False,
|
|
606
|
+
"code": "ENVIRONMENT_RECEIPT_INVALID",
|
|
607
|
+
"message": "environment receipt identity does not match requested stack",
|
|
608
|
+
}
|
|
609
|
+
canaries = (
|
|
610
|
+
environment_receipt.get("content", {}).get("canaries")
|
|
611
|
+
if isinstance(environment_receipt.get("content"), dict)
|
|
612
|
+
else None
|
|
613
|
+
)
|
|
614
|
+
if not isinstance(canaries, list) or not canaries or any(
|
|
615
|
+
not isinstance(item, dict) or item.get("status") != "PASS"
|
|
616
|
+
for item in canaries
|
|
617
|
+
):
|
|
618
|
+
return {
|
|
619
|
+
"ok": False,
|
|
620
|
+
"code": "ENVIRONMENT_CONTENT_NOT_READY",
|
|
621
|
+
"message": "all environment content canaries must pass before acquire",
|
|
622
|
+
}
|
|
287
623
|
lease = {
|
|
288
|
-
"schemaVersion": 1,
|
|
624
|
+
"schemaVersion": 2 if environment_receipt is not None else 1,
|
|
289
625
|
"changeId": change_id,
|
|
290
626
|
"stackId": stack_id,
|
|
291
627
|
"environmentHash": environment_hash,
|
|
292
628
|
"writableVolumes": volumes,
|
|
293
629
|
"acquiredAt": started.isoformat(timespec="seconds"),
|
|
630
|
+
"heartbeatAt": started.isoformat(timespec="seconds"),
|
|
294
631
|
"expiresAt": expires.isoformat(timespec="seconds"),
|
|
295
632
|
"projectRoot": str(project.resolve()),
|
|
633
|
+
"status": "ACTIVE",
|
|
634
|
+
"owner": {
|
|
635
|
+
"pid": os.getpid(),
|
|
636
|
+
"executable": hservice.get_process_executable(os.getpid())
|
|
637
|
+
or str(Path(sys.executable).resolve()),
|
|
638
|
+
"startedAt": (
|
|
639
|
+
hservice.get_process_create_time(os.getpid())
|
|
640
|
+
or started
|
|
641
|
+
).isoformat(timespec="seconds"),
|
|
642
|
+
},
|
|
296
643
|
}
|
|
644
|
+
if environment_receipt is not None:
|
|
645
|
+
lease["contentFingerprint"] = environment_receipt["contentFingerprint"]
|
|
646
|
+
lease["environmentReceiptId"] = environment_receipt.get("receiptId")
|
|
297
647
|
_write_json(path, lease)
|
|
298
648
|
return {"ok": True, "code": "LEASE_ACQUIRED", "lease": lease, "path": str(path)}
|
|
299
649
|
|
|
@@ -344,20 +694,155 @@ def cmd_fingerprint(args: argparse.Namespace) -> int:
|
|
|
344
694
|
)
|
|
345
695
|
|
|
346
696
|
|
|
697
|
+
def _load_receipt_arg(value: str | None) -> dict[str, Any] | None:
|
|
698
|
+
if not value:
|
|
699
|
+
return None
|
|
700
|
+
path = Path(value).resolve()
|
|
701
|
+
data = _read_json(path)
|
|
702
|
+
if not isinstance(data, dict):
|
|
703
|
+
raise ValueError(f"environment receipt must be a JSON object: {path}")
|
|
704
|
+
return data
|
|
705
|
+
|
|
706
|
+
|
|
707
|
+
def cmd_prepare(args: argparse.Namespace) -> int:
|
|
708
|
+
project = Path(args.project).resolve()
|
|
709
|
+
evidence_path = Path(args.content_evidence_file).resolve()
|
|
710
|
+
try:
|
|
711
|
+
evidence = _read_json(evidence_path)
|
|
712
|
+
except (OSError, json.JSONDecodeError) as exc:
|
|
713
|
+
return emit_json(
|
|
714
|
+
{
|
|
715
|
+
"ok": False,
|
|
716
|
+
"code": "ENVIRONMENT_CONTENT_EVIDENCE_INVALID",
|
|
717
|
+
"message": str(exc),
|
|
718
|
+
},
|
|
719
|
+
ok=False,
|
|
720
|
+
)
|
|
721
|
+
if not isinstance(evidence, dict):
|
|
722
|
+
return emit_json(
|
|
723
|
+
{
|
|
724
|
+
"ok": False,
|
|
725
|
+
"code": "ENVIRONMENT_CONTENT_EVIDENCE_INVALID",
|
|
726
|
+
"message": "content evidence must be a JSON object",
|
|
727
|
+
},
|
|
728
|
+
ok=False,
|
|
729
|
+
)
|
|
730
|
+
try:
|
|
731
|
+
canonical_evidence = _canonical_content_evidence(evidence)
|
|
732
|
+
except ValueError as exc:
|
|
733
|
+
return emit_json(
|
|
734
|
+
{
|
|
735
|
+
"ok": False,
|
|
736
|
+
"code": "ENVIRONMENT_CONTENT_EVIDENCE_INVALID",
|
|
737
|
+
"message": str(exc),
|
|
738
|
+
},
|
|
739
|
+
ok=False,
|
|
740
|
+
)
|
|
741
|
+
missing_content = [
|
|
742
|
+
field
|
|
743
|
+
for field in REQUIRED_CONTENT_EVIDENCE_FIELDS
|
|
744
|
+
if not canonical_evidence.get(field)
|
|
745
|
+
]
|
|
746
|
+
if not canonical_evidence.get("canaries"):
|
|
747
|
+
missing_content.append("canaries")
|
|
748
|
+
if missing_content:
|
|
749
|
+
return emit_json(
|
|
750
|
+
{
|
|
751
|
+
"ok": False,
|
|
752
|
+
"code": "ENVIRONMENT_CONTENT_EVIDENCE_INCOMPLETE",
|
|
753
|
+
"message": "formal environment receipts require complete content evidence",
|
|
754
|
+
"missing": missing_content,
|
|
755
|
+
},
|
|
756
|
+
ok=False,
|
|
757
|
+
)
|
|
758
|
+
field_names = sorted(
|
|
759
|
+
{str(item).strip() for item in (args.environment_field or []) if str(item).strip()}
|
|
760
|
+
)
|
|
761
|
+
missing = [field for field in field_names if not os.environ.get(field)]
|
|
762
|
+
if missing:
|
|
763
|
+
return emit_json(
|
|
764
|
+
{
|
|
765
|
+
"ok": False,
|
|
766
|
+
"code": "VERIFICATION_ENVIRONMENT_INCOMPLETE",
|
|
767
|
+
"message": "declared environment fields are missing",
|
|
768
|
+
"missing": missing,
|
|
769
|
+
},
|
|
770
|
+
ok=False,
|
|
771
|
+
)
|
|
772
|
+
powershell = {
|
|
773
|
+
"edition": str(
|
|
774
|
+
args.powershell_edition
|
|
775
|
+
or os.environ.get("HARNESS_POWERSHELL_EDITION")
|
|
776
|
+
or ""
|
|
777
|
+
),
|
|
778
|
+
"version": str(
|
|
779
|
+
args.powershell_version
|
|
780
|
+
or os.environ.get("HARNESS_POWERSHELL_VERSION")
|
|
781
|
+
or ""
|
|
782
|
+
),
|
|
783
|
+
}
|
|
784
|
+
if bool(powershell["edition"]) != bool(powershell["version"]):
|
|
785
|
+
return emit_json(
|
|
786
|
+
{
|
|
787
|
+
"ok": False,
|
|
788
|
+
"code": "POWERSHELL_RUNTIME_INCOMPLETE",
|
|
789
|
+
"message": "PowerShell edition and version must be recorded together",
|
|
790
|
+
},
|
|
791
|
+
ok=False,
|
|
792
|
+
)
|
|
793
|
+
try:
|
|
794
|
+
receipt = create_environment_receipt(
|
|
795
|
+
project,
|
|
796
|
+
stack_id=args.stack_id,
|
|
797
|
+
content_evidence=evidence,
|
|
798
|
+
environment_values={field: os.environ[field] for field in field_names},
|
|
799
|
+
powershell=powershell,
|
|
800
|
+
)
|
|
801
|
+
except ValueError as exc:
|
|
802
|
+
return emit_json(
|
|
803
|
+
{
|
|
804
|
+
"ok": False,
|
|
805
|
+
"code": "ENVIRONMENT_CONTENT_EVIDENCE_INVALID",
|
|
806
|
+
"message": str(exc),
|
|
807
|
+
},
|
|
808
|
+
ok=False,
|
|
809
|
+
)
|
|
810
|
+
output = Path(args.output).resolve()
|
|
811
|
+
_write_json(output, receipt)
|
|
812
|
+
return emit_json(
|
|
813
|
+
{
|
|
814
|
+
"ok": True,
|
|
815
|
+
"code": "ENVIRONMENT_RECEIPT_PREPARED",
|
|
816
|
+
"receiptId": receipt["receiptId"],
|
|
817
|
+
"contentFingerprint": receipt["contentFingerprint"],
|
|
818
|
+
"output": str(output),
|
|
819
|
+
}
|
|
820
|
+
)
|
|
821
|
+
|
|
822
|
+
|
|
347
823
|
def cmd_acquire(args: argparse.Namespace) -> int:
|
|
348
824
|
project = Path(args.project).resolve()
|
|
349
825
|
root = Path(args.lease_root).resolve() if args.lease_root else default_lease_root(project)
|
|
350
826
|
env_hash = args.environment_hash or compute_environment_hash(project)
|
|
351
827
|
volumes = [v for v in (args.writable_volume or []) if v]
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
828
|
+
try:
|
|
829
|
+
receipt = _load_receipt_arg(args.environment_receipt)
|
|
830
|
+
result = acquire_lease(
|
|
831
|
+
project,
|
|
832
|
+
change_id=args.change,
|
|
833
|
+
stack_id=args.stack_id,
|
|
834
|
+
environment_hash=env_hash,
|
|
835
|
+
lease_root=root,
|
|
836
|
+
writable_volumes=volumes,
|
|
837
|
+
ttl_seconds=int(args.ttl_seconds or 3600),
|
|
838
|
+
environment_receipt=receipt,
|
|
839
|
+
)
|
|
840
|
+
except (OSError, json.JSONDecodeError, ValueError) as exc:
|
|
841
|
+
result = {
|
|
842
|
+
"ok": False,
|
|
843
|
+
"code": "ENVIRONMENT_RECEIPT_INVALID",
|
|
844
|
+
"message": str(exc),
|
|
845
|
+
}
|
|
361
846
|
return emit_json(result, ok=bool(result.get("ok")))
|
|
362
847
|
|
|
363
848
|
|
|
@@ -376,12 +861,21 @@ def cmd_release(args: argparse.Namespace) -> int:
|
|
|
376
861
|
def cmd_require(args: argparse.Namespace) -> int:
|
|
377
862
|
project = Path(args.project).resolve()
|
|
378
863
|
root = Path(args.lease_root).resolve() if args.lease_root else default_lease_root(project)
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
864
|
+
try:
|
|
865
|
+
receipt = _load_receipt_arg(args.environment_receipt)
|
|
866
|
+
result = require_writable_lease(
|
|
867
|
+
project,
|
|
868
|
+
change_id=args.change,
|
|
869
|
+
stack_id=args.stack_id,
|
|
870
|
+
lease_root=root,
|
|
871
|
+
environment_receipt=receipt,
|
|
872
|
+
)
|
|
873
|
+
except (OSError, json.JSONDecodeError, ValueError) as exc:
|
|
874
|
+
result = {
|
|
875
|
+
"ok": False,
|
|
876
|
+
"code": "ENVIRONMENT_RECEIPT_INVALID",
|
|
877
|
+
"message": str(exc),
|
|
878
|
+
}
|
|
385
879
|
return emit_json(result, ok=bool(result.get("ok")))
|
|
386
880
|
|
|
387
881
|
|
|
@@ -394,7 +888,7 @@ def cmd_status(args: argparse.Namespace) -> int:
|
|
|
394
888
|
"action": "status",
|
|
395
889
|
"projectRoot": str(project),
|
|
396
890
|
"environmentHash": compute_environment_hash(project),
|
|
397
|
-
"leases":
|
|
891
|
+
"leases": classify_leases(root),
|
|
398
892
|
"leaseRoot": str(root),
|
|
399
893
|
"contract": [
|
|
400
894
|
"prepare",
|
|
@@ -409,10 +903,26 @@ def cmd_status(args: argparse.Namespace) -> int:
|
|
|
409
903
|
)
|
|
410
904
|
|
|
411
905
|
|
|
906
|
+
def cmd_reap_expired(args: argparse.Namespace) -> int:
|
|
907
|
+
project = Path(args.project).resolve()
|
|
908
|
+
root = Path(args.lease_root).resolve() if args.lease_root else default_lease_root(project)
|
|
909
|
+
return emit_json(reap_expired_leases(root))
|
|
910
|
+
|
|
911
|
+
|
|
412
912
|
def build_parser() -> argparse.ArgumentParser:
|
|
413
913
|
parser = argparse.ArgumentParser(prog="harness_environment.py")
|
|
414
914
|
sub = parser.add_subparsers(dest="command", required=True)
|
|
415
915
|
|
|
916
|
+
prep = sub.add_parser("prepare")
|
|
917
|
+
prep.add_argument("--project", required=True)
|
|
918
|
+
prep.add_argument("--stack-id", required=True)
|
|
919
|
+
prep.add_argument("--content-evidence-file", required=True)
|
|
920
|
+
prep.add_argument("--environment-field", action="append", default=[])
|
|
921
|
+
prep.add_argument("--powershell-edition")
|
|
922
|
+
prep.add_argument("--powershell-version")
|
|
923
|
+
prep.add_argument("--output", required=True)
|
|
924
|
+
prep.set_defaults(func=cmd_prepare)
|
|
925
|
+
|
|
416
926
|
fp = sub.add_parser("fingerprint")
|
|
417
927
|
fp.add_argument("--project", required=True)
|
|
418
928
|
fp.set_defaults(func=cmd_fingerprint)
|
|
@@ -422,6 +932,7 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
422
932
|
ac.add_argument("--change", required=True)
|
|
423
933
|
ac.add_argument("--stack-id", required=True)
|
|
424
934
|
ac.add_argument("--environment-hash")
|
|
935
|
+
ac.add_argument("--environment-receipt")
|
|
425
936
|
ac.add_argument("--lease-root")
|
|
426
937
|
ac.add_argument("--writable-volume", action="append", default=[])
|
|
427
938
|
ac.add_argument("--ttl-seconds", type=int, default=3600)
|
|
@@ -439,6 +950,7 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
439
950
|
req.add_argument("--change", required=True)
|
|
440
951
|
req.add_argument("--stack-id", required=True)
|
|
441
952
|
req.add_argument("--lease-root")
|
|
953
|
+
req.add_argument("--environment-receipt")
|
|
442
954
|
req.set_defaults(func=cmd_require)
|
|
443
955
|
|
|
444
956
|
st = sub.add_parser("status")
|
|
@@ -446,6 +958,11 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
446
958
|
st.add_argument("--lease-root")
|
|
447
959
|
st.set_defaults(func=cmd_status)
|
|
448
960
|
|
|
961
|
+
reap = sub.add_parser("reap-expired")
|
|
962
|
+
reap.add_argument("--project", required=True)
|
|
963
|
+
reap.add_argument("--lease-root")
|
|
964
|
+
reap.set_defaults(func=cmd_reap_expired)
|
|
965
|
+
|
|
449
966
|
return parser
|
|
450
967
|
|
|
451
968
|
|