@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,400 @@
|
|
|
1
|
+
import { diagnostic, type Diagnostic } from './diagnostics';
|
|
2
|
+
import {
|
|
3
|
+
blocking,
|
|
4
|
+
type Check,
|
|
5
|
+
type FleetNode,
|
|
6
|
+
type NodeReport,
|
|
7
|
+
type Runner,
|
|
8
|
+
} from './types';
|
|
9
|
+
import { discoverTailnet, type TailnetComputer } from './tailscale';
|
|
10
|
+
import { matchComputers } from './tailnet-matching';
|
|
11
|
+
import { sshArgs, sshFailure } from './ssh';
|
|
12
|
+
import type { FleetDefinition, MachineDefinition } from './definition';
|
|
13
|
+
import { PUBLIC_HUB_URL } from './monitoring/beszel';
|
|
14
|
+
import {
|
|
15
|
+
convergeable,
|
|
16
|
+
evaluatePlatform,
|
|
17
|
+
PLATFORM_PROBE,
|
|
18
|
+
type MachinePlatform,
|
|
19
|
+
} from './machine/platform';
|
|
20
|
+
|
|
21
|
+
export interface ManagerNode extends NodeReport {
|
|
22
|
+
/** The singleton manager converges locally; workers over SSH. */
|
|
23
|
+
role?: 'manager' | 'worker';
|
|
24
|
+
reachability: 'online' | 'offline' | 'missing' | 'conflict' | 'unknown';
|
|
25
|
+
desiredRevision: string | null;
|
|
26
|
+
appliedRevision: string | null;
|
|
27
|
+
durationMs: number;
|
|
28
|
+
diagnostics?: Diagnostic[];
|
|
29
|
+
}
|
|
30
|
+
export interface ManagerReport {
|
|
31
|
+
version: 3;
|
|
32
|
+
command: 'fleet-sync';
|
|
33
|
+
execution: 'manager-push';
|
|
34
|
+
ok: boolean;
|
|
35
|
+
applied: boolean;
|
|
36
|
+
desiredRevision: string | null;
|
|
37
|
+
checks: Check[];
|
|
38
|
+
nodes: ManagerNode[];
|
|
39
|
+
candidates: number;
|
|
40
|
+
dashboardUrl: string | null;
|
|
41
|
+
}
|
|
42
|
+
export type DeployNode = (
|
|
43
|
+
node: FleetNode,
|
|
44
|
+
revision: string
|
|
45
|
+
) => Promise<NodeReport>;
|
|
46
|
+
|
|
47
|
+
/** Minimal read-only POSIX inspection requires no workspace, Node, Bun or Git. */
|
|
48
|
+
export async function inspectBootstrap(
|
|
49
|
+
node: FleetNode,
|
|
50
|
+
run: Runner,
|
|
51
|
+
os: MachinePlatform = 'linux'
|
|
52
|
+
): Promise<Check[]> {
|
|
53
|
+
const result = await run([...sshArgs(node), PLATFORM_PROBE], {
|
|
54
|
+
timeoutMs: 20_000,
|
|
55
|
+
});
|
|
56
|
+
if (!result.ok) return [sshFailure(result)];
|
|
57
|
+
const baseline = evaluatePlatform(os, result.stdout);
|
|
58
|
+
return [
|
|
59
|
+
{
|
|
60
|
+
id: 'ssh:fresh-session',
|
|
61
|
+
status: 'pass',
|
|
62
|
+
summary:
|
|
63
|
+
'Fresh host-key-verified SSH session succeeded without remote developer prerequisites.',
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
id: baseline.id,
|
|
67
|
+
status: baseline.supported ? 'pass' : 'fail',
|
|
68
|
+
summary: baseline.summary,
|
|
69
|
+
remedy: {
|
|
70
|
+
action:
|
|
71
|
+
'Review the release/architecture migration in source. No operating-system upgrade or downgrade is performed.',
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** One invocation, one immutable revision. Serial application bounds pressure and
|
|
78
|
+
* lets a failed declared canary stop promotion; diagnostics still cover all nodes.
|
|
79
|
+
*/
|
|
80
|
+
export async function synchronizeManager(
|
|
81
|
+
input: {
|
|
82
|
+
definition: FleetDefinition;
|
|
83
|
+
nodes: FleetNode[];
|
|
84
|
+
revision: string;
|
|
85
|
+
apply: boolean;
|
|
86
|
+
canary?: string;
|
|
87
|
+
progress?: (node: string, phase: string) => void;
|
|
88
|
+
/** Called once per node as soon as its result is final. */
|
|
89
|
+
finished?: (node: ManagerNode) => void;
|
|
90
|
+
/** Converges the manager locally; omitted in tests. */
|
|
91
|
+
manager?: () => Promise<ManagerNode>;
|
|
92
|
+
},
|
|
93
|
+
run: Runner,
|
|
94
|
+
deploy: DeployNode
|
|
95
|
+
): Promise<ManagerReport> {
|
|
96
|
+
const tailnet = await discoverTailnet(run);
|
|
97
|
+
const matches = await matchComputers(input.nodes, tailnet.computers, run);
|
|
98
|
+
const report: ManagerReport = {
|
|
99
|
+
version: 3,
|
|
100
|
+
command: 'fleet-sync',
|
|
101
|
+
execution: 'manager-push',
|
|
102
|
+
ok: false,
|
|
103
|
+
applied: false,
|
|
104
|
+
desiredRevision: input.revision,
|
|
105
|
+
checks: tailnet.checks,
|
|
106
|
+
// Printed for people: the stable public name, never a tailnet address.
|
|
107
|
+
dashboardUrl: PUBLIC_HUB_URL,
|
|
108
|
+
nodes: [],
|
|
109
|
+
candidates: tailnet.computers.filter(
|
|
110
|
+
(peer) => !peer.self && !matches.get(peer.id)?.length
|
|
111
|
+
).length,
|
|
112
|
+
};
|
|
113
|
+
const managerRun = input.manager ? convergeManagerNode(input) : null;
|
|
114
|
+
// The manager finishes first: updating it may restart the proxy every
|
|
115
|
+
// worker is then verified against.
|
|
116
|
+
if (managerRun) await managerRun;
|
|
117
|
+
const workers = await Promise.all(
|
|
118
|
+
input.definition.workers.filter(convergeable).map((machine) => {
|
|
119
|
+
input.progress?.(
|
|
120
|
+
machine.id,
|
|
121
|
+
'Inspecting enrolled identity and fresh SSH access'
|
|
122
|
+
);
|
|
123
|
+
return inspectManagerNode(
|
|
124
|
+
machine,
|
|
125
|
+
{
|
|
126
|
+
nodes: input.nodes,
|
|
127
|
+
revision: input.revision,
|
|
128
|
+
computers: tailnet.computers,
|
|
129
|
+
matches,
|
|
130
|
+
},
|
|
131
|
+
run
|
|
132
|
+
);
|
|
133
|
+
})
|
|
134
|
+
);
|
|
135
|
+
report.nodes.push(...workers);
|
|
136
|
+
if (input.apply) await applyNodes(report, input, deploy);
|
|
137
|
+
else workers.forEach((node) => input.finished?.(node));
|
|
138
|
+
if (managerRun) report.nodes.unshift(await managerRun);
|
|
139
|
+
report.ok = ready(report);
|
|
140
|
+
for (const node of report.nodes)
|
|
141
|
+
node.diagnostics = node.checks.map((check) => diagnostic(node.node, check));
|
|
142
|
+
return report;
|
|
143
|
+
}
|
|
144
|
+
async function convergeManagerNode(input: {
|
|
145
|
+
definition: FleetDefinition;
|
|
146
|
+
progress?: (node: string, phase: string) => void;
|
|
147
|
+
finished?: (node: ManagerNode) => void;
|
|
148
|
+
manager?: () => Promise<ManagerNode>;
|
|
149
|
+
}): Promise<ManagerNode> {
|
|
150
|
+
input.progress?.(
|
|
151
|
+
input.definition.manager.id,
|
|
152
|
+
'Converging the manager and its singleton services locally'
|
|
153
|
+
);
|
|
154
|
+
const manager = await input.manager!();
|
|
155
|
+
input.finished?.(manager);
|
|
156
|
+
return manager;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** Ready when nothing required failed; warnings and skips do not count. */
|
|
160
|
+
function ready(report: ManagerReport): boolean {
|
|
161
|
+
return (
|
|
162
|
+
!report.checks.some(blocking) &&
|
|
163
|
+
report.nodes.length > 0 &&
|
|
164
|
+
report.nodes.every(
|
|
165
|
+
(node) => node.checks.length > 0 && !node.checks.some(blocking)
|
|
166
|
+
)
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async function applyNodes(
|
|
171
|
+
report: ManagerReport,
|
|
172
|
+
input: {
|
|
173
|
+
nodes: FleetNode[];
|
|
174
|
+
revision: string;
|
|
175
|
+
canary?: string;
|
|
176
|
+
progress?: (node: string, phase: string) => void;
|
|
177
|
+
finished?: (node: ManagerNode) => void;
|
|
178
|
+
},
|
|
179
|
+
deploy: DeployNode
|
|
180
|
+
): Promise<void> {
|
|
181
|
+
const workers = report.nodes.filter((node) => node.role !== 'manager');
|
|
182
|
+
const canary = workers.find((node) => node.node === input.canary);
|
|
183
|
+
let blocked = report.checks.some(blocking);
|
|
184
|
+
const step = async (result: ManagerNode) => {
|
|
185
|
+
const deployable = mayDeploy(result, input.nodes);
|
|
186
|
+
if (await rollOut(result, { deployable, blocked }, input, deploy))
|
|
187
|
+
report.applied = true;
|
|
188
|
+
input.finished?.(result);
|
|
189
|
+
return deployable;
|
|
190
|
+
};
|
|
191
|
+
// The canary goes first; a failed canary stops promotion. The remaining
|
|
192
|
+
// workers are independent, so they converge concurrently.
|
|
193
|
+
if (canary) {
|
|
194
|
+
const deployable = await step(canary);
|
|
195
|
+
if (!deployable || canary.checks.some(blocksPromotion)) blocked = true;
|
|
196
|
+
}
|
|
197
|
+
await Promise.all(workers.filter((node) => node !== canary).map(step));
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** One node's rollout step; true when its remote operation ran. */
|
|
201
|
+
async function rollOut(
|
|
202
|
+
result: ManagerNode,
|
|
203
|
+
{ deployable, blocked }: { deployable: boolean; blocked: boolean },
|
|
204
|
+
input: {
|
|
205
|
+
nodes: FleetNode[];
|
|
206
|
+
revision: string;
|
|
207
|
+
progress?: (node: string, phase: string) => void;
|
|
208
|
+
},
|
|
209
|
+
deploy: DeployNode
|
|
210
|
+
): Promise<boolean> {
|
|
211
|
+
if (!deployable) return false;
|
|
212
|
+
if (blocked) {
|
|
213
|
+
result.checks.push({
|
|
214
|
+
id: 'rollout:promotion',
|
|
215
|
+
status: 'fail',
|
|
216
|
+
summary: 'FLEET_PROMOTION_BLOCKED',
|
|
217
|
+
remedy: {
|
|
218
|
+
action:
|
|
219
|
+
'Resolve the discovery or canary failure above, then rerun the same approved revision.',
|
|
220
|
+
},
|
|
221
|
+
});
|
|
222
|
+
return false;
|
|
223
|
+
}
|
|
224
|
+
input.progress?.(
|
|
225
|
+
result.node,
|
|
226
|
+
'Applying the approved revision and verifying postconditions'
|
|
227
|
+
);
|
|
228
|
+
return deployNode(result, input, deploy);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/** Deploys one node and records its evidence; true when the remote ran. */
|
|
232
|
+
async function deployNode(
|
|
233
|
+
result: ManagerNode,
|
|
234
|
+
input: { nodes: FleetNode[]; revision: string },
|
|
235
|
+
deploy: DeployNode
|
|
236
|
+
): Promise<boolean> {
|
|
237
|
+
const started = Date.now();
|
|
238
|
+
let ran = false;
|
|
239
|
+
try {
|
|
240
|
+
const outcome = await deploy(
|
|
241
|
+
input.nodes.find((node) => node.id === result.node)!,
|
|
242
|
+
input.revision
|
|
243
|
+
);
|
|
244
|
+
ran = true;
|
|
245
|
+
result.checks.push(...outcome.checks);
|
|
246
|
+
if (!outcome.checks.length)
|
|
247
|
+
result.checks.push({
|
|
248
|
+
id: 'rollout:evidence',
|
|
249
|
+
status: 'fail',
|
|
250
|
+
summary: 'FLEET_VERIFICATION_MISSING',
|
|
251
|
+
});
|
|
252
|
+
} catch {
|
|
253
|
+
result.checks.push({
|
|
254
|
+
id: 'rollout:operation',
|
|
255
|
+
status: 'fail',
|
|
256
|
+
summary: 'FLEET_REMOTE_OPERATION_FAILED',
|
|
257
|
+
remedy: {
|
|
258
|
+
action:
|
|
259
|
+
'Inspect the remote operation lock before retrying. A disconnected manager does not establish that installation stopped.',
|
|
260
|
+
},
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
result.durationMs += Date.now() - started;
|
|
264
|
+
if (!result.checks.some(blocking)) result.appliedRevision = input.revision;
|
|
265
|
+
return ran;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function connectionState(
|
|
269
|
+
conflict: boolean,
|
|
270
|
+
peer: TailnetComputer | undefined
|
|
271
|
+
): ManagerNode['reachability'] {
|
|
272
|
+
if (conflict) return 'conflict';
|
|
273
|
+
if (!peer) return 'missing';
|
|
274
|
+
return peer.online ? 'online' : 'offline';
|
|
275
|
+
}
|
|
276
|
+
async function inspectManagerNode(
|
|
277
|
+
machine: MachineDefinition,
|
|
278
|
+
context: {
|
|
279
|
+
nodes: FleetNode[];
|
|
280
|
+
revision: string;
|
|
281
|
+
computers: TailnetComputer[];
|
|
282
|
+
matches: Map<string, FleetNode[]>;
|
|
283
|
+
},
|
|
284
|
+
run: Runner
|
|
285
|
+
): Promise<ManagerNode> {
|
|
286
|
+
const { nodes, revision, computers, matches } = context;
|
|
287
|
+
const id = machine.id;
|
|
288
|
+
const started = Date.now();
|
|
289
|
+
const node = nodes.find((entry) => entry.id === id);
|
|
290
|
+
const peers = computers.filter((peer) =>
|
|
291
|
+
matches.get(peer.id)?.some((entry) => entry.id === id)
|
|
292
|
+
);
|
|
293
|
+
const peer = peers[0];
|
|
294
|
+
const conflict =
|
|
295
|
+
peers.length > 1 || (peer && matches.get(peer.id)!.length > 1);
|
|
296
|
+
const checks: Check[] = [];
|
|
297
|
+
if (!node)
|
|
298
|
+
checks.push({
|
|
299
|
+
id: 'enrollment:connection',
|
|
300
|
+
status: 'fail',
|
|
301
|
+
summary: 'FLEET_CONNECTION_REQUIRED',
|
|
302
|
+
remedy: {
|
|
303
|
+
action:
|
|
304
|
+
'Add this canonical node’s reviewed SSH alias and managed workspace path to the private manager overrides. Do not substitute a discovered peer.',
|
|
305
|
+
},
|
|
306
|
+
});
|
|
307
|
+
else if (conflict)
|
|
308
|
+
checks.push({
|
|
309
|
+
id: 'tailscale:identity',
|
|
310
|
+
status: 'fail',
|
|
311
|
+
summary: 'TAILSCALE_IDENTITY_CONFLICT',
|
|
312
|
+
remedy: {
|
|
313
|
+
action:
|
|
314
|
+
'Resolve duplicate alias/device mappings before enrollment or apply.',
|
|
315
|
+
},
|
|
316
|
+
});
|
|
317
|
+
else {
|
|
318
|
+
checks.push(...(await inspectBootstrap(node, run, machine.os)));
|
|
319
|
+
checks.push(...identityChecks(node, peer));
|
|
320
|
+
}
|
|
321
|
+
if (!peer || !peer.online)
|
|
322
|
+
checks.push({
|
|
323
|
+
id: 'tailscale:reachability',
|
|
324
|
+
status: 'fail',
|
|
325
|
+
summary: peer ? 'TAILSCALE_NODE_OFFLINE' : 'TAILSCALE_NODE_MISSING',
|
|
326
|
+
remedy: {
|
|
327
|
+
action:
|
|
328
|
+
'Restore the intended node’s private connection. A trusted LAN SSH route can be inspected, but discovery never enrolls a replacement.',
|
|
329
|
+
},
|
|
330
|
+
});
|
|
331
|
+
return {
|
|
332
|
+
node: id,
|
|
333
|
+
role: 'worker',
|
|
334
|
+
checks,
|
|
335
|
+
desiredRevision: revision,
|
|
336
|
+
appliedRevision: null,
|
|
337
|
+
durationMs: Date.now() - started,
|
|
338
|
+
reachability: connectionState(Boolean(conflict), peer),
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function identityChecks(
|
|
343
|
+
node: FleetNode,
|
|
344
|
+
peer: TailnetComputer | undefined
|
|
345
|
+
): Check[] {
|
|
346
|
+
const checks: Check[] = [];
|
|
347
|
+
if (!node.tailscaleId || (peer && peer.id !== node.tailscaleId))
|
|
348
|
+
checks.push({
|
|
349
|
+
id: 'tailscale:enrolled-identity',
|
|
350
|
+
status: 'fail',
|
|
351
|
+
summary: node.tailscaleId
|
|
352
|
+
? 'TAILSCALE_DEVICE_REPLACED'
|
|
353
|
+
: 'TAILSCALE_ENROLLMENT_REQUIRED',
|
|
354
|
+
remedy: {
|
|
355
|
+
action:
|
|
356
|
+
'Verify the device identity and SSH host key, then record its stable tailscaleId in the reviewed private connection override. Names alone cannot authorize replacement.',
|
|
357
|
+
},
|
|
358
|
+
});
|
|
359
|
+
return checks;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function mayDeploy(result: ManagerNode, nodes: FleetNode[]): boolean {
|
|
363
|
+
const allowBootstrap =
|
|
364
|
+
nodes.find((node) => node.id === result.node)?.bootstrapViaSsh === true;
|
|
365
|
+
return result.checks.every(
|
|
366
|
+
(check) =>
|
|
367
|
+
!blocking(check) ||
|
|
368
|
+
(allowBootstrap &&
|
|
369
|
+
[
|
|
370
|
+
'TAILSCALE_NODE_OFFLINE',
|
|
371
|
+
'TAILSCALE_NODE_MISSING',
|
|
372
|
+
'TAILSCALE_ENROLLMENT_REQUIRED',
|
|
373
|
+
].includes(check.summary))
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/** Service/account acceptance remains required for readiness, but a missing
|
|
378
|
+
* external account, dashboard or client pairing must not stop independent OS
|
|
379
|
+
* configuration on other nodes. Actual deployment failures stop promotion.
|
|
380
|
+
*/
|
|
381
|
+
export function blocksPromotion(check: Check): boolean {
|
|
382
|
+
if (!blocking(check)) return false;
|
|
383
|
+
if (check.id === 't3:service' && T3_MAINTENANCE.has(check.summary))
|
|
384
|
+
return false;
|
|
385
|
+
return !/^(worker:|beszel:|provider:|agent:|proxy:|t3:end-to-end$|t3:paired-access$|t3:policy-loaded$|ubuntu:boot-policy$)/.test(
|
|
386
|
+
check.id
|
|
387
|
+
);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/** Native T3 outcomes that need an operator maintenance window (an active
|
|
391
|
+
* listener or running release must never be killed by routine sync). They are
|
|
392
|
+
* reported as failures but do not block independent OS policy on other nodes.
|
|
393
|
+
*/
|
|
394
|
+
const T3_MAINTENANCE = new Set([
|
|
395
|
+
'T3_LISTENER_CONFLICT',
|
|
396
|
+
'T3_ACTIVE_LISTENER_MIGRATION_REQUIRED',
|
|
397
|
+
'T3_MAINTENANCE_REQUIRED',
|
|
398
|
+
'T3_POLICY_RESTART_PENDING',
|
|
399
|
+
'T3_DESKTOP_MIGRATION_REQUIRED',
|
|
400
|
+
]);
|
package/src/manager.ts
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { compareFingerprints, fingerprint } from './fingerprint';
|
|
2
|
+
import { probe } from './probe';
|
|
3
|
+
import { mapBounded } from './report';
|
|
4
|
+
import { bundleProbe, inspectRemote } from './transport';
|
|
5
|
+
import type { Capability, FleetNode, NodeReport, Runner } from './types';
|
|
6
|
+
|
|
7
|
+
export interface DoctorOptions {
|
|
8
|
+
root: string;
|
|
9
|
+
timeoutMs: number;
|
|
10
|
+
local: boolean;
|
|
11
|
+
required: Capability[];
|
|
12
|
+
source?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export async function doctor(
|
|
16
|
+
nodes: FleetNode[],
|
|
17
|
+
options: DoctorOptions,
|
|
18
|
+
run: Runner
|
|
19
|
+
): Promise<NodeReport[]> {
|
|
20
|
+
const local = await probe(options.root, options.required, run);
|
|
21
|
+
const agent = await run(['ssh-add', '-l']);
|
|
22
|
+
const reports: NodeReport[] = [
|
|
23
|
+
{
|
|
24
|
+
node: 'manager',
|
|
25
|
+
snapshot: local,
|
|
26
|
+
checks: [
|
|
27
|
+
...local.checks,
|
|
28
|
+
{
|
|
29
|
+
id: 'ssh:agent',
|
|
30
|
+
status: agent.ok ? 'pass' : 'warn',
|
|
31
|
+
summary: agent.ok
|
|
32
|
+
? 'SSH agent exposes at least one identity.'
|
|
33
|
+
: 'No accessible SSH agent identities; explicit IdentityFile keys may still work.',
|
|
34
|
+
...(!agent.ok
|
|
35
|
+
? {
|
|
36
|
+
remedy: {
|
|
37
|
+
action:
|
|
38
|
+
'Unlock/load the intended key in the manager session. Check SSH_AUTH_SOCK and GUI/editor agent visibility; do not copy private keys.',
|
|
39
|
+
},
|
|
40
|
+
}
|
|
41
|
+
: {}),
|
|
42
|
+
},
|
|
43
|
+
],
|
|
44
|
+
},
|
|
45
|
+
];
|
|
46
|
+
if (options.local || nodes.length === 0) return reports;
|
|
47
|
+
const source = options.source ?? (await bundleProbe());
|
|
48
|
+
const reference = await fingerprint(options.root, run);
|
|
49
|
+
const remote = await mapBounded(nodes, async (node) => {
|
|
50
|
+
const report = await inspectRemote(
|
|
51
|
+
node,
|
|
52
|
+
{ action: 'probe', source, timeoutMs: options.timeoutMs },
|
|
53
|
+
run
|
|
54
|
+
);
|
|
55
|
+
if (report.snapshot)
|
|
56
|
+
report.checks.push(
|
|
57
|
+
...compareFingerprints(reference, report.snapshot.fingerprint)
|
|
58
|
+
);
|
|
59
|
+
return report;
|
|
60
|
+
});
|
|
61
|
+
return [...reports, ...remote];
|
|
62
|
+
}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { runAnsibleWithReceipt } from '../platform/ansible-receipt';
|
|
2
|
+
import { convergeManagerHub } from './manager-hub';
|
|
3
|
+
import { resolve } from 'node:path';
|
|
4
|
+
import { fleet } from '../fleet-config';
|
|
5
|
+
import { hubUrl, roleOf } from '../topology';
|
|
6
|
+
import { record } from '../config';
|
|
7
|
+
import { isRevision } from '../machine/state';
|
|
8
|
+
import { privateHub } from './beszel';
|
|
9
|
+
import { runLockedCommand as runCommand } from '../runner';
|
|
10
|
+
import type { Check, Runner } from '../types';
|
|
11
|
+
|
|
12
|
+
export async function applyAgent(
|
|
13
|
+
value: unknown,
|
|
14
|
+
root: string,
|
|
15
|
+
run: Runner
|
|
16
|
+
): Promise<Check[]> {
|
|
17
|
+
const request = validateRequest(value);
|
|
18
|
+
const definition = fleet;
|
|
19
|
+
await validateSource(request, root, run);
|
|
20
|
+
const role = roleOf(definition, request.node);
|
|
21
|
+
if (role === null) throw new Error('BESZEL_NOT_ENROLLED');
|
|
22
|
+
if (request.mode === 'hub') {
|
|
23
|
+
// The hub runs only on the manager; workers only ever run the agent.
|
|
24
|
+
if (role !== 'manager') throw new Error('BESZEL_NOT_HUB');
|
|
25
|
+
return convergeManagerHub(root, definition, run);
|
|
26
|
+
}
|
|
27
|
+
if (
|
|
28
|
+
typeof request.publicKey !== 'string' ||
|
|
29
|
+
typeof request.token !== 'string'
|
|
30
|
+
)
|
|
31
|
+
throw new Error('BESZEL_CREDENTIALS_REQUIRED');
|
|
32
|
+
// Agents connect only to the manager's private hub URL.
|
|
33
|
+
if (privateHub(request.hubUrl) !== privateHub(hubUrl(definition)))
|
|
34
|
+
throw new Error('BESZEL_HUB_MISMATCH');
|
|
35
|
+
return installAgent(request, root, run);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Workers (Linux) install a hardened systemd unit. The macOS manager's own
|
|
39
|
+
* agent is a per-user launch agent installed locally by fleet-sync
|
|
40
|
+
* (installManagerAgent), never through this remote entry point.
|
|
41
|
+
*/
|
|
42
|
+
async function installAgent(
|
|
43
|
+
request: Request,
|
|
44
|
+
root: string,
|
|
45
|
+
run: Runner
|
|
46
|
+
): Promise<Check[]> {
|
|
47
|
+
const monitoring = resolve(root, 'ansible/monitoring');
|
|
48
|
+
const result = await runAnsibleWithReceipt(
|
|
49
|
+
run,
|
|
50
|
+
'agent.yml',
|
|
51
|
+
{
|
|
52
|
+
cwd: monitoring,
|
|
53
|
+
timeoutMs: 300_000,
|
|
54
|
+
input: JSON.stringify({
|
|
55
|
+
beszel_node: request.node,
|
|
56
|
+
beszel_hub_url: privateHub(request.hubUrl),
|
|
57
|
+
beszel_public_key: request.publicKey,
|
|
58
|
+
beszel_agent_token: request.token,
|
|
59
|
+
}),
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
name: 'beszel-agent',
|
|
63
|
+
sources: [monitoring],
|
|
64
|
+
verify: () => agentActive(run),
|
|
65
|
+
}
|
|
66
|
+
);
|
|
67
|
+
if (!result.ok)
|
|
68
|
+
return [
|
|
69
|
+
{
|
|
70
|
+
id: 'beszel:installation',
|
|
71
|
+
status: 'fail',
|
|
72
|
+
summary: 'BESZEL_INSTALLATION_INCOMPLETE',
|
|
73
|
+
remedy: {
|
|
74
|
+
action:
|
|
75
|
+
'Inspect sudo, signed artifact access, free disk and existing service ownership. Preserve the native agent DATA_DIR and rerun the manager.',
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
];
|
|
79
|
+
const ready = await agentActive(run);
|
|
80
|
+
return [
|
|
81
|
+
{
|
|
82
|
+
id: 'beszel:service',
|
|
83
|
+
status: ready ? 'pass' : 'fail',
|
|
84
|
+
summary: ready
|
|
85
|
+
? 'Native Beszel service is enabled and active; hub metrics remain a separate gate.'
|
|
86
|
+
: 'BESZEL_SERVICE_STOPPED',
|
|
87
|
+
},
|
|
88
|
+
];
|
|
89
|
+
}
|
|
90
|
+
async function agentActive(run: Runner): Promise<boolean> {
|
|
91
|
+
return (
|
|
92
|
+
(await run(['systemctl', 'is-enabled', 'beszel-agent.service'])).ok &&
|
|
93
|
+
(await run(['systemctl', 'is-active', 'beszel-agent.service'])).ok
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
interface Request {
|
|
97
|
+
node: string;
|
|
98
|
+
revision: string;
|
|
99
|
+
hubUrl: string;
|
|
100
|
+
mode?: unknown;
|
|
101
|
+
publicKey?: unknown;
|
|
102
|
+
token?: unknown;
|
|
103
|
+
}
|
|
104
|
+
function validateRequest(value: unknown): Request {
|
|
105
|
+
if (
|
|
106
|
+
!record(value) ||
|
|
107
|
+
typeof value['node'] !== 'string' ||
|
|
108
|
+
!isRevision(value['revision']) ||
|
|
109
|
+
typeof value['hubUrl'] !== 'string'
|
|
110
|
+
)
|
|
111
|
+
throw new Error('BESZEL_INPUT_INVALID');
|
|
112
|
+
return value as unknown as Request;
|
|
113
|
+
}
|
|
114
|
+
async function validateSource(
|
|
115
|
+
request: Request,
|
|
116
|
+
root: string,
|
|
117
|
+
run: Runner
|
|
118
|
+
): Promise<void> {
|
|
119
|
+
for (const [args, expected] of [
|
|
120
|
+
[['git', 'rev-parse', 'HEAD'], request.revision],
|
|
121
|
+
[['git', 'rev-parse', '--show-toplevel'], resolve(root)],
|
|
122
|
+
[['git', 'status', '--porcelain'], ''],
|
|
123
|
+
] as const) {
|
|
124
|
+
const observed = await run([...args], { cwd: root });
|
|
125
|
+
if (!observed.ok || observed.stdout.trim() !== expected)
|
|
126
|
+
throw new Error('BESZEL_SOURCE_MISMATCH');
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
async function main(): Promise<Check[]> {
|
|
130
|
+
let raw = '';
|
|
131
|
+
for await (const chunk of process.stdin) {
|
|
132
|
+
raw += String(chunk);
|
|
133
|
+
if (raw.length > 16_384) throw new Error('BESZEL_INPUT_LIMIT');
|
|
134
|
+
}
|
|
135
|
+
return applyAgent(JSON.parse(raw), process.cwd(), runCommand);
|
|
136
|
+
}
|
|
137
|
+
/** Turns a known refusal into structured evidence; anything else is redacted. */
|
|
138
|
+
export function refusal(error: unknown): Check[] {
|
|
139
|
+
const code = error instanceof Error ? error.message : '';
|
|
140
|
+
const action = REFUSALS[code];
|
|
141
|
+
return [
|
|
142
|
+
action
|
|
143
|
+
? {
|
|
144
|
+
id: 'beszel:operation',
|
|
145
|
+
status: 'fail',
|
|
146
|
+
summary: code,
|
|
147
|
+
remedy: { action },
|
|
148
|
+
}
|
|
149
|
+
: {
|
|
150
|
+
id: 'beszel:operation',
|
|
151
|
+
status: 'fail',
|
|
152
|
+
summary: 'BESZEL_OPERATION_INCOMPLETE',
|
|
153
|
+
},
|
|
154
|
+
];
|
|
155
|
+
}
|
|
156
|
+
const REFUSALS: Record<string, string> = {
|
|
157
|
+
BESZEL_NOT_HUB:
|
|
158
|
+
'The Beszel hub runs only on the manager. Workers run only the agent; converge the hub from the manager with fleet-sync.',
|
|
159
|
+
BESZEL_NOT_ENROLLED:
|
|
160
|
+
'Declare this machine in fleet.json (the manager or a worker) before enrolling monitoring.',
|
|
161
|
+
BESZEL_SOURCE_MISMATCH:
|
|
162
|
+
'The checkout is not the clean, expected revision. Rerun fleet-sync so the reviewed source is installed first.',
|
|
163
|
+
BESZEL_HUB_MISMATCH:
|
|
164
|
+
"Agents connect only to the manager's private hub URL from fleet.json. Rerun fleet-sync from an up-to-date checkout.",
|
|
165
|
+
BESZEL_CREDENTIALS_REQUIRED:
|
|
166
|
+
'Agent enrollment credentials were not delivered. Rerun fleet-sync from the manager.',
|
|
167
|
+
};
|
|
168
|
+
if (import.meta.main) {
|
|
169
|
+
const checks = await main().catch(refusal);
|
|
170
|
+
process.stdout.write(JSON.stringify({ version: 1, checks }));
|
|
171
|
+
}
|