@mriekki/lean 0.4.0 → 0.4.2

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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
3
  "package": "@mriekki/lean",
4
- "version": "0.4.0",
4
+ "version": "0.4.2",
5
5
  "defaultProfile": "greenfield-poc",
6
6
  "profiles": {
7
7
  "greenfield-poc": {
@@ -23,7 +23,7 @@
23
23
  },
24
24
  "deployment": {
25
25
  "repository": "mriekki/lean-poc-deployment-kit",
26
- "commit": "4c23140576b39608a3ef285cad4abe53dccb9c47",
26
+ "commit": "2763e604c20e64720d7de71fa93a88d99677be36",
27
27
  "distribution": "greenfield-poc",
28
28
  "descriptor": "assets/deployment/distribution.json",
29
29
  "sourceRoot": "assets/generated/deployment",
@@ -18,9 +18,8 @@
18
18
  {"source": "src/lean_poc_deployment/errors.py", "destination": "tools/lean-deployment/lean_poc_deployment/errors.py"},
19
19
  {"source": "src/lean_poc_deployment/keys.py", "destination": "tools/lean-deployment/lean_poc_deployment/keys.py"},
20
20
  {"source": "src/lean_poc_deployment/profiles.py", "destination": "tools/lean-deployment/lean_poc_deployment/profiles.py"},
21
- {"source": "src/lean_poc_deployment/teardown.py", "destination": "tools/lean-deployment/lean_poc_deployment/teardown.py"},
22
- {"source": "src/lean_poc_deployment/teardown_cli.py", "destination": "tools/lean-deployment/lean_poc_deployment/teardown_cli.py"},
23
- {"source": "src/lean_poc_deployment/teardown_remote.py", "destination": "tools/lean-deployment/lean_poc_deployment/teardown_remote.py"},
21
+ {"source": "src/lean_poc_deployment/registry_access.py", "destination": "tools/lean-deployment/lean_poc_deployment/registry_access.py"},
22
+ {"source": "src/lean_poc_deployment/registry_cli.py", "destination": "tools/lean-deployment/lean_poc_deployment/registry_cli.py"},
24
23
  {"source": "src/lean_poc_deployment/route_registry.py", "destination": "tools/lean-deployment/lean_poc_deployment/route_registry.py"},
25
24
  {"source": "src/lean_poc_deployment/route_registry_remote.py", "destination": "tools/lean-deployment/lean_poc_deployment/route_registry_remote.py"},
26
25
  {"source": "src/lean_poc_deployment/security.py", "destination": "tools/lean-deployment/lean_poc_deployment/security.py"},
@@ -29,6 +28,9 @@
29
28
  {"source": "src/lean_poc_deployment/sqlite_cli.py", "destination": "tools/lean-deployment/lean_poc_deployment/sqlite_cli.py"},
30
29
  {"source": "src/lean_poc_deployment/sqlite_import_recovery.py", "destination": "tools/lean-deployment/lean_poc_deployment/sqlite_import_recovery.py"},
31
30
  {"source": "src/lean_poc_deployment/ssh.py", "destination": "tools/lean-deployment/lean_poc_deployment/ssh.py"},
31
+ {"source": "src/lean_poc_deployment/teardown.py", "destination": "tools/lean-deployment/lean_poc_deployment/teardown.py"},
32
+ {"source": "src/lean_poc_deployment/teardown_cli.py", "destination": "tools/lean-deployment/lean_poc_deployment/teardown_cli.py"},
33
+ {"source": "src/lean_poc_deployment/teardown_remote.py", "destination": "tools/lean-deployment/lean_poc_deployment/teardown_remote.py"},
32
34
  {"source": "src/lean_poc_deployment/trust.py", "destination": "tools/lean-deployment/lean_poc_deployment/trust.py"}
33
35
  ]
34
36
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
3
  "package": "@mriekki/lean",
4
- "version": "0.4.0",
4
+ "version": "0.4.2",
5
5
  "components": {
6
6
  "core": {
7
7
  "repository": "mriekki/lean-agentic-core",
@@ -243,7 +243,7 @@
243
243
  },
244
244
  "deployment": {
245
245
  "repository": "mriekki/lean-poc-deployment-kit",
246
- "commit": "4c23140576b39608a3ef285cad4abe53dccb9c47",
246
+ "commit": "2763e604c20e64720d7de71fa93a88d99677be36",
247
247
  "distribution": "greenfield-poc",
248
248
  "files": [
249
249
  {
@@ -304,7 +304,7 @@
304
304
  {
305
305
  "source": "src/lean_poc_deployment/entrypoint.py",
306
306
  "destination": "tools/lean-deployment/lean_poc_deployment/entrypoint.py",
307
- "sha256": "08a27a64680f7a72b68c6272f5185c7fdb23678efbda456fd0894272e2e6187e"
307
+ "sha256": "5161aec6d5a1fc684894a1011b3d577868cedaa7039ea647862a4628067fabd8"
308
308
  },
309
309
  {
310
310
  "source": "src/lean_poc_deployment/errors.py",
@@ -322,19 +322,14 @@
322
322
  "sha256": "9d73f86714db5c59a07354348e394647d62a3c31b173c4d3c4c4353872da908a"
323
323
  },
324
324
  {
325
- "source": "src/lean_poc_deployment/teardown.py",
326
- "destination": "tools/lean-deployment/lean_poc_deployment/teardown.py",
327
- "sha256": "edcf921aa92988176b756abf4bbc9b0b83ae1d855b6e8d487b67aaad7727cb55"
325
+ "source": "src/lean_poc_deployment/registry_access.py",
326
+ "destination": "tools/lean-deployment/lean_poc_deployment/registry_access.py",
327
+ "sha256": "5815a1921ecf37dc395f9c68b1ebda2b17dd6a44620b2356d1adff5df44c4382"
328
328
  },
329
329
  {
330
- "source": "src/lean_poc_deployment/teardown_cli.py",
331
- "destination": "tools/lean-deployment/lean_poc_deployment/teardown_cli.py",
332
- "sha256": "ec1271db5f57135189892e87f2df3ffc5fcb6e763ff1eb9604bc4e50788c3325"
333
- },
334
- {
335
- "source": "src/lean_poc_deployment/teardown_remote.py",
336
- "destination": "tools/lean-deployment/lean_poc_deployment/teardown_remote.py",
337
- "sha256": "9745fd9f721eea2da8f5d32688cc6a293e46aa9f9e5c5825c6a08cacb1121fb1"
330
+ "source": "src/lean_poc_deployment/registry_cli.py",
331
+ "destination": "tools/lean-deployment/lean_poc_deployment/registry_cli.py",
332
+ "sha256": "6288d85ae44708362ba09e1b82546617a9660dd9d5601ced081ee44d3fc27bda"
338
333
  },
339
334
  {
340
335
  "source": "src/lean_poc_deployment/route_registry.py",
@@ -374,7 +369,22 @@
374
369
  {
375
370
  "source": "src/lean_poc_deployment/ssh.py",
376
371
  "destination": "tools/lean-deployment/lean_poc_deployment/ssh.py",
377
- "sha256": "9eb568ec75a288b1069e629c67d567d5bb0a7a2074563c70f49057326154f7d2"
372
+ "sha256": "916a272ed5978df642e6084a3030c32f2c9dcb4b3b686a194428f9810ec55e9d"
373
+ },
374
+ {
375
+ "source": "src/lean_poc_deployment/teardown.py",
376
+ "destination": "tools/lean-deployment/lean_poc_deployment/teardown.py",
377
+ "sha256": "edcf921aa92988176b756abf4bbc9b0b83ae1d855b6e8d487b67aaad7727cb55"
378
+ },
379
+ {
380
+ "source": "src/lean_poc_deployment/teardown_cli.py",
381
+ "destination": "tools/lean-deployment/lean_poc_deployment/teardown_cli.py",
382
+ "sha256": "ec1271db5f57135189892e87f2df3ffc5fcb6e763ff1eb9604bc4e50788c3325"
383
+ },
384
+ {
385
+ "source": "src/lean_poc_deployment/teardown_remote.py",
386
+ "destination": "tools/lean-deployment/lean_poc_deployment/teardown_remote.py",
387
+ "sha256": "9745fd9f721eea2da8f5d32688cc6a293e46aa9f9e5c5825c6a08cacb1121fb1"
378
388
  },
379
389
  {
380
390
  "source": "src/lean_poc_deployment/trust.py",
@@ -6,6 +6,7 @@ import sys
6
6
 
7
7
  from .app_cli import main as app_main
8
8
  from .cli import main as core_main
9
+ from .registry_cli import main as registry_main
9
10
  from .sqlite_cli import main as sqlite_main
10
11
  from .teardown_cli import main as teardown_main
11
12
 
@@ -14,12 +15,15 @@ def main(argv: list[str] | None = None) -> int:
14
15
  args = list(sys.argv[1:] if argv is None else argv)
15
16
  if args and args[0] == "app":
16
17
  return app_main(args[1:])
18
+ if args and args[0] == "registry":
19
+ return registry_main(args[1:])
17
20
  if args and args[0] == "sqlite":
18
21
  return sqlite_main(args[1:])
19
22
  if args and args[0] == "tear-down":
20
23
  return teardown_main(args[1:])
21
24
  if not args or args == ["--help"] or args == ["-h"]:
22
25
  print("Command group: app Generic immutable application runtime inventory/deployment")
26
+ print("Command group: registry Read-only exact-image registry access verification")
23
27
  print("Command group: sqlite SQLite persistence import/backup/migration/rollback checks")
24
28
  print("Command group: tear-down Scoped destructive POC environment/local cleanup")
25
29
  return core_main(args)
@@ -0,0 +1,114 @@
1
+ """Read-only exact-image registry access verification for logical targets."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ import shlex
7
+ import subprocess
8
+ from typing import Any
9
+
10
+ from .errors import DeploymentKitError
11
+ from .profiles import load_profile
12
+ from .ssh import ssh_args, verify_access
13
+
14
+
15
+ _IMAGE_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._/:+-]*@sha256:[0-9a-f]{64}$")
16
+
17
+ _REMOTE_SCRIPT = r'''#!/usr/bin/env bash
18
+ set -Eeuo pipefail
19
+
20
+ unset DOCKER_CONFIG
21
+
22
+ IMAGE="$1"
23
+
24
+ [[ "$IMAGE" =~ ^[A-Za-z0-9][A-Za-z0-9._/:+-]*@sha256:[0-9a-f]{64}$ ]] || {
25
+ echo "Image must be an immutable sha256 digest reference." >&2
26
+ exit 41
27
+ }
28
+
29
+ command -v docker >/dev/null 2>&1 || {
30
+ echo "Docker CLI is unavailable on the target." >&2
31
+ exit 42
32
+ }
33
+
34
+ config_dir="$HOME/.docker"
35
+ config_file="$config_dir/config.json"
36
+ credential_state="absent"
37
+
38
+ if [[ -e "$config_file" ]]; then
39
+ [[ -d "$config_dir" && ! -L "$config_dir" && -f "$config_file" && ! -L "$config_file" ]] || {
40
+ echo "Docker credential path is not a regular protected directory/file." >&2
41
+ exit 43
42
+ }
43
+ dir_mode="$(stat -c '%a' "$config_dir")"
44
+ file_mode="$(stat -c '%a' "$config_file")"
45
+ current_uid="$(id -u)"
46
+ if (( (8#$dir_mode & 077) != 0 || (8#$file_mode & 077) != 0 )) ||
47
+ [[ "$(stat -c '%u' "$config_dir")" != "$current_uid" || "$(stat -c '%u' "$config_file")" != "$current_uid" ]]; then
48
+ echo "Docker credentials must be owned by the deployment account and deny group/other access." >&2
49
+ exit 43
50
+ fi
51
+ export DOCKER_CONFIG="$config_dir"
52
+ credential_state="protected"
53
+ fi
54
+
55
+ if ! docker manifest inspect "$IMAGE" </dev/null >/dev/null 2>&1; then
56
+ echo "Registry manifest access failed for the exact image." >&2
57
+ exit 44
58
+ fi
59
+
60
+ printf 'ACCESS\t%s\n' "$credential_state"
61
+ '''
62
+
63
+
64
+ def normalize_registry_image(value: str) -> str:
65
+ if not isinstance(value, str) or _IMAGE_RE.fullmatch(value) is None:
66
+ raise DeploymentKitError(
67
+ "image must be an immutable image reference ending in @sha256:<64 lowercase hex>."
68
+ )
69
+ return value
70
+
71
+
72
+ def build_remote_script() -> str:
73
+ return _REMOTE_SCRIPT
74
+
75
+
76
+ def _parse_access(output: str) -> str:
77
+ lines = [line for line in output.splitlines() if line]
78
+ if len(lines) != 1:
79
+ raise DeploymentKitError("Malformed registry access verification output.")
80
+ parts = lines[0].split("\t")
81
+ if len(parts) != 2 or parts[0] != "ACCESS" or parts[1] not in {"absent", "protected"}:
82
+ raise DeploymentKitError("Malformed registry access verification output.")
83
+ return parts[1]
84
+
85
+
86
+ def verify_registry_access(name: str, image: str) -> dict[str, Any]:
87
+ exact_image = normalize_registry_image(image)
88
+ profile = load_profile(name)
89
+ access = verify_access(profile)
90
+ command = ssh_args(
91
+ profile,
92
+ access["address"],
93
+ command="bash -s -- " + shlex.quote(exact_image),
94
+ )
95
+ completed = subprocess.run(
96
+ command,
97
+ input=build_remote_script().encode("utf-8"),
98
+ check=False,
99
+ capture_output=True,
100
+ )
101
+ stdout = completed.stdout.decode("utf-8", errors="replace")
102
+ stderr = completed.stderr.decode("utf-8", errors="replace")
103
+ if completed.returncode != 0:
104
+ detail = stderr.strip() or stdout.strip() or f"exit {completed.returncode}"
105
+ raise DeploymentKitError(f"Read-only registry access verification failed: {detail}")
106
+
107
+ credential_state = _parse_access(stdout)
108
+ return {
109
+ "targetName": name,
110
+ "image": exact_image,
111
+ "manifestAccess": "verified",
112
+ "credentialConfig": credential_state,
113
+ "mutated": False,
114
+ }
@@ -0,0 +1,51 @@
1
+ """CLI for read-only registry access verification."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import json
7
+ import sys
8
+ from typing import Any
9
+
10
+ from .errors import DeploymentKitError
11
+ from .registry_access import verify_registry_access
12
+
13
+
14
+ def _print(value: Any, *, as_json: bool) -> None:
15
+ if as_json:
16
+ print(json.dumps(value, indent=2, sort_keys=True))
17
+ return
18
+ if isinstance(value, dict):
19
+ for key, item in value.items():
20
+ print(f"{key}: {item}")
21
+ return
22
+ print(value)
23
+
24
+
25
+ def build_parser() -> argparse.ArgumentParser:
26
+ parser = argparse.ArgumentParser(prog="lean-poc-deploy registry")
27
+ sub = parser.add_subparsers(dest="command", required=True)
28
+
29
+ verify = sub.add_parser(
30
+ "verify",
31
+ help="Verify exact-image registry manifest access without pulling or modifying target state",
32
+ )
33
+ verify.add_argument("name")
34
+ verify.add_argument("--image", required=True)
35
+ verify.add_argument("--json", action="store_true")
36
+ return parser
37
+
38
+
39
+ def main(argv: list[str] | None = None) -> int:
40
+ args = build_parser().parse_args(argv)
41
+ try:
42
+ if args.command == "verify":
43
+ _print(
44
+ verify_registry_access(args.name, args.image),
45
+ as_json=args.json,
46
+ )
47
+ return 0
48
+ except DeploymentKitError as error:
49
+ print(f"ERROR: {error}", file=sys.stderr)
50
+ return 2
51
+ return 2
@@ -108,6 +108,7 @@ def verify_access(profile: dict[str, Any]) -> dict[str, str]:
108
108
  text=True,
109
109
  encoding="utf-8",
110
110
  errors="replace",
111
+ stdin=subprocess.DEVNULL,
111
112
  )
112
113
  if result.returncode == 0:
113
114
  return {"targetName": profile["targetName"], "address": address, "status": "verified"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mriekki/lean",
3
- "version": "0.4.0",
3
+ "version": "0.4.2",
4
4
  "description": "Public consumer distribution and updater for Lean engineering assets.",
5
5
  "type": "module",
6
6
  "bin": {