@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,92 @@
|
|
|
1
|
+
import type { Check } from './types';
|
|
2
|
+
|
|
3
|
+
export interface Diagnostic {
|
|
4
|
+
node: string;
|
|
5
|
+
phase: string;
|
|
6
|
+
checkId: string;
|
|
7
|
+
status: Check['status'];
|
|
8
|
+
code: string;
|
|
9
|
+
summary: string;
|
|
10
|
+
expected: string;
|
|
11
|
+
observed: string;
|
|
12
|
+
scope: 'manager' | 'node' | 'unverified';
|
|
13
|
+
durationMs: number | null;
|
|
14
|
+
retryable: boolean;
|
|
15
|
+
actionRequired: boolean;
|
|
16
|
+
remedy: { owner: string; action: string };
|
|
17
|
+
}
|
|
18
|
+
const EXPECTED: Record<string, string> = {
|
|
19
|
+
ssh: 'Fresh noninteractive SSH session with the enrolled host key and designated user.',
|
|
20
|
+
tailscale:
|
|
21
|
+
'The enrolled device identity is connected and reachable through the intended tailnet.',
|
|
22
|
+
ubuntu:
|
|
23
|
+
'Observed Ubuntu configuration matches the reviewed baseline without a distribution upgrade.',
|
|
24
|
+
t3: 'Pinned native T3 service and the specifically named runtime acceptance check pass.',
|
|
25
|
+
provider:
|
|
26
|
+
'The declared gateway model passes authenticated inference, completed streaming and a bounded tool round trip.',
|
|
27
|
+
proxy:
|
|
28
|
+
'One declared gateway owner preserves runtime identities and serves authorized clients.',
|
|
29
|
+
beszel:
|
|
30
|
+
'Pinned monitoring artifacts, canonical registration and fresh authenticated metrics are independently verified.',
|
|
31
|
+
rollout: 'Required prerequisites and canary gates pass before promotion.',
|
|
32
|
+
manager:
|
|
33
|
+
'Manager enrollment, approval and exclusive operation ownership are valid.',
|
|
34
|
+
source:
|
|
35
|
+
'The approved full commit and packaged canonical definition match verified source.',
|
|
36
|
+
configuration:
|
|
37
|
+
'The existing Workstation owner applies and verifies the selected source configuration.',
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/** Derived only from already-redacted checks. No raw subprocess errors, journal
|
|
41
|
+
* contents, auth payloads or environment fields enter the diagnostic contract.
|
|
42
|
+
*/
|
|
43
|
+
export function diagnostic(node: string, check: Check): Diagnostic {
|
|
44
|
+
const phase = check.id.split(':')[0]!;
|
|
45
|
+
const failed = check.status !== 'pass';
|
|
46
|
+
const code = /^[A-Z][A-Z0-9_]{2,100}$/.test(check.summary)
|
|
47
|
+
? check.summary
|
|
48
|
+
: `${check.id.replaceAll(/[^a-z0-9]/g, '_').toUpperCase()}_${failed ? 'UNVERIFIED' : 'VERIFIED'}`;
|
|
49
|
+
return {
|
|
50
|
+
node,
|
|
51
|
+
phase,
|
|
52
|
+
checkId: check.id,
|
|
53
|
+
status: check.status,
|
|
54
|
+
code,
|
|
55
|
+
summary: check.summary,
|
|
56
|
+
expected:
|
|
57
|
+
EXPECTED[phase] ??
|
|
58
|
+
'The named required check has independent verification evidence.',
|
|
59
|
+
observed: check.summary,
|
|
60
|
+
scope: scope(check.id),
|
|
61
|
+
durationMs: check.phaseDurationMs ?? null,
|
|
62
|
+
retryable: failed && /OFFLINE|UNREACHABLE|METRICS_STALE|TIMEOUT/.test(code),
|
|
63
|
+
actionRequired: failed,
|
|
64
|
+
remedy: {
|
|
65
|
+
owner: owner(phase),
|
|
66
|
+
action:
|
|
67
|
+
check.remedy?.action ??
|
|
68
|
+
'Inspect this named check in the Fleet runbook; preserve runtime identity and repair persistent configuration in source.',
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
function scope(id: string): Diagnostic['scope'] {
|
|
73
|
+
if (
|
|
74
|
+
/^(t3:manager-endpoint|ssh:|manager:|rollout:|source:|tailscale:(reachability|identity|enrolled-identity))/.test(
|
|
75
|
+
id
|
|
76
|
+
)
|
|
77
|
+
)
|
|
78
|
+
return 'manager';
|
|
79
|
+
if (
|
|
80
|
+
/^(provider:|proxy:|configuration:|ubuntu:baseline|t3:(service|policy)|tailscale:configuration)/.test(
|
|
81
|
+
id
|
|
82
|
+
)
|
|
83
|
+
)
|
|
84
|
+
return 'node';
|
|
85
|
+
if (/^beszel:(version|registration|connection|freshness|api)/.test(id))
|
|
86
|
+
return 'manager';
|
|
87
|
+
return 'unverified';
|
|
88
|
+
}
|
|
89
|
+
function owner(phase: string): string {
|
|
90
|
+
if (phase === 'proxy') return 'cliproxy';
|
|
91
|
+
return 'dev-fleet';
|
|
92
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export type CheckSeverity = 'pass' | 'warn' | 'fail';
|
|
2
|
+
|
|
3
|
+
/** One human-readable health finding, with an optional fix to run. */
|
|
4
|
+
export type DoctorCheck = {
|
|
5
|
+
id: string;
|
|
6
|
+
label: string;
|
|
7
|
+
severity: CheckSeverity;
|
|
8
|
+
detail: string;
|
|
9
|
+
fix?: string | undefined;
|
|
10
|
+
};
|
package/src/errors.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export class FleetUsageError extends Error {}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { readFile, mkdir, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { record } from './config';
|
|
4
|
+
import {
|
|
5
|
+
fileHash,
|
|
6
|
+
manifestHash,
|
|
7
|
+
sidecarHash,
|
|
8
|
+
INPUT_FILES,
|
|
9
|
+
FINGERPRINT_FILES,
|
|
10
|
+
} from './input-hashes';
|
|
11
|
+
import type { Fingerprint, Runner, Check } from './types';
|
|
12
|
+
|
|
13
|
+
const RECEIPT = 'node_modules/.fleet-sync.json';
|
|
14
|
+
|
|
15
|
+
export async function fingerprint(
|
|
16
|
+
root: string,
|
|
17
|
+
run: Runner
|
|
18
|
+
): Promise<Fingerprint> {
|
|
19
|
+
const files: Fingerprint['files'] = {};
|
|
20
|
+
for (const name of INPUT_FILES)
|
|
21
|
+
files[name] = await fileHash(join(root, name));
|
|
22
|
+
files['workspace-manifests'] = await manifestHash(root);
|
|
23
|
+
files['mise-sidecars'] = await sidecarHash(root);
|
|
24
|
+
const head = await run(['git', 'rev-parse', 'HEAD'], { cwd: root });
|
|
25
|
+
return {
|
|
26
|
+
head:
|
|
27
|
+
head.ok && /^[a-f0-9]{40,64}\s*$/.test(head.stdout)
|
|
28
|
+
? head.stdout.trim()
|
|
29
|
+
: null,
|
|
30
|
+
files,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export async function dependencyCheck(
|
|
35
|
+
root: string,
|
|
36
|
+
current: Fingerprint
|
|
37
|
+
): Promise<Check> {
|
|
38
|
+
const remedy = {
|
|
39
|
+
action:
|
|
40
|
+
'Review devfleet fleet-repair --repair workspace, then explicitly apply a frozen toolchain/dependency sync.',
|
|
41
|
+
};
|
|
42
|
+
try {
|
|
43
|
+
const receipt: unknown = JSON.parse(
|
|
44
|
+
await readFile(join(root, RECEIPT), 'utf8')
|
|
45
|
+
);
|
|
46
|
+
const matches =
|
|
47
|
+
record(receipt) &&
|
|
48
|
+
receipt['version'] === 1 &&
|
|
49
|
+
JSON.stringify(receipt['files']) === JSON.stringify(current.files);
|
|
50
|
+
return {
|
|
51
|
+
id: 'dependencies',
|
|
52
|
+
status: matches ? 'pass' : 'fail',
|
|
53
|
+
summary: matches
|
|
54
|
+
? 'Last successful frozen sync matches input hashes; not a full integrity audit.'
|
|
55
|
+
: 'Dependency sync receipt is stale or invalid.',
|
|
56
|
+
...(matches ? {} : { remedy }),
|
|
57
|
+
};
|
|
58
|
+
} catch {
|
|
59
|
+
return {
|
|
60
|
+
id: 'dependencies',
|
|
61
|
+
status: 'warn',
|
|
62
|
+
summary: 'No frozen-sync receipt; installed dependencies are unverified.',
|
|
63
|
+
remedy,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export async function writeReceipt(
|
|
69
|
+
root: string,
|
|
70
|
+
current: Fingerprint
|
|
71
|
+
): Promise<void> {
|
|
72
|
+
await mkdir(join(root, 'node_modules'), { recursive: true });
|
|
73
|
+
await writeFile(
|
|
74
|
+
join(root, RECEIPT),
|
|
75
|
+
JSON.stringify({ version: 1, files: current.files }),
|
|
76
|
+
{ mode: 0o600 }
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function compareFingerprints(
|
|
81
|
+
expected: Fingerprint,
|
|
82
|
+
actual: Fingerprint
|
|
83
|
+
): Check[] {
|
|
84
|
+
return [
|
|
85
|
+
compare('revision', expected.head, actual.head),
|
|
86
|
+
...FINGERPRINT_FILES.map((name) =>
|
|
87
|
+
compare(
|
|
88
|
+
`sync:${name}`,
|
|
89
|
+
expected.files[name] ?? null,
|
|
90
|
+
actual.files[name] ?? null
|
|
91
|
+
)
|
|
92
|
+
),
|
|
93
|
+
];
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function compare(
|
|
97
|
+
id: string,
|
|
98
|
+
expected: string | null,
|
|
99
|
+
actual: string | null
|
|
100
|
+
): Check {
|
|
101
|
+
const known = expected !== null && actual !== null;
|
|
102
|
+
const matches = known && expected === actual;
|
|
103
|
+
return {
|
|
104
|
+
id,
|
|
105
|
+
status: matches ? 'pass' : 'fail',
|
|
106
|
+
summary: matches
|
|
107
|
+
? 'Matches the manager checkout.'
|
|
108
|
+
: known
|
|
109
|
+
? 'Differs from the manager checkout.'
|
|
110
|
+
: 'Cannot verify; reference or node input is missing.',
|
|
111
|
+
...(matches
|
|
112
|
+
? {}
|
|
113
|
+
: {
|
|
114
|
+
remedy: {
|
|
115
|
+
action:
|
|
116
|
+
'Reconcile branches/lockfiles; review devfleet fleet-sync for the approved revision. Never reset a working tree.',
|
|
117
|
+
},
|
|
118
|
+
}),
|
|
119
|
+
};
|
|
120
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import {
|
|
3
|
+
existsSync,
|
|
4
|
+
mkdirSync,
|
|
5
|
+
readFileSync,
|
|
6
|
+
renameSync,
|
|
7
|
+
writeFileSync,
|
|
8
|
+
} from 'node:fs';
|
|
9
|
+
import { homedir } from 'node:os';
|
|
10
|
+
import { join } from 'node:path';
|
|
11
|
+
import { parseDefinition, type FleetDefinition } from './definition';
|
|
12
|
+
import { FleetUsageError } from './errors';
|
|
13
|
+
import { pinnedPath } from './agents/toolchain';
|
|
14
|
+
import type { Runner } from './types';
|
|
15
|
+
import {
|
|
16
|
+
secretsProvider,
|
|
17
|
+
type SecretCommand,
|
|
18
|
+
type SecretsProvider,
|
|
19
|
+
} from './secrets';
|
|
20
|
+
|
|
21
|
+
/** Where every machine keeps the fleet configuration. fleet-sync installs the
|
|
22
|
+
* approved copy here, so launchers and scheduled jobs find it without any
|
|
23
|
+
* environment. `DEVFLEET_CONFIG` names another file (the manager's checkout of
|
|
24
|
+
* its configuration repository, or a test fixture).
|
|
25
|
+
*/
|
|
26
|
+
export function configPath(env = process.env, home = homedir()): string {
|
|
27
|
+
return env['DEVFLEET_CONFIG'] || join(home, '.config/dev-fleet/fleet.json');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Installs the configuration this process runs with as the machine's copy,
|
|
31
|
+
* so launchers and scheduled jobs (which get no environment) use the same one.
|
|
32
|
+
* Returns whether the file changed.
|
|
33
|
+
*/
|
|
34
|
+
export function installConfig(home = homedir()): boolean {
|
|
35
|
+
const target = join(home, '.config/dev-fleet/fleet.json');
|
|
36
|
+
const content = JSON.stringify(fleet, null, 2) + '\n';
|
|
37
|
+
if (existsSync(target) && readFileSync(target, 'utf8') === content)
|
|
38
|
+
return false;
|
|
39
|
+
mkdirSync(join(home, '.config/dev-fleet'), { recursive: true, mode: 0o700 });
|
|
40
|
+
writeFileSync(`${target}.new`, content, { mode: 0o600 });
|
|
41
|
+
renameSync(`${target}.new`, target);
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function load(): FleetDefinition {
|
|
46
|
+
const path = configPath();
|
|
47
|
+
if (!existsSync(path))
|
|
48
|
+
throw new FleetUsageError(
|
|
49
|
+
`No fleet configuration at ${path}. Copy examples/fleet.json, edit it for your machines, and set DEVFLEET_CONFIG or place it there.`
|
|
50
|
+
);
|
|
51
|
+
return parseDefinition(JSON.parse(readFileSync(path, 'utf8')));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** This fleet's own configuration: its machines, endpoints, accounts and source
|
|
55
|
+
* repository. Fleet code never names any of them; it reads them here, and this
|
|
56
|
+
* is the only module that loads the file.
|
|
57
|
+
*/
|
|
58
|
+
export const fleet = load();
|
|
59
|
+
|
|
60
|
+
/** Reads one of this fleet's secrets for this node. Tools run from the pinned
|
|
61
|
+
* toolchain, whatever PATH the caller (T3, launchd, systemd) started with.
|
|
62
|
+
*/
|
|
63
|
+
export const secret: SecretsProvider = (name) =>
|
|
64
|
+
secretsProvider(fleet.secrets, { run: pinned })(name);
|
|
65
|
+
|
|
66
|
+
/** The same, through an injected runner (fleet-sync phases and their tests). */
|
|
67
|
+
export const secretVia = (run: Runner): SecretsProvider =>
|
|
68
|
+
secretsProvider(fleet.secrets, {
|
|
69
|
+
run: async (argv) => {
|
|
70
|
+
const result = await run(argv, { timeoutMs: 20_000 });
|
|
71
|
+
return result.ok ? result.stdout : null;
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const pinned: SecretCommand = (argv) =>
|
|
76
|
+
new Promise((resolve) => {
|
|
77
|
+
const child = spawn(argv[0]!, argv.slice(1), {
|
|
78
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
79
|
+
timeout: 20_000,
|
|
80
|
+
env: { ...process.env, PATH: pinnedPath() },
|
|
81
|
+
});
|
|
82
|
+
let output = '';
|
|
83
|
+
child.stdout.on('data', (chunk: Buffer) => {
|
|
84
|
+
if (output.length < 65_536) output += chunk.toString('utf8');
|
|
85
|
+
});
|
|
86
|
+
child.on('error', () => resolve(null));
|
|
87
|
+
child.on('close', (code) => resolve(code === 0 ? output : null));
|
|
88
|
+
});
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { parseConfig, loadConfig, selectNodes } from './config';
|
|
2
|
+
export { doctor } from './manager';
|
|
3
|
+
export { probe } from './probe';
|
|
4
|
+
export { makeReport, reportExitCode, renderReport } from './report';
|
|
5
|
+
export { syncPlan, syncWorkspace } from './sync';
|
|
6
|
+
export type * from './types';
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { readFile, readdir } from 'node:fs/promises';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
|
|
5
|
+
export const INPUT_FILES = [
|
|
6
|
+
'package.json',
|
|
7
|
+
'bun.lock',
|
|
8
|
+
'mise.toml',
|
|
9
|
+
'mise.lock',
|
|
10
|
+
];
|
|
11
|
+
export const FINGERPRINT_FILES = [
|
|
12
|
+
...INPUT_FILES,
|
|
13
|
+
'workspace-manifests',
|
|
14
|
+
'mise-sidecars',
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
export async function fileHash(path: string): Promise<string | null> {
|
|
18
|
+
try {
|
|
19
|
+
return digest(await readFile(path));
|
|
20
|
+
} catch {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function manifestHash(root: string): Promise<string | null> {
|
|
26
|
+
try {
|
|
27
|
+
const entries = await readdir(join(root, 'packages'), {
|
|
28
|
+
withFileTypes: true,
|
|
29
|
+
});
|
|
30
|
+
const hashes: string[] = [];
|
|
31
|
+
for (const entry of entries
|
|
32
|
+
.filter((item) => item.isDirectory())
|
|
33
|
+
.sort((left, right) => left.name.localeCompare(right.name, 'en'))) {
|
|
34
|
+
hashes.push(
|
|
35
|
+
`${entry.name}:${await fileHash(join(root, 'packages', entry.name, 'package.json'))}`
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
return digest(hashes.join('\n'));
|
|
39
|
+
} catch {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export async function sidecarHash(root: string): Promise<string | null> {
|
|
45
|
+
try {
|
|
46
|
+
const files = await readdir(join(root, '.mise/locks'), {
|
|
47
|
+
recursive: true,
|
|
48
|
+
withFileTypes: true,
|
|
49
|
+
});
|
|
50
|
+
if (files.length > 4096 || files.some((entry) => entry.isSymbolicLink()))
|
|
51
|
+
return null;
|
|
52
|
+
const hashes: string[] = [];
|
|
53
|
+
const paths = files
|
|
54
|
+
.filter((entry) => entry.isFile())
|
|
55
|
+
.map((entry) => join(entry.parentPath, entry.name))
|
|
56
|
+
.sort();
|
|
57
|
+
for (const path of paths) {
|
|
58
|
+
const hash = await fileHash(path);
|
|
59
|
+
if (hash === null) return null;
|
|
60
|
+
const relative = path
|
|
61
|
+
.slice(join(root, '.mise/locks').length)
|
|
62
|
+
.replaceAll('\\', '/');
|
|
63
|
+
hashes.push(`${relative}:${hash}`);
|
|
64
|
+
}
|
|
65
|
+
return digest(hashes.join('\n'));
|
|
66
|
+
} catch {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function digest(value: string | Buffer): string {
|
|
72
|
+
return createHash('sha256').update(value).digest('hex');
|
|
73
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { isRevision } from './state';
|
|
2
|
+
import { resolve } from 'node:path';
|
|
3
|
+
import type { Runner } from '../types';
|
|
4
|
+
import { fleet } from '../fleet-config';
|
|
5
|
+
|
|
6
|
+
/** `https://host/owner/repo.git` and `git@host:owner/repo` name one repository. */
|
|
7
|
+
export function sameRepository(remote: string, declared: string): boolean {
|
|
8
|
+
const name = (url: string) =>
|
|
9
|
+
url
|
|
10
|
+
.replace(/^(https:\/\/|ssh:\/\/git@|git@)/, '')
|
|
11
|
+
.replace(':', '/')
|
|
12
|
+
.replace(/\.git$/, '')
|
|
13
|
+
.toLowerCase();
|
|
14
|
+
return name(remote) === name(declared);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Use committed objects, never working-tree files, for an approved local branch. */
|
|
18
|
+
export async function approvedLocalSource(
|
|
19
|
+
checkout: string,
|
|
20
|
+
revision: string,
|
|
21
|
+
run: Runner
|
|
22
|
+
): Promise<string | null> {
|
|
23
|
+
const options = { cwd: checkout, timeoutMs: 10_000 };
|
|
24
|
+
const top = await run(['git', 'rev-parse', '--show-toplevel'], options);
|
|
25
|
+
if (!top.ok || resolve(top.stdout.trim()) !== resolve(checkout)) return null;
|
|
26
|
+
const origin = await run(['git', 'remote', 'get-url', 'origin'], options);
|
|
27
|
+
if (
|
|
28
|
+
!origin.ok ||
|
|
29
|
+
!sameRepository(origin.stdout.trim(), fleet.source.repository)
|
|
30
|
+
)
|
|
31
|
+
return null;
|
|
32
|
+
const commit = await run(
|
|
33
|
+
['git', 'rev-parse', '--verify', `${revision}^{commit}`],
|
|
34
|
+
options
|
|
35
|
+
);
|
|
36
|
+
return commit.ok && commit.stdout.trim() === revision ? checkout : null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export async function localCheckoutRevision(
|
|
40
|
+
checkout: string,
|
|
41
|
+
run: Runner
|
|
42
|
+
): Promise<string | undefined> {
|
|
43
|
+
const result = await run(['git', 'rev-parse', 'HEAD'], {
|
|
44
|
+
cwd: checkout,
|
|
45
|
+
timeoutMs: 10_000,
|
|
46
|
+
});
|
|
47
|
+
const revision = result.stdout.trim();
|
|
48
|
+
if (!result.ok || !isRevision(revision)) return undefined;
|
|
49
|
+
return (await approvedLocalSource(checkout, revision, run))
|
|
50
|
+
? revision
|
|
51
|
+
: undefined;
|
|
52
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { MachineDefinition } from '../definition';
|
|
2
|
+
|
|
3
|
+
export type MachinePlatform = MachineDefinition['os'];
|
|
4
|
+
|
|
5
|
+
/** One POSIX probe for every supported machine: family, release and CPU. */
|
|
6
|
+
export const PLATFORM_PROBE =
|
|
7
|
+
'sh -c \'if [ "$(uname -s)" = Darwin ]; then printf "macos\\n%s\\n%s\\n" "$(sw_vers -productVersion)" "$(uname -m)"; else . /etc/os-release && printf "%s\\n%s\\n%s\\n" "$ID" "$VERSION_ID" "$(uname -m)"; fi\'';
|
|
8
|
+
|
|
9
|
+
export interface PlatformBaseline {
|
|
10
|
+
id: 'ubuntu:release' | 'macos:release';
|
|
11
|
+
supported: boolean;
|
|
12
|
+
summary: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Ubuntu is the fully accepted worker platform; macOS is best effort. */
|
|
16
|
+
export function evaluatePlatform(
|
|
17
|
+
os: MachinePlatform,
|
|
18
|
+
probe: string
|
|
19
|
+
): PlatformBaseline {
|
|
20
|
+
if (os === 'macOS') {
|
|
21
|
+
const supported =
|
|
22
|
+
/^macos\n(1[4-9]|[2-9][0-9])(\.[0-9]+)*\n(arm64|x86_64)\s*$/.test(probe);
|
|
23
|
+
return {
|
|
24
|
+
id: 'macos:release',
|
|
25
|
+
supported,
|
|
26
|
+
summary: supported
|
|
27
|
+
? 'macOS 14 or newer and a supported CPU architecture observed; macOS policy is best effort.'
|
|
28
|
+
: 'MACOS_BASELINE_MISMATCH',
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
const supported = /^ubuntu\n26\.04\n(x86_64|aarch64)\s*$/.test(probe);
|
|
32
|
+
return {
|
|
33
|
+
id: 'ubuntu:release',
|
|
34
|
+
supported,
|
|
35
|
+
summary: supported
|
|
36
|
+
? 'Ubuntu 26.04 and supported CPU architecture observed.'
|
|
37
|
+
: 'UBUNTU_BASELINE_MISMATCH',
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Windows machines stay declared but never receive manager convergence. */
|
|
42
|
+
export function convergeable(node: MachineDefinition): boolean {
|
|
43
|
+
return node.os !== 'windows';
|
|
44
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { approvedLocalSource } from './local-source';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { mkdir } from 'node:fs/promises';
|
|
4
|
+
import type { Runner } from '../types';
|
|
5
|
+
import { ensurePrivateRoot } from './state';
|
|
6
|
+
import { fleet } from '../fleet-config';
|
|
7
|
+
|
|
8
|
+
const REPOSITORY = fleet.source.repository;
|
|
9
|
+
|
|
10
|
+
/** Called after approval; local sources supply Git objects only, never dirty files. */
|
|
11
|
+
export async function prepareSource(
|
|
12
|
+
root: string,
|
|
13
|
+
revision: string,
|
|
14
|
+
run: Runner,
|
|
15
|
+
checkout?: string
|
|
16
|
+
): Promise<string | null> {
|
|
17
|
+
if (checkout) {
|
|
18
|
+
const local = await approvedLocalSource(checkout, revision, run);
|
|
19
|
+
if (local) return local;
|
|
20
|
+
}
|
|
21
|
+
return prepareRemoteSource(root, revision, run);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function prepareRemoteSource(
|
|
25
|
+
root: string,
|
|
26
|
+
revision: string,
|
|
27
|
+
run: Runner
|
|
28
|
+
): Promise<string | null> {
|
|
29
|
+
const source = join(root, 'source');
|
|
30
|
+
await ensurePrivateRoot(source);
|
|
31
|
+
await mkdir(root, { recursive: true, mode: 0o700 });
|
|
32
|
+
let origin = await run(['git', 'remote', 'get-url', 'origin'], {
|
|
33
|
+
cwd: source,
|
|
34
|
+
});
|
|
35
|
+
if (!origin.ok) {
|
|
36
|
+
const cloned = await run(
|
|
37
|
+
[
|
|
38
|
+
'git',
|
|
39
|
+
'clone',
|
|
40
|
+
'--no-checkout',
|
|
41
|
+
'--origin',
|
|
42
|
+
'origin',
|
|
43
|
+
REPOSITORY,
|
|
44
|
+
source,
|
|
45
|
+
],
|
|
46
|
+
{ timeoutMs: 120_000 }
|
|
47
|
+
);
|
|
48
|
+
if (!cloned.ok) return null;
|
|
49
|
+
origin = await run(['git', 'remote', 'get-url', 'origin'], { cwd: source });
|
|
50
|
+
}
|
|
51
|
+
if (!origin.ok || origin.stdout.trim() !== REPOSITORY) return null;
|
|
52
|
+
let found = await run(
|
|
53
|
+
['git', 'rev-parse', '--verify', `${revision}^{commit}`],
|
|
54
|
+
{ cwd: source }
|
|
55
|
+
);
|
|
56
|
+
if (!found.ok || found.stdout.trim() !== revision) {
|
|
57
|
+
const fetch = await run(['git', 'fetch', '--no-tags', 'origin', revision], {
|
|
58
|
+
cwd: source,
|
|
59
|
+
timeoutMs: 120_000,
|
|
60
|
+
});
|
|
61
|
+
if (!fetch.ok) return null;
|
|
62
|
+
found = await run(
|
|
63
|
+
['git', 'rev-parse', '--verify', `${revision}^{commit}`],
|
|
64
|
+
{ cwd: source }
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
return found.ok && found.stdout.trim() === revision ? source : null;
|
|
68
|
+
}
|