@crvouga/dev-fleet 0.1.0 → 0.3.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 +265 -37
- package/agents/setup.md +42 -12
- package/ansible/macos/manager.yml +10 -7
- package/ansible/macos/policy.json +2 -10
- package/ansible/macos/t3-policy.yml +49 -0
- package/ansible/monitoring/README.md +86 -25
- package/ansible/monitoring/agent.yml +40 -2
- package/ansible/monitoring/hub-route-darwin.yml +51 -0
- package/ansible/monitoring/lock.json +5 -0
- package/ansible/pyproject.toml +12 -0
- package/ansible/ubuntu/baseline.yml +1 -1
- package/ansible/ubuntu/disk-gc.py +336 -0
- package/ansible/ubuntu/policy.json +13 -3
- package/ansible/ubuntu/t3-desktop.yml +19 -0
- package/ansible/ubuntu/t3-health.sh +93 -22
- package/ansible/ubuntu/t3-idle.py +42 -0
- package/ansible/ubuntu/t3-policy.yml +4 -11
- package/ansible/ubuntu/tailscale.yml +8 -5
- package/ansible/ubuntu/tasks/recovery.yml +60 -1
- package/ansible/ubuntu/tasks/resources.yml +61 -1
- package/ansible/ubuntu/tasks/t3-restart.yml +37 -0
- package/ansible/ubuntu/wip-guard.py +121 -0
- package/ansible/uv.lock +190 -0
- package/bootstrap/bootstrap.sh +25 -18
- package/bootstrap/doctor.ts +46 -1
- package/bootstrap/fleet-manager-run.sh +42 -15
- package/bootstrap/fleet-remote-bootstrap.sh +85 -12
- package/bootstrap/global-config.ts +64 -6
- package/bootstrap/install-launcher.sh +3 -0
- package/bootstrap/native-t3.ts +22 -11
- package/bootstrap/npm.ts +10 -2
- package/bootstrap/release-version.ts +157 -0
- package/cliproxy/README.md +3 -3
- package/cliproxy/scripts/accounts.py +216 -0
- package/cliproxy/scripts/checkpoint.py +29 -1
- package/cliproxy/scripts/cli.py +1 -1
- package/cliproxy/scripts/clientkeys.py +99 -0
- package/cliproxy/scripts/lease.py +80 -0
- package/cliproxy/scripts/ops +137 -25
- package/cliproxy/scripts/restore-verify +15 -0
- package/docs/accounts.md +154 -0
- package/docs/consumer-setup.md +408 -0
- package/docs/declared.md +91 -0
- package/docs/durability.md +26 -0
- package/docs/machine-setup.md +400 -0
- package/docs/operations.md +276 -0
- package/docs/releasing.md +103 -0
- package/docs/state.md +254 -0
- package/docs/topology.md +69 -0
- package/docs/ubuntu-sync.md +357 -0
- package/docs/worker-contract.md +67 -0
- package/examples/fleet.json +5 -0
- package/llms.txt +36 -0
- package/mise.lock +29 -0
- package/mise.toml +15 -2
- package/package.json +11 -2
- package/schema/fleet.schema.json +137 -15
- package/src/accounts/checks.ts +220 -0
- package/src/accounts/context.ts +76 -0
- package/src/accounts/propagate.ts +62 -0
- package/src/accounts/roster.ts +85 -0
- package/src/accounts/selection.ts +67 -0
- package/src/accounts/sessions.ts +120 -0
- package/src/agents/account-relay.ts +176 -26
- package/src/agents/fleet-verify.ts +9 -0
- package/src/agents/launch.ts +84 -5
- package/src/agents/native-verify.ts +293 -22
- package/src/agents/provider-errors.ts +9 -2
- package/src/agents/registry.ts +7 -1
- package/src/cli/agent-guide.ts +39 -5
- package/src/cli/app.ts +11 -4
- package/src/cli/cli.ts +7 -0
- package/src/cli/command-index.ts +7 -0
- package/src/cli/fleet-accounts.ts +253 -0
- package/src/cli/fleet-state.ts +499 -0
- package/src/cli/fleet-sync-accounts.ts +58 -0
- package/src/cli/fleet-sync-checkout.ts +56 -17
- package/src/cli/fleet-sync-enrollment.ts +19 -23
- package/src/cli/fleet-sync-forward.ts +1 -1
- package/src/cli/fleet-sync-manager.ts +84 -12
- package/src/cli/fleet-sync-monitoring.ts +5 -5
- package/src/cli/fleet-sync-output.ts +7 -3
- package/src/cli/fleet-sync-remote.ts +31 -18
- package/src/cli/fleet-sync-state.ts +86 -0
- package/src/cli/fleet-sync.ts +159 -57
- package/src/cli/fleet.ts +4 -2
- package/src/cli/managed-health.ts +4 -4
- package/src/cli/node-sync.ts +7 -0
- package/src/cli/program.ts +20 -4
- package/src/cli/project.ts +51 -0
- package/src/cli/register-fleet.ts +86 -1
- package/src/cli/registry.ts +41 -1
- package/src/cli/roster.ts +46 -30
- package/src/cli/settings.ts +15 -12
- package/src/cli/source-operations.ts +27 -3
- package/src/cli/tailnet.ts +2 -2
- package/src/definition.ts +116 -4
- package/src/diagnostics.ts +1 -1
- package/src/fleet-config.ts +105 -19
- package/src/ide/definition.ts +86 -0
- package/src/ide/ide.ts +43 -0
- package/src/ide/index.ts +5 -0
- package/src/ide/orchestrate.ts +50 -0
- package/src/ide/t3.ts +111 -0
- package/src/machine/source.ts +3 -3
- package/src/machine/state.ts +15 -27
- package/src/manager-sync.ts +9 -18
- package/src/monitoring/agent-evidence.ts +146 -0
- package/src/monitoring/apply.ts +15 -3
- package/src/monitoring/beszel.ts +11 -4
- package/src/monitoring/enroll.ts +2 -1
- package/src/monitoring/hub-access.ts +4 -3
- package/src/monitoring/hub-backup.ts +121 -0
- package/src/monitoring/hub-viewer.ts +8 -8
- package/src/monitoring/hub.ts +3 -3
- package/src/monitoring/manager-hub.ts +30 -14
- package/src/node-converge-linux.ts +30 -7
- package/src/package-info.ts +45 -0
- package/src/platform/ansible.ts +19 -15
- package/src/platform/darwin.ts +0 -13
- package/src/platform/fallback.ts +0 -8
- package/src/platform/linux.ts +0 -10
- package/src/platform/resource-verify.ts +8 -4
- package/src/platform/t3-policy.ts +33 -6
- package/src/platform/t3-service.ts +104 -0
- package/src/platform/types.ts +0 -4
- package/src/platform/ubuntu-audit.ts +5 -2
- package/src/platform/windows.ts +0 -14
- package/src/remote-bootstrap.ts +17 -19
- package/src/remote-entry.ts +15 -1
- package/src/repair-catalog.ts +1 -1
- package/src/runner.ts +1 -1
- package/src/service-auth.ts +15 -5
- package/src/state/bucket.ts +208 -0
- package/src/state/check.ts +74 -0
- package/src/state/connection.ts +92 -0
- package/src/state/enrollment.ts +45 -0
- package/src/state/legacy.ts +89 -0
- package/src/state/litestream.ts +143 -0
- package/src/state/operator.ts +20 -0
- package/src/state/paths.ts +40 -0
- package/src/state/roster.ts +62 -0
- package/src/state/store.ts +169 -0
- package/src/state/sync.ts +352 -0
- package/src/t3/accounts.ts +7 -1
- package/src/t3/models.ts +133 -0
- package/src/t3/pairing.ts +31 -0
- package/src/t3/profile.ts +203 -0
- package/src/t3/version.ts +119 -0
- package/src/t3-endpoint.ts +64 -8
- package/src/t3-linked-check.ts +24 -2
- package/src/toolchain-checks.ts +25 -0
- package/src/transport.ts +3 -2
- package/src/types.ts +4 -0
- package/src/worker-access-checks.ts +56 -3
- package/src/worker-host-checks.ts +41 -13
- package/src/workstation-checks.ts +1 -1
- package/src/index.ts +0 -6
- package/src/test-fixtures.ts +0 -36
|
@@ -4,6 +4,13 @@ import { join } from 'node:path';
|
|
|
4
4
|
import type { Check, Runner } from '../types';
|
|
5
5
|
import { pinnedPath } from './toolchain';
|
|
6
6
|
import { registry } from './registry';
|
|
7
|
+
import type { Account } from './accounts';
|
|
8
|
+
import { accountSlug } from './accounts';
|
|
9
|
+
import {
|
|
10
|
+
accountSessionChecks,
|
|
11
|
+
sessionResult,
|
|
12
|
+
type Instance,
|
|
13
|
+
} from '../accounts/sessions';
|
|
7
14
|
import {
|
|
8
15
|
cacheKey,
|
|
9
16
|
cachedCheck,
|
|
@@ -23,6 +30,8 @@ export const NATIVE_AGENTS = [
|
|
|
23
30
|
binary: 'claude',
|
|
24
31
|
pin: 'claude',
|
|
25
32
|
wrapper: 'claude-proxy.ts',
|
|
33
|
+
// T3's binaryPath: the Claude Agent SDK runs a `.ts` path with node.
|
|
34
|
+
launcher: 'claude-proxy',
|
|
26
35
|
args: ['-p', PROMPT],
|
|
27
36
|
},
|
|
28
37
|
{
|
|
@@ -30,6 +39,7 @@ export const NATIVE_AGENTS = [
|
|
|
30
39
|
binary: 'codex',
|
|
31
40
|
pin: 'codex',
|
|
32
41
|
wrapper: 'codex-proxy.ts',
|
|
42
|
+
launcher: 'codex-proxy.ts',
|
|
33
43
|
args: ['exec', '--skip-git-repo-check', PROMPT],
|
|
34
44
|
},
|
|
35
45
|
{
|
|
@@ -37,6 +47,7 @@ export const NATIVE_AGENTS = [
|
|
|
37
47
|
binary: 'grok',
|
|
38
48
|
pin: 'http:grok',
|
|
39
49
|
wrapper: 'grok-proxy.ts',
|
|
50
|
+
launcher: 'grok-proxy.ts',
|
|
40
51
|
args: ['-p', PROMPT],
|
|
41
52
|
},
|
|
42
53
|
{
|
|
@@ -44,6 +55,7 @@ export const NATIVE_AGENTS = [
|
|
|
44
55
|
binary: 'antigravity',
|
|
45
56
|
pin: 'http:antigravity',
|
|
46
57
|
wrapper: 'antigravity-proxy.ts',
|
|
58
|
+
launcher: 'antigravity-proxy.ts',
|
|
47
59
|
args: [
|
|
48
60
|
'-p',
|
|
49
61
|
PROMPT,
|
|
@@ -58,14 +70,31 @@ export const NATIVE_AGENTS = [
|
|
|
58
70
|
binary: 'opencode',
|
|
59
71
|
pin: 'aqua:anomalyco/opencode',
|
|
60
72
|
wrapper: 'opencode-proxy.ts',
|
|
73
|
+
launcher: 'opencode-proxy.ts',
|
|
61
74
|
args: ['run', '--model', 'personal_proxy/devin/muse-wonder-1-0', PROMPT],
|
|
62
75
|
},
|
|
63
76
|
] as const;
|
|
64
77
|
|
|
65
78
|
type Agent = (typeof NATIVE_AGENTS)[number];
|
|
66
79
|
|
|
80
|
+
/** The user service T3 runs as on a worker, and so the environment every
|
|
81
|
+
* agent it starts inherits.
|
|
82
|
+
*/
|
|
83
|
+
export const T3_SERVICE = 't3code-desktop.service';
|
|
84
|
+
|
|
85
|
+
/** How a session is launched: by the sync, through Bun on the pinned PATH,
|
|
86
|
+
* or exactly as the T3 service starts it, with that service's environment
|
|
87
|
+
* and nothing of the sync's.
|
|
88
|
+
*/
|
|
89
|
+
type Launch =
|
|
90
|
+
| { kind: 'sync' }
|
|
91
|
+
| { kind: 'service'; env: Record<string, string> }
|
|
92
|
+
| { kind: 'incomplete' };
|
|
93
|
+
|
|
67
94
|
/** Same versions everywhere (from the shared mise.lock) and a working signed-in
|
|
68
95
|
* session through each T3 launcher. Agents run concurrently and are bounded.
|
|
96
|
+
* With `t3Service` (a Linux worker), sessions run in the environment of the
|
|
97
|
+
* T3 user service, where T3 itself starts them, not in the sync's.
|
|
69
98
|
*/
|
|
70
99
|
export async function verifyNativeAgents(
|
|
71
100
|
node: string,
|
|
@@ -75,10 +104,20 @@ export async function verifyNativeAgents(
|
|
|
75
104
|
/** Proxy routes already found out of quota; their sessions are not run. */
|
|
76
105
|
outOfQuota?: Set<string>;
|
|
77
106
|
cache?: VerifyCache;
|
|
107
|
+
t3Service?: boolean;
|
|
108
|
+
/** The proxy's accounts and what this node offers. Every cached result
|
|
109
|
+
* is keyed on the roster version and the client key's version, so any
|
|
110
|
+
* account change or key rotation re-verifies at once.
|
|
111
|
+
*/
|
|
112
|
+
accounts?: AccountContext;
|
|
78
113
|
} = {}
|
|
79
114
|
): Promise<Check[]> {
|
|
80
115
|
const { outOfQuota = new Set<string>(), cache = {} } = options;
|
|
81
116
|
const pins = parsePins(await readFile(join(workspace, 'mise.toml'), 'utf8'));
|
|
117
|
+
const environment = options.t3Service
|
|
118
|
+
? await serviceLaunch(run)
|
|
119
|
+
: { launch: { kind: 'sync' } as Launch, checks: [] };
|
|
120
|
+
const { launch } = environment;
|
|
82
121
|
const cwd = await mkdtemp(join(tmpdir(), 'fleet-agents-'));
|
|
83
122
|
try {
|
|
84
123
|
const results = await Promise.all(
|
|
@@ -86,26 +125,143 @@ export async function verifyNativeAgents(
|
|
|
86
125
|
await versionCheck(agent, pins[agent.pin], run),
|
|
87
126
|
outOfQuota.has(agent.name)
|
|
88
127
|
? quotaPass(agent)
|
|
89
|
-
:
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
node
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
128
|
+
: launch.kind === 'incomplete'
|
|
129
|
+
? incompleteSession(agent)
|
|
130
|
+
: await cachedCheck(
|
|
131
|
+
`agent-${node}-${agent.name}`,
|
|
132
|
+
cacheKey(
|
|
133
|
+
node,
|
|
134
|
+
agent,
|
|
135
|
+
pins[agent.pin] ?? '',
|
|
136
|
+
registry,
|
|
137
|
+
await sourceKey(launcherSources(workspace, agent)),
|
|
138
|
+
options.accounts?.rosterVersion ?? '',
|
|
139
|
+
options.accounts?.keyVersion ?? ''
|
|
140
|
+
),
|
|
141
|
+
() =>
|
|
142
|
+
sessionCheck(agent, { node, workspace, cwd, launch }, run),
|
|
143
|
+
cache
|
|
97
144
|
),
|
|
98
|
-
() => sessionCheck(agent, { node, workspace, cwd }, run),
|
|
99
|
-
cache
|
|
100
|
-
),
|
|
101
145
|
])
|
|
102
146
|
);
|
|
103
|
-
|
|
147
|
+
const accounts =
|
|
148
|
+
options.accounts && launch.kind !== 'incomplete'
|
|
149
|
+
? await accountSessionChecks(
|
|
150
|
+
options.accounts.live,
|
|
151
|
+
options.accounts.exposed,
|
|
152
|
+
(instance) =>
|
|
153
|
+
accountSession(instance, options.accounts!, {
|
|
154
|
+
node,
|
|
155
|
+
workspace,
|
|
156
|
+
cwd,
|
|
157
|
+
launch,
|
|
158
|
+
cache,
|
|
159
|
+
run,
|
|
160
|
+
})
|
|
161
|
+
)
|
|
162
|
+
: [];
|
|
163
|
+
return [...environment.checks, ...results.flat(), ...accounts];
|
|
104
164
|
} finally {
|
|
105
165
|
await rm(cwd, { recursive: true, force: true });
|
|
106
166
|
}
|
|
107
167
|
}
|
|
108
168
|
|
|
169
|
+
export interface AccountContext {
|
|
170
|
+
live: Account[];
|
|
171
|
+
exposed: Instance[];
|
|
172
|
+
rosterVersion: string;
|
|
173
|
+
keyVersion: string;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** How a per-account instance is launched: its tool's launcher, pinned to
|
|
177
|
+
* the account as T3 pins it.
|
|
178
|
+
*/
|
|
179
|
+
const ACCOUNT_LAUNCH: Record<
|
|
180
|
+
string,
|
|
181
|
+
{ wrapper: string; launcher: string; args: string[] }
|
|
182
|
+
> = {
|
|
183
|
+
claude: {
|
|
184
|
+
wrapper: 'claude-proxy.ts',
|
|
185
|
+
launcher: 'claude-proxy',
|
|
186
|
+
args: ['-p', PROMPT],
|
|
187
|
+
},
|
|
188
|
+
codex: {
|
|
189
|
+
wrapper: 'codex-proxy.ts',
|
|
190
|
+
launcher: 'codex-proxy.ts',
|
|
191
|
+
args: ['exec', '--skip-git-repo-check', PROMPT],
|
|
192
|
+
},
|
|
193
|
+
opencode: {
|
|
194
|
+
wrapper: 'opencode-proxy.ts',
|
|
195
|
+
launcher: 'opencode-proxy.ts',
|
|
196
|
+
args: ['run', PROMPT],
|
|
197
|
+
},
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
async function accountSession(
|
|
201
|
+
instance: Instance,
|
|
202
|
+
accounts: AccountContext,
|
|
203
|
+
context: {
|
|
204
|
+
node: string;
|
|
205
|
+
workspace: string;
|
|
206
|
+
cwd: string;
|
|
207
|
+
launch: Launch;
|
|
208
|
+
cache: VerifyCache;
|
|
209
|
+
run: Runner;
|
|
210
|
+
}
|
|
211
|
+
): Promise<'ok' | 'cooling-down' | 'failed'> {
|
|
212
|
+
const spec = ACCOUNT_LAUNCH[instance.tool];
|
|
213
|
+
if (!spec) return 'failed';
|
|
214
|
+
const check = await cachedCheck(
|
|
215
|
+
`account-${context.node}-${instance.tool}-${accountSlug(instance.account)}`,
|
|
216
|
+
cacheKey(
|
|
217
|
+
context.node,
|
|
218
|
+
instance,
|
|
219
|
+
spec,
|
|
220
|
+
accounts.rosterVersion,
|
|
221
|
+
accounts.keyVersion
|
|
222
|
+
),
|
|
223
|
+
async () => {
|
|
224
|
+
const agents = join(context.workspace, 'agents');
|
|
225
|
+
const service = context.launch.kind === 'service';
|
|
226
|
+
const result = await context.run(
|
|
227
|
+
service
|
|
228
|
+
? [join(agents, spec.launcher), ...spec.args]
|
|
229
|
+
: ['bun', join(agents, spec.wrapper), ...spec.args],
|
|
230
|
+
{
|
|
231
|
+
cwd: context.cwd,
|
|
232
|
+
env: {
|
|
233
|
+
...(context.launch.kind === 'service'
|
|
234
|
+
? context.launch.env
|
|
235
|
+
: { PATH: pinnedPath() }),
|
|
236
|
+
WS_AGENT_NODE: context.node,
|
|
237
|
+
WS_AGENT_ACCOUNT: instance.account,
|
|
238
|
+
},
|
|
239
|
+
...(service ? { isolatedEnv: true } : {}),
|
|
240
|
+
input: '',
|
|
241
|
+
timeoutMs: 150_000,
|
|
242
|
+
}
|
|
243
|
+
);
|
|
244
|
+
const outcome = sessionResult(result.ok, result.stdout + result.stderr);
|
|
245
|
+
return {
|
|
246
|
+
id: 'account-session',
|
|
247
|
+
status:
|
|
248
|
+
outcome === 'ok'
|
|
249
|
+
? 'pass'
|
|
250
|
+
: outcome === 'cooling-down'
|
|
251
|
+
? 'warn'
|
|
252
|
+
: 'fail',
|
|
253
|
+
summary: outcome,
|
|
254
|
+
};
|
|
255
|
+
},
|
|
256
|
+
context.cache
|
|
257
|
+
);
|
|
258
|
+
return check.status === 'pass'
|
|
259
|
+
? 'ok'
|
|
260
|
+
: check.status === 'warn'
|
|
261
|
+
? 'cooling-down'
|
|
262
|
+
: 'failed';
|
|
263
|
+
}
|
|
264
|
+
|
|
109
265
|
/** Reads `tool = "x"` and `"tool" = { version = "x", ... }` pins. */
|
|
110
266
|
export function parsePins(source: string): Record<string, string> {
|
|
111
267
|
const pins: Record<string, string> = {};
|
|
@@ -168,19 +324,26 @@ function quotaPass(agent: Agent): Check {
|
|
|
168
324
|
|
|
169
325
|
async function sessionCheck(
|
|
170
326
|
agent: Agent,
|
|
171
|
-
context: { node: string; workspace: string; cwd: string },
|
|
327
|
+
context: { node: string; workspace: string; cwd: string; launch: Launch },
|
|
172
328
|
run: Runner
|
|
173
329
|
): Promise<Check> {
|
|
330
|
+
const agents = join(context.workspace, 'agents');
|
|
331
|
+
const command =
|
|
332
|
+
context.launch.kind === 'service'
|
|
333
|
+
? [join(agents, agent.launcher), ...agent.args]
|
|
334
|
+
: ['bun', join(agents, agent.wrapper), ...agent.args];
|
|
335
|
+
const env =
|
|
336
|
+
context.launch.kind === 'service'
|
|
337
|
+
? { ...context.launch.env, WS_AGENT_NODE: context.node }
|
|
338
|
+
: { WS_AGENT_NODE: context.node, PATH: pinnedPath() };
|
|
174
339
|
const launch = () =>
|
|
175
|
-
run(
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
}
|
|
183
|
-
);
|
|
340
|
+
run(command, {
|
|
341
|
+
cwd: context.cwd,
|
|
342
|
+
env,
|
|
343
|
+
...(context.launch.kind === 'service' ? { isolatedEnv: true } : {}),
|
|
344
|
+
input: '',
|
|
345
|
+
timeoutMs: 150_000,
|
|
346
|
+
});
|
|
184
347
|
const expected = /\bOK\b/;
|
|
185
348
|
let result = await launch();
|
|
186
349
|
// One retry: a CLI's first launch on a node may still be initializing.
|
|
@@ -196,6 +359,114 @@ async function sessionCheck(
|
|
|
196
359
|
};
|
|
197
360
|
}
|
|
198
361
|
|
|
362
|
+
/** The environment of the T3 user service: what its user manager holds
|
|
363
|
+
* (PATH included) and what the unit adds. Every launcher starts with
|
|
364
|
+
* `#!/usr/bin/env bun` or runs `bun`, so that PATH must find Bun.
|
|
365
|
+
*/
|
|
366
|
+
async function serviceLaunch(
|
|
367
|
+
run: Runner
|
|
368
|
+
): Promise<{ launch: Launch; checks: Check[] }> {
|
|
369
|
+
const manager = await run(['systemctl', '--user', 'show-environment'], {
|
|
370
|
+
timeoutMs: 15_000,
|
|
371
|
+
});
|
|
372
|
+
const unit = await run(
|
|
373
|
+
[
|
|
374
|
+
'systemctl',
|
|
375
|
+
'--user',
|
|
376
|
+
'show',
|
|
377
|
+
T3_SERVICE,
|
|
378
|
+
'--property=Environment',
|
|
379
|
+
'--value',
|
|
380
|
+
],
|
|
381
|
+
{ timeoutMs: 15_000 }
|
|
382
|
+
);
|
|
383
|
+
if (!manager.ok || !unit.ok)
|
|
384
|
+
return {
|
|
385
|
+
launch: { kind: 'sync' },
|
|
386
|
+
checks: [
|
|
387
|
+
environmentCheck(
|
|
388
|
+
'warn',
|
|
389
|
+
'AGENT_SERVICE_ENVIRONMENT_UNVERIFIED',
|
|
390
|
+
'The environment of the T3 service could not be read, so sessions were launched from the sync instead. Inspect the user service manager of this worker: systemctl --user show-environment.'
|
|
391
|
+
),
|
|
392
|
+
],
|
|
393
|
+
};
|
|
394
|
+
const env = {
|
|
395
|
+
...parseAssignments(manager.stdout.split('\n')),
|
|
396
|
+
...parseAssignments(words(unit.stdout)),
|
|
397
|
+
};
|
|
398
|
+
const bun = await run(['/bin/sh', '-c', 'command -v bun'], {
|
|
399
|
+
env,
|
|
400
|
+
isolatedEnv: true,
|
|
401
|
+
timeoutMs: 15_000,
|
|
402
|
+
});
|
|
403
|
+
if (!bun.ok)
|
|
404
|
+
return {
|
|
405
|
+
launch: { kind: 'incomplete' },
|
|
406
|
+
checks: [
|
|
407
|
+
environmentCheck(
|
|
408
|
+
'fail',
|
|
409
|
+
'AGENT_SERVICE_ENVIRONMENT_INCOMPLETE',
|
|
410
|
+
INCOMPLETE_REMEDY
|
|
411
|
+
),
|
|
412
|
+
],
|
|
413
|
+
};
|
|
414
|
+
return {
|
|
415
|
+
launch: { kind: 'service', env },
|
|
416
|
+
checks: [
|
|
417
|
+
environmentCheck(
|
|
418
|
+
'pass',
|
|
419
|
+
'The T3 service environment finds Bun; every session was launched with it alone.'
|
|
420
|
+
),
|
|
421
|
+
],
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
const INCOMPLETE_REMEDY = `The PATH of ${T3_SERVICE} does not find Bun, so T3 cannot start any agent launcher even though they work from a shell. Put the pinned shims on the PATH of the user service manager through the baseline (never by hand), then run fleet-sync.`;
|
|
426
|
+
|
|
427
|
+
function environmentCheck(
|
|
428
|
+
status: Check['status'],
|
|
429
|
+
summary: string,
|
|
430
|
+
remedy?: string
|
|
431
|
+
): Check {
|
|
432
|
+
return {
|
|
433
|
+
id: 'agent:service-environment',
|
|
434
|
+
status,
|
|
435
|
+
summary,
|
|
436
|
+
...(remedy ? { remedy: { action: remedy } } : {}),
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
function incompleteSession(agent: Agent): Check {
|
|
441
|
+
return {
|
|
442
|
+
id: `agent:${agent.name}:session`,
|
|
443
|
+
status: 'fail',
|
|
444
|
+
summary: 'AGENT_SERVICE_ENVIRONMENT_INCOMPLETE',
|
|
445
|
+
remedy: { action: INCOMPLETE_REMEDY },
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/** `KEY=value` lines or words; a value systemd quoted loses its quotes. */
|
|
450
|
+
function parseAssignments(entries: string[]): Record<string, string> {
|
|
451
|
+
const env: Record<string, string> = {};
|
|
452
|
+
for (const entry of entries) {
|
|
453
|
+
const match = /^([A-Za-z_][A-Za-z0-9_]*)=(.*)$/s.exec(entry.trim());
|
|
454
|
+
if (match) env[match[1]!] = unquote(match[2]!);
|
|
455
|
+
}
|
|
456
|
+
return env;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/** `systemctl show -p Environment` separates assignments with spaces and
|
|
460
|
+
* double-quotes one that holds a space.
|
|
461
|
+
*/
|
|
462
|
+
function words(line: string): string[] {
|
|
463
|
+
return line.trim().match(/(?:[^\s"]+|"[^"]*")+/g) ?? [];
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
function unquote(value: string): string {
|
|
467
|
+
return value.replace(/"([^"]*)"/g, '$1').replace(/^\$'(.*)'$/s, '$1');
|
|
468
|
+
}
|
|
469
|
+
|
|
199
470
|
/** A stable code only; raw CLI output is never reported. */
|
|
200
471
|
function sessionFailure(output: string): string {
|
|
201
472
|
if (/missing or out of date/.test(output)) return 'AGENT_PROFILE_STALE';
|
|
@@ -8,9 +8,15 @@ export class ProviderVerificationError extends AgentConfigurationError {
|
|
|
8
8
|
super(message);
|
|
9
9
|
}
|
|
10
10
|
}
|
|
11
|
+
/** The gateway has no such route, method or media type for this protocol:
|
|
12
|
+
* the route as declared cannot work, whatever the account.
|
|
13
|
+
*/
|
|
14
|
+
const INCOMPATIBLE = new Set([404, 405, 415, 501]);
|
|
15
|
+
|
|
11
16
|
export function providerHttpFailure(status: number): ProviderVerificationError {
|
|
12
|
-
const code =
|
|
13
|
-
|
|
17
|
+
const code = INCOMPATIBLE.has(status)
|
|
18
|
+
? 'PROVIDER_PROTOCOL_INCOMPATIBLE'
|
|
19
|
+
: status === 401 || status === 403
|
|
14
20
|
? 'PROVIDER_AUTH_REJECTED'
|
|
15
21
|
: status === 429
|
|
16
22
|
? 'PROVIDER_RATE_LIMITED'
|
|
@@ -27,6 +33,7 @@ const SAFE_CODES = new Set([
|
|
|
27
33
|
'PROVIDER_RATE_LIMITED',
|
|
28
34
|
'PROVIDER_UPSTREAM_UNAVAILABLE',
|
|
29
35
|
'PROVIDER_PROTOCOL_REJECTED',
|
|
36
|
+
'PROVIDER_PROTOCOL_INCOMPATIBLE',
|
|
30
37
|
'PROVIDER_MODEL_UNAVAILABLE',
|
|
31
38
|
'PROVIDER_OUTPUT_UNVERIFIED',
|
|
32
39
|
'PROVIDER_TOOL_RESULT_UNVERIFIED',
|
package/src/agents/registry.ts
CHANGED
|
@@ -14,7 +14,13 @@ export type Selection = {
|
|
|
14
14
|
/** The tracked registry plus the proxy endpoint from this fleet's config. */
|
|
15
15
|
export const registry = {
|
|
16
16
|
...source,
|
|
17
|
-
proxy: {
|
|
17
|
+
proxy: {
|
|
18
|
+
// Read on use: a command that reaches no proxy needs no configuration.
|
|
19
|
+
get endpoint(): string {
|
|
20
|
+
return fleet.services.proxy.endpoint;
|
|
21
|
+
},
|
|
22
|
+
...source.proxy,
|
|
23
|
+
},
|
|
18
24
|
};
|
|
19
25
|
|
|
20
26
|
export function select(node: string, tool: string, profile: string): Selection {
|
package/src/cli/agent-guide.ts
CHANGED
|
@@ -4,6 +4,7 @@ const SAFETY = [
|
|
|
4
4
|
'No arguments prints help, including on a TTY. --json returns this document. The menu requires --interactive; --json and --non-interactive never prompt.',
|
|
5
5
|
'Diagnostics, onboarding instructions, schema, catalogs, scripts and repair previews are read-only.',
|
|
6
6
|
'fleet-sync, fleet-node-sync, t3-apply and t3-cleanup apply by invocation; --dry-run previews. t3-apply --import ID explicitly adopts ownership. --json never implies a dry run. fleet-node-add and fleet-repair require --apply --yes.',
|
|
7
|
+
'fleet-state alone is read-only; --init, --import, --push and --restore each act on invocation. Bucket credentials come from DEVFLEET_BUCKET_ACCESS_KEY_ID and DEVFLEET_BUCKET_SECRET_ACCESS_KEY or a terminal prompt, never from arguments.',
|
|
7
8
|
'Do not copy credentials, forward SSH agents, bypass host keys, provision infrastructure or execute unreviewed remedies.',
|
|
8
9
|
'A healthy manager does not prove another machine can connect to it. Run fleet-status from each intended manager.',
|
|
9
10
|
'fleet-repair executes only allowlisted owner actions, then reruns diagnostics. Native logins, host trust and resource permissions remain explicit operator steps. Pass the reviewed planHash as --plan-hash to reject a changed plan.',
|
|
@@ -14,6 +15,7 @@ const SAFETY = [
|
|
|
14
15
|
const EXAMPLES = [
|
|
15
16
|
'devfleet fleet-node-sync',
|
|
16
17
|
'devfleet fleet-status',
|
|
18
|
+
'devfleet fleet-state --json',
|
|
17
19
|
'devfleet t3-status',
|
|
18
20
|
'devfleet fleet-status --local --workspace /path/to/personal --json',
|
|
19
21
|
'devfleet fleet-status --json --strict --timeout 120000',
|
|
@@ -28,6 +30,23 @@ const EXAMPLES = [
|
|
|
28
30
|
'devfleet fleet-repair --local --repair workspace --plan-hash <reviewed-sha256> --apply --yes --json',
|
|
29
31
|
];
|
|
30
32
|
|
|
33
|
+
/** For an agent working in another project, which is every project that uses
|
|
34
|
+
* dev-fleet: a change to dev-fleet is asked for, never made in place.
|
|
35
|
+
*/
|
|
36
|
+
function support(release: Release) {
|
|
37
|
+
return {
|
|
38
|
+
issues: release.issues ?? null,
|
|
39
|
+
report:
|
|
40
|
+
'A defect in dev-fleet, or something it cannot do yet, is raised as an issue in the dev-fleet repository: one issue per bug or feature request, after searching the open ones. From any other project that is the only way to change dev-fleet. Tell the operator what is blocked and link the issue.',
|
|
41
|
+
never:
|
|
42
|
+
'Do not edit the installed package, a checkout fleet-sync owns or a file dev-fleet manages on a machine: the next install or fleet-sync replaces each of them. Do not clone dev-fleet to patch it, vendor a modified copy, or hide the defect behind a workaround in the project that holds fleet.json.',
|
|
43
|
+
include:
|
|
44
|
+
'devfleet --version, the command and its flags, the error code or the failing check IDs from --json, what happened and what was expected.',
|
|
45
|
+
withhold:
|
|
46
|
+
'The issue tracker is public. Leave out credentials, the fleet configuration, hostnames, accounts, domains, tailnet names, paths under a home directory and raw subprocess output.',
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
31
50
|
export function agentGuide(release: Release) {
|
|
32
51
|
return {
|
|
33
52
|
version: 1,
|
|
@@ -36,21 +55,29 @@ export function agentGuide(release: Release) {
|
|
|
36
55
|
releasedAt: release.releasedAt,
|
|
37
56
|
invocation: 'devfleet <command> --json --non-interactive',
|
|
38
57
|
discovery: 'devfleet commands --json',
|
|
58
|
+
setup:
|
|
59
|
+
'Setting up a fleet from a project: llms.txt and docs/consumer-setup.md in this package (node_modules/@crvouga/dev-fleet/), also at https://github.com/crvouga/dev-fleet/blob/main/docs/consumer-setup.md',
|
|
39
60
|
defaultCommand: 'help',
|
|
40
61
|
help: 'devfleet <command> --help',
|
|
41
62
|
interactive:
|
|
42
63
|
'devfleet --interactive (explicit opt-in; TTY required, disabled in CI)',
|
|
43
64
|
groups: {
|
|
44
65
|
'fleet-*':
|
|
45
|
-
'Developer computers: fleet-status, fleet-discover, fleet-sync, fleet-node-sync, fleet-repair, fleet-nodes, fleet-node-add',
|
|
66
|
+
'Developer computers: fleet-status, fleet-discover, fleet-sync, fleet-state, fleet-node-sync, fleet-repair, fleet-nodes, fleet-node-add',
|
|
46
67
|
't3-*': 'Declared T3 provider instances: t3-status, t3-apply, t3-cleanup',
|
|
47
68
|
cliproxy: 'The CLI proxy service on the manager; arguments are forwarded',
|
|
48
69
|
},
|
|
49
70
|
configuration: {
|
|
50
71
|
workspace:
|
|
51
|
-
'--workspace PATH, PERSONAL_WORKSPACE,
|
|
72
|
+
'The published package itself. Run from a checkout (development): --workspace PATH, PERSONAL_WORKSPACE, then the dev-fleet checkout the command is started in',
|
|
52
73
|
roster:
|
|
53
|
-
'--config PATH, PERSONAL_FLEET_CONFIG, closest ancestor .fleet.local.json, then
|
|
74
|
+
'--config PATH, PERSONAL_FLEET_CONFIG, closest ancestor .fleet.local.json, then the roster in the fleet state',
|
|
75
|
+
definition:
|
|
76
|
+
'DEVFLEET_CONFIG, then the closest fleet.json in the working directory or above it (the operator’s project), then the configuration fleet-sync last applied from the fleet state (the devfleet CLI on the manager), then the copy fleet-sync installed at ~/.config/dev-fleet/fleet.json',
|
|
77
|
+
state:
|
|
78
|
+
'DEVFLEET_STATE or ~/.local/share/dev-fleet/state: a local copy of the roster, the approved revision and the configuration last applied. The bucket named under "state" in the fleet configuration is the source of truth; its credentials are passed in DEVFLEET_BUCKET_ACCESS_KEY_ID and DEVFLEET_BUCKET_SECRET_ACCESS_KEY, or saved by devfleet fleet-state --init in ~/.config/dev-fleet/bucket.json for unattended runs. devfleet fleet-state --json compares the copies',
|
|
79
|
+
revision:
|
|
80
|
+
'The commit the published package names (devfleet --version). fleet-sync converges every machine to it; --revision SHA selects another',
|
|
54
81
|
},
|
|
55
82
|
output: {
|
|
56
83
|
stdout:
|
|
@@ -70,13 +97,20 @@ export function agentGuide(release: Release) {
|
|
|
70
97
|
'Default fleet-status tests the manager and all configured nodes. --local tests only this manager; --tailnet and --workers cover Tailscale computers. A missing roster fails explicitly. T3/VS Code end-to-end checks remain manual.',
|
|
71
98
|
},
|
|
72
99
|
safety: SAFETY,
|
|
100
|
+
support: support(release),
|
|
101
|
+
toolchain:
|
|
102
|
+
'On a fleet machine the global mise configuration belongs to dev-fleet: fleet-sync publishes it, and fleet-status fails tool:global-mise-config when anything replaced it. A project pins its own tools in a project mise.toml (mise install --locked in its checkout) and never publishes a global one.',
|
|
103
|
+
library:
|
|
104
|
+
'None. The package is the devfleet command: it declares no main or exports, and nothing under src/ is a supported import; any release may change it. Ask for a command in an issue instead.',
|
|
73
105
|
fleetSync: {
|
|
74
106
|
command: 'devfleet fleet-sync',
|
|
75
107
|
preview: 'devfleet fleet-sync --dry-run --json',
|
|
76
|
-
approval:
|
|
108
|
+
approval:
|
|
109
|
+
'Applies on invocation without a prompt; --dry-run previews and changes no machine: on the manager it only checks the release out into dev-fleet’s cache and installs the Bun it pins, never the toolchain, the global mise configuration or shell profiles',
|
|
77
110
|
scope:
|
|
78
111
|
'Whole enrolled Ubuntu fleet over verified SSH; --node filters diagnostics/rollout',
|
|
79
|
-
state:
|
|
112
|
+
state:
|
|
113
|
+
'The approved and applied revision are kept in the fleet state and uploaded to its bucket after every run; the report carries the state:replica check',
|
|
80
114
|
readiness:
|
|
81
115
|
'Full Ubuntu acceptance is incomplete and fails closed; do not override it or claim deployment success.',
|
|
82
116
|
},
|
package/src/cli/app.ts
CHANGED
|
@@ -3,13 +3,20 @@ import { resolveSettings, type Settings } from './settings';
|
|
|
3
3
|
import { COMMANDS } from './registry';
|
|
4
4
|
import { execute } from './program';
|
|
5
5
|
import { parseArgs } from 'node:util';
|
|
6
|
-
import
|
|
6
|
+
import { packageInfo } from '../package-info';
|
|
7
7
|
|
|
8
8
|
/** The manifest carries `0.0.0-development` in the repository; the release
|
|
9
|
-
* workflow stamps the tag's version into the package it
|
|
9
|
+
* workflow stamps the tag's version, and the commit, into the package it
|
|
10
|
+
* publishes.
|
|
10
11
|
*/
|
|
11
|
-
|
|
12
|
-
const
|
|
12
|
+
const PACKAGE = packageInfo();
|
|
13
|
+
export const VERSION = PACKAGE.version;
|
|
14
|
+
const RELEASE = {
|
|
15
|
+
version: VERSION,
|
|
16
|
+
releasedAt: null,
|
|
17
|
+
revision: PACKAGE.revision,
|
|
18
|
+
issues: PACKAGE.issues,
|
|
19
|
+
};
|
|
13
20
|
|
|
14
21
|
export function wantsInteractive(
|
|
15
22
|
args: string[],
|
package/src/cli/cli.ts
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { main } from './app';
|
|
3
|
+
import { useProjectConfig } from './project';
|
|
4
|
+
import { actAsOperator } from '../state/operator';
|
|
5
|
+
import { databasePath } from '../state/paths';
|
|
6
|
+
import { readDocument } from '../state/store';
|
|
7
|
+
|
|
8
|
+
actAsOperator((root) => readDocument(databasePath(root), 'definition'));
|
|
9
|
+
useProjectConfig();
|
|
3
10
|
|
|
4
11
|
try {
|
|
5
12
|
process.exitCode = await main();
|
package/src/cli/command-index.ts
CHANGED
|
@@ -30,6 +30,13 @@ function execution(entry: CommandSpec) {
|
|
|
30
30
|
'enrolled Ubuntu fleet over verified SSH; manager is not an implicit target',
|
|
31
31
|
note: 'JSON/noninteractive never prompt. Full Ubuntu acceptance is not yet implemented.',
|
|
32
32
|
};
|
|
33
|
+
if (entry.mutation && entry.id.startsWith('fleet-state '))
|
|
34
|
+
return {
|
|
35
|
+
default: 'apply',
|
|
36
|
+
authorization: 'command-invocation',
|
|
37
|
+
preview: 'devfleet fleet-state --json',
|
|
38
|
+
note: 'Each flag is one explicit action on this manager’s fleet state. JSON selects output format; it does not prevent writes.',
|
|
39
|
+
};
|
|
33
40
|
if (APPLIES.has(entry.id))
|
|
34
41
|
return {
|
|
35
42
|
default: 'apply',
|