@mriekki/lean 0.1.0 → 0.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.
Files changed (41) hide show
  1. package/README.md +71 -67
  2. package/assets/components.json +33 -33
  3. package/assets/core/distribution.json +52 -51
  4. package/assets/deployment/distribution.json +34 -34
  5. package/assets/generated/ASSEMBLY.json +38 -33
  6. package/assets/generated/core/core/skills/core-sync/SKILL.md +78 -120
  7. package/assets/generated/core/core/skills/deploy/SKILL.md +74 -91
  8. package/assets/generated/core/core/skills/lean-update/SKILL.md +189 -0
  9. package/assets/generated/core/core/skills/upstream-fix/SKILL.md +91 -76
  10. package/assets/generated/deployment/distribution/lean-poc-deploy.py +8 -8
  11. package/assets/generated/deployment/src/lean_poc_deployment/__init__.py +3 -3
  12. package/assets/generated/deployment/src/lean_poc_deployment/__main__.py +3 -3
  13. package/assets/generated/deployment/src/lean_poc_deployment/app_cli.py +140 -140
  14. package/assets/generated/deployment/src/lean_poc_deployment/app_runtime.py +455 -455
  15. package/assets/generated/deployment/src/lean_poc_deployment/app_runtime_remote.py +415 -415
  16. package/assets/generated/deployment/src/lean_poc_deployment/box_platform.py +208 -208
  17. package/assets/generated/deployment/src/lean_poc_deployment/box_platform_assets.py +100 -100
  18. package/assets/generated/deployment/src/lean_poc_deployment/box_platform_remote.py +271 -271
  19. package/assets/generated/deployment/src/lean_poc_deployment/cli.py +437 -437
  20. package/assets/generated/deployment/src/lean_poc_deployment/doctor.py +121 -121
  21. package/assets/generated/deployment/src/lean_poc_deployment/entrypoint.py +25 -25
  22. package/assets/generated/deployment/src/lean_poc_deployment/errors.py +5 -5
  23. package/assets/generated/deployment/src/lean_poc_deployment/keys.py +171 -171
  24. package/assets/generated/deployment/src/lean_poc_deployment/profiles.py +204 -204
  25. package/assets/generated/deployment/src/lean_poc_deployment/route_registry.py +367 -367
  26. package/assets/generated/deployment/src/lean_poc_deployment/route_registry_remote.py +443 -443
  27. package/assets/generated/deployment/src/lean_poc_deployment/security.py +290 -290
  28. package/assets/generated/deployment/src/lean_poc_deployment/sqlite_adapter.py +334 -334
  29. package/assets/generated/deployment/src/lean_poc_deployment/sqlite_adapter_remote.py +339 -339
  30. package/assets/generated/deployment/src/lean_poc_deployment/sqlite_cli.py +85 -85
  31. package/assets/generated/deployment/src/lean_poc_deployment/sqlite_import_recovery.py +437 -437
  32. package/assets/generated/deployment/src/lean_poc_deployment/ssh.py +118 -118
  33. package/assets/generated/deployment/src/lean_poc_deployment/teardown.py +286 -286
  34. package/assets/generated/deployment/src/lean_poc_deployment/teardown_cli.py +120 -120
  35. package/assets/generated/deployment/src/lean_poc_deployment/teardown_remote.py +100 -100
  36. package/assets/generated/deployment/src/lean_poc_deployment/trust.py +278 -278
  37. package/bin/lean.mjs +9 -9
  38. package/package.json +33 -33
  39. package/src/cli.mjs +131 -131
  40. package/src/engine.mjs +243 -243
  41. package/src/package-data.mjs +18 -18
@@ -1,208 +1,208 @@
1
- """Shared Ubuntu box platform ownership and lifecycle operations."""
2
-
3
- from __future__ import annotations
4
-
5
- import base64
6
- import hashlib
7
- import json
8
- import shlex
9
- import subprocess
10
- from typing import Any
11
-
12
- from .box_platform_assets import (
13
- AVAHI_SERVICE,
14
- CADDYFILE,
15
- CADDY_CONTAINER,
16
- CADDY_IMAGE,
17
- COMPOSE_YAML,
18
- HOST_PLATFORM_WRITE_PHRASE,
19
- NETWORK_NAME,
20
- PLATFORM_DISTRIBUTION,
21
- PLATFORM_ROOT,
22
- PLATFORM_SCHEMA_VERSION,
23
- PLATFORM_VERSION,
24
- PUBLISH_ALIAS_SH,
25
- )
26
- from .box_platform_remote import REMOTE_SCRIPT
27
- from .errors import DeploymentKitError
28
- from .profiles import load_profile
29
- from .ssh import ssh_args, verify_access
30
-
31
- _BOOL_KEYS = {
32
- "markerPresent",
33
- "prerequisitesReady",
34
- "networkPresent",
35
- "caddyRunning",
36
- "avahiRunning",
37
- "port80InUse",
38
- }
39
- _EXPECTED_KEYS = {
40
- "state",
41
- "markerPresent",
42
- "markerSchemaVersion",
43
- "markerDistribution",
44
- "markerPlatformVersion",
45
- "expectedPlatformVersion",
46
- "prerequisitesReady",
47
- "networkPresent",
48
- "caddyRunning",
49
- "avahiRunning",
50
- "port80InUse",
51
- "platformRoot",
52
- "networkName",
53
- "caddyContainer",
54
- }
55
-
56
-
57
- def build_remote_script() -> str:
58
- replacements = {
59
- "__PLATFORM_ROOT__": PLATFORM_ROOT,
60
- "__NETWORK_NAME__": NETWORK_NAME,
61
- "__CADDY_CONTAINER__": CADDY_CONTAINER,
62
- "__CADDY_IMAGE__": CADDY_IMAGE,
63
- "__SCHEMA_VERSION__": str(PLATFORM_SCHEMA_VERSION),
64
- "__PLATFORM_VERSION__": PLATFORM_VERSION,
65
- "__DISTRIBUTION__": PLATFORM_DISTRIBUTION,
66
- "__COMPOSE_B64__": base64.b64encode(COMPOSE_YAML.encode()).decode("ascii"),
67
- "__CADDYFILE_B64__": base64.b64encode(CADDYFILE.encode()).decode("ascii"),
68
- "__PUBLISH_ALIAS_B64__": base64.b64encode(PUBLISH_ALIAS_SH.encode()).decode("ascii"),
69
- "__AVAHI_SERVICE_B64__": base64.b64encode(AVAHI_SERVICE.encode()).decode("ascii"),
70
- }
71
- script = REMOTE_SCRIPT
72
- for placeholder, value in replacements.items():
73
- script = script.replace(placeholder, value)
74
- if "__" in script:
75
- raise DeploymentKitError("Internal host-platform script contains an unresolved placeholder.")
76
- return script
77
-
78
-
79
- def _parse_status(output: str) -> dict[str, Any]:
80
- result: dict[str, Any] = {}
81
- for raw in output.splitlines():
82
- if not raw.strip():
83
- continue
84
- if "=" not in raw:
85
- raise DeploymentKitError(f"Malformed host-platform status line: {raw!r}")
86
- key, value = raw.split("=", 1)
87
- if key not in _EXPECTED_KEYS or key in result:
88
- raise DeploymentKitError(f"Unexpected or duplicate host-platform status key: {key!r}")
89
- if key in _BOOL_KEYS:
90
- if value not in {"true", "false"}:
91
- raise DeploymentKitError(f"Invalid boolean host-platform status value for {key}.")
92
- result[key] = value == "true"
93
- else:
94
- result[key] = value
95
- missing = _EXPECTED_KEYS - result.keys()
96
- if missing:
97
- raise DeploymentKitError(
98
- "Host-platform status omitted required keys: " + ", ".join(sorted(missing))
99
- )
100
- return result
101
-
102
-
103
- def _run_remote(profile: dict[str, Any], address: str, action: str, *, sudo: bool) -> str:
104
- if action not in {"status", "install", "upgrade"}:
105
- raise DeploymentKitError(f"Unsupported host-platform action: {action}")
106
- remote_command = "bash -s -- " + shlex.quote(action)
107
- if sudo:
108
- remote_command = "sudo -n " + remote_command
109
- command = ssh_args(profile, address, command=remote_command)
110
- completed = subprocess.run(
111
- command,
112
- input=build_remote_script().encode("utf-8"),
113
- check=False,
114
- capture_output=True,
115
- )
116
- stdout = completed.stdout.decode("utf-8", errors="replace")
117
- stderr = completed.stderr.decode("utf-8", errors="replace")
118
- if completed.returncode != 0:
119
- detail = stderr.strip() or stdout.strip() or f"exit {completed.returncode}"
120
- raise DeploymentKitError(f"Remote host-platform {action} failed: {detail}")
121
- return stdout
122
-
123
-
124
- def platform_status(name: str) -> dict[str, Any]:
125
- profile = load_profile(name)
126
- evidence = verify_access(profile)
127
- status = _parse_status(_run_remote(profile, evidence["address"], "status", sudo=True))
128
- status["targetName"] = name
129
- status["verifiedAddress"] = evidence["address"]
130
- return status
131
-
132
-
133
- def platform_doctor(name: str) -> dict[str, Any]:
134
- status = platform_status(name)
135
- next_actions = {
136
- "installable": "platform-install",
137
- "upgrade-required": "platform-upgrade",
138
- "drifted": "inspect-platform-drift",
139
- "blocked": "prepare-host-prerequisites",
140
- "blocked-port-80": "identify-existing-port-80-owner",
141
- "incompatible": "manual-platform-recovery",
142
- "ready": "none",
143
- }
144
- status["nextAction"] = next_actions.get(status["state"], "manual-review")
145
- status["mutated"] = False
146
- return status
147
-
148
-
149
- def _snapshot(status: dict[str, Any]) -> str:
150
- selected = {key: status[key] for key in sorted(_EXPECTED_KEYS)}
151
- return hashlib.sha256(json.dumps(selected, sort_keys=True, separators=(",", ":")).encode()).hexdigest()
152
-
153
-
154
- def plan_platform_change(name: str, action: str) -> dict[str, Any]:
155
- if action not in {"install", "upgrade"}:
156
- raise DeploymentKitError("Platform change action must be install or upgrade.")
157
- status = platform_status(name)
158
- expected_state = "installable" if action == "install" else "upgrade-required"
159
- if status["state"] != expected_state:
160
- raise DeploymentKitError(
161
- f"Platform {action} requires state '{expected_state}', but target '{name}' is '{status['state']}'."
162
- )
163
- return {
164
- "targetName": name,
165
- "action": action,
166
- "fromState": status["state"],
167
- "toPlatformVersion": PLATFORM_VERSION,
168
- "distribution": PLATFORM_DISTRIBUTION,
169
- "platformRoot": PLATFORM_ROOT,
170
- "preserves": ["route-conf.d", "route-registry", "caddy-data", "caddy-config", "shared-network"],
171
- "doesNotOwn": ["application-containers", "application-data", "application-routes"],
172
- "statusSnapshot": _snapshot(status),
173
- }
174
-
175
-
176
- def apply_platform_change(plan: dict[str, Any]) -> dict[str, Any]:
177
- required = {
178
- "targetName",
179
- "action",
180
- "fromState",
181
- "toPlatformVersion",
182
- "distribution",
183
- "platformRoot",
184
- "preserves",
185
- "doesNotOwn",
186
- "statusSnapshot",
187
- }
188
- if not isinstance(plan, dict) or set(plan) != required:
189
- raise DeploymentKitError("Malformed host-platform change plan.")
190
- name = plan["targetName"]
191
- action = plan["action"]
192
- if action not in {"install", "upgrade"}:
193
- raise DeploymentKitError("Malformed host-platform change action.")
194
- current = platform_status(name)
195
- if _snapshot(current) != plan["statusSnapshot"]:
196
- raise DeploymentKitError(
197
- "Host-platform state changed after the plan was reviewed; create and authorize a fresh plan."
198
- )
199
- profile = load_profile(name)
200
- evidence = verify_access(profile)
201
- _run_remote(profile, evidence["address"], action, sudo=True)
202
- final = platform_status(name)
203
- if final["state"] != "ready":
204
- raise DeploymentKitError(
205
- f"Host-platform {action} completed but readiness is '{final['state']}', not 'ready'."
206
- )
207
- final["mutated"] = True
208
- return final
1
+ """Shared Ubuntu box platform ownership and lifecycle operations."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import base64
6
+ import hashlib
7
+ import json
8
+ import shlex
9
+ import subprocess
10
+ from typing import Any
11
+
12
+ from .box_platform_assets import (
13
+ AVAHI_SERVICE,
14
+ CADDYFILE,
15
+ CADDY_CONTAINER,
16
+ CADDY_IMAGE,
17
+ COMPOSE_YAML,
18
+ HOST_PLATFORM_WRITE_PHRASE,
19
+ NETWORK_NAME,
20
+ PLATFORM_DISTRIBUTION,
21
+ PLATFORM_ROOT,
22
+ PLATFORM_SCHEMA_VERSION,
23
+ PLATFORM_VERSION,
24
+ PUBLISH_ALIAS_SH,
25
+ )
26
+ from .box_platform_remote import REMOTE_SCRIPT
27
+ from .errors import DeploymentKitError
28
+ from .profiles import load_profile
29
+ from .ssh import ssh_args, verify_access
30
+
31
+ _BOOL_KEYS = {
32
+ "markerPresent",
33
+ "prerequisitesReady",
34
+ "networkPresent",
35
+ "caddyRunning",
36
+ "avahiRunning",
37
+ "port80InUse",
38
+ }
39
+ _EXPECTED_KEYS = {
40
+ "state",
41
+ "markerPresent",
42
+ "markerSchemaVersion",
43
+ "markerDistribution",
44
+ "markerPlatformVersion",
45
+ "expectedPlatformVersion",
46
+ "prerequisitesReady",
47
+ "networkPresent",
48
+ "caddyRunning",
49
+ "avahiRunning",
50
+ "port80InUse",
51
+ "platformRoot",
52
+ "networkName",
53
+ "caddyContainer",
54
+ }
55
+
56
+
57
+ def build_remote_script() -> str:
58
+ replacements = {
59
+ "__PLATFORM_ROOT__": PLATFORM_ROOT,
60
+ "__NETWORK_NAME__": NETWORK_NAME,
61
+ "__CADDY_CONTAINER__": CADDY_CONTAINER,
62
+ "__CADDY_IMAGE__": CADDY_IMAGE,
63
+ "__SCHEMA_VERSION__": str(PLATFORM_SCHEMA_VERSION),
64
+ "__PLATFORM_VERSION__": PLATFORM_VERSION,
65
+ "__DISTRIBUTION__": PLATFORM_DISTRIBUTION,
66
+ "__COMPOSE_B64__": base64.b64encode(COMPOSE_YAML.encode()).decode("ascii"),
67
+ "__CADDYFILE_B64__": base64.b64encode(CADDYFILE.encode()).decode("ascii"),
68
+ "__PUBLISH_ALIAS_B64__": base64.b64encode(PUBLISH_ALIAS_SH.encode()).decode("ascii"),
69
+ "__AVAHI_SERVICE_B64__": base64.b64encode(AVAHI_SERVICE.encode()).decode("ascii"),
70
+ }
71
+ script = REMOTE_SCRIPT
72
+ for placeholder, value in replacements.items():
73
+ script = script.replace(placeholder, value)
74
+ if "__" in script:
75
+ raise DeploymentKitError("Internal host-platform script contains an unresolved placeholder.")
76
+ return script
77
+
78
+
79
+ def _parse_status(output: str) -> dict[str, Any]:
80
+ result: dict[str, Any] = {}
81
+ for raw in output.splitlines():
82
+ if not raw.strip():
83
+ continue
84
+ if "=" not in raw:
85
+ raise DeploymentKitError(f"Malformed host-platform status line: {raw!r}")
86
+ key, value = raw.split("=", 1)
87
+ if key not in _EXPECTED_KEYS or key in result:
88
+ raise DeploymentKitError(f"Unexpected or duplicate host-platform status key: {key!r}")
89
+ if key in _BOOL_KEYS:
90
+ if value not in {"true", "false"}:
91
+ raise DeploymentKitError(f"Invalid boolean host-platform status value for {key}.")
92
+ result[key] = value == "true"
93
+ else:
94
+ result[key] = value
95
+ missing = _EXPECTED_KEYS - result.keys()
96
+ if missing:
97
+ raise DeploymentKitError(
98
+ "Host-platform status omitted required keys: " + ", ".join(sorted(missing))
99
+ )
100
+ return result
101
+
102
+
103
+ def _run_remote(profile: dict[str, Any], address: str, action: str, *, sudo: bool) -> str:
104
+ if action not in {"status", "install", "upgrade"}:
105
+ raise DeploymentKitError(f"Unsupported host-platform action: {action}")
106
+ remote_command = "bash -s -- " + shlex.quote(action)
107
+ if sudo:
108
+ remote_command = "sudo -n " + remote_command
109
+ command = ssh_args(profile, address, command=remote_command)
110
+ completed = subprocess.run(
111
+ command,
112
+ input=build_remote_script().encode("utf-8"),
113
+ check=False,
114
+ capture_output=True,
115
+ )
116
+ stdout = completed.stdout.decode("utf-8", errors="replace")
117
+ stderr = completed.stderr.decode("utf-8", errors="replace")
118
+ if completed.returncode != 0:
119
+ detail = stderr.strip() or stdout.strip() or f"exit {completed.returncode}"
120
+ raise DeploymentKitError(f"Remote host-platform {action} failed: {detail}")
121
+ return stdout
122
+
123
+
124
+ def platform_status(name: str) -> dict[str, Any]:
125
+ profile = load_profile(name)
126
+ evidence = verify_access(profile)
127
+ status = _parse_status(_run_remote(profile, evidence["address"], "status", sudo=True))
128
+ status["targetName"] = name
129
+ status["verifiedAddress"] = evidence["address"]
130
+ return status
131
+
132
+
133
+ def platform_doctor(name: str) -> dict[str, Any]:
134
+ status = platform_status(name)
135
+ next_actions = {
136
+ "installable": "platform-install",
137
+ "upgrade-required": "platform-upgrade",
138
+ "drifted": "inspect-platform-drift",
139
+ "blocked": "prepare-host-prerequisites",
140
+ "blocked-port-80": "identify-existing-port-80-owner",
141
+ "incompatible": "manual-platform-recovery",
142
+ "ready": "none",
143
+ }
144
+ status["nextAction"] = next_actions.get(status["state"], "manual-review")
145
+ status["mutated"] = False
146
+ return status
147
+
148
+
149
+ def _snapshot(status: dict[str, Any]) -> str:
150
+ selected = {key: status[key] for key in sorted(_EXPECTED_KEYS)}
151
+ return hashlib.sha256(json.dumps(selected, sort_keys=True, separators=(",", ":")).encode()).hexdigest()
152
+
153
+
154
+ def plan_platform_change(name: str, action: str) -> dict[str, Any]:
155
+ if action not in {"install", "upgrade"}:
156
+ raise DeploymentKitError("Platform change action must be install or upgrade.")
157
+ status = platform_status(name)
158
+ expected_state = "installable" if action == "install" else "upgrade-required"
159
+ if status["state"] != expected_state:
160
+ raise DeploymentKitError(
161
+ f"Platform {action} requires state '{expected_state}', but target '{name}' is '{status['state']}'."
162
+ )
163
+ return {
164
+ "targetName": name,
165
+ "action": action,
166
+ "fromState": status["state"],
167
+ "toPlatformVersion": PLATFORM_VERSION,
168
+ "distribution": PLATFORM_DISTRIBUTION,
169
+ "platformRoot": PLATFORM_ROOT,
170
+ "preserves": ["route-conf.d", "route-registry", "caddy-data", "caddy-config", "shared-network"],
171
+ "doesNotOwn": ["application-containers", "application-data", "application-routes"],
172
+ "statusSnapshot": _snapshot(status),
173
+ }
174
+
175
+
176
+ def apply_platform_change(plan: dict[str, Any]) -> dict[str, Any]:
177
+ required = {
178
+ "targetName",
179
+ "action",
180
+ "fromState",
181
+ "toPlatformVersion",
182
+ "distribution",
183
+ "platformRoot",
184
+ "preserves",
185
+ "doesNotOwn",
186
+ "statusSnapshot",
187
+ }
188
+ if not isinstance(plan, dict) or set(plan) != required:
189
+ raise DeploymentKitError("Malformed host-platform change plan.")
190
+ name = plan["targetName"]
191
+ action = plan["action"]
192
+ if action not in {"install", "upgrade"}:
193
+ raise DeploymentKitError("Malformed host-platform change action.")
194
+ current = platform_status(name)
195
+ if _snapshot(current) != plan["statusSnapshot"]:
196
+ raise DeploymentKitError(
197
+ "Host-platform state changed after the plan was reviewed; create and authorize a fresh plan."
198
+ )
199
+ profile = load_profile(name)
200
+ evidence = verify_access(profile)
201
+ _run_remote(profile, evidence["address"], action, sudo=True)
202
+ final = platform_status(name)
203
+ if final["state"] != "ready":
204
+ raise DeploymentKitError(
205
+ f"Host-platform {action} completed but readiness is '{final['state']}', not 'ready'."
206
+ )
207
+ final["mutated"] = True
208
+ return final
@@ -1,100 +1,100 @@
1
- """Static shared-box platform assets and contract constants."""
2
-
3
- PLATFORM_SCHEMA_VERSION = 1
4
- PLATFORM_VERSION = "2"
5
- PLATFORM_DISTRIBUTION = "trusted-lan-ubuntu-compose"
6
- PLATFORM_ROOT = "/srv/lean-poc-platform"
7
- NETWORK_NAME = "caddy-net"
8
- CADDY_CONTAINER = "lean-poc-caddy"
9
- CADDY_IMAGE = "caddy:2.8.4-alpine@sha256:af32e97399febea808609119bb21544d0265c58a02836576e32a2d082c262c17"
10
- HOST_PLATFORM_WRITE_PHRASE = "[HOST PLATFORM WRITE]"
11
-
12
- COMPOSE_YAML = f"""services:
13
- caddy:
14
- image: {CADDY_IMAGE}
15
- container_name: {CADDY_CONTAINER}
16
- restart: unless-stopped
17
- ports:
18
- - \"80:80\"
19
- volumes:
20
- - {PLATFORM_ROOT}/proxy/Caddyfile:/etc/caddy/Caddyfile:ro
21
- - {PLATFORM_ROOT}/proxy/conf.d:/etc/caddy/conf.d:ro
22
- - {PLATFORM_ROOT}/proxy/data:/data
23
- - {PLATFORM_ROOT}/proxy/config:/config
24
- networks:
25
- - {NETWORK_NAME}
26
- logging:
27
- driver: json-file
28
- options:
29
- max-size: \"10m\"
30
- max-file: \"3\"
31
-
32
- networks:
33
- {NETWORK_NAME}:
34
- external: true
35
- """
36
-
37
- CADDYFILE = """{
38
- auto_https off
39
- }
40
-
41
- import /etc/caddy/conf.d/*.caddy
42
- """
43
-
44
- PUBLISH_ALIAS_SH = """#!/usr/bin/env bash
45
- set -Eeuo pipefail
46
-
47
- : \"${ALIAS_HOSTNAME:?ALIAS_HOSTNAME is required}\"
48
-
49
- current_ip() {
50
- local ip
51
- ip=\"$(ip route get 1.1.1.1 2>/dev/null | awk '{for(i=1;i<=NF;i++) if($i==\"src\") print $(i+1)}')\"
52
- if [[ -z \"$ip\" ]]; then
53
- ip=\"$(ip -4 addr show scope global | grep -v 'docker' | grep -v 'br-' | awk '/inet / {print $2}' | cut -d/ -f1 | head -n1)\"
54
- fi
55
- printf '%s\\n' \"$ip\"
56
- }
57
-
58
- CURRENT_IP=\"$(current_ip)\"
59
- if [[ -z \"$CURRENT_IP\" ]]; then
60
- echo \"lean-poc alias publisher: could not determine host LAN IPv4 address.\" >&2
61
- exit 1
62
- fi
63
-
64
- /usr/bin/avahi-publish -a -R \"$ALIAS_HOSTNAME\" \"$CURRENT_IP\" &
65
- AVAHI_PID=$!
66
-
67
- cleanup() {
68
- kill \"$AVAHI_PID\" 2>/dev/null || true
69
- wait \"$AVAHI_PID\" 2>/dev/null || true
70
- }
71
- trap cleanup SIGTERM SIGINT EXIT
72
-
73
- while kill -0 \"$AVAHI_PID\" 2>/dev/null; do
74
- sleep 10
75
- NEW_IP=\"$(current_ip)\"
76
- if [[ -n \"$NEW_IP\" && \"$NEW_IP\" != \"$CURRENT_IP\" ]]; then
77
- cleanup
78
- exit 0
79
- fi
80
- done
81
- wait \"$AVAHI_PID\"
82
- """
83
-
84
- AVAHI_SERVICE = """[Unit]
85
- Description=Lean POC mDNS alias %i
86
- After=network-online.target avahi-daemon.service
87
- Wants=network-online.target
88
- Requires=avahi-daemon.service
89
-
90
- [Service]
91
- Type=simple
92
- EnvironmentFile=/etc/lean-poc-platform/aliases/%i.env
93
- ExecStart=/usr/local/lib/lean-poc-platform/publish-alias.sh
94
- Restart=always
95
- RestartSec=2
96
- NoNewPrivileges=true
97
-
98
- [Install]
99
- WantedBy=multi-user.target
100
- """
1
+ """Static shared-box platform assets and contract constants."""
2
+
3
+ PLATFORM_SCHEMA_VERSION = 1
4
+ PLATFORM_VERSION = "2"
5
+ PLATFORM_DISTRIBUTION = "trusted-lan-ubuntu-compose"
6
+ PLATFORM_ROOT = "/srv/lean-poc-platform"
7
+ NETWORK_NAME = "caddy-net"
8
+ CADDY_CONTAINER = "lean-poc-caddy"
9
+ CADDY_IMAGE = "caddy:2.8.4-alpine@sha256:af32e97399febea808609119bb21544d0265c58a02836576e32a2d082c262c17"
10
+ HOST_PLATFORM_WRITE_PHRASE = "[HOST PLATFORM WRITE]"
11
+
12
+ COMPOSE_YAML = f"""services:
13
+ caddy:
14
+ image: {CADDY_IMAGE}
15
+ container_name: {CADDY_CONTAINER}
16
+ restart: unless-stopped
17
+ ports:
18
+ - \"80:80\"
19
+ volumes:
20
+ - {PLATFORM_ROOT}/proxy/Caddyfile:/etc/caddy/Caddyfile:ro
21
+ - {PLATFORM_ROOT}/proxy/conf.d:/etc/caddy/conf.d:ro
22
+ - {PLATFORM_ROOT}/proxy/data:/data
23
+ - {PLATFORM_ROOT}/proxy/config:/config
24
+ networks:
25
+ - {NETWORK_NAME}
26
+ logging:
27
+ driver: json-file
28
+ options:
29
+ max-size: \"10m\"
30
+ max-file: \"3\"
31
+
32
+ networks:
33
+ {NETWORK_NAME}:
34
+ external: true
35
+ """
36
+
37
+ CADDYFILE = """{
38
+ auto_https off
39
+ }
40
+
41
+ import /etc/caddy/conf.d/*.caddy
42
+ """
43
+
44
+ PUBLISH_ALIAS_SH = """#!/usr/bin/env bash
45
+ set -Eeuo pipefail
46
+
47
+ : \"${ALIAS_HOSTNAME:?ALIAS_HOSTNAME is required}\"
48
+
49
+ current_ip() {
50
+ local ip
51
+ ip=\"$(ip route get 1.1.1.1 2>/dev/null | awk '{for(i=1;i<=NF;i++) if($i==\"src\") print $(i+1)}')\"
52
+ if [[ -z \"$ip\" ]]; then
53
+ ip=\"$(ip -4 addr show scope global | grep -v 'docker' | grep -v 'br-' | awk '/inet / {print $2}' | cut -d/ -f1 | head -n1)\"
54
+ fi
55
+ printf '%s\\n' \"$ip\"
56
+ }
57
+
58
+ CURRENT_IP=\"$(current_ip)\"
59
+ if [[ -z \"$CURRENT_IP\" ]]; then
60
+ echo \"lean-poc alias publisher: could not determine host LAN IPv4 address.\" >&2
61
+ exit 1
62
+ fi
63
+
64
+ /usr/bin/avahi-publish -a -R \"$ALIAS_HOSTNAME\" \"$CURRENT_IP\" &
65
+ AVAHI_PID=$!
66
+
67
+ cleanup() {
68
+ kill \"$AVAHI_PID\" 2>/dev/null || true
69
+ wait \"$AVAHI_PID\" 2>/dev/null || true
70
+ }
71
+ trap cleanup SIGTERM SIGINT EXIT
72
+
73
+ while kill -0 \"$AVAHI_PID\" 2>/dev/null; do
74
+ sleep 10
75
+ NEW_IP=\"$(current_ip)\"
76
+ if [[ -n \"$NEW_IP\" && \"$NEW_IP\" != \"$CURRENT_IP\" ]]; then
77
+ cleanup
78
+ exit 0
79
+ fi
80
+ done
81
+ wait \"$AVAHI_PID\"
82
+ """
83
+
84
+ AVAHI_SERVICE = """[Unit]
85
+ Description=Lean POC mDNS alias %i
86
+ After=network-online.target avahi-daemon.service
87
+ Wants=network-online.target
88
+ Requires=avahi-daemon.service
89
+
90
+ [Service]
91
+ Type=simple
92
+ EnvironmentFile=/etc/lean-poc-platform/aliases/%i.env
93
+ ExecStart=/usr/local/lib/lean-poc-platform/publish-alias.sh
94
+ Restart=always
95
+ RestartSec=2
96
+ NoNewPrivileges=true
97
+
98
+ [Install]
99
+ WantedBy=multi-user.target
100
+ """