@crvouga/dev-fleet 0.3.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 +22 -16
- package/ansible/github/git.yml +66 -0
- package/ansible/macos/policy.json +11 -1
- 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 +50 -1
- package/ansible/ubuntu/tasks/browser.yml +56 -0
- package/ansible/ubuntu/tasks/docker.yml +52 -0
- package/ansible/ubuntu/tasks/resources.yml +2 -2
- package/ansible/ubuntu/tasks/services.yml +13 -0
- package/bootstrap/doctor.ts +32 -6
- package/bootstrap/fleet-remote-bootstrap.sh +31 -4
- package/bootstrap/setup.ts +23 -8
- package/bootstrap/worker-tools.ts +118 -0
- package/cliproxy/README.md +1 -1
- package/cliproxy/compose.yaml +4 -0
- package/cliproxy/scripts/checkpoint.py +1 -1
- package/docs/authentication.md +90 -0
- package/docs/declared.md +2 -0
- package/docs/machine-setup.md +22 -12
- package/docs/operations.md +87 -31
- package/docs/runbooks.md +380 -0
- package/docs/state.md +13 -7
- package/docs/ubuntu-sync.md +7 -6
- package/examples/fleet.json +19 -1
- package/llms.txt +2 -0
- package/mise.toml +7 -0
- package/package.json +2 -2
- package/schema/check-ids.json +522 -0
- package/schema/fleet.schema.json +178 -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/auth-checks.ts +3 -0
- package/src/cli/agent-guide.ts +5 -3
- package/src/cli/fleet-report.ts +126 -0
- package/src/cli/fleet-sync-manager.ts +11 -0
- package/src/cli/fleet-sync-plan.ts +200 -0
- package/src/cli/fleet-sync-remote.ts +3 -1
- package/src/cli/fleet-sync-schedule.ts +142 -35
- package/src/cli/fleet-sync.ts +90 -8
- package/src/cli/fleet-watch.ts +198 -0
- package/src/cli/fleet.ts +4 -0
- package/src/cli/register-fleet.ts +51 -0
- package/src/cli/registry.ts +13 -0
- package/src/cli/schedule-logs.ts +36 -0
- package/src/clock.ts +63 -0
- package/src/definition.ts +142 -5
- package/src/git-identity.ts +160 -0
- package/src/machine-health.ts +51 -4
- package/src/manager-health.ts +205 -0
- package/src/manager-sync.ts +12 -2
- package/src/monitoring/hub-backup.ts +63 -17
- package/src/monitoring/manager-hub.ts +44 -2
- package/src/node-converge-linux.ts +16 -5
- package/src/notify.ts +239 -0
- package/src/remote-bootstrap.ts +87 -25
- package/src/remote-operation.ts +213 -0
- package/src/schedule-check.ts +40 -0
- package/src/secrets.ts +26 -8
- package/src/ssh.ts +10 -2
- package/src/state/history.ts +172 -0
- package/src/state/store.ts +11 -4
- package/src/tailscale.ts +51 -0
- package/src/toolchain-checks.ts +8 -3
- package/src/worker-capability-checks.ts +167 -0
- package/src/worker-capacity.ts +19 -7
- package/src/worker-host-checks.ts +32 -0
- package/src/worker-policy.ts +33 -0
- package/src/worker-readiness.ts +10 -1
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
---
|
|
2
|
+
# Headless Chromium for browser tests (Playwright, Storybook, end-to-end
|
|
3
|
+
# suites): the system libraries it loads, at the snapshot's exact versions,
|
|
4
|
+
# and one pinned Chrome Headless Shell build. Applied to workers that select
|
|
5
|
+
# the web tool profile; the launch below backs worker:browser.
|
|
6
|
+
- name: Install the exact libraries headless Chromium loads
|
|
7
|
+
ansible.builtin.apt:
|
|
8
|
+
name: '{{ item.key }}={{ item.value }}'
|
|
9
|
+
state: present
|
|
10
|
+
lock_timeout: '{{ aptLockTimeoutSeconds }}'
|
|
11
|
+
allow_downgrade: false
|
|
12
|
+
allow_unauthenticated: false
|
|
13
|
+
fail_on_autoremove: true
|
|
14
|
+
policy_rc_d: 101
|
|
15
|
+
environment:
|
|
16
|
+
APT_CONFIG: /etc/personal-fleet/apt.conf
|
|
17
|
+
loop: '{{ browser.packages | dict2items }}'
|
|
18
|
+
- name: Name the headless shell build of this architecture
|
|
19
|
+
ansible.builtin.set_fact:
|
|
20
|
+
browser_build: "{{ browser.headlessShell['arm64' if ansible_facts.architecture == 'aarch64' else 'amd64'] }}"
|
|
21
|
+
browser_root: '/opt/personal-fleet/browser/{{ browser.headlessShell.version }}'
|
|
22
|
+
- name: Create the package cache and the browser directory
|
|
23
|
+
ansible.builtin.file:
|
|
24
|
+
path: '{{ item }}'
|
|
25
|
+
state: directory
|
|
26
|
+
owner: root
|
|
27
|
+
group: root
|
|
28
|
+
mode: '0755'
|
|
29
|
+
loop:
|
|
30
|
+
- /var/cache/personal-fleet
|
|
31
|
+
- '{{ browser_root }}'
|
|
32
|
+
- name: Download the pinned headless shell
|
|
33
|
+
ansible.builtin.get_url:
|
|
34
|
+
url: 'https://cdn.playwright.dev/builds/cft/{{ browser.headlessShell.version }}/{{ browser_build.archive }}.zip'
|
|
35
|
+
dest: '/var/cache/personal-fleet/chrome-headless-shell-{{ browser.headlessShell.version }}-{{ browser_build.archive | basename }}.zip'
|
|
36
|
+
checksum: 'sha256:{{ browser_build.sha256 }}'
|
|
37
|
+
mode: '0644'
|
|
38
|
+
timeout: 300
|
|
39
|
+
- name: Unpack the verified headless shell
|
|
40
|
+
ansible.builtin.unarchive:
|
|
41
|
+
src: '/var/cache/personal-fleet/chrome-headless-shell-{{ browser.headlessShell.version }}-{{ browser_build.archive | basename }}.zip'
|
|
42
|
+
dest: '{{ browser_root }}'
|
|
43
|
+
remote_src: true
|
|
44
|
+
creates: '{{ browser_root }}/{{ browser_build.archive | basename }}/chrome-headless-shell'
|
|
45
|
+
- name: Launch headless as the developer, load a blank page and exit (BROWSER_LAUNCH_FAILED)
|
|
46
|
+
ansible.builtin.command:
|
|
47
|
+
argv:
|
|
48
|
+
- '{{ browser_root }}/{{ browser_build.archive | basename }}/chrome-headless-shell'
|
|
49
|
+
- --headless
|
|
50
|
+
- --no-sandbox
|
|
51
|
+
- --disable-gpu
|
|
52
|
+
- --dump-dom
|
|
53
|
+
- about:blank
|
|
54
|
+
become_user: '{{ development_user }}'
|
|
55
|
+
timeout: '{{ browser.launchTimeoutSeconds }}'
|
|
56
|
+
changed_when: false
|
|
@@ -29,3 +29,55 @@
|
|
|
29
29
|
- [docker, compose, version]
|
|
30
30
|
- [docker, buildx, version]
|
|
31
31
|
changed_when: false
|
|
32
|
+
# The engine answering is not proof that an agent can use it: run a pinned
|
|
33
|
+
# image and build a one-line image as the developer, in a fresh session that
|
|
34
|
+
# reads the docker group from scratch. The image is cached here so the same
|
|
35
|
+
# test works offline in worker:docker-run.
|
|
36
|
+
- name: Look for the cached smoke-test image
|
|
37
|
+
ansible.builtin.command:
|
|
38
|
+
argv:
|
|
39
|
+
[
|
|
40
|
+
docker,
|
|
41
|
+
image,
|
|
42
|
+
inspect,
|
|
43
|
+
--format,
|
|
44
|
+
'{{ "{{.Id}}" }}',
|
|
45
|
+
'{{ dockerSmokeTest.image }}',
|
|
46
|
+
]
|
|
47
|
+
become_user: '{{ development_user }}'
|
|
48
|
+
check_mode: false
|
|
49
|
+
changed_when: false
|
|
50
|
+
failed_when: false
|
|
51
|
+
register: docker_smoke_image
|
|
52
|
+
- name: Cache the pinned smoke-test image
|
|
53
|
+
ansible.builtin.command:
|
|
54
|
+
argv: [docker, pull, --quiet, '{{ dockerSmokeTest.image }}']
|
|
55
|
+
become_user: '{{ development_user }}'
|
|
56
|
+
when: docker_smoke_image.rc | default(0) != 0
|
|
57
|
+
changed_when: true
|
|
58
|
+
- name: Run a container as the developer, offline (DOCKER_RUN_FAILED)
|
|
59
|
+
ansible.builtin.command:
|
|
60
|
+
argv:
|
|
61
|
+
[
|
|
62
|
+
docker,
|
|
63
|
+
run,
|
|
64
|
+
--rm,
|
|
65
|
+
--network,
|
|
66
|
+
none,
|
|
67
|
+
--pull,
|
|
68
|
+
never,
|
|
69
|
+
'{{ dockerSmokeTest.image }}',
|
|
70
|
+
'true',
|
|
71
|
+
]
|
|
72
|
+
become_user: '{{ development_user }}'
|
|
73
|
+
timeout: '{{ dockerSmokeTest.timeoutSeconds }}'
|
|
74
|
+
changed_when: false
|
|
75
|
+
- name: Build a one-line image as the developer, offline (DOCKER_BUILD_FAILED)
|
|
76
|
+
ansible.builtin.command:
|
|
77
|
+
argv: [docker, build, --quiet, --pull=false, --network, none, '-']
|
|
78
|
+
stdin: |
|
|
79
|
+
FROM {{ dockerSmokeTest.image }}
|
|
80
|
+
RUN true
|
|
81
|
+
become_user: '{{ development_user }}'
|
|
82
|
+
timeout: '{{ dockerSmokeTest.timeoutSeconds }}'
|
|
83
|
+
changed_when: false
|
|
@@ -224,7 +224,7 @@
|
|
|
224
224
|
StartLimitIntervalSec=0
|
|
225
225
|
[Service]
|
|
226
226
|
Type=simple
|
|
227
|
-
ExecStart=/usr/bin/python3 -I -B {{ disk_guard_source.stat.path }} --path / --path {{ development_home }} --floor-mib {{ resourceSafety.diskGuardFloorMiB }} --write-mib {{ resourceSafety.diskGuardWriteMiB }} --interval {{ resourceSafety.diskGuardIntervalSeconds }}
|
|
227
|
+
ExecStart=/usr/bin/python3 -I -B {{ disk_guard_source.stat.path }} --path / --path {{ development_home }} --floor-mib {{ resourceSafety.diskGuardFloorMiB }} --inode-floor-percent {{ resourceSafety.inodeFailFreePercent }} --write-mib {{ resourceSafety.diskGuardWriteMiB }} --interval {{ resourceSafety.diskGuardIntervalSeconds }}
|
|
228
228
|
Restart=always
|
|
229
229
|
RestartSec=5
|
|
230
230
|
OOMScoreAdjust=-1000
|
|
@@ -276,7 +276,7 @@
|
|
|
276
276
|
After=docker.service
|
|
277
277
|
[Service]
|
|
278
278
|
Type=oneshot
|
|
279
|
-
ExecStart=/usr/bin/python3 -I -B {{ disk_gc_source.stat.path }} --home {{ development_home }} --path / --path {{ development_home }} --idle-days {{ resourceSafety.diskGcIdleDays }} --pressure-mib {{ resourceSafety.diskGcPressureMiB }} --pressure-idle-hours {{ resourceSafety.diskGcPressureIdleHours }} --docker-age-hours {{ resourceSafety.diskGcDockerAgeHours }} --pressure-docker-age-hours {{ resourceSafety.diskGcPressureDockerAgeHours }}{% for cache in resourceSafety.diskGcPressureCaches %} --pressure-cache {{ cache }}{% endfor %}
|
|
279
|
+
ExecStart=/usr/bin/python3 -I -B {{ disk_gc_source.stat.path }} --home {{ development_home }} --path / --path {{ development_home }} --idle-days {{ resourceSafety.diskGcIdleDays }} --pressure-mib {{ resourceSafety.diskGcPressureMiB }} --pressure-inode-percent {{ resourceSafety.inodeWarnFreePercent }} --pressure-idle-hours {{ resourceSafety.diskGcPressureIdleHours }} --docker-age-hours {{ resourceSafety.diskGcDockerAgeHours }} --pressure-docker-age-hours {{ resourceSafety.diskGcPressureDockerAgeHours }}{% for cache in resourceSafety.diskGcPressureCaches %} --pressure-cache {{ cache }}{% endfor %}
|
|
280
280
|
|
|
281
281
|
TimeoutStartSec=2h
|
|
282
282
|
Nice=19
|
|
@@ -29,3 +29,16 @@
|
|
|
29
29
|
ansible.builtin.systemd_service:
|
|
30
30
|
daemon_reload: true
|
|
31
31
|
when: recovery_dropins.changed
|
|
32
|
+
# A skewed clock breaks TLS, tokens, apt signatures and Vault logins with
|
|
33
|
+
# errors that never mention time. Ubuntu ships one time service (chrony on
|
|
34
|
+
# newer releases, systemd-timesyncd before); whichever is installed stays
|
|
35
|
+
# enabled. A container takes its clock from its host. Measured as worker:clock.
|
|
36
|
+
- name: Inspect the time services this worker has
|
|
37
|
+
ansible.builtin.service_facts:
|
|
38
|
+
- name: Keep the clock synchronised with the time service Ubuntu ships
|
|
39
|
+
ansible.builtin.systemd_service:
|
|
40
|
+
name: '{{ item }}'
|
|
41
|
+
enabled: true
|
|
42
|
+
state: started
|
|
43
|
+
loop: "{{ (['chrony.service', 'systemd-timesyncd.service'] | select('in', ansible_facts.services) | list)[:1] }}"
|
|
44
|
+
when: ansible_facts.virtualization_type not in ['docker', 'container', 'podman', 'lxc', 'systemd-nspawn']
|
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/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
|
@@ -150,7 +150,7 @@ devfleet cliproxy inference messages EXACT_CLAUDE_MODEL_ID
|
|
|
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
|
|
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']
|
|
@@ -68,7 +68,7 @@ def validate(root):
|
|
|
68
68
|
raise ValueError("Provider cache directory missing")
|
|
69
69
|
for path in (state / "runtime/auths").iterdir():
|
|
70
70
|
if path.name == "kv.db" and path.is_file() and path.stat().st_size > 0:
|
|
71
|
-
continue # Opaque upstream bbolt state,
|
|
71
|
+
continue # Opaque upstream bbolt state, copied while the proxy runs (see capture).
|
|
72
72
|
# .json: provider credential; .cds: upstream per-account cooldown
|
|
73
73
|
# records (JSON object, no credential), kept so limits survive moves.
|
|
74
74
|
if path.suffix not in (".json", ".cds") or not path.is_file():
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# Authentication and accounts
|
|
2
|
+
|
|
3
|
+
Where each credential dev-fleet uses lives, who can read it, how it reaches
|
|
4
|
+
each node, how it rotates and what happens when it fails. The three tables
|
|
5
|
+
are generated from the code; `features/rules/authentication.feature` fails
|
|
6
|
+
when one is out of date (`DEVFLEET_UPDATE_DOCS=1 bun test features/rules`
|
|
7
|
+
rewrites them).
|
|
8
|
+
|
|
9
|
+
## Where credentials live
|
|
10
|
+
|
|
11
|
+
| Credential | Lives in | Read by |
|
|
12
|
+
| ----------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------ |
|
|
13
|
+
| The fleet's secrets (table below) | The provider `secrets` names in `fleet.json` | The node that needs it, through `src/secrets.ts` |
|
|
14
|
+
| The bucket's two keys | The environment of the `devfleet` command; `~/.config/dev-fleet/bucket.json` (0600) on the manager after `fleet-state --init` | The manager only |
|
|
15
|
+
| A node's Vault token | That node's own Vault session (`~/.vault-token`) | That node only; never copied |
|
|
16
|
+
| A node's GitHub login | `gh`'s own store on that node | `gh`, and git through its credential helper |
|
|
17
|
+
| Provider accounts (Claude, Codex …) | The CLI proxy's store on the manager | The proxy; nodes hold only the proxy client key |
|
|
18
|
+
| Tailscale and T3 pairing | Each machine, signed in by a person | That machine |
|
|
19
|
+
|
|
20
|
+
## How they reach each node
|
|
21
|
+
|
|
22
|
+
- **Vault.** A person logs each machine in once (`fleet-sync` asks on the
|
|
23
|
+
manager and passes the password to the worker over SSH on standard input).
|
|
24
|
+
From then on the worker's `personal-worker-auth` timer renews its own token
|
|
25
|
+
(`bootstrap/worker-auth.ts`). Nothing renders provider configuration from
|
|
26
|
+
Vault on a worker: agent profiles come from `fleet-sync`.
|
|
27
|
+
- **GitHub.** The same job restores the declared account (`worker.githubLogin`)
|
|
28
|
+
with the GitHub service token it reads from its own Vault session
|
|
29
|
+
(`bootstrap/worker-github.ts`, on standard input, never argv). `fleet-sync`
|
|
30
|
+
makes `gh` git's credential helper and writes the declared identity
|
|
31
|
+
(`git` in `fleet.json`, `ansible/github/git.yml`) on every node.
|
|
32
|
+
- **The proxy client key** is read by each node from its own secrets provider
|
|
33
|
+
whenever a launcher starts an agent.
|
|
34
|
+
- **The monitoring hub's backup storage keys** are read on the manager by
|
|
35
|
+
`fleet-sync` and handed to the hub's playbook in a private variables file.
|
|
36
|
+
|
|
37
|
+
## Rotation and failure
|
|
38
|
+
|
|
39
|
+
Rotate a secret where the provider keeps it; the next read uses it, nothing
|
|
40
|
+
caches it. A Vault token that is not renewed expires: `worker:vault-renewal`
|
|
41
|
+
fails and the fix is one login of that machine. A GitHub token that stops
|
|
42
|
+
working fails `auth:github` and `worker:git-push-auth`; store the new one in
|
|
43
|
+
Vault and the worker restores itself within ten minutes. During a proxy
|
|
44
|
+
outage agents on workers wait: no worker is configured for native clients.
|
|
45
|
+
Every failure code is in [the runbooks](runbooks.md).
|
|
46
|
+
|
|
47
|
+
## The fleet's secrets
|
|
48
|
+
|
|
49
|
+
<!-- generated: secrets -->
|
|
50
|
+
|
|
51
|
+
| Secret | Required | `env` provider variable | `file` provider file |
|
|
52
|
+
| ------------------------------ | ---------------------------- | ---------------------------------------------- | ---------------------------------------------------------- |
|
|
53
|
+
| `proxy-client-key` | yes | `DEVFLEET_SECRET_PROXY_CLIENT_KEY` | `~/.config/dev-fleet/secrets/proxy-client-key` |
|
|
54
|
+
| `monitoring-account-password` | yes | `DEVFLEET_SECRET_MONITORING_ACCOUNT_PASSWORD` | `~/.config/dev-fleet/secrets/monitoring-account-password` |
|
|
55
|
+
| `monitoring-tunnel-token` | yes | `DEVFLEET_SECRET_MONITORING_TUNNEL_TOKEN` | `~/.config/dev-fleet/secrets/monitoring-tunnel-token` |
|
|
56
|
+
| `proxy-tunnel-token` | yes | `DEVFLEET_SECRET_PROXY_TUNNEL_TOKEN` | `~/.config/dev-fleet/secrets/proxy-tunnel-token` |
|
|
57
|
+
| `monitoring-backup-access-key` | when its feature is declared | `DEVFLEET_SECRET_MONITORING_BACKUP_ACCESS_KEY` | `~/.config/dev-fleet/secrets/monitoring-backup-access-key` |
|
|
58
|
+
| `monitoring-backup-secret-key` | when its feature is declared | `DEVFLEET_SECRET_MONITORING_BACKUP_SECRET_KEY` | `~/.config/dev-fleet/secrets/monitoring-backup-secret-key` |
|
|
59
|
+
| `notify-webhook-url` | when its feature is declared | `DEVFLEET_SECRET_NOTIFY_WEBHOOK_URL` | `~/.config/dev-fleet/secrets/notify-webhook-url` |
|
|
60
|
+
| `notify-ntfy-url` | when its feature is declared | `DEVFLEET_SECRET_NOTIFY_NTFY_URL` | `~/.config/dev-fleet/secrets/notify-ntfy-url` |
|
|
61
|
+
|
|
62
|
+
<!-- end: secrets -->
|
|
63
|
+
|
|
64
|
+
## Checks of credentials
|
|
65
|
+
|
|
66
|
+
<!-- generated: checks -->
|
|
67
|
+
|
|
68
|
+
| Check | Measured in |
|
|
69
|
+
| ----------------------------- | ------------------------------------------ |
|
|
70
|
+
| `auth:vault` | `src/cli/fleet-sync-vault.ts` |
|
|
71
|
+
| `worker:git-identity` | `src/git-identity.ts` |
|
|
72
|
+
| `worker:git-push-auth` | `src/definition.ts`, `src/git-identity.ts` |
|
|
73
|
+
| `worker:github-principal` | `src/worker-access-checks.ts` |
|
|
74
|
+
| `worker:proxy-authentication` | `src/worker-access-checks.ts` |
|
|
75
|
+
| `worker:vault-renewal` | `src/worker-access-checks.ts` |
|
|
76
|
+
|
|
77
|
+
<!-- end: checks -->
|
|
78
|
+
|
|
79
|
+
## Timers
|
|
80
|
+
|
|
81
|
+
<!-- generated: timers -->
|
|
82
|
+
|
|
83
|
+
| Timer | Declared in | Schedule |
|
|
84
|
+
| -------------------------------- | -------------------------------------- | ------------------------------------- |
|
|
85
|
+
| `personal-fleet-disk-gc.timer` | `ansible/ubuntu/tasks/resources.yml` | OnBootSec=10min, OnCalendar=hourly |
|
|
86
|
+
| `personal-worker-auth.timer` | `ansible/ubuntu/tasks/credentials.yml` | OnBootSec=2min, OnUnitActiveSec=10min |
|
|
87
|
+
| `personal-worker-recovery.timer` | `ansible/ubuntu/tasks/recovery.yml` | OnBootSec=1min, OnUnitActiveSec=1min |
|
|
88
|
+
| `personal-worker-recovery.timer` | `ansible/ubuntu/tasks/recovery.yml` | OnBootSec=5min, OnUnitActiveSec=15min |
|
|
89
|
+
|
|
90
|
+
<!-- end: timers -->
|
package/docs/declared.md
CHANGED
|
@@ -31,6 +31,8 @@ may only shrink.
|
|
|
31
31
|
| Ansible on a worker | `ansible-core` in `ansible/ubuntu/policy.json` | Exact version | `ansible/ubuntu/tasks/packages.yml` | `package:ansible-core` |
|
|
32
32
|
| The CLI proxy | `cliproxy/compose.yaml`, `image.lock.json`, `requirements.lock`, `launchd/`, `config.template.yaml` | Image digests; hashed Python dependencies | `devfleet cliproxy install`, and `fleet-sync` after it | The proxy's audit |
|
|
33
33
|
| Agent profiles and T3 providers | `agents/registry.json`, `agents/t3.json` | The commit | `fleet-sync` | Profile receipts; the plan after every apply |
|
|
34
|
+
| Worker time service | `ansible/ubuntu/tasks/services.yml`, offsets in `clock` in `ansible/ubuntu/policy.json` | The commit | `fleet-sync` | `worker:clock` |
|
|
35
|
+
| The manager's container watchdog | `ansible/monitoring/container-watchdog.py`, `containerWatchdog` in `ansible/macos/policy.json` | The commit | `fleet-sync` (`hub-darwin.yml`) | `manager:container-watchdog` |
|
|
34
36
|
| Docker Desktop on the manager | `docker_desktop_*` under `[vars]` in `mise.toml` | Version and build | You | `manager:docker` (warns) |
|
|
35
37
|
| CI | `.github/workflows/` | Each action at a commit; tools from `mise.lock` | GitHub | `features/rules/` |
|
|
36
38
|
|