@mriekki/project-forge 4.3.0 → 4.4.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 +2 -0
- package/assets/generated/ASSEMBLY.json +20 -10
- package/assets/generated/core/core/process/DEPLOYMENT_ENABLEMENT.md +25 -1
- package/assets/generated/core/core/skills/deploy/SKILL.md +15 -3
- package/assets/generated/core/core/skills/deployment-readiness/SKILL.md +45 -1
- package/assets/generated/deployment/src/project_forge_deployment/app_cli.py +72 -21
- package/assets/generated/deployment/src/project_forge_deployment/app_deployment.py +194 -0
- package/assets/generated/deployment/src/project_forge_deployment/environment_cli.py +4 -1
- package/assets/generated/deployment/src/project_forge_deployment/environment_isolation.py +438 -0
- package/assets/generated/deployment/src/project_forge_deployment/environment_probe.py +57 -1
- package/assets/generated/deployment/src/project_forge_deployment/environment_setup.py +72 -26
- package/package.json +1 -1
|
@@ -12,6 +12,7 @@ from typing import Any
|
|
|
12
12
|
from .app_runtime import normalize_app_spec, plan_app_deploy
|
|
13
13
|
from .app_runtime_workstation import apply_app_deploy_with_preloaded_image
|
|
14
14
|
from .box_platform import platform_status
|
|
15
|
+
from .environment_isolation import environment_isolation_preflight
|
|
15
16
|
from .errors import DeploymentKitError
|
|
16
17
|
from .image_transfer import remote_image_present, transfer_image, verify_local_image
|
|
17
18
|
from .proof_receipt import write_environment_proof_receipt
|
|
@@ -92,6 +93,27 @@ def _resolve_probe_image(image_tag: str) -> str:
|
|
|
92
93
|
raise DeploymentKitError("Environment proof image has no usable immutable sha256 RepoDigest.")
|
|
93
94
|
|
|
94
95
|
|
|
96
|
+
def _proof_isolation_preflight(
|
|
97
|
+
target_name: str,
|
|
98
|
+
*,
|
|
99
|
+
owner_repo: str,
|
|
100
|
+
environment: str,
|
|
101
|
+
production: bool,
|
|
102
|
+
probe_app_slug: str,
|
|
103
|
+
) -> dict[str, Any]:
|
|
104
|
+
return environment_isolation_preflight(
|
|
105
|
+
target_name,
|
|
106
|
+
owner_repo=owner_repo,
|
|
107
|
+
environment=environment,
|
|
108
|
+
production=production,
|
|
109
|
+
app_slug=probe_app_slug,
|
|
110
|
+
container_name=f"{probe_app_slug}--{environment}",
|
|
111
|
+
runtime_port=80,
|
|
112
|
+
persistence_mode="none",
|
|
113
|
+
require_vacant_app_identity=True,
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
|
|
95
117
|
def plan_environment_proof(
|
|
96
118
|
target_name: str,
|
|
97
119
|
*,
|
|
@@ -111,17 +133,27 @@ def plan_environment_proof(
|
|
|
111
133
|
raise DeploymentKitError(
|
|
112
134
|
f"Environment proof requires a ready shared platform; target '{target_name}' is '{platform.get('state', 'unknown')}'."
|
|
113
135
|
)
|
|
136
|
+
probe_app_slug = _probe_slug(owner)
|
|
137
|
+
isolation = _proof_isolation_preflight(
|
|
138
|
+
target_name,
|
|
139
|
+
owner_repo=owner,
|
|
140
|
+
environment=env,
|
|
141
|
+
production=production,
|
|
142
|
+
probe_app_slug=probe_app_slug,
|
|
143
|
+
)
|
|
114
144
|
return {
|
|
115
145
|
"targetName": target_name,
|
|
116
146
|
"ownerRepo": owner,
|
|
117
147
|
"environment": env,
|
|
118
148
|
"production": production,
|
|
119
|
-
"probeAppSlug":
|
|
149
|
+
"probeAppSlug": probe_app_slug,
|
|
120
150
|
"probeImageTag": probe_image,
|
|
121
151
|
"artifactDelivery": "workstation-transfer",
|
|
122
152
|
"persistence": "none",
|
|
123
153
|
"healthPath": "/",
|
|
124
154
|
"cleanup": "automatic",
|
|
155
|
+
"isolationPreflight": isolation,
|
|
156
|
+
"runnable": isolation["state"] == "clear",
|
|
125
157
|
"requiredAuthorization": (
|
|
126
158
|
PRODUCTION_ENVIRONMENT_PROOF_WRITE_PHRASE if production else ENVIRONMENT_PROOF_WRITE_PHRASE
|
|
127
159
|
),
|
|
@@ -139,6 +171,8 @@ def _proof_result(plan: dict[str, Any]) -> dict[str, Any]:
|
|
|
139
171
|
"preflight": {
|
|
140
172
|
"targetAccess": "verified-by-platform-doctor",
|
|
141
173
|
"hostPlatform": "ready",
|
|
174
|
+
"environmentIsolation": plan["isolationPreflight"]["state"],
|
|
175
|
+
"isolationEvidenceSha256": plan["isolationPreflight"]["evidenceSha256"],
|
|
142
176
|
},
|
|
143
177
|
"registryDigest": None,
|
|
144
178
|
"localImageId": None,
|
|
@@ -199,10 +233,32 @@ def run_environment_proof(plan: dict[str, Any]) -> dict[str, Any]:
|
|
|
199
233
|
"persistence",
|
|
200
234
|
"healthPath",
|
|
201
235
|
"cleanup",
|
|
236
|
+
"isolationPreflight",
|
|
237
|
+
"runnable",
|
|
202
238
|
"requiredAuthorization",
|
|
203
239
|
}
|
|
204
240
|
if not isinstance(plan, dict) or set(plan) != required:
|
|
205
241
|
raise DeploymentKitError("Malformed environment proof plan.")
|
|
242
|
+
if plan["isolationPreflight"].get("state") != "clear" or not plan["runnable"]:
|
|
243
|
+
raise DeploymentKitError(
|
|
244
|
+
"Environment proof is blocked by the reviewed isolation/collision preflight; resolve the reported conflicts and create a fresh plan."
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
current_isolation = _proof_isolation_preflight(
|
|
248
|
+
plan["targetName"],
|
|
249
|
+
owner_repo=plan["ownerRepo"],
|
|
250
|
+
environment=plan["environment"],
|
|
251
|
+
production=plan["production"],
|
|
252
|
+
probe_app_slug=plan["probeAppSlug"],
|
|
253
|
+
)
|
|
254
|
+
if current_isolation["state"] != "clear":
|
|
255
|
+
raise DeploymentKitError(
|
|
256
|
+
"Environment isolation became blocked after the proof plan was reviewed; no proof mutation was attempted."
|
|
257
|
+
)
|
|
258
|
+
if current_isolation["evidenceSha256"] != plan["isolationPreflight"].get("evidenceSha256"):
|
|
259
|
+
raise DeploymentKitError(
|
|
260
|
+
"Environment isolation evidence changed after the proof plan was reviewed; create and authorize a fresh plan."
|
|
261
|
+
)
|
|
206
262
|
|
|
207
263
|
result = _proof_result(plan)
|
|
208
264
|
deploy_plan: dict[str, Any] | None = None
|
|
@@ -248,7 +248,7 @@ def _next_action(status: dict[str, Any]) -> dict[str, Any]:
|
|
|
248
248
|
return {
|
|
249
249
|
"kind": "complete",
|
|
250
250
|
"action": "remote-environment-ready",
|
|
251
|
-
"reason": "Fresh workstation-local proof evidence matches this Project/environment/target context.",
|
|
251
|
+
"reason": "Fresh workstation-local proof evidence matches this Project/environment/target context and current isolation remains clear.",
|
|
252
252
|
"proofReceipt": phases["deploymentPath"].get("detail"),
|
|
253
253
|
}
|
|
254
254
|
|
|
@@ -259,6 +259,36 @@ def _next_action(status: dict[str, Any]) -> dict[str, Any]:
|
|
|
259
259
|
}
|
|
260
260
|
|
|
261
261
|
|
|
262
|
+
def _proof_command(name: str, proof: dict[str, Any], *, production: bool) -> str:
|
|
263
|
+
command = (
|
|
264
|
+
f"project-forge-deploy environment proof plan {name} "
|
|
265
|
+
f"--owner-repo {proof['ownerRepo']} --environment {proof['environment']}"
|
|
266
|
+
)
|
|
267
|
+
if production:
|
|
268
|
+
command += " --production"
|
|
269
|
+
return command
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def _blocked_isolation_action(
|
|
273
|
+
name: str,
|
|
274
|
+
proof: dict[str, Any],
|
|
275
|
+
*,
|
|
276
|
+
production: bool,
|
|
277
|
+
) -> dict[str, Any]:
|
|
278
|
+
return {
|
|
279
|
+
"kind": "blocked",
|
|
280
|
+
"action": "resolve-environment-isolation-collision",
|
|
281
|
+
"command": _proof_command(name, proof, production=production),
|
|
282
|
+
"reason": (
|
|
283
|
+
"The read-only colocated-environment preflight found a managed ownership or footprint collision. "
|
|
284
|
+
"Do not redirect this environment to a sibling target identity or take over existing runtime, route, persistence, or lock state."
|
|
285
|
+
),
|
|
286
|
+
"proofPlan": proof,
|
|
287
|
+
"isolationPreflight": proof.get("isolationPreflight"),
|
|
288
|
+
"safety": "Inspect the reported managed ownership records and resolve the collision explicitly before creating a fresh proof plan.",
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
|
|
262
292
|
def remote_setup_plan(
|
|
263
293
|
name: str,
|
|
264
294
|
*,
|
|
@@ -272,48 +302,64 @@ def remote_setup_plan(
|
|
|
272
302
|
raise DeploymentKitError("ownerRepo and environment must be supplied together for setup proof planning.")
|
|
273
303
|
|
|
274
304
|
status = remote_setup_status(name)
|
|
275
|
-
|
|
276
|
-
|
|
305
|
+
proof: dict[str, Any] | None = None
|
|
306
|
+
receipt: dict[str, Any] | None = None
|
|
307
|
+
if (
|
|
308
|
+
owner_repo is not None
|
|
309
|
+
and environment is not None
|
|
310
|
+
and status["phases"]["hostPlatform"]["state"] == "ready"
|
|
311
|
+
):
|
|
312
|
+
# Current machine isolation must be checked even when a prior proof receipt is
|
|
313
|
+
# otherwise reusable. A sibling environment can be added after the receipt was
|
|
314
|
+
# recorded without changing workstation/target identity or trust evidence.
|
|
315
|
+
proof = plan_environment_proof(
|
|
277
316
|
name,
|
|
278
317
|
owner_repo=owner_repo,
|
|
279
318
|
environment=environment,
|
|
280
319
|
production=production,
|
|
281
320
|
)
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
321
|
+
receipt = load_environment_proof_receipt(
|
|
322
|
+
name,
|
|
323
|
+
owner_repo=owner_repo,
|
|
324
|
+
environment=environment,
|
|
325
|
+
production=production,
|
|
286
326
|
)
|
|
327
|
+
isolation_clear = proof.get("runnable", True)
|
|
328
|
+
if isolation_clear:
|
|
329
|
+
status["phases"]["deploymentPath"] = _phase(
|
|
330
|
+
"DEPLOYMENT PATH PROVEN",
|
|
331
|
+
"proven" if receipt["state"] == "proven" else "not-proven",
|
|
332
|
+
detail=receipt,
|
|
333
|
+
)
|
|
334
|
+
if receipt["state"] == "proven":
|
|
335
|
+
status["state"] = "environment-ready"
|
|
336
|
+
else:
|
|
337
|
+
status["phases"]["deploymentPath"] = _phase(
|
|
338
|
+
"DEPLOYMENT PATH PROVEN",
|
|
339
|
+
"blocked",
|
|
340
|
+
detail={
|
|
341
|
+
"proofReceipt": receipt,
|
|
342
|
+
"isolationPreflight": proof.get("isolationPreflight"),
|
|
343
|
+
},
|
|
344
|
+
)
|
|
345
|
+
status["state"] = "needs-setup"
|
|
287
346
|
status["currentPhase"] = _current_phase(status["phases"])
|
|
288
|
-
if receipt["state"] == "proven":
|
|
289
|
-
status["state"] = "environment-ready"
|
|
290
347
|
|
|
291
348
|
next_action = _next_action(status)
|
|
292
349
|
|
|
293
|
-
if (
|
|
350
|
+
if proof is not None and not proof.get("runnable", True):
|
|
351
|
+
next_action = _blocked_isolation_action(name, proof, production=production)
|
|
352
|
+
elif (
|
|
294
353
|
next_action["action"] == "plan-deployment-path-proof"
|
|
295
|
-
and
|
|
296
|
-
and environment is not None
|
|
354
|
+
and proof is not None
|
|
297
355
|
):
|
|
298
|
-
proof = plan_environment_proof(
|
|
299
|
-
name,
|
|
300
|
-
owner_repo=owner_repo,
|
|
301
|
-
environment=environment,
|
|
302
|
-
production=production,
|
|
303
|
-
)
|
|
304
|
-
command = (
|
|
305
|
-
f"project-forge-deploy environment proof plan {name} "
|
|
306
|
-
f"--owner-repo {proof['ownerRepo']} --environment {proof['environment']}"
|
|
307
|
-
)
|
|
308
|
-
if production:
|
|
309
|
-
command += " --production"
|
|
310
356
|
next_action = {
|
|
311
357
|
"kind": "read-only",
|
|
312
358
|
"action": "review-deployment-path-proof",
|
|
313
|
-
"command":
|
|
359
|
+
"command": _proof_command(name, proof, production=production),
|
|
314
360
|
"proofPlan": proof,
|
|
315
361
|
"runAuthorization": proof["requiredAuthorization"],
|
|
316
|
-
"previousProof":
|
|
362
|
+
"previousProof": receipt,
|
|
317
363
|
}
|
|
318
364
|
|
|
319
365
|
return {
|