@mriekki/project-forge 4.1.0 → 4.2.0
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/assets/components.json +3 -3
- package/assets/deployment/distribution.json +1 -0
- package/assets/generated/ASSEMBLY.json +16 -11
- package/assets/generated/core/core/instructions/project-forge-workflow.instructions.md +3 -1
- package/assets/generated/core/core/skills/setup/SKILL.md +61 -0
- package/assets/generated/core/core/skills/upstream-fix/SKILL.md +137 -18
- package/assets/generated/deployment/docs/ENVIRONMENT_PROOF.md +26 -0
- package/assets/generated/deployment/src/project_forge_deployment/environment_probe.py +7 -0
- package/assets/generated/deployment/src/project_forge_deployment/environment_setup.py +106 -24
- package/assets/generated/deployment/src/project_forge_deployment/home_lab.py +22 -1
- package/assets/generated/deployment/src/project_forge_deployment/proof_receipt.py +183 -0
- package/assets/generated/deployment/src/project_forge_deployment/ssh.py +277 -23
- package/package.json +1 -1
|
@@ -14,7 +14,8 @@ from .doctor import target_doctor, workstation_doctor
|
|
|
14
14
|
from .environment_probe import plan_environment_proof
|
|
15
15
|
from .errors import DeploymentKitError
|
|
16
16
|
from .profiles import load_profile
|
|
17
|
-
from .
|
|
17
|
+
from .proof_receipt import load_environment_proof_receipt
|
|
18
|
+
from .ssh import diagnose_access
|
|
18
19
|
|
|
19
20
|
|
|
20
21
|
READINESS_ORDER = (
|
|
@@ -34,6 +35,13 @@ def _phase(label: str, state: str, *, detail: Any = None) -> dict[str, Any]:
|
|
|
34
35
|
return value
|
|
35
36
|
|
|
36
37
|
|
|
38
|
+
def _current_phase(phases: dict[str, dict[str, Any]]) -> str:
|
|
39
|
+
return next(
|
|
40
|
+
(key for key in READINESS_ORDER if phases[key]["state"] not in {"ready", "proven", "not-part-of-setup"}),
|
|
41
|
+
"application",
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
37
45
|
def remote_setup_status(name: str) -> dict[str, Any]:
|
|
38
46
|
"""Return one read-only, terminal status model for remote environment setup."""
|
|
39
47
|
|
|
@@ -57,19 +65,15 @@ def remote_setup_status(name: str) -> dict[str, Any]:
|
|
|
57
65
|
"application": _phase("APPLICATION DEPLOYED", "not-part-of-setup"),
|
|
58
66
|
}
|
|
59
67
|
|
|
60
|
-
target_access: dict[str, Any] | None = None
|
|
61
68
|
host_platform: dict[str, Any] | None = None
|
|
62
69
|
|
|
63
70
|
if target["state"] == "ready-to-verify":
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
"verification-failed",
|
|
71
|
-
detail={"reason": str(error)},
|
|
72
|
-
)
|
|
71
|
+
target_access = diagnose_access(load_profile(name))
|
|
72
|
+
phases["targetAccess"] = _phase(
|
|
73
|
+
"TARGET ACCESS READY",
|
|
74
|
+
"ready" if target_access["state"] == "verified" else "diagnosis-required",
|
|
75
|
+
detail=target_access,
|
|
76
|
+
)
|
|
73
77
|
elif target["state"] == "needs-profile":
|
|
74
78
|
phases["targetAccess"] = _phase("TARGET ACCESS READY", "blocked-by-target-definition")
|
|
75
79
|
else:
|
|
@@ -96,19 +100,76 @@ def remote_setup_status(name: str) -> dict[str, Any]:
|
|
|
96
100
|
else:
|
|
97
101
|
phases["hostPlatform"] = _phase("HOST PLATFORM READY", "blocked-by-target-access")
|
|
98
102
|
|
|
99
|
-
current = next(
|
|
100
|
-
(key for key in READINESS_ORDER if phases[key]["state"] not in {"ready", "not-part-of-setup"}),
|
|
101
|
-
"application",
|
|
102
|
-
)
|
|
103
103
|
return {
|
|
104
104
|
"targetName": name,
|
|
105
105
|
"state": "host-platform-ready" if phases["hostPlatform"]["state"] == "ready" else "needs-setup",
|
|
106
|
-
"currentPhase":
|
|
106
|
+
"currentPhase": _current_phase(phases),
|
|
107
107
|
"phases": phases,
|
|
108
108
|
"mutated": False,
|
|
109
109
|
}
|
|
110
110
|
|
|
111
111
|
|
|
112
|
+
def _access_recovery(status: dict[str, Any]) -> dict[str, Any]:
|
|
113
|
+
detail = status["phases"]["targetAccess"].get("detail") or {}
|
|
114
|
+
category = detail.get("failureCategory")
|
|
115
|
+
target = status["targetName"]
|
|
116
|
+
command = f"project-forge-deploy environment setup status {target} --json"
|
|
117
|
+
|
|
118
|
+
if category in {"remote-authentication-rejected", "remote-account-policy-rejected"}:
|
|
119
|
+
return {
|
|
120
|
+
"kind": "operator-action-required",
|
|
121
|
+
"action": "inspect-remote-account-access-policy",
|
|
122
|
+
"command": command,
|
|
123
|
+
"reason": detail.get("guidance"),
|
|
124
|
+
"safety": (
|
|
125
|
+
"Use a trusted target console or already-authorized administrative session. "
|
|
126
|
+
"Do not reinstall keys repeatedly, enable password SSH, widen AllowUsers, or weaken host verification implicitly."
|
|
127
|
+
),
|
|
128
|
+
}
|
|
129
|
+
if category == "local-signing-failed":
|
|
130
|
+
return {
|
|
131
|
+
"kind": "local-repair",
|
|
132
|
+
"action": "repair-local-key-signing",
|
|
133
|
+
"command": command,
|
|
134
|
+
"reason": detail.get("guidance"),
|
|
135
|
+
}
|
|
136
|
+
if category == "host-identity-rejected":
|
|
137
|
+
return {
|
|
138
|
+
"kind": "human-verification",
|
|
139
|
+
"action": "reverify-host-identity",
|
|
140
|
+
"command": f"project-forge-deploy target trust-candidates {target}",
|
|
141
|
+
"reason": detail.get("guidance"),
|
|
142
|
+
}
|
|
143
|
+
if category in {"transport-failed", "name-resolution-failed"}:
|
|
144
|
+
return {
|
|
145
|
+
"kind": "missing-facts",
|
|
146
|
+
"action": "repair-target-address-or-network-path",
|
|
147
|
+
"command": command,
|
|
148
|
+
"reason": detail.get("guidance") or detail.get("reason"),
|
|
149
|
+
}
|
|
150
|
+
if category == "missing-identity":
|
|
151
|
+
return {
|
|
152
|
+
"kind": "write-gated",
|
|
153
|
+
"action": "create-or-select-key",
|
|
154
|
+
"command": f"project-forge-deploy target key-create {target}",
|
|
155
|
+
"authorization": "[LOCAL KEY WRITE]",
|
|
156
|
+
"reason": detail.get("reason"),
|
|
157
|
+
}
|
|
158
|
+
if category == "missing-trust":
|
|
159
|
+
return {
|
|
160
|
+
"kind": "human-verification",
|
|
161
|
+
"action": "verify-and-enroll-host-trust",
|
|
162
|
+
"command": f"project-forge-deploy target trust-candidates {target}",
|
|
163
|
+
"reason": detail.get("reason"),
|
|
164
|
+
}
|
|
165
|
+
return {
|
|
166
|
+
"kind": "read-only",
|
|
167
|
+
"action": "diagnose-target-access",
|
|
168
|
+
"command": command,
|
|
169
|
+
"reason": detail.get("guidance") or detail.get("reason"),
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
|
|
112
173
|
def _next_action(status: dict[str, Any]) -> dict[str, Any]:
|
|
113
174
|
phases = status["phases"]
|
|
114
175
|
target = phases["targetDefinition"].get("detail") or {}
|
|
@@ -151,12 +212,7 @@ def _next_action(status: dict[str, Any]) -> dict[str, Any]:
|
|
|
151
212
|
}
|
|
152
213
|
|
|
153
214
|
if phases["targetAccess"]["state"] != "ready":
|
|
154
|
-
return
|
|
155
|
-
"kind": "read-only",
|
|
156
|
-
"action": "verify-target-access",
|
|
157
|
-
"command": f"project-forge-deploy target verify {status['targetName']}",
|
|
158
|
-
"reason": (phases["targetAccess"].get("detail") or {}).get("reason"),
|
|
159
|
-
}
|
|
215
|
+
return _access_recovery(status)
|
|
160
216
|
|
|
161
217
|
platform = phases["hostPlatform"].get("detail") or {}
|
|
162
218
|
platform_state = phases["hostPlatform"]["state"]
|
|
@@ -188,6 +244,14 @@ def _next_action(status: dict[str, Any]) -> dict[str, Any]:
|
|
|
188
244
|
"reason": f"Host platform state is {platform_state}.",
|
|
189
245
|
}
|
|
190
246
|
|
|
247
|
+
if phases["deploymentPath"]["state"] == "proven":
|
|
248
|
+
return {
|
|
249
|
+
"kind": "complete",
|
|
250
|
+
"action": "remote-environment-ready",
|
|
251
|
+
"reason": "Fresh workstation-local proof evidence matches this Project/environment/target context.",
|
|
252
|
+
"proofReceipt": phases["deploymentPath"].get("detail"),
|
|
253
|
+
}
|
|
254
|
+
|
|
191
255
|
return {
|
|
192
256
|
"kind": "proof-context-required",
|
|
193
257
|
"action": "plan-deployment-path-proof",
|
|
@@ -204,7 +268,26 @@ def remote_setup_plan(
|
|
|
204
268
|
) -> dict[str, Any]:
|
|
205
269
|
"""Return the exact next deterministic setup action without mutating state."""
|
|
206
270
|
|
|
271
|
+
if (owner_repo is None) != (environment is None):
|
|
272
|
+
raise DeploymentKitError("ownerRepo and environment must be supplied together for setup proof planning.")
|
|
273
|
+
|
|
207
274
|
status = remote_setup_status(name)
|
|
275
|
+
if owner_repo is not None and environment is not None and status["phases"]["hostPlatform"]["state"] == "ready":
|
|
276
|
+
receipt = load_environment_proof_receipt(
|
|
277
|
+
name,
|
|
278
|
+
owner_repo=owner_repo,
|
|
279
|
+
environment=environment,
|
|
280
|
+
production=production,
|
|
281
|
+
)
|
|
282
|
+
status["phases"]["deploymentPath"] = _phase(
|
|
283
|
+
"DEPLOYMENT PATH PROVEN",
|
|
284
|
+
"proven" if receipt["state"] == "proven" else "not-proven",
|
|
285
|
+
detail=receipt,
|
|
286
|
+
)
|
|
287
|
+
status["currentPhase"] = _current_phase(status["phases"])
|
|
288
|
+
if receipt["state"] == "proven":
|
|
289
|
+
status["state"] = "environment-ready"
|
|
290
|
+
|
|
208
291
|
next_action = _next_action(status)
|
|
209
292
|
|
|
210
293
|
if (
|
|
@@ -230,9 +313,8 @@ def remote_setup_plan(
|
|
|
230
313
|
"command": command,
|
|
231
314
|
"proofPlan": proof,
|
|
232
315
|
"runAuthorization": proof["requiredAuthorization"],
|
|
316
|
+
"previousProof": status["phases"]["deploymentPath"].get("detail"),
|
|
233
317
|
}
|
|
234
|
-
elif (owner_repo is None) != (environment is None):
|
|
235
|
-
raise DeploymentKitError("ownerRepo and environment must be supplied together for setup proof planning.")
|
|
236
318
|
|
|
237
319
|
return {
|
|
238
320
|
**status,
|
|
@@ -14,7 +14,7 @@ from .keys import key_fingerprint, public_key_info, required_tool as required_ss
|
|
|
14
14
|
from .portable_profile import _machine_paths
|
|
15
15
|
from .profiles import load_profile, save_profile, target_profile_path, validate_profile, validate_target_name
|
|
16
16
|
from .security import check_identity_security, prepare_ssh_directory, validate_target_trust_file
|
|
17
|
-
from .ssh import verify_access
|
|
17
|
+
from .ssh import probe_authentication_methods, resolve_addresses, verify_access
|
|
18
18
|
|
|
19
19
|
|
|
20
20
|
HOME_LAB_BOOTSTRAP_PHRASE = "[HOME LAB ACCESS BOOTSTRAP]"
|
|
@@ -212,6 +212,23 @@ def _password_bootstrap_args(profile: dict[str, Any], *, first_use: bool, comman
|
|
|
212
212
|
]
|
|
213
213
|
|
|
214
214
|
|
|
215
|
+
def _require_password_bootstrap_capability(profile: dict[str, Any]) -> list[dict[str, Any]]:
|
|
216
|
+
"""Fail before password onboarding when a trusted server advertises key-only auth."""
|
|
217
|
+
|
|
218
|
+
evidence = [probe_authentication_methods(profile, address) for address in resolve_addresses(profile)]
|
|
219
|
+
observed = [item for item in evidence if item["observed"]]
|
|
220
|
+
if observed and not any(item["passwordSupported"] for item in observed):
|
|
221
|
+
advertised = sorted({method for item in observed for method in item["methods"]})
|
|
222
|
+
shown = ", ".join(advertised) if advertised else "none observed"
|
|
223
|
+
raise DeploymentKitError(
|
|
224
|
+
"Password-assisted Home Lab bootstrap is unavailable for this trusted target: "
|
|
225
|
+
f"the SSH server advertises {shown}, not password/keyboard-interactive authentication. "
|
|
226
|
+
"Use the Strict key-onboarding path and a trusted console or already-authorized session "
|
|
227
|
+
"for any required server-side key/account-policy change. Do not enable password SSH merely for bootstrap."
|
|
228
|
+
)
|
|
229
|
+
return evidence
|
|
230
|
+
|
|
231
|
+
|
|
215
232
|
def _install_public_key_with_password(profile: dict[str, Any], *, first_use: bool) -> None:
|
|
216
233
|
info = public_key_info(profile)
|
|
217
234
|
encoded = base64.b64encode(info["publicKey"].encode("utf-8")).decode("ascii")
|
|
@@ -316,6 +333,10 @@ def apply_home_lab_bootstrap(
|
|
|
316
333
|
except DeploymentKitError:
|
|
317
334
|
pass
|
|
318
335
|
|
|
336
|
+
# Existing trust lets us discover the server's advertised auth methods safely.
|
|
337
|
+
# Avoid a guaranteed-to-fail password prompt against public-key-only servers.
|
|
338
|
+
_require_password_bootstrap_capability(desired)
|
|
339
|
+
|
|
319
340
|
_install_public_key_with_password(desired, first_use=first_use)
|
|
320
341
|
validate_target_trust_file(desired)
|
|
321
342
|
evidence = verify_access(desired)
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
"""Durable workstation-local receipts for disposable environment proofs.
|
|
2
|
+
|
|
3
|
+
Receipts are diagnostic/readiness evidence, not deployment authorization or a workflow
|
|
4
|
+
database. They contain no credentials or mutable application data.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import hashlib
|
|
10
|
+
import json
|
|
11
|
+
import os
|
|
12
|
+
import re
|
|
13
|
+
from datetime import datetime, timezone
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
from . import __version__
|
|
18
|
+
from .errors import DeploymentKitError
|
|
19
|
+
from .keys import key_fingerprint
|
|
20
|
+
from .profiles import load_profile
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
SCHEMA_VERSION = 1
|
|
24
|
+
PROOF_CONTRACT_VERSION = 1
|
|
25
|
+
_ENV_RE = re.compile(r"^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$")
|
|
26
|
+
_REPO_RE = re.compile(r"^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _root() -> Path:
|
|
30
|
+
configured = os.environ.get("PROJECT_FORGE_HOME")
|
|
31
|
+
return Path(configured).expanduser() if configured else Path.home() / ".project-forge"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _receipt_path(owner_repo: str, environment: str) -> Path:
|
|
35
|
+
if _REPO_RE.fullmatch(owner_repo) is None:
|
|
36
|
+
raise DeploymentKitError("ownerRepo must use GitHub owner/repository form without URL syntax.")
|
|
37
|
+
if _ENV_RE.fullmatch(environment) is None:
|
|
38
|
+
raise DeploymentKitError("environment must use lowercase letters, digits, and internal hyphens.")
|
|
39
|
+
owner, repo = owner_repo.split("/", 1)
|
|
40
|
+
return _root() / "evidence" / "environment-proofs" / owner / repo / f"{environment}.json"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _sha256_file(path: Path) -> str:
|
|
44
|
+
try:
|
|
45
|
+
data = path.read_bytes()
|
|
46
|
+
except OSError as error:
|
|
47
|
+
raise DeploymentKitError(f"Cannot read target trust evidence {path}: {error}") from error
|
|
48
|
+
return hashlib.sha256(data).hexdigest()
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def current_target_signature(target_name: str) -> dict[str, Any]:
|
|
52
|
+
"""Return non-secret workstation/target identity used to invalidate stale proof."""
|
|
53
|
+
|
|
54
|
+
profile = load_profile(target_name)
|
|
55
|
+
identity = Path(profile["identityFile"])
|
|
56
|
+
known_hosts = Path(profile["knownHostsFile"])
|
|
57
|
+
signature = {
|
|
58
|
+
"targetName": profile["targetName"],
|
|
59
|
+
"targetOs": profile["targetOs"],
|
|
60
|
+
"hostName": profile["hostName"],
|
|
61
|
+
"sshAlias": profile["sshAlias"],
|
|
62
|
+
"user": profile["user"],
|
|
63
|
+
"port": profile["port"],
|
|
64
|
+
"addressOverride": profile["addressOverride"],
|
|
65
|
+
"identityFingerprint": key_fingerprint(identity),
|
|
66
|
+
"trustSha256": _sha256_file(known_hosts),
|
|
67
|
+
}
|
|
68
|
+
canonical = json.dumps(signature, sort_keys=True, separators=(",", ":")).encode("utf-8")
|
|
69
|
+
return {**signature, "signatureSha256": hashlib.sha256(canonical).hexdigest()}
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def write_environment_proof_receipt(result: dict[str, Any]) -> dict[str, Any]:
|
|
73
|
+
required = {
|
|
74
|
+
"ownerRepo",
|
|
75
|
+
"environment",
|
|
76
|
+
"targetName",
|
|
77
|
+
"production",
|
|
78
|
+
"proof",
|
|
79
|
+
"cleanupState",
|
|
80
|
+
"finalMutationState",
|
|
81
|
+
}
|
|
82
|
+
missing = sorted(required.difference(result))
|
|
83
|
+
if missing:
|
|
84
|
+
raise DeploymentKitError("Environment proof result is missing receipt fields: " + ", ".join(missing))
|
|
85
|
+
|
|
86
|
+
path = _receipt_path(result["ownerRepo"], result["environment"])
|
|
87
|
+
target = current_target_signature(result["targetName"])
|
|
88
|
+
receipt = {
|
|
89
|
+
"schemaVersion": SCHEMA_VERSION,
|
|
90
|
+
"proofContractVersion": PROOF_CONTRACT_VERSION,
|
|
91
|
+
"deploymentKitVersion": __version__,
|
|
92
|
+
"recordedAt": datetime.now(timezone.utc).isoformat().replace("+00:00", "Z"),
|
|
93
|
+
"ownerRepo": result["ownerRepo"],
|
|
94
|
+
"environment": result["environment"],
|
|
95
|
+
"targetName": result["targetName"],
|
|
96
|
+
"production": bool(result["production"]),
|
|
97
|
+
"proof": result["proof"],
|
|
98
|
+
"cleanupState": result["cleanupState"],
|
|
99
|
+
"finalMutationState": result["finalMutationState"],
|
|
100
|
+
"registryDigest": result.get("registryDigest"),
|
|
101
|
+
"localImageId": result.get("localImageId"),
|
|
102
|
+
"remoteImageId": result.get("remoteImageId"),
|
|
103
|
+
"targetSignature": target,
|
|
104
|
+
}
|
|
105
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
106
|
+
temp = path.with_suffix(".tmp")
|
|
107
|
+
temp.write_text(json.dumps(receipt, indent=2, sort_keys=True) + "\n", encoding="utf-8")
|
|
108
|
+
temp.replace(path)
|
|
109
|
+
return {"path": str(path), "recordedAt": receipt["recordedAt"], "state": "recorded"}
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def load_environment_proof_receipt(
|
|
113
|
+
target_name: str,
|
|
114
|
+
*,
|
|
115
|
+
owner_repo: str,
|
|
116
|
+
environment: str,
|
|
117
|
+
production: bool,
|
|
118
|
+
) -> dict[str, Any]:
|
|
119
|
+
"""Return whether a prior proof receipt is still applicable on this workstation."""
|
|
120
|
+
|
|
121
|
+
path = _receipt_path(owner_repo, environment)
|
|
122
|
+
if not path.is_file():
|
|
123
|
+
return {"state": "missing", "path": str(path), "mutated": False}
|
|
124
|
+
try:
|
|
125
|
+
receipt = json.loads(path.read_text(encoding="utf-8"))
|
|
126
|
+
except (OSError, json.JSONDecodeError) as error:
|
|
127
|
+
return {
|
|
128
|
+
"state": "invalid",
|
|
129
|
+
"path": str(path),
|
|
130
|
+
"reason": f"Cannot read proof receipt: {error}",
|
|
131
|
+
"mutated": False,
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
expected = {
|
|
135
|
+
"schemaVersion": SCHEMA_VERSION,
|
|
136
|
+
"proofContractVersion": PROOF_CONTRACT_VERSION,
|
|
137
|
+
"deploymentKitVersion": __version__,
|
|
138
|
+
"ownerRepo": owner_repo,
|
|
139
|
+
"environment": environment,
|
|
140
|
+
"targetName": target_name,
|
|
141
|
+
"production": bool(production),
|
|
142
|
+
}
|
|
143
|
+
mismatches = [key for key, value in expected.items() if receipt.get(key) != value]
|
|
144
|
+
if mismatches:
|
|
145
|
+
return {
|
|
146
|
+
"state": "stale",
|
|
147
|
+
"path": str(path),
|
|
148
|
+
"reason": "Receipt context changed: " + ", ".join(mismatches),
|
|
149
|
+
"mutated": False,
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if receipt.get("proof") != "passed" or receipt.get("cleanupState") not in {"removed", "absent"}:
|
|
153
|
+
return {
|
|
154
|
+
"state": "not-proven",
|
|
155
|
+
"path": str(path),
|
|
156
|
+
"reason": "Latest receipt is not a clean passing proof.",
|
|
157
|
+
"mutated": False,
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
try:
|
|
161
|
+
current = current_target_signature(target_name)
|
|
162
|
+
except DeploymentKitError as error:
|
|
163
|
+
return {"state": "stale", "path": str(path), "reason": str(error), "mutated": False}
|
|
164
|
+
previous = receipt.get("targetSignature") or {}
|
|
165
|
+
if previous.get("signatureSha256") != current.get("signatureSha256"):
|
|
166
|
+
return {
|
|
167
|
+
"state": "stale",
|
|
168
|
+
"path": str(path),
|
|
169
|
+
"reason": "Target/workstation identity or trust changed after the recorded proof.",
|
|
170
|
+
"mutated": False,
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return {
|
|
174
|
+
"state": "proven",
|
|
175
|
+
"path": str(path),
|
|
176
|
+
"recordedAt": receipt.get("recordedAt"),
|
|
177
|
+
"targetName": target_name,
|
|
178
|
+
"ownerRepo": owner_repo,
|
|
179
|
+
"environment": environment,
|
|
180
|
+
"production": bool(production),
|
|
181
|
+
"finalMutationState": receipt.get("finalMutationState"),
|
|
182
|
+
"mutated": False,
|
|
183
|
+
}
|