@mriekki/project-forge 4.3.0 → 4.4.1

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.
@@ -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": _probe_slug(owner),
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
- if owner_repo is not None and environment is not None and status["phases"]["hostPlatform"]["state"] == "ready":
276
- receipt = load_environment_proof_receipt(
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
- status["phases"]["deploymentPath"] = _phase(
283
- "DEPLOYMENT PATH PROVEN",
284
- "proven" if receipt["state"] == "proven" else "not-proven",
285
- detail=receipt,
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 owner_repo is not None
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": command,
359
+ "command": _proof_command(name, proof, production=production),
314
360
  "proofPlan": proof,
315
361
  "runAuthorization": proof["requiredAuthorization"],
316
- "previousProof": status["phases"]["deploymentPath"].get("detail"),
362
+ "previousProof": receipt,
317
363
  }
318
364
 
319
365
  return {
@@ -252,9 +252,18 @@ def _reservation_from_request(request: dict[str, Any]) -> dict[str, Any]:
252
252
  }
253
253
 
254
254
 
255
- def plan_sqlite_operation(name: str, action: str, request: dict[str, Any]) -> dict[str, Any]:
255
+ def plan_sqlite_operation(
256
+ name: str,
257
+ action: str,
258
+ request: dict[str, Any],
259
+ *,
260
+ production: bool = False,
261
+ ) -> dict[str, Any]:
256
262
  if action not in {"import", "backup", "migrate", "rollback-check"}:
257
263
  raise DeploymentKitError("Unsupported SQLite plan action.")
264
+ if not isinstance(production, bool):
265
+ raise DeploymentKitError("SQLite Production classification must be a boolean.")
266
+ production_classified = production or request["environment"] == "production"
258
267
  _require_ready_platform(name)
259
268
  inventory = sqlite_inventory(name)
260
269
  existing = _find(inventory, request["appSlug"], request["environment"])
@@ -284,7 +293,8 @@ def plan_sqlite_operation(name: str, action: str, request: dict[str, Any]) -> di
284
293
  raise DeploymentKitError(f"SQLite {action} requires an existing database.")
285
294
  return {
286
295
  "targetName": name, "action": action, "ownerRepo": request["ownerRepo"],
287
- "environment": request["environment"], "appSlug": request["appSlug"], "image": request["image"],
296
+ "environment": request["environment"], "productionClassified": production_classified,
297
+ "appSlug": request["appSlug"], "image": request["image"],
288
298
  "databaseFile": spec["databaseFile"], "runAs": spec["runAs"],
289
299
  "sourceDatabase": request["sourceDatabase"] if action == "import" else None,
290
300
  "firstInstall": bool(request["firstInstall"] if action == "migrate" else False),
@@ -295,7 +305,9 @@ def plan_sqlite_operation(name: str, action: str, request: dict[str, Any]) -> di
295
305
 
296
306
 
297
307
  def authorization_phrase(plan: dict[str, Any]) -> str:
298
- production = plan["environment"] == "production"
308
+ production = plan.get("productionClassified")
309
+ if not isinstance(production, bool):
310
+ raise DeploymentKitError("Malformed SQLite operation plan Production classification.")
299
311
  mapping = {
300
312
  "import": (SQLITE_IMPORT_WRITE_PHRASE, PRODUCTION_SQLITE_IMPORT_WRITE_PHRASE),
301
313
  "backup": (SQLITE_BACKUP_WRITE_PHRASE, PRODUCTION_SQLITE_BACKUP_WRITE_PHRASE),
@@ -307,9 +319,11 @@ def authorization_phrase(plan: dict[str, Any]) -> str:
307
319
 
308
320
 
309
321
  def apply_sqlite_operation(plan: dict[str, Any]) -> dict[str, Any]:
310
- required = {"targetName", "action", "ownerRepo", "environment", "appSlug", "image", "databaseFile", "runAs", "sourceDatabase", "firstInstall", "stopsManagedRuntime", "createsVerifiedBackup", "inventorySnapshot", "expectedReservationSha256", "spec"}
311
- if not isinstance(plan, dict) or set(plan) != required:
322
+ required = {"targetName", "action", "ownerRepo", "environment", "productionClassified", "appSlug", "image", "databaseFile", "runAs", "sourceDatabase", "firstInstall", "stopsManagedRuntime", "createsVerifiedBackup", "inventorySnapshot", "expectedReservationSha256", "spec"}
323
+ if not isinstance(plan, dict) or set(plan) != required or not isinstance(plan["productionClassified"], bool):
312
324
  raise DeploymentKitError("Malformed SQLite operation plan.")
325
+ if plan["environment"] == "production" and not plan["productionClassified"]:
326
+ raise DeploymentKitError("Literal production environment must remain Production-classified.")
313
327
  _require_ready_platform(plan["targetName"])
314
328
  current = sqlite_inventory(plan["targetName"])
315
329
  if _snapshot(current) != plan["inventorySnapshot"]:
@@ -319,6 +333,7 @@ def apply_sqlite_operation(plan: dict[str, Any]) -> dict[str, Any]:
319
333
  result = sqlite_inventory(plan["targetName"])
320
334
  result["mutated"] = True
321
335
  result["operation"] = plan["action"]; result["operationOutput"] = output.strip()
336
+ result["productionClassified"] = plan["productionClassified"]
322
337
  return result
323
338
 
324
339
 
@@ -332,4 +347,4 @@ def assert_app_spec_matches_persistence(name: str, app_spec: dict[str, Any]) ->
332
347
  if existing["runAs"] != app_spec["runAs"]:
333
348
  raise DeploymentKitError("Application runAs does not match the prepared SQLite persistence reservation.")
334
349
  if app_spec.get("persistence", {}).get("mode") != "directory":
335
- raise DeploymentKitError("Prepared SQLite persistence requires directory application persistence.")
350
+ raise DeploymentKitError("Prepared SQLite persistence requires directory application persistence.")
@@ -38,6 +38,11 @@ def _common(parser: argparse.ArgumentParser) -> None:
38
38
  parser.add_argument('name')
39
39
  parser.add_argument('--owner-repo', required=True)
40
40
  parser.add_argument('--environment', required=True)
41
+ parser.add_argument(
42
+ '--production',
43
+ action='store_true',
44
+ help="Classify this named environment as Production even when its name is not literally 'production'",
45
+ )
41
46
  parser.add_argument('--app-slug', required=True)
42
47
  parser.add_argument('--image', required=True)
43
48
  parser.add_argument('--maintenance-env', action='append', default=[], help='Non-secret image maintenance environment value in NAME=value form; repeat as needed')
@@ -68,19 +73,20 @@ def main(argv: list[str]|None=None) -> int:
68
73
  args=build_parser().parse_args(argv)
69
74
  try:
70
75
  if args.command=='inventory': _print(sqlite_inventory(args.name),as_json=args.json); return 0
76
+ production_classified = args.production or args.environment == 'production'
71
77
  request=normalize_sqlite_request(owner_repo=args.owner_repo,environment=args.environment,app_slug=args.app_slug,image=args.image,
72
78
  maintenance_environment=_parse_env(args.maintenance_env),run_as=getattr(args,'run_as',None),database_file=getattr(args,'database_file',None),
73
79
  source_database=getattr(args,'source_database',None),first_install=getattr(args,'first_install',False))
74
80
  if args.command=='recover-import':
75
- plan=plan_sqlite_import_recovery(args.name,request)
81
+ plan=plan_sqlite_import_recovery(args.name,request,production=production_classified)
76
82
  _print({k:v for k,v in plan.items() if k!='request'},as_json=False)
77
83
  phrase=recovery_authorization_phrase(plan)
78
84
  _confirm(args,phrase,f"recover SQLite import for {plan['appSlug']} ({plan['environment']}) on target {args.name}")
79
85
  _print(apply_sqlite_import_recovery(plan),as_json=args.json); return 0
80
- plan=plan_sqlite_operation(args.name,args.command,request)
86
+ plan=plan_sqlite_operation(args.name,args.command,request,production=production_classified)
81
87
  _print({k:v for k,v in plan.items() if k!='spec'},as_json=False)
82
88
  phrase=authorization_phrase(plan)
83
89
  _confirm(args,phrase,f"run SQLite {args.command} for {plan['appSlug']} ({plan['environment']}) on target {args.name}")
84
90
  _print(apply_sqlite_operation(plan),as_json=args.json); return 0
85
91
  except DeploymentKitError as error:
86
- print(f"ERROR: {error}",file=sys.stderr); return 2
92
+ print(f"ERROR: {error}",file=sys.stderr); return 2
@@ -356,13 +356,22 @@ def _snapshot(inspection: dict[str, Any]) -> str:
356
356
  return hashlib.sha256(json.dumps(value, sort_keys=True, separators=(",", ":")).encode("utf-8")).hexdigest()
357
357
 
358
358
 
359
- def plan_sqlite_import_recovery(name: str, request: dict[str, Any]) -> dict[str, Any]:
359
+ def plan_sqlite_import_recovery(
360
+ name: str,
361
+ request: dict[str, Any],
362
+ *,
363
+ production: bool = False,
364
+ ) -> dict[str, Any]:
365
+ if not isinstance(production, bool):
366
+ raise DeploymentKitError("SQLite Production classification must be a boolean.")
367
+ production_classified = production or request["environment"] == "production"
360
368
  inspection = inspect_sqlite_import_recovery(name, request)
361
369
  return {
362
370
  "targetName": name,
363
371
  "action": "recover-import",
364
372
  "ownerRepo": request["ownerRepo"],
365
373
  "environment": request["environment"],
374
+ "productionClassified": production_classified,
366
375
  "appSlug": request["appSlug"],
367
376
  "image": request["image"],
368
377
  "runAs": request["runAs"],
@@ -381,22 +390,32 @@ def plan_sqlite_import_recovery(name: str, request: dict[str, Any]) -> dict[str,
381
390
 
382
391
 
383
392
  def authorization_phrase(plan: dict[str, Any]) -> str:
393
+ production = plan.get("productionClassified")
394
+ if not isinstance(production, bool):
395
+ raise DeploymentKitError("Malformed SQLite import recovery Production classification.")
384
396
  return (
385
397
  PRODUCTION_SQLITE_IMPORT_RECOVERY_WRITE_PHRASE
386
- if plan["environment"] == "production"
398
+ if production
387
399
  else SQLITE_IMPORT_RECOVERY_WRITE_PHRASE
388
400
  )
389
401
 
390
402
 
391
403
  def apply_sqlite_import_recovery(plan: dict[str, Any]) -> dict[str, Any]:
392
404
  required = {
393
- "targetName", "action", "ownerRepo", "environment", "appSlug", "image", "runAs",
405
+ "targetName", "action", "ownerRepo", "environment", "productionClassified", "appSlug", "image", "runAs",
394
406
  "databaseFile", "sourceDatabase", "databaseSha256", "sourceSha256", "databaseBytes",
395
407
  "stateSnapshot", "overwritesDatabase", "deletesPersistentData", "createsReservation",
396
408
  "reverifiesDatabase", "request",
397
409
  }
398
- if not isinstance(plan, dict) or set(plan) != required or plan["action"] != "recover-import":
410
+ if (
411
+ not isinstance(plan, dict)
412
+ or set(plan) != required
413
+ or plan["action"] != "recover-import"
414
+ or not isinstance(plan["productionClassified"], bool)
415
+ ):
399
416
  raise DeploymentKitError("Malformed SQLite import recovery plan.")
417
+ if plan["environment"] == "production" and not plan["productionClassified"]:
418
+ raise DeploymentKitError("Literal production environment must remain Production-classified.")
400
419
  current = inspect_sqlite_import_recovery(plan["targetName"], plan["request"])
401
420
  if _snapshot(current) != plan["stateSnapshot"]:
402
421
  raise DeploymentKitError(
@@ -434,4 +453,5 @@ def apply_sqlite_import_recovery(plan: dict[str, Any]) -> dict[str, Any]:
434
453
  inventory["mutated"] = True
435
454
  inventory["operation"] = "recover-import"
436
455
  inventory["operationOutput"] = output.strip()
456
+ inventory["productionClassified"] = plan["productionClassified"]
437
457
  return inventory
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mriekki/project-forge",
3
- "version": "4.3.0",
3
+ "version": "4.4.1",
4
4
  "description": "Public consumer distribution and updater for Project Forge engineering assets.",
5
5
  "type": "module",
6
6
  "bin": {