@crvouga/dev-fleet 0.7.0 → 0.9.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 +88 -7
- package/agents/audit.md +18 -18
- package/ansible/agents/agents.yml +134 -0
- package/ansible/agents/templates/antigravity.settings.json.j2 +3 -0
- package/ansible/agents/templates/claude.settings.json.j2 +3 -0
- package/ansible/agents/templates/codex.config.toml.j2 +11 -0
- package/ansible/agents/templates/gemini.settings.json.j2 +14 -0
- package/ansible/agents/templates/grok.config.toml.j2 +19 -0
- package/ansible/agents/templates/opencode.json.j2 +24 -0
- package/ansible/agents/templates/receipt.json.j2 +10 -0
- package/ansible/ansible.cfg +14 -0
- package/ansible/github/git.yml +82 -0
- package/ansible/macos/manager-privilege.yml +83 -0
- package/ansible/macos/manager.yml +164 -9
- package/ansible/macos/policy.json +13 -0
- package/ansible/macos/t3-policy.yml +5 -3
- package/ansible/macos/vault-renew.yml +85 -0
- package/ansible/macos/worker-host-key.yml +33 -0
- package/ansible/monitoring/agent-darwin.yml +3 -0
- package/ansible/monitoring/agent.yml +2 -0
- package/ansible/monitoring/hub-darwin.yml +3 -0
- package/ansible/monitoring/hub-route-darwin.yml +1 -0
- package/ansible/net/remote-probe.sh +4 -1
- package/ansible/platforms.json +2 -2
- package/ansible/pyproject.toml +5 -0
- package/ansible/ubuntu/baseline.yml +39 -13
- package/ansible/ubuntu/disk-gc.py +6 -2
- package/ansible/ubuntu/firewall.nft.j2 +50 -0
- package/ansible/ubuntu/policy.json +125 -6
- package/ansible/ubuntu/reboot.yml +89 -0
- package/ansible/ubuntu/recovery.yml +13 -3
- package/ansible/ubuntu/t3/policy.json +3 -0
- package/ansible/ubuntu/t3-desktop.yml +23 -2
- package/ansible/ubuntu/t3-headless.yml +128 -0
- package/ansible/ubuntu/t3-health.sh +24 -10
- package/ansible/ubuntu/t3-idle.py +37 -2
- package/ansible/ubuntu/t3-policy.yml +4 -1
- package/ansible/ubuntu/t3-route.yml +1 -0
- package/ansible/ubuntu/tailscale.yml +54 -8
- package/ansible/ubuntu/tasks/browser.yml +3 -0
- package/ansible/ubuntu/tasks/containers-off.yml +43 -0
- package/ansible/ubuntu/tasks/docker.yml +53 -6
- package/ansible/ubuntu/tasks/dpkg.yml +4 -0
- package/ansible/ubuntu/tasks/features.yml +22 -0
- package/ansible/ubuntu/tasks/firewall.yml +172 -0
- package/ansible/ubuntu/tasks/install.yml +2 -1
- package/ansible/ubuntu/tasks/packages.yml +4 -2
- package/ansible/ubuntu/tasks/release.yml +6 -0
- package/ansible/ubuntu/tasks/resources.yml +6 -4
- package/ansible/ubuntu/tasks/services.yml +2 -2
- package/ansible/ubuntu/tasks/sizing.yml +49 -0
- package/ansible/ubuntu/tasks/snapshot.yml +14 -3
- package/ansible/ubuntu/tasks/ssh.yml +61 -4
- package/ansible/ubuntu/tasks/t3-restart.yml +3 -1
- package/ansible/ubuntu/tasks/user.yml +1 -0
- package/ansible/ubuntu/worker-account.yml +117 -0
- package/ansible/uv.lock +265 -0
- package/bin/devfleet.mjs +42 -4
- package/bootstrap/fleet-remote-bootstrap.sh +91 -25
- package/bootstrap/global-config.ts +29 -3
- package/bootstrap/package-smoke.ts +127 -0
- package/bootstrap/pins.ts +189 -0
- package/bootstrap/release-notes.ts +88 -0
- package/bootstrap/release-version.ts +41 -7
- package/bootstrap/worker-features.ts +114 -0
- package/bootstrap/worker-seed.yaml +65 -0
- package/bootstrap/worker-tools.ts +39 -9
- package/cliproxy/README.md +1 -1
- package/cliproxy/docs/health-workflow.example.yml +20 -143
- package/docs/authentication.md +21 -11
- package/docs/consumer-setup.md +74 -58
- package/docs/convergence.md +67 -0
- package/docs/declared.md +61 -34
- package/docs/machine-setup.md +73 -7
- package/docs/manager-setup.md +34 -0
- package/docs/operations.md +64 -9
- package/docs/releasing.md +49 -1
- package/docs/runbooks.md +112 -7
- package/docs/state.md +23 -11
- package/docs/support-matrix.md +40 -4
- package/docs/topology.md +82 -12
- package/docs/ubuntu-sync.md +98 -42
- package/docs/worker-contract.md +122 -19
- package/docs/worker-requirements.md +107 -0
- package/examples/fleet.json +1 -0
- package/examples/workers-from-infrastructure/main.tf +132 -0
- package/examples/workers-from-infrastructure/worker-records.json +14 -0
- package/llms.txt +3 -1
- package/mise.lock +29 -0
- package/mise.toml +5 -3
- package/package.json +2 -2
- package/schema/check-ids.json +1991 -162
- package/schema/check-ids.schema.json +34 -0
- package/schema/fleet.schema.json +349 -10
- package/schema/output/check.schema.json +1 -1
- package/schema/output/fleet-state-definition.schema.json +14 -0
- package/schema/output/fleet-sync-schedule.schema.json +26 -0
- package/schema/output/fleet-sync.schema.json +18 -0
- package/schema/output/report.schema.json +3 -2
- package/schema/worker-record.schema.json +76 -0
- package/src/agents/converge.ts +140 -24
- package/src/agents/files.ts +2 -32
- package/src/agents/native-verify.ts +34 -3
- package/src/agents/secrets.ts +39 -4
- package/src/check-owners.ts +62 -2
- package/src/check-registry.ts +630 -0
- package/src/cli/agent-guide.ts +3 -3
- package/src/cli/cli.ts +4 -0
- package/src/cli/fleet-manifest.ts +108 -0
- package/src/cli/fleet-node-lifecycle.ts +704 -0
- package/src/cli/fleet-node-park.ts +238 -0
- package/src/cli/fleet-node-seed.ts +167 -0
- package/src/cli/fleet-state.ts +2 -1
- package/src/cli/fleet-sync-checkout.ts +7 -0
- package/src/cli/fleet-sync-enrollment.ts +193 -32
- package/src/cli/fleet-sync-forward.ts +6 -0
- package/src/cli/fleet-sync-manager.ts +123 -8
- package/src/cli/fleet-sync-output.ts +31 -11
- package/src/cli/fleet-sync-plan.ts +65 -9
- package/src/cli/fleet-sync-records.ts +113 -0
- package/src/cli/fleet-sync-remote.ts +172 -15
- package/src/cli/fleet-sync-schedule.ts +14 -4
- package/src/cli/fleet-sync-state.ts +31 -10
- package/src/cli/fleet-sync-vault.ts +152 -26
- package/src/cli/fleet-sync-worker.ts +214 -0
- package/src/cli/fleet-sync.ts +172 -21
- package/src/cli/fleet-validate.ts +277 -0
- package/src/cli/fleet-watch.ts +7 -1
- package/src/cli/fleet.ts +87 -5
- package/src/cli/managed-health.ts +25 -1
- package/src/cli/register-fleet.ts +251 -4
- package/src/cli/registry.ts +47 -0
- package/src/cli/tailnet.ts +43 -10
- package/src/cli/ui-data.ts +3 -1
- package/src/cli/worker-usage-report.ts +63 -0
- package/src/clock.ts +42 -0
- package/src/definition.ts +762 -232
- package/src/diagnostics.ts +5 -2
- package/src/exposure.ts +102 -0
- package/src/fleet-config.ts +3 -1
- package/src/git-identity.ts +76 -3
- package/src/ide/ide.ts +5 -0
- package/src/ide/t3.ts +12 -2
- package/src/input-hashes.ts +6 -0
- package/src/machine/machine-class.ts +59 -0
- package/src/machine/platform.ts +90 -11
- package/src/machine/state.ts +70 -16
- package/src/machine/support-matrix.ts +11 -9
- package/src/machine-health.ts +28 -5
- package/src/manager-health.ts +148 -2
- package/src/manager-sync.ts +89 -8
- package/src/monitoring/beszel.ts +8 -8
- package/src/monitoring/enroll.ts +8 -1
- package/src/monitoring/hub-access.ts +10 -4
- package/src/monitoring/systems.ts +138 -0
- package/src/network-definition.ts +13 -4
- package/src/node-converge-linux.ts +136 -27
- package/src/node-converge.ts +13 -3
- package/src/platform/ansible-receipt.ts +17 -2
- package/src/platform/ansible.ts +40 -4
- package/src/platform/manager-host.ts +25 -8
- package/src/platform/resource-verify.ts +18 -7
- package/src/platform/t3-policy.ts +24 -10
- package/src/platform/t3-service.ts +109 -34
- package/src/platform/ubuntu-release.ts +24 -0
- package/src/platform/ubuntu-verify.ts +31 -3
- package/src/platform/ubuntu-worker.ts +58 -0
- package/src/platform/worker-platform.ts +72 -0
- package/src/protocol.ts +1 -1
- package/src/public-reachability.ts +186 -0
- package/src/remote-bootstrap.ts +50 -12
- package/src/remote-operation.ts +18 -4
- package/src/repair-plan.ts +23 -12
- package/src/report.ts +2 -1
- package/src/schedule-check.ts +21 -2
- package/src/secrets.ts +7 -0
- package/src/service-auth.ts +55 -48
- package/src/ssh-connection.ts +141 -0
- package/src/ssh.ts +63 -24
- package/src/state/store.ts +4 -2
- package/src/state/sync.ts +35 -6
- package/src/state/workers.ts +140 -0
- package/src/t3/pairing.ts +59 -0
- package/src/t3/peer-link.ts +333 -0
- package/src/t3/version.ts +1 -1
- package/src/tailnet-cleanup.ts +185 -0
- package/src/tailnet-control.ts +346 -0
- package/src/tailscale.ts +1 -1
- package/src/toolchain-checks.ts +11 -1
- package/src/types.ts +4 -2
- package/src/worker-account.ts +63 -0
- package/src/worker-activity.ts +303 -0
- package/src/worker-capacity.ts +31 -0
- package/src/worker-credentials.ts +201 -0
- package/src/worker-exposure.ts +388 -0
- package/src/worker-features.ts +201 -0
- package/src/worker-host-checks.ts +71 -20
- package/src/worker-manifest.ts +224 -0
- package/src/worker-parking.ts +70 -0
- package/src/worker-policy.ts +38 -36
- package/src/worker-readiness.ts +15 -3
- package/src/worker-reboot.ts +129 -0
- package/src/worker-records.ts +305 -0
- package/src/worker-seed.ts +143 -0
- package/src/worker-sizing.ts +76 -0
- package/src/cli/schedule-logs.ts +0 -36
package/docs/support-matrix.md
CHANGED
|
@@ -13,11 +13,12 @@ rest. The default is one macOS manager and Ubuntu workers treated as cattle.
|
|
|
13
13
|
| -------------- | --------- | ------------------- | --------------- | ----------- | ----------------------------------------------------------------------------------------- |
|
|
14
14
|
| manager | macOS | 14 or newer | arm64, x86_64 | supported | ansible/macos/policy.json |
|
|
15
15
|
| worker | Ubuntu | 26.04 (resolute) | amd64, arm64 | supported | ansible/ubuntu/policy.json; a machine of its own, physical or virtual |
|
|
16
|
+
| worker | Ubuntu | 24.04 (noble) | amd64, arm64 | supported | ansible/ubuntu/policy.json; a machine of its own, physical or virtual |
|
|
16
17
|
| manager | any other | any | any | refused | The fleet configuration accepts only a macOS manager. |
|
|
17
18
|
| worker | Ubuntu | any other release | any | refused | UBUNTU_BASELINE_MISMATCH |
|
|
18
19
|
| worker | Ubuntu | an accepted release | any other | refused | UBUNTU_BASELINE_MISMATCH |
|
|
19
|
-
| worker | Debian | any | any | refused | UBUNTU_BASELINE_MISMATCH
|
|
20
|
-
| worker | macOS | any | any | refused | The fleet configuration accepts only Linux workers.
|
|
20
|
+
| worker | Debian | any | any | refused | UBUNTU_BASELINE_MISMATCH; not yet supported (see below). |
|
|
21
|
+
| worker | macOS | any | any | refused | The fleet configuration accepts only Linux workers; not yet supported (see below). |
|
|
21
22
|
| worker | Windows | any | any | refused | The fleet configuration accepts only Linux workers. |
|
|
22
23
|
| worker | WSL2 | any | any | refused | WORKER_ENVIRONMENT_UNSUPPORTED |
|
|
23
24
|
| worker | container | any | any | refused | WORKER_ENVIRONMENT_UNSUPPORTED |
|
|
@@ -32,5 +33,40 @@ refuses a worker that is not Linux, the platform probe refuses another
|
|
|
32
33
|
release, CPU or environment (`ubuntu:release`, `platform:environment`), and
|
|
33
34
|
the bootstrap and the Ubuntu baseline refuse them again on the machine.
|
|
34
35
|
|
|
35
|
-
|
|
36
|
-
|
|
36
|
+
## Adding a platform or a release
|
|
37
|
+
|
|
38
|
+
What differs from one worker platform to another is named by the
|
|
39
|
+
`WorkerPlatform` interface (`src/platform/worker-platform.ts`): its policy and
|
|
40
|
+
playbooks, its package manager and pins, its Tailscale key, its service
|
|
41
|
+
manager, its session model and its host checks. Ubuntu is the one
|
|
42
|
+
implementation (`src/platform/ubuntu-worker.ts`).
|
|
43
|
+
|
|
44
|
+
Another Ubuntu release is one entry in `releases` of
|
|
45
|
+
`ansible/ubuntu/policy.json`: its version, codename and architectures, its
|
|
46
|
+
package and browser library pins read from the snapshot's Packages indexes
|
|
47
|
+
for that release (`packages`, `browserPackages`), and `tailscaleKeySha256`
|
|
48
|
+
only when Tailscale signs its suite with another key than the common one.
|
|
49
|
+
`ansible/ubuntu/tasks/release.yml` selects the entry from the facts, so the
|
|
50
|
+
playbooks need no change of their own. The release is supported once the
|
|
51
|
+
fresh-box features pass on it: CI runs them for every accepted release
|
|
52
|
+
(`DEVFLEET_TEST_FRESH_BOX_RELEASE`), with its image pinned in
|
|
53
|
+
`features/fresh-box/Dockerfile`.
|
|
54
|
+
|
|
55
|
+
## Not yet supported
|
|
56
|
+
|
|
57
|
+
These are refused today. What each would need, beyond a row here:
|
|
58
|
+
|
|
59
|
+
- **Debian workers.** A `WorkerPlatform` of its own with a Debian policy:
|
|
60
|
+
pins read from snapshot.debian.org for each release, Tailscale's key digest
|
|
61
|
+
for the Debian suite, the platform probe accepting `ID=debian`, and the
|
|
62
|
+
bootstrap and `tasks/release.yml` accepting it in place of
|
|
63
|
+
`UBUNTU_BASELINE_MISMATCH`. The playbooks assume Ubuntu's archive keyring,
|
|
64
|
+
snapshot service and unattended-upgrades origins, and the GNOME desktop
|
|
65
|
+
T3 runs in; each needs a Debian equivalent. Then a fresh-box image and job.
|
|
66
|
+
- **macOS workers.** Everything a worker platform names differs: launchd in
|
|
67
|
+
place of systemd as the service manager, a logged-in user session in place
|
|
68
|
+
of a lingering systemd user manager, Homebrew or pinned downloads in place
|
|
69
|
+
of APT, power and sleep policy through `pmset`, and host checks of their
|
|
70
|
+
own. The fleet configuration would accept `macOS` for a worker, and the
|
|
71
|
+
convergence would run playbooks under `ansible/macos` for it. No container
|
|
72
|
+
can stand in for a fresh macOS machine, so the test would be a macOS runner.
|
package/docs/topology.md
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
> This document was written for the fleet dev-fleet was extracted from. Paths and
|
|
2
2
|
> commands were updated; some statements still describe that fleet and are being
|
|
3
|
-
> revised. The README is authoritative
|
|
3
|
+
> revised. The README is authoritative, and what a machine must offer to be a
|
|
4
|
+
> worker is listed only in [worker requirements](worker-requirements.md).
|
|
4
5
|
|
|
5
6
|
# Fleet topology: one manager, many workers
|
|
6
7
|
|
|
@@ -8,10 +9,10 @@ The fleet has exactly two roles, declared in
|
|
|
8
9
|
your project's `fleet.json` (version 3, checked by [the schema](../schema/fleet.schema.json)) and validated by
|
|
9
10
|
[`src/definition.ts`](../src/definition.ts):
|
|
10
11
|
|
|
11
|
-
| Role | How many | Machine
|
|
12
|
-
| ----------- | ----------- |
|
|
13
|
-
| **manager** | exactly one | the human's macOS machine (`manager`)
|
|
14
|
-
| **workers** | any | Linux boxes (`workers`, Ubuntu 26.04) | no | the T3 desktop app (UI plus server), agent tools, the Beszel agent; clients only |
|
|
12
|
+
| Role | How many | Machine | Holds state | Runs |
|
|
13
|
+
| ----------- | ----------- | ---------------------------------------------- | ----------- | ---------------------------------------------------------------------------------------- |
|
|
14
|
+
| **manager** | exactly one | the human's macOS machine (`manager`) | yes | `fleet-sync`, the T3 desktop that links to every worker, and **every singleton service** |
|
|
15
|
+
| **workers** | any | Linux boxes (`workers`, Ubuntu 26.04 or 24.04) | no | the T3 desktop app (UI plus server), agent tools, the Beszel agent; clients only |
|
|
15
16
|
|
|
16
17
|
## Manager
|
|
17
18
|
|
|
@@ -35,9 +36,12 @@ your project's `fleet.json` (version 3, checked by [the schema](../schema/fleet.
|
|
|
35
36
|
A new singleton service is declared under `services` and placed on the
|
|
36
37
|
manager; it never names a host.
|
|
37
38
|
- Workers depend on it, so it never sleeps on AC power
|
|
38
|
-
(`pmset` in `ansible/macos/policy.json`, checked as `manager:power
|
|
39
|
-
|
|
40
|
-
|
|
39
|
+
(`pmset` in `ansible/macos/policy.json`, checked as `manager:power` and
|
|
40
|
+
applied by `ansible/macos/manager.yml` on every `fleet-sync` once an
|
|
41
|
+
administrator has given the one grant `ansible/macos/manager-privilege.yml`;
|
|
42
|
+
the failed check prints the command, and
|
|
43
|
+
[docs/manager-setup.md](manager-setup.md) lists every step only a person
|
|
44
|
+
takes). While the
|
|
41
45
|
manager is asleep, closed or offline, workers have no CLI proxy and no
|
|
42
46
|
dashboard; nothing fails over, because a singleton never runs twice.
|
|
43
47
|
- Its state (proxy OAuth/runtime state, the hub database) is backed up off the
|
|
@@ -48,7 +52,9 @@ your project's `fleet.json` (version 3, checked by [the schema](../schema/fleet.
|
|
|
48
52
|
|
|
49
53
|
- Stateless cattle: turn one off, re-image it or add another at will. A worker
|
|
50
54
|
is declared in `workers`, enrolled once (SSH trust, sudo, its own Vault
|
|
51
|
-
token), and then fully rebuilt by `fleet-sync` from Git and Vault.
|
|
55
|
+
token), and then fully rebuilt by `fleet-sync` from Git and Vault. What a
|
|
56
|
+
machine must offer before that is listed in
|
|
57
|
+
[worker requirements](worker-requirements.md).
|
|
52
58
|
- Workers never host a singleton service. The proxy reports any local install
|
|
53
59
|
on a worker as a duplicate owner, and the hub refuses to install anywhere but
|
|
54
60
|
the manager.
|
|
@@ -56,13 +62,77 @@ your project's `fleet.json` (version 3, checked by [the schema](../schema/fleet.
|
|
|
56
62
|
checkouts. Durable work lives in Git remotes; credentials come from Vault.
|
|
57
63
|
Work in progress in agent worktrees is pushed to a recovery ref every 15
|
|
58
64
|
minutes; [durability](durability.md) says what a lost machine takes with it.
|
|
59
|
-
-
|
|
60
|
-
fleet
|
|
61
|
-
|
|
65
|
+
- One worker is the rollout canary for a revision new to the fleet: the one
|
|
66
|
+
`fleet-sync --canary` or `canary` in `fleet.json` names, else the first
|
|
67
|
+
declared worker. When it cannot be reached (powered off, being rebuilt),
|
|
68
|
+
the next reachable worker in declared order proves the revision instead and
|
|
69
|
+
the unreachable one is reported as offline, never as a blocked rollout. A
|
|
70
|
+
canary that was deployed and failed stops promotion to the other workers.
|
|
71
|
+
Each worker's applied revision is kept in the fleet state, so one that
|
|
72
|
+
converged keeps its progress when another did not. Repeating a revision
|
|
73
|
+
that already converged everywhere runs all workers at once.
|
|
62
74
|
|
|
63
75
|
## Changing the topology
|
|
64
76
|
|
|
65
77
|
- **Add or replace a worker:** add it to `workers`, enroll it with
|
|
66
78
|
`fleet-sync`, done. Remove it by deleting the entry.
|
|
79
|
+
- **Workers an infrastructure tool creates:** `workerRecords` in `fleet.json`
|
|
80
|
+
names a file (relative to `fleet.json`) or a command that returns worker
|
|
81
|
+
records, the provider-neutral description of a machine in
|
|
82
|
+
[`schema/worker-record.schema.json`](../schema/worker-record.schema.json):
|
|
83
|
+
`id`, `tailnetName` or `address`, `user`, `port`, `hostKey` and optional
|
|
84
|
+
`labels` and `billing`. Their workers join the inline list; dev-fleet only
|
|
85
|
+
reads them and never calls a provider or creates a machine. A record that
|
|
86
|
+
appears is a declared worker, enrolled by the next `fleet-sync`; one that
|
|
87
|
+
disappears (a file that is gone holds none) is retired by the next
|
|
88
|
+
`fleet-sync` exactly as `fleet-node-retire` retires one. A worker listed by
|
|
89
|
+
hand is never retired by itself. An invalid record is refused naming its
|
|
90
|
+
field, such as `$.workerRecords[0].workers[1].hostKey`.
|
|
91
|
+
[`examples/workers-from-infrastructure`](../examples/workers-from-infrastructure/main.tf)
|
|
92
|
+
shows the handoff with a local virtual machine, the seed going in and the
|
|
93
|
+
record coming out. The canary stays the one `canary` names, so the order
|
|
94
|
+
of generated records means nothing.
|
|
95
|
+
- **Replace or retire a worker:** `fleet-node-replace` and `fleet-node-retire`
|
|
96
|
+
take everything of the old machine with it: its pinned host key, its
|
|
97
|
+
monitoring system (pointed at the new machine, or deleted), the manager's
|
|
98
|
+
T3 link to its environment, its Vault token and its tailnet device. The
|
|
99
|
+
manager's T3 pairs with the replacement's environment by itself
|
|
100
|
+
(`t3:link`): at once when its T3 runs, otherwise in the next `fleet-sync`
|
|
101
|
+
once it does, and the roster records the new environment. The pairing code
|
|
102
|
+
is minted on the worker over SSH and handed to the manager's own T3 server
|
|
103
|
+
in a request body, never on a command line, in a report or in a log. The device is deleted through the
|
|
104
|
+
control plane fleet.json declares under `tailnet`, asked with the secret
|
|
105
|
+
`tailnet-admin` (workers are not registered as ephemeral devices); without
|
|
106
|
+
either, `tailscale:device` is skipped and names the device to remove in the
|
|
107
|
+
admin console. `fleet-status --workers` warns about a monitoring system that
|
|
108
|
+
matches no declared worker (`beszel:leftover`), and `fleet-status` about a
|
|
109
|
+
device carrying the worker tag that is no declared worker, parked or not,
|
|
110
|
+
and no device the roster binds (`tailscale:leftover`).
|
|
111
|
+
|
|
112
|
+
## Parked and idle workers
|
|
113
|
+
|
|
114
|
+
A rented worker costs money while it runs. `devfleet fleet-node-park --id ID`
|
|
115
|
+
records in the fleet state that it is stopped or destroyed on purpose: from
|
|
116
|
+
then on `fleet-sync` and `fleet-status` list it as parked with the time since
|
|
117
|
+
(`worker:parked`, not applicable), never contact it, never make it the
|
|
118
|
+
canary, and it never changes an exit code. Parking refuses while agent turns
|
|
119
|
+
run on it (`WORKER_BUSY`) unless `--force`. `devfleet fleet-node-resume --id
|
|
120
|
+
ID` marks it active and runs `fleet-sync --node ID`, which converges and
|
|
121
|
+
verifies it. `parking.stop` and `parking.start` in `fleet.json` name commands
|
|
122
|
+
(an infrastructure tool, never a provider dev-fleet calls) run with the worker
|
|
123
|
+
ID as their last argument. A parked worker found online on the tailnet is a
|
|
124
|
+
warning (`worker:parked-online`); one that comes back as a new machine goes
|
|
125
|
+
through `fleet-node-replace`.
|
|
126
|
+
|
|
127
|
+
`fleet-status` reports, for every worker it reaches, its uptime, the agent
|
|
128
|
+
turns under way and the time of the last agent activity, read from what T3
|
|
129
|
+
records (status and times, never a session's content), in its text and as
|
|
130
|
+
`workers` in its JSON. With `parking.idleAfterHours`, a worker with no agent
|
|
131
|
+
activity for longer is `worker:idle` (a warning whose remedy is to park it);
|
|
132
|
+
one with a turn under way never is. `billing.hourlyRate` on a worker (or its
|
|
133
|
+
record) is only displayed and summed: `usage` says how many workers run, how
|
|
134
|
+
many are idle and what the idle ones cost per day. `parking.autoPark: true`
|
|
135
|
+
lets `fleet-watch` park idle workers by itself; it is off unless set.
|
|
136
|
+
|
|
67
137
|
- **Replace the manager:** follow
|
|
68
138
|
[replacing the manager](manager-recovery.md).
|
package/docs/ubuntu-sync.md
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
> This document was written for the fleet dev-fleet was extracted from. Paths and
|
|
2
2
|
> commands were updated; some statements still describe that fleet and are being
|
|
3
|
-
> revised. The README is authoritative
|
|
3
|
+
> revised. The README is authoritative, and what a machine must offer to be a
|
|
4
|
+
> worker is listed only in [worker requirements](worker-requirements.md).
|
|
4
5
|
|
|
5
6
|
# Developer fleet: one manager (macOS) and stateless workers (Ubuntu)
|
|
6
7
|
|
|
@@ -10,7 +11,7 @@ What is proven, and where:
|
|
|
10
11
|
|
|
11
12
|
| Evidence | Where |
|
|
12
13
|
| ------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------- |
|
|
13
|
-
| A bare Ubuntu
|
|
14
|
+
| A bare Ubuntu machine bootstrapped over real SSH, then repeated | `features/fresh-box/fresh-box.feature`, on every pull request (each accepted release, amd64 and arm64) |
|
|
14
15
|
| One run over several machines, one versioned report, no partial receipt | `features/fresh-box/fleet.feature` |
|
|
15
16
|
| Interrupted checkout, package installation and toolchain setup resumed | `features/fresh-box/fleet.feature` |
|
|
16
17
|
| Overlapping runs, a dropped connection, a held APT lock, no DNS, no disk | `features/fresh-box/fleet.feature` |
|
|
@@ -86,9 +87,11 @@ existing SSH aliases, tests strict SSH access, and offers one enrollment
|
|
|
86
87
|
confirmation for the verified connections. The apply approval binds a fingerprint
|
|
87
88
|
of the selected revision, policy and connection records; changed bindings fail
|
|
88
89
|
with `FLEET_PLAN_STALE` before source preparation or remote mutation. It stores private overrides mode 0600. Unknown host keys are never accepted automatically. Compare fingerprints
|
|
89
|
-
out of band before enrolling trust. Unreachable machines remain listed;
|
|
90
|
-
|
|
91
|
-
|
|
90
|
+
out of band before enrolling trust. Unreachable machines remain listed; what a machine
|
|
91
|
+
needs to be reachable the first time (SSH with a key, a known host key, the
|
|
92
|
+
tailnet, or first-boot data that provides them) is listed in
|
|
93
|
+
[worker requirements](worker-requirements.md). No software on the manager can
|
|
94
|
+
bootstrap a disconnected box.
|
|
92
95
|
|
|
93
96
|
A previously configured manager uses saved connection bindings and its approved
|
|
94
97
|
revision. A changed Tailscale device ID is a replacement gate, even if the old
|
|
@@ -177,37 +180,43 @@ independently. Bespoke TypeScript remains only where a native CLI or API has no
|
|
|
177
180
|
declarative owner: T3 service lifecycle, Beszel enrollment tokens, provider
|
|
178
181
|
verification and the CLIProxy singleton.
|
|
179
182
|
|
|
180
|
-
| Policy | Workers (Ubuntu 26.04, stateless)
|
|
181
|
-
| --------------------------- |
|
|
182
|
-
| Never sleep | systemd sleep targets masked; logind lid/idle policy; GNOME power plugin set to `nothing`
|
|
183
|
-
| No automatic logout or lock | GNOME `lock-enabled false`, `idle-delay 0`; designated-user lingering
|
|
184
|
-
| Always-on services | drop-ins `Restart=always`, `StartLimitIntervalSec=0` for ssh, tailscaled, docker, beszel-agent
|
|
185
|
-
| T3 health and connection | `fleet-sync` and `fleet-status --workers` require `worker:t3-health` (local API answers), `t3:manager-endpoint` (manager HTTPS over Tailscale) and `t3:paired-access` (manager T3 link reachable)
|
|
186
|
-
| Memory safety | `systemd-oomd` (swap 90%, pressure 60%); `user.slice` `MemoryMax=90%`; each `t3code-*.scope` agent `MemoryMax=30%`, `OOMPolicy=continue`; swap file when none
|
|
187
|
-
| Disk safety | `personal-fleet-disk-guard` kills only the top user-cgroup writer below 3 GiB free, never deletes files; hourly `personal-fleet-disk-gc.timer` deletes git-ignored build output (`node_modules`, `.cache`, `dist`, ...) in checkouts idle 7d (12h under 15 GiB free) with no process inside, plus unused Docker images and build cache, never source, worktrees or volumes; journal 1G; `/tmp` 25% of RAM, aged 2d hourly, a measured write probe (`TMP_QUOTA_EXHAUSTED`), and T3 agents use disk-backed `/var/tmp` (`TMPDIR`, aged 14d); Docker log rotation
|
|
188
|
-
| Tailscale | pinned APT package, `tailscaled` enabled, native login required
|
|
189
|
-
| Dashboard agent | pinned native binary, hardened `beszel-agent` systemd unit as user `beszel`
|
|
190
|
-
| Credential renewal | user timer `personal-worker-auth` renews the node's own Vault token every 10min; `personal-worker-github` signs it in to GitHub from the stored token ([GitHub](github.md))
|
|
191
|
-
| Noninteractive sudo | validated `/etc/sudoers.d/personal-fleet-USER`
|
|
192
|
-
| T3 worker app | pinned desktop `.deb` is the only T3 process (UI plus embedded server); GDM auto-login, user unit `t3code-desktop.service` bound to the graphical session with `Restart=always`; `personal-worker-recovery` runs a copy of `t3-health.sh` installed in `/usr/local/lib/personal-fleet` every minute: it stops and disables a still-running headless `t3code.service`, reports `T3_NO_GRAPHICAL_SESSION` and restarts the display manager (three times at most) when there is no session, starts a stopped app after `reset-failed`, restarts one whose local API missed three checks, and restarts one on another release than the installed one or with a deferred policy restart once it runs no agent turn; administrator policy | not a worker |
|
|
193
|
-
| T3 providers | the same managed instances (`fleet/agents/t3.json`) through locked T3 administrator policy
|
|
194
|
-
| Singleton services | never; clients of the manager's services only
|
|
183
|
+
| Policy | Workers (Ubuntu 26.04 or 24.04, stateless) | Manager (macOS, singleton) |
|
|
184
|
+
| --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
185
|
+
| Never sleep | systemd sleep targets masked; logind lid/idle policy; GNOME power plugin set to `nothing` | never sleeps on AC power (`pmset -c sleep 0`, `macos/manager.yml`, verified as `manager:power`); battery sleep is left to the human |
|
|
186
|
+
| No automatic logout or lock | GNOME `lock-enabled false`, `idle-delay 0`; designated-user lingering | not managed (the human uses it) |
|
|
187
|
+
| Always-on services | drop-ins `Restart=always`, `StartLimitIntervalSec=0` for ssh, tailscaled, docker, beszel-agent | singleton services under launchd and Docker Desktop (see the last row) |
|
|
188
|
+
| T3 health and connection | `fleet-sync` and `fleet-status --workers` require `worker:t3-health` (local API answers), `t3:manager-endpoint` (manager HTTPS over Tailscale) and `t3:paired-access` (manager T3 link reachable) | the human's T3 desktop holds the links to every worker (`t3:paired-access`) |
|
|
189
|
+
| Memory safety | `systemd-oomd` (swap 90%, pressure 60%); `user.slice` `MemoryMax=90%`; each `t3code-*.scope` agent `MemoryMax=30%`, `OOMPolicy=continue`; swap file when none | not managed |
|
|
190
|
+
| Disk safety | `personal-fleet-disk-guard` kills only the top user-cgroup writer below 3 GiB free, never deletes files; hourly `personal-fleet-disk-gc.timer` deletes git-ignored build output (`node_modules`, `.cache`, `dist`, ...) in checkouts idle 7d (12h under 15 GiB free) with no process inside, plus unused Docker images and build cache, never source, worktrees or volumes; journal 1G; `/tmp` 25% of RAM, aged 2d hourly, a measured write probe (`TMP_QUOTA_EXHAUSTED`), and T3 agents use disk-backed `/var/tmp` (`TMPDIR`, aged 14d); Docker log rotation | not managed |
|
|
191
|
+
| Tailscale | pinned APT package, `tailscaled` enabled, native login required | Tailscale app, human login |
|
|
192
|
+
| Dashboard agent | pinned native binary, hardened `beszel-agent` systemd unit as user `beszel` | not installed |
|
|
193
|
+
| Credential renewal | user timer `personal-worker-auth` renews the node's own Vault token every 10min; `personal-worker-github` signs it in to GitHub from the stored token ([GitHub](github.md)) | the human's own Vault and GitHub sessions |
|
|
194
|
+
| Noninteractive sudo | validated `/etc/sudoers.d/personal-fleet-USER` | only `pmset -c`, granted once by `manager-privilege.yml` (`-K`) |
|
|
195
|
+
| T3 worker app | pinned desktop `.deb` is the only T3 process (UI plus embedded server) on a desktop worker, and the pinned server as the lingering user unit `t3code-headless.service` on a headless one (`features.session`; no session is waited for); GDM auto-login, user unit `t3code-desktop.service` bound to the graphical session with `Restart=always`; `personal-worker-recovery` runs a copy of `t3-health.sh` installed in `/usr/local/lib/personal-fleet` every minute: it stops and disables a still-running headless `t3code.service`, reports `T3_NO_GRAPHICAL_SESSION` and restarts the display manager (three times at most) when there is no session, starts a stopped app after `reset-failed`, restarts one whose local API missed three checks, and restarts one on another release than the installed one or with a deferred policy restart once it runs no agent turn; administrator policy | not a worker |
|
|
196
|
+
| T3 providers | the same managed instances (`fleet/agents/t3.json`) through locked T3 administrator policy | reconciled into the human's T3 settings by `fleet-sync` (`t3:providers`) with the ownership-safe `t3-apply` engine, so every T3 offers the identical set; unrelated and unmanaged instances are preserved. `fleet-sync` also applies the same proxy agent profiles (`configuration:apply`) and verifies the shared Vault gateway credential end to end with `provider:*:inference`, exactly as on workers |
|
|
197
|
+
| Singleton services | never; clients of the manager's services only | every singleton runs here: CLI proxy (`cliproxy`), Beszel hub (`workstation/monitoring`) |
|
|
195
198
|
|
|
196
199
|
Credential sync is deliberately indirect: no secret is copied between machines.
|
|
197
|
-
|
|
198
|
-
(`
|
|
199
|
-
`
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
200
|
+
Each worker has a Vault token of its own, kept in a document of its own,
|
|
201
|
+
`<mount>/<project>/workers/<id>` (fields `token` and `accessor`; `<project>`
|
|
202
|
+
is the first segment of the `proxy-client-key` path). The manager issues it
|
|
203
|
+
with `devfleet fleet-node-replace --id ID --apply --yes`: a periodic orphan
|
|
204
|
+
token under the policy `fleet-worker-<id>`, which reads the fleet's shared
|
|
205
|
+
document and the worker's own document and nothing else, so one worker's
|
|
206
|
+
token cannot read another's (`features/fleet-sync/worker-credentials.feature`
|
|
207
|
+
proves it against a Vault development server). The manager's own login needs
|
|
208
|
+
to write policies, create tokens and write the mount.
|
|
209
|
+
|
|
210
|
+
`devfleet fleet-sync` delivers each node's token over SSH stdin (never as an
|
|
211
|
+
argument) whenever that node has no working Vault session. A token still kept
|
|
212
|
+
in the shared document as `FLEET_WORKER_TOKEN_<NODE>` is delivered too, with
|
|
213
|
+
the warning `VAULT_WORKER_TOKEN_SHARED`: every worker can read that document,
|
|
214
|
+
so issue the worker its own token and remove the field. A password login with
|
|
215
|
+
the fleet's shared Vault user on the worker's own terminal happens only with
|
|
216
|
+
`fleet-sync --vault-password-login`. Without a token the node reports
|
|
217
|
+
`VAULT_LOGIN_REQUIRED` and its T3 proxy providers stay unavailable.
|
|
218
|
+
`devfleet fleet-node-retire --id ID --apply --yes` revokes a worker's token by
|
|
219
|
+
its accessor and removes its document and policy.
|
|
211
220
|
|
|
212
221
|
From then on the node's `personal-worker-auth` timer renews that token and its
|
|
213
222
|
`personal-worker-github` timer keeps its GitHub sign-in current
|
|
@@ -230,14 +239,58 @@ GNOME lock/idle changes apply to the designated user only. Effective running
|
|
|
230
239
|
session/logind behavior still needs a maintenance/reboot acceptance check; writing
|
|
231
240
|
a file does not prove logout/reboot survival. The sudoers fragment is validated
|
|
232
241
|
with visudo before installation; this user and its agents can administer the
|
|
233
|
-
machine.
|
|
242
|
+
machine.
|
|
243
|
+
|
|
244
|
+
SSH changes validate the complete configuration before reload and restore the
|
|
245
|
+
previous fragments on validation failure. sshd takes keys only and refuses
|
|
246
|
+
root (`10-personal-fleet-exposure.conf`, which sorts before an image's own
|
|
247
|
+
fragments; `SSH_HARDENING_OVERRIDDEN` when `sshd -T` still shows otherwise).
|
|
248
|
+
Existing listeners and socket activation are preserved; unmanaged fragments
|
|
249
|
+
require review. The manager makes a **new** strict SSH connection after apply.
|
|
250
|
+
|
|
251
|
+
### Exposure
|
|
252
|
+
|
|
253
|
+
Every worker, with a public address or without one, admits inbound traffic
|
|
254
|
+
only over loopback and the tailnet (`ansible/ubuntu/tasks/firewall.yml`, the
|
|
255
|
+
`inet personal_fleet` nftables table, loaded at boot by
|
|
256
|
+
`personal-fleet-firewall.service`). Open beyond it: tailscaled's UDP port,
|
|
257
|
+
DHCP replies, ICMP, and what the exposure policy opens. Where Docker is
|
|
258
|
+
installed, a published container port is dropped from outside the tailnet
|
|
259
|
+
before Docker's own chains see it. Docker's and Tailscale's tables are left to
|
|
260
|
+
their owners.
|
|
261
|
+
|
|
262
|
+
The policy is `exposure` in `fleet.json`, fleet-wide under `worker` and per
|
|
263
|
+
worker beside `network`; a field a worker names replaces the fleet-wide one:
|
|
264
|
+
|
|
265
|
+
```json
|
|
266
|
+
"exposure": {
|
|
267
|
+
"publicSsh": false,
|
|
268
|
+
"publicPorts": [{ "protocol": "udp", "port": 51820 }],
|
|
269
|
+
"otherListeners": "warn"
|
|
270
|
+
}
|
|
271
|
+
```
|
|
234
272
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
273
|
+
`publicSsh` is break-glass access: SSH stays open beyond the tailnet.
|
|
274
|
+
Without it the public SSH port closes only when the session running the sync
|
|
275
|
+
arrived over the tailnet, after the tailnet path answers a new probe and sshd
|
|
276
|
+
a new connection on the tailnet address; a failed verification restores the
|
|
277
|
+
previous rules (`FIREWALL_TAILNET_UNVERIFIED`). A session from a public
|
|
278
|
+
address, a tailnet that is not up, or a run without a session that finds the
|
|
279
|
+
port open keeps it open, and `worker:exposure` reports
|
|
280
|
+
`WORKER_PUBLIC_SSH_UNDECLARED` until the manager's SSH alias names the
|
|
281
|
+
worker's tailnet name.
|
|
282
|
+
|
|
283
|
+
`worker:exposure` (on the worker) lists listening sockets and fails on one
|
|
284
|
+
bound beyond loopback and the tailnet outside the allow-list (sshd, tailscaled,
|
|
285
|
+
the system's DHCP and name-resolution listeners, `publicPorts`), naming the
|
|
286
|
+
port and its unit. A fleet service there always fails; anything else, such as
|
|
287
|
+
an agent's development server, warns or fails by `otherListeners`.
|
|
288
|
+
`worker:public-reachability` (from the manager, in `fleet-status --workers`)
|
|
289
|
+
tries the SSH, T3 and monitoring ports on the public address Tailscale knows
|
|
290
|
+
for the worker; it never blocks readiness, because the manager may share the
|
|
291
|
+
worker's network. Both are not applicable without an address outside the
|
|
292
|
+
tailnet. Closing the public path on a machine with two interfaces still needs
|
|
293
|
+
the disposable VM or a real machine to prove.
|
|
241
294
|
|
|
242
295
|
The manager is the only configuration coordinator. No per-node pull timer is
|
|
243
296
|
installed. Manager schedule controls use the existing platform adapter:
|
|
@@ -321,7 +374,10 @@ A worker whose T3 has not loaded the staged provider policy, or runs an older
|
|
|
321
374
|
release than the one installed, is restarted only while it runs no agent turn;
|
|
322
375
|
otherwise sync reports the pending restart and the recovery timer applies it
|
|
323
376
|
at the first idle check, failing the check after
|
|
324
|
-
`t3RestartMaxDeferralHours` (`ansible/ubuntu/policy.json`).
|
|
377
|
+
`t3RestartMaxDeferralHours` (`ansible/ubuntu/policy.json`).
|
|
378
|
+
`fleet-sync --restart-busy` passes `t3_restart_busy` to the plays, which then
|
|
379
|
+
restart a busy T3 at once and end its turns; the `--dry-run` plan says, per
|
|
380
|
+
T3 restart, whether running turns end. A pending proxy
|
|
325
381
|
update is installed even when it recreates the gateway. The manager finishes before the workers
|
|
326
382
|
start, so they are verified against the updated proxy. `--dry-run` previews.
|
|
327
383
|
|