@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,105 @@
|
|
|
1
|
+
/** Persistent shims make the same tool commands work in interactive shells and agents. */
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { dirname, join } from 'node:path';
|
|
5
|
+
import { spawnSync } from 'node:child_process';
|
|
6
|
+
|
|
7
|
+
export function appendOnce(path: string, block: string): void {
|
|
8
|
+
const current = existsSync(path) ? readFileSync(path, 'utf8') : '';
|
|
9
|
+
if (current.includes(block.trim())) return;
|
|
10
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
11
|
+
writeFileSync(path, `${current}\n${block}\n`);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function miseDataDir(
|
|
15
|
+
platform = process.platform,
|
|
16
|
+
env = process.env,
|
|
17
|
+
home = homedir()
|
|
18
|
+
): string {
|
|
19
|
+
const fallback =
|
|
20
|
+
platform === 'win32'
|
|
21
|
+
? env['LOCALAPPDATA'] || join(home, 'AppData', 'Local')
|
|
22
|
+
: join(home, '.local', 'share');
|
|
23
|
+
return env['MISE_DATA_DIR'] || join(env['XDG_DATA_HOME'] || fallback, 'mise');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function persistWindowsPath(bin: string, shims: string): void {
|
|
27
|
+
const script =
|
|
28
|
+
"$paths = @($env:WORKSPACE_TOOL_SHIMS, $env:WORKSPACE_TOOL_BIN); $old = [Environment]::GetEnvironmentVariable('Path', 'User'); $new = (($paths + ($old -split ';') | Where-Object { $_ } | Select-Object -Unique) -join ';'); [Environment]::SetEnvironmentVariable('Path', $new, 'User')";
|
|
29
|
+
const result = spawnSync(
|
|
30
|
+
'powershell.exe',
|
|
31
|
+
['-NoProfile', '-Command', script],
|
|
32
|
+
{
|
|
33
|
+
stdio: 'inherit',
|
|
34
|
+
env: {
|
|
35
|
+
...process.env,
|
|
36
|
+
WORKSPACE_TOOL_BIN: bin,
|
|
37
|
+
WORKSPACE_TOOL_SHIMS: shims,
|
|
38
|
+
},
|
|
39
|
+
}
|
|
40
|
+
);
|
|
41
|
+
if (result.status !== 0)
|
|
42
|
+
throw new Error(
|
|
43
|
+
'Could not persist the user PATH. See docs/machine-setup.md.'
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function configurePosixShellPath(
|
|
48
|
+
bin: string,
|
|
49
|
+
shims: string,
|
|
50
|
+
home = homedir(),
|
|
51
|
+
zdotdir = process.env['ZDOTDIR'] || home
|
|
52
|
+
): void {
|
|
53
|
+
const quote = (value: string) => `'${value.replaceAll("'", "'\\''")}'`;
|
|
54
|
+
const paths = quote(`${shims}:${bin}`);
|
|
55
|
+
const legacy = `# dev-fleet toolchain\nexport PATH=${paths}:"$PATH"`;
|
|
56
|
+
const block = `# dev-fleet toolchain\ncase ":$PATH:" in\n *${quote(`:${shims}:${bin}:`)}*) ;;\n *) export PATH=${paths}:"$PATH" ;;\nesac`;
|
|
57
|
+
const targets = new Set([
|
|
58
|
+
join(home, '.profile'),
|
|
59
|
+
...['.bash_profile', '.bash_login']
|
|
60
|
+
.map((name) => join(home, name))
|
|
61
|
+
.filter(existsSync),
|
|
62
|
+
join(home, '.bashrc'),
|
|
63
|
+
join(zdotdir, '.zshenv'),
|
|
64
|
+
join(zdotdir, '.zshrc'),
|
|
65
|
+
]);
|
|
66
|
+
// Version managers and vendor installers (nvm, a native Claude installer's
|
|
67
|
+
// ~/.local/bin) prepend their own directories later in these files; the
|
|
68
|
+
// last line puts the pinned shims back in front, so every shell and the
|
|
69
|
+
// apps that inherit its PATH (T3) run the locked tool versions.
|
|
70
|
+
// Guarded like the block above so nested shells do not grow PATH.
|
|
71
|
+
const unguarded = `${TAIL_MARKER}\nexport PATH=${quote(shims)}:"$PATH"\n`;
|
|
72
|
+
const tail = `${TAIL_MARKER}\ncase "$PATH" in\n ${quote(shims)}:*) ;;\n *) export PATH=${quote(shims)}:"$PATH" ;;\nesac\n`;
|
|
73
|
+
for (const path of targets) {
|
|
74
|
+
const current = existsSync(path) ? readFileSync(path, 'utf8') : '';
|
|
75
|
+
const body = current
|
|
76
|
+
.replace(`${block}\n`, '')
|
|
77
|
+
.replace(legacy, '')
|
|
78
|
+
.split(tail)
|
|
79
|
+
.join('')
|
|
80
|
+
.split(unguarded)
|
|
81
|
+
.join('');
|
|
82
|
+
const next = `${block}\n${body.replace(/\n*$/, '\n')}${tail}`;
|
|
83
|
+
if (next === current) continue;
|
|
84
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
85
|
+
writeFileSync(path, next);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const TAIL_MARKER = '# pinned tool shims first (dev-fleet, keep this last)';
|
|
90
|
+
|
|
91
|
+
export function configureShellPath(): void {
|
|
92
|
+
const bin = join(homedir(), '.local', 'bin');
|
|
93
|
+
const shims = process.env['MISE_SHIMS_DIR'] || join(miseDataDir(), 'shims');
|
|
94
|
+
if (process.platform === 'win32') {
|
|
95
|
+
persistWindowsPath(bin, shims);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
configurePosixShellPath(bin, shims);
|
|
99
|
+
const fishQuote = (value: string) =>
|
|
100
|
+
`'${value.replaceAll('\\', '\\\\').replaceAll("'", "\\'")}'`;
|
|
101
|
+
appendOnce(
|
|
102
|
+
join(homedir(), '.config/fish/config.fish'),
|
|
103
|
+
`# dev-fleet toolchain\nfish_add_path --prepend ${fishQuote(shims)} ${fishQuote(bin)}`
|
|
104
|
+
);
|
|
105
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""Verify the pinned T3 archive against its user-owned installation.
|
|
2
|
+
|
|
3
|
+
Release-builder numeric identities are not portable. All installed members must
|
|
4
|
+
instead belong to the invoking development user; content, modes and links must
|
|
5
|
+
still match. This reads the archive and installation without modifying either.
|
|
6
|
+
"""
|
|
7
|
+
import os
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
import stat
|
|
10
|
+
import sys
|
|
11
|
+
import tarfile
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def verify(archive, root):
|
|
15
|
+
root = Path(root).resolve()
|
|
16
|
+
with tarfile.open(archive, 'r:gz') as source:
|
|
17
|
+
for member in source:
|
|
18
|
+
path = root / member.name
|
|
19
|
+
if not path.parent.resolve().is_relative_to(root):
|
|
20
|
+
return False
|
|
21
|
+
observed = path.lstat()
|
|
22
|
+
if observed.st_uid != os.getuid():
|
|
23
|
+
return False
|
|
24
|
+
if member.issym():
|
|
25
|
+
if not stat.S_ISLNK(observed.st_mode) or os.readlink(path) != member.linkname:
|
|
26
|
+
return False
|
|
27
|
+
elif member.isdir():
|
|
28
|
+
if not stat.S_ISDIR(observed.st_mode):
|
|
29
|
+
return False
|
|
30
|
+
elif member.isfile():
|
|
31
|
+
if not stat.S_ISREG(observed.st_mode) or observed.st_size != member.size:
|
|
32
|
+
return False
|
|
33
|
+
if stat.S_IMODE(observed.st_mode) != member.mode:
|
|
34
|
+
return False
|
|
35
|
+
with source.extractfile(member) as expected, path.open('rb') as actual:
|
|
36
|
+
while chunk := expected.read(1024 * 1024):
|
|
37
|
+
if actual.read(len(chunk)) != chunk:
|
|
38
|
+
return False
|
|
39
|
+
else:
|
|
40
|
+
return False
|
|
41
|
+
return True
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
if __name__ == '__main__':
|
|
45
|
+
try:
|
|
46
|
+
valid = verify(sys.argv[1], sys.argv[2])
|
|
47
|
+
except (OSError, ValueError, tarfile.TarError):
|
|
48
|
+
valid = False
|
|
49
|
+
sys.exit(0 if valid else 1)
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { syncWorkerGithub } from './worker-github';
|
|
2
|
+
/** Private scheduled job. The worker retains its own token; raw auth output is never logged. */
|
|
3
|
+
import { spawnSync } from 'node:child_process';
|
|
4
|
+
import { resolveVaultConfig, usesVault } from '../src/vault-config';
|
|
5
|
+
|
|
6
|
+
export function renewWorkerVault(
|
|
7
|
+
run: (args: string[]) => { status: number | null; stdout: string | null } = (
|
|
8
|
+
args: string[]
|
|
9
|
+
) => spawnSync(args[0]!, args.slice(1), { encoding: 'utf8', timeout: 30_000 })
|
|
10
|
+
): boolean {
|
|
11
|
+
if (!usesVault()) return true;
|
|
12
|
+
const { addr } = resolveVaultConfig();
|
|
13
|
+
const result = run([
|
|
14
|
+
'vault',
|
|
15
|
+
'token',
|
|
16
|
+
'renew',
|
|
17
|
+
`-address=${addr}`,
|
|
18
|
+
'-format=json',
|
|
19
|
+
'-increment=24h',
|
|
20
|
+
]);
|
|
21
|
+
if (result.status !== 0) return false;
|
|
22
|
+
try {
|
|
23
|
+
const data: unknown = JSON.parse(result.stdout ?? '');
|
|
24
|
+
return (
|
|
25
|
+
typeof data === 'object' &&
|
|
26
|
+
data !== null &&
|
|
27
|
+
'auth' in data &&
|
|
28
|
+
typeof data.auth === 'object' &&
|
|
29
|
+
data.auth !== null &&
|
|
30
|
+
'lease_duration' in data.auth &&
|
|
31
|
+
typeof data.auth.lease_duration === 'number' &&
|
|
32
|
+
data.auth.lease_duration > 0
|
|
33
|
+
);
|
|
34
|
+
} catch {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
if (import.meta.main) {
|
|
39
|
+
// One JSON line per event; never a token or raw auth output.
|
|
40
|
+
const log = {
|
|
41
|
+
info: (event: string, fields: Record<string, unknown>) =>
|
|
42
|
+
process.stdout.write(
|
|
43
|
+
JSON.stringify({ name: 'dev-fleet-worker-auth', event, ...fields }) +
|
|
44
|
+
'\n'
|
|
45
|
+
),
|
|
46
|
+
};
|
|
47
|
+
const started = Date.now();
|
|
48
|
+
log.info('job_started', {
|
|
49
|
+
jobId: 'vault-self-renewal',
|
|
50
|
+
operation: 'vault.token.renew',
|
|
51
|
+
});
|
|
52
|
+
const vault = renewWorkerVault();
|
|
53
|
+
const github = vault && syncWorkerGithub();
|
|
54
|
+
const ok = vault && github;
|
|
55
|
+
log.info('job_finished', {
|
|
56
|
+
jobId: 'vault-self-renewal',
|
|
57
|
+
operation: 'vault.token.renew',
|
|
58
|
+
vaultAuthenticated: vault,
|
|
59
|
+
githubAuthenticated: github,
|
|
60
|
+
outcome: ok ? 'success' : 'authentication_required',
|
|
61
|
+
durationMs: Date.now() - started,
|
|
62
|
+
retryDelayMs: 600000,
|
|
63
|
+
});
|
|
64
|
+
process.exitCode = ok ? 0 : 1;
|
|
65
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/** Acquire declared service credentials from this worker's Vault session; never copy a login directory. */
|
|
2
|
+
import { spawnSync } from 'node:child_process';
|
|
3
|
+
import { resolveVaultConfig, usesVault } from '../src/vault-config';
|
|
4
|
+
import { fleet as definition } from '../src/fleet-config';
|
|
5
|
+
|
|
6
|
+
export type AuthRunner = (
|
|
7
|
+
args: string[],
|
|
8
|
+
options?: { input?: string; env?: NodeJS.ProcessEnv }
|
|
9
|
+
) => { status: number | null; stdout: string | null };
|
|
10
|
+
const execute: AuthRunner = (args, options = {}) =>
|
|
11
|
+
spawnSync(args[0]!, args.slice(1), {
|
|
12
|
+
encoding: 'utf8',
|
|
13
|
+
timeout: 30_000,
|
|
14
|
+
...options,
|
|
15
|
+
});
|
|
16
|
+
export function syncWorkerGithub(run: AuthRunner = execute): boolean {
|
|
17
|
+
const expected = definition.worker!.githubLogin;
|
|
18
|
+
const current = run(['gh', 'api', 'user', '--jq', '.login']);
|
|
19
|
+
if (current.status === 0 && current.stdout?.trim() === expected) return true;
|
|
20
|
+
// The service token is kept in the shared Vault document only.
|
|
21
|
+
if (!usesVault()) return false;
|
|
22
|
+
const { addr, mount, project, config } = resolveVaultConfig();
|
|
23
|
+
const secret = run([
|
|
24
|
+
'vault',
|
|
25
|
+
'kv',
|
|
26
|
+
'get',
|
|
27
|
+
`-address=${addr}`,
|
|
28
|
+
'-field=GITHUB_TOKEN',
|
|
29
|
+
`${mount}/${project}/${config}`,
|
|
30
|
+
]);
|
|
31
|
+
const token = secret.stdout?.trim() ?? '';
|
|
32
|
+
if (secret.status !== 0 || !/^[\x21-\x7e]{16,4096}$/.test(token))
|
|
33
|
+
return false;
|
|
34
|
+
const validated = run(['gh', 'api', 'user', '--jq', '.login'], {
|
|
35
|
+
env: { ...process.env, GH_TOKEN: token, GITHUB_TOKEN: undefined },
|
|
36
|
+
});
|
|
37
|
+
if (validated.status !== 0 || validated.stdout?.trim() !== expected)
|
|
38
|
+
return false;
|
|
39
|
+
const login = run(
|
|
40
|
+
['gh', 'auth', 'login', '--hostname', 'github.com', '--with-token'],
|
|
41
|
+
{
|
|
42
|
+
input: token + '\n',
|
|
43
|
+
env: { ...process.env, GH_TOKEN: undefined, GITHUB_TOKEN: undefined },
|
|
44
|
+
}
|
|
45
|
+
);
|
|
46
|
+
return login.status === 0;
|
|
47
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/** A worker installs only this declared subset; every version still comes from mise.lock. */
|
|
2
|
+
export function workerTools<T>(
|
|
3
|
+
tools: Record<string, T>,
|
|
4
|
+
selected: unknown
|
|
5
|
+
): Record<string, T> {
|
|
6
|
+
if (typeof selected === 'string') selected = selected.split(',');
|
|
7
|
+
if (
|
|
8
|
+
!Array.isArray(selected) ||
|
|
9
|
+
!selected.length ||
|
|
10
|
+
new Set(selected).size !== selected.length ||
|
|
11
|
+
!selected.every(
|
|
12
|
+
(key) => typeof key === 'string' && Object.hasOwn(tools, key)
|
|
13
|
+
) ||
|
|
14
|
+
!['bun', 'node', 'gh', 'python', 'uv', 'docker-cli', 'vault'].every((key) =>
|
|
15
|
+
selected.includes(key)
|
|
16
|
+
)
|
|
17
|
+
)
|
|
18
|
+
throw new Error(
|
|
19
|
+
'Invalid worker tool profile: all required tools need authoritative mise pins.'
|
|
20
|
+
);
|
|
21
|
+
return Object.fromEntries(
|
|
22
|
+
selected.map((key: string) => [key, tools[key]!])
|
|
23
|
+
) as Record<string, T>;
|
|
24
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# CLIProxyAPI package
|
|
2
|
+
|
|
3
|
+
This package owns reproducible local deployment and runtime operations. The proxy runs only on the fleet **manager** (`manager.id` in `fleet.json` v3: a macOS machine with Docker Desktop + per-user launchd agents, never sudo). **Workers** are clients and never host it; `fleet_sync.py` reports a worker install (`PROXY_DUPLICATE_OWNER` / `PROXY_DECOMMISSION_PENDING`) and never stops it. The Linux systemd backend (`/opt/cliproxy-fleet`) is legacy, kept only to fence, back up and decommission the previous host via `docs/migration.md`. Keep platform dispatch in `scripts/cliproxy_host.py` explicit and small.
|
|
4
|
+
|
|
5
|
+
Authored code/config templates must be committed here before installation. Never patch an installed root (`~/Library/Application Support/cliproxy-fleet` or `/opt/cliproxy-fleet`); its operational paths, LaunchAgents and units must link to clean Git-tracked code at the recorded deployment commit. Do not render executable wrappers, plists or units, or copy code outside source control; `launchd/*.plist` are tracked as-is and reference the default manager root via `$HOME`. Client commands must link to the tracked launcher. Preserve `state/` during updates, and keep it out of Git. Secrets live in Vault and encrypted backups.
|
|
6
|
+
|
|
7
|
+
Do not add cloud provisioning, tailnet policy mutation, Vault writes or secret seeding here. Those belong to the repository's OpenTofu roots. Runtime login, token renewal, local advertisement of existing Services, secret reads and backup operations are allowed. Never print credentials or activate a second copy of provider/Tailscale state during restore tests.
|
|
8
|
+
|
|
9
|
+
Check with `bun run --filter @pkgs/cliproxy-fleet lint` and `test`. Tests must be offline and must not read the production Vault token or deployment; fake `launchctl`, `scutil` and Docker Desktop detection. Installation uses committed blobs and records a manifest; cover state preservation and drift detection when changing that boundary.
|
|
10
|
+
|
|
11
|
+
Model health lives in `models.json` (declarative) and `scripts/modelhealth/` (generic engines); read `docs/model-health.md`. Adding a model, capability or fault scenario is a `models.json` change, never model-specific code. Never report an untested capability as healthy, never retry inference, never log keys or prompt text, never send management keys from health checks, and keep live inference out of offline tests (use the emulator). Do not claim live health without a successful `live`-target report. Routing is fill-first (drain one account, then the next); keep `config.template.yaml`, `models.json` `routing`, the emulator and `validate-config --require-fill-first` in step, and never claim the live proxy uses it until the host monitor is clean.
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
# CLIProxyAPI fleet service
|
|
2
|
+
|
|
3
|
+
`@pkgs/cliproxy-fleet` owns the source for the authenticated CLIProxyAPI deployment. The package contains Compose configuration, immutable upstream image pins, launchd agents for the manager, legacy systemd units, operational scripts, client bootstrap code and recovery procedures. It is private and is not published to npm or as a custom container image.
|
|
4
|
+
|
|
5
|
+
## Deployment diagram
|
|
6
|
+
|
|
7
|
+

|
|
8
|
+
|
|
9
|
+
[Open the scalable diagram](docs/deployment.svg).
|
|
10
|
+
|
|
11
|
+
## Fleet roles: the manager hosts the proxy
|
|
12
|
+
|
|
13
|
+
The fleet has exactly two roles (see `fleet.json`, version 3). The **manager** is the singleton macOS machine (`manager.id`). It runs fleet-sync and hosts every singleton service, including this proxy, with Docker Desktop and per-user launchd agents (no sudo). **Workers** (the Ubuntu boxes) are stateless clients: they use the endpoint and never host the proxy. There is no separate `proxy.owner`; the proxy owner is always `manager.id`.
|
|
14
|
+
|
|
15
|
+
Availability follows the manager: while it sleeps, is offline or is logged out, workers have no proxy (use plain native clients). Fleet policy keeps the manager awake on AC power (`manager.neverSleepOnPower`), and the manager must stay logged in because launchd agents and Docker Desktop run in the user session. Enable Docker Desktop's **Start Docker Desktop when you sign in**.
|
|
16
|
+
|
|
17
|
+
The proxy still runs on the Linux worker `worker-one` (systemd, `/opt/cliproxy-fleet`) until the operator-approved move in [migration](docs/migration.md#move-the-proxy-from-a-linux-worker-to-the-manager). The Linux systemd backend is legacy: keep it only to fence, back up and decommission that host.
|
|
18
|
+
|
|
19
|
+
## Source and installed artifacts
|
|
20
|
+
|
|
21
|
+
The only executable source is Git-tracked code in `crvouga/dev-fleet`. Installation creates a detached sparse Git worktree at `INSTALL_ROOT/revisions/COMMIT`; operational paths under `INSTALL_ROOT` and the launchd agents (or legacy systemd units) are links into that worktree. `INSTALL_ROOT` is `~/Library/Application Support/cliproxy-fleet` on the manager and `/opt/cliproxy-fleet` on the legacy Linux host; `CLIPROXY_ROOT` overrides it for commands (the tracked LaunchAgents always use the default manager root). No executable source is rendered or copied outside Git. Client launchers are also links to the tracked `clients/launcher.py` in a checkout.
|
|
22
|
+
|
|
23
|
+
`INSTALLATION.json` records the worktree, commit and hashes. `audit` verifies Git tracking, a clean deployed checkout, link targets, unit links (`~/Library/LaunchAgents` plus loaded `gui/$UID` agents on the manager; `/etc/systemd/system` on Linux), content hashes and unexpected code. Startup and monitoring enforce it. Uncommitted code is never installed; keep the original Git repository/common object database available, or rebuild a deployment worktree from a fresh clone during recovery.
|
|
24
|
+
|
|
25
|
+
- **Git:** all executable code, Compose/templates, immutable image pins, unit definitions, clients, tests and runbooks.
|
|
26
|
+
- **Vault:** provider-independent credentials and existing recovery keys; never commit secrets.
|
|
27
|
+
- **Encrypted R2 snapshots:** live provider/config state, dedicated Tailscale identity and source-revision metadata.
|
|
28
|
+
- **Generated non-code data:** private client settings, installation metadata and `.env` image-pin data; their generators are tracked. Runtime data remains separate under `state/`.
|
|
29
|
+
|
|
30
|
+
## Stable public HTTPS
|
|
31
|
+
|
|
32
|
+
The canonical public API is `https://cli-proxy.example.com/v1` and the dashboard is `https://cli-proxy.example.com/management.html`. Clients send the existing client API key as a bearer token; management requires its separate key. No browser login or Tailscale client is required for this hostname. The existing private `https://ai-proxy.tail0a1b2c.ts.net` endpoint remains available.
|
|
33
|
+
|
|
34
|
+
The foundation OpenTofu root owns the Cloudflare tunnel, remotely managed ingress and proxied DNS. The vault root owns the isolated `secret/cliproxy-fleet/tunnel` document. Both are declared in the canonical inventory. No local script provisions them. The pinned cloudflared container shares the proxy's Compose network and connects outbound, with no additional public host port.
|
|
35
|
+
|
|
36
|
+
After the reviewed main-branch deployment creates these resources, enable the local connector:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
devfleet cliproxy public-endpoint enable
|
|
40
|
+
devfleet cliproxy public-endpoint verify
|
|
41
|
+
python3 -B cliproxy/clients/bootstrap.py \
|
|
42
|
+
--endpoint https://cli-proxy.example.com --model gpt-6.1-sol
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
`enable` records runtime intent; subsequent service starts fetch the current token from Vault before recreating the connector. The token is mounted from a private file, never passed in arguments or copied into source. It is excluded from backups because recovery fetches it from Vault. To reload a rotated token, run `devfleet cliproxy public-endpoint start`. `disable` stops only the public connector and removes the startup marker. The private endpoint is independent.
|
|
46
|
+
|
|
47
|
+
This hostname routes to whichever host runs the connector (the manager after the move); it does not move compute into Railway or provide availability when that host is off. During migration, stop the old host, restore on the manager, and enable the connector there. DNS remains unchanged. Never run competing proxy/provider-state copies.
|
|
48
|
+
|
|
49
|
+
## Install, update and rollback
|
|
50
|
+
|
|
51
|
+
Manager (macOS) requirements: Docker Desktop (running) and the shared mise toolchain. `cliproxy/scripts/install-dependencies` (never sudo on macOS) builds `INSTALL_ROOT/tools`: a venv on the pinned mise Python with hash-locked PyYAML (`requirements.lock`) plus the pinned, checksum-locked `restic` from `mise.lock`. Every LaunchAgent puts `tools/bin` first on `PATH`, so jobs never depend on whatever happens to be installed on the Mac. Homebrew is not used. Legacy Linux: `sudo cliproxy/scripts/install-dependencies` installs the Ubuntu packages.
|
|
52
|
+
|
|
53
|
+
From the repository root on the manager, after committing/reviewing the package revision:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
devfleet cliproxy install --dry-run
|
|
57
|
+
devfleet cliproxy install --revision HEAD
|
|
58
|
+
devfleet cliproxy audit
|
|
59
|
+
devfleet cliproxy status
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
On the manager the installer runs as the logged-in user (never sudo). It links the tracked `launchd/*.plist` agents into `~/Library/LaunchAgents`, reloads them with `launchctl bootout`/`enable`/`bootstrap gui/$UID`, and starts the stack through the same `service-start` script the service agent runs. It records the installing user's UID/GID in the generated `.env` so containers own their bind-mounted state; the legacy Linux host keeps UID/GID 1000. It stops with `DOCKER_DESKTOP_NOT_RUNNING` (or `DOCKER_CLI_MISSING`) when Docker Desktop is unavailable. On Linux the installer still uses sudo and systemd.
|
|
63
|
+
|
|
64
|
+
| launchd label (manager) | systemd unit (legacy Linux) | Schedule |
|
|
65
|
+
| ------------------------------------ | ----------------------------- | ------------------------------- |
|
|
66
|
+
| `dev.devfleet.cliproxy-fleet` | `cliproxy-fleet.service` | at load/login; retried on error |
|
|
67
|
+
| `dev.devfleet.cliproxy-monitor` | `cliproxy-monitor.timer` | every 5 minutes |
|
|
68
|
+
| `dev.devfleet.cliproxy-vault-renew` | `cliproxy-vault-renew.timer` | at load, then every 15 minutes |
|
|
69
|
+
| `dev.devfleet.cliproxy-backup` | `cliproxy-backup.timer` | minutes 0, 15, 30, 45 |
|
|
70
|
+
| `dev.devfleet.cliproxy-backup-check` | `cliproxy-backup-check.timer` | Sunday 05:00 |
|
|
71
|
+
| `dev.devfleet.cliproxy-gc` | `cliproxy-gc.timer` | Sunday 04:00 |
|
|
72
|
+
|
|
73
|
+
Backup, backup-check and GC agents load only after `recover`/`verify-recovery` confirms the independent Restic password. launchd has no stop hook: `devfleet cliproxy stop` stops containers; `launchctl bootout gui/$UID/LABEL` unloads an agent. launchd runs missed calendar jobs after wake but has no jitter or GC retry; failures stay visible through status markers and the monitor.
|
|
74
|
+
|
|
75
|
+
The installer preserves the live configuration and credentials, refreshes managed files/units, and restarts only this stack. It never provisions cloud resources or writes Vault secrets. On a fresh host, first use the restore procedure below; do not create a competing active instance with copied credentials.
|
|
76
|
+
|
|
77
|
+
To update, change the package/image pins in Git, pass package/repository checks, commit, and install the selected revision. To undo a Git revision/image update:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
devfleet cliproxy install --rollback
|
|
81
|
+
# Or explicitly install a previously reviewed package commit:
|
|
82
|
+
devfleet cliproxy install --revision PREVIOUS_COMMIT
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
The previous installation revision is recorded separately. Image rollback cannot undo application state migrations; restore a compatible pre-upgrade snapshot when needed. `.env` is generated, so image changes go through `image.lock.json`, not local edits.
|
|
86
|
+
|
|
87
|
+
## Architecture and endpoints
|
|
88
|
+
|
|
89
|
+
One upstream CLIProxyAPI v8.0.55 container runs as the manager user's UID/GID (1000 on the legacy Linux host) with a read-only root filesystem, no extra capabilities, bounded CPU/memory/processes and rotated Docker logs. Only `127.0.0.1:8317` is published. A separate nonprivileged userspace Tailscale container proxies over the Compose bridge and keeps its own tagged identity. It does not replace the manager's own Tailscale identity or change SSH/firewall settings. The upstream official dashboard is pinned and checksum-verified; background updates are disabled.
|
|
90
|
+
|
|
91
|
+
- Stable private origin: **https://ai-proxy.tail0a1b2c.ts.net**
|
|
92
|
+
- OpenAI/Responses base: **https://ai-proxy.tail0a1b2c.ts.net/v1**
|
|
93
|
+
- Official dashboard: **https://ai-proxy.tail0a1b2c.ts.net/management.html**
|
|
94
|
+
- Stable tailnet resource: `svc:ai-proxy`, `tcp:443`; host identity: `tag:ai-proxy`.
|
|
95
|
+
|
|
96
|
+
Inference requires a client API key. Management uses a separate key; ordinary inference keys cannot authorize admin APIs. HTML is publicly reachable; management data/actions require authentication. Existing broader tailnet policy grants remain in force; application authentication is always enforced. The public hostname uses the OpenTofu-managed Cloudflare tunnel; Tailscale Funnel is not used.
|
|
97
|
+
|
|
98
|
+
Cloud service definitions, policies and Vault values belong to the OpenTofu platform workflow. The original one-off cloud provisioning/secret-writing helpers have been retired. Do not recreate them in this package. Existing Tailscale Service resources survive workstation replacement; local `activate` advertises an existing resource and is a runtime operation. See [cloud adoption notes](docs/cloud-adoption.md) before changing the cloud definitions.
|
|
99
|
+
|
|
100
|
+
## State, permissions and configuration
|
|
101
|
+
|
|
102
|
+
`INSTALL_ROOT/state/` is mode 0700. On the manager it is owned by the installing user; on the legacy Linux host restricted files are owned by numeric UID 1000. The inference container mounts only `state/runtime`; it cannot access backup secrets, the Docker socket or dedicated Tailscale state.
|
|
103
|
+
|
|
104
|
+
| Path under installation | Purpose |
|
|
105
|
+
| --------------------------- | ---------------------------------------------------------------------- |
|
|
106
|
+
| `state/runtime/config.yaml` | Working config cache; dashboard edits checkpoint to R2 |
|
|
107
|
+
| `state/runtime/auths/` | Provider OAuth, account metadata, bbolt store, cooldown state |
|
|
108
|
+
| `state/runtime/static/` | Pinned official management HTML |
|
|
109
|
+
| `state/secrets/` | Private local material read from Vault and renewable Vault login token |
|
|
110
|
+
| `state/tailscale/` | Dedicated service identity; never run two copies concurrently |
|
|
111
|
+
| `state/network/` | Legacy policy cache; not required for recovery |
|
|
112
|
+
| `state/endpoint.json` | Verified stable endpoint metadata |
|
|
113
|
+
| `state/status/` | Operational results and shared operation lock |
|
|
114
|
+
| `INSTALLATION.json` | Git revision and hashes of generated installed code |
|
|
115
|
+
|
|
116
|
+
`config.template.yaml` is a versioned starting policy, not a second live config. Initialization never overwrites an existing live config. Dashboard edits persist in the directory mount and hot-reload. Reconcile intended nonsecret policy changes into the Git template manually; never commit a credential-bearing config export. CLI updates use `apply FILE`: safety validation and credential-free isolated container staging before replacement/restart. Upstream has no exhaustive standalone config checker; staging does not prove every provider option is valid.
|
|
117
|
+
|
|
118
|
+
## Routine commands
|
|
119
|
+
|
|
120
|
+
Run these from the repository; operational commands use the installed revision's scripts:
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
devfleet cliproxy audit
|
|
124
|
+
devfleet cliproxy start
|
|
125
|
+
devfleet cliproxy stop
|
|
126
|
+
devfleet cliproxy restart
|
|
127
|
+
devfleet cliproxy status
|
|
128
|
+
devfleet cliproxy health
|
|
129
|
+
devfleet cliproxy models
|
|
130
|
+
devfleet cliproxy logs
|
|
131
|
+
devfleet cliproxy validate
|
|
132
|
+
devfleet cliproxy apply /protected/candidate.yaml
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Docker restart policy recovers process crashes. On the manager the service agent starts the stack at login (Docker Desktop must be running; failed starts retry every minute); the legacy systemd unit starts at boot. A health probe checks HTTP/auth reachability, and the five-minute monitor checks code provenance, application/admin authentication, backup freshness and Vault failures. Persistent unhealthy states are surfaced, not hidden by automatic restart loops. `logs` is best-effort redaction; keep debug and request logging disabled. Off-machine verification runs in GitHub Actions; notification delivery follows GitHub notification settings.
|
|
136
|
+
|
|
137
|
+
## Providers and model selection
|
|
138
|
+
|
|
139
|
+
One OpenAI/Codex account was explicitly authorized during the initial deployment. Native coding-client OAuth files were not imported. Anthropic and Google require their own supported authorization/API-key setup in the official dashboard. All provider credentials stay server-side.
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
devfleet cliproxy add-account codex
|
|
143
|
+
devfleet cliproxy add-account claude
|
|
144
|
+
devfleet cliproxy models
|
|
145
|
+
devfleet cliproxy inference responses EXACT_CODEX_MODEL_ID
|
|
146
|
+
devfleet cliproxy inference messages EXACT_CLAUDE_MODEL_ID
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Codex uses the upstream device-code flow. Remote callback flows may require the documented dashboard callback flow or temporary SSH loopback forwarding; callback ports are not exposed by default. This pinned version has no generic Gemini CLI login flag; use its supported dashboard/API-key integration. Provider rules still govern permissible account use.
|
|
150
|
+
|
|
151
|
+
**Account routing.** Credential files are the only list of accounts. `ops declared` (run by every fleet-sync) gives each Claude, Codex and Antigravity credential the model prefix `<type>-<email>` through the proxy's management API (`scripts/accounts.py`; a credential file is never edited directly). A request for `<type>-<email>/<model>` is served only by that account, and `/v1/models` lists those ids, which is how every fleet node discovers the accounts to offer in T3. Nothing in Git names an account.
|
|
152
|
+
|
|
153
|
+
Unprefixed routing is **fill-first** (`routing.strategy: fill-first`): one account serves until its quota is exhausted and it cools down, then the next account, with session affinity keeping a session on its account while available and bounded retries across eligible accounts for the requested model. `cliproxy apply` requires this policy and the host monitor reports drift; see [Multi-account routing](docs/model-health.md#multi-account-routing-fill-first) for applying it to a running proxy. Automatic preview-model switching is disabled. No model aliases are configured: clients use the upstream model ids exactly as `/v1/models` lists them. [`models.json`](models.json) declares the model ids that are health-checked (`claude-opus-5-5`, `claude-sonnet-5-5`, `gpt-6-astra`, `gpt-6.1-sol`), and the checks fail loudly when one is not served. Usage statistics are enabled but are not a durable billing ledger. Available quota details depend on the provider.
|
|
154
|
+
|
|
155
|
+
## Fleet clients: macOS, Ubuntu and Windows/WSL
|
|
156
|
+
|
|
157
|
+
Use the same versioned bootstrap from this repository on every machine. Requires Python 3 and the desired client installed via the shared toolchain. No remote machine is changed automatically.
|
|
158
|
+
|
|
159
|
+
```bash
|
|
160
|
+
python3 cliproxy/clients/bootstrap.py \
|
|
161
|
+
--endpoint https://ai-proxy.tail0a1b2c.ts.net \
|
|
162
|
+
--model gpt-6.1-sol
|
|
163
|
+
python3 cliproxy/clients/bootstrap.py --validate
|
|
164
|
+
~/.local/bin/codex-fleet
|
|
165
|
+
~/.local/bin/opencode-fleet
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
Enter the inference key at the hidden prompt. It is available in Vault `secret/personal/prd:CLIPROXY_FLEET_CLIENT_API_KEY`; dashboard login uses `CLIPROXY_FLEET_MANAGEMENT_KEY`. Secrets never appear in command arguments. Keys are saved mode 0600 and loaded only into child environments. Give runners separate revocable inference keys where needed, never provider OAuth credentials or the admin key.
|
|
169
|
+
|
|
170
|
+
Codex uses custom-provider CLI overrides and Responses HTTP/SSE. Claude uses its supported base URL/auth-token variables and an explicit Claude model. OpenCode uses its config-file override, with separate Responses and Anthropic SDK providers. Once Claude is authenticated, rerun with `--claude-model MODEL_ID`; otherwise the Claude launcher fails with an explicit setup requirement. Native client config files are preserved. Use plain native clients during a proxy outage.
|
|
171
|
+
|
|
172
|
+
Run bootstrap from a repository checkout containing `clients/launcher.py`; detached downloaded bootstrap files are unsupported. Bootstrap is idempotent and refuses unrelated launcher collisions. `--uninstall` removes launcher links while retaining private settings for deliberate cleanup. On Windows run inside WSL; native PowerShell launchers are not provided. Validate from each actual machine and target repository because OpenCode merges project settings.
|
|
173
|
+
|
|
174
|
+
## Vault, backups and restore
|
|
175
|
+
|
|
176
|
+
Every backup operation fetches current `S3_ENDPOINT`, `S3_REGION`, `S3_BUCKET`, `S3_ACCESS_KEY_ID` and `S3_SECRET_ACCESS_KEY` from Vault `secret/personal/prd`. No S3 keys are cached. The existing production R2 bucket uses only `cliproxy-fleet/restic`, with forced path-style addressing. Destination changes are detected against `state/repository.json`; review an intentional repository migration rather than silently changing destinations.
|
|
177
|
+
|
|
178
|
+
```bash
|
|
179
|
+
devfleet cliproxy vault-login
|
|
180
|
+
devfleet cliproxy verify-recovery
|
|
181
|
+
devfleet cliproxy backup
|
|
182
|
+
devfleet cliproxy snapshots
|
|
183
|
+
devfleet cliproxy check-full
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
Vault login is interactive with hidden input; its token is restricted locally and renewed every 15 minutes where permitted. Maximum token lifetime still applies; re-login when renewal is no longer allowed. The Restic password has an independent existing copy at `CLIPROXY_FLEET_RESTIC_PASSWORD`. Secret creation/rotation goes through the platform's `bun run vault:edit`/OpenTofu workflow, not runtime helpers.
|
|
187
|
+
|
|
188
|
+
Encrypted cloud checkpoints run every 15 minutes (legacy systemd adds up to 30 seconds jitter). A checkpoint never stops, pauses or restarts the stack: agents use the proxy around the clock, and a stopped connector fails every request. The capture copies the live state and keeps the copy only if no state file changed while it was taken (retrying a few times); a stack that never holds still fails that backup. A failed capture, restart, upload or retention operation records failure and never advances the success marker. Retention keeps the latest 96 snapshots plus 7 daily, 5 weekly and 12 monthly. Weekly integrity checks read 10% of stored data; `check-full` reads all data. Monitoring fails when the last successful checkpoint is older than 45 minutes. Failures are visible in `cliproxy status` (and `launchctl print gui/$UID/LABEL` or systemd); off-machine checks also run through the CLI proxy health workflow described below.
|
|
189
|
+
|
|
190
|
+
R2 checkpoints are the durable recovery source for config, provider OAuth/account state and the dedicated Tailscale identity. Local state is a working cache with asynchronous durability: normally up to approximately 15 minutes plus upload duration can be lost, and an outage can extend that indefinitely. Run a successful manual checkpoint after important login/config changes or before moving machines. Provider refresh-token invalidation can still require a fresh provider login after recovering an older checkpoint. This is a single-writer deployment, with operator fencing of the previous node; there is no distributed lease or active-active support.
|
|
191
|
+
|
|
192
|
+
Checkpoints include only allowlisted runtime state, endpoint intent, repository identity and source revision metadata. GitHub supplies code; Vault supplies client/admin keys, the encryption password and current cloud credentials. Vault/tunnel tokens, local secret files, logs, prior config copies, status files and executable source are excluded. Config and OAuth files themselves contain secrets and remain encrypted in R2. Local capture/restore directories are private plaintext caches, deleted after normal completion. Abandoned generated `checkpoint-*` and `restore-*` directories under `recovery/` expire after 24 hours. Cleanup runs under the operation lock before each backup operation, so active local captures/restores cannot be collected. Unknown/manual rollback directories are preserved. Restic runs without a persistent local metadata cache.
|
|
193
|
+
|
|
194
|
+
Weekly garbage collection (`dev.devfleet.cliproxy-gc`, Sunday 04:00; legacy `cliproxy-gc.timer` adds up to five minutes jitter) physically frees unreferenced R2 data. `devfleet cliproxy gc` runs it on demand. It requires a cloud checkpoint less than one hour old, checks repository integrity before removing snapshot references, applies the same retention policy, prunes to at most 5% unused packed data, and checks integrity again. Restic's remote locks and the shared local operation lock serialize maintenance; the proxy remains running. Long maintenance can delay checkpoints. On legacy systemd, failed scheduled runs retry after 30 minutes. Failures have a separate `gc-failed` marker, and monitoring alerts locally when no successful GC exists or it is over nine days old. No forced unlock or repository-wide bucket lifecycle rule is used.
|
|
195
|
+
|
|
196
|
+
Retention groups only by the fixed host, not temporary checkpoint paths or version tags. This bounds normal history to at most 120 recovery points across format changes (usually fewer because daily/weekly/monthly selections overlap). GC also removes clean, unused deployment worktrees, retaining current/previous revisions and revisions referenced by fleet launchers or service links. Dirty or locked worktrees are never force-deleted. Runtime/provider state, other projects, shared Docker storage and manually created recovery directories are not deleted. Retention bounds historical copies, not the size of the current live dataset; failed GC is surfaced rather than silently discarding recovery points.
|
|
197
|
+
|
|
198
|
+
On the manager (or a replacement manager), prepare the tracked installation, log into Vault, recover the password, and run `devfleet cliproxy restore-cache --previous-node-stopped`. This downloads and verifies the latest tagged checkpoint, checks its manifest and security invariants, and populates an empty cache without starting services. It refuses existing cache state, unexpected files, symlinks, incomplete checkpoints and partial recovery. Installation refuses to invent a blank config when recovery is required. See [migration](docs/migration.md) for the complete cutover commands.
|
|
199
|
+
|
|
200
|
+
## Off-machine checks and offline recovery
|
|
201
|
+
|
|
202
|
+
The `CLI proxy health` GitHub Actions workflow runs at minutes 7, 22, 37 and 52 of each hour and supports manual dispatch. It uses the existing read-only GitHub OIDC Vault role, with a short-lived token rather than a stored workstation/admin credential. From a GitHub-hosted runner it checks public endpoint authentication, available models, dashboard availability, a cloud checkpoint no more than 45 minutes old, and the last successful GC no more than nine days old. GC status is included in subsequent encrypted checkpoints. A failed workflow is visible independently of this workstation and uses GitHub's existing notification settings; it is not a guaranteed paging service, and schedules can be delayed by GitHub.
|
|
203
|
+
|
|
204
|
+
The `CLI proxy health` workflow also runs the declarative model checks described in [Model validation and health checks](docs/model-health.md): a free readiness check every 15 minutes, a light two-request-per-model inference check hourly, a comprehensive nightly suite and an offline pull-request contract job, with per-model status, capability coverage, JSON/Markdown reports, an agent brief and a tracking issue. The local five-minute monitor does not run inference, attempt management logins, access provider credential files, prune storage, change cloud infrastructure, or restart the workstation. Management authorization remains covered by explicit verification; repeated failed management logins can trigger upstream IP bans, so they are excluded from scheduled monitoring. Its Restic client permits only read operations and uses no remote locks, so a killed runner cannot leave a lock blocking backups. Concurrent pruning can cause a transient read failure; subsequent scheduled runs retry. HTTPS redirects are rejected to prevent forwarding credentials to another origin.
|
|
205
|
+
|
|
206
|
+
With an existing private Vault token, one command prepares a complete offline replacement cache:
|
|
207
|
+
|
|
208
|
+
```bash
|
|
209
|
+
devfleet cliproxy recover-node /protected/new-node --vault-token-file /protected/vault-token
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
The parent directory must exist and the destination must not exist (even an empty directory is refused). Destinations inside the existing deployment are also refused, including paths routed through parent-directory symlinks, so recovered credentials cannot enter a live watched directory. The command reads fresh secrets from Vault, pins the latest checkpoint ID, restores and verifies it, checks its source manifest against Git, checks repository/client-key consistency, and publishes the complete private cache atomically. It includes API/management keys, the independently recovered encryption password and the supplied Vault login. It never overwrites the current deployment or activates a duplicate OAuth/Tailscale identity. Interrupted or rejected recovery leaves the existing deployment untouched. This command supports the macOS manager and Linux.
|
|
213
|
+
|
|
214
|
+
`RECOVERY.json` records the source revision and snapshot. The recovered directory contains data only. On the manager, use the default `INSTALL_ROOT` as the new destination, stop or otherwise fence the old machine, then install a reviewed Git revision with `devfleet cliproxy install --revision REV`. Activation remains explicit: there is no automatic failover or machine enrollment. A valid existing Vault login is still required; the runtime token has not been replaced with a least-privilege machine identity by this change.
|
|
215
|
+
|
|
216
|
+
## Verification and checks
|
|
217
|
+
|
|
218
|
+
```bash
|
|
219
|
+
bun run --filter @pkgs/cliproxy-fleet lint
|
|
220
|
+
bun run --filter @pkgs/cliproxy-fleet test
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
Model validation, Cursor-style protocol compatibility and per-model health commands (`devfleet cliproxy model-health`, `devfleet cliproxy model-suite`, offline `--target mock`) are documented in [docs/model-health.md](docs/model-health.md); their deterministic tests run in the same offline test command.
|
|
224
|
+
|
|
225
|
+
Explicit integration checks are also versioned: `python3 -B cliproxy/tests/live.py --codex --recovery --restore`. They read the existing deployment/Vault login, spend a small number of provider tokens and restore only into a temporary offline directory. Add `--opencode` when the locked OpenCode CLI is installed; no tool is installed automatically.
|
|
226
|
+
|
|
227
|
+
Offline checks require Restic and PyYAML. They cover real encrypted backup/restore with source deletion and wrong-password rejection, fault injection at capture/restart/upload/retention boundaries, corrupt and partial recovery, credential rotation, source-only installation and client bootstrap. Integration verification is explicit and uses inexpensive inference. See [migration/restore commands](docs/migration.md). Mac/WSL remote tests, additional provider authentication and a physical host migration remain operator-run checks.
|
|
228
|
+
|
|
229
|
+
Sources: [upstream release](https://github.com/router-for-me/CLIProxyAPI/tree/v8.0.55), [official dashboard](https://help.router-for.me/management/webui), [Codex config](https://learn.chatgpt.com/docs/config-file/config-reference), [Claude gateway](https://code.claude.com/docs/en/llm-gateway-connect), [OpenCode providers](https://opencode.ai/docs/providers/), [Tailscale Services](https://tailscale.com/docs/features/tailscale-services). The vendored upstream config is retained as version-specific schema documentation, not live configuration.
|
|
230
|
+
|
|
231
|
+
## Manager fleet integration
|
|
232
|
+
|
|
233
|
+
`devfleet fleet-sync` delegates singleton inspection and permitted stopped-agent
|
|
234
|
+
recovery to `scripts/fleet_sync.py`, which reads Fleet definition version 3: the
|
|
235
|
+
proxy owner is `manager.id`. Flags: `--definition`, `--node`, `--revision`,
|
|
236
|
+
`--apply`. It prints one JSON object (`version` 1, `command`,
|
|
237
|
+
`node`, `role` = `manager`|`worker`, `owner`, `backend` = `launchd`|`systemd`,
|
|
238
|
+
`ok`, `changed`, `desiredRevision`, `phases`, `durationMs`); each phase has a
|
|
239
|
+
`PROXY_*` `code`.
|
|
240
|
+
|
|
241
|
+
- **Manager:** refuses with `PROXY_PLATFORM_MISMATCH` off macOS and
|
|
242
|
+
`PROXY_IDENTITY_MISMATCH` unless the normalized `scutil --get LocalHostName`
|
|
243
|
+
equals `manager.id`. Missing state is `PROXY_RECOVERY_REQUIRED` (fenced
|
|
244
|
+
encrypted recovery only); drift is `PROXY_SOURCE_DRIFT`; a different
|
|
245
|
+
installed revision is installed by every fleet-sync. Containers mount the
|
|
246
|
+
stable install root, so the gateway keeps running unless `compose.yaml` or
|
|
247
|
+
`image.lock.json` changed; then it is recreated, which briefly interrupts
|
|
248
|
+
in-flight inference. `PROXY_UPDATE_PENDING` remains only when that install
|
|
249
|
+
failed. Settings the fleet owns in the live
|
|
250
|
+
config (`oauth.model-alias` and `upstream.claude`, from `config.template.yaml`;
|
|
251
|
+
the Claude Code version the proxy presents to Anthropic is replaced with the
|
|
252
|
+
`claude` pin in `mise.toml`, so it follows the harness every node runs) are compared
|
|
253
|
+
by `ops declared` (exit 3 when they differ) and reported as
|
|
254
|
+
`PROXY_CONFIG_PENDING`; `ops declared --apply` (run by every fleet-sync)
|
|
255
|
+
validates and stages the candidate, replaces only those keys and relies on
|
|
256
|
+
the proxy's config hot reload (restarting only if the reload never serves
|
|
257
|
+
the declared aliases). With
|
|
258
|
+
`--apply` it only loads stopped agents (`PROXY_UNIT_*`) or enables the
|
|
259
|
+
connector (`PROXY_ENDPOINT_DISABLED`), then checks `PROXY_ENDPOINT_AUTH`.
|
|
260
|
+
- **Workers:** `PROXY_CLIENT_ROLE` when clean. An active or enabled local proxy
|
|
261
|
+
is `PROXY_DUPLICATE_OWNER` and a stopped leftover installation is
|
|
262
|
+
`PROXY_DECOMMISSION_PENDING`; both point to the fence/decommission runbook and
|
|
263
|
+
are never stopped automatically.
|
|
264
|
+
|
|
265
|
+
Routine checks use client endpoint health, never management-login attempts.
|
|
266
|
+
This integration does not provision a connector, mutate DNS/Vault/tailnet
|
|
267
|
+
policy, seed secrets, or copy provider OAuth. Live five-provider authentication
|
|
268
|
+
and tool execution remain separate acceptance checks in the
|
|
269
|
+
[Fleet guide](../fleet/docs/ubuntu-sync.md).
|