@crvouga/dev-fleet 0.3.0 → 0.5.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 +25 -17
- package/ansible/github/git.yml +66 -0
- package/ansible/macos/policy.json +12 -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/platforms.json +111 -0
- package/ansible/ubuntu/authorize-sudo.sh +7 -16
- package/ansible/ubuntu/baseline.yml +6 -28
- package/ansible/ubuntu/disk-gc.py +211 -7
- package/ansible/ubuntu/disk-guard.py +24 -5
- package/ansible/ubuntu/policy.json +68 -4
- package/ansible/ubuntu/t3-desktop.yml +2 -2
- package/ansible/ubuntu/tailscale.yml +20 -9
- package/ansible/ubuntu/tasks/browser.yml +56 -0
- package/ansible/ubuntu/tasks/docker.yml +52 -0
- package/ansible/ubuntu/tasks/dpkg.yml +46 -0
- package/ansible/ubuntu/tasks/install.yml +38 -0
- package/ansible/ubuntu/tasks/packages.yml +4 -55
- package/ansible/ubuntu/tasks/release.yml +25 -0
- package/ansible/ubuntu/tasks/resources.yml +7 -6
- package/ansible/ubuntu/tasks/services.yml +13 -0
- package/ansible/ubuntu/tasks/snapshot.yml +50 -0
- package/ansible/ubuntu/tasks/unattended-upgrades.yml +24 -0
- package/bootstrap/doctor.ts +32 -6
- package/bootstrap/fleet-remote-bootstrap.sh +49 -45
- 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/consumer-setup.md +32 -30
- package/docs/declared.md +6 -5
- package/docs/machine-setup.md +22 -12
- package/docs/operations.md +87 -31
- package/docs/runbooks.md +377 -0
- package/docs/state.md +13 -7
- package/docs/support-matrix.md +36 -0
- package/docs/ubuntu-sync.md +16 -13
- package/docs/worker-contract.md +28 -1
- 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 +526 -0
- package/schema/fleet.schema.json +185 -4
- 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 +4 -2
- 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 +160 -12
- package/src/git-identity.ts +160 -0
- package/src/machine/platform.ts +48 -28
- package/src/machine/support-matrix.ts +76 -0
- package/src/machine-health.ts +51 -4
- package/src/manager-health.ts +205 -0
- package/src/manager-sync.ts +27 -9
- package/src/monitoring/hub-backup.ts +63 -17
- package/src/monitoring/manager-hub.ts +44 -2
- package/src/network-definition.ts +1 -1
- package/src/node-converge-linux.ts +16 -5
- package/src/node-converge.ts +1 -1
- package/src/notify.ts +239 -0
- package/src/platform/debian-version.ts +62 -0
- package/src/platform/ubuntu-audit.ts +11 -4
- package/src/platform/ubuntu-release.ts +41 -0
- package/src/remote-bootstrap.ts +89 -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 +68 -10
- package/src/worker-policy.ts +33 -0
- package/src/worker-readiness.ts +10 -1
|
@@ -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/consumer-setup.md
CHANGED
|
@@ -153,35 +153,35 @@ npx devfleet fleet-state --example > fleet.json
|
|
|
153
153
|
Then replace every placeholder. Each field, from
|
|
154
154
|
[`schema/fleet.schema.json`](../schema/fleet.schema.json):
|
|
155
155
|
|
|
156
|
-
| Field | Value
|
|
157
|
-
| ----------------------------------------------------- |
|
|
158
|
-
| `version` | `3`
|
|
159
|
-
| `source.repository` | `https://github.com/crvouga/dev-fleet.git`, or your fork if you publish one. Machines only accept code from it.
|
|
160
|
-
| `state` | `{ "bucket", "endpoint"?, "region"?, "prefix"? }` or `{ "directory": "/absolute/path" }`. Never credentials.
|
|
161
|
-
| `secrets` | See [secrets](#secrets).
|
|
162
|
-
| `manager.id` | A short lowercase id, `^[a-z][a-z0-9-]{0,62}$`, e.g. `manager-mac`.
|
|
163
|
-
| `manager.os` | `"macOS"`
|
|
164
|
-
| `manager.network.tailscaleName` | The manager's MagicDNS name: `tailscale status --json` → `.Self.DNSName` without the trailing dot.
|
|
165
|
-
| `manager.network.user` | The manager's login name (`id -un`).
|
|
166
|
-
| `manager.neverSleepOnPower` | `true` (workers depend on the manager's services).
|
|
167
|
-
| `workers[].id` | A short lowercase id. The **first** worker is the canary: a new revision must pass there before the others get it.
|
|
168
|
-
| `workers[].ssh` | The worker's alias in the manager's `~/.ssh/config`.
|
|
169
|
-
| `workers[].os` | `"linux"`
|
|
170
|
-
| `workers[].network.tailscaleName` | The worker's MagicDNS name (from `tailscale status --json` → `.Peer[].DNSName` on the manager).
|
|
171
|
-
| `workers[].network.sshPort` | `22` unless the worker's sshd listens elsewhere.
|
|
172
|
-
| `workers[].network.user` | The worker account (`ssh WORKER_ALIAS id -un`).
|
|
173
|
-
| `workers[].network.t3Port` | `3773`
|
|
174
|
-
| `services.proxy.endpoint` | The public HTTPS origin of the CLI proxy, e.g. `https://cli-proxy.example.com`.
|
|
175
|
-
| `services.monitoring.hubPort` | `8443`
|
|
176
|
-
| `services.monitoring.publicUrl` | The public HTTPS origin of the monitoring dashboard.
|
|
177
|
-
| `services.monitoring.managerAccount` | The email of the monitoring account the manager enrolls machines with.
|
|
178
|
-
| `services.monitoring.viewerAccount` | The email of the read-only account visitors are signed in as.
|
|
179
|
-
| `ubuntu` | `{ "release": "26.04", "architectures": ["amd64", "arm64"] }
|
|
180
|
-
| `worker.requiredCapabilities` | Normally all of `github`, `vault`, `t3`, `toolchain`, `workstation`, `worker`.
|
|
181
|
-
| `worker.githubLogin` | The GitHub login every worker must be signed in as.
|
|
182
|
-
| `worker.vaultUser` | The Vault user each worker's token is issued for.
|
|
183
|
-
| `worker.toolchain`, `agentProfile` | `"mise.lock"`, `"proxy"` (fixed).
|
|
184
|
-
| `worker.unattendedSudo`, `persistentT3`, `neverSleep` | `true` (fixed policy).
|
|
156
|
+
| Field | Value |
|
|
157
|
+
| ----------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- |
|
|
158
|
+
| `version` | `3` |
|
|
159
|
+
| `source.repository` | `https://github.com/crvouga/dev-fleet.git`, or your fork if you publish one. Machines only accept code from it. |
|
|
160
|
+
| `state` | `{ "bucket", "endpoint"?, "region"?, "prefix"? }` or `{ "directory": "/absolute/path" }`. Never credentials. |
|
|
161
|
+
| `secrets` | See [secrets](#secrets). |
|
|
162
|
+
| `manager.id` | A short lowercase id, `^[a-z][a-z0-9-]{0,62}$`, e.g. `manager-mac`. |
|
|
163
|
+
| `manager.os` | `"macOS"` |
|
|
164
|
+
| `manager.network.tailscaleName` | The manager's MagicDNS name: `tailscale status --json` → `.Self.DNSName` without the trailing dot. |
|
|
165
|
+
| `manager.network.user` | The manager's login name (`id -un`). |
|
|
166
|
+
| `manager.neverSleepOnPower` | `true` (workers depend on the manager's services). |
|
|
167
|
+
| `workers[].id` | A short lowercase id. The **first** worker is the canary: a new revision must pass there before the others get it. |
|
|
168
|
+
| `workers[].ssh` | The worker's alias in the manager's `~/.ssh/config`. |
|
|
169
|
+
| `workers[].os` | `"linux"` |
|
|
170
|
+
| `workers[].network.tailscaleName` | The worker's MagicDNS name (from `tailscale status --json` → `.Peer[].DNSName` on the manager). |
|
|
171
|
+
| `workers[].network.sshPort` | `22` unless the worker's sshd listens elsewhere. |
|
|
172
|
+
| `workers[].network.user` | The worker account (`ssh WORKER_ALIAS id -un`). |
|
|
173
|
+
| `workers[].network.t3Port` | `3773` |
|
|
174
|
+
| `services.proxy.endpoint` | The public HTTPS origin of the CLI proxy, e.g. `https://cli-proxy.example.com`. |
|
|
175
|
+
| `services.monitoring.hubPort` | `8443` |
|
|
176
|
+
| `services.monitoring.publicUrl` | The public HTTPS origin of the monitoring dashboard. |
|
|
177
|
+
| `services.monitoring.managerAccount` | The email of the monitoring account the manager enrolls machines with. |
|
|
178
|
+
| `services.monitoring.viewerAccount` | The email of the read-only account visitors are signed in as. |
|
|
179
|
+
| `ubuntu` | `{ "release": "26.04", "architectures": ["amd64", "arm64"] }`: a release of the [support matrix](support-matrix.md). |
|
|
180
|
+
| `worker.requiredCapabilities` | Normally all of `github`, `vault`, `t3`, `toolchain`, `workstation`, `worker`. |
|
|
181
|
+
| `worker.githubLogin` | The GitHub login every worker must be signed in as. |
|
|
182
|
+
| `worker.vaultUser` | The Vault user each worker's token is issued for. |
|
|
183
|
+
| `worker.toolchain`, `agentProfile` | `"mise.lock"`, `"proxy"` (fixed). |
|
|
184
|
+
| `worker.unattendedSudo`, `persistentT3`, `neverSleep` | `true` (fixed policy). |
|
|
185
185
|
|
|
186
186
|
Use the same values in every place they repeat. Validate before going on:
|
|
187
187
|
|
|
@@ -352,7 +352,9 @@ Read `summary` and `remedy.action` of the failing check. The common ones:
|
|
|
352
352
|
| `TAILSCALE_NODE_OFFLINE` | The worker is off or off the tailnet. It is listed and skipped; others continue. |
|
|
353
353
|
| `TAILSCALE_DEVICE_REPLACED` | The tailnet device behind a worker changed. Nothing is written to it until the human confirms the replacement. |
|
|
354
354
|
| `SUDO_AUTHORIZATION_REQUIRED` | The worker account cannot use sudo without a password. Run the first sync interactively. |
|
|
355
|
-
| `UBUNTU_BASELINE_MISMATCH` | The worker is not Ubuntu
|
|
355
|
+
| `UBUNTU_BASELINE_MISMATCH` | The worker is not an Ubuntu release the [support matrix](support-matrix.md) lists. |
|
|
356
|
+
| `WORKER_ENVIRONMENT_UNSUPPORTED` | The worker is WSL2 or a container. Use a physical or virtual machine of its own. |
|
|
357
|
+
| `PACKAGE_NEWER_THAN_PIN` (warning) | Ubuntu's security updates moved a pinned package past its pin. It is kept; the pin is a minimum. |
|
|
356
358
|
| `ROOT_DEVELOPMENT_USER_REJECTED` | The worker's SSH user is root. Use a normal account with sudo. |
|
|
357
359
|
| `BOOTSTRAP_DISK_CAPACITY_REQUIRED` | Less than 2 GiB free (or too few inodes) where the fleet keeps its files on the worker. |
|
|
358
360
|
| `FLEET_REMOTE_LOCKED` | Another operation is running on that worker. Wait and run again. |
|
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
|
|
|
@@ -45,11 +47,10 @@ stays in the playbook: the playbook is its declaration.
|
|
|
45
47
|
launchd job from TypeScript and has never worked from the published package.
|
|
46
48
|
It should become a playbook.
|
|
47
49
|
- **The rest of a worker's packages.** Only the packages in `policy.json` are
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
is a policy choice, not a missing line.
|
|
50
|
+
pinned. The T3 desktop libraries are installed at whatever version the
|
|
51
|
+
machine's own archive holds, and Ubuntu's security updates move everything
|
|
52
|
+
else, pinned packages included: a pin is a minimum
|
|
53
|
+
([worker contract](worker-contract.md#package-pins-and-security-updates)).
|
|
53
54
|
- **What a worker needs before Ansible exists.** `fleet-remote-bootstrap.sh`
|
|
54
55
|
installs Git, curl, CA certificates, Python and `ansible-core` from the
|
|
55
56
|
machine's own archive (the snapshot is HTTPS, and a new machine has no CA
|
package/docs/machine-setup.md
CHANGED
|
@@ -181,19 +181,21 @@ wrangler login
|
|
|
181
181
|
railway login
|
|
182
182
|
```
|
|
183
183
|
|
|
184
|
-
Only sign into providers the task actually needs.
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
184
|
+
Only sign into providers the task actually needs. On fleet machines the Git
|
|
185
|
+
identity and the GitHub credential helper come from the `git` block of
|
|
186
|
+
`fleet.json`: `fleet-sync` writes them in `~/.config/git/config` and checks
|
|
187
|
+
them as `worker:git-identity` and `worker:git-push-auth`. Elsewhere set `git
|
|
188
|
+
config --global user.name` and `user.email` after confirming them; do not guess
|
|
189
|
+
an email or overwrite an existing identity. Enable Git LFS hooks when needed with `git lfs install`.
|
|
188
190
|
|
|
189
191
|
Optional existing workstation/agent settings:
|
|
190
192
|
|
|
191
193
|
```sh
|
|
192
194
|
sh bootstrap/install-launcher.sh
|
|
193
|
-
|
|
195
|
+
devfleet fleet-status --local
|
|
194
196
|
# Only for tasks needing the workspace's managed secrets:
|
|
195
197
|
vault login --method=userpass username=example-user
|
|
196
|
-
|
|
198
|
+
devfleet fleet-sync
|
|
197
199
|
```
|
|
198
200
|
|
|
199
201
|
The machine bootstrap installs tools/dependencies only. `personal:install` additionally
|
|
@@ -231,7 +233,7 @@ The manifest installs the **Docker client**. Containers also require a local or
|
|
|
231
233
|
remote Docker engine, Compose v2 and Buildx plugins. These cannot be installed as
|
|
232
234
|
one identical unprivileged binary on all OSes.
|
|
233
235
|
|
|
234
|
-
- **macOS:** run `
|
|
236
|
+
- **macOS:** run `bash bootstrap/docker-desktop.sh` to install the pinned,
|
|
235
237
|
checksum-verified Docker Desktop bundle from this repository and open it.
|
|
236
238
|
Complete its first-run agreement/setup in the app. Repeated invocation reuses
|
|
237
239
|
an existing installation. The shared mise Docker client remains in use.
|
|
@@ -280,11 +282,11 @@ mise install --locked bun
|
|
|
280
282
|
mise exec --locked bun -- bun bootstrap/setup.ts
|
|
281
283
|
```
|
|
282
284
|
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
without
|
|
287
|
-
them. Sync is explicit; Git commits do not silently update other machines.
|
|
285
|
+
On fleet machines `devfleet fleet-sync` refreshes the toolchain, the global
|
|
286
|
+
configuration and frozen dependencies; `mise exec --locked bun -- bun
|
|
287
|
+
bootstrap/setup.ts --tools-only` reapplies tool pins and the global
|
|
288
|
+
configuration without touching project dependencies, and `bun
|
|
289
|
+
bootstrap/doctor.ts --json` verifies them. Sync is explicit; Git commits do not silently update other machines.
|
|
288
290
|
|
|
289
291
|
For multiple development computers, use [the fleet package](operations.md):
|
|
290
292
|
`devfleet fleet-node-add` registers an existing SSH alias in a local roster,
|
|
@@ -395,6 +397,14 @@ It installs the `vars.worker_tools` subset from `mise.toml`, with versions and
|
|
|
395
397
|
artifacts taken from the same `mise.lock`; native T3 and Ubuntu Docker/service
|
|
396
398
|
policy remain source-pinned. `bun bootstrap/doctor.ts --worker --docker --json`
|
|
397
399
|
checks that profile. It does not require unrelated cloud CLIs on workers.
|
|
400
|
+
|
|
401
|
+
Tool profiles add locked tool sets on top of it: `worker.toolProfiles` in
|
|
402
|
+
`fleet.json` (fleet-wide) or `toolProfiles` on one worker selects `web` (pnpm,
|
|
403
|
+
Deno, Yarn and headless Chromium for browser tests), `go`, `rust` (with CMake
|
|
404
|
+
and Ninja) or `jvm` (Temurin), each a `worker_profile_<name>` under `[vars]` in
|
|
405
|
+
`mise.toml`. `worker.tools` adds single tools dev-fleet already pins. The setup
|
|
406
|
+
installs them (`--node` reads the installed configuration) and the doctor
|
|
407
|
+
checks each as `tool:<name>` (`--profiles`, `--tools`).
|
|
398
408
|
The source-owned `worker-auth.ts` timer renews the worker's existing Vault token
|
|
399
409
|
and can restore the declared GitHub account from its authorized Vault service
|
|
400
410
|
credential, without copying another machine's login directory.
|