@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,73 @@
|
|
|
1
|
+
import { userInfo } from 'node:os';
|
|
2
|
+
import { resolve } from 'node:path';
|
|
3
|
+
import macos from '../../ansible/macos/policy.json';
|
|
4
|
+
import { runAnsibleWithReceipt } from '../platform/ansible-receipt';
|
|
5
|
+
import { privateHub } from './beszel';
|
|
6
|
+
import type { AgentEnrollment } from './enroll';
|
|
7
|
+
import type { Check, Runner } from '../types';
|
|
8
|
+
|
|
9
|
+
/** Installs the manager's own Beszel agent locally (never over SSH) as a
|
|
10
|
+
* per-user launchd agent, so the manager is always a system on its own hub.
|
|
11
|
+
* No sudo: fleet-sync on the manager stays unattended.
|
|
12
|
+
*/
|
|
13
|
+
export async function installManagerAgent(
|
|
14
|
+
root: string,
|
|
15
|
+
{ node, enrollment }: { node: string; enrollment: AgentEnrollment },
|
|
16
|
+
run: Runner,
|
|
17
|
+
{ uid = userInfo().uid, directory }: { uid?: number; directory?: string } = {}
|
|
18
|
+
): Promise<Check[]> {
|
|
19
|
+
const monitoring = resolve(root, 'ansible/monitoring');
|
|
20
|
+
const result = await runAnsibleWithReceipt(
|
|
21
|
+
run,
|
|
22
|
+
'agent-darwin.yml',
|
|
23
|
+
{
|
|
24
|
+
cwd: monitoring,
|
|
25
|
+
timeoutMs: 300_000,
|
|
26
|
+
input: JSON.stringify({
|
|
27
|
+
beszel_node: node,
|
|
28
|
+
beszel_hub_url: privateHub(enrollment.hubUrl),
|
|
29
|
+
beszel_public_key: enrollment.publicKey,
|
|
30
|
+
beszel_agent_token: enrollment.token,
|
|
31
|
+
}),
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
name: 'beszel-manager-agent',
|
|
35
|
+
sources: [monitoring, resolve(root, 'ansible/macos')],
|
|
36
|
+
verify: async () => agentRunning(run, uid),
|
|
37
|
+
...(directory ? { directory } : {}),
|
|
38
|
+
}
|
|
39
|
+
);
|
|
40
|
+
if (!result.ok)
|
|
41
|
+
return [
|
|
42
|
+
{
|
|
43
|
+
id: 'beszel:installation',
|
|
44
|
+
status: 'fail',
|
|
45
|
+
summary: 'BESZEL_INSTALLATION_INCOMPLETE',
|
|
46
|
+
remedy: {
|
|
47
|
+
action:
|
|
48
|
+
'Inspect signed artifact access, free disk and ~/Library/LaunchAgents on the manager. A leftover root daemon must be removed once (sudo launchctl bootout system/' +
|
|
49
|
+
macos.beszelDaemon +
|
|
50
|
+
'). Agent data under ~/Library/Application Support/personal-beszel-agent is preserved.',
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
];
|
|
54
|
+
const running = await agentRunning(run, uid);
|
|
55
|
+
return [
|
|
56
|
+
{
|
|
57
|
+
id: 'beszel:service',
|
|
58
|
+
status: running ? 'pass' : 'fail',
|
|
59
|
+
summary: running
|
|
60
|
+
? 'The manager runs its own Beszel agent as an always-on launch agent; hub metrics remain a separate gate.'
|
|
61
|
+
: 'BESZEL_SERVICE_STOPPED',
|
|
62
|
+
},
|
|
63
|
+
];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function agentRunning(run: Runner, uid: number): Promise<boolean> {
|
|
67
|
+
const state = await run([
|
|
68
|
+
'/bin/launchctl',
|
|
69
|
+
'print',
|
|
70
|
+
`gui/${uid}/${macos.beszelDaemon}`,
|
|
71
|
+
]);
|
|
72
|
+
return state.ok && /state = running/.test(state.stdout);
|
|
73
|
+
}
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { delimiter, join, resolve } from 'node:path';
|
|
4
|
+
import { runAnsibleWithReceipt } from '../platform/ansible-receipt';
|
|
5
|
+
import { failure, inspectRoute, publicEndpointCheck, tunnelToken } from './hub';
|
|
6
|
+
import { BESZEL_PUBLIC_VIEWER_IDENTITY, fleetAccount } from './hub-access';
|
|
7
|
+
import { ensurePublicViewer, type HubLogin } from './hub-viewer';
|
|
8
|
+
import type { FleetDefinition } from '../definition';
|
|
9
|
+
import { hostId, hubUrl } from '../topology';
|
|
10
|
+
import { PUBLIC_HUB_URL, privateHub } from './beszel';
|
|
11
|
+
import type { Check, Runner } from '../types';
|
|
12
|
+
|
|
13
|
+
/** Bundled CLI of the Tailscale macOS app when no `tailscale` is on PATH. */
|
|
14
|
+
export const TAILSCALE_APP_CLI =
|
|
15
|
+
'/Applications/Tailscale.app/Contents/MacOS/Tailscale';
|
|
16
|
+
const LOCAL_HUB = 'http://127.0.0.1:8090';
|
|
17
|
+
|
|
18
|
+
/** Per-user hub paths on the manager. Data stays in the human's home (and its
|
|
19
|
+
* normal backups); it is never copied to a worker.
|
|
20
|
+
*/
|
|
21
|
+
export function managerHubPaths(home = homedir()): {
|
|
22
|
+
data: string;
|
|
23
|
+
tunnel: string;
|
|
24
|
+
} {
|
|
25
|
+
const support = join(home, 'Library', 'Application Support');
|
|
26
|
+
return {
|
|
27
|
+
data: join(support, 'personal-beszel'),
|
|
28
|
+
tunnel: join(support, 'personal-beszel-tunnel'),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** `tailscale` on PATH (Homebrew or the app's installed CLI), else the app
|
|
33
|
+
* bundle's executable; null when neither exists.
|
|
34
|
+
*/
|
|
35
|
+
export function tailscaleCommand(
|
|
36
|
+
path = process.env['PATH'] ?? '',
|
|
37
|
+
exists: (candidate: string) => boolean = existsSync
|
|
38
|
+
): string | null {
|
|
39
|
+
for (const directory of path.split(delimiter).filter(Boolean)) {
|
|
40
|
+
const candidate = join(directory, 'tailscale');
|
|
41
|
+
if (exists(candidate)) return candidate;
|
|
42
|
+
}
|
|
43
|
+
return exists(TAILSCALE_APP_CLI) ? TAILSCALE_APP_CLI : null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface ManagerHubOptions {
|
|
47
|
+
home?: string;
|
|
48
|
+
tailscale?: string | null;
|
|
49
|
+
/** Fleet login for a fresh hub; defaults to the manager's Vault session. */
|
|
50
|
+
account?: () => Promise<HubLogin | null>;
|
|
51
|
+
/** Receipt directory for skipping an unchanged hub apply. */
|
|
52
|
+
receipts?: string;
|
|
53
|
+
/** Ensures the read-only AUTO_LOGIN user; defaults to the loopback hub API. */
|
|
54
|
+
viewer?: (login: HubLogin | null) => Promise<Check>;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Converges the singleton Beszel hub locally on the manager: pinned Compose
|
|
58
|
+
* under Docker Desktop, the connector token from the manager's own Vault
|
|
59
|
+
* session (private variables file, never argv or output), a private Tailscale
|
|
60
|
+
* Serve route, then independent health checks locally and over the tailnet.
|
|
61
|
+
*/
|
|
62
|
+
export async function convergeManagerHub(
|
|
63
|
+
root: string,
|
|
64
|
+
definition: FleetDefinition,
|
|
65
|
+
run: Runner,
|
|
66
|
+
options: ManagerHubOptions = {}
|
|
67
|
+
): Promise<Check[]> {
|
|
68
|
+
const hub = privateHub(hubUrl(definition));
|
|
69
|
+
const refused = await preconditions(definition, run);
|
|
70
|
+
if (refused) return refused;
|
|
71
|
+
const paths = managerHubPaths(options.home);
|
|
72
|
+
const tunnel = await tunnelToken(run);
|
|
73
|
+
const account = await (options.account ?? fleetAccount)();
|
|
74
|
+
const monitoring = resolve(root, 'ansible/monitoring');
|
|
75
|
+
const result = await runAnsibleWithReceipt(
|
|
76
|
+
run,
|
|
77
|
+
'hub-darwin.yml',
|
|
78
|
+
{
|
|
79
|
+
cwd: monitoring,
|
|
80
|
+
input: JSON.stringify(hubVariables(hub, paths, tunnel, account)),
|
|
81
|
+
timeoutMs: 300_000,
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
name: 'beszel-hub',
|
|
85
|
+
sources: [monitoring],
|
|
86
|
+
// Health is verified independently below on every run.
|
|
87
|
+
verify: async () =>
|
|
88
|
+
(
|
|
89
|
+
await run([
|
|
90
|
+
'curl',
|
|
91
|
+
'-fsS',
|
|
92
|
+
'--max-time',
|
|
93
|
+
'5',
|
|
94
|
+
'-o',
|
|
95
|
+
'/dev/null',
|
|
96
|
+
`${LOCAL_HUB}/api/health`,
|
|
97
|
+
])
|
|
98
|
+
).ok,
|
|
99
|
+
...(options.receipts ? { directory: options.receipts } : {}),
|
|
100
|
+
}
|
|
101
|
+
);
|
|
102
|
+
if (!result.ok)
|
|
103
|
+
return failure(
|
|
104
|
+
'BESZEL_HUB_INSTALLATION_INCOMPLETE',
|
|
105
|
+
'Inspect Docker Desktop, the Compose image, file sharing for ~/Library/Application Support and disk space. Existing data is never deleted.'
|
|
106
|
+
);
|
|
107
|
+
const route = await servePrivateRoute(
|
|
108
|
+
hub,
|
|
109
|
+
run,
|
|
110
|
+
options.tailscale === undefined ? tailscaleCommand() : options.tailscale
|
|
111
|
+
);
|
|
112
|
+
if (route) return route;
|
|
113
|
+
const health = await verifyHealth(hub, run);
|
|
114
|
+
if (health) return health;
|
|
115
|
+
return [
|
|
116
|
+
{
|
|
117
|
+
id: 'beszel:hub',
|
|
118
|
+
status: 'pass',
|
|
119
|
+
summary: `Pinned hub on the manager is healthy locally and at ${hub}; authentication and agent metrics are separate checks.`,
|
|
120
|
+
},
|
|
121
|
+
publicEndpointCheck(tunnel !== null),
|
|
122
|
+
await ensureViewer(options, account),
|
|
123
|
+
];
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function hubVariables(
|
|
127
|
+
hub: string,
|
|
128
|
+
paths: { data: string; tunnel: string },
|
|
129
|
+
tunnel: string | null,
|
|
130
|
+
account: HubLogin | null
|
|
131
|
+
): Record<string, string> {
|
|
132
|
+
return {
|
|
133
|
+
beszel_hub_url: hub,
|
|
134
|
+
beszel_public_url: PUBLIC_HUB_URL,
|
|
135
|
+
beszel_auto_login: BESZEL_PUBLIC_VIEWER_IDENTITY,
|
|
136
|
+
beszel_data_dir: paths.data,
|
|
137
|
+
beszel_tunnel_dir: paths.tunnel,
|
|
138
|
+
...(tunnel ? { beszel_tunnel_token: tunnel } : {}),
|
|
139
|
+
...(account
|
|
140
|
+
? {
|
|
141
|
+
beszel_fleet_identity: account.identity,
|
|
142
|
+
beszel_fleet_password: account.password,
|
|
143
|
+
}
|
|
144
|
+
: {}),
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function ensureViewer(
|
|
149
|
+
options: ManagerHubOptions,
|
|
150
|
+
account: HubLogin | null
|
|
151
|
+
): Promise<Check> {
|
|
152
|
+
return (options.viewer ?? ensurePublicViewer)(account);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** Runs only on the manager, and only with Docker Desktop's engine up. */
|
|
156
|
+
async function preconditions(
|
|
157
|
+
definition: FleetDefinition,
|
|
158
|
+
run: Runner
|
|
159
|
+
): Promise<Check[] | null> {
|
|
160
|
+
const local = await run(['/usr/sbin/scutil', '--get', 'LocalHostName']);
|
|
161
|
+
if (!local.ok || hostId(local.stdout.trim()) !== definition.manager.id)
|
|
162
|
+
return failure(
|
|
163
|
+
'BESZEL_NOT_MANAGER',
|
|
164
|
+
`The Beszel hub runs only on the manager (${definition.manager.id}). Run fleet-sync from the manager; workers only run the agent.`
|
|
165
|
+
);
|
|
166
|
+
const docker = await run(
|
|
167
|
+
['docker', 'info', '--format', '{{.ServerVersion}}'],
|
|
168
|
+
{ timeoutMs: 20_000 }
|
|
169
|
+
);
|
|
170
|
+
if (!docker.ok || docker.stdout.trim() === '')
|
|
171
|
+
return failure(
|
|
172
|
+
'BESZEL_DOCKER_DESKTOP_REQUIRED',
|
|
173
|
+
'Start Docker Desktop on the manager (and enable "Start Docker Desktop when you sign in"), then rerun fleet-sync. Existing hub data is never deleted.'
|
|
174
|
+
);
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
async function servePrivateRoute(
|
|
179
|
+
hub: string,
|
|
180
|
+
run: Runner,
|
|
181
|
+
cli: string | null
|
|
182
|
+
): Promise<Check[] | null> {
|
|
183
|
+
if (!cli)
|
|
184
|
+
return failure(
|
|
185
|
+
'BESZEL_TAILSCALE_REQUIRED',
|
|
186
|
+
`Install and sign in to Tailscale on the manager (Homebrew CLI or ${TAILSCALE_APP_CLI}).`
|
|
187
|
+
);
|
|
188
|
+
const status = await run([cli, 'serve', 'status', '--json']);
|
|
189
|
+
if (!status.ok)
|
|
190
|
+
return failure(
|
|
191
|
+
'BESZEL_PRIVATE_ROUTE_UNVERIFIED',
|
|
192
|
+
'Inspect Tailscale Serve permission and existing private routes before retry.'
|
|
193
|
+
);
|
|
194
|
+
const route = inspectRoute(status.stdout, hub);
|
|
195
|
+
if (Array.isArray(route)) return route;
|
|
196
|
+
if (!route) {
|
|
197
|
+
const serve = [cli, 'serve', '--bg', '--https=8443', LOCAL_HUB] as const;
|
|
198
|
+
const applied =
|
|
199
|
+
(await run([...serve], { timeoutMs: 20_000 })).ok ||
|
|
200
|
+
(await run(['sudo', '-n', ...serve], { timeoutMs: 20_000 })).ok;
|
|
201
|
+
if (!applied)
|
|
202
|
+
return failure(
|
|
203
|
+
'BESZEL_PRIVATE_ROUTE_REQUIRED',
|
|
204
|
+
'Allow Tailscale Serve (HTTPS certificates) for this tailnet and approve it in the Tailscale app if asked; no tailnet policy or public ingress is changed.'
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
const verified = await run([cli, 'serve', 'status', '--json']);
|
|
208
|
+
if (!verified.ok || inspectRoute(verified.stdout, hub) !== true)
|
|
209
|
+
return failure(
|
|
210
|
+
'BESZEL_PRIVATE_ROUTE_UNVERIFIED',
|
|
211
|
+
'The requested route was not independently observed after application. Inspect Serve permissions and configuration; installer success is insufficient.'
|
|
212
|
+
);
|
|
213
|
+
return null;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
async function verifyHealth(hub: string, run: Runner): Promise<Check[] | null> {
|
|
217
|
+
const probe = (url: string) =>
|
|
218
|
+
run(['curl', '-fsS', '--max-time', '10', '-o', '/dev/null', url], {
|
|
219
|
+
timeoutMs: 15_000,
|
|
220
|
+
});
|
|
221
|
+
if (!(await probe(`${LOCAL_HUB}/api/health`)).ok)
|
|
222
|
+
return failure(
|
|
223
|
+
'BESZEL_HUB_UNHEALTHY',
|
|
224
|
+
'The hub container did not answer /api/health locally. Inspect Docker Desktop and the personal-beszel Compose project; preserve its data.'
|
|
225
|
+
);
|
|
226
|
+
if (!(await probe(`${hub}/api/health`)).ok)
|
|
227
|
+
return failure(
|
|
228
|
+
'BESZEL_HUB_UNREACHABLE',
|
|
229
|
+
`The hub is healthy locally but not at ${hub}. Inspect Tailscale on the manager and its Serve route.`
|
|
230
|
+
);
|
|
231
|
+
return null;
|
|
232
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { Check, FleetNode, Runner } from '../types';
|
|
2
|
+
import { shellQuote, sshArgs, sshFailure } from '../ssh';
|
|
3
|
+
import { validCheck, cleanCheck } from '../protocol';
|
|
4
|
+
import { record } from '../config';
|
|
5
|
+
import { isRevision } from '../machine/state';
|
|
6
|
+
import type { AgentEnrollment } from './enroll';
|
|
7
|
+
import { remoteLockPrelude } from '../remote-lock';
|
|
8
|
+
|
|
9
|
+
/** Installs the Beszel agent on a worker. Only node-specific agent credentials
|
|
10
|
+
* cross SSH; hub account credentials stay on the manager. Workers never host
|
|
11
|
+
* the hub (it runs only on the manager), so there is no remote hub mode.
|
|
12
|
+
* The installed source and remote operation lock are reused.
|
|
13
|
+
*/
|
|
14
|
+
export async function installRemoteAgent(
|
|
15
|
+
node: FleetNode,
|
|
16
|
+
revision: string,
|
|
17
|
+
enrollment: AgentEnrollment,
|
|
18
|
+
run: Runner
|
|
19
|
+
): Promise<Check[]> {
|
|
20
|
+
if (!isRevision(revision)) throw new Error('BESZEL_REVISION');
|
|
21
|
+
const root = `"$HOME/.local/share/personal/fleet/revisions/${revision}"`;
|
|
22
|
+
const locked = shellQuote(
|
|
23
|
+
JSON.stringify({
|
|
24
|
+
version: 1,
|
|
25
|
+
checks: [
|
|
26
|
+
{
|
|
27
|
+
id: 'beszel:lock',
|
|
28
|
+
status: 'fail',
|
|
29
|
+
summary: 'FLEET_REMOTE_LOCKED',
|
|
30
|
+
remedy: {
|
|
31
|
+
action:
|
|
32
|
+
'Another remote operation owns the lock. Inspect its state and retry after it finishes; never delete the lock or start a competing installer.',
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
],
|
|
36
|
+
})
|
|
37
|
+
);
|
|
38
|
+
const command = `cd ${root} && { ${remoteLockPrelude()} || { printf '%s\\n' ${locked}; exit 0; }; } && trap '' HUP && exec "$HOME/.local/bin/mise" exec --locked bun -- bun ${shellQuote('src/monitoring/apply.ts')}`;
|
|
39
|
+
const result = await run([...sshArgs(node), command], {
|
|
40
|
+
input: JSON.stringify({ ...enrollment, node: node.id, revision }),
|
|
41
|
+
timeoutMs: 360_000,
|
|
42
|
+
});
|
|
43
|
+
if (!result.ok) return [sshFailure(result)];
|
|
44
|
+
try {
|
|
45
|
+
const value: unknown = JSON.parse(result.stdout);
|
|
46
|
+
if (
|
|
47
|
+
record(value) &&
|
|
48
|
+
value['version'] === 1 &&
|
|
49
|
+
Array.isArray(value['checks']) &&
|
|
50
|
+
value['checks'].length > 0 &&
|
|
51
|
+
value['checks'].length <= 20 &&
|
|
52
|
+
value['checks'].every(validCheck)
|
|
53
|
+
)
|
|
54
|
+
return value['checks'].map(cleanCheck);
|
|
55
|
+
} catch {
|
|
56
|
+
/* Never expose native output or credentials. */
|
|
57
|
+
}
|
|
58
|
+
return [
|
|
59
|
+
{
|
|
60
|
+
id: 'beszel:protocol',
|
|
61
|
+
status: 'fail',
|
|
62
|
+
summary: 'BESZEL_REMOTE_EVIDENCE_INVALID',
|
|
63
|
+
},
|
|
64
|
+
];
|
|
65
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { type FleetDefinition, type MachineDefinition } from './definition';
|
|
2
|
+
import { record } from './config';
|
|
3
|
+
import { FleetUsageError } from './errors';
|
|
4
|
+
|
|
5
|
+
export interface NetworkHost {
|
|
6
|
+
alias: string;
|
|
7
|
+
tailscaleName: string;
|
|
8
|
+
sshPort: number;
|
|
9
|
+
user: string;
|
|
10
|
+
identityFile: string;
|
|
11
|
+
os: 'linux' | 'macOS' | 'windows';
|
|
12
|
+
t3Port: number;
|
|
13
|
+
}
|
|
14
|
+
export function networkProjection(
|
|
15
|
+
definition: FleetDefinition,
|
|
16
|
+
identities: Record<string, string>
|
|
17
|
+
): NetworkHost[] {
|
|
18
|
+
return definition.workers.flatMap((node) =>
|
|
19
|
+
node.network
|
|
20
|
+
? [
|
|
21
|
+
{
|
|
22
|
+
alias: node.ssh,
|
|
23
|
+
...node.network,
|
|
24
|
+
os: node.os,
|
|
25
|
+
identityFile: identities[node.id] ?? '',
|
|
26
|
+
},
|
|
27
|
+
]
|
|
28
|
+
: []
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
function compatible(
|
|
32
|
+
node: MachineDefinition | undefined,
|
|
33
|
+
host: Record<string, unknown>
|
|
34
|
+
): node is MachineDefinition {
|
|
35
|
+
return Boolean(
|
|
36
|
+
node?.network &&
|
|
37
|
+
Object.entries(node.network).every(([key, value]) => host[key] === value) &&
|
|
38
|
+
node.os === host['os']
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
/** Immutable migration input supplies old manager paths only, never desired hosts. */
|
|
42
|
+
export function legacyIdentities(
|
|
43
|
+
definition: FleetDefinition,
|
|
44
|
+
raw: unknown
|
|
45
|
+
): Record<string, string> {
|
|
46
|
+
if (!record(raw) || !Array.isArray(raw['hosts']))
|
|
47
|
+
throw new FleetUsageError('Invalid legacy Workstation inventory.');
|
|
48
|
+
const result: Record<string, string> = {};
|
|
49
|
+
for (const host of raw['hosts']) {
|
|
50
|
+
if (!record(host))
|
|
51
|
+
throw new FleetUsageError('Invalid legacy network host.');
|
|
52
|
+
const node = definition.workers.find((node) => node.ssh === host['alias']);
|
|
53
|
+
// A retired host has no manager identity to migrate into this fleet.
|
|
54
|
+
if (!node) continue;
|
|
55
|
+
if (!compatible(node, host))
|
|
56
|
+
throw new FleetUsageError(
|
|
57
|
+
'Legacy Workstation and canonical Fleet definitions conflict. Review IDs, aliases and network settings before migration.'
|
|
58
|
+
);
|
|
59
|
+
const identity = host['identityFile'];
|
|
60
|
+
if (
|
|
61
|
+
typeof identity !== 'string' ||
|
|
62
|
+
!/^(~\/|\/)[^\x00-\x1f\x7f]+$/.test(identity)
|
|
63
|
+
)
|
|
64
|
+
throw new FleetUsageError('Legacy identity path is invalid.');
|
|
65
|
+
if (result[node.id] !== undefined)
|
|
66
|
+
throw new FleetUsageError('Duplicate legacy machine assignment.');
|
|
67
|
+
result[node.id] = identity;
|
|
68
|
+
}
|
|
69
|
+
return result;
|
|
70
|
+
}
|
|
71
|
+
/** `legacy` is the consumer's earlier inventory, when it has one: it only
|
|
72
|
+
* supplies identity paths the fleet configuration does not hold.
|
|
73
|
+
*/
|
|
74
|
+
export function loadNetworkDefinition(
|
|
75
|
+
definition: FleetDefinition,
|
|
76
|
+
legacy: unknown,
|
|
77
|
+
overrides?: Record<string, string>
|
|
78
|
+
): NetworkHost[] {
|
|
79
|
+
return networkProjection(
|
|
80
|
+
definition,
|
|
81
|
+
overrides ?? legacyIdentities(definition, legacy)
|
|
82
|
+
);
|
|
83
|
+
}
|