@crvouga/dev-fleet 0.0.0-stage → 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.
- package/.mise/locks/aws-cdk/2.1146.1/aube-lock.yaml +24 -0
- package/.mise/locks/aws-cdk/2.1146.1/package.json +7 -0
- package/.mise/locks/azure-cli/2.91.0~a837e014/pyproject.toml +5 -0
- package/.mise/locks/azure-cli/2.91.0~a837e014/uv.lock +2794 -0
- package/.mise/locks/gemini/0.66.0~84aac8e6/aube-lock.yaml +123 -0
- package/.mise/locks/gemini/0.66.0~84aac8e6/package.json +12 -0
- package/.mise/locks/npm-devcontainers-cli/0.90.0/aube-lock.yaml +24 -0
- package/.mise/locks/npm-devcontainers-cli/0.90.0/package.json +7 -0
- package/.mise/locks/npm-modelcontextprotocol-inspector/2.13.0~4c109bea/aube-lock.yaml +1432 -0
- package/.mise/locks/npm-modelcontextprotocol-inspector/2.13.0~4c109bea/package.json +7 -0
- package/.mise/locks/npm-vercel/65.8.0/aube-lock.yaml +3595 -0
- package/.mise/locks/npm-vercel/65.8.0/package.json +7 -0
- package/.mise/locks/npm-wrangler/4.156.0/aube-lock.yaml +895 -0
- package/.mise/locks/npm-wrangler/4.156.0/package.json +7 -0
- package/.mise/locks/npm-yarnpkg-cli-dist/4.18.1/aube-lock.yaml +24 -0
- package/.mise/locks/npm-yarnpkg-cli-dist/4.18.1/package.json +7 -0
- package/LICENSE +21 -0
- package/README.md +357 -2
- package/agents/antigravity-proxy.ts +3 -0
- package/agents/audit.md +89 -0
- package/agents/claude-proxy +5 -0
- package/agents/claude-proxy.ts +3 -0
- package/agents/codex-proxy.ts +3 -0
- package/agents/grok-proxy.ts +3 -0
- package/agents/managed-proxy.ts +55 -0
- package/agents/opencode-proxy.ts +3 -0
- package/agents/registry.json +209 -0
- package/agents/registry.schema.json +149 -0
- package/agents/setup.md +158 -0
- package/agents/t3.json +40 -0
- package/ansible/macos/manager.yml +30 -0
- package/ansible/macos/policy.json +9 -0
- package/ansible/monitoring/README.md +215 -0
- package/ansible/monitoring/agent-darwin.yml +198 -0
- package/ansible/monitoring/agent.yml +172 -0
- package/ansible/monitoring/beszel-agent.service +42 -0
- package/ansible/monitoring/compose.yml +74 -0
- package/ansible/monitoring/hub-darwin.yml +102 -0
- package/ansible/monitoring/hub-route-darwin.yml +51 -0
- package/ansible/monitoring/lock.json +16 -0
- package/ansible/net/remote-probe.sh +13 -0
- package/ansible/pyproject.toml +12 -0
- package/ansible/ubuntu/authorize-sudo.sh +51 -0
- package/ansible/ubuntu/baseline.yml +89 -0
- package/ansible/ubuntu/disk-gc.py +336 -0
- package/ansible/ubuntu/disk-guard.py +104 -0
- package/ansible/ubuntu/policy.json +52 -0
- package/ansible/ubuntu/recovery.yml +15 -0
- package/ansible/ubuntu/t3/policy.json +7 -0
- package/ansible/ubuntu/t3-desktop.yml +220 -0
- package/ansible/ubuntu/t3-health.sh +33 -0
- package/ansible/ubuntu/t3-policy.yml +76 -0
- package/ansible/ubuntu/t3-route.yml +28 -0
- package/ansible/ubuntu/tailscale.yml +78 -0
- package/ansible/ubuntu/tasks/credentials.yml +47 -0
- package/ansible/ubuntu/tasks/docker.yml +31 -0
- package/ansible/ubuntu/tasks/packages.yml +56 -0
- package/ansible/ubuntu/tasks/power.yml +51 -0
- package/ansible/ubuntu/tasks/recovery.yml +39 -0
- package/ansible/ubuntu/tasks/resources.yml +312 -0
- package/ansible/ubuntu/tasks/services.yml +31 -0
- package/ansible/ubuntu/tasks/ssh.yml +71 -0
- package/ansible/ubuntu/tasks/user.yml +120 -0
- package/ansible/uv.lock +190 -0
- package/bin/devfleet.mjs +36 -0
- package/bootstrap/bootstrap.ps1 +50 -0
- package/bootstrap/bootstrap.sh +80 -0
- package/bootstrap/devfleet +7 -0
- package/bootstrap/docker-desktop.sh +33 -0
- package/bootstrap/doctor.ts +234 -0
- package/bootstrap/fleet-manager-run.sh +103 -0
- package/bootstrap/fleet-remote-bootstrap.sh +258 -0
- package/bootstrap/global-config.ts +171 -0
- package/bootstrap/install-launcher.sh +10 -0
- package/bootstrap/native-aws.ts +86 -0
- package/bootstrap/native-t3.ts +154 -0
- package/bootstrap/npm.ts +358 -0
- package/bootstrap/release-version.ts +157 -0
- package/bootstrap/setup-status.ts +51 -0
- package/bootstrap/setup.ts +88 -0
- package/bootstrap/shell-path.ts +105 -0
- package/bootstrap/verify-t3-archive.py +49 -0
- package/bootstrap/worker-auth.ts +65 -0
- package/bootstrap/worker-github.ts +47 -0
- package/bootstrap/worker-tools.ts +24 -0
- package/cliproxy/AGENTS.md +11 -0
- package/cliproxy/README.md +269 -0
- package/cliproxy/clients/bootstrap.py +143 -0
- package/cliproxy/clients/launcher.py +57 -0
- package/cliproxy/compose.yaml +90 -0
- package/cliproxy/config.template.yaml +70 -0
- package/cliproxy/docs/cloud-adoption.md +16 -0
- package/cliproxy/docs/deployment.png +0 -0
- package/cliproxy/docs/deployment.svg +109 -0
- package/cliproxy/docs/health-workflow.example.yml +185 -0
- package/cliproxy/docs/migration.md +131 -0
- package/cliproxy/docs/model-health-report.schema.json +364 -0
- package/cliproxy/docs/model-health.md +393 -0
- package/cliproxy/image.lock.json +5 -0
- package/cliproxy/launchd/dev.devfleet.cliproxy-backup-check.plist +38 -0
- package/cliproxy/launchd/dev.devfleet.cliproxy-backup.plist +48 -0
- package/cliproxy/launchd/dev.devfleet.cliproxy-fleet.plist +32 -0
- package/cliproxy/launchd/dev.devfleet.cliproxy-gc.plist +38 -0
- package/cliproxy/launchd/dev.devfleet.cliproxy-monitor.plist +29 -0
- package/cliproxy/launchd/dev.devfleet.cliproxy-vault-renew.plist +31 -0
- package/cliproxy/models.json +1618 -0
- package/cliproxy/requirements.lock +78 -0
- package/cliproxy/scripts/accounts.py +38 -0
- package/cliproxy/scripts/audit.py +123 -0
- package/cliproxy/scripts/backup +234 -0
- package/cliproxy/scripts/check.py +34 -0
- package/cliproxy/scripts/checkpoint.py +226 -0
- package/cliproxy/scripts/cli.py +64 -0
- package/cliproxy/scripts/cliproxy_host.py +257 -0
- package/cliproxy/scripts/cloud_state.py +150 -0
- package/cliproxy/scripts/container-health +6 -0
- package/cliproxy/scripts/docker +24 -0
- package/cliproxy/scripts/external-health.py +79 -0
- package/cliproxy/scripts/fleet_config.py +43 -0
- package/cliproxy/scripts/fleet_sync.py +274 -0
- package/cliproxy/scripts/initialize +84 -0
- package/cliproxy/scripts/install-dependencies +48 -0
- package/cliproxy/scripts/install.py +406 -0
- package/cliproxy/scripts/model_health.py +16 -0
- package/cliproxy/scripts/modelhealth/__init__.py +5 -0
- package/cliproxy/scripts/modelhealth/checks.py +429 -0
- package/cliproxy/scripts/modelhealth/cli.py +117 -0
- package/cliproxy/scripts/modelhealth/config.py +186 -0
- package/cliproxy/scripts/modelhealth/emulator.py +536 -0
- package/cliproxy/scripts/modelhealth/expect.py +88 -0
- package/cliproxy/scripts/modelhealth/findings.py +183 -0
- package/cliproxy/scripts/modelhealth/protocol.py +330 -0
- package/cliproxy/scripts/modelhealth/redact.py +28 -0
- package/cliproxy/scripts/modelhealth/report.py +150 -0
- package/cliproxy/scripts/modelhealth/runner.py +390 -0
- package/cliproxy/scripts/modelhealth/scenario.py +131 -0
- package/cliproxy/scripts/modelhealth/schema.py +63 -0
- package/cliproxy/scripts/modelhealth/simulate.py +111 -0
- package/cliproxy/scripts/modelhealth/transport.py +213 -0
- package/cliproxy/scripts/monitor +68 -0
- package/cliproxy/scripts/ops +527 -0
- package/cliproxy/scripts/public-endpoint +127 -0
- package/cliproxy/scripts/recover_node.py +177 -0
- package/cliproxy/scripts/restore-verify +60 -0
- package/cliproxy/scripts/retention.py +144 -0
- package/cliproxy/scripts/service-start +7 -0
- package/cliproxy/scripts/validate-config +56 -0
- package/cliproxy/scripts/vault-access.py +57 -0
- package/cliproxy/scripts/vault-backup-login +55 -0
- package/cliproxy/scripts/vault-recovery +56 -0
- package/cliproxy/scripts/vault-renew +30 -0
- package/cliproxy/systemd/cliproxy-backup-check.service +10 -0
- package/cliproxy/systemd/cliproxy-backup-check.timer +8 -0
- package/cliproxy/systemd/cliproxy-backup.service +13 -0
- package/cliproxy/systemd/cliproxy-backup.timer +8 -0
- package/cliproxy/systemd/cliproxy-fleet.service +17 -0
- package/cliproxy/systemd/cliproxy-gc.service +13 -0
- package/cliproxy/systemd/cliproxy-gc.timer +8 -0
- package/cliproxy/systemd/cliproxy-monitor.service +8 -0
- package/cliproxy/systemd/cliproxy-monitor.timer +7 -0
- package/cliproxy/systemd/cliproxy-vault-renew.service +8 -0
- package/cliproxy/systemd/cliproxy-vault-renew.timer +7 -0
- package/cliproxy/vendor/LICENSE +22 -0
- package/cliproxy/vendor/config.upstream.yaml +1349 -0
- package/docs/consumer-setup.md +408 -0
- package/docs/declared.md +89 -0
- package/docs/machine-setup.md +399 -0
- package/docs/operations.md +276 -0
- package/docs/releasing.md +103 -0
- package/docs/state.md +254 -0
- package/docs/topology.md +67 -0
- package/docs/ubuntu-sync.md +351 -0
- package/docs/worker-contract.md +67 -0
- package/examples/fleet.json +100 -0
- package/examples/roster.json +13 -0
- package/llms.txt +35 -0
- package/mise.lock +1521 -0
- package/mise.toml +110 -0
- package/package.json +74 -4
- package/schema/fleet.schema.json +420 -0
- package/schema/roster.schema.json +77 -0
- package/src/agents/account-relay.ts +111 -0
- package/src/agents/accounts.ts +85 -0
- package/src/agents/audit.ts +77 -0
- package/src/agents/cli.ts +13 -0
- package/src/agents/command.ts +204 -0
- package/src/agents/converge.ts +123 -0
- package/src/agents/doctor.ts +107 -0
- package/src/agents/errors.ts +2 -0
- package/src/agents/files.ts +73 -0
- package/src/agents/fleet-verify.ts +165 -0
- package/src/agents/launch.ts +290 -0
- package/src/agents/native-verify.ts +371 -0
- package/src/agents/provider-errors.ts +57 -0
- package/src/agents/registry.ts +61 -0
- package/src/agents/render.ts +110 -0
- package/src/agents/round-trip.ts +192 -0
- package/src/agents/secrets.ts +13 -0
- package/src/agents/toolchain.ts +34 -0
- package/src/agents/verify-cache.ts +103 -0
- package/src/agents/verify.ts +127 -0
- package/src/auth-checks.ts +163 -0
- package/src/cli/agent-guide.ts +129 -0
- package/src/cli/app.ts +122 -0
- package/src/cli/cli.ts +28 -0
- package/src/cli/command-index.ts +57 -0
- package/src/cli/contracts.ts +25 -0
- package/src/cli/fleet-reference.ts +17 -0
- package/src/cli/fleet-state.ts +499 -0
- package/src/cli/fleet-sync-checkout.ts +130 -0
- package/src/cli/fleet-sync-enrollment.ts +140 -0
- package/src/cli/fleet-sync-forward.ts +134 -0
- package/src/cli/fleet-sync-manager.ts +249 -0
- package/src/cli/fleet-sync-monitoring.ts +185 -0
- package/src/cli/fleet-sync-output.ts +317 -0
- package/src/cli/fleet-sync-privilege.ts +27 -0
- package/src/cli/fleet-sync-progress.ts +109 -0
- package/src/cli/fleet-sync-remote.ts +242 -0
- package/src/cli/fleet-sync-report.ts +26 -0
- package/src/cli/fleet-sync-schedule.ts +138 -0
- package/src/cli/fleet-sync-state.ts +86 -0
- package/src/cli/fleet-sync-vault.ts +166 -0
- package/src/cli/fleet-sync.ts +375 -0
- package/src/cli/fleet.ts +165 -0
- package/src/cli/managed-health.ts +219 -0
- package/src/cli/node-deployment.ts +34 -0
- package/src/cli/node-sync.ts +121 -0
- package/src/cli/node-t3.ts +31 -0
- package/src/cli/options.ts +141 -0
- package/src/cli/probe-source.ts +7 -0
- package/src/cli/program.ts +185 -0
- package/src/cli/project.ts +51 -0
- package/src/cli/register-fleet.ts +296 -0
- package/src/cli/registry.ts +185 -0
- package/src/cli/repair-apply.ts +67 -0
- package/src/cli/repair-guard.ts +11 -0
- package/src/cli/repair-inspect.ts +150 -0
- package/src/cli/repair-script.ts +76 -0
- package/src/cli/repair.ts +209 -0
- package/src/cli/roster.ts +105 -0
- package/src/cli/search.ts +35 -0
- package/src/cli/settings.ts +67 -0
- package/src/cli/source-operations.ts +102 -0
- package/src/cli/t3/command.ts +90 -0
- package/src/cli/t3/register.ts +55 -0
- package/src/cli/tailnet.ts +127 -0
- package/src/cli/ui-actions.ts +133 -0
- package/src/cli/ui-controller.ts +113 -0
- package/src/cli/ui-data.ts +106 -0
- package/src/cli/ui-execute.ts +163 -0
- package/src/cli/ui-repairs.ts +34 -0
- package/src/cli/ui-reports.ts +211 -0
- package/src/cli/ui-targets.ts +100 -0
- package/src/cli/ui-terminal.ts +109 -0
- package/src/cli/ui-types.ts +49 -0
- package/src/cli/ui-wizard.ts +101 -0
- package/src/cli/ui.ts +9 -0
- package/src/cli/worker-health-output.ts +37 -0
- package/src/config.ts +186 -0
- package/src/configure.ts +255 -0
- package/src/definition.ts +290 -0
- package/src/diagnostics.ts +92 -0
- package/src/doctor-check.ts +10 -0
- package/src/errors.ts +1 -0
- package/src/fingerprint.ts +120 -0
- package/src/fleet-config.ts +174 -0
- package/src/input-hashes.ts +73 -0
- package/src/machine/local-source.ts +52 -0
- package/src/machine/lock-error.ts +2 -0
- package/src/machine/platform.ts +44 -0
- package/src/machine/source.ts +68 -0
- package/src/machine/state.ts +173 -0
- package/src/machine-health.ts +95 -0
- package/src/managed-source.ts +47 -0
- package/src/manager-failure.ts +48 -0
- package/src/manager-sync.ts +405 -0
- package/src/manager.ts +62 -0
- package/src/monitoring/agent-evidence.ts +146 -0
- package/src/monitoring/apply.ts +183 -0
- package/src/monitoring/beszel.ts +198 -0
- package/src/monitoring/enroll.ts +111 -0
- package/src/monitoring/hub-access.ts +114 -0
- package/src/monitoring/hub-backup.ts +121 -0
- package/src/monitoring/hub-viewer.ts +142 -0
- package/src/monitoring/hub.ts +93 -0
- package/src/monitoring/manager-agent.ts +73 -0
- package/src/monitoring/manager-hub.ts +248 -0
- package/src/monitoring/remote.ts +65 -0
- package/src/network-definition.ts +83 -0
- package/src/node-converge-linux.ts +374 -0
- package/src/node-converge-shared.ts +83 -0
- package/src/node-converge.ts +40 -0
- package/src/onboard.ts +35 -0
- package/src/package-info.ts +45 -0
- package/src/platform/ansible-receipt.ts +128 -0
- package/src/platform/ansible.ts +66 -0
- package/src/platform/darwin.ts +203 -0
- package/src/platform/detect.ts +11 -0
- package/src/platform/fallback.ts +110 -0
- package/src/platform/index.ts +38 -0
- package/src/platform/launchd.ts +98 -0
- package/src/platform/linux.ts +139 -0
- package/src/platform/ps.ts +45 -0
- package/src/platform/resource-verify.ts +132 -0
- package/src/platform/systemd-user.ts +95 -0
- package/src/platform/t3-policy.ts +137 -0
- package/src/platform/t3-route.ts +68 -0
- package/src/platform/t3-service.ts +75 -0
- package/src/platform/types.ts +123 -0
- package/src/platform/ubuntu-audit.ts +66 -0
- package/src/platform/ubuntu-verify.ts +105 -0
- package/src/platform/windows.ts +148 -0
- package/src/power-recovery.ts +55 -0
- package/src/probe.ts +51 -0
- package/src/protocol.ts +119 -0
- package/src/proxy-converge.ts +85 -0
- package/src/remote-bootstrap.ts +210 -0
- package/src/remote-entry.ts +89 -0
- package/src/remote-lock.ts +12 -0
- package/src/repair-catalog.ts +206 -0
- package/src/repair-plan.ts +78 -0
- package/src/repair-runner.ts +84 -0
- package/src/report.ts +71 -0
- package/src/runner.ts +104 -0
- package/src/secrets.ts +169 -0
- package/src/service-auth.ts +115 -0
- package/src/ssh.ts +135 -0
- package/src/state/bucket.ts +208 -0
- package/src/state/check.ts +74 -0
- package/src/state/connection.ts +92 -0
- package/src/state/enrollment.ts +45 -0
- package/src/state/legacy.ts +89 -0
- package/src/state/litestream.ts +143 -0
- package/src/state/operator.ts +20 -0
- package/src/state/paths.ts +40 -0
- package/src/state/roster.ts +62 -0
- package/src/state/store.ts +169 -0
- package/src/state/sync.ts +352 -0
- package/src/sync.ts +104 -0
- package/src/t3/accounts.ts +81 -0
- package/src/t3/desired.ts +165 -0
- package/src/t3/files.ts +81 -0
- package/src/t3/names-check.ts +53 -0
- package/src/t3/names.ts +155 -0
- package/src/t3/plan.ts +82 -0
- package/src/t3/providers.ts +74 -0
- package/src/t3/reconcile.ts +123 -0
- package/src/t3-endpoint.ts +104 -0
- package/src/t3-linked-check.ts +123 -0
- package/src/tailnet-health.ts +231 -0
- package/src/tailnet-matching.ts +37 -0
- package/src/tailscale.ts +141 -0
- package/src/toolchain-checks.ts +108 -0
- package/src/topology.ts +24 -0
- package/src/transport.ts +122 -0
- package/src/types.ts +99 -0
- package/src/vault-config.ts +38 -0
- package/src/worker-access-checks.ts +168 -0
- package/src/worker-capacity.ts +34 -0
- package/src/worker-host-checks.ts +181 -0
- package/src/worker-policy.ts +64 -0
- package/src/worker-readiness.ts +64 -0
- package/src/workstation-checks.ts +29 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
import sys
|
|
3
|
+
|
|
4
|
+
sys.dont_write_bytecode = True
|
|
5
|
+
|
|
6
|
+
import os, pathlib
|
|
7
|
+
|
|
8
|
+
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent))
|
|
9
|
+
import cliproxy_host
|
|
10
|
+
|
|
11
|
+
args = sys.argv[1:]
|
|
12
|
+
if args and args[0] == "compose" and "--project-directory" not in args:
|
|
13
|
+
args = [
|
|
14
|
+
"compose",
|
|
15
|
+
"--project-directory",
|
|
16
|
+
str(cliproxy_host.default_root()),
|
|
17
|
+
*args[1:],
|
|
18
|
+
]
|
|
19
|
+
try:
|
|
20
|
+
argv, environment = cliproxy_host.docker_invocation(args, __file__)
|
|
21
|
+
except cliproxy_host.DockerUnavailable as error:
|
|
22
|
+
print(str(error), file=sys.stderr)
|
|
23
|
+
sys.exit(cliproxy_host.DOCKER_UNAVAILABLE)
|
|
24
|
+
os.execvpe(argv[0], argv, environment)
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Off-machine, read-only checks using short-lived GitHub OIDC Vault access."""
|
|
3
|
+
|
|
4
|
+
import sys
|
|
5
|
+
|
|
6
|
+
sys.dont_write_bytecode = True
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import os
|
|
10
|
+
import pathlib
|
|
11
|
+
import time
|
|
12
|
+
import cloud_state
|
|
13
|
+
import fleet_config
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def endpoint_health(endpoint, key, request=cloud_state.request):
|
|
17
|
+
def get(path, authenticated=False):
|
|
18
|
+
return request(
|
|
19
|
+
endpoint + path,
|
|
20
|
+
{"Authorization": "Bearer " + key} if authenticated else None,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
if get("/v1/models")[0] != 401:
|
|
24
|
+
raise ValueError("Anonymous inference must be rejected")
|
|
25
|
+
status, body = get("/v1/models", True)
|
|
26
|
+
models = json.loads(body).get("data") if status == 200 else None
|
|
27
|
+
if not isinstance(models, list) or not models:
|
|
28
|
+
raise ValueError("Authenticated endpoint has no available models")
|
|
29
|
+
# Upstream counts missing/wrong management keys toward a per-IP ban.
|
|
30
|
+
# Scheduled health checks must never generate those failed login attempts.
|
|
31
|
+
if get("/management.html")[0] != 200:
|
|
32
|
+
raise ValueError("Dashboard unavailable")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def cloud_health(restic):
|
|
36
|
+
snapshot = cloud_state.latest(restic)
|
|
37
|
+
cloud_state.fresh(snapshot["time"], 45 * 60)
|
|
38
|
+
path = snapshot["paths"][0].rstrip("/") + "/state/status/gc-success.json"
|
|
39
|
+
gc = json.loads(restic("dump", snapshot["id"], path))
|
|
40
|
+
cloud_state.fresh(gc["time"], 9 * 86400)
|
|
41
|
+
return snapshot["id"]
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def main():
|
|
45
|
+
token = os.environ.get("VAULT_TOKEN", "")
|
|
46
|
+
if not token:
|
|
47
|
+
raise ValueError("Existing short-lived Vault login required")
|
|
48
|
+
data = cloud_state.settings(token)
|
|
49
|
+
errors = []
|
|
50
|
+
for attempt in range(3):
|
|
51
|
+
try:
|
|
52
|
+
endpoint_health(fleet_config.endpoint(), data["CLIPROXY_FLEET_CLIENT_API_KEY"])
|
|
53
|
+
print("PASS: external HTTPS, client authentication, models and dashboard.")
|
|
54
|
+
break
|
|
55
|
+
except Exception:
|
|
56
|
+
if attempt == 2:
|
|
57
|
+
errors.append("Public endpoint authentication/availability failed.")
|
|
58
|
+
else:
|
|
59
|
+
time.sleep(10)
|
|
60
|
+
try:
|
|
61
|
+
cloud_health(cloud_state.client(data))
|
|
62
|
+
print(
|
|
63
|
+
"PASS: cloud checkpoint under 45 minutes old; successful GC under nine days old."
|
|
64
|
+
)
|
|
65
|
+
except Exception:
|
|
66
|
+
errors.append("Cloud checkpoint or GC freshness/readability failed.")
|
|
67
|
+
if errors:
|
|
68
|
+
for error in errors:
|
|
69
|
+
print("FAIL:", error)
|
|
70
|
+
raise ValueError(" ".join(errors))
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
if __name__ == "__main__":
|
|
74
|
+
try:
|
|
75
|
+
main()
|
|
76
|
+
except Exception:
|
|
77
|
+
raise SystemExit(
|
|
78
|
+
"External CLI proxy verification failed; credentials and upstream responses suppressed. See the last completed check above."
|
|
79
|
+
) from None
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""This fleet's own configuration, as the proxy scripts need it.
|
|
2
|
+
|
|
3
|
+
Nothing here is a default: the endpoint, the Vault address, its paths and the
|
|
4
|
+
login all come from the fleet configuration every machine keeps at
|
|
5
|
+
~/.config/dev-fleet/fleet.json (or DEVFLEET_CONFIG).
|
|
6
|
+
"""
|
|
7
|
+
import json
|
|
8
|
+
import os
|
|
9
|
+
import pathlib
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def path():
|
|
13
|
+
return pathlib.Path(os.environ.get('DEVFLEET_CONFIG')
|
|
14
|
+
or pathlib.Path.home() / '.config/dev-fleet/fleet.json')
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def load():
|
|
18
|
+
try:
|
|
19
|
+
return json.loads(path().read_text())
|
|
20
|
+
except (OSError, ValueError):
|
|
21
|
+
raise ValueError('Fleet configuration unavailable: ' + str(path())) from None
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def endpoint():
|
|
25
|
+
"""Public HTTPS origin of the proxy."""
|
|
26
|
+
return load()['services']['proxy']['endpoint']
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def vault():
|
|
30
|
+
"""The proxy's backup, recovery and connector credentials live in Vault."""
|
|
31
|
+
config = load()
|
|
32
|
+
secrets = config['secrets']
|
|
33
|
+
if secrets.get('provider') != 'vault':
|
|
34
|
+
raise ValueError('The proxy service operations need the vault secrets provider')
|
|
35
|
+
locations = secrets['locations']
|
|
36
|
+
return {
|
|
37
|
+
'address': secrets['address'],
|
|
38
|
+
# KV v2 API paths: <mount>/data/<path>.
|
|
39
|
+
'shared': secrets['mount'] + '/data/' + locations['proxy-client-key']['path'],
|
|
40
|
+
'tunnel': secrets['mount'] + '/data/' + locations['proxy-tunnel-token']['path'],
|
|
41
|
+
'tunnel_field': locations['proxy-tunnel-token']['field'],
|
|
42
|
+
'user': config['worker']['vaultUser'],
|
|
43
|
+
}
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Fleet singleton reconciliation: the manager hosts the proxy, workers are clients."""
|
|
3
|
+
import sys
|
|
4
|
+
sys.dont_write_bytecode = True
|
|
5
|
+
|
|
6
|
+
import argparse
|
|
7
|
+
import importlib.util
|
|
8
|
+
import json
|
|
9
|
+
import os
|
|
10
|
+
import pathlib
|
|
11
|
+
import re
|
|
12
|
+
import subprocess
|
|
13
|
+
import time
|
|
14
|
+
|
|
15
|
+
SCRIPTS = pathlib.Path(__file__).resolve().parent
|
|
16
|
+
sys.path.insert(0, str(SCRIPTS))
|
|
17
|
+
import cliproxy_host
|
|
18
|
+
import fleet_config
|
|
19
|
+
|
|
20
|
+
DECOMMISSION = ('Workers never host singletons. Fence and decommission this local proxy with '
|
|
21
|
+
'cliproxy/docs/migration.md (disable timers, drain, stop, disable, '
|
|
22
|
+
'final backup); fleet-sync never stops it automatically.')
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def execute(args, timeout=120):
|
|
26
|
+
try:
|
|
27
|
+
result = subprocess.run(args, capture_output=True, text=True, timeout=timeout,
|
|
28
|
+
env={**os.environ, 'PYTHONDONTWRITEBYTECODE': '1'})
|
|
29
|
+
return result.returncode == 0
|
|
30
|
+
except (OSError, subprocess.TimeoutExpired):
|
|
31
|
+
return False
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def audit(root):
|
|
35
|
+
spec = importlib.util.spec_from_file_location('fleet_audit', SCRIPTS / 'audit.py')
|
|
36
|
+
module = importlib.util.module_from_spec(spec)
|
|
37
|
+
spec.loader.exec_module(module)
|
|
38
|
+
try:
|
|
39
|
+
# Provenance and unit links only: loaded/active state is reported per unit below.
|
|
40
|
+
manifest, errors = module.audit(root, run=lambda argv: True)
|
|
41
|
+
return not errors, manifest.get('revision')
|
|
42
|
+
except Exception:
|
|
43
|
+
return False, None
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def phase(code, status, summary, expected, observed, retryable=False):
|
|
47
|
+
return {'id': 'proxy:' + code.lower().replace('_', '-'), 'code': code,
|
|
48
|
+
'status': status, 'summary': summary, 'expected': expected,
|
|
49
|
+
'observed': observed, 'retryable': retryable,
|
|
50
|
+
'actionRequired': status in ('failed', 'action-required'),
|
|
51
|
+
'scope': 'node', 'remedy': {'owner': 'cliproxy-fleet', 'action': summary}}
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def unit_code(unit):
|
|
55
|
+
return 'PROXY_UNIT_' + re.sub('[^A-Z0-9]', '_', unit.upper())
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def equivalent_source(installed, revision):
|
|
59
|
+
"""A monorepo-only update must not force a running gateway to restart."""
|
|
60
|
+
if not isinstance(installed, str) or not re.fullmatch('[a-f0-9]{40}', installed):
|
|
61
|
+
return False
|
|
62
|
+
try:
|
|
63
|
+
result = subprocess.run(['git', '-C', str(SCRIPTS.parents[1]), 'rev-parse',
|
|
64
|
+
installed + ':cliproxy', revision + ':cliproxy'],
|
|
65
|
+
capture_output=True, text=True, timeout=15)
|
|
66
|
+
trees = result.stdout.splitlines()
|
|
67
|
+
return result.returncode == 0 and len(trees) == 2 and all(re.fullmatch('[a-f0-9]{40}', tree) for tree in trees) and trees[0] == trees[1]
|
|
68
|
+
except (OSError, subprocess.TimeoutExpired):
|
|
69
|
+
return False
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def inspect_worker(root, backend, run):
|
|
73
|
+
"""Client role. A local host is reported, never stopped."""
|
|
74
|
+
live = [unit for unit in backend.units if any(backend.state(unit, run))]
|
|
75
|
+
if live:
|
|
76
|
+
return [phase('PROXY_DUPLICATE_OWNER', 'action-required', DECOMMISSION,
|
|
77
|
+
'No proxy host on workers', 'Active or enabled local host: ' + ', '.join(live))]
|
|
78
|
+
if (root / 'INSTALLATION.json').exists():
|
|
79
|
+
return [phase('PROXY_DECOMMISSION_PENDING', 'action-required',
|
|
80
|
+
'A fenced proxy installation (with private state) remains on this worker. After the manager '
|
|
81
|
+
'cutover is verified, remove it as described in cliproxy/docs/migration.md.',
|
|
82
|
+
'No proxy installation on workers', 'Stopped local installation present')]
|
|
83
|
+
return [phase('PROXY_CLIENT_ROLE', 'unchanged', 'Worker; the proxy runs on the manager.',
|
|
84
|
+
'No proxy host on workers', 'No active host')]
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def inspect(root, node, owner, revision, run=execute, audit_fn=audit, compatible_fn=equivalent_source,
|
|
88
|
+
backend=None, identity=None):
|
|
89
|
+
backend = backend or cliproxy_host.backend()
|
|
90
|
+
if node != owner:
|
|
91
|
+
return inspect_worker(root, backend, run)
|
|
92
|
+
if backend.name != 'launchd':
|
|
93
|
+
return [phase('PROXY_PLATFORM_MISMATCH', 'action-required',
|
|
94
|
+
'The manager hosts the proxy on macOS with launchd; run fleet-sync for the manager on the manager.',
|
|
95
|
+
'macOS launchd owner backend', backend.name)]
|
|
96
|
+
if identity is not None and identity != owner:
|
|
97
|
+
return [phase('PROXY_IDENTITY_MISMATCH', 'action-required',
|
|
98
|
+
'This host is not the declared manager; changing the manager requires reviewed identity assignment and fencing.',
|
|
99
|
+
owner, identity)]
|
|
100
|
+
if not (root / 'state/runtime/config.yaml').is_file():
|
|
101
|
+
return [phase('PROXY_RECOVERY_REQUIRED', 'action-required',
|
|
102
|
+
'Use the existing encrypted recovery flow after fencing the previous owner; never initialize a second provider identity.',
|
|
103
|
+
'Recovered runtime state on the manager', 'Runtime cache missing')]
|
|
104
|
+
clean, installed = audit_fn(root)
|
|
105
|
+
if not clean:
|
|
106
|
+
return [phase('PROXY_SOURCE_DRIFT', 'action-required',
|
|
107
|
+
'Inspect cliproxy audit, preserve runtime state, then reinstall a reviewed revision during maintenance.',
|
|
108
|
+
'Clean tracked installation and units', 'Missing or modified deployment')]
|
|
109
|
+
if installed != revision and not compatible_fn(installed, revision):
|
|
110
|
+
return [phase('PROXY_UPDATE_PENDING', 'action-required',
|
|
111
|
+
'Installing the reviewed revision failed. Inspect `bun run cliproxy install --revision REV` on the '
|
|
112
|
+
'manager; runtime state is preserved.',
|
|
113
|
+
revision, installed if isinstance(installed, str) and re.fullmatch('[0-9a-f]{40}', installed) else 'Unknown revision')]
|
|
114
|
+
checks = []
|
|
115
|
+
for unit in backend.units:
|
|
116
|
+
active, enabled = backend.state(unit, run)
|
|
117
|
+
checks.append(phase(unit_code(unit),
|
|
118
|
+
'unchanged' if active and enabled else 'action-required',
|
|
119
|
+
'Enable/start the declared unit; do not restart an already running gateway.',
|
|
120
|
+
'Enabled and active ' + unit,
|
|
121
|
+
'Enabled and active' if active and enabled else 'Stopped or disabled', True))
|
|
122
|
+
if not (root / 'state/public-endpoint-enabled').exists():
|
|
123
|
+
checks.append(phase('PROXY_ENDPOINT_DISABLED', 'action-required',
|
|
124
|
+
'Enable the existing public connector using its existing Vault credential.',
|
|
125
|
+
'Public connector enabled', 'Endpoint marker absent', True))
|
|
126
|
+
if not run(declared(root, apply=False)):
|
|
127
|
+
checks.append(phase('PROXY_CONFIG_PENDING', 'action-required',
|
|
128
|
+
'The live proxy lacks fleet-declared settings (model aliases, account prefixes) and '
|
|
129
|
+
'applying them failed. Inspect `ops declared --apply` on the manager; the previous config '
|
|
130
|
+
'is kept as state/runtime/config.yaml.previous.',
|
|
131
|
+
'Declared config settings and a model prefix per signed-in account', 'Live proxy differs', True))
|
|
132
|
+
return checks
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def claude_pin(repository=SCRIPTS.parents[1]):
|
|
136
|
+
"""The Claude Code version every node runs (the shared toolchain pin)."""
|
|
137
|
+
try:
|
|
138
|
+
match = re.search(r'^claude = "(\d+\.\d+\.\d+)"$', (repository / 'mise.toml').read_text(), re.M)
|
|
139
|
+
return match.group(1) if match else None
|
|
140
|
+
except OSError:
|
|
141
|
+
return None
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def declared(root, apply):
|
|
145
|
+
"""Checks (exit 0 when unchanged) or applies the template-owned settings.
|
|
146
|
+
Runs under the install's pinned interpreter: only it carries PyYAML. The
|
|
147
|
+
proxy must claim the Claude Code version the fleet actually runs."""
|
|
148
|
+
python = root / 'tools/bin/python3'
|
|
149
|
+
version = claude_pin()
|
|
150
|
+
return [str(python) if python.exists() else sys.executable, '-B', str(root / 'scripts/ops'),
|
|
151
|
+
'declared', *(['--claude-version', version] if version else []), *(['--apply'] if apply else [])]
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def endpoint_health(root):
|
|
155
|
+
try:
|
|
156
|
+
spec = importlib.util.spec_from_file_location('fleet_external_health', SCRIPTS / 'external-health.py')
|
|
157
|
+
module = importlib.util.module_from_spec(spec)
|
|
158
|
+
spec.loader.exec_module(module)
|
|
159
|
+
endpoint = fleet_config.endpoint()
|
|
160
|
+
key = (root / 'state/secrets/client-key').read_text().strip()
|
|
161
|
+
module.endpoint_health(endpoint, key)
|
|
162
|
+
return True
|
|
163
|
+
except Exception:
|
|
164
|
+
return False
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def endpoint_verified(root, endpoint_check, attempts=6, delay=10, sleep=time.sleep):
|
|
168
|
+
"""The public route can take a few seconds to reconnect after a start or
|
|
169
|
+
update; a bounded retry keeps that from reading as a failure."""
|
|
170
|
+
for attempt in range(attempts):
|
|
171
|
+
if endpoint_check(root):
|
|
172
|
+
return True
|
|
173
|
+
if attempt + 1 < attempts:
|
|
174
|
+
sleep(delay)
|
|
175
|
+
return False
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def converge_units(root, checks, backend, run):
|
|
179
|
+
"""Loads stopped agents and enables the connector; never restarts a running gateway."""
|
|
180
|
+
changed = False
|
|
181
|
+
for item in checks:
|
|
182
|
+
if item['status'] != 'action-required':
|
|
183
|
+
continue
|
|
184
|
+
if item['code'].startswith('PROXY_UNIT_'):
|
|
185
|
+
unit = item['expected'].removeprefix('Enabled and active ')
|
|
186
|
+
# Backup operations must not run before existing recovery confirmation.
|
|
187
|
+
if unit in backend.recovery_gated and not (root / 'state/secrets/recovery-confirmed').exists():
|
|
188
|
+
continue
|
|
189
|
+
for command in backend.enable(unit):
|
|
190
|
+
changed = run(command) or changed
|
|
191
|
+
elif item['code'] == 'PROXY_ENDPOINT_DISABLED':
|
|
192
|
+
changed = run([sys.executable, '-B', str(root / 'scripts/public-endpoint'), 'enable']) or changed
|
|
193
|
+
return changed
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def reconcile(root, node, owner, revision, apply=False, run=execute, audit_fn=audit,
|
|
197
|
+
endpoint_check=endpoint_health, compatible_fn=equivalent_source, backend=None, identity=None,
|
|
198
|
+
sleep=time.sleep):
|
|
199
|
+
started = time.monotonic()
|
|
200
|
+
backend = backend or cliproxy_host.backend()
|
|
201
|
+
checks = inspect(root, node, owner, revision, run, audit_fn, compatible_fn, backend, identity)
|
|
202
|
+
changed = False
|
|
203
|
+
blocked = {'PROXY_PLATFORM_MISMATCH', 'PROXY_IDENTITY_MISMATCH', 'PROXY_RECOVERY_REQUIRED',
|
|
204
|
+
'PROXY_SOURCE_DRIFT', 'PROXY_UPDATE_PENDING'}
|
|
205
|
+
# Two passes: an update installed in the first may surface declared
|
|
206
|
+
# settings (or stopped units) that the second then converges.
|
|
207
|
+
for _ in range(2 if apply and node == owner else 0):
|
|
208
|
+
codes = {item['code'] for item in checks}
|
|
209
|
+
if codes & (blocked - {'PROXY_UPDATE_PENDING'}):
|
|
210
|
+
break
|
|
211
|
+
if 'PROXY_UPDATE_PENDING' in codes:
|
|
212
|
+
# Every sync installs a pending update, including one that recreates
|
|
213
|
+
# the gateway (which interrupts in-flight inference for a moment).
|
|
214
|
+
changed = run(backend.install(sys.executable, SCRIPTS, revision), 1500)
|
|
215
|
+
if not changed:
|
|
216
|
+
break
|
|
217
|
+
checks = inspect(root, node, owner, revision, run, audit_fn, compatible_fn, backend, identity)
|
|
218
|
+
continue
|
|
219
|
+
changed = converge_units(root, checks, backend, run) or changed
|
|
220
|
+
if 'PROXY_CONFIG_PENDING' in codes:
|
|
221
|
+
# Self-healing and restart-free: the proxy hot-reloads its config.
|
|
222
|
+
changed = run(declared(root, apply=True), 600) or changed
|
|
223
|
+
checks = inspect(root, node, owner, revision, run, audit_fn, compatible_fn, backend, identity)
|
|
224
|
+
break
|
|
225
|
+
if node == owner and all(item['status'] == 'unchanged' for item in checks):
|
|
226
|
+
verified = endpoint_verified(root, endpoint_check, sleep=sleep)
|
|
227
|
+
checks.append(phase('PROXY_ENDPOINT_AUTH', 'unchanged' if verified else 'action-required',
|
|
228
|
+
'Verify the existing endpoint/client credential. Provider inference is a separate gate; routine checks never attempt management logins.',
|
|
229
|
+
'Authenticated public gateway client route',
|
|
230
|
+
'Verified' if verified else 'Endpoint or authentication unavailable', True))
|
|
231
|
+
return {'version': 1, 'command': 'cliproxy fleet-sync', 'node': node,
|
|
232
|
+
'role': 'manager' if node == owner else 'worker', 'owner': owner,
|
|
233
|
+
'backend': backend.name, 'ok': all(item['status'] == 'unchanged' for item in checks),
|
|
234
|
+
'changed': changed, 'desiredRevision': revision, 'phases': checks,
|
|
235
|
+
'durationMs': round((time.monotonic() - started) * 1000)}
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def roles(definition):
|
|
239
|
+
"""Validate definition v3 and return (manager id, worker ids)."""
|
|
240
|
+
manager = definition['manager']
|
|
241
|
+
if (definition.get('version') != 3 or manager.get('os') != 'macOS'
|
|
242
|
+
or not re.fullmatch(r'https://[A-Za-z0-9.-]+', str(definition['services']['proxy'].get('endpoint')))):
|
|
243
|
+
raise ValueError('Invalid definition')
|
|
244
|
+
workers = {worker['id'] for worker in definition['workers']}
|
|
245
|
+
if manager['id'] in workers:
|
|
246
|
+
raise ValueError('The manager is never a worker')
|
|
247
|
+
return manager['id'], workers
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def main():
|
|
251
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
252
|
+
parser.add_argument('--definition', type=pathlib.Path, required=True)
|
|
253
|
+
parser.add_argument('--node', required=True)
|
|
254
|
+
parser.add_argument('--revision', required=True)
|
|
255
|
+
parser.add_argument('--apply', action='store_true')
|
|
256
|
+
args = parser.parse_args()
|
|
257
|
+
owner, workers = roles(json.loads(args.definition.read_text()))
|
|
258
|
+
if not re.fullmatch('[a-f0-9]{40}', args.revision) or args.node not in workers | {owner}:
|
|
259
|
+
raise ValueError('Invalid selection')
|
|
260
|
+
backend = cliproxy_host.backend()
|
|
261
|
+
identity = cliproxy_host.local_node_id() if args.node == owner else None
|
|
262
|
+
result = reconcile(cliproxy_host.default_root(), args.node, owner, args.revision, args.apply,
|
|
263
|
+
backend=backend, identity=identity)
|
|
264
|
+
print(json.dumps(result))
|
|
265
|
+
return 0 if result['ok'] else 1
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
if __name__ == '__main__':
|
|
269
|
+
try:
|
|
270
|
+
raise SystemExit(main())
|
|
271
|
+
except Exception:
|
|
272
|
+
print(json.dumps({'version': 1, 'ok': False, 'code': 'PROXY_RECONCILE_FAILED',
|
|
273
|
+
'summary': 'Inspect reviewed definition, singleton identity and local permissions; raw errors withheld.'}))
|
|
274
|
+
raise SystemExit(1) from None
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Materialize local state from existing cloud secrets; never seed Vault."""
|
|
3
|
+
|
|
4
|
+
import sys
|
|
5
|
+
|
|
6
|
+
sys.dont_write_bytecode = True
|
|
7
|
+
|
|
8
|
+
import pathlib as _pathlib
|
|
9
|
+
|
|
10
|
+
sys.path.insert(0, str(_pathlib.Path(__file__).resolve().parent))
|
|
11
|
+
import cliproxy_host
|
|
12
|
+
import fleet_config
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
import hashlib
|
|
16
|
+
import importlib.util
|
|
17
|
+
import json
|
|
18
|
+
import os
|
|
19
|
+
import pathlib
|
|
20
|
+
import urllib.request
|
|
21
|
+
|
|
22
|
+
root = cliproxy_host.default_root().resolve()
|
|
23
|
+
os.umask(0o077)
|
|
24
|
+
state = root / "state"
|
|
25
|
+
for directory in ["status", "secrets"]:
|
|
26
|
+
(state / directory).mkdir(parents=True, exist_ok=True)
|
|
27
|
+
fields = {
|
|
28
|
+
"client-key": "CLIPROXY_FLEET_CLIENT_API_KEY",
|
|
29
|
+
"management-key": "CLIPROXY_FLEET_MANAGEMENT_KEY",
|
|
30
|
+
"restic-password": "CLIPROXY_FLEET_RESTIC_PASSWORD",
|
|
31
|
+
}
|
|
32
|
+
missing = [name for name in fields if not (state / "secrets" / name).exists()]
|
|
33
|
+
if missing:
|
|
34
|
+
spec = importlib.util.spec_from_file_location(
|
|
35
|
+
"vault_access", root / "scripts/vault-access.py"
|
|
36
|
+
)
|
|
37
|
+
vault = importlib.util.module_from_spec(spec)
|
|
38
|
+
spec.loader.exec_module(vault)
|
|
39
|
+
try:
|
|
40
|
+
data = vault.request(fleet_config.vault()["shared"], vault.token())["data"]["data"]
|
|
41
|
+
for name in missing:
|
|
42
|
+
if not data.get(fields[name]):
|
|
43
|
+
raise ValueError("Required registered secret missing")
|
|
44
|
+
for name in missing:
|
|
45
|
+
(state / "secrets" / name).write_text(data[fields[name]] + "\n")
|
|
46
|
+
except Exception:
|
|
47
|
+
raise SystemExit(
|
|
48
|
+
"Existing Vault secrets required. Run vault-backup-login, and manage key values through the OpenTofu Vault workflow; response suppressed."
|
|
49
|
+
)
|
|
50
|
+
config = state / "runtime/config.yaml"
|
|
51
|
+
if not config.exists():
|
|
52
|
+
raise SystemExit(
|
|
53
|
+
"Runtime cache missing. Recover from the cloud checkpoint with restore-cache; refusing to seed an empty deployment."
|
|
54
|
+
)
|
|
55
|
+
panel = state / "runtime/static/management.html"
|
|
56
|
+
if not panel.exists():
|
|
57
|
+
metadata = json.load(
|
|
58
|
+
urllib.request.urlopen(
|
|
59
|
+
"https://api.github.com/repos/router-for-me/Cli-Proxy-API-Management-Center/releases/tags/v1.27.2",
|
|
60
|
+
timeout=30,
|
|
61
|
+
)
|
|
62
|
+
)
|
|
63
|
+
asset = next(
|
|
64
|
+
asset for asset in metadata["assets"] if asset["name"] == "management.html"
|
|
65
|
+
)
|
|
66
|
+
data = urllib.request.urlopen(asset["browser_download_url"], timeout=60).read()
|
|
67
|
+
if (
|
|
68
|
+
hashlib.sha256(data).hexdigest()
|
|
69
|
+
!= "2f6cf57a8ca4ace1fadde0280cbd41084f56999738d0e692a30b135420f7fc30"
|
|
70
|
+
):
|
|
71
|
+
raise SystemExit("Dashboard checksum mismatch")
|
|
72
|
+
panel.write_bytes(data)
|
|
73
|
+
for file in state.rglob("*"):
|
|
74
|
+
if file.is_symlink():
|
|
75
|
+
continue
|
|
76
|
+
file.chmod(0o700 if file.is_dir() else 0o600)
|
|
77
|
+
if os.geteuid() == 0:
|
|
78
|
+
os.chown(file, 1000, 1000)
|
|
79
|
+
if os.geteuid() == 0:
|
|
80
|
+
os.chown(state, 1000, 1000)
|
|
81
|
+
state.chmod(0o700)
|
|
82
|
+
print(
|
|
83
|
+
"Initialized local state from existing cloud credentials; live configuration preserved."
|
|
84
|
+
)
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
if [[ $(uname -s) == Darwin ]]; then
|
|
4
|
+
# Manager (macOS): per-user, never sudo, no Homebrew. Builds a private tools
|
|
5
|
+
# directory from the repository's pinned, checksum-locked toolchain:
|
|
6
|
+
# tools/bin/python3 venv on mise's pinned Python with hash-locked PyYAML
|
|
7
|
+
# tools/bin/restic mise's pinned restic (mise.lock checksums)
|
|
8
|
+
# The LaunchAgents put tools/bin first on PATH, so jobs never depend on
|
|
9
|
+
# whatever python3/restic happens to be installed on the machine.
|
|
10
|
+
[[ $EUID != 0 ]] || { echo 'Do not use sudo on macOS; the manager installation is per-user.'; exit 1; }
|
|
11
|
+
here=$(cd "$(dirname "$0")/.." && pwd)
|
|
12
|
+
repo=$(cd "$here/../.." && pwd)
|
|
13
|
+
tools="$HOME/Library/Application Support/cliproxy-fleet/tools"
|
|
14
|
+
command -v mise >/dev/null || { echo 'mise is required: run the shared toolchain setup (bootstrap/README.md).'; exit 1; }
|
|
15
|
+
[[ -d /Applications/Docker.app ]] || { echo 'Docker Desktop (/Applications/Docker.app) is required.'; exit 1; }
|
|
16
|
+
cd "$repo"
|
|
17
|
+
mise install --quiet python uv restic
|
|
18
|
+
python=$(mise where python)/bin/python3
|
|
19
|
+
restic=$(mise where restic)/restic
|
|
20
|
+
[[ -x $restic ]] || restic=$(find "$(mise where restic)" -maxdepth 2 -type f -name 'restic*' -perm -u+x | head -1)
|
|
21
|
+
[[ -x $python && -x $restic ]] || { echo 'Pinned python/restic not found after mise install.'; exit 1; }
|
|
22
|
+
mkdir -p "$(dirname "$tools")"
|
|
23
|
+
chmod 700 "$(dirname "$tools")"
|
|
24
|
+
# Rebuild only when the pinned interpreter or lock changed.
|
|
25
|
+
stamp="$python $(shasum -a 256 "$here/requirements.lock" | cut -d' ' -f1)"
|
|
26
|
+
if [[ ! -x $tools/bin/python3 || $(cat "$tools/.pins" 2>/dev/null) != "$stamp" ]]; then
|
|
27
|
+
# A venv records its own path, so it is built where it runs.
|
|
28
|
+
rm -rf "$tools"
|
|
29
|
+
mise exec -- uv venv --quiet --python "$python" "$tools"
|
|
30
|
+
mise exec -- uv pip install --quiet --python "$tools/bin/python3" \
|
|
31
|
+
--require-hashes --no-deps -r "$here/requirements.lock"
|
|
32
|
+
printf '%s' "$stamp" > "$tools/.pins"
|
|
33
|
+
fi
|
|
34
|
+
ln -sfn "$restic" "$tools/bin/restic"
|
|
35
|
+
"$tools/bin/python3" -c 'import yaml' || { echo 'PyYAML is not importable from the tools venv.'; exit 1; }
|
|
36
|
+
"$tools/bin/restic" version >/dev/null || { echo 'restic does not run.'; exit 1; }
|
|
37
|
+
printf 'Manager prerequisites ready in %s. Start Docker Desktop (sign-in start) before installing.\n' "$tools"
|
|
38
|
+
exit 0
|
|
39
|
+
fi
|
|
40
|
+
# Legacy Linux owner (decommission-only): Ubuntu packages.
|
|
41
|
+
[[ $EUID == 0 ]] || { echo 'Run with sudo.'; exit 1; }
|
|
42
|
+
apt-get update
|
|
43
|
+
apt-get install -y docker.io docker-compose-v2 restic python3-yaml curl openssl shellcheck
|
|
44
|
+
systemctl enable --now docker
|
|
45
|
+
if [[ -n ${SUDO_USER:-} && $SUDO_USER != root ]]; then
|
|
46
|
+
usermod -aG docker "$SUDO_USER"
|
|
47
|
+
fi
|
|
48
|
+
printf '\nDependencies installed. Return to Codex to continue deployment.\n'
|