@crvouga/personal 0.0.0-stage → 0.1.1
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 +237 -2
- package/cli.mjs +4207 -0
- package/commands.json +491 -0
- package/package.json +42 -4
- package/probe.mjs +1010 -0
- package/releasing.md +160 -0
- package/schema.json +56 -0
package/probe.mjs
ADDED
|
@@ -0,0 +1,1010 @@
|
|
|
1
|
+
// packages/fleet/src/configure.ts
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { mkdir, writeFile, rename } from "node:fs/promises";
|
|
4
|
+
import { randomUUID } from "node:crypto";
|
|
5
|
+
|
|
6
|
+
// packages/fleet/src/errors.ts
|
|
7
|
+
class FleetUsageError extends Error {
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// packages/fleet/src/config.ts
|
|
11
|
+
var CAPABILITIES = [
|
|
12
|
+
"github",
|
|
13
|
+
"vault",
|
|
14
|
+
"t3",
|
|
15
|
+
"vscode",
|
|
16
|
+
"toolchain",
|
|
17
|
+
"codex",
|
|
18
|
+
"claude",
|
|
19
|
+
"services",
|
|
20
|
+
"workstation"
|
|
21
|
+
];
|
|
22
|
+
function record(value) {
|
|
23
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
24
|
+
}
|
|
25
|
+
function fields(value, allowed) {
|
|
26
|
+
if (Object.keys(value).some((key) => !allowed.includes(key))) {
|
|
27
|
+
throw new FleetUsageError("Unknown configuration field. Run fleet schema.");
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
function absolutePath(value) {
|
|
31
|
+
return typeof value === "string" && value.startsWith("/") && value.length <= 4096 && !/[\x00-\x1f\x7f]/.test(value);
|
|
32
|
+
}
|
|
33
|
+
function parseNode(value) {
|
|
34
|
+
if (!record(value))
|
|
35
|
+
throw new FleetUsageError("Each node must be an object.");
|
|
36
|
+
fields(value, ["id", "ssh", "workspace", "runtime", "required"]);
|
|
37
|
+
const names = nodeNames(value);
|
|
38
|
+
if (!absolutePath(value["workspace"]))
|
|
39
|
+
throw new FleetUsageError("workspace must be an absolute POSIX path.");
|
|
40
|
+
if (value["runtime"] !== undefined && !absolutePath(value["runtime"])) {
|
|
41
|
+
throw new FleetUsageError("runtime must be an absolute POSIX path to Node or Bun.");
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
...names,
|
|
45
|
+
workspace: value["workspace"],
|
|
46
|
+
...typeof value["runtime"] === "string" ? { runtime: value["runtime"] } : {},
|
|
47
|
+
required: requiredCapabilities(value["required"])
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
function nodeNames(value) {
|
|
51
|
+
if (typeof value["id"] !== "string" || !/^[a-z][a-z0-9-]{0,62}$/.test(value["id"])) {
|
|
52
|
+
throw new FleetUsageError("Node IDs must be lowercase names (1–63 characters).");
|
|
53
|
+
}
|
|
54
|
+
if (typeof value["ssh"] !== "string" || !/^[a-zA-Z0-9][a-zA-Z0-9._-]{0,252}$/.test(value["ssh"])) {
|
|
55
|
+
throw new FleetUsageError("ssh must be a plain SSH alias or hostname, not options or user@host.");
|
|
56
|
+
}
|
|
57
|
+
return { id: value["id"], ssh: value["ssh"] };
|
|
58
|
+
}
|
|
59
|
+
function requiredCapabilities(value) {
|
|
60
|
+
const required = value === undefined ? ["github", "vault", "t3", "toolchain"] : value;
|
|
61
|
+
if (!Array.isArray(required) || required.some((item) => !CAPABILITIES.includes(item))) {
|
|
62
|
+
throw new FleetUsageError("required must contain only capability names from fleet schema.");
|
|
63
|
+
}
|
|
64
|
+
if (new Set(required).size !== required.length)
|
|
65
|
+
throw new FleetUsageError("required capabilities must be unique.");
|
|
66
|
+
return required;
|
|
67
|
+
}
|
|
68
|
+
function parseConfig(value) {
|
|
69
|
+
if (!record(value))
|
|
70
|
+
throw new FleetUsageError("Fleet configuration must be an object.");
|
|
71
|
+
fields(value, ["$schema", "version", "nodes"]);
|
|
72
|
+
if (value["$schema"] !== undefined && typeof value["$schema"] !== "string")
|
|
73
|
+
throw new FleetUsageError("$schema must be a string.");
|
|
74
|
+
if (value["version"] !== 1 || !Array.isArray(value["nodes"])) {
|
|
75
|
+
throw new FleetUsageError("Expected version 1 and a nodes array.");
|
|
76
|
+
}
|
|
77
|
+
if (value["nodes"].length < 1 || value["nodes"].length > 64) {
|
|
78
|
+
throw new FleetUsageError("Configure between 1 and 64 developer nodes.");
|
|
79
|
+
}
|
|
80
|
+
const nodes = value["nodes"].map(parseNode);
|
|
81
|
+
if (new Set(nodes.map((node) => node.id)).size !== nodes.length) {
|
|
82
|
+
throw new FleetUsageError("Node IDs must be unique.");
|
|
83
|
+
}
|
|
84
|
+
return { version: 1, nodes };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// packages/fleet/src/configure.ts
|
|
88
|
+
function validConfiguration(value) {
|
|
89
|
+
return record(value) && typeof value["revision"] === "string" && /^[a-f0-9]{40}$/.test(value["revision"]) && ["codex", "claude", "opencode", "gemini"].includes(String(value["tool"])) && ["direct", "proxy"].includes(String(value["profile"])) && typeof value["node"] === "string" && /^[a-z0-9][a-z0-9-]{0,62}$/.test(value["node"]);
|
|
90
|
+
}
|
|
91
|
+
async function configureWorkspace(root, request, run) {
|
|
92
|
+
if (!validConfiguration(request))
|
|
93
|
+
return failure("Invalid reviewed revision or configuration selection.");
|
|
94
|
+
const directory = join(root, "node_modules", ".fleet-revisions", request.revision);
|
|
95
|
+
const head = await run(["git", "rev-parse", "--verify", `${request.revision}^{commit}`], { cwd: root });
|
|
96
|
+
if (!head.ok || head.stdout.trim() !== request.revision)
|
|
97
|
+
return failure("Reviewed commit is unavailable locally. Fetch it explicitly and retry.");
|
|
98
|
+
const prepared = await prepareRevision(root, directory, request.revision, run);
|
|
99
|
+
if (prepared.length)
|
|
100
|
+
return prepared;
|
|
101
|
+
const setup = await run([
|
|
102
|
+
"mise",
|
|
103
|
+
"exec",
|
|
104
|
+
"--locked",
|
|
105
|
+
"bun",
|
|
106
|
+
"--",
|
|
107
|
+
"bun",
|
|
108
|
+
"scripts/dev/setup.ts",
|
|
109
|
+
"--non-interactive"
|
|
110
|
+
], { cwd: directory, timeoutMs: 1200000 });
|
|
111
|
+
if (!setup.ok)
|
|
112
|
+
return failure("Existing setup owner failed. Partial tool/dependency installation is possible; no configuration receipt was advanced.");
|
|
113
|
+
const applied = await applyOwner(directory, request, run);
|
|
114
|
+
if (!applied.ok)
|
|
115
|
+
return failure("personal workstation configuration failed. Inspect preview on this node; no rollout receipt was advanced.");
|
|
116
|
+
const receipt = decodeReceipt(applied.stdout, request.revision);
|
|
117
|
+
if (!receipt)
|
|
118
|
+
return failure("personal workstation returned an invalid receipt; rollout remains unverified.");
|
|
119
|
+
await recordConfiguration(root, request, receipt);
|
|
120
|
+
return [
|
|
121
|
+
{
|
|
122
|
+
id: "configuration:apply",
|
|
123
|
+
status: "pass",
|
|
124
|
+
summary: `personal workstation applied revision ${request.revision}; configuration fingerprint ${receipt}. Authentication and T3 end-to-end remain separate gates.`
|
|
125
|
+
}
|
|
126
|
+
];
|
|
127
|
+
}
|
|
128
|
+
function decodeReceipt(output, revision) {
|
|
129
|
+
try {
|
|
130
|
+
const data = JSON.parse(output);
|
|
131
|
+
return record(data) && data["ok"] === true && data["revision"] === revision && typeof data["fingerprint"] === "string" && /^[a-f0-9]{64}$/.test(data["fingerprint"]) ? data["fingerprint"] : null;
|
|
132
|
+
} catch {
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
function failure(summary) {
|
|
137
|
+
return [{ id: "configuration:apply", status: "fail", summary }];
|
|
138
|
+
}
|
|
139
|
+
async function prepareRevision(root, directory, revision, run) {
|
|
140
|
+
await mkdir(join(root, "node_modules", ".fleet-revisions"), {
|
|
141
|
+
recursive: true
|
|
142
|
+
});
|
|
143
|
+
let deployed = await run(["git", "rev-parse", "HEAD"], { cwd: directory });
|
|
144
|
+
if (!deployed.ok) {
|
|
145
|
+
const created = await run(["git", "worktree", "add", "--detach", directory, revision], { cwd: root, timeoutMs: 30000 });
|
|
146
|
+
if (!created.ok)
|
|
147
|
+
return failure("Cannot create pinned deployment worktree. Existing data was preserved.");
|
|
148
|
+
deployed = await run(["git", "rev-parse", "HEAD"], { cwd: directory });
|
|
149
|
+
}
|
|
150
|
+
const clean = await run(["git", "status", "--porcelain"], { cwd: directory });
|
|
151
|
+
if (!deployed.ok || deployed.stdout.trim() !== revision || !clean.ok || clean.stdout.trim())
|
|
152
|
+
return failure("Pinned deployment checkout differs or is dirty. Inspect it; fleet never resets it.");
|
|
153
|
+
const top = await run(["git", "rev-parse", "--show-toplevel"], {
|
|
154
|
+
cwd: directory
|
|
155
|
+
});
|
|
156
|
+
if (!top.ok || top.stdout.trim() !== directory)
|
|
157
|
+
return failure("Deployment path is not its own Git worktree.");
|
|
158
|
+
return [];
|
|
159
|
+
}
|
|
160
|
+
async function recordConfiguration(root, request, receipt) {
|
|
161
|
+
const receipts = join(root, "node_modules", ".fleet-configuration");
|
|
162
|
+
await mkdir(receipts, { recursive: true, mode: 448 });
|
|
163
|
+
const path = join(receipts, `${request.node}-${request.tool}-${request.profile}.json`);
|
|
164
|
+
const temp = `${path}.${randomUUID()}.tmp`;
|
|
165
|
+
await writeFile(temp, JSON.stringify({ version: 1, ...request, fingerprint: receipt }) + `
|
|
166
|
+
`, { flag: "wx", mode: 384 });
|
|
167
|
+
await rename(temp, path);
|
|
168
|
+
}
|
|
169
|
+
async function applyOwner(directory, request, run) {
|
|
170
|
+
return run([
|
|
171
|
+
"mise",
|
|
172
|
+
"exec",
|
|
173
|
+
"--locked",
|
|
174
|
+
"bun",
|
|
175
|
+
"--",
|
|
176
|
+
"bun",
|
|
177
|
+
"packages/workstation/cli/internal.ts",
|
|
178
|
+
"--json",
|
|
179
|
+
"--yes",
|
|
180
|
+
"--non-interactive",
|
|
181
|
+
"agents",
|
|
182
|
+
"apply",
|
|
183
|
+
"--node",
|
|
184
|
+
request.node,
|
|
185
|
+
"--tool",
|
|
186
|
+
request.tool,
|
|
187
|
+
"--profile",
|
|
188
|
+
request.profile
|
|
189
|
+
], { cwd: directory, timeoutMs: 120000 });
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// packages/fleet/src/fingerprint.ts
|
|
193
|
+
import { readFile as readFile2, mkdir as mkdir2, writeFile as writeFile2 } from "node:fs/promises";
|
|
194
|
+
import { join as join3 } from "node:path";
|
|
195
|
+
|
|
196
|
+
// packages/fleet/src/input-hashes.ts
|
|
197
|
+
import { createHash } from "node:crypto";
|
|
198
|
+
import { readFile, readdir } from "node:fs/promises";
|
|
199
|
+
import { join as join2 } from "node:path";
|
|
200
|
+
var INPUT_FILES = [
|
|
201
|
+
"package.json",
|
|
202
|
+
"bun.lock",
|
|
203
|
+
"mise.toml",
|
|
204
|
+
"mise.lock"
|
|
205
|
+
];
|
|
206
|
+
var FINGERPRINT_FILES = [
|
|
207
|
+
...INPUT_FILES,
|
|
208
|
+
"workspace-manifests",
|
|
209
|
+
"mise-sidecars"
|
|
210
|
+
];
|
|
211
|
+
async function fileHash(path) {
|
|
212
|
+
try {
|
|
213
|
+
return digest(await readFile(path));
|
|
214
|
+
} catch {
|
|
215
|
+
return null;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
async function manifestHash(root) {
|
|
219
|
+
try {
|
|
220
|
+
const entries = await readdir(join2(root, "packages"), {
|
|
221
|
+
withFileTypes: true
|
|
222
|
+
});
|
|
223
|
+
const hashes = [];
|
|
224
|
+
for (const entry of entries.filter((item) => item.isDirectory()).sort((left, right) => left.name.localeCompare(right.name, "en"))) {
|
|
225
|
+
hashes.push(`${entry.name}:${await fileHash(join2(root, "packages", entry.name, "package.json"))}`);
|
|
226
|
+
}
|
|
227
|
+
return digest(hashes.join(`
|
|
228
|
+
`));
|
|
229
|
+
} catch {
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
async function sidecarHash(root) {
|
|
234
|
+
try {
|
|
235
|
+
const files = await readdir(join2(root, ".mise/locks"), {
|
|
236
|
+
recursive: true,
|
|
237
|
+
withFileTypes: true
|
|
238
|
+
});
|
|
239
|
+
if (files.length > 4096 || files.some((entry) => entry.isSymbolicLink()))
|
|
240
|
+
return null;
|
|
241
|
+
const hashes = [];
|
|
242
|
+
const paths = files.filter((entry) => entry.isFile()).map((entry) => join2(entry.parentPath, entry.name)).sort();
|
|
243
|
+
for (const path of paths) {
|
|
244
|
+
const hash = await fileHash(path);
|
|
245
|
+
if (hash === null)
|
|
246
|
+
return null;
|
|
247
|
+
const relative = path.slice(join2(root, ".mise/locks").length).replaceAll("\\", "/");
|
|
248
|
+
hashes.push(`${relative}:${hash}`);
|
|
249
|
+
}
|
|
250
|
+
return digest(hashes.join(`
|
|
251
|
+
`));
|
|
252
|
+
} catch {
|
|
253
|
+
return null;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
function digest(value) {
|
|
257
|
+
return createHash("sha256").update(value).digest("hex");
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// packages/fleet/src/fingerprint.ts
|
|
261
|
+
var RECEIPT = "node_modules/.fleet-sync.json";
|
|
262
|
+
async function fingerprint(root, run) {
|
|
263
|
+
const files = {};
|
|
264
|
+
for (const name of INPUT_FILES)
|
|
265
|
+
files[name] = await fileHash(join3(root, name));
|
|
266
|
+
files["workspace-manifests"] = await manifestHash(root);
|
|
267
|
+
files["mise-sidecars"] = await sidecarHash(root);
|
|
268
|
+
const head = await run(["git", "rev-parse", "HEAD"], { cwd: root });
|
|
269
|
+
return {
|
|
270
|
+
head: head.ok && /^[a-f0-9]{40,64}\s*$/.test(head.stdout) ? head.stdout.trim() : null,
|
|
271
|
+
files
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
async function dependencyCheck(root, current) {
|
|
275
|
+
const remedy = {
|
|
276
|
+
action: "Review fleet sync, then explicitly apply a frozen toolchain/dependency sync."
|
|
277
|
+
};
|
|
278
|
+
try {
|
|
279
|
+
const receipt = JSON.parse(await readFile2(join3(root, RECEIPT), "utf8"));
|
|
280
|
+
const matches = record(receipt) && receipt["version"] === 1 && JSON.stringify(receipt["files"]) === JSON.stringify(current.files);
|
|
281
|
+
return {
|
|
282
|
+
id: "dependencies",
|
|
283
|
+
status: matches ? "pass" : "fail",
|
|
284
|
+
summary: matches ? "Last successful frozen sync matches input hashes; not a full integrity audit." : "Dependency sync receipt is stale or invalid.",
|
|
285
|
+
...matches ? {} : { remedy }
|
|
286
|
+
};
|
|
287
|
+
} catch {
|
|
288
|
+
return {
|
|
289
|
+
id: "dependencies",
|
|
290
|
+
status: "warn",
|
|
291
|
+
summary: "No frozen-sync receipt; installed dependencies are unverified.",
|
|
292
|
+
remedy
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
async function writeReceipt(root, current) {
|
|
297
|
+
await mkdir2(join3(root, "node_modules"), { recursive: true });
|
|
298
|
+
await writeFile2(join3(root, RECEIPT), JSON.stringify({ version: 1, files: current.files }), { mode: 384 });
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// packages/fleet/src/service-auth.ts
|
|
302
|
+
function json(result) {
|
|
303
|
+
try {
|
|
304
|
+
const data = JSON.parse(result.stdout);
|
|
305
|
+
return record(data) ? data : null;
|
|
306
|
+
} catch {
|
|
307
|
+
return null;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
async function codingAuth(id, run) {
|
|
311
|
+
return [
|
|
312
|
+
await serviceCheck(id === "codex" ? {
|
|
313
|
+
id,
|
|
314
|
+
command: ["codex", "login", "status"],
|
|
315
|
+
action: "Run codex login on this computer as its operator.",
|
|
316
|
+
valid: (result) => /^Logged in using (ChatGPT|an API key|API key)\b/im.test(`${result.stdout}
|
|
317
|
+
${result.stderr}`)
|
|
318
|
+
} : {
|
|
319
|
+
id,
|
|
320
|
+
command: ["claude", "auth", "status", "--json"],
|
|
321
|
+
action: "Run claude auth login on this computer as its operator.",
|
|
322
|
+
valid: (result) => json(result)?.["loggedIn"] === true
|
|
323
|
+
}, run)
|
|
324
|
+
];
|
|
325
|
+
}
|
|
326
|
+
async function serviceAuth(run) {
|
|
327
|
+
const services = [
|
|
328
|
+
{
|
|
329
|
+
id: "railway",
|
|
330
|
+
command: ["railway", "whoami", "--json"],
|
|
331
|
+
action: "Run railway login on this computer.",
|
|
332
|
+
valid: (result) => typeof json(result)?.["id"] === "string"
|
|
333
|
+
},
|
|
334
|
+
{
|
|
335
|
+
id: "vercel",
|
|
336
|
+
command: ["vercel", "whoami"],
|
|
337
|
+
action: "Run vercel login on this computer.",
|
|
338
|
+
valid: (result) => /^[a-zA-Z0-9][a-zA-Z0-9_-]{0,100}$/.test(result.stdout.trim())
|
|
339
|
+
},
|
|
340
|
+
{
|
|
341
|
+
id: "cloudflare",
|
|
342
|
+
command: ["wrangler", "whoami", "--json"],
|
|
343
|
+
action: "Run wrangler login on this computer.",
|
|
344
|
+
valid: (result) => json(result)?.["loggedIn"] === true
|
|
345
|
+
},
|
|
346
|
+
{
|
|
347
|
+
id: "aws",
|
|
348
|
+
command: [
|
|
349
|
+
"aws",
|
|
350
|
+
"sts",
|
|
351
|
+
"get-caller-identity",
|
|
352
|
+
"--output",
|
|
353
|
+
"json",
|
|
354
|
+
"--no-cli-pager"
|
|
355
|
+
],
|
|
356
|
+
action: "Select and authenticate the intended AWS profile on this computer.",
|
|
357
|
+
valid: (result) => typeof json(result)?.["Account"] === "string" && typeof json(result)?.["Arn"] === "string"
|
|
358
|
+
},
|
|
359
|
+
{
|
|
360
|
+
id: "npm",
|
|
361
|
+
command: ["npm", "whoami", "--registry=https://registry.npmjs.org/"],
|
|
362
|
+
action: "Run npm login only if this computer should publish packages.",
|
|
363
|
+
valid: (result) => /^[a-z0-9][a-z0-9._-]{0,213}$/.test(result.stdout.trim())
|
|
364
|
+
}
|
|
365
|
+
];
|
|
366
|
+
const checks = [];
|
|
367
|
+
for (const service of services)
|
|
368
|
+
checks.push(await serviceCheck(service, run));
|
|
369
|
+
for (const id of ["gemini", "copilot", "opencode"])
|
|
370
|
+
checks.push({
|
|
371
|
+
id: `auth:${id}`,
|
|
372
|
+
status: "skip",
|
|
373
|
+
summary: `${id} provider authorization has no verified noninteractive status adapter in this CLI. Installed binaries or saved keys are not proof of access.`,
|
|
374
|
+
remedy: {
|
|
375
|
+
action: `Verify ${id} provider access locally using its supported account diagnostics. No prompt or billable request was made.`
|
|
376
|
+
}
|
|
377
|
+
});
|
|
378
|
+
return checks;
|
|
379
|
+
}
|
|
380
|
+
async function serviceCheck(service, run) {
|
|
381
|
+
const result = await run(service.command, { timeoutMs: 15000 });
|
|
382
|
+
const ok = result.ok && service.valid(result);
|
|
383
|
+
return {
|
|
384
|
+
id: `auth:${service.id}`,
|
|
385
|
+
status: ok ? "pass" : "fail",
|
|
386
|
+
summary: ok ? `${service.id} reports an authenticated session in this process context; no credential or account output retained.` : `${service.id} login is missing or could not be verified (${result.reason}); no credential output retained.`,
|
|
387
|
+
...!ok ? { remedy: { action: service.action } } : {}
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// packages/fleet/src/workstation-checks.ts
|
|
392
|
+
async function workstationChecks(root, run) {
|
|
393
|
+
const result = await run(["personal", "workstation", "doctor", "--json", "--non-interactive"], {
|
|
394
|
+
cwd: root,
|
|
395
|
+
timeoutMs: 20000
|
|
396
|
+
});
|
|
397
|
+
const fallback = {
|
|
398
|
+
id: "workstation:configuration",
|
|
399
|
+
status: "fail",
|
|
400
|
+
summary: "Managed workstation configuration could not be verified.",
|
|
401
|
+
remedy: {
|
|
402
|
+
action: "Run personal workstation doctor --json on this computer. Review managed links, PATH, coding-tool/provider configuration and notification settings; apply personal workstation sync only explicitly."
|
|
403
|
+
}
|
|
404
|
+
};
|
|
405
|
+
try {
|
|
406
|
+
const data = JSON.parse(result.stdout);
|
|
407
|
+
if (!record(data) || !Array.isArray(data["checks"]) || !data["checks"].length || data["checks"].length > 100)
|
|
408
|
+
return [fallback];
|
|
409
|
+
const checks = data["checks"].map(configurationCheck);
|
|
410
|
+
if (checks.some((check) => check === null))
|
|
411
|
+
return [fallback];
|
|
412
|
+
return [
|
|
413
|
+
...!result.ok || data["ok"] !== true ? [fallback] : [],
|
|
414
|
+
...checks
|
|
415
|
+
];
|
|
416
|
+
} catch {
|
|
417
|
+
return [fallback];
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
function configurationCheck(value) {
|
|
421
|
+
if (!record(value) || typeof value["id"] !== "string" || !/^[a-zA-Z0-9:._-]{1,75}$/.test(value["id"]) || !["pass", "warn", "fail"].includes(String(value["severity"])))
|
|
422
|
+
return null;
|
|
423
|
+
const ok = value["severity"] === "pass";
|
|
424
|
+
return {
|
|
425
|
+
id: `workstation:${value["id"].toLowerCase()}`,
|
|
426
|
+
status: value["severity"],
|
|
427
|
+
summary: ok ? "Managed configuration check succeeded (configuration presence; provider authorization is checked separately)." : "Managed configuration is missing, conflicting or unverified.",
|
|
428
|
+
...!ok ? {
|
|
429
|
+
remedy: {
|
|
430
|
+
action: "Inspect personal workstation doctor --json on this computer. Use explicit personal workstation sync only after reviewing the configuration changes."
|
|
431
|
+
}
|
|
432
|
+
} : {}
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// packages/fleet/src/toolchain-checks.ts
|
|
437
|
+
import { join as join4 } from "node:path";
|
|
438
|
+
async function toolchainChecks(root, run) {
|
|
439
|
+
const result = await run(["bun", join4(root, "scripts/dev/doctor.ts"), "--json", "--docker"], { cwd: root, timeoutMs: 90000 });
|
|
440
|
+
const fallback = {
|
|
441
|
+
id: "auth:toolchain",
|
|
442
|
+
status: "fail",
|
|
443
|
+
summary: "Pinned tool/dependency and Docker checks could not be verified.",
|
|
444
|
+
remedy: {
|
|
445
|
+
action: "Run the checkout scripts/dev/doctor.ts --json --docker on this computer. Reconcile versions only through the shared mise setup."
|
|
446
|
+
}
|
|
447
|
+
};
|
|
448
|
+
try {
|
|
449
|
+
const data = JSON.parse(result.stdout);
|
|
450
|
+
if (!record(data) || typeof data["ok"] !== "boolean" || !Array.isArray(data["checks"]) || !data["checks"].length || data["checks"].length > 150)
|
|
451
|
+
return [fallback];
|
|
452
|
+
const checks = data["checks"].map(toolCheck);
|
|
453
|
+
if (checks.some((check) => check === null))
|
|
454
|
+
return [fallback];
|
|
455
|
+
return [
|
|
456
|
+
...!result.ok || !data["ok"] ? [fallback] : [],
|
|
457
|
+
...checks
|
|
458
|
+
];
|
|
459
|
+
} catch {
|
|
460
|
+
return [fallback];
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
function toolCheck(value) {
|
|
464
|
+
if (!record(value) || typeof value["id"] !== "string" || !/^[a-zA-Z0-9@:/._-]{1,75}$/.test(value["id"]) || !["pass", "fail", "warn", "skip"].includes(String(value["status"])))
|
|
465
|
+
return null;
|
|
466
|
+
const ok = value["status"] === "pass";
|
|
467
|
+
const version = typeof value["detail"] === "string" && /^v?\d{1,6}(?:\.\d{1,6}){1,3}$/.test(value["detail"]) ? ` ${value["detail"]}` : "";
|
|
468
|
+
return {
|
|
469
|
+
id: `tool:${value["id"].toLowerCase().replace(/[^a-z0-9:._-]/g, "_")}`,
|
|
470
|
+
status: value["status"],
|
|
471
|
+
summary: ok ? `Expected tool version or runtime check succeeded${version}.` : "Expected tool version, runtime or authentication check did not pass.",
|
|
472
|
+
...!ok ? {
|
|
473
|
+
remedy: {
|
|
474
|
+
action: "Review scripts/dev/doctor.ts --json --docker locally. Use shared mise/workstation setup to reconcile this computer."
|
|
475
|
+
}
|
|
476
|
+
} : {}
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// packages/fleet/src/auth-checks.ts
|
|
481
|
+
async function authCheck(capability, run, root) {
|
|
482
|
+
if (capability === "t3")
|
|
483
|
+
return t3Checks(run);
|
|
484
|
+
if (capability === "codex" || capability === "claude")
|
|
485
|
+
return codingAuth(capability, run);
|
|
486
|
+
if (capability === "services")
|
|
487
|
+
return serviceAuth(run);
|
|
488
|
+
if (capability === "workstation")
|
|
489
|
+
return workstationChecks(root, run);
|
|
490
|
+
if (capability === "toolchain")
|
|
491
|
+
return toolchainChecks(root, run);
|
|
492
|
+
return basicAuth(capability, run, root);
|
|
493
|
+
}
|
|
494
|
+
async function basicAuth(capability, run, root) {
|
|
495
|
+
const spec = {
|
|
496
|
+
github: {
|
|
497
|
+
command: ["gh", "auth", "status", "--active"],
|
|
498
|
+
action: "Authenticate on this node with gh auth login, then gh auth setup-git."
|
|
499
|
+
},
|
|
500
|
+
vault: {
|
|
501
|
+
command: ["vault", "token", "lookup", "-format=json"],
|
|
502
|
+
action: "Authenticate to the configured Vault on this node. Do not copy a token from another computer."
|
|
503
|
+
},
|
|
504
|
+
vscode: {
|
|
505
|
+
command: ["code", "--list-extensions"],
|
|
506
|
+
action: "Install the VS Code CLI on PATH and ms-vscode-remote.remote-ssh in the local editor profile."
|
|
507
|
+
}
|
|
508
|
+
}[capability];
|
|
509
|
+
const result = await run(spec.command, {
|
|
510
|
+
cwd: root,
|
|
511
|
+
timeoutMs: 15000
|
|
512
|
+
});
|
|
513
|
+
const extensions = result.stdout.toLowerCase().split(/\s+/);
|
|
514
|
+
const ok = result.ok && (capability !== "vscode" || extensions.includes("ms-vscode-remote.remote-ssh"));
|
|
515
|
+
return [
|
|
516
|
+
{
|
|
517
|
+
id: `auth:${capability}`,
|
|
518
|
+
status: ok ? "pass" : "fail",
|
|
519
|
+
summary: ok ? `${capability} check succeeded in this process context.` : `${capability} check failed (${result.reason}); no credential output retained.`,
|
|
520
|
+
...ok ? {} : { remedy: { action: spec.action } }
|
|
521
|
+
}
|
|
522
|
+
];
|
|
523
|
+
}
|
|
524
|
+
async function t3Checks(run) {
|
|
525
|
+
const config = await run(["t3", "status", "--json"]);
|
|
526
|
+
const checks = [t3Settings(config)];
|
|
527
|
+
const live = await run(["t3", "environment", "list", "--json"]);
|
|
528
|
+
checks.push({
|
|
529
|
+
id: "t3:api",
|
|
530
|
+
status: live.ok ? "pass" : "fail",
|
|
531
|
+
summary: live.ok ? "Local T3 API answered a read-only environment listing; peer connectivity is not inferred." : "Local T3 API did not answer the environment listing.",
|
|
532
|
+
...!live.ok ? {
|
|
533
|
+
remedy: {
|
|
534
|
+
action: "Check t3 service status and t3 status. Inspect linked peers in T3 Settings → Connections; relink only with operator approval."
|
|
535
|
+
}
|
|
536
|
+
} : {}
|
|
537
|
+
});
|
|
538
|
+
checks.push({
|
|
539
|
+
id: "t3:end-to-end",
|
|
540
|
+
status: "skip",
|
|
541
|
+
summary: "Relay traversal and remote peer authorization require an operator connection test; local settings are not proof."
|
|
542
|
+
});
|
|
543
|
+
return checks;
|
|
544
|
+
}
|
|
545
|
+
function t3Settings(config) {
|
|
546
|
+
const fallback = {
|
|
547
|
+
id: "t3:settings",
|
|
548
|
+
status: "fail",
|
|
549
|
+
summary: "T3 settings unavailable or unsupported CLI response.",
|
|
550
|
+
remedy: {
|
|
551
|
+
action: "Run t3 status --json on this node. Complete t3 up --connect as its operator if needed."
|
|
552
|
+
}
|
|
553
|
+
};
|
|
554
|
+
try {
|
|
555
|
+
const data = JSON.parse(config.stdout);
|
|
556
|
+
if (config.ok && record(data) && record(data["connect"])) {
|
|
557
|
+
const enabled = data["connect"]["enabled"] === true && data["connect"]["revoked"] === false;
|
|
558
|
+
return {
|
|
559
|
+
id: "t3:settings",
|
|
560
|
+
status: enabled ? "pass" : "fail",
|
|
561
|
+
summary: enabled ? "T3 Connect enabled and not marked revoked (settings only)." : "T3 Connect disabled, revoked, or unknown.",
|
|
562
|
+
...!enabled ? {
|
|
563
|
+
remedy: {
|
|
564
|
+
action: "Review t3 status, then complete t3 up --connect as the operator. Never share pairing tokens."
|
|
565
|
+
}
|
|
566
|
+
} : {}
|
|
567
|
+
};
|
|
568
|
+
}
|
|
569
|
+
} catch {}
|
|
570
|
+
return fallback;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
// packages/fleet/src/probe.ts
|
|
574
|
+
async function probe(root, required, run) {
|
|
575
|
+
const current = await fingerprint(root, run);
|
|
576
|
+
const checks = [
|
|
577
|
+
{
|
|
578
|
+
id: "workspace",
|
|
579
|
+
status: Object.values(current.files).every(Boolean) && current.head ? "pass" : "fail",
|
|
580
|
+
summary: "Checkout revision and shared dependency/toolchain input availability."
|
|
581
|
+
}
|
|
582
|
+
];
|
|
583
|
+
const dirty = await run(["git", "status", "--porcelain"], { cwd: root });
|
|
584
|
+
checks.push({
|
|
585
|
+
id: "git:clean",
|
|
586
|
+
status: dirty.ok ? dirty.stdout.trim() ? "warn" : "pass" : "fail",
|
|
587
|
+
summary: dirty.ok && !dirty.stdout.trim() ? "Working tree is clean." : "Working tree is dirty or unreadable; sync --pull will refuse."
|
|
588
|
+
});
|
|
589
|
+
checks.push(await dependencyCheck(root, current));
|
|
590
|
+
for (const capability of CAPABILITIES) {
|
|
591
|
+
if (required.includes(capability))
|
|
592
|
+
checks.push(...await authCheck(capability, run, root));
|
|
593
|
+
else
|
|
594
|
+
checks.push({
|
|
595
|
+
id: `auth:${capability}`,
|
|
596
|
+
status: "skip",
|
|
597
|
+
summary: "Not required by this node configuration."
|
|
598
|
+
});
|
|
599
|
+
}
|
|
600
|
+
return {
|
|
601
|
+
version: 1,
|
|
602
|
+
platform: process.platform,
|
|
603
|
+
fingerprint: current,
|
|
604
|
+
checks
|
|
605
|
+
};
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
// packages/fleet/src/runner.ts
|
|
609
|
+
import { spawn } from "node:child_process";
|
|
610
|
+
var MAX_OUTPUT_BYTES = 1024 * 1024;
|
|
611
|
+
function terminate(child) {
|
|
612
|
+
try {
|
|
613
|
+
if (process.platform !== "win32" && child.pid)
|
|
614
|
+
process.kill(-child.pid, "SIGKILL");
|
|
615
|
+
else
|
|
616
|
+
child.kill("SIGKILL");
|
|
617
|
+
} catch {}
|
|
618
|
+
}
|
|
619
|
+
function runCommand(command, options = {}) {
|
|
620
|
+
return new Promise((resolve) => {
|
|
621
|
+
const child = spawn(command[0], command.slice(1), {
|
|
622
|
+
cwd: options.cwd,
|
|
623
|
+
env: {
|
|
624
|
+
...process.env,
|
|
625
|
+
...options.env,
|
|
626
|
+
GIT_TERMINAL_PROMPT: "0",
|
|
627
|
+
CI: "1"
|
|
628
|
+
},
|
|
629
|
+
stdio: "pipe",
|
|
630
|
+
windowsHide: true,
|
|
631
|
+
detached: process.platform !== "win32"
|
|
632
|
+
});
|
|
633
|
+
let stdout = "";
|
|
634
|
+
let stderr = "";
|
|
635
|
+
let bytes = 0;
|
|
636
|
+
let finished = false;
|
|
637
|
+
let reason = "exit";
|
|
638
|
+
const finish = (ok) => {
|
|
639
|
+
if (finished)
|
|
640
|
+
return;
|
|
641
|
+
finished = true;
|
|
642
|
+
clearTimeout(timer);
|
|
643
|
+
child.stdin.destroy();
|
|
644
|
+
child.stdout.destroy();
|
|
645
|
+
child.stderr.destroy();
|
|
646
|
+
resolve({ ok, stdout, stderr, reason });
|
|
647
|
+
};
|
|
648
|
+
const abort = (cause) => {
|
|
649
|
+
reason = cause;
|
|
650
|
+
terminate(child);
|
|
651
|
+
finish(false);
|
|
652
|
+
};
|
|
653
|
+
const timer = setTimeout(() => {
|
|
654
|
+
abort("timeout");
|
|
655
|
+
}, options.timeoutMs ?? 15000);
|
|
656
|
+
const capture = (stream, chunk) => {
|
|
657
|
+
if (finished)
|
|
658
|
+
return;
|
|
659
|
+
bytes += Buffer.byteLength(chunk);
|
|
660
|
+
if (bytes > MAX_OUTPUT_BYTES) {
|
|
661
|
+
abort("overflow");
|
|
662
|
+
return;
|
|
663
|
+
}
|
|
664
|
+
if (stream === "stdout")
|
|
665
|
+
stdout += chunk;
|
|
666
|
+
else
|
|
667
|
+
stderr += chunk;
|
|
668
|
+
};
|
|
669
|
+
child.stdout.setEncoding("utf8").on("data", (chunk) => capture("stdout", chunk));
|
|
670
|
+
child.stderr.setEncoding("utf8").on("data", (chunk) => capture("stderr", chunk));
|
|
671
|
+
child.stdin.on("error", () => {
|
|
672
|
+
return;
|
|
673
|
+
});
|
|
674
|
+
child.on("error", () => {
|
|
675
|
+
abort("missing");
|
|
676
|
+
});
|
|
677
|
+
child.on("close", (code) => {
|
|
678
|
+
finish(code === 0 && reason === "exit");
|
|
679
|
+
});
|
|
680
|
+
child.stdin.end(options.input);
|
|
681
|
+
});
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
// packages/fleet/src/sync.ts
|
|
685
|
+
import { unlink } from "node:fs/promises";
|
|
686
|
+
import { join as join5 } from "node:path";
|
|
687
|
+
async function syncWorkspace(root, pull, run) {
|
|
688
|
+
const before = await fingerprint(root, run);
|
|
689
|
+
if (!completeFingerprint(before)) {
|
|
690
|
+
return failed("Not a complete workspace checkout. Follow fleet onboard first.");
|
|
691
|
+
}
|
|
692
|
+
if (pull) {
|
|
693
|
+
const dirty = await run(["git", "status", "--porcelain"], { cwd: root });
|
|
694
|
+
const branch = await run(["git", "symbolic-ref", "--quiet", "HEAD"], {
|
|
695
|
+
cwd: root
|
|
696
|
+
});
|
|
697
|
+
if (!dirty.ok || dirty.stdout.trim() || !branch.ok)
|
|
698
|
+
return failed("Refusing pull: dirty, detached, or unreadable checkout. Reconcile manually.");
|
|
699
|
+
}
|
|
700
|
+
await unlink(join5(root, "node_modules/.fleet-sync.json")).catch(() => {
|
|
701
|
+
return;
|
|
702
|
+
});
|
|
703
|
+
if (pull) {
|
|
704
|
+
const result = await run(["git", "pull", "--ff-only"], {
|
|
705
|
+
cwd: root,
|
|
706
|
+
timeoutMs: 120000
|
|
707
|
+
});
|
|
708
|
+
if (!result.ok)
|
|
709
|
+
return failed("Fast-forward pull failed. Inspect upstream/network/auth manually; nothing was reset.");
|
|
710
|
+
}
|
|
711
|
+
const expected = await fingerprint(root, run);
|
|
712
|
+
const result = await run([
|
|
713
|
+
"mise",
|
|
714
|
+
"exec",
|
|
715
|
+
"--locked",
|
|
716
|
+
"bun",
|
|
717
|
+
"--",
|
|
718
|
+
"bun",
|
|
719
|
+
"scripts/dev/setup.ts",
|
|
720
|
+
"--non-interactive"
|
|
721
|
+
], { cwd: root, timeoutMs: 1200000 });
|
|
722
|
+
if (!result.ok)
|
|
723
|
+
return failed(`Shared toolchain/dependency sync failed (${result.reason}). Inspect setup locally; partial installs are possible.`);
|
|
724
|
+
const after = await fingerprint(root, run);
|
|
725
|
+
if (JSON.stringify(expected) !== JSON.stringify(after))
|
|
726
|
+
return failed("Checkout inputs changed during sync. Retry from a stable checkout.");
|
|
727
|
+
await writeReceipt(root, after);
|
|
728
|
+
return [
|
|
729
|
+
{
|
|
730
|
+
id: "sync",
|
|
731
|
+
status: "pass",
|
|
732
|
+
summary: "Shared setup succeeded; frozen-sync receipt recorded. Auth and live connectivity still need doctor checks."
|
|
733
|
+
}
|
|
734
|
+
];
|
|
735
|
+
}
|
|
736
|
+
function failed(summary) {
|
|
737
|
+
return [
|
|
738
|
+
{
|
|
739
|
+
id: "sync",
|
|
740
|
+
status: "fail",
|
|
741
|
+
summary,
|
|
742
|
+
remedy: {
|
|
743
|
+
action: "Resolve locally using scripts/dev/README.md, then review and retry fleet sync."
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
];
|
|
747
|
+
}
|
|
748
|
+
function completeFingerprint(value) {
|
|
749
|
+
return value.head !== null && Object.values(value.files).every(Boolean);
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
// packages/fleet/src/repair-catalog.ts
|
|
753
|
+
var login = (id, title, commands, action) => ({
|
|
754
|
+
id: `login:${id}`,
|
|
755
|
+
title,
|
|
756
|
+
owner: `${id} native CLI`,
|
|
757
|
+
automatic: false,
|
|
758
|
+
action,
|
|
759
|
+
commands
|
|
760
|
+
});
|
|
761
|
+
var REPAIR_RECIPES = [
|
|
762
|
+
{
|
|
763
|
+
id: "workspace",
|
|
764
|
+
title: "Install locked tools and frozen dependencies",
|
|
765
|
+
owner: "mise / scripts/dev/setup.ts",
|
|
766
|
+
automatic: true,
|
|
767
|
+
action: "Runs the existing shared setup and trusted checkout lifecycle scripts. Updates user mise/PATH configuration and writes a sync receipt. Does not pull or reset Git.",
|
|
768
|
+
commands: [
|
|
769
|
+
[
|
|
770
|
+
"mise",
|
|
771
|
+
"exec",
|
|
772
|
+
"--locked",
|
|
773
|
+
"bun",
|
|
774
|
+
"--",
|
|
775
|
+
"bun",
|
|
776
|
+
"scripts/dev/setup.ts",
|
|
777
|
+
"--non-interactive"
|
|
778
|
+
]
|
|
779
|
+
]
|
|
780
|
+
},
|
|
781
|
+
{
|
|
782
|
+
id: "workstation",
|
|
783
|
+
title: "Install launcher and converge managed workstation configuration",
|
|
784
|
+
owner: "workstation",
|
|
785
|
+
automatic: true,
|
|
786
|
+
action: "Runs personal:install --yes: frozen install, launcher/PATH, managed links, notifications and provider configuration. Reads available provider keys from this machine’s Vault session and writes owner-managed 0600 configuration. Conflicting unmanaged files fail.",
|
|
787
|
+
commands: [["bun", "run", "personal:install", "--yes"]]
|
|
788
|
+
},
|
|
789
|
+
{
|
|
790
|
+
id: "providers",
|
|
791
|
+
title: "Refresh shared provider configuration from Vault",
|
|
792
|
+
owner: "workstation / secret-store",
|
|
793
|
+
automatic: true,
|
|
794
|
+
action: "Uses this machine’s own Vault authorization to fetch the existing permitted provider keys. The workstation owner writes its 0600 tool configuration; no credentials are copied from another computer. Missing provider keys may remain skipped.",
|
|
795
|
+
commands: [
|
|
796
|
+
[
|
|
797
|
+
"bun",
|
|
798
|
+
"packages/workstation/cli/internal.ts",
|
|
799
|
+
"opencode",
|
|
800
|
+
"sync",
|
|
801
|
+
"--strict",
|
|
802
|
+
"--json",
|
|
803
|
+
"--non-interactive",
|
|
804
|
+
"--yes"
|
|
805
|
+
]
|
|
806
|
+
]
|
|
807
|
+
},
|
|
808
|
+
login("codex", "Sign in to Codex on this machine", [["codex", "login", "--device-auth"]], "Approve the device flow in your browser. Run codex login status afterward; retain this machine’s own credential store."),
|
|
809
|
+
login("claude", "Sign in to Claude Code on this machine", [["claude", "auth", "login"]], "Complete the native browser login, then run claude auth status --json. API-key and subscription contexts can differ."),
|
|
810
|
+
login("github", "Sign in to GitHub and configure Git", [
|
|
811
|
+
[
|
|
812
|
+
"gh",
|
|
813
|
+
"auth",
|
|
814
|
+
"login",
|
|
815
|
+
"--hostname",
|
|
816
|
+
"github.com",
|
|
817
|
+
"--web",
|
|
818
|
+
"--git-protocol",
|
|
819
|
+
"https"
|
|
820
|
+
],
|
|
821
|
+
["gh", "auth", "setup-git"]
|
|
822
|
+
], "Approve GitHub in the browser on the intended account, then run gh auth status --active. Environment tokens can override stored login; inspect their source locally."),
|
|
823
|
+
login("vault", "Authorize this machine for the shared Vault", [], "Run vault login -method=userpass username=YOUR_USERNAME in your own terminal. The native CLI prompts privately for the password. Use the existing permitted project/config; never copy VAULT_TOKEN between computers."),
|
|
824
|
+
login("railway", "Sign in to Railway", [["railway", "login", "--browserless"]], "Complete the browserless native flow, then run railway whoami --json."),
|
|
825
|
+
login("vercel", "Sign in to Vercel", [["vercel", "login"]], "Complete native login, then run vercel whoami. Select the intended team/project separately; do not create resources as a login repair."),
|
|
826
|
+
login("cloudflare", "Sign in to Cloudflare", [["wrangler", "login", "--browser=false"]], "Complete native OAuth login, then run wrangler whoami --json. For headless SSH, prefer an existing scoped API token injected by the approved Vault config. --browser=false suppresses browser launch; it does not make a remote loopback callback reachable. Any OAuth callback tunnel needs explicit operator setup. Resource access still depends on account permissions."),
|
|
827
|
+
login("aws", "Refresh the selected AWS SSO session", [["aws", "sso", "login"]], "Configure the intended profile with aws configure sso first if needed. Login and the health check use this shell’s current profile; no access keys are cloned."),
|
|
828
|
+
login("npm", "Sign in to npm on this machine", [
|
|
829
|
+
[
|
|
830
|
+
"npm",
|
|
831
|
+
"login",
|
|
832
|
+
"--auth-type=web",
|
|
833
|
+
"--registry=https://registry.npmjs.org/"
|
|
834
|
+
]
|
|
835
|
+
], "Complete the specific CLI approval and run npm whoami. Prefer the existing trusted-publisher CI workflow for releases."),
|
|
836
|
+
login("opencode", "Authorize an OpenCode provider", [["opencode", "auth", "login"]], "Prefer the existing Vault-backed providers repair for managed API keys. Native auth login handles an operator-owned OAuth provider. Credential listing alone does not prove live provider access."),
|
|
837
|
+
login("copilot", "Sign in to Copilot", [["copilot", "login"]], "Complete native login; provider authorization still needs supported operator diagnostics. No model prompt is used for health checking."),
|
|
838
|
+
login("gemini", "Sign in to Gemini", [], "Open Gemini interactively and choose its supported sign-in flow. Provider authorization remains unverified without a supported read-only status adapter."),
|
|
839
|
+
{
|
|
840
|
+
id: "t3",
|
|
841
|
+
title: "Enable and authorize T3 Connect",
|
|
842
|
+
owner: "T3",
|
|
843
|
+
automatic: false,
|
|
844
|
+
action: "Run t3 up --connect in this machine’s terminal and approve the intended connection. Treat pairing material as private. Then check t3 status --json and perform an operator peer test.",
|
|
845
|
+
commands: [["t3", "up", "--connect"]]
|
|
846
|
+
},
|
|
847
|
+
{
|
|
848
|
+
id: "docker",
|
|
849
|
+
title: "Start the intended Docker runtime",
|
|
850
|
+
owner: "OS / Docker",
|
|
851
|
+
automatic: false,
|
|
852
|
+
action: "Start Docker Desktop on desktop systems, or the already-installed Docker service using the OS service manager on Linux. Confirm the intended context with docker context ls. This repair never installs a daemon, invokes sudo, changes contexts or exposes a TCP socket.",
|
|
853
|
+
commands: [
|
|
854
|
+
["docker", "context", "ls"],
|
|
855
|
+
["docker", "info"]
|
|
856
|
+
]
|
|
857
|
+
},
|
|
858
|
+
{
|
|
859
|
+
id: "password-manager",
|
|
860
|
+
title: "Review shared human login onboarding",
|
|
861
|
+
owner: "password-manager / Bitwarden",
|
|
862
|
+
automatic: false,
|
|
863
|
+
action: "Use the existing password-manager doctor and actions list on this computer. Its setup guides native consent and Bitwarden enrollment. Keep its private state and OS credential store local. Password vault access does not authorize native CLI OAuth sessions; imports and rotations remain separate reviewed workflows.",
|
|
864
|
+
commands: [
|
|
865
|
+
["bun", "run", "password-manager", "doctor"],
|
|
866
|
+
["bun", "run", "password-manager", "actions", "list"]
|
|
867
|
+
]
|
|
868
|
+
},
|
|
869
|
+
{
|
|
870
|
+
id: "operator",
|
|
871
|
+
title: "Resolve the check with its owning tool",
|
|
872
|
+
owner: "operator / existing owner",
|
|
873
|
+
automatic: false,
|
|
874
|
+
action: "Follow the check’s remedy on the affected machine. Verify host fingerprints, register real SSH aliases/checkouts, reconcile Git changes, or perform the named end-to-end test. Infrastructure changes remain OpenTofu-owned.",
|
|
875
|
+
commands: []
|
|
876
|
+
}
|
|
877
|
+
];
|
|
878
|
+
var AUTOMATED_REPAIRS = [
|
|
879
|
+
"workspace",
|
|
880
|
+
"workstation",
|
|
881
|
+
"providers"
|
|
882
|
+
];
|
|
883
|
+
function recipe(id) {
|
|
884
|
+
return REPAIR_RECIPES.find((entry) => entry.id === id) ?? REPAIR_RECIPES.find((entry) => entry.id === "operator");
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
// packages/fleet/src/repair-runner.ts
|
|
888
|
+
function validRepairs(value) {
|
|
889
|
+
return Array.isArray(value) && value.length > 0 && value.length <= 3 && new Set(value).size === value.length && value.every((id) => AUTOMATED_REPAIRS.includes(id));
|
|
890
|
+
}
|
|
891
|
+
async function repairWorkspace(root, repairs, run, expected) {
|
|
892
|
+
if (!validRepairs(repairs))
|
|
893
|
+
throw new Error("Unsupported repairs");
|
|
894
|
+
const before = await fingerprint(root, run);
|
|
895
|
+
if (!before.head || !Object.values(before.files).every(Boolean))
|
|
896
|
+
return [
|
|
897
|
+
{
|
|
898
|
+
id: "repair:workspace",
|
|
899
|
+
status: "fail",
|
|
900
|
+
summary: "Repairs require a complete existing checkout. Register/bootstrap it first."
|
|
901
|
+
}
|
|
902
|
+
];
|
|
903
|
+
if (expected && JSON.stringify(before) !== JSON.stringify(expected))
|
|
904
|
+
return [
|
|
905
|
+
{
|
|
906
|
+
id: "repair:inputs",
|
|
907
|
+
status: "fail",
|
|
908
|
+
summary: "Checkout inputs changed since diagnostics. Generate a fresh repair plan before applying."
|
|
909
|
+
}
|
|
910
|
+
];
|
|
911
|
+
const checks = [];
|
|
912
|
+
for (const id of AUTOMATED_REPAIRS.filter((entry) => repairs.includes(entry))) {
|
|
913
|
+
const result = id === "workspace" ? await syncWorkspace(root, false, run) : await ownerCommand(root, id, run);
|
|
914
|
+
checks.push(...result);
|
|
915
|
+
if (result.some((check) => check.status !== "pass"))
|
|
916
|
+
break;
|
|
917
|
+
}
|
|
918
|
+
const after = await fingerprint(root, run);
|
|
919
|
+
if (JSON.stringify(before) !== JSON.stringify(after))
|
|
920
|
+
checks.push({
|
|
921
|
+
id: "repair:inputs",
|
|
922
|
+
status: "fail",
|
|
923
|
+
summary: "Checkout inputs changed during repair. Stop and review the checkout before retrying."
|
|
924
|
+
});
|
|
925
|
+
return checks;
|
|
926
|
+
}
|
|
927
|
+
async function ownerCommand(root, id, run) {
|
|
928
|
+
const result = await run(recipe(id).commands[0], {
|
|
929
|
+
cwd: root,
|
|
930
|
+
timeoutMs: 1200000
|
|
931
|
+
});
|
|
932
|
+
return [
|
|
933
|
+
{
|
|
934
|
+
id: `repair:${id}`,
|
|
935
|
+
status: result.ok ? "pass" : "fail",
|
|
936
|
+
summary: result.ok ? "The existing owner command completed. Fresh diagnostics determine whether configuration and authorization now pass." : `The owner command failed (${result.reason}); partial changes are possible. Raw output and credentials are withheld. Later repairs on this computer were stopped.`
|
|
937
|
+
}
|
|
938
|
+
];
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
// packages/fleet/src/protocol.ts
|
|
942
|
+
var STATUSES = ["pass", "warn", "fail", "skip"];
|
|
943
|
+
function validCheck(value) {
|
|
944
|
+
if (!record(value) || typeof value["id"] !== "string" || !/^[a-z0-9:._-]{1,100}$/.test(value["id"]) || !STATUSES.includes(String(value["status"])) || typeof value["summary"] !== "string" || value["summary"].length > 2000)
|
|
945
|
+
return false;
|
|
946
|
+
const remedy = value["remedy"];
|
|
947
|
+
return remedy === undefined || validRemedy(remedy);
|
|
948
|
+
}
|
|
949
|
+
function validRemedy(value) {
|
|
950
|
+
return record(value) && typeof value["action"] === "string" && value["action"].length < 2000 && (value["command"] === undefined || typeof value["command"] === "string");
|
|
951
|
+
}
|
|
952
|
+
function validSnapshot(value) {
|
|
953
|
+
if (!record(value) || value["version"] !== 1 || typeof value["platform"] !== "string" || !Array.isArray(value["checks"]) || value["checks"].length > 200 || !value["checks"].every(validCheck))
|
|
954
|
+
return false;
|
|
955
|
+
return validFingerprint(value["fingerprint"]);
|
|
956
|
+
}
|
|
957
|
+
function validFingerprint(value) {
|
|
958
|
+
if (!record(value) || !record(value["files"]))
|
|
959
|
+
return false;
|
|
960
|
+
if (!Object.keys(value["files"]).every((key) => FINGERPRINT_FILES.includes(key)))
|
|
961
|
+
return false;
|
|
962
|
+
const head = value["head"];
|
|
963
|
+
return (head === null || typeof head === "string" && /^[a-f0-9]{40,64}$/.test(head)) && Object.values(value["files"]).every(validHash);
|
|
964
|
+
}
|
|
965
|
+
function validHash(value) {
|
|
966
|
+
return value === null || typeof value === "string" && /^[a-f0-9]{64}$/.test(value);
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
// packages/fleet/src/remote-entry.ts
|
|
970
|
+
async function main() {
|
|
971
|
+
const raw = JSON.parse(Buffer.from(process.argv.at(-1) ?? "", "base64").toString());
|
|
972
|
+
if (!validRequest(raw))
|
|
973
|
+
throw new Error("Invalid request.");
|
|
974
|
+
const node = parseConfig({ version: 1, nodes: [raw["node"]] }).nodes[0];
|
|
975
|
+
if (raw["action"] === "configure") {
|
|
976
|
+
await configureRequest(raw, node);
|
|
977
|
+
} else if (raw["action"] === "repair") {
|
|
978
|
+
if (raw["apply"] !== true || !validRepairs(raw["repairs"]) || !validSnapshot({
|
|
979
|
+
version: 1,
|
|
980
|
+
platform: "request",
|
|
981
|
+
checks: [],
|
|
982
|
+
fingerprint: raw["expected"]
|
|
983
|
+
}))
|
|
984
|
+
throw new Error("Repair requires explicit authorization and supported actions.");
|
|
985
|
+
const checks = await repairWorkspace(node.workspace, raw["repairs"], runCommand, raw["expected"]);
|
|
986
|
+
process.stdout.write(JSON.stringify({ version: 1, checks }));
|
|
987
|
+
} else if (raw["action"] === "sync") {
|
|
988
|
+
if (raw["apply"] !== true)
|
|
989
|
+
throw new Error("Sync requires explicit authorization.");
|
|
990
|
+
const checks = await syncWorkspace(node.workspace, raw["pull"] === true, runCommand);
|
|
991
|
+
process.stdout.write(JSON.stringify({ version: 1, checks }));
|
|
992
|
+
} else
|
|
993
|
+
process.stdout.write(JSON.stringify(await probe(node.workspace, node.required, runCommand)));
|
|
994
|
+
}
|
|
995
|
+
await main().catch(() => {
|
|
996
|
+
process.stdout.write(JSON.stringify({
|
|
997
|
+
version: 1,
|
|
998
|
+
error: "Remote operation failed; inspect node locally."
|
|
999
|
+
}));
|
|
1000
|
+
process.exitCode = 1;
|
|
1001
|
+
});
|
|
1002
|
+
async function configureRequest(raw, node) {
|
|
1003
|
+
if (raw["apply"] !== true || !validConfiguration(raw["configuration"]) || raw["configuration"].node !== node.id)
|
|
1004
|
+
throw new Error("Invalid configuration request.");
|
|
1005
|
+
const checks = await configureWorkspace(node.workspace, raw["configuration"], runCommand);
|
|
1006
|
+
process.stdout.write(JSON.stringify({ version: 1, checks }));
|
|
1007
|
+
}
|
|
1008
|
+
function validRequest(raw) {
|
|
1009
|
+
return record(raw) && ["probe", "sync", "repair", "configure"].includes(String(raw["action"]));
|
|
1010
|
+
}
|