@crvouga/dev-fleet 0.0.0-stage → 0.1.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 +157 -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 +128 -0
- package/agents/t3.json +40 -0
- package/ansible/macos/manager.yml +30 -0
- package/ansible/macos/policy.json +17 -0
- package/ansible/monitoring/README.md +154 -0
- package/ansible/monitoring/agent-darwin.yml +198 -0
- package/ansible/monitoring/agent.yml +134 -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/lock.json +11 -0
- package/ansible/net/remote-probe.sh +13 -0
- package/ansible/ubuntu/authorize-sudo.sh +51 -0
- package/ansible/ubuntu/baseline.yml +89 -0
- package/ansible/ubuntu/disk-guard.py +104 -0
- package/ansible/ubuntu/policy.json +43 -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 +75 -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 +252 -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/bin/devfleet.mjs +36 -0
- package/bootstrap/bootstrap.ps1 +50 -0
- package/bootstrap/bootstrap.sh +73 -0
- package/bootstrap/devfleet +7 -0
- package/bootstrap/docker-desktop.sh +33 -0
- package/bootstrap/doctor.ts +189 -0
- package/bootstrap/fleet-manager-run.sh +76 -0
- package/bootstrap/fleet-remote-bootstrap.sh +185 -0
- package/bootstrap/global-config.ts +113 -0
- package/bootstrap/install-launcher.sh +7 -0
- package/bootstrap/native-aws.ts +86 -0
- package/bootstrap/native-t3.ts +154 -0
- package/bootstrap/npm.ts +350 -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/examples/fleet.json +95 -0
- package/examples/roster.json +13 -0
- package/mise.lock +1492 -0
- package/mise.toml +99 -0
- package/package.json +65 -4
- package/schema/fleet.schema.json +364 -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 +156 -0
- package/src/agents/launch.ts +290 -0
- package/src/agents/native-verify.ts +226 -0
- package/src/agents/provider-errors.ts +50 -0
- package/src/agents/registry.ts +55 -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 +95 -0
- package/src/cli/app.ts +115 -0
- package/src/cli/cli.ts +21 -0
- package/src/cli/command-index.ts +50 -0
- package/src/cli/contracts.ts +25 -0
- package/src/cli/fleet-reference.ts +17 -0
- package/src/cli/fleet-sync-checkout.ts +91 -0
- package/src/cli/fleet-sync-enrollment.ts +144 -0
- package/src/cli/fleet-sync-forward.ts +134 -0
- package/src/cli/fleet-sync-manager.ts +193 -0
- package/src/cli/fleet-sync-monitoring.ts +185 -0
- package/src/cli/fleet-sync-output.ts +314 -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 +236 -0
- package/src/cli/fleet-sync-report.ts +26 -0
- package/src/cli/fleet-sync-schedule.ts +138 -0
- package/src/cli/fleet-sync-vault.ts +166 -0
- package/src/cli/fleet-sync.ts +309 -0
- package/src/cli/fleet.ts +163 -0
- package/src/cli/managed-health.ts +219 -0
- package/src/cli/node-deployment.ts +34 -0
- package/src/cli/node-sync.ts +114 -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 +169 -0
- package/src/cli/register-fleet.ts +246 -0
- package/src/cli/registry.ts +158 -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 +89 -0
- package/src/cli/search.ts +35 -0
- package/src/cli/settings.ts +64 -0
- package/src/cli/source-operations.ts +78 -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 +260 -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 +88 -0
- package/src/index.ts +6 -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 +185 -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 +400 -0
- package/src/manager.ts +62 -0
- package/src/monitoring/apply.ts +171 -0
- package/src/monitoring/beszel.ts +191 -0
- package/src/monitoring/enroll.ts +110 -0
- package/src/monitoring/hub-access.ts +113 -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 +232 -0
- package/src/monitoring/remote.ts +65 -0
- package/src/network-definition.ts +83 -0
- package/src/node-converge-linux.ts +372 -0
- package/src/node-converge-shared.ts +83 -0
- package/src/node-converge.ts +40 -0
- package/src/onboard.ts +35 -0
- package/src/platform/ansible-receipt.ts +128 -0
- package/src/platform/ansible.ts +62 -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 +128 -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 +63 -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 +212 -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/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 +48 -0
- package/src/t3-linked-check.ts +101 -0
- package/src/tailnet-health.ts +231 -0
- package/src/tailnet-matching.ts +37 -0
- package/src/tailscale.ts +141 -0
- package/src/test-fixtures.ts +36 -0
- package/src/toolchain-checks.ts +83 -0
- package/src/topology.ts +24 -0
- package/src/transport.ts +122 -0
- package/src/types.ts +95 -0
- package/src/vault-config.ts +38 -0
- package/src/worker-access-checks.ts +115 -0
- package/src/worker-capacity.ts +34 -0
- package/src/worker-host-checks.ts +176 -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,527 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
import sys
|
|
3
|
+
|
|
4
|
+
sys.dont_write_bytecode = True
|
|
5
|
+
|
|
6
|
+
import pathlib as _pathlib
|
|
7
|
+
|
|
8
|
+
sys.path.insert(0, str(_pathlib.Path(__file__).resolve().parent))
|
|
9
|
+
import accounts
|
|
10
|
+
import cliproxy_host
|
|
11
|
+
|
|
12
|
+
import fcntl, json, os, pathlib, shutil, subprocess, sys, tempfile, time, urllib.request, urllib.error
|
|
13
|
+
|
|
14
|
+
root = cliproxy_host.default_root().resolve()
|
|
15
|
+
# tools/bin holds the pinned python3 + PyYAML venv. LaunchAgents already put it
|
|
16
|
+
# first; interactive installs must too, or validate-config gets a system python3.
|
|
17
|
+
os.environ["PATH"] = os.pathsep.join(
|
|
18
|
+
[str(root / "scripts"), str(root / "tools/bin"), os.environ["PATH"]]
|
|
19
|
+
)
|
|
20
|
+
os.umask(0o077)
|
|
21
|
+
os.chdir(root)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def run(args, **kw):
|
|
25
|
+
return subprocess.run(args, check=True, **kw)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def compose(*args, **kw):
|
|
29
|
+
return run(["docker", "compose", *args], **kw)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def validate():
|
|
33
|
+
if (root / "state/recovery-incomplete").exists():
|
|
34
|
+
raise ValueError("Interrupted cache recovery must be resolved before startup")
|
|
35
|
+
run([str(root / "scripts/validate-config")])
|
|
36
|
+
compose("config", "--quiet")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def request(path, key=None, data=None, base="http://127.0.0.1:8317", method=None):
|
|
40
|
+
headers = {}
|
|
41
|
+
if key:
|
|
42
|
+
headers["Authorization"] = "Bearer " + key
|
|
43
|
+
if data is not None:
|
|
44
|
+
headers["Content-Type"] = "application/json"
|
|
45
|
+
req = urllib.request.Request(
|
|
46
|
+
base + path,
|
|
47
|
+
data=json.dumps(data).encode() if data is not None else None,
|
|
48
|
+
headers=headers,
|
|
49
|
+
method=method,
|
|
50
|
+
)
|
|
51
|
+
try:
|
|
52
|
+
with urllib.request.urlopen(req, timeout=30) as r:
|
|
53
|
+
return r.status, r.read()
|
|
54
|
+
except urllib.error.HTTPError as e:
|
|
55
|
+
return e.code, e.read()
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def health():
|
|
59
|
+
key = (root / "state/secrets/client-key").read_text().strip()
|
|
60
|
+
assert request("/v1/models")[0] == 401, (
|
|
61
|
+
"Anonymous inference accepted or unavailable"
|
|
62
|
+
)
|
|
63
|
+
code, body = request("/v1/models", key)
|
|
64
|
+
assert code == 200, "Authenticated model listing failed"
|
|
65
|
+
assert request("/v0/management/config", key)[0] in (401, 403), (
|
|
66
|
+
"Client key accessed management"
|
|
67
|
+
)
|
|
68
|
+
admin = (root / "state/secrets/management-key").read_text().strip()
|
|
69
|
+
assert request("/v0/management/config", admin)[0] == 200, (
|
|
70
|
+
"Management authentication failed"
|
|
71
|
+
)
|
|
72
|
+
assert request("/management.html")[0] == 200, "Official dashboard unavailable"
|
|
73
|
+
models = json.loads(body).get("data", [])
|
|
74
|
+
if not isinstance(models, list) or not models:
|
|
75
|
+
raise ValueError("No usable provider models")
|
|
76
|
+
print(
|
|
77
|
+
"PASS: anonymous rejected; client models OK; separate management key enforced; dashboard available."
|
|
78
|
+
)
|
|
79
|
+
print("Available model count:", len(models))
|
|
80
|
+
return models
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def wait():
|
|
84
|
+
for _ in range(30):
|
|
85
|
+
try:
|
|
86
|
+
health()
|
|
87
|
+
return
|
|
88
|
+
except Exception:
|
|
89
|
+
time.sleep(2)
|
|
90
|
+
sys.exit(
|
|
91
|
+
"Health did not recover in 60 seconds. Inspect status; use rollback if upgrading."
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
# Runtime config keys owned by config.template.yaml (everything else in the
|
|
96
|
+
# live config, such as keys and accounts, is runtime state and preserved).
|
|
97
|
+
DECLARED = [("oauth", "model-alias"), ("upstream", "claude")]
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def claude_version():
|
|
101
|
+
"""`--claude-version X.Y.Z`: the Claude Code version every node runs."""
|
|
102
|
+
import re
|
|
103
|
+
|
|
104
|
+
for index, argument in enumerate(sys.argv):
|
|
105
|
+
if argument == "--claude-version":
|
|
106
|
+
version = sys.argv[index + 1] if index + 1 < len(sys.argv) else ""
|
|
107
|
+
if not re.fullmatch(r"\d+\.\d+\.\d+", version):
|
|
108
|
+
raise ValueError("Invalid Claude Code version")
|
|
109
|
+
return version
|
|
110
|
+
return None
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def declared_template():
|
|
114
|
+
"""The tracked template; its Claude client version follows the fleet pin."""
|
|
115
|
+
import re, yaml
|
|
116
|
+
|
|
117
|
+
text = (root / "config.template.yaml").read_text()
|
|
118
|
+
version = claude_version()
|
|
119
|
+
if version:
|
|
120
|
+
text = re.sub(r"claude-cli/\d+\.\d+\.\d+", "claude-cli/" + version, text)
|
|
121
|
+
return yaml.safe_load(text)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def declared_candidate():
|
|
125
|
+
"""The live config with the declared settings from the template, or None."""
|
|
126
|
+
import yaml
|
|
127
|
+
|
|
128
|
+
template = declared_template()
|
|
129
|
+
live = yaml.safe_load((root / "state/runtime/config.yaml").read_text())
|
|
130
|
+
changed = False
|
|
131
|
+
for section, key in DECLARED:
|
|
132
|
+
want = (template.get(section) or {}).get(key)
|
|
133
|
+
have = (live.get(section) or {}).get(key)
|
|
134
|
+
if want == have:
|
|
135
|
+
continue
|
|
136
|
+
changed = True
|
|
137
|
+
live.setdefault(section, {})
|
|
138
|
+
if want is None:
|
|
139
|
+
live[section].pop(key, None)
|
|
140
|
+
else:
|
|
141
|
+
live[section][key] = want
|
|
142
|
+
return yaml.safe_dump(live, sort_keys=False) if changed else None
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def apply_config(candidate, restart=True):
|
|
146
|
+
run([str(root / "scripts/validate-config"), str(candidate), "--require-fill-first"])
|
|
147
|
+
staging(candidate)
|
|
148
|
+
shutil.copy2(
|
|
149
|
+
root / "state/runtime/config.yaml",
|
|
150
|
+
root / "state/runtime/config.yaml.previous",
|
|
151
|
+
)
|
|
152
|
+
shutil.copyfile(candidate, root / "state/runtime/config.yaml.new")
|
|
153
|
+
os.chmod(root / "state/runtime/config.yaml.new", 0o600)
|
|
154
|
+
os.replace(
|
|
155
|
+
root / "state/runtime/config.yaml.new", root / "state/runtime/config.yaml"
|
|
156
|
+
)
|
|
157
|
+
if restart:
|
|
158
|
+
compose("restart", "proxy")
|
|
159
|
+
wait()
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def declared_aliases():
|
|
163
|
+
"""Client-visible model ids the declared aliases must add."""
|
|
164
|
+
import yaml
|
|
165
|
+
|
|
166
|
+
template = yaml.safe_load((root / "config.template.yaml").read_text())
|
|
167
|
+
aliases = ((template.get("oauth") or {}).get("model-alias") or {}).values()
|
|
168
|
+
return {entry["alias"] for entries in aliases for entry in entries or []}
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def hot_reloaded(expected):
|
|
172
|
+
"""The proxy watches its config file; wait for the reload to serve it."""
|
|
173
|
+
for _ in range(15):
|
|
174
|
+
try:
|
|
175
|
+
served = {model.get("id") for model in health()}
|
|
176
|
+
if expected <= served:
|
|
177
|
+
return True
|
|
178
|
+
except Exception:
|
|
179
|
+
pass
|
|
180
|
+
time.sleep(2)
|
|
181
|
+
return False
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def pending_prefixes():
|
|
185
|
+
"""Credentials that lack their account's model prefix."""
|
|
186
|
+
return accounts.pending(root / "state/runtime/auths")
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def apply_prefixes(prefixes):
|
|
190
|
+
# The proxy owns its credential files (it rewrites them on token refresh),
|
|
191
|
+
# so the prefix is set through its own API, never by editing a file.
|
|
192
|
+
admin = (root / "state/secrets/management-key").read_text().strip()
|
|
193
|
+
for name, prefix in prefixes:
|
|
194
|
+
code, _ = request(
|
|
195
|
+
"/v0/management/auth-files/fields",
|
|
196
|
+
admin,
|
|
197
|
+
{"name": name, "prefix": prefix},
|
|
198
|
+
method="PATCH",
|
|
199
|
+
)
|
|
200
|
+
assert code == 200, "Credential prefix update failed"
|
|
201
|
+
for _ in range(15):
|
|
202
|
+
if not pending_prefixes():
|
|
203
|
+
return
|
|
204
|
+
time.sleep(2)
|
|
205
|
+
sys.exit("The proxy did not record the declared account prefixes.")
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def lock():
|
|
209
|
+
f = open(root / "state/status/operation.lock", "w")
|
|
210
|
+
if os.geteuid() == 0:
|
|
211
|
+
os.chown(root / "state/status/operation.lock", 1000, 1000)
|
|
212
|
+
fcntl.flock(f, fcntl.LOCK_EX)
|
|
213
|
+
return f
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def staging(config):
|
|
217
|
+
# No provider credential files are mounted; isolated port and temporary state.
|
|
218
|
+
import yaml
|
|
219
|
+
|
|
220
|
+
with tempfile.TemporaryDirectory(prefix="cliproxy-stage-") as d:
|
|
221
|
+
stage = pathlib.Path(d)
|
|
222
|
+
stage.chmod(0o755)
|
|
223
|
+
cfg = yaml.safe_load(pathlib.Path(config).read_text())
|
|
224
|
+
cfg["server"]["port"] = 8317
|
|
225
|
+
cfg["oauth"] = {"auth-dir": "/stage/auths"}
|
|
226
|
+
cfg["management"]["disable-control-panel"] = True
|
|
227
|
+
for field in [
|
|
228
|
+
"api-keys",
|
|
229
|
+
"gemini-api-key",
|
|
230
|
+
"claude-api-key",
|
|
231
|
+
"codex-api-key",
|
|
232
|
+
"openai-compatibility",
|
|
233
|
+
"xai-api-key",
|
|
234
|
+
"meta-api-key",
|
|
235
|
+
]:
|
|
236
|
+
cfg.pop(field, None)
|
|
237
|
+
cfg["management"]["disable-auto-update-panel"] = True
|
|
238
|
+
(stage / "auths").mkdir(mode=0o700)
|
|
239
|
+
(stage / "config.yaml").write_text(yaml.safe_dump(cfg))
|
|
240
|
+
(stage / "config.yaml").chmod(0o600)
|
|
241
|
+
image = dict(
|
|
242
|
+
line.split("=", 1) for line in (root / ".env").read_text().splitlines()
|
|
243
|
+
)["CLI_PROXY_IMAGE"]
|
|
244
|
+
name = "cliproxy-validate-" + str(os.getpid())
|
|
245
|
+
try:
|
|
246
|
+
run(
|
|
247
|
+
[
|
|
248
|
+
"docker",
|
|
249
|
+
"run",
|
|
250
|
+
"-d",
|
|
251
|
+
"--name",
|
|
252
|
+
name,
|
|
253
|
+
"--user",
|
|
254
|
+
"%d:%d" % cliproxy_host.runtime_ids(),
|
|
255
|
+
"--cap-drop=ALL",
|
|
256
|
+
"--security-opt=no-new-privileges",
|
|
257
|
+
"--memory=512m",
|
|
258
|
+
"--pids-limit=128",
|
|
259
|
+
"-v",
|
|
260
|
+
str(stage) + ":/stage",
|
|
261
|
+
"-p",
|
|
262
|
+
"127.0.0.1::8317",
|
|
263
|
+
image,
|
|
264
|
+
"/CLIProxyAPI/CLIProxyAPI",
|
|
265
|
+
"-config",
|
|
266
|
+
"/stage/config.yaml",
|
|
267
|
+
],
|
|
268
|
+
stdout=subprocess.DEVNULL,
|
|
269
|
+
)
|
|
270
|
+
mapping = subprocess.check_output(
|
|
271
|
+
["docker", "port", name, "8317/tcp"], text=True
|
|
272
|
+
).strip()
|
|
273
|
+
for _ in range(20):
|
|
274
|
+
try:
|
|
275
|
+
assert request("/v1/models", base="http://" + mapping)[0] == 401
|
|
276
|
+
key = cfg["access"]["api-keys"][0]
|
|
277
|
+
assert (
|
|
278
|
+
request("/v1/models", key, base="http://" + mapping)[0] == 200
|
|
279
|
+
)
|
|
280
|
+
print("Isolated runtime validation passed.")
|
|
281
|
+
return
|
|
282
|
+
except Exception:
|
|
283
|
+
time.sleep(1)
|
|
284
|
+
raise RuntimeError("staging failed")
|
|
285
|
+
finally:
|
|
286
|
+
run(["docker", "rm", "-f", name], stdout=subprocess.DEVNULL)
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
command = sys.argv[1] if len(sys.argv) > 1 else "status"
|
|
290
|
+
try:
|
|
291
|
+
if command == "validate":
|
|
292
|
+
validate()
|
|
293
|
+
staging(root / "state/runtime/config.yaml")
|
|
294
|
+
elif command == "start":
|
|
295
|
+
operation_lock = lock()
|
|
296
|
+
validate()
|
|
297
|
+
compose("up", "-d", "--wait")
|
|
298
|
+
health()
|
|
299
|
+
if (root / "state/endpoint.json").exists():
|
|
300
|
+
compose("--profile", "portable-endpoint", "up", "-d", "tailnet")
|
|
301
|
+
if (root / "state/public-endpoint-enabled").exists():
|
|
302
|
+
run([str(root / "scripts/public-endpoint"), "start"])
|
|
303
|
+
elif command == "stop":
|
|
304
|
+
operation_lock = lock()
|
|
305
|
+
compose(
|
|
306
|
+
"--profile", "portable-endpoint", "--profile", "public-endpoint", "stop"
|
|
307
|
+
)
|
|
308
|
+
elif command == "restart":
|
|
309
|
+
operation_lock = lock()
|
|
310
|
+
validate()
|
|
311
|
+
compose("restart", "proxy")
|
|
312
|
+
wait()
|
|
313
|
+
elif command == "health":
|
|
314
|
+
health()
|
|
315
|
+
elif command == "status":
|
|
316
|
+
compose("--profile", "portable-endpoint", "--profile", "public-endpoint", "ps")
|
|
317
|
+
health()
|
|
318
|
+
for f in [
|
|
319
|
+
"backup-success.json",
|
|
320
|
+
"backup-failed",
|
|
321
|
+
"backup-check-failed",
|
|
322
|
+
"backup-operation-failed",
|
|
323
|
+
"monitor-failed",
|
|
324
|
+
"gc-failed",
|
|
325
|
+
"gc-success.json",
|
|
326
|
+
]:
|
|
327
|
+
p = root / "state/status" / f
|
|
328
|
+
if p.exists():
|
|
329
|
+
print(f, p.read_text().strip())
|
|
330
|
+
elif command == "models":
|
|
331
|
+
for m in health():
|
|
332
|
+
print(m["id"])
|
|
333
|
+
elif command == "logs":
|
|
334
|
+
# Avoid emitting unreviewed upstream logs: redact every known credential, then token patterns.
|
|
335
|
+
import re, yaml
|
|
336
|
+
|
|
337
|
+
data = subprocess.check_output(
|
|
338
|
+
["docker", "compose", "logs", "--no-color", "--tail", "100", "proxy"],
|
|
339
|
+
stderr=subprocess.STDOUT,
|
|
340
|
+
text=True,
|
|
341
|
+
)
|
|
342
|
+
config = yaml.safe_load((root / "state/runtime/config.yaml").read_text())
|
|
343
|
+
values = []
|
|
344
|
+
|
|
345
|
+
def visit(obj):
|
|
346
|
+
if isinstance(obj, dict):
|
|
347
|
+
for k, v in obj.items():
|
|
348
|
+
if isinstance(v, str) and any(
|
|
349
|
+
x in k.lower() for x in ["key", "token", "secret", "password"]
|
|
350
|
+
):
|
|
351
|
+
values.append(v)
|
|
352
|
+
visit(v)
|
|
353
|
+
elif isinstance(obj, list):
|
|
354
|
+
for v in obj:
|
|
355
|
+
visit(v)
|
|
356
|
+
|
|
357
|
+
visit(config)
|
|
358
|
+
values.extend(config["access"]["api-keys"])
|
|
359
|
+
for f in (root / "state/secrets").iterdir():
|
|
360
|
+
if f.is_file() and f.stat().st_size < 10000:
|
|
361
|
+
try:
|
|
362
|
+
text = f.read_text().strip()
|
|
363
|
+
if f.suffix == ".json":
|
|
364
|
+
visit(json.loads(text))
|
|
365
|
+
else:
|
|
366
|
+
values.append(text)
|
|
367
|
+
except Exception:
|
|
368
|
+
pass
|
|
369
|
+
for f in (root / "state/runtime/auths").glob("*.json"):
|
|
370
|
+
try:
|
|
371
|
+
visit(json.loads(f.read_text()))
|
|
372
|
+
except Exception:
|
|
373
|
+
pass
|
|
374
|
+
for value in sorted(set(values), key=len, reverse=True):
|
|
375
|
+
if len(value) >= 6:
|
|
376
|
+
data = data.replace(value, "[REDACTED]")
|
|
377
|
+
data = re.sub(r'(?i)(bearer\s+)[^\s"\']+', r"\1[REDACTED]", data)
|
|
378
|
+
data = re.sub(r"(?:sk-|eyJ)[A-Za-z0-9_.-]{15,}", "[REDACTED]", data)
|
|
379
|
+
print(data)
|
|
380
|
+
elif command == "apply":
|
|
381
|
+
operation_lock = lock()
|
|
382
|
+
apply_config(pathlib.Path(sys.argv[2]).resolve())
|
|
383
|
+
elif command == "declared":
|
|
384
|
+
# Fleet-owned settings: template config keys and one model prefix per
|
|
385
|
+
# signed-in account. Check exits 3 when the live proxy differs;
|
|
386
|
+
# --apply replaces only those settings.
|
|
387
|
+
candidate = declared_candidate()
|
|
388
|
+
prefixes = pending_prefixes()
|
|
389
|
+
if candidate is None and not prefixes:
|
|
390
|
+
print("Declared settings match the live config.")
|
|
391
|
+
elif "--apply" not in sys.argv[2:]:
|
|
392
|
+
print("Declared settings differ from the live config.")
|
|
393
|
+
sys.exit(3)
|
|
394
|
+
else:
|
|
395
|
+
operation_lock = lock()
|
|
396
|
+
if candidate is not None:
|
|
397
|
+
with tempfile.NamedTemporaryFile(
|
|
398
|
+
"w", dir=root / "state/runtime", suffix=".yaml", delete=False
|
|
399
|
+
) as file:
|
|
400
|
+
file.write(candidate)
|
|
401
|
+
try:
|
|
402
|
+
os.chmod(file.name, 0o600)
|
|
403
|
+
# Restart-free: the proxy hot-reloads config.yaml. Restart
|
|
404
|
+
# only if the reload never serves the declared settings.
|
|
405
|
+
apply_config(pathlib.Path(file.name), restart=False)
|
|
406
|
+
if not hot_reloaded(declared_aliases()):
|
|
407
|
+
compose("restart", "proxy")
|
|
408
|
+
wait()
|
|
409
|
+
finally:
|
|
410
|
+
pathlib.Path(file.name).unlink(missing_ok=True)
|
|
411
|
+
if prefixes:
|
|
412
|
+
apply_prefixes(prefixes)
|
|
413
|
+
elif command in ["upgrade", "rollback"]:
|
|
414
|
+
sys.exit(
|
|
415
|
+
"Install a reviewed Git revision with bun run cliproxy install --revision REV; use --rollback for the recorded previous revision."
|
|
416
|
+
)
|
|
417
|
+
elif command == "add-account":
|
|
418
|
+
operation_lock = lock()
|
|
419
|
+
provider = sys.argv[2]
|
|
420
|
+
flags = {
|
|
421
|
+
"codex": "-codex-device-login",
|
|
422
|
+
"claude": "-claude-login",
|
|
423
|
+
"gemini": "-login",
|
|
424
|
+
"antigravity": "-antigravity-login",
|
|
425
|
+
"kimi": "-kimi-login",
|
|
426
|
+
"xai": "-xai-login",
|
|
427
|
+
"meta": "-meta-login",
|
|
428
|
+
"devin": "-devin-login",
|
|
429
|
+
}
|
|
430
|
+
if provider == "gemini":
|
|
431
|
+
sys.exit(
|
|
432
|
+
"This release has no Gemini CLI login flag. Use official dashboard OAuth/API key configuration."
|
|
433
|
+
)
|
|
434
|
+
flag = flags[provider]
|
|
435
|
+
compose(
|
|
436
|
+
"exec",
|
|
437
|
+
"proxy",
|
|
438
|
+
"/CLIProxyAPI/CLIProxyAPI",
|
|
439
|
+
"-config",
|
|
440
|
+
"/state/config.yaml",
|
|
441
|
+
flag,
|
|
442
|
+
"-no-browser",
|
|
443
|
+
)
|
|
444
|
+
elif command == "inference":
|
|
445
|
+
protocol, model = sys.argv[2:4]
|
|
446
|
+
key = (root / "state/secrets/client-key").read_text().strip()
|
|
447
|
+
if protocol == "responses":
|
|
448
|
+
path = "/v1/responses"
|
|
449
|
+
body = {
|
|
450
|
+
"model": model,
|
|
451
|
+
"input": "Reply only OK",
|
|
452
|
+
"stream": True,
|
|
453
|
+
"max_output_tokens": 32,
|
|
454
|
+
}
|
|
455
|
+
elif protocol == "messages":
|
|
456
|
+
path = "/v1/messages"
|
|
457
|
+
body = {
|
|
458
|
+
"model": model,
|
|
459
|
+
"messages": [{"role": "user", "content": "Reply only OK"}],
|
|
460
|
+
"stream": True,
|
|
461
|
+
"max_tokens": 16,
|
|
462
|
+
}
|
|
463
|
+
else:
|
|
464
|
+
path = "/v1/chat/completions"
|
|
465
|
+
body = {
|
|
466
|
+
"model": model,
|
|
467
|
+
"messages": [{"role": "user", "content": "Reply only OK"}],
|
|
468
|
+
"stream": True,
|
|
469
|
+
"max_tokens": 16,
|
|
470
|
+
}
|
|
471
|
+
code, data = request(path, key, body)
|
|
472
|
+
assert code == 200 and b"data:" in data, (
|
|
473
|
+
"Streaming inference failed (response suppressed)"
|
|
474
|
+
)
|
|
475
|
+
print(
|
|
476
|
+
"Streaming transport returned SSE. Check protocol completion and tools with the actual client before production use."
|
|
477
|
+
)
|
|
478
|
+
elif command == "portable-login":
|
|
479
|
+
compose("--profile", "portable-endpoint", "up", "-d", "tailnet")
|
|
480
|
+
compose(
|
|
481
|
+
"exec",
|
|
482
|
+
"tailnet",
|
|
483
|
+
"tailscale",
|
|
484
|
+
"--socket=/tmp/tailscaled.sock",
|
|
485
|
+
"up",
|
|
486
|
+
"--hostname=cliproxy-fleet",
|
|
487
|
+
"--advertise-tags=tag:ai-proxy",
|
|
488
|
+
"--accept-dns=false",
|
|
489
|
+
)
|
|
490
|
+
elif command == "activate":
|
|
491
|
+
health()
|
|
492
|
+
compose(
|
|
493
|
+
"exec",
|
|
494
|
+
"tailnet",
|
|
495
|
+
"tailscale",
|
|
496
|
+
"--socket=/tmp/tailscaled.sock",
|
|
497
|
+
"serve",
|
|
498
|
+
"--service=svc:ai-proxy",
|
|
499
|
+
"--https=443",
|
|
500
|
+
"http://proxy:8317",
|
|
501
|
+
)
|
|
502
|
+
compose(
|
|
503
|
+
"exec",
|
|
504
|
+
"tailnet",
|
|
505
|
+
"tailscale",
|
|
506
|
+
"--socket=/tmp/tailscaled.sock",
|
|
507
|
+
"serve",
|
|
508
|
+
"status",
|
|
509
|
+
)
|
|
510
|
+
elif command == "drain":
|
|
511
|
+
compose(
|
|
512
|
+
"exec",
|
|
513
|
+
"tailnet",
|
|
514
|
+
"tailscale",
|
|
515
|
+
"--socket=/tmp/tailscaled.sock",
|
|
516
|
+
"serve",
|
|
517
|
+
"drain",
|
|
518
|
+
"svc:ai-proxy",
|
|
519
|
+
)
|
|
520
|
+
else:
|
|
521
|
+
sys.exit(
|
|
522
|
+
"Commands: start stop restart status health models logs validate apply FILE declared [--apply] add-account PROVIDER inference PROTOCOL MODEL portable-login activate drain"
|
|
523
|
+
)
|
|
524
|
+
except Exception:
|
|
525
|
+
sys.exit(
|
|
526
|
+
"Operation failed; secrets and upstream error responses suppressed. Check sanitized logs and service status."
|
|
527
|
+
)
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Run the OpenTofu-owned public connector; never provision cloud resources."""
|
|
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
|
+
import importlib.util
|
|
15
|
+
import json
|
|
16
|
+
import os
|
|
17
|
+
import pathlib
|
|
18
|
+
import subprocess
|
|
19
|
+
import urllib.error
|
|
20
|
+
import urllib.request
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def save_token(path, value):
|
|
24
|
+
"""Atomically refreshes the credential; returns whether it changed."""
|
|
25
|
+
if not isinstance(value, str) or not value.strip() or "\n" in value.strip():
|
|
26
|
+
raise ValueError("Missing or invalid connector credential")
|
|
27
|
+
try:
|
|
28
|
+
if path.read_text() == value.strip() + "\n":
|
|
29
|
+
return False
|
|
30
|
+
except OSError:
|
|
31
|
+
pass
|
|
32
|
+
temporary = path.with_name(path.name + ".new")
|
|
33
|
+
descriptor = os.open(temporary, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
|
|
34
|
+
with os.fdopen(descriptor, "w") as file:
|
|
35
|
+
file.write(value.strip() + "\n")
|
|
36
|
+
temporary.chmod(0o600)
|
|
37
|
+
if os.geteuid() == 0:
|
|
38
|
+
os.chown(temporary, 1000, 1000)
|
|
39
|
+
os.replace(temporary, path)
|
|
40
|
+
return True
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def connector_running(compose):
|
|
44
|
+
result = subprocess.run(
|
|
45
|
+
compose + ["ps", "--status", "running", "--quiet", "cloudflared"],
|
|
46
|
+
capture_output=True,
|
|
47
|
+
text=True,
|
|
48
|
+
)
|
|
49
|
+
return result.returncode == 0 and bool(result.stdout.strip())
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def verify(root, endpoint):
|
|
53
|
+
def request(path, key=None):
|
|
54
|
+
headers = {"User-Agent": "cliproxy-fleet/1.0"}
|
|
55
|
+
if key:
|
|
56
|
+
headers["Authorization"] = "Bearer " + key
|
|
57
|
+
try:
|
|
58
|
+
with urllib.request.urlopen(
|
|
59
|
+
urllib.request.Request(endpoint + path, headers=headers), timeout=30
|
|
60
|
+
) as response:
|
|
61
|
+
return response.status, response.read()
|
|
62
|
+
except urllib.error.HTTPError as error:
|
|
63
|
+
return error.code, b""
|
|
64
|
+
|
|
65
|
+
client = (root / "state/secrets/client-key").read_text().strip()
|
|
66
|
+
admin = (root / "state/secrets/management-key").read_text().strip()
|
|
67
|
+
assert request("/v1/models")[0] == 401
|
|
68
|
+
status, body = request("/v1/models", client)
|
|
69
|
+
assert status == 200 and json.loads(body).get("data")
|
|
70
|
+
assert request("/v0/management/config")[0] in (401, 403)
|
|
71
|
+
assert request("/v0/management/config", client)[0] in (401, 403)
|
|
72
|
+
assert request("/v0/management/config", admin)[0] == 200
|
|
73
|
+
assert request("/management.html")[0] == 200
|
|
74
|
+
print(
|
|
75
|
+
"PASS: public HTTPS, authenticated models, separate management key and dashboard."
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def main():
|
|
80
|
+
os.umask(0o077)
|
|
81
|
+
root = cliproxy_host.default_root()
|
|
82
|
+
command = sys.argv[1] if len(sys.argv) == 2 else "verify"
|
|
83
|
+
if command not in ["enable", "start", "disable", "verify"]:
|
|
84
|
+
raise ValueError("Use enable, start, disable or verify")
|
|
85
|
+
marker = root / "state/public-endpoint-enabled"
|
|
86
|
+
if command == "verify":
|
|
87
|
+
verify(root, fleet_config.endpoint())
|
|
88
|
+
return
|
|
89
|
+
compose = [str(root / "scripts/docker"), "compose", "--profile", "public-endpoint"]
|
|
90
|
+
if command == "disable":
|
|
91
|
+
subprocess.run(compose + ["stop", "cloudflared"], check=True)
|
|
92
|
+
marker.unlink(missing_ok=True)
|
|
93
|
+
return
|
|
94
|
+
spec = importlib.util.spec_from_file_location(
|
|
95
|
+
"vault_access", root / "scripts/vault-access.py"
|
|
96
|
+
)
|
|
97
|
+
vault = importlib.util.module_from_spec(spec)
|
|
98
|
+
spec.loader.exec_module(vault)
|
|
99
|
+
vault.renew()
|
|
100
|
+
location = fleet_config.vault()
|
|
101
|
+
value = vault.request(location["tunnel"], vault.token())["data"]["data"][
|
|
102
|
+
location["tunnel_field"]
|
|
103
|
+
]
|
|
104
|
+
changed = save_token(root / "state/secrets/tunnel-token", value)
|
|
105
|
+
# Recreate only when the bind mount must see a refreshed credential (or the
|
|
106
|
+
# connector is down): a needless recreate drops the public endpoint on
|
|
107
|
+
# every proxy start or update.
|
|
108
|
+
if changed or not connector_running(compose):
|
|
109
|
+
subprocess.run(
|
|
110
|
+
compose + ["up", "-d", "--force-recreate", "cloudflared"], check=True
|
|
111
|
+
)
|
|
112
|
+
if command == "enable":
|
|
113
|
+
marker.touch(mode=0o600)
|
|
114
|
+
if os.geteuid() == 0:
|
|
115
|
+
os.chown(marker, 1000, 1000)
|
|
116
|
+
print(
|
|
117
|
+
"Public connector running at " + fleet_config.endpoint() + "; credential withheld."
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
if __name__ == "__main__":
|
|
122
|
+
try:
|
|
123
|
+
main()
|
|
124
|
+
except Exception:
|
|
125
|
+
raise SystemExit(
|
|
126
|
+
"Public endpoint operation failed; check Vault access, deployment and DNS. Secret values suppressed."
|
|
127
|
+
) from None
|