@crvouga/dev-fleet 0.2.0 → 0.4.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/README.md +50 -16
- package/ansible/github/git.yml +66 -0
- package/ansible/macos/manager.yml +3 -0
- package/ansible/macos/policy.json +11 -1
- package/ansible/macos/t3-policy.yml +49 -0
- package/ansible/monitoring/compose.yml +10 -0
- package/ansible/monitoring/container-watchdog.py +118 -0
- package/ansible/monitoring/hub-darwin.yml +146 -0
- package/ansible/net/remote-probe.sh +5 -0
- package/ansible/ubuntu/baseline.yml +2 -0
- package/ansible/ubuntu/disk-gc.py +25 -3
- package/ansible/ubuntu/disk-guard.py +24 -5
- package/ansible/ubuntu/policy.json +51 -1
- package/ansible/ubuntu/t3-desktop.yml +19 -0
- package/ansible/ubuntu/t3-health.sh +93 -22
- package/ansible/ubuntu/t3-idle.py +42 -0
- package/ansible/ubuntu/t3-policy.yml +4 -11
- package/ansible/ubuntu/tasks/browser.yml +56 -0
- package/ansible/ubuntu/tasks/docker.yml +52 -0
- package/ansible/ubuntu/tasks/recovery.yml +60 -1
- package/ansible/ubuntu/tasks/resources.yml +2 -2
- package/ansible/ubuntu/tasks/services.yml +13 -0
- package/ansible/ubuntu/tasks/t3-restart.yml +37 -0
- package/ansible/ubuntu/wip-guard.py +121 -0
- package/bootstrap/doctor.ts +32 -6
- package/bootstrap/fleet-remote-bootstrap.sh +31 -4
- package/bootstrap/native-t3.ts +22 -11
- package/bootstrap/setup.ts +23 -8
- package/bootstrap/worker-tools.ts +118 -0
- package/cliproxy/README.md +4 -4
- package/cliproxy/compose.yaml +4 -0
- package/cliproxy/scripts/accounts.py +216 -0
- package/cliproxy/scripts/checkpoint.py +30 -2
- package/cliproxy/scripts/cli.py +1 -1
- package/cliproxy/scripts/clientkeys.py +99 -0
- package/cliproxy/scripts/lease.py +80 -0
- package/cliproxy/scripts/ops +137 -25
- package/cliproxy/scripts/restore-verify +15 -0
- package/docs/accounts.md +154 -0
- package/docs/authentication.md +90 -0
- package/docs/declared.md +5 -1
- package/docs/durability.md +26 -0
- package/docs/machine-setup.md +26 -15
- package/docs/operations.md +87 -31
- package/docs/runbooks.md +380 -0
- package/docs/state.md +13 -7
- package/docs/topology.md +2 -0
- package/docs/ubuntu-sync.md +35 -28
- package/examples/fleet.json +19 -1
- package/llms.txt +3 -0
- package/mise.toml +10 -1
- package/package.json +2 -2
- package/schema/check-ids.json +522 -0
- package/schema/fleet.schema.json +244 -1
- package/schema/output/check.schema.json +23 -0
- package/schema/output/error.schema.json +14 -0
- package/schema/output/fleet-report.schema.json +79 -0
- package/schema/output/fleet-sync.schema.json +112 -0
- package/schema/output/report.schema.json +38 -0
- package/src/accounts/checks.ts +220 -0
- package/src/accounts/context.ts +76 -0
- package/src/accounts/propagate.ts +62 -0
- package/src/accounts/roster.ts +85 -0
- package/src/accounts/selection.ts +67 -0
- package/src/accounts/sessions.ts +120 -0
- package/src/agents/account-relay.ts +176 -26
- package/src/agents/launch.ts +84 -5
- package/src/agents/native-verify.ts +128 -2
- package/src/auth-checks.ts +3 -0
- package/src/cli/agent-guide.ts +5 -3
- package/src/cli/fleet-accounts.ts +253 -0
- package/src/cli/fleet-report.ts +126 -0
- package/src/cli/fleet-sync-accounts.ts +58 -0
- package/src/cli/fleet-sync-manager.ts +31 -4
- package/src/cli/fleet-sync-output.ts +3 -2
- package/src/cli/fleet-sync-plan.ts +200 -0
- package/src/cli/fleet-sync-remote.ts +23 -14
- package/src/cli/fleet-sync-schedule.ts +142 -35
- package/src/cli/fleet-sync.ts +126 -8
- package/src/cli/fleet-watch.ts +198 -0
- package/src/cli/fleet.ts +4 -0
- package/src/cli/register-fleet.ts +86 -0
- package/src/cli/registry.ts +26 -0
- package/src/cli/schedule-logs.ts +36 -0
- package/src/clock.ts +63 -0
- package/src/definition.ts +226 -7
- package/src/git-identity.ts +160 -0
- package/src/ide/definition.ts +86 -0
- package/src/ide/ide.ts +43 -0
- package/src/ide/index.ts +5 -0
- package/src/ide/orchestrate.ts +50 -0
- package/src/ide/t3.ts +111 -0
- package/src/machine-health.ts +51 -4
- package/src/manager-health.ts +205 -0
- package/src/manager-sync.ts +13 -17
- package/src/monitoring/hub-backup.ts +63 -17
- package/src/monitoring/manager-hub.ts +44 -2
- package/src/node-converge-linux.ts +44 -12
- package/src/notify.ts +239 -0
- package/src/platform/darwin.ts +0 -13
- package/src/platform/fallback.ts +0 -8
- package/src/platform/linux.ts +0 -10
- package/src/platform/t3-policy.ts +33 -6
- package/src/platform/t3-service.ts +104 -0
- package/src/platform/types.ts +0 -4
- package/src/platform/windows.ts +0 -14
- package/src/remote-bootstrap.ts +87 -25
- package/src/remote-entry.ts +15 -1
- package/src/remote-operation.ts +213 -0
- package/src/schedule-check.ts +40 -0
- package/src/secrets.ts +26 -8
- package/src/service-auth.ts +15 -5
- package/src/ssh.ts +10 -2
- package/src/state/history.ts +172 -0
- package/src/state/store.ts +11 -4
- package/src/t3/accounts.ts +7 -1
- package/src/t3/models.ts +133 -0
- package/src/t3/pairing.ts +31 -0
- package/src/t3/profile.ts +203 -0
- package/src/t3/version.ts +119 -0
- package/src/t3-linked-check.ts +1 -1
- package/src/tailscale.ts +51 -0
- package/src/toolchain-checks.ts +8 -3
- package/src/transport.ts +3 -2
- package/src/worker-capability-checks.ts +167 -0
- package/src/worker-capacity.ts +19 -7
- package/src/worker-host-checks.ts +68 -13
- package/src/worker-policy.ts +33 -0
- package/src/worker-readiness.ts +10 -1
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
#!/usr/bin/python3
|
|
2
|
+
"""Push work in progress of every agent worktree to a recovery ref.
|
|
3
|
+
|
|
4
|
+
Managed by personal fleet-sync (tasks/recovery.yml) as
|
|
5
|
+
personal-worker-wip-guard.service, started by personal-worker-wip-guard.timer.
|
|
6
|
+
|
|
7
|
+
Workers are disposable: a worker lost mid-task must lose at most one interval
|
|
8
|
+
of uncommitted work. For each Git worktree under --root, the files as they are
|
|
9
|
+
now (tracked changes and untracked files that are not ignored) are written as
|
|
10
|
+
one commit on top of HEAD, built in a private index so the worktree, its index
|
|
11
|
+
and its branches are never touched, and pushed to
|
|
12
|
+
|
|
13
|
+
refs/recovery/<node>/<branch>
|
|
14
|
+
|
|
15
|
+
of the worktree's own remote. A clean worktree whose HEAD is already on a
|
|
16
|
+
remote branch has nothing to recover and pushes nothing. Recover with
|
|
17
|
+
`git fetch origin 'refs/recovery/*:refs/recovery/*'`.
|
|
18
|
+
|
|
19
|
+
Every decision is one JSON line on stdout.
|
|
20
|
+
|
|
21
|
+
Usage: wip-guard.py --root DIR [--root DIR ...] [--node ID] [--remote NAME]
|
|
22
|
+
[--max-depth N] [--dry-run]
|
|
23
|
+
"""
|
|
24
|
+
import argparse
|
|
25
|
+
import json
|
|
26
|
+
import os
|
|
27
|
+
import re
|
|
28
|
+
import socket
|
|
29
|
+
import subprocess
|
|
30
|
+
import tempfile
|
|
31
|
+
from pathlib import Path
|
|
32
|
+
|
|
33
|
+
GIT = ['git', '-c', 'safe.directory=*']
|
|
34
|
+
AUTHOR = {
|
|
35
|
+
'GIT_AUTHOR_NAME': 'dev-fleet work-in-progress guard',
|
|
36
|
+
'GIT_AUTHOR_EMAIL': 'wip-guard@example.invalid',
|
|
37
|
+
'GIT_COMMITTER_NAME': 'dev-fleet work-in-progress guard',
|
|
38
|
+
'GIT_COMMITTER_EMAIL': 'wip-guard@example.invalid',
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def log(**event):
|
|
43
|
+
print(json.dumps(event, sort_keys=True), flush=True)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def git(path, *args, env=None):
|
|
47
|
+
return subprocess.run(
|
|
48
|
+
[*GIT, '-C', str(path), *args], capture_output=True, text=True,
|
|
49
|
+
env={**os.environ, **(env or {})}, timeout=120)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def worktrees(root, depth):
|
|
53
|
+
if depth < 0 or not root.is_dir() or root.is_symlink():
|
|
54
|
+
return
|
|
55
|
+
if (root / '.git').exists():
|
|
56
|
+
yield root
|
|
57
|
+
return
|
|
58
|
+
for child in sorted(root.iterdir()):
|
|
59
|
+
if child.is_dir() and not child.is_symlink():
|
|
60
|
+
yield from worktrees(child, depth - 1)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def ref_part(value):
|
|
64
|
+
part = re.sub(r'[^A-Za-z0-9._/-]+', '-', value).strip('./-')
|
|
65
|
+
part = re.sub(r'/+', '/', part).replace('..', '.')
|
|
66
|
+
return part or 'unnamed'
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def snapshot(path):
|
|
70
|
+
"""The worktree's files as one commit on HEAD, or HEAD when clean."""
|
|
71
|
+
head = git(path, 'rev-parse', '--verify', 'HEAD').stdout.strip()
|
|
72
|
+
if not head:
|
|
73
|
+
return None, None
|
|
74
|
+
with tempfile.TemporaryDirectory() as private:
|
|
75
|
+
index = {'GIT_INDEX_FILE': os.path.join(private, 'index')}
|
|
76
|
+
if git(path, 'read-tree', 'HEAD', env=index).returncode != 0:
|
|
77
|
+
return head, None
|
|
78
|
+
if git(path, 'add', '--all', '--', '.', env=index).returncode != 0:
|
|
79
|
+
return head, None
|
|
80
|
+
tree = git(path, 'write-tree', env=index).stdout.strip()
|
|
81
|
+
if tree == git(path, 'rev-parse', 'HEAD^{tree}').stdout.strip():
|
|
82
|
+
return head, head
|
|
83
|
+
commit = git(path, 'commit-tree', tree, '-p', head, '-m',
|
|
84
|
+
'Work in progress saved by the dev-fleet guard', env=AUTHOR)
|
|
85
|
+
return head, commit.stdout.strip() or None
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def guard(path, node, remote, dry_run):
|
|
89
|
+
if git(path, 'remote', 'get-url', remote).returncode != 0:
|
|
90
|
+
return log(worktree=str(path), action='skip', reason='no-remote')
|
|
91
|
+
head, commit = snapshot(path)
|
|
92
|
+
if not commit:
|
|
93
|
+
return log(worktree=str(path), action='skip', reason='no-commit')
|
|
94
|
+
if commit == head and git(path, 'branch', '--remotes', '--contains', head).stdout.strip():
|
|
95
|
+
return log(worktree=str(path), action='skip', reason='clean')
|
|
96
|
+
branch = git(path, 'symbolic-ref', '--quiet', '--short', 'HEAD').stdout.strip()
|
|
97
|
+
ref = 'refs/recovery/%s/%s' % (ref_part(node), ref_part(branch or 'detached-' + head[:12]))
|
|
98
|
+
if dry_run:
|
|
99
|
+
return log(worktree=str(path), action='would-push', ref=ref)
|
|
100
|
+
pushed = git(path, 'push', '--force', '--no-verify', '--quiet', remote, '%s:%s' % (commit, ref))
|
|
101
|
+
log(worktree=str(path), action='pushed' if pushed.returncode == 0 else 'failed', ref=ref)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def main():
|
|
105
|
+
parser = argparse.ArgumentParser()
|
|
106
|
+
parser.add_argument('--root', action='append', required=True)
|
|
107
|
+
parser.add_argument('--node', default=socket.gethostname().split('.')[0])
|
|
108
|
+
parser.add_argument('--remote', default='origin')
|
|
109
|
+
parser.add_argument('--max-depth', type=int, default=3)
|
|
110
|
+
parser.add_argument('--dry-run', action='store_true')
|
|
111
|
+
options = parser.parse_args()
|
|
112
|
+
for root in options.root:
|
|
113
|
+
for path in worktrees(Path(root).expanduser(), options.max_depth):
|
|
114
|
+
try:
|
|
115
|
+
guard(path, options.node, options.remote, options.dry_run)
|
|
116
|
+
except (OSError, subprocess.SubprocessError):
|
|
117
|
+
log(worktree=str(path), action='failed', reason='git')
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
if __name__ == '__main__':
|
|
121
|
+
main()
|
package/bootstrap/doctor.ts
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
NO_SELECTION,
|
|
3
|
+
parseSelectionArguments,
|
|
4
|
+
workerToolset,
|
|
5
|
+
type ToolSelection,
|
|
6
|
+
} from './worker-tools';
|
|
2
7
|
/** Read-only toolchain checks; no logins, installations or changes to Docker. */
|
|
3
8
|
import { spawnSync } from 'node:child_process';
|
|
4
9
|
import { readFileSync } from 'node:fs';
|
|
@@ -15,7 +20,7 @@ import {
|
|
|
15
20
|
type Tool = string | { version: string; lazy?: boolean; os?: string[] };
|
|
16
21
|
type Config = {
|
|
17
22
|
tools: Record<string, Tool>;
|
|
18
|
-
vars?:
|
|
23
|
+
vars?: Record<string, unknown>;
|
|
19
24
|
};
|
|
20
25
|
export type Check = {
|
|
21
26
|
id: string;
|
|
@@ -42,6 +47,11 @@ const VERSION_ARGS: Record<string, string[]> = {
|
|
|
42
47
|
kubectl: ['version', '--client=true', '--output=json'],
|
|
43
48
|
helm: ['version', '--short'],
|
|
44
49
|
};
|
|
50
|
+
/** The version a tool prints, where it differs from its mise pin. */
|
|
51
|
+
const PRINTED: Record<string, (pin: string) => string> = {
|
|
52
|
+
// temurin-25.0.5+8.0.LTS prints as 25.0.5.
|
|
53
|
+
java: (pin) => pin.replace(/^[a-z]+-/, '').replace(/\+.*$/, ''),
|
|
54
|
+
};
|
|
45
55
|
const run: Runner = (command) => {
|
|
46
56
|
const result = spawnSync(command[0]!, command.slice(1), {
|
|
47
57
|
encoding: 'utf8',
|
|
@@ -80,7 +90,8 @@ export function toolChecks(
|
|
|
80
90
|
if (typeof tool !== 'string' && tool.lazy) continue;
|
|
81
91
|
// A tool pinned only for other platforms has no artifact to verify here.
|
|
82
92
|
if (unavailable(id, tool, platform)) continue;
|
|
83
|
-
const
|
|
93
|
+
const pin = typeof tool === 'string' ? tool : tool.version;
|
|
94
|
+
const version = PRINTED[id]?.(pin) ?? pin;
|
|
84
95
|
const command = COMMANDS[id] ?? id;
|
|
85
96
|
const result = execute([command, ...(VERSION_ARGS[id] ?? ['--version'])]);
|
|
86
97
|
const exact = new RegExp(
|
|
@@ -195,7 +206,21 @@ export function globalConfigCheck(
|
|
|
195
206
|
};
|
|
196
207
|
}
|
|
197
208
|
|
|
198
|
-
|
|
209
|
+
/** A worker installs every tool it selects, lazy or not, so each is checked. */
|
|
210
|
+
function installed(tools: Record<string, Tool>): Record<string, Tool> {
|
|
211
|
+
return Object.fromEntries(
|
|
212
|
+
Object.entries(tools).map(([id, tool]) => [
|
|
213
|
+
id,
|
|
214
|
+
typeof tool === 'string' ? tool : { ...tool, lazy: false },
|
|
215
|
+
])
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export function checkMachine(
|
|
220
|
+
requireDocker = false,
|
|
221
|
+
worker = false,
|
|
222
|
+
selection: ToolSelection = NO_SELECTION
|
|
223
|
+
): Check[] {
|
|
199
224
|
const config = Bun.TOML.parse(
|
|
200
225
|
readFileSync(join(workspaceRoot, 'mise.toml'), 'utf8')
|
|
201
226
|
) as Config;
|
|
@@ -203,7 +228,7 @@ export function checkMachine(requireDocker = false, worker = false): Check[] {
|
|
|
203
228
|
return [
|
|
204
229
|
...toolChecks(
|
|
205
230
|
worker
|
|
206
|
-
?
|
|
231
|
+
? installed(workerToolset(config.tools, config.vars ?? {}, selection))
|
|
207
232
|
: config.tools
|
|
208
233
|
),
|
|
209
234
|
...dockerChecks(requireDocker),
|
|
@@ -222,7 +247,8 @@ export function checkMachine(requireDocker = false, worker = false): Check[] {
|
|
|
222
247
|
if (import.meta.main) {
|
|
223
248
|
const checks = checkMachine(
|
|
224
249
|
process.argv.includes('--docker'),
|
|
225
|
-
process.argv.includes('--worker')
|
|
250
|
+
process.argv.includes('--worker'),
|
|
251
|
+
parseSelectionArguments(process.argv)
|
|
226
252
|
);
|
|
227
253
|
const ok = checks.every((check) => check.status !== 'fail');
|
|
228
254
|
if (process.argv.includes('--json'))
|
|
@@ -41,7 +41,7 @@ remedy() {
|
|
|
41
41
|
BOOTSTRAP_LOCK_TOOL_REQUIRED) echo 'Install python3 so the operation lock can be taken, then rerun fleet-sync.';;
|
|
42
42
|
SOURCE_CACHE_CHANGED|SOURCE_STATUS_FAILED|SOURCE_CHECKOUT_CONFLICT) echo 'The fleet-owned checkout of this revision holds changes fleet-sync did not make. Inspect and remove that revisions directory, then rerun fleet-sync.';;
|
|
43
43
|
CONFIGURATION_INSTALL_FAILED) echo 'The fleet configuration could not be written under ~/.config/dev-fleet. Check that directory, then rerun fleet-sync.';;
|
|
44
|
-
TOOLCHAIN_*) echo 'The pinned toolchain did not converge. Rerun fleet-sync; it resumes where it stopped. If it repeats, run devfleet
|
|
44
|
+
TOOLCHAIN_*) echo 'The pinned toolchain did not converge. Rerun fleet-sync; it resumes where it stopped. If it repeats, run devfleet fleet-status --node for this worker.';;
|
|
45
45
|
*) echo 'Inspect the remote operation lock and rerun the approved revision. No success receipt advanced.';;
|
|
46
46
|
esac
|
|
47
47
|
}
|
|
@@ -97,7 +97,18 @@ else
|
|
|
97
97
|
command -v python3 >/dev/null 2>&1 || die BOOTSTRAP_LOCK_TOOL_REQUIRED
|
|
98
98
|
python3 -c 'import fcntl; fcntl.flock(9, fcntl.LOCK_EX | fcntl.LOCK_NB)' 9>&9 || die FLEET_REMOTE_LOCKED
|
|
99
99
|
fi
|
|
100
|
-
|
|
100
|
+
# The operation record: a manager that lost its session reads it to follow
|
|
101
|
+
# this operation to its end instead of starting another (see
|
|
102
|
+
# src/remote-operation.ts). The result of the last operation is beside it.
|
|
103
|
+
started=$(date +%s)
|
|
104
|
+
result="$root/remote-operation-result.json"
|
|
105
|
+
[ ! -L "$root/remote-operation.json" ] && [ ! -L "$result" ] || die FLEET_STATE_PATH_UNSAFE
|
|
106
|
+
rm -f -- "$result"
|
|
107
|
+
operation() {
|
|
108
|
+
printf '{"version":1,"pid":%s,"revision":"%s","node":"%s","started":%s,"phase":"%s"%s}\n' "$$" "$revision" "$node" "$started" "$1" "${2:-}" >"$root/remote-operation.json.new" &&
|
|
109
|
+
mv -f -- "$root/remote-operation.json.new" "$root/remote-operation.json"
|
|
110
|
+
}
|
|
111
|
+
operation lock
|
|
101
112
|
# The fleet configuration is installed under the lock, so two overlapping
|
|
102
113
|
# runs never write it at once. It holds no secret.
|
|
103
114
|
phase='configuration-install'
|
|
@@ -109,6 +120,7 @@ if [ -n "$configuration" ]; then
|
|
|
109
120
|
mv "$config_dir/fleet.json.new" "$config_dir/fleet.json" || die CONFIGURATION_INSTALL_FAILED
|
|
110
121
|
fi
|
|
111
122
|
phase=resources
|
|
123
|
+
operation resources
|
|
112
124
|
# A bootstrap floor protects source transfer and minimal prerequisites. The
|
|
113
125
|
# Workstation baseline subsequently enforces its stricter declared capacity.
|
|
114
126
|
free_kib=$(df -Pk "$root" | awk 'END {print $4}')
|
|
@@ -200,6 +212,7 @@ bundle=''
|
|
|
200
212
|
clean=$(git -C "$deployment" status --porcelain) || die SOURCE_STATUS_FAILED
|
|
201
213
|
[ -z "$clean" ] || die SOURCE_CHECKOUT_CONFLICT
|
|
202
214
|
phase=toolchain
|
|
215
|
+
operation toolchain
|
|
203
216
|
cd "$deployment"
|
|
204
217
|
export PATH="$HOME/.local/bin:$HOME/.local/share/mise/shims:/usr/local/bin:/usr/bin:/bin"
|
|
205
218
|
status_file="$root/toolchain-status.json"
|
|
@@ -211,6 +224,11 @@ ready_file="$root/toolchain-ready"
|
|
|
211
224
|
# are still checked (and reinstalled on drift) by node convergence.
|
|
212
225
|
toolchain_key=$(git rev-parse HEAD:mise.toml HEAD:mise.lock HEAD:.mise HEAD:bun.lock HEAD:package.json HEAD:bootstrap 2>/dev/null | tr '\n' ' ')
|
|
213
226
|
[ -n "$toolchain_key" ] || toolchain_key=$revision
|
|
227
|
+
# The tool profiles of the fleet configuration choose what is installed, so a
|
|
228
|
+
# changed configuration runs the setup again.
|
|
229
|
+
if [ -f "$HOME/.config/dev-fleet/fleet.json" ]; then
|
|
230
|
+
toolchain_key="$toolchain_key$(cksum <"$HOME/.config/dev-fleet/fleet.json" | cut -d' ' -f1)"
|
|
231
|
+
fi
|
|
214
232
|
if [ "$(cat "$ready_file" 2>/dev/null)" = "$toolchain_key" ] && [ -n "$(find "$ready_file" -mmin -1440 2>/dev/null)" ]; then
|
|
215
233
|
toolchain_warm=1
|
|
216
234
|
else
|
|
@@ -219,7 +237,7 @@ else
|
|
|
219
237
|
fi
|
|
220
238
|
if command -v timeout >/dev/null 2>&1; then bounded='timeout 1800'; else bounded=''; fi
|
|
221
239
|
# shellcheck disable=SC2086
|
|
222
|
-
if [ -z "$toolchain_warm" ] && ! $bounded bash bootstrap/bootstrap.sh --worker --non-interactive --status-file "$status_file" >/dev/null 2>&1; then
|
|
240
|
+
if [ -z "$toolchain_warm" ] && ! $bounded bash bootstrap/bootstrap.sh --worker --node "$node" --non-interactive --status-file "$status_file" >/dev/null 2>&1; then
|
|
223
241
|
tool_phase=$(sed -n 's/.*"phase":"\([a-z0-9-]*\)".*/\1/p' "$status_file" 2>/dev/null) || tool_phase=''
|
|
224
242
|
case "$tool_phase" in
|
|
225
243
|
managed-config) die TOOLCHAIN_CONFIG_CONFLICT;;
|
|
@@ -254,5 +272,14 @@ for old in $(ls -1t "$root/revisions"); do
|
|
|
254
272
|
rm -rf -- "${root:?}/revisions/$old" || true
|
|
255
273
|
done
|
|
256
274
|
phase=configuration
|
|
275
|
+
operation configuration
|
|
257
276
|
trap - EXIT INT TERM
|
|
258
|
-
|
|
277
|
+
# Node convergence writes its evidence to the result file, not to the SSH
|
|
278
|
+
# session: the session may be gone by the time it ends. The evidence is then
|
|
279
|
+
# printed for the manager that is still connected, under the same lock.
|
|
280
|
+
code=0
|
|
281
|
+
mise exec --locked bun -- bun src/node-converge.ts "$node" "$revision" >"$result.partial" 2>"$root/remote-operation.log" || code=$?
|
|
282
|
+
mv -f -- "$result.partial" "$result"
|
|
283
|
+
operation finished ",\"finished\":$(date +%s),\"exit\":$code"
|
|
284
|
+
cat -- "$result" || true
|
|
285
|
+
exit "$code"
|
package/bootstrap/native-t3.ts
CHANGED
|
@@ -19,6 +19,7 @@ interface Configuration {
|
|
|
19
19
|
t3_version: string;
|
|
20
20
|
t3_linux_x64_sha256: string;
|
|
21
21
|
t3_linux_arm64_sha256: string;
|
|
22
|
+
t3_darwin_arm64_sha256?: string;
|
|
22
23
|
t3_desktop_amd64_sha256: string;
|
|
23
24
|
t3_desktop_arm64_sha256: string;
|
|
24
25
|
};
|
|
@@ -52,20 +53,27 @@ export function managedT3Desktop() {
|
|
|
52
53
|
) as Configuration;
|
|
53
54
|
return desktopT3Artifact(config, process.arch);
|
|
54
55
|
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
56
|
+
/** The pinned server archive: Ubuntu workers on either architecture, and
|
|
57
|
+
* the manager (macOS on Apple silicon, the only macOS build upstream ships).
|
|
58
|
+
*/
|
|
59
|
+
export function nativeT3Artifact(
|
|
60
|
+
config: Configuration,
|
|
61
|
+
architecture: string,
|
|
62
|
+
platform: string = 'linux'
|
|
63
|
+
) {
|
|
64
|
+
const sha256 = (<Record<string, string | undefined>>{
|
|
65
|
+
'linux-x64': config.vars.t3_linux_x64_sha256,
|
|
66
|
+
'linux-arm64': config.vars.t3_linux_arm64_sha256,
|
|
67
|
+
'darwin-arm64': config.vars.t3_darwin_arm64_sha256,
|
|
68
|
+
})[`${platform}-${architecture}`];
|
|
69
|
+
if (sha256 === undefined) throw new Error('Unsupported T3 architecture.');
|
|
58
70
|
const version = config.vars.t3_version;
|
|
59
|
-
const sha256 =
|
|
60
|
-
architecture === 'x64'
|
|
61
|
-
? config.vars.t3_linux_x64_sha256
|
|
62
|
-
: config.vars.t3_linux_arm64_sha256;
|
|
63
71
|
if (
|
|
64
72
|
!/^\d+\.\d+\.\d+(?:-nightly\.\d{8}\.\d+)?$/.test(version) ||
|
|
65
73
|
!/^[a-f0-9]{64}$/.test(sha256)
|
|
66
74
|
)
|
|
67
75
|
throw new Error('Invalid T3 pin.');
|
|
68
|
-
const name = `t3-${version}
|
|
76
|
+
const name = `t3-${version}-${platform}-${architecture}`;
|
|
69
77
|
return {
|
|
70
78
|
version,
|
|
71
79
|
sha256,
|
|
@@ -77,18 +85,21 @@ export function managedT3(): { path: string; version: string } {
|
|
|
77
85
|
const config = Bun.TOML.parse(
|
|
78
86
|
readFileSync(join(workspaceRoot, 'mise.toml'), 'utf8')
|
|
79
87
|
) as Configuration;
|
|
80
|
-
const artifact = nativeT3Artifact(config, process.arch);
|
|
88
|
+
const artifact = nativeT3Artifact(config, process.arch, process.platform);
|
|
81
89
|
return {
|
|
82
90
|
path: join(homedir(), '.local/share/personal/tools', artifact.name, 't3'),
|
|
83
91
|
version: artifact.version,
|
|
84
92
|
};
|
|
85
93
|
}
|
|
94
|
+
/** Installs the pinned, checksum-verified server archive on a worker and on
|
|
95
|
+
* the manager, so both run the T3 version the fleet pins.
|
|
96
|
+
*/
|
|
86
97
|
export async function installNativeT3(): Promise<void> {
|
|
87
|
-
if (process.platform
|
|
98
|
+
if (!['linux', 'darwin'].includes(process.platform)) return;
|
|
88
99
|
const config = Bun.TOML.parse(
|
|
89
100
|
readFileSync(join(workspaceRoot, 'mise.toml'), 'utf8')
|
|
90
101
|
) as Configuration;
|
|
91
|
-
const artifact = nativeT3Artifact(config, process.arch);
|
|
102
|
+
const artifact = nativeT3Artifact(config, process.arch, process.platform);
|
|
92
103
|
const root = join(homedir(), '.local/share/personal/tools');
|
|
93
104
|
const archive = join(root, `${artifact.name}.tar.gz`);
|
|
94
105
|
mkdirSync(root, { recursive: true, mode: 0o700 });
|
package/bootstrap/setup.ts
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
installedSelection,
|
|
3
|
+
NO_SELECTION,
|
|
4
|
+
workerToolset,
|
|
5
|
+
type ToolSelection,
|
|
6
|
+
} from './worker-tools';
|
|
2
7
|
import { readFileSync } from 'node:fs';
|
|
3
8
|
import { join } from 'node:path';
|
|
4
9
|
/** Used by both OS bootstraps, dev-fleet tools sync and dev-fleet update. No workspace dependencies. */
|
|
@@ -26,7 +31,8 @@ function run(args: string[]): void {
|
|
|
26
31
|
|
|
27
32
|
export async function syncTools(
|
|
28
33
|
observe: (phase: SetupPhase) => void = () => {},
|
|
29
|
-
worker = false
|
|
34
|
+
worker = false,
|
|
35
|
+
selection: ToolSelection = NO_SELECTION
|
|
30
36
|
): Promise<void> {
|
|
31
37
|
observe('managed-config');
|
|
32
38
|
const dir = globalConfigDir();
|
|
@@ -34,9 +40,9 @@ export async function syncTools(
|
|
|
34
40
|
observe('mise');
|
|
35
41
|
const config = Bun.TOML.parse(
|
|
36
42
|
readFileSync(join(workspaceRoot, 'mise.toml'), 'utf8')
|
|
37
|
-
) as { tools: Record<string, unknown>; vars:
|
|
43
|
+
) as { tools: Record<string, unknown>; vars: Record<string, unknown> };
|
|
38
44
|
const selected = worker
|
|
39
|
-
? Object.keys(
|
|
45
|
+
? Object.keys(workerToolset(config.tools, config.vars, selection))
|
|
40
46
|
: [];
|
|
41
47
|
run(['mise', 'install', '--locked', ...selected]);
|
|
42
48
|
observe('aws');
|
|
@@ -54,11 +60,20 @@ if (import.meta.main) {
|
|
|
54
60
|
const flag = process.argv.indexOf('--status-file');
|
|
55
61
|
const report = setupStatus(flag < 0 ? undefined : process.argv[flag + 1]);
|
|
56
62
|
let phase: SetupPhase = 'managed-config';
|
|
63
|
+
const worker = process.argv.includes('--worker');
|
|
64
|
+
const node = process.argv.indexOf('--node');
|
|
57
65
|
try {
|
|
58
|
-
await syncTools(
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
66
|
+
await syncTools(
|
|
67
|
+
(current) => {
|
|
68
|
+
phase = current;
|
|
69
|
+
report(phase, 'running');
|
|
70
|
+
},
|
|
71
|
+
worker,
|
|
72
|
+
// The tool profiles of the fleet configuration fleet-sync installed.
|
|
73
|
+
worker && node >= 0
|
|
74
|
+
? installedSelection(process.argv[node + 1])
|
|
75
|
+
: NO_SELECTION
|
|
76
|
+
);
|
|
62
77
|
if (!process.argv.includes('--tools-only')) {
|
|
63
78
|
phase = 'dependencies';
|
|
64
79
|
report(phase, 'running');
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
|
|
1
5
|
/** A worker installs only this declared subset; every version still comes from mise.lock. */
|
|
2
6
|
export function workerTools<T>(
|
|
3
7
|
tools: Record<string, T>,
|
|
@@ -22,3 +26,117 @@ export function workerTools<T>(
|
|
|
22
26
|
selected.map((key: string) => [key, tools[key]!])
|
|
23
27
|
) as Record<string, T>;
|
|
24
28
|
}
|
|
29
|
+
|
|
30
|
+
/** Named tool sets a fleet adds to its workers on top of the base set
|
|
31
|
+
* (`worker_tools`). Each is `worker_profile_<name>` under [vars] in
|
|
32
|
+
* mise.toml; every version still comes from mise.lock.
|
|
33
|
+
*/
|
|
34
|
+
export const TOOL_PROFILES = ['web', 'go', 'rust', 'jvm'] as const;
|
|
35
|
+
export type ToolProfile = (typeof TOOL_PROFILES)[number];
|
|
36
|
+
|
|
37
|
+
/** What a fleet configuration adds to one worker's base tools: profiles and
|
|
38
|
+
* single tools, each already pinned in mise.toml.
|
|
39
|
+
*/
|
|
40
|
+
export interface ToolSelection {
|
|
41
|
+
profiles: string[];
|
|
42
|
+
tools: string[];
|
|
43
|
+
}
|
|
44
|
+
export const NO_SELECTION: ToolSelection = { profiles: [], tools: [] };
|
|
45
|
+
|
|
46
|
+
const list = (value: unknown): string[] =>
|
|
47
|
+
Array.isArray(value)
|
|
48
|
+
? value.filter((item): item is string => typeof item === 'string')
|
|
49
|
+
: [];
|
|
50
|
+
const record = (value: unknown): value is Record<string, unknown> =>
|
|
51
|
+
typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
52
|
+
|
|
53
|
+
/** The selection a fleet configuration makes for one worker: the fleet-wide
|
|
54
|
+
* `worker.toolProfiles` and `worker.tools`, plus that worker's own
|
|
55
|
+
* `toolProfiles`. Read from the plain document, so the bootstrap needs no
|
|
56
|
+
* fleet code.
|
|
57
|
+
*/
|
|
58
|
+
export function workerSelection(
|
|
59
|
+
definition: unknown,
|
|
60
|
+
node?: string
|
|
61
|
+
): ToolSelection {
|
|
62
|
+
if (!record(definition)) return NO_SELECTION;
|
|
63
|
+
const policy = record(definition['worker']) ? definition['worker'] : {};
|
|
64
|
+
const workers: unknown[] = Array.isArray(definition['workers'])
|
|
65
|
+
? definition['workers']
|
|
66
|
+
: [];
|
|
67
|
+
const own = workers
|
|
68
|
+
.filter(record)
|
|
69
|
+
.filter((entry) => node !== undefined && entry['id'] === node)
|
|
70
|
+
.flatMap((entry) => list(entry['toolProfiles']));
|
|
71
|
+
return {
|
|
72
|
+
profiles: [...new Set([...list(policy['toolProfiles']), ...own])],
|
|
73
|
+
tools: [...new Set(list(policy['tools']))],
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** The selection of the fleet configuration fleet-sync installed on this
|
|
78
|
+
* machine; none when there is no configuration yet.
|
|
79
|
+
*/
|
|
80
|
+
export function installedSelection(
|
|
81
|
+
node: string | undefined,
|
|
82
|
+
home: string = homedir()
|
|
83
|
+
): ToolSelection {
|
|
84
|
+
const path = join(home, '.config/dev-fleet/fleet.json');
|
|
85
|
+
if (!existsSync(path)) return NO_SELECTION;
|
|
86
|
+
try {
|
|
87
|
+
return workerSelection(JSON.parse(readFileSync(path, 'utf8')), node);
|
|
88
|
+
} catch {
|
|
89
|
+
return NO_SELECTION;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** The tools of one worker: the base set, then each selected profile's, then
|
|
94
|
+
* the single tools, in that order and each once. A profile mise.toml does not
|
|
95
|
+
* declare, or a tool it does not pin, is refused.
|
|
96
|
+
*/
|
|
97
|
+
export function workerToolset<T>(
|
|
98
|
+
tools: Record<string, T>,
|
|
99
|
+
vars: Record<string, unknown>,
|
|
100
|
+
selection: ToolSelection = NO_SELECTION
|
|
101
|
+
): Record<string, T> {
|
|
102
|
+
const names = (value: unknown) =>
|
|
103
|
+
typeof value === 'string' ? value.split(',').filter(Boolean) : [];
|
|
104
|
+
const base = names(vars['worker_tools']);
|
|
105
|
+
const added = selection.profiles.flatMap((profile) => {
|
|
106
|
+
const declared = vars[`worker_profile_${profile}`];
|
|
107
|
+
if (typeof declared !== 'string' || !declared)
|
|
108
|
+
throw new Error(
|
|
109
|
+
`Unknown worker tool profile "${profile}". Choose from ${TOOL_PROFILES.join(', ')}.`
|
|
110
|
+
);
|
|
111
|
+
return names(declared);
|
|
112
|
+
});
|
|
113
|
+
for (const tool of selection.tools)
|
|
114
|
+
if (!Object.hasOwn(tools, tool))
|
|
115
|
+
throw new Error(
|
|
116
|
+
`The worker tool "${tool}" has no pin in dev-fleet's mise.toml. Ask for it in an issue.`
|
|
117
|
+
);
|
|
118
|
+
return workerTools(tools, [
|
|
119
|
+
...new Set([...base, ...added, ...selection.tools]),
|
|
120
|
+
]);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** `--profiles web,go --tools fd` for the doctor and the setup. */
|
|
124
|
+
export function selectionArguments(selection: ToolSelection): string[] {
|
|
125
|
+
return [
|
|
126
|
+
...(selection.profiles.length
|
|
127
|
+
? ['--profiles', selection.profiles.join(',')]
|
|
128
|
+
: []),
|
|
129
|
+
...(selection.tools.length ? ['--tools', selection.tools.join(',')] : []),
|
|
130
|
+
];
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** The selection named by `--profiles` and `--tools`. */
|
|
134
|
+
export function parseSelectionArguments(argv: string[]): ToolSelection {
|
|
135
|
+
const value = (flag: string) => {
|
|
136
|
+
const index = argv.indexOf(flag);
|
|
137
|
+
return index < 0
|
|
138
|
+
? []
|
|
139
|
+
: (argv[index + 1] ?? '').split(',').filter((item) => item.length > 0);
|
|
140
|
+
};
|
|
141
|
+
return { profiles: value('--profiles'), tools: value('--tools') };
|
|
142
|
+
}
|
package/cliproxy/README.md
CHANGED
|
@@ -146,11 +146,11 @@ devfleet cliproxy inference responses EXACT_CODEX_MODEL_ID
|
|
|
146
146
|
devfleet cliproxy inference messages EXACT_CLAUDE_MODEL_ID
|
|
147
147
|
```
|
|
148
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.
|
|
149
|
+
`devfleet fleet-accounts --add|--relogin|--remove` wraps these and propagates the result to every node; a login writes into a staging directory and only a complete credential is moved in. 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
150
|
|
|
151
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
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.
|
|
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. The only model aliases are the ones `config.template.yaml` declares under `oauth.model-alias` (`ops` applies them and waits until `/v1/models` serves them); every other model id is the upstream id exactly as `/v1/models` lists it. [`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
154
|
|
|
155
155
|
## Fleet clients: macOS, Ubuntu and Windows/WSL
|
|
156
156
|
|
|
@@ -167,7 +167,7 @@ python3 cliproxy/clients/bootstrap.py --validate
|
|
|
167
167
|
|
|
168
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
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.
|
|
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. Fleet nodes never fall back to native clients: provider accounts exist only on the proxy (see [docs/accounts.md](../docs/accounts.md)), and during a proxy outage every relayed launcher retries for about a minute, then reports "proxy unreachable".
|
|
171
171
|
|
|
172
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
173
|
|
|
@@ -187,7 +187,7 @@ Vault login is interactive with hidden input; its token is restricted locally an
|
|
|
187
187
|
|
|
188
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
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.
|
|
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. Each checkpoint records a digest of every refresh token, so `restore-verify RESTORED --newest NEWEST` names exactly the accounts that need one. This is a single-writer deployment, with operator fencing of the previous node; there is no distributed lease or active-active support.
|
|
191
191
|
|
|
192
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
193
|
|
package/cliproxy/compose.yaml
CHANGED
|
@@ -3,6 +3,10 @@ services:
|
|
|
3
3
|
proxy:
|
|
4
4
|
image: ${CLI_PROXY_IMAGE:?image must be pinned}
|
|
5
5
|
restart: unless-stopped
|
|
6
|
+
# Docker restarts a proxy that exits; the container watchdog on the
|
|
7
|
+
# manager restarts one that stays unhealthy.
|
|
8
|
+
labels:
|
|
9
|
+
dev.devfleet.watchdog: restart
|
|
6
10
|
user: '${CLIPROXY_UID:-1000}:${CLIPROXY_GID:-1000}'
|
|
7
11
|
working_dir: /state
|
|
8
12
|
command: ['/CLIProxyAPI/CLIProxyAPI', '-config', '/state/config.yaml']
|