@crvouga/dev-fleet 0.0.0-stage → 0.1.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/.mise/locks/aws-cdk/2.1146.1/aube-lock.yaml +24 -0
- package/.mise/locks/aws-cdk/2.1146.1/package.json +7 -0
- package/.mise/locks/azure-cli/2.91.0~a837e014/pyproject.toml +5 -0
- package/.mise/locks/azure-cli/2.91.0~a837e014/uv.lock +2794 -0
- package/.mise/locks/gemini/0.66.0~84aac8e6/aube-lock.yaml +123 -0
- package/.mise/locks/gemini/0.66.0~84aac8e6/package.json +12 -0
- package/.mise/locks/npm-devcontainers-cli/0.90.0/aube-lock.yaml +24 -0
- package/.mise/locks/npm-devcontainers-cli/0.90.0/package.json +7 -0
- package/.mise/locks/npm-modelcontextprotocol-inspector/2.13.0~4c109bea/aube-lock.yaml +1432 -0
- package/.mise/locks/npm-modelcontextprotocol-inspector/2.13.0~4c109bea/package.json +7 -0
- package/.mise/locks/npm-vercel/65.8.0/aube-lock.yaml +3595 -0
- package/.mise/locks/npm-vercel/65.8.0/package.json +7 -0
- package/.mise/locks/npm-wrangler/4.156.0/aube-lock.yaml +895 -0
- package/.mise/locks/npm-wrangler/4.156.0/package.json +7 -0
- package/.mise/locks/npm-yarnpkg-cli-dist/4.18.1/aube-lock.yaml +24 -0
- package/.mise/locks/npm-yarnpkg-cli-dist/4.18.1/package.json +7 -0
- package/LICENSE +21 -0
- package/README.md +157 -2
- package/agents/antigravity-proxy.ts +3 -0
- package/agents/audit.md +89 -0
- package/agents/claude-proxy +5 -0
- package/agents/claude-proxy.ts +3 -0
- package/agents/codex-proxy.ts +3 -0
- package/agents/grok-proxy.ts +3 -0
- package/agents/managed-proxy.ts +55 -0
- package/agents/opencode-proxy.ts +3 -0
- package/agents/registry.json +209 -0
- package/agents/registry.schema.json +149 -0
- package/agents/setup.md +128 -0
- package/agents/t3.json +40 -0
- package/ansible/macos/manager.yml +30 -0
- package/ansible/macos/policy.json +17 -0
- package/ansible/monitoring/README.md +154 -0
- package/ansible/monitoring/agent-darwin.yml +198 -0
- package/ansible/monitoring/agent.yml +134 -0
- package/ansible/monitoring/beszel-agent.service +42 -0
- package/ansible/monitoring/compose.yml +74 -0
- package/ansible/monitoring/hub-darwin.yml +102 -0
- package/ansible/monitoring/lock.json +11 -0
- package/ansible/net/remote-probe.sh +13 -0
- package/ansible/ubuntu/authorize-sudo.sh +51 -0
- package/ansible/ubuntu/baseline.yml +89 -0
- package/ansible/ubuntu/disk-guard.py +104 -0
- package/ansible/ubuntu/policy.json +43 -0
- package/ansible/ubuntu/recovery.yml +15 -0
- package/ansible/ubuntu/t3/policy.json +7 -0
- package/ansible/ubuntu/t3-desktop.yml +220 -0
- package/ansible/ubuntu/t3-health.sh +33 -0
- package/ansible/ubuntu/t3-policy.yml +76 -0
- package/ansible/ubuntu/t3-route.yml +28 -0
- package/ansible/ubuntu/tailscale.yml +75 -0
- package/ansible/ubuntu/tasks/credentials.yml +47 -0
- package/ansible/ubuntu/tasks/docker.yml +31 -0
- package/ansible/ubuntu/tasks/packages.yml +56 -0
- package/ansible/ubuntu/tasks/power.yml +51 -0
- package/ansible/ubuntu/tasks/recovery.yml +39 -0
- package/ansible/ubuntu/tasks/resources.yml +252 -0
- package/ansible/ubuntu/tasks/services.yml +31 -0
- package/ansible/ubuntu/tasks/ssh.yml +71 -0
- package/ansible/ubuntu/tasks/user.yml +120 -0
- package/bin/devfleet.mjs +36 -0
- package/bootstrap/bootstrap.ps1 +50 -0
- package/bootstrap/bootstrap.sh +73 -0
- package/bootstrap/devfleet +7 -0
- package/bootstrap/docker-desktop.sh +33 -0
- package/bootstrap/doctor.ts +189 -0
- package/bootstrap/fleet-manager-run.sh +76 -0
- package/bootstrap/fleet-remote-bootstrap.sh +185 -0
- package/bootstrap/global-config.ts +113 -0
- package/bootstrap/install-launcher.sh +7 -0
- package/bootstrap/native-aws.ts +86 -0
- package/bootstrap/native-t3.ts +154 -0
- package/bootstrap/npm.ts +350 -0
- package/bootstrap/setup-status.ts +51 -0
- package/bootstrap/setup.ts +88 -0
- package/bootstrap/shell-path.ts +105 -0
- package/bootstrap/verify-t3-archive.py +49 -0
- package/bootstrap/worker-auth.ts +65 -0
- package/bootstrap/worker-github.ts +47 -0
- package/bootstrap/worker-tools.ts +24 -0
- package/cliproxy/AGENTS.md +11 -0
- package/cliproxy/README.md +269 -0
- package/cliproxy/clients/bootstrap.py +143 -0
- package/cliproxy/clients/launcher.py +57 -0
- package/cliproxy/compose.yaml +90 -0
- package/cliproxy/config.template.yaml +70 -0
- package/cliproxy/docs/cloud-adoption.md +16 -0
- package/cliproxy/docs/deployment.png +0 -0
- package/cliproxy/docs/deployment.svg +109 -0
- package/cliproxy/docs/health-workflow.example.yml +185 -0
- package/cliproxy/docs/migration.md +131 -0
- package/cliproxy/docs/model-health-report.schema.json +364 -0
- package/cliproxy/docs/model-health.md +393 -0
- package/cliproxy/image.lock.json +5 -0
- package/cliproxy/launchd/dev.devfleet.cliproxy-backup-check.plist +38 -0
- package/cliproxy/launchd/dev.devfleet.cliproxy-backup.plist +48 -0
- package/cliproxy/launchd/dev.devfleet.cliproxy-fleet.plist +32 -0
- package/cliproxy/launchd/dev.devfleet.cliproxy-gc.plist +38 -0
- package/cliproxy/launchd/dev.devfleet.cliproxy-monitor.plist +29 -0
- package/cliproxy/launchd/dev.devfleet.cliproxy-vault-renew.plist +31 -0
- package/cliproxy/models.json +1618 -0
- package/cliproxy/requirements.lock +78 -0
- package/cliproxy/scripts/accounts.py +38 -0
- package/cliproxy/scripts/audit.py +123 -0
- package/cliproxy/scripts/backup +234 -0
- package/cliproxy/scripts/check.py +34 -0
- package/cliproxy/scripts/checkpoint.py +226 -0
- package/cliproxy/scripts/cli.py +64 -0
- package/cliproxy/scripts/cliproxy_host.py +257 -0
- package/cliproxy/scripts/cloud_state.py +150 -0
- package/cliproxy/scripts/container-health +6 -0
- package/cliproxy/scripts/docker +24 -0
- package/cliproxy/scripts/external-health.py +79 -0
- package/cliproxy/scripts/fleet_config.py +43 -0
- package/cliproxy/scripts/fleet_sync.py +274 -0
- package/cliproxy/scripts/initialize +84 -0
- package/cliproxy/scripts/install-dependencies +48 -0
- package/cliproxy/scripts/install.py +406 -0
- package/cliproxy/scripts/model_health.py +16 -0
- package/cliproxy/scripts/modelhealth/__init__.py +5 -0
- package/cliproxy/scripts/modelhealth/checks.py +429 -0
- package/cliproxy/scripts/modelhealth/cli.py +117 -0
- package/cliproxy/scripts/modelhealth/config.py +186 -0
- package/cliproxy/scripts/modelhealth/emulator.py +536 -0
- package/cliproxy/scripts/modelhealth/expect.py +88 -0
- package/cliproxy/scripts/modelhealth/findings.py +183 -0
- package/cliproxy/scripts/modelhealth/protocol.py +330 -0
- package/cliproxy/scripts/modelhealth/redact.py +28 -0
- package/cliproxy/scripts/modelhealth/report.py +150 -0
- package/cliproxy/scripts/modelhealth/runner.py +390 -0
- package/cliproxy/scripts/modelhealth/scenario.py +131 -0
- package/cliproxy/scripts/modelhealth/schema.py +63 -0
- package/cliproxy/scripts/modelhealth/simulate.py +111 -0
- package/cliproxy/scripts/modelhealth/transport.py +213 -0
- package/cliproxy/scripts/monitor +68 -0
- package/cliproxy/scripts/ops +527 -0
- package/cliproxy/scripts/public-endpoint +127 -0
- package/cliproxy/scripts/recover_node.py +177 -0
- package/cliproxy/scripts/restore-verify +60 -0
- package/cliproxy/scripts/retention.py +144 -0
- package/cliproxy/scripts/service-start +7 -0
- package/cliproxy/scripts/validate-config +56 -0
- package/cliproxy/scripts/vault-access.py +57 -0
- package/cliproxy/scripts/vault-backup-login +55 -0
- package/cliproxy/scripts/vault-recovery +56 -0
- package/cliproxy/scripts/vault-renew +30 -0
- package/cliproxy/systemd/cliproxy-backup-check.service +10 -0
- package/cliproxy/systemd/cliproxy-backup-check.timer +8 -0
- package/cliproxy/systemd/cliproxy-backup.service +13 -0
- package/cliproxy/systemd/cliproxy-backup.timer +8 -0
- package/cliproxy/systemd/cliproxy-fleet.service +17 -0
- package/cliproxy/systemd/cliproxy-gc.service +13 -0
- package/cliproxy/systemd/cliproxy-gc.timer +8 -0
- package/cliproxy/systemd/cliproxy-monitor.service +8 -0
- package/cliproxy/systemd/cliproxy-monitor.timer +7 -0
- package/cliproxy/systemd/cliproxy-vault-renew.service +8 -0
- package/cliproxy/systemd/cliproxy-vault-renew.timer +7 -0
- package/cliproxy/vendor/LICENSE +22 -0
- package/cliproxy/vendor/config.upstream.yaml +1349 -0
- package/examples/fleet.json +95 -0
- package/examples/roster.json +13 -0
- package/mise.lock +1492 -0
- package/mise.toml +99 -0
- package/package.json +65 -4
- package/schema/fleet.schema.json +364 -0
- package/schema/roster.schema.json +77 -0
- package/src/agents/account-relay.ts +111 -0
- package/src/agents/accounts.ts +85 -0
- package/src/agents/audit.ts +77 -0
- package/src/agents/cli.ts +13 -0
- package/src/agents/command.ts +204 -0
- package/src/agents/converge.ts +123 -0
- package/src/agents/doctor.ts +107 -0
- package/src/agents/errors.ts +2 -0
- package/src/agents/files.ts +73 -0
- package/src/agents/fleet-verify.ts +156 -0
- package/src/agents/launch.ts +290 -0
- package/src/agents/native-verify.ts +226 -0
- package/src/agents/provider-errors.ts +50 -0
- package/src/agents/registry.ts +55 -0
- package/src/agents/render.ts +110 -0
- package/src/agents/round-trip.ts +192 -0
- package/src/agents/secrets.ts +13 -0
- package/src/agents/toolchain.ts +34 -0
- package/src/agents/verify-cache.ts +103 -0
- package/src/agents/verify.ts +127 -0
- package/src/auth-checks.ts +163 -0
- package/src/cli/agent-guide.ts +95 -0
- package/src/cli/app.ts +115 -0
- package/src/cli/cli.ts +21 -0
- package/src/cli/command-index.ts +50 -0
- package/src/cli/contracts.ts +25 -0
- package/src/cli/fleet-reference.ts +17 -0
- package/src/cli/fleet-sync-checkout.ts +91 -0
- package/src/cli/fleet-sync-enrollment.ts +144 -0
- package/src/cli/fleet-sync-forward.ts +134 -0
- package/src/cli/fleet-sync-manager.ts +193 -0
- package/src/cli/fleet-sync-monitoring.ts +185 -0
- package/src/cli/fleet-sync-output.ts +314 -0
- package/src/cli/fleet-sync-privilege.ts +27 -0
- package/src/cli/fleet-sync-progress.ts +109 -0
- package/src/cli/fleet-sync-remote.ts +236 -0
- package/src/cli/fleet-sync-report.ts +26 -0
- package/src/cli/fleet-sync-schedule.ts +138 -0
- package/src/cli/fleet-sync-vault.ts +166 -0
- package/src/cli/fleet-sync.ts +309 -0
- package/src/cli/fleet.ts +163 -0
- package/src/cli/managed-health.ts +219 -0
- package/src/cli/node-deployment.ts +34 -0
- package/src/cli/node-sync.ts +114 -0
- package/src/cli/node-t3.ts +31 -0
- package/src/cli/options.ts +141 -0
- package/src/cli/probe-source.ts +7 -0
- package/src/cli/program.ts +169 -0
- package/src/cli/register-fleet.ts +246 -0
- package/src/cli/registry.ts +158 -0
- package/src/cli/repair-apply.ts +67 -0
- package/src/cli/repair-guard.ts +11 -0
- package/src/cli/repair-inspect.ts +150 -0
- package/src/cli/repair-script.ts +76 -0
- package/src/cli/repair.ts +209 -0
- package/src/cli/roster.ts +89 -0
- package/src/cli/search.ts +35 -0
- package/src/cli/settings.ts +64 -0
- package/src/cli/source-operations.ts +78 -0
- package/src/cli/t3/command.ts +90 -0
- package/src/cli/t3/register.ts +55 -0
- package/src/cli/tailnet.ts +127 -0
- package/src/cli/ui-actions.ts +133 -0
- package/src/cli/ui-controller.ts +113 -0
- package/src/cli/ui-data.ts +106 -0
- package/src/cli/ui-execute.ts +163 -0
- package/src/cli/ui-repairs.ts +34 -0
- package/src/cli/ui-reports.ts +211 -0
- package/src/cli/ui-targets.ts +100 -0
- package/src/cli/ui-terminal.ts +109 -0
- package/src/cli/ui-types.ts +49 -0
- package/src/cli/ui-wizard.ts +101 -0
- package/src/cli/ui.ts +9 -0
- package/src/cli/worker-health-output.ts +37 -0
- package/src/config.ts +186 -0
- package/src/configure.ts +255 -0
- package/src/definition.ts +260 -0
- package/src/diagnostics.ts +92 -0
- package/src/doctor-check.ts +10 -0
- package/src/errors.ts +1 -0
- package/src/fingerprint.ts +120 -0
- package/src/fleet-config.ts +88 -0
- package/src/index.ts +6 -0
- package/src/input-hashes.ts +73 -0
- package/src/machine/local-source.ts +52 -0
- package/src/machine/lock-error.ts +2 -0
- package/src/machine/platform.ts +44 -0
- package/src/machine/source.ts +68 -0
- package/src/machine/state.ts +185 -0
- package/src/machine-health.ts +95 -0
- package/src/managed-source.ts +47 -0
- package/src/manager-failure.ts +48 -0
- package/src/manager-sync.ts +400 -0
- package/src/manager.ts +62 -0
- package/src/monitoring/apply.ts +171 -0
- package/src/monitoring/beszel.ts +191 -0
- package/src/monitoring/enroll.ts +110 -0
- package/src/monitoring/hub-access.ts +113 -0
- package/src/monitoring/hub-viewer.ts +142 -0
- package/src/monitoring/hub.ts +93 -0
- package/src/monitoring/manager-agent.ts +73 -0
- package/src/monitoring/manager-hub.ts +232 -0
- package/src/monitoring/remote.ts +65 -0
- package/src/network-definition.ts +83 -0
- package/src/node-converge-linux.ts +372 -0
- package/src/node-converge-shared.ts +83 -0
- package/src/node-converge.ts +40 -0
- package/src/onboard.ts +35 -0
- package/src/platform/ansible-receipt.ts +128 -0
- package/src/platform/ansible.ts +62 -0
- package/src/platform/darwin.ts +203 -0
- package/src/platform/detect.ts +11 -0
- package/src/platform/fallback.ts +110 -0
- package/src/platform/index.ts +38 -0
- package/src/platform/launchd.ts +98 -0
- package/src/platform/linux.ts +139 -0
- package/src/platform/ps.ts +45 -0
- package/src/platform/resource-verify.ts +128 -0
- package/src/platform/systemd-user.ts +95 -0
- package/src/platform/t3-policy.ts +137 -0
- package/src/platform/t3-route.ts +68 -0
- package/src/platform/t3-service.ts +75 -0
- package/src/platform/types.ts +123 -0
- package/src/platform/ubuntu-audit.ts +63 -0
- package/src/platform/ubuntu-verify.ts +105 -0
- package/src/platform/windows.ts +148 -0
- package/src/power-recovery.ts +55 -0
- package/src/probe.ts +51 -0
- package/src/protocol.ts +119 -0
- package/src/proxy-converge.ts +85 -0
- package/src/remote-bootstrap.ts +212 -0
- package/src/remote-entry.ts +89 -0
- package/src/remote-lock.ts +12 -0
- package/src/repair-catalog.ts +206 -0
- package/src/repair-plan.ts +78 -0
- package/src/repair-runner.ts +84 -0
- package/src/report.ts +71 -0
- package/src/runner.ts +104 -0
- package/src/secrets.ts +169 -0
- package/src/service-auth.ts +115 -0
- package/src/ssh.ts +135 -0
- package/src/sync.ts +104 -0
- package/src/t3/accounts.ts +81 -0
- package/src/t3/desired.ts +165 -0
- package/src/t3/files.ts +81 -0
- package/src/t3/names-check.ts +53 -0
- package/src/t3/names.ts +155 -0
- package/src/t3/plan.ts +82 -0
- package/src/t3/providers.ts +74 -0
- package/src/t3/reconcile.ts +123 -0
- package/src/t3-endpoint.ts +48 -0
- package/src/t3-linked-check.ts +101 -0
- package/src/tailnet-health.ts +231 -0
- package/src/tailnet-matching.ts +37 -0
- package/src/tailscale.ts +141 -0
- package/src/test-fixtures.ts +36 -0
- package/src/toolchain-checks.ts +83 -0
- package/src/topology.ts +24 -0
- package/src/transport.ts +122 -0
- package/src/types.ts +95 -0
- package/src/vault-config.ts +38 -0
- package/src/worker-access-checks.ts +115 -0
- package/src/worker-capacity.ts +34 -0
- package/src/worker-host-checks.ts +176 -0
- package/src/worker-policy.ts +64 -0
- package/src/worker-readiness.ts +64 -0
- package/src/workstation-checks.ts +29 -0
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { mkdir, mkdtemp, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { tmpdir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import type { CommandResult, Runner, FleetNode } from './types';
|
|
5
|
+
|
|
6
|
+
export const HEAD = 'a'.repeat(40);
|
|
7
|
+
export const NODE: FleetNode = {
|
|
8
|
+
id: 'dev-one',
|
|
9
|
+
ssh: 'dev-one',
|
|
10
|
+
workspace: '/home/dev/workspace',
|
|
11
|
+
required: [],
|
|
12
|
+
};
|
|
13
|
+
export const success = (stdout = ''): CommandResult => ({
|
|
14
|
+
ok: true,
|
|
15
|
+
stdout,
|
|
16
|
+
stderr: '',
|
|
17
|
+
reason: 'exit',
|
|
18
|
+
});
|
|
19
|
+
export const failure = (stderr = ''): CommandResult => ({
|
|
20
|
+
ok: false,
|
|
21
|
+
stdout: '',
|
|
22
|
+
stderr,
|
|
23
|
+
reason: 'exit',
|
|
24
|
+
});
|
|
25
|
+
export const gitRunner: Runner = async (command) =>
|
|
26
|
+
success(command.includes('rev-parse') ? HEAD : '');
|
|
27
|
+
|
|
28
|
+
export async function workspace(): Promise<string> {
|
|
29
|
+
const root = await mkdtemp(join(tmpdir(), "fleet test's "));
|
|
30
|
+
for (const file of ['package.json', 'bun.lock', 'mise.toml', 'mise.lock'])
|
|
31
|
+
await writeFile(join(root, file), '{}');
|
|
32
|
+
await mkdir(join(root, 'packages/example'), { recursive: true });
|
|
33
|
+
await writeFile(join(root, 'packages/example/package.json'), '{}');
|
|
34
|
+
await mkdir(join(root, '.mise/locks'), { recursive: true });
|
|
35
|
+
return root;
|
|
36
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import { record } from './config';
|
|
3
|
+
import type { Check, Runner } from './types';
|
|
4
|
+
|
|
5
|
+
export async function toolchainChecks(
|
|
6
|
+
root: string,
|
|
7
|
+
run: Runner,
|
|
8
|
+
worker?: boolean
|
|
9
|
+
): Promise<Check[]> {
|
|
10
|
+
const result = await run(doctorArguments(root, worker), {
|
|
11
|
+
cwd: root,
|
|
12
|
+
timeoutMs: 90_000,
|
|
13
|
+
});
|
|
14
|
+
const fallback: Check = {
|
|
15
|
+
id: 'auth:toolchain',
|
|
16
|
+
status: 'fail',
|
|
17
|
+
summary: 'Pinned tool/dependency and Docker checks could not be verified.',
|
|
18
|
+
remedy: {
|
|
19
|
+
action:
|
|
20
|
+
'Run the checkout bootstrap/doctor.ts --json --docker on this computer. Reconcile versions only through the shared mise setup.',
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
try {
|
|
24
|
+
const data: unknown = JSON.parse(result.stdout);
|
|
25
|
+
if (
|
|
26
|
+
!record(data) ||
|
|
27
|
+
typeof data['ok'] !== 'boolean' ||
|
|
28
|
+
!Array.isArray(data['checks']) ||
|
|
29
|
+
!data['checks'].length ||
|
|
30
|
+
data['checks'].length > 150
|
|
31
|
+
)
|
|
32
|
+
return [fallback];
|
|
33
|
+
const checks = data['checks'].map(toolCheck);
|
|
34
|
+
if (checks.some((check) => check === null)) return [fallback];
|
|
35
|
+
return [
|
|
36
|
+
...(!result.ok || !data['ok'] ? [fallback] : []),
|
|
37
|
+
...(checks as Check[]),
|
|
38
|
+
];
|
|
39
|
+
} catch {
|
|
40
|
+
return [fallback];
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function toolCheck(value: unknown): Check | null {
|
|
45
|
+
if (
|
|
46
|
+
!record(value) ||
|
|
47
|
+
typeof value['id'] !== 'string' ||
|
|
48
|
+
!/^[a-zA-Z0-9@:/._-]{1,75}$/.test(value['id']) ||
|
|
49
|
+
!['pass', 'fail', 'warn', 'skip'].includes(String(value['status']))
|
|
50
|
+
)
|
|
51
|
+
return null;
|
|
52
|
+
const ok = value['status'] === 'pass';
|
|
53
|
+
const version =
|
|
54
|
+
typeof value['detail'] === 'string' &&
|
|
55
|
+
/^v?\d{1,6}(?:\.\d{1,6}){1,3}$/.test(value['detail'])
|
|
56
|
+
? ` ${value['detail']}`
|
|
57
|
+
: '';
|
|
58
|
+
return {
|
|
59
|
+
id: `tool:${value['id'].toLowerCase().replace(/[^a-z0-9:._-]/g, '_')}`,
|
|
60
|
+
status: value['status'] as Check['status'],
|
|
61
|
+
summary: ok
|
|
62
|
+
? `Expected tool version or runtime check succeeded${version}.`
|
|
63
|
+
: 'Expected tool version, runtime or authentication check did not pass.',
|
|
64
|
+
...(!ok
|
|
65
|
+
? {
|
|
66
|
+
remedy: {
|
|
67
|
+
action:
|
|
68
|
+
'Review bootstrap/doctor.ts --json --docker locally. Use shared mise/workstation setup to reconcile this computer.',
|
|
69
|
+
},
|
|
70
|
+
}
|
|
71
|
+
: {}),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function doctorArguments(root: string, worker?: boolean): string[] {
|
|
76
|
+
return [
|
|
77
|
+
'bun',
|
|
78
|
+
join(root, 'bootstrap/doctor.ts'),
|
|
79
|
+
'--json',
|
|
80
|
+
'--docker',
|
|
81
|
+
...(worker ? ['--worker'] : []),
|
|
82
|
+
];
|
|
83
|
+
}
|
package/src/topology.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { FleetDefinition } from './definition';
|
|
2
|
+
|
|
3
|
+
/** Fleet topology: one singleton manager (the human's machine) and stateless
|
|
4
|
+
* Linux workers. Every singleton service runs on the manager; workers only
|
|
5
|
+
* consume them and can be turned off, replaced or re-imaged at will. */
|
|
6
|
+
export type Role = 'manager' | 'worker';
|
|
7
|
+
|
|
8
|
+
export function roleOf(definition: FleetDefinition, id: string): Role | null {
|
|
9
|
+
if (definition.manager.id === id) return 'manager';
|
|
10
|
+
return definition.workers.some((entry) => entry.id === id) ? 'worker' : null;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Private Beszel hub URL on the manager; agents never use the public name. */
|
|
14
|
+
export function hubUrl(definition: FleetDefinition): string {
|
|
15
|
+
return `https://${definition.manager.network.tailscaleName}:${definition.services.monitoring.hubPort}`;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Normalizes a macOS LocalHostName / hostname to a fleet ID for identity checks. */
|
|
19
|
+
export function hostId(hostname: string): string {
|
|
20
|
+
return hostname
|
|
21
|
+
.toLowerCase()
|
|
22
|
+
.replace(/\.local$/, '')
|
|
23
|
+
.replace(/[^a-z0-9-]/g, '-');
|
|
24
|
+
}
|
package/src/transport.ts
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { managedProbeTarget } from './managed-source';
|
|
2
|
+
import type { ConfigurationRequest } from './configure';
|
|
3
|
+
import { remoteCommand, sshFailure, sshArgs } from './ssh';
|
|
4
|
+
import { cleanCheck, decodeSnapshot, validCheck } from './protocol';
|
|
5
|
+
import { record } from './config';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
import type { FleetNode, NodeReport, Runner, Fingerprint } from './types';
|
|
8
|
+
import type { AutomatedRepair } from './repair-catalog';
|
|
9
|
+
|
|
10
|
+
export async function bundleProbe(): Promise<string> {
|
|
11
|
+
const result = await Bun.build({
|
|
12
|
+
entrypoints: [fileURLToPath(new URL('./remote-entry.ts', import.meta.url))],
|
|
13
|
+
target: 'node',
|
|
14
|
+
minify: true,
|
|
15
|
+
});
|
|
16
|
+
if (!result.success || !result.outputs[0])
|
|
17
|
+
throw new Error('Cannot bundle the fleet probe.');
|
|
18
|
+
return result.outputs[0].text();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface RemoteOptions {
|
|
22
|
+
source: string;
|
|
23
|
+
timeoutMs: number;
|
|
24
|
+
action: 'probe' | 'sync' | 'repair' | 'configure';
|
|
25
|
+
configuration?: ConfigurationRequest;
|
|
26
|
+
repairs?: AutomatedRepair[];
|
|
27
|
+
expected?: Fingerprint;
|
|
28
|
+
pull?: boolean;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function inspectRemote(
|
|
32
|
+
node: FleetNode,
|
|
33
|
+
options: RemoteOptions,
|
|
34
|
+
run: Runner
|
|
35
|
+
): Promise<NodeReport> {
|
|
36
|
+
if (node.required.includes('worker')) {
|
|
37
|
+
const preflight = await run([...sshArgs(node), 'true'], {
|
|
38
|
+
timeoutMs: 15_000,
|
|
39
|
+
});
|
|
40
|
+
if (!preflight.ok)
|
|
41
|
+
return { node: node.id, checks: [sshFailure(preflight)] };
|
|
42
|
+
}
|
|
43
|
+
const target =
|
|
44
|
+
options.action === 'probe' && options.expected
|
|
45
|
+
? await managedProbeTarget(node, options.expected.head, run)
|
|
46
|
+
: { node, checks: [] };
|
|
47
|
+
const command = remoteCommand(target.node, {
|
|
48
|
+
action: options.action,
|
|
49
|
+
configuration: options.configuration,
|
|
50
|
+
node: target.node,
|
|
51
|
+
apply: options.action !== 'probe',
|
|
52
|
+
repairs: options.repairs,
|
|
53
|
+
expected: options.expected,
|
|
54
|
+
pull: options.pull === true,
|
|
55
|
+
});
|
|
56
|
+
const result = await run(command, {
|
|
57
|
+
input: options.source,
|
|
58
|
+
timeoutMs: options.timeoutMs,
|
|
59
|
+
});
|
|
60
|
+
if (!result.ok)
|
|
61
|
+
return { node: node.id, checks: [...target.checks, sshFailure(result)] };
|
|
62
|
+
if (options.action !== 'probe') return decodeSync(node.id, result.stdout);
|
|
63
|
+
const snapshot = decodeSnapshot(result.stdout);
|
|
64
|
+
if (!snapshot) return protocolFailure(node.id);
|
|
65
|
+
return {
|
|
66
|
+
node: node.id,
|
|
67
|
+
snapshot,
|
|
68
|
+
checks: [
|
|
69
|
+
...target.checks,
|
|
70
|
+
{
|
|
71
|
+
id: 'ssh:session',
|
|
72
|
+
status: 'pass',
|
|
73
|
+
summary:
|
|
74
|
+
'Fresh noninteractive SSH session and Node-compatible probe succeeded with verified host keys and no agent forwarding.',
|
|
75
|
+
},
|
|
76
|
+
...snapshot.checks,
|
|
77
|
+
{
|
|
78
|
+
id: 'vscode:forwarding',
|
|
79
|
+
status: 'skip',
|
|
80
|
+
required: node.required.includes('vscode'),
|
|
81
|
+
summary:
|
|
82
|
+
'VS Code Remote-SSH tunnel/server installation is not tested by a shell session. Use the onboarding forwarding test.',
|
|
83
|
+
},
|
|
84
|
+
],
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function decodeSync(node: string, output: string): NodeReport {
|
|
89
|
+
try {
|
|
90
|
+
const data: unknown = JSON.parse(output);
|
|
91
|
+
if (
|
|
92
|
+
record(data) &&
|
|
93
|
+
data['version'] === 1 &&
|
|
94
|
+
Array.isArray(data['checks']) &&
|
|
95
|
+
data['checks'].length > 0 &&
|
|
96
|
+
data['checks'].length <= 200 &&
|
|
97
|
+
data['checks'].every(validCheck)
|
|
98
|
+
) {
|
|
99
|
+
return { node, checks: data['checks'].map(cleanCheck) };
|
|
100
|
+
}
|
|
101
|
+
} catch {
|
|
102
|
+
return protocolFailure(node);
|
|
103
|
+
}
|
|
104
|
+
return protocolFailure(node);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function protocolFailure(node: string): NodeReport {
|
|
108
|
+
return {
|
|
109
|
+
node,
|
|
110
|
+
checks: [
|
|
111
|
+
{
|
|
112
|
+
id: 'ssh:protocol',
|
|
113
|
+
status: 'fail',
|
|
114
|
+
summary: 'Remote response is not a supported fleet JSON document.',
|
|
115
|
+
remedy: {
|
|
116
|
+
action:
|
|
117
|
+
'Remove stdout banners from noninteractive shell startup; check the Node/Bun runtime. No raw remote output is retained.',
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
],
|
|
121
|
+
};
|
|
122
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
export type Status = 'pass' | 'warn' | 'fail' | 'skip';
|
|
2
|
+
export type Capability =
|
|
3
|
+
| 'github'
|
|
4
|
+
| 'vault'
|
|
5
|
+
| 't3'
|
|
6
|
+
| 'vscode'
|
|
7
|
+
| 'toolchain'
|
|
8
|
+
| 'codex'
|
|
9
|
+
| 'claude'
|
|
10
|
+
| 'services'
|
|
11
|
+
| 'workstation'
|
|
12
|
+
| 'worker';
|
|
13
|
+
|
|
14
|
+
export interface FleetNode {
|
|
15
|
+
id: string;
|
|
16
|
+
ssh: string;
|
|
17
|
+
workspace: string;
|
|
18
|
+
tailscaleId?: string;
|
|
19
|
+
t3EnvironmentId?: string;
|
|
20
|
+
bootstrapViaSsh?: boolean;
|
|
21
|
+
runtime?: string;
|
|
22
|
+
required: Capability[];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface FleetConfig {
|
|
26
|
+
version: 1;
|
|
27
|
+
nodes: FleetNode[];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface Check {
|
|
31
|
+
/** Explicitly out-of-scope checks do not affect required readiness. */
|
|
32
|
+
required?: boolean;
|
|
33
|
+
/** Measured enclosing phase duration; omitted when not observed. */
|
|
34
|
+
phaseDurationMs?: number;
|
|
35
|
+
id: string;
|
|
36
|
+
status: Status;
|
|
37
|
+
summary: string;
|
|
38
|
+
remedy?: { action: string; command?: string };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Only a required failure blocks readiness. Warnings (conditions outside the
|
|
42
|
+
* fleet, such as a provider quota) and skips (not observable here) are shown
|
|
43
|
+
* but never fail a run.
|
|
44
|
+
*/
|
|
45
|
+
export function blocking(check: Check): boolean {
|
|
46
|
+
return check.status === 'fail' && check.required !== false;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface Fingerprint {
|
|
50
|
+
head: string | null;
|
|
51
|
+
files: Record<string, string | null>;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface Snapshot {
|
|
55
|
+
version: 1;
|
|
56
|
+
platform: string;
|
|
57
|
+
fingerprint: Fingerprint;
|
|
58
|
+
checks: Check[];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface NodeReport {
|
|
62
|
+
node: string;
|
|
63
|
+
checks: Check[];
|
|
64
|
+
snapshot?: Snapshot;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface Report {
|
|
68
|
+
version: 1;
|
|
69
|
+
command: string;
|
|
70
|
+
ok: boolean;
|
|
71
|
+
generatedAt: string;
|
|
72
|
+
nodes: NodeReport[];
|
|
73
|
+
summary: Record<Status, number>;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface CommandOptions {
|
|
77
|
+
/** Internal remote worker: keep the already-held operation lock on fd 9. */
|
|
78
|
+
preserveOperationLock?: boolean;
|
|
79
|
+
cwd?: string;
|
|
80
|
+
input?: string;
|
|
81
|
+
timeoutMs?: number;
|
|
82
|
+
env?: NodeJS.ProcessEnv;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export interface CommandResult {
|
|
86
|
+
ok: boolean;
|
|
87
|
+
stdout: string;
|
|
88
|
+
stderr: string;
|
|
89
|
+
reason: 'exit' | 'missing' | 'timeout' | 'overflow';
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export type Runner = (
|
|
93
|
+
command: string[],
|
|
94
|
+
options?: CommandOptions
|
|
95
|
+
) => Promise<CommandResult>;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { fleet } from './fleet-config';
|
|
2
|
+
import { FleetUsageError } from './errors';
|
|
3
|
+
|
|
4
|
+
export type VaultConfig = {
|
|
5
|
+
addr: string;
|
|
6
|
+
mount: string;
|
|
7
|
+
project: string;
|
|
8
|
+
config: string;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
/** True when this fleet keeps its secrets in Vault. Only then does each node
|
|
12
|
+
* hold its own Vault session, with the logins, renewal and checks that go
|
|
13
|
+
* with it; the other providers need none of that.
|
|
14
|
+
*/
|
|
15
|
+
export const usesVault = (): boolean => fleet.secrets.provider === 'vault';
|
|
16
|
+
|
|
17
|
+
/** This fleet's Vault and the shared document (`<mount>/<project>/<config>`)
|
|
18
|
+
* that holds the proxy client key, from its `secrets` configuration.
|
|
19
|
+
*/
|
|
20
|
+
export function resolveVaultConfig(): VaultConfig {
|
|
21
|
+
const secrets = fleet.secrets;
|
|
22
|
+
if (secrets.provider !== 'vault')
|
|
23
|
+
throw new FleetUsageError(
|
|
24
|
+
'This fleet does not use the vault secrets provider.'
|
|
25
|
+
);
|
|
26
|
+
const [project = '', ...rest] =
|
|
27
|
+
secrets.locations['proxy-client-key'].path.split('/');
|
|
28
|
+
return {
|
|
29
|
+
addr: process.env['VAULT_ADDR']?.trim() || secrets.address,
|
|
30
|
+
mount: secrets.mount,
|
|
31
|
+
project,
|
|
32
|
+
config: rest.join('/'),
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function kvPath(vault: VaultConfig): string {
|
|
37
|
+
return `${vault.mount}/data/${vault.project}/${vault.config}`;
|
|
38
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import type { Check, Runner } from './types';
|
|
2
|
+
import { fleet, secretVia } from './fleet-config';
|
|
3
|
+
import { registry } from './agents/registry';
|
|
4
|
+
import { resolveVaultConfig, usesVault } from './vault-config';
|
|
5
|
+
import { record } from './config';
|
|
6
|
+
|
|
7
|
+
/** Inject every subprocess; never put credentials in argv, reports, or logs. */
|
|
8
|
+
export async function workerAccessChecks(run: Runner): Promise<Check[]> {
|
|
9
|
+
const policy = fleet.worker!;
|
|
10
|
+
const github = await run(['gh', 'api', 'user', '--jq', '.login']);
|
|
11
|
+
return [
|
|
12
|
+
{
|
|
13
|
+
id: 'worker:github-principal',
|
|
14
|
+
status:
|
|
15
|
+
github.ok && github.stdout.trim() === policy.githubLogin
|
|
16
|
+
? 'pass'
|
|
17
|
+
: 'fail',
|
|
18
|
+
summary:
|
|
19
|
+
'Worker GitHub authentication must match the declared common account.',
|
|
20
|
+
remedy: {
|
|
21
|
+
action:
|
|
22
|
+
'Authenticate this worker with the declared GitHub account. Credentials stay on their original machines.',
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
await proxyAuthentication(run),
|
|
26
|
+
...(usesVault() ? [await vaultRenewalCheck(run)] : []),
|
|
27
|
+
];
|
|
28
|
+
}
|
|
29
|
+
async function proxyAuthentication(run: Runner): Promise<Check> {
|
|
30
|
+
const unavailable: Check = {
|
|
31
|
+
id: 'worker:proxy-authentication',
|
|
32
|
+
status: 'fail',
|
|
33
|
+
summary: 'UNATTENDED_PROXY_AUTHENTICATION_REQUIRED',
|
|
34
|
+
remedy: {
|
|
35
|
+
action:
|
|
36
|
+
'Restore this worker’s own Vault authorization and renewal; no other node’s login or gateway identity is copied.',
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
try {
|
|
40
|
+
const key = (await secretVia(run)('proxy-client-key')) ?? '';
|
|
41
|
+
if (!/^[\x21-\x7e]{16,4096}$/.test(key)) return unavailable;
|
|
42
|
+
const result = await run(
|
|
43
|
+
[
|
|
44
|
+
'curl',
|
|
45
|
+
'--fail',
|
|
46
|
+
'--silent',
|
|
47
|
+
'--show-error',
|
|
48
|
+
'--max-time',
|
|
49
|
+
'10',
|
|
50
|
+
'--header',
|
|
51
|
+
'@-',
|
|
52
|
+
`${registry.proxy.endpoint}/v1/models`,
|
|
53
|
+
],
|
|
54
|
+
{ input: `Authorization: Bearer ${key}\n`, timeoutMs: 15_000 }
|
|
55
|
+
);
|
|
56
|
+
const data: unknown = JSON.parse(result.stdout);
|
|
57
|
+
if (
|
|
58
|
+
!result.ok ||
|
|
59
|
+
!record(data) ||
|
|
60
|
+
!Array.isArray(data['data']) ||
|
|
61
|
+
!data['data'].length
|
|
62
|
+
)
|
|
63
|
+
return unavailable;
|
|
64
|
+
return {
|
|
65
|
+
...unavailable,
|
|
66
|
+
status: 'pass',
|
|
67
|
+
summary:
|
|
68
|
+
'Worker credential authenticates to the declared singleton gateway model catalog.',
|
|
69
|
+
};
|
|
70
|
+
} catch {
|
|
71
|
+
return unavailable;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function vaultRenewalCheck(run: Runner): Promise<Check> {
|
|
76
|
+
const unavailable: Check = {
|
|
77
|
+
id: 'worker:vault-renewal',
|
|
78
|
+
status: 'fail',
|
|
79
|
+
summary: 'UNATTENDED_VAULT_IDENTITY_REQUIRED',
|
|
80
|
+
remedy: {
|
|
81
|
+
action:
|
|
82
|
+
'Enroll this worker’s own periodic token or auto-auth identity. Ordinary expiring user sessions are not evidence of unattended renewal; no credentials are copied between machines.',
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
try {
|
|
86
|
+
const { addr } = resolveVaultConfig();
|
|
87
|
+
const result = await run([
|
|
88
|
+
'vault',
|
|
89
|
+
'token',
|
|
90
|
+
'lookup',
|
|
91
|
+
`-address=${addr}`,
|
|
92
|
+
'-format=json',
|
|
93
|
+
]);
|
|
94
|
+
const data: unknown = JSON.parse(result.stdout);
|
|
95
|
+
if (!result.ok || !record(data) || !record(data['data']))
|
|
96
|
+
return unavailable;
|
|
97
|
+
const token = data['data'];
|
|
98
|
+
if (
|
|
99
|
+
token['renewable'] !== true ||
|
|
100
|
+
typeof token['period'] !== 'number' ||
|
|
101
|
+
token['period'] <= 0 ||
|
|
102
|
+
typeof token['ttl'] !== 'number' ||
|
|
103
|
+
token['ttl'] <= 1200
|
|
104
|
+
)
|
|
105
|
+
return unavailable;
|
|
106
|
+
return {
|
|
107
|
+
...unavailable,
|
|
108
|
+
status: 'pass',
|
|
109
|
+
summary:
|
|
110
|
+
'This worker has a renewable periodic Vault identity with sufficient lifetime for the renewal timer.',
|
|
111
|
+
};
|
|
112
|
+
} catch {
|
|
113
|
+
return unavailable;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import { healthChecks, parseProbe } from './machine-health';
|
|
3
|
+
import type { Check, Runner } from './types';
|
|
4
|
+
|
|
5
|
+
export async function workerCapacity(
|
|
6
|
+
root: string,
|
|
7
|
+
node: string,
|
|
8
|
+
run: Runner
|
|
9
|
+
): Promise<Check[]> {
|
|
10
|
+
const result = await run(['sh', join(root, 'ansible/net/remote-probe.sh')], {
|
|
11
|
+
timeoutMs: 10_000,
|
|
12
|
+
});
|
|
13
|
+
const sample = parseProbe(result.stdout);
|
|
14
|
+
if (
|
|
15
|
+
!result.ok ||
|
|
16
|
+
sample.ncpu === undefined ||
|
|
17
|
+
sample.load === undefined ||
|
|
18
|
+
sample.memFreePct === undefined ||
|
|
19
|
+
sample.diskMb === undefined
|
|
20
|
+
)
|
|
21
|
+
return [
|
|
22
|
+
{
|
|
23
|
+
id: 'worker:capacity',
|
|
24
|
+
status: 'fail',
|
|
25
|
+
summary: 'Required CPU, memory and disk evidence is unavailable.',
|
|
26
|
+
},
|
|
27
|
+
];
|
|
28
|
+
return healthChecks(sample, node).map((check) => ({
|
|
29
|
+
id: `worker:capacity:${check.id}`,
|
|
30
|
+
status: check.severity,
|
|
31
|
+
summary: check.detail,
|
|
32
|
+
...(check.fix ? { remedy: { action: check.fix } } : {}),
|
|
33
|
+
}));
|
|
34
|
+
}
|