@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,25 @@
|
|
|
1
|
+
export interface CommandSpec {
|
|
2
|
+
id: string;
|
|
3
|
+
title: string;
|
|
4
|
+
description: string;
|
|
5
|
+
group: string;
|
|
6
|
+
keywords: string[];
|
|
7
|
+
args: string[];
|
|
8
|
+
examples: string[];
|
|
9
|
+
mutation: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface CommandOutcome {
|
|
13
|
+
exitCode: number;
|
|
14
|
+
data: unknown;
|
|
15
|
+
text?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface InteractiveOptions {
|
|
19
|
+
version: string;
|
|
20
|
+
releasedAt: string | null;
|
|
21
|
+
workspace: string;
|
|
22
|
+
config: string;
|
|
23
|
+
commands: CommandSpec[];
|
|
24
|
+
execute: (args: string[]) => Promise<CommandOutcome>;
|
|
25
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { Fingerprint, Snapshot } from '../types';
|
|
2
|
+
|
|
3
|
+
declare const PERSONAL_FLEET_REFERENCE: Fingerprint | null | undefined;
|
|
4
|
+
/** Published health carries safe input hashes; it needs no manager checkout or Bun. */
|
|
5
|
+
export function fleetReference(): Snapshot | undefined {
|
|
6
|
+
if (
|
|
7
|
+
typeof PERSONAL_FLEET_REFERENCE === 'undefined' ||
|
|
8
|
+
!PERSONAL_FLEET_REFERENCE?.head
|
|
9
|
+
)
|
|
10
|
+
return undefined;
|
|
11
|
+
return {
|
|
12
|
+
version: 1,
|
|
13
|
+
platform: 'declared-worker-policy',
|
|
14
|
+
fingerprint: PERSONAL_FLEET_REFERENCE,
|
|
15
|
+
checks: [],
|
|
16
|
+
};
|
|
17
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { trackedScript } from '../remote-bootstrap';
|
|
3
|
+
import type { Runner } from '../types';
|
|
4
|
+
import type { CommandOutcome } from './contracts';
|
|
5
|
+
import type { FleetSyncOptions } from './fleet-sync';
|
|
6
|
+
import { forwardedFlags, type ForwardSpawn } from './fleet-sync-forward';
|
|
7
|
+
|
|
8
|
+
/** Exported by `fleet-manager-run.sh`: this process already runs from the
|
|
9
|
+
* fleet-owned checkout of that revision.
|
|
10
|
+
*/
|
|
11
|
+
export const RUN_CHECKOUT_ENV = 'PERSONAL_FLEET_RUN_CHECKOUT';
|
|
12
|
+
|
|
13
|
+
export const inRunCheckout = (revision: string): boolean =>
|
|
14
|
+
process.env[RUN_CHECKOUT_ENV] === revision;
|
|
15
|
+
|
|
16
|
+
/** The manager converges from a clean, fleet-owned checkout of the exact
|
|
17
|
+
* revision, like every worker, never from the checkout fleet-sync was started
|
|
18
|
+
* in: that one is a working tree whose uncommitted edits, branch switches or
|
|
19
|
+
* removal would otherwise decide whether a sync (and every provider it
|
|
20
|
+
* installs) works. Null when the revision has no tracked run script.
|
|
21
|
+
*/
|
|
22
|
+
export async function runFromCheckout(
|
|
23
|
+
options: FleetSyncOptions,
|
|
24
|
+
source: { revision: string; workspace: string },
|
|
25
|
+
run: Runner,
|
|
26
|
+
spawnLocal: ForwardSpawn = spawnInherited
|
|
27
|
+
): Promise<CommandOutcome | null> {
|
|
28
|
+
const { revision } = source;
|
|
29
|
+
const script = await trackedScript(
|
|
30
|
+
source.workspace,
|
|
31
|
+
revision,
|
|
32
|
+
'bootstrap/fleet-manager-run.sh',
|
|
33
|
+
run
|
|
34
|
+
);
|
|
35
|
+
if (!script) return null;
|
|
36
|
+
// The run script takes the revision from this clone.
|
|
37
|
+
process.env['DEVFLEET_REPO'] ||= source.workspace;
|
|
38
|
+
const result = await spawnLocal([
|
|
39
|
+
'sh',
|
|
40
|
+
'-c',
|
|
41
|
+
script,
|
|
42
|
+
'fleet-manager-run',
|
|
43
|
+
revision,
|
|
44
|
+
...forwardedFlags(options, undefined),
|
|
45
|
+
]);
|
|
46
|
+
const stdout = result.stdout.trim();
|
|
47
|
+
if (options.json) {
|
|
48
|
+
try {
|
|
49
|
+
return { exitCode: result.exitCode, data: JSON.parse(stdout) as unknown };
|
|
50
|
+
} catch {
|
|
51
|
+
/* Fall through: the run checkout could not start; its reason is on stderr. */
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return {
|
|
55
|
+
exitCode: result.exitCode,
|
|
56
|
+
data: { version: 1, revision, exitCode: result.exitCode },
|
|
57
|
+
text: stdout,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Null when this process is already the run checkout (or a test, which
|
|
62
|
+
* never leaves its own process: the same rule as the worker hand-off).
|
|
63
|
+
*/
|
|
64
|
+
export async function rehome(
|
|
65
|
+
options: FleetSyncOptions,
|
|
66
|
+
source: { revision: string; workspace: string },
|
|
67
|
+
run: Runner
|
|
68
|
+
): Promise<CommandOutcome | null> {
|
|
69
|
+
if (inRunCheckout(source.revision) || process.env['NODE_ENV'] === 'test')
|
|
70
|
+
return null;
|
|
71
|
+
return runFromCheckout(options, source, run);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Prompts and progress stay on the terminal; the report is returned. */
|
|
75
|
+
function spawnInherited(
|
|
76
|
+
command: string[]
|
|
77
|
+
): Promise<{ exitCode: number; stdout: string }> {
|
|
78
|
+
return new Promise((resolve) => {
|
|
79
|
+
const color = process.stdout.isTTY && !process.env['NO_COLOR'];
|
|
80
|
+
const child = spawn(command[0]!, command.slice(1), {
|
|
81
|
+
stdio: ['inherit', 'pipe', 'inherit'],
|
|
82
|
+
env: { ...process.env, ...(color ? { FORCE_COLOR: '1' } : {}) },
|
|
83
|
+
});
|
|
84
|
+
let stdout = '';
|
|
85
|
+
child.stdout.setEncoding('utf8').on('data', (chunk: string) => {
|
|
86
|
+
stdout += chunk;
|
|
87
|
+
});
|
|
88
|
+
child.on('error', () => resolve({ exitCode: 1, stdout: '' }));
|
|
89
|
+
child.on('close', (code) => resolve({ exitCode: code ?? 1, stdout }));
|
|
90
|
+
});
|
|
91
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { readFile, mkdir, writeFile, rename, rm } from 'node:fs/promises';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import { loadConfig } from '../config';
|
|
4
|
+
import { enrolledRoster } from '../definition';
|
|
5
|
+
import { FleetUsageError } from '../errors';
|
|
6
|
+
import { dirname } from 'node:path';
|
|
7
|
+
import { fleet as definition } from '../fleet-config';
|
|
8
|
+
import { discoverTailnet } from '../tailscale';
|
|
9
|
+
import { matchComputers } from '../tailnet-matching';
|
|
10
|
+
import { sshArgs, sshFailure } from '../ssh';
|
|
11
|
+
import { ensurePrivateRoot } from '../machine/state';
|
|
12
|
+
import type { FleetNode, Runner } from '../types';
|
|
13
|
+
|
|
14
|
+
/** Interactive enrollment/resume only. Existing paths and explicit identities
|
|
15
|
+
* are preserved. Tailnet candidates never enter the declared membership.
|
|
16
|
+
*/
|
|
17
|
+
export async function enrollManager(path: string, run: Runner): Promise<void> {
|
|
18
|
+
const canonical = definition;
|
|
19
|
+
const original = await existingEnrollment(path);
|
|
20
|
+
const saved =
|
|
21
|
+
original === null
|
|
22
|
+
? []
|
|
23
|
+
: enrolledRoster(canonical, await loadConfig(path)).nodes;
|
|
24
|
+
const nodes: FleetNode[] = canonical.workers.map((node) => ({
|
|
25
|
+
id: node.id,
|
|
26
|
+
ssh: node.ssh,
|
|
27
|
+
workspace: '/',
|
|
28
|
+
required: ['t3', 'toolchain', 'workstation'],
|
|
29
|
+
}));
|
|
30
|
+
const pending = nodes.filter(
|
|
31
|
+
(node) => !saved.find((entry) => entry.id === node.id)?.tailscaleId
|
|
32
|
+
);
|
|
33
|
+
if (!pending.length) return;
|
|
34
|
+
const tailnet = await discoverTailnet(run);
|
|
35
|
+
const matches = await matchComputers(nodes, tailnet.computers, run);
|
|
36
|
+
const enrolled: FleetNode[] = [];
|
|
37
|
+
for (const node of pending) {
|
|
38
|
+
const peers = tailnet.computers.filter((peer) =>
|
|
39
|
+
matches.get(peer.id)?.some((candidate) => candidate.id === node.id)
|
|
40
|
+
);
|
|
41
|
+
if (
|
|
42
|
+
ambiguous(
|
|
43
|
+
peers.map((peer) => peer.id),
|
|
44
|
+
matches
|
|
45
|
+
)
|
|
46
|
+
)
|
|
47
|
+
continue;
|
|
48
|
+
const connection = await inspectConnection(node, peers[0]?.id, run);
|
|
49
|
+
if (connection) enrolled.push(connection);
|
|
50
|
+
}
|
|
51
|
+
const merged = mergeEnrollment(saved, enrolled);
|
|
52
|
+
if (JSON.stringify(merged) === JSON.stringify(saved)) return;
|
|
53
|
+
// Only canonical nodes reached over host-key-verified SSH get here, so
|
|
54
|
+
// enrollment needs no extra confirmation; the list is still shown.
|
|
55
|
+
process.stderr.write(
|
|
56
|
+
`Enrolling verified SSH connections for canonical targets:\n${enrolled.map((node) => ` ${node.id} → ${node.ssh}; device ${enrollmentIdentity(node)}`).join('\n')}\nMissing targets remain visible and are never replaced by discovered peers.\n`
|
|
57
|
+
);
|
|
58
|
+
await ensurePrivateRoot(dirname(path));
|
|
59
|
+
await mkdir(dirname(path), { recursive: true, mode: 0o700 });
|
|
60
|
+
await persistEnrollment(path, original, merged);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function inspectConnection(
|
|
64
|
+
node: FleetNode,
|
|
65
|
+
peerId: string | undefined,
|
|
66
|
+
run: Runner
|
|
67
|
+
): Promise<FleetNode | null> {
|
|
68
|
+
const home = await run([...sshArgs(node), 'printf "%s" "$HOME"'], {
|
|
69
|
+
timeoutMs: 20_000,
|
|
70
|
+
});
|
|
71
|
+
if (!home.ok) {
|
|
72
|
+
const check = sshFailure(home);
|
|
73
|
+
process.stderr.write(
|
|
74
|
+
`[${node.id}] ${check.id}: ${check.summary} ${check.remedy?.action ?? ''}\n`
|
|
75
|
+
);
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
if (!/^\/[^\x00-\x1f\x7f]{1,4000}$/.test(home.stdout)) return null;
|
|
79
|
+
return {
|
|
80
|
+
...node,
|
|
81
|
+
workspace: `${home.stdout}/.local/share/personal/fleet`,
|
|
82
|
+
...(peerId ? { tailscaleId: peerId } : { bootstrapViaSsh: true }),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function enrollmentIdentity(node: FleetNode): string {
|
|
87
|
+
return (
|
|
88
|
+
node.tailscaleId ??
|
|
89
|
+
'initial trusted SSH bootstrap; native Tailscale login required'
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function ambiguous(ids: string[], matches: Map<string, FleetNode[]>): boolean {
|
|
94
|
+
return (
|
|
95
|
+
ids.length > 1 || (ids.length === 1 && matches.get(ids[0]!)?.length !== 1)
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
async function existingEnrollment(path: string): Promise<string | null> {
|
|
99
|
+
try {
|
|
100
|
+
return await readFile(path, 'utf8');
|
|
101
|
+
} catch (error) {
|
|
102
|
+
if ((error as NodeJS.ErrnoException).code === 'ENOENT') return null;
|
|
103
|
+
throw error;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
export function mergeEnrollment(
|
|
107
|
+
saved: FleetNode[],
|
|
108
|
+
discovered: FleetNode[]
|
|
109
|
+
): FleetNode[] {
|
|
110
|
+
const result = saved.map((node) => ({ ...node }));
|
|
111
|
+
for (const candidate of discovered) {
|
|
112
|
+
const existing = result.find((node) => node.id === candidate.id);
|
|
113
|
+
if (!existing) result.push(candidate);
|
|
114
|
+
else if (!existing.tailscaleId && candidate.tailscaleId) {
|
|
115
|
+
existing.tailscaleId = candidate.tailscaleId;
|
|
116
|
+
delete existing.bootstrapViaSsh;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return result;
|
|
120
|
+
}
|
|
121
|
+
async function persistEnrollment(
|
|
122
|
+
path: string,
|
|
123
|
+
original: string | null,
|
|
124
|
+
nodes: FleetNode[]
|
|
125
|
+
): Promise<void> {
|
|
126
|
+
const lock = `${path}.enroll.lock`;
|
|
127
|
+
await mkdir(lock, { mode: 0o700 });
|
|
128
|
+
const temporary = `${path}.${randomUUID()}.tmp`;
|
|
129
|
+
try {
|
|
130
|
+
if ((await existingEnrollment(path)) !== original)
|
|
131
|
+
throw new FleetUsageError(
|
|
132
|
+
'Enrollment changed during approval. Inspect it and rerun; no connection was overwritten.'
|
|
133
|
+
);
|
|
134
|
+
await writeFile(
|
|
135
|
+
temporary,
|
|
136
|
+
JSON.stringify({ version: 1, nodes }, null, 2) + '\n',
|
|
137
|
+
{ flag: 'wx', mode: 0o600 }
|
|
138
|
+
);
|
|
139
|
+
await rename(temporary, path);
|
|
140
|
+
} finally {
|
|
141
|
+
await rm(temporary, { force: true });
|
|
142
|
+
await rm(lock, { recursive: true });
|
|
143
|
+
}
|
|
144
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import type { FleetDefinition } from '../definition';
|
|
3
|
+
import { shellQuote, sshArgs } from '../ssh';
|
|
4
|
+
import type { FleetNode } from '../types';
|
|
5
|
+
import type { CommandOutcome } from './contracts';
|
|
6
|
+
import type { FleetSyncOptions } from './fleet-sync';
|
|
7
|
+
|
|
8
|
+
/** Set on the manager side of a hand-off (and in tests): never hand off again. */
|
|
9
|
+
export const NO_FORWARD_ENV = 'PERSONAL_FLEET_NO_FORWARD';
|
|
10
|
+
|
|
11
|
+
/** Streams the remote command: progress on stderr, the report on stdout. */
|
|
12
|
+
export type ForwardSpawn = (
|
|
13
|
+
command: string[]
|
|
14
|
+
) => Promise<{ exitCode: number; stdout: string }>;
|
|
15
|
+
|
|
16
|
+
export function forwardingDisabled(): boolean {
|
|
17
|
+
return Boolean(process.env[NO_FORWARD_ENV]);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** fleet-sync only ever runs on the manager. Started on a worker, it hands the
|
|
21
|
+
* same request to the manager over host-key-verified SSH (no agent forwarding,
|
|
22
|
+
* no credentials copied) and reports the manager's result unchanged.
|
|
23
|
+
*/
|
|
24
|
+
export async function forwardToManager(
|
|
25
|
+
definition: FleetDefinition,
|
|
26
|
+
options: FleetSyncOptions,
|
|
27
|
+
revision: string | undefined,
|
|
28
|
+
remote: { spawn?: ForwardSpawn | undefined; script?: string | null } = {}
|
|
29
|
+
): Promise<CommandOutcome> {
|
|
30
|
+
// A test must inject the spawn: it can never reach the real manager.
|
|
31
|
+
if (!remote.spawn && process.env['NODE_ENV'] === 'test')
|
|
32
|
+
throw new Error('FLEET_FORWARD_IN_TEST');
|
|
33
|
+
const spawnRemote = remote.spawn ?? spawnInherited;
|
|
34
|
+
const runScript = remote.script ?? null;
|
|
35
|
+
const { manager } = definition;
|
|
36
|
+
process.stderr.write(
|
|
37
|
+
`fleet-sync runs on the manager; handing off to ${manager.id} over SSH…\n`
|
|
38
|
+
);
|
|
39
|
+
const result = await spawnRemote(
|
|
40
|
+
managerCommand(definition, options, revision, runScript)
|
|
41
|
+
);
|
|
42
|
+
const stdout = result.stdout.trim();
|
|
43
|
+
if (options.json) {
|
|
44
|
+
try {
|
|
45
|
+
return { exitCode: result.exitCode, data: JSON.parse(stdout) as unknown };
|
|
46
|
+
} catch {
|
|
47
|
+
/* Fall through to the structured failure below. */
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
if (result.exitCode === 255 && !stdout)
|
|
51
|
+
return {
|
|
52
|
+
exitCode: 1,
|
|
53
|
+
data: {
|
|
54
|
+
version: 1,
|
|
55
|
+
ok: false,
|
|
56
|
+
code: 'FLEET_MANAGER_UNREACHABLE',
|
|
57
|
+
error: `Could not reach the manager (${manager.id}) over SSH. Wake it, check Tailscale and SSH (Remote Login), or run fleet-sync on the manager directly.`,
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
return {
|
|
61
|
+
exitCode: result.exitCode,
|
|
62
|
+
data: { version: 1, forwardedTo: manager.id, exitCode: result.exitCode },
|
|
63
|
+
text: stdout,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** With the revision's tracked `fleet-manager-run.sh`, the manager runs
|
|
68
|
+
* fleet-sync from a clean checkout of that exact revision; otherwise (no
|
|
69
|
+
* revision, or an older one without the script) its installed launcher.
|
|
70
|
+
*/
|
|
71
|
+
export function managerCommand(
|
|
72
|
+
definition: FleetDefinition,
|
|
73
|
+
options: FleetSyncOptions,
|
|
74
|
+
revision: string | undefined,
|
|
75
|
+
runScript: string | null = null
|
|
76
|
+
): string[] {
|
|
77
|
+
const { manager } = definition;
|
|
78
|
+
const target: FleetNode = {
|
|
79
|
+
id: manager.id,
|
|
80
|
+
ssh: `${manager.network.user}@${manager.network.tailscaleName}`,
|
|
81
|
+
workspace: '~',
|
|
82
|
+
required: [],
|
|
83
|
+
};
|
|
84
|
+
const args =
|
|
85
|
+
runScript && revision
|
|
86
|
+
? [
|
|
87
|
+
'sh',
|
|
88
|
+
'-c',
|
|
89
|
+
runScript,
|
|
90
|
+
'fleet-manager-run',
|
|
91
|
+
revision,
|
|
92
|
+
...forwardedFlags(options, undefined),
|
|
93
|
+
]
|
|
94
|
+
: ['personal', 'fleet-sync', ...forwardedFlags(options, revision)];
|
|
95
|
+
const color = process.stdout.isTTY && !process.env['NO_COLOR'];
|
|
96
|
+
// A login shell gives the manager's normal PATH (mise, Docker, Tailscale).
|
|
97
|
+
const script = `export ${NO_FORWARD_ENV}=1${color ? ' FORCE_COLOR=1' : ''}; exec ${args.map(shellQuote).join(' ')}`;
|
|
98
|
+
return [...sshArgs(target), `exec /bin/zsh -lc ${shellQuote(script)}`];
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function forwardedFlags(
|
|
102
|
+
options: FleetSyncOptions,
|
|
103
|
+
revision: string | undefined
|
|
104
|
+
): string[] {
|
|
105
|
+
const flags = revision ? ['--revision', revision] : [];
|
|
106
|
+
for (const node of options.node ?? []) flags.push('--node', node);
|
|
107
|
+
if (options.schedule) flags.push('--schedule', options.schedule);
|
|
108
|
+
const switches: [boolean | undefined, string][] = [
|
|
109
|
+
[options.all, '--all'],
|
|
110
|
+
[options.dryRun, '--dry-run'],
|
|
111
|
+
[options.apply, '--apply'],
|
|
112
|
+
[options.yes, '--yes'],
|
|
113
|
+
[options.json, '--json'],
|
|
114
|
+
[options.nonInteractive, '--non-interactive'],
|
|
115
|
+
];
|
|
116
|
+
for (const [on, flag] of switches) if (on) flags.push(flag);
|
|
117
|
+
return flags;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function spawnInherited(
|
|
121
|
+
command: string[]
|
|
122
|
+
): Promise<{ exitCode: number; stdout: string }> {
|
|
123
|
+
return new Promise((resolve) => {
|
|
124
|
+
const child = spawn(command[0]!, command.slice(1), {
|
|
125
|
+
stdio: ['ignore', 'pipe', 'inherit'],
|
|
126
|
+
});
|
|
127
|
+
let stdout = '';
|
|
128
|
+
child.stdout.setEncoding('utf8').on('data', (chunk: string) => {
|
|
129
|
+
stdout += chunk;
|
|
130
|
+
});
|
|
131
|
+
child.on('error', () => resolve({ exitCode: 255, stdout: '' }));
|
|
132
|
+
child.on('close', (code) => resolve({ exitCode: code ?? 1, stdout }));
|
|
133
|
+
});
|
|
134
|
+
}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { blocking, type Check, type Runner } from '../types';
|
|
2
|
+
import type { FleetDefinition } from '../definition';
|
|
3
|
+
import type { ManagerNode } from '../manager-sync';
|
|
4
|
+
import { hostId } from '../topology';
|
|
5
|
+
import { convergeProxy } from '../proxy-converge';
|
|
6
|
+
import { attempt } from '../node-converge-shared';
|
|
7
|
+
import { quotaLimited, verifyFleetProviders } from '../agents/fleet-verify';
|
|
8
|
+
import { verifyNativeAgents } from '../agents/native-verify';
|
|
9
|
+
import { convergeManagerHub } from '../monitoring/manager-hub';
|
|
10
|
+
import { enrollManagerMonitoring } from './fleet-sync-monitoring';
|
|
11
|
+
import { installConfig } from '../fleet-config';
|
|
12
|
+
import { machineProviders } from '../t3/providers';
|
|
13
|
+
import { providerNames } from '../t3/names-check';
|
|
14
|
+
|
|
15
|
+
/** The manager converges itself locally (never over SSH) before any worker:
|
|
16
|
+
* it must be the declared manager, stay awake on AC power, and host every
|
|
17
|
+
* singleton service (CLI proxy, Beszel hub). Workers only consume them.
|
|
18
|
+
*/
|
|
19
|
+
export async function convergeManager(
|
|
20
|
+
definition: FleetDefinition,
|
|
21
|
+
input: {
|
|
22
|
+
revision: string;
|
|
23
|
+
apply: boolean;
|
|
24
|
+
workspace: string;
|
|
25
|
+
},
|
|
26
|
+
run: Runner
|
|
27
|
+
): Promise<ManagerNode> {
|
|
28
|
+
const started = Date.now();
|
|
29
|
+
const id = definition.manager.id;
|
|
30
|
+
const identity = await managerIdentity(id, run);
|
|
31
|
+
const checks: Check[] = [identity];
|
|
32
|
+
if (identity.status === 'pass') {
|
|
33
|
+
// Launchers and scheduled jobs on the manager read the installed copy.
|
|
34
|
+
if (input.apply) installConfig();
|
|
35
|
+
checks.push(await managerPower(run));
|
|
36
|
+
checks.push(await machineProviders(id, input.workspace, input.apply));
|
|
37
|
+
checks.push(await providerNames(id, input.workspace, input.apply));
|
|
38
|
+
if (input.apply) {
|
|
39
|
+
// Same provider profiles and the same Vault gateway credential as every
|
|
40
|
+
// worker: the instances above launch from these profiles, and the check
|
|
41
|
+
// proves the credential works end to end through the shared proxy.
|
|
42
|
+
checks.push(
|
|
43
|
+
...(await attempt('configuration:apply', () =>
|
|
44
|
+
managerAgentProfiles(id, input.workspace, run)
|
|
45
|
+
))
|
|
46
|
+
);
|
|
47
|
+
const providers = await attempt('provider:verification', () =>
|
|
48
|
+
verifyFleetProviders(id)
|
|
49
|
+
);
|
|
50
|
+
checks.push(...providers);
|
|
51
|
+
// Same agents, versions and shared accounts as every worker.
|
|
52
|
+
checks.push(
|
|
53
|
+
...(await attempt('agent:verification', () =>
|
|
54
|
+
verifyNativeAgents(id, input.workspace, run, {
|
|
55
|
+
outOfQuota: quotaLimited(providers),
|
|
56
|
+
})
|
|
57
|
+
))
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
checks.push(
|
|
61
|
+
...(await convergeProxy(id, input.revision, run, {
|
|
62
|
+
apply: input.apply,
|
|
63
|
+
cwd: input.workspace,
|
|
64
|
+
}))
|
|
65
|
+
);
|
|
66
|
+
if (input.apply) {
|
|
67
|
+
const hub = await convergeManagerHub(input.workspace, definition, run);
|
|
68
|
+
checks.push(...hub);
|
|
69
|
+
// The manager is always a monitored system on its own hub.
|
|
70
|
+
if (!hub.some(blocking))
|
|
71
|
+
checks.push(
|
|
72
|
+
...(await enrollManagerMonitoring(definition, input.workspace, run))
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return {
|
|
77
|
+
node: id,
|
|
78
|
+
role: 'manager',
|
|
79
|
+
checks,
|
|
80
|
+
reachability: 'online',
|
|
81
|
+
desiredRevision: input.revision,
|
|
82
|
+
appliedRevision:
|
|
83
|
+
input.apply && !checks.some(blocking) ? input.revision : null,
|
|
84
|
+
durationMs: Date.now() - started,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const PROFILE_TOOLS = [
|
|
89
|
+
'codex',
|
|
90
|
+
'claude',
|
|
91
|
+
'gemini',
|
|
92
|
+
'opencode',
|
|
93
|
+
'grok',
|
|
94
|
+
'antigravity',
|
|
95
|
+
] as const;
|
|
96
|
+
|
|
97
|
+
/** Applies the proxy agent profiles the managed T3 instances launch from
|
|
98
|
+
* (codex-proxy.ts refuses to start with a missing or stale profile). Workers
|
|
99
|
+
* apply the same profiles from a pinned deployment; the manager's checkout is
|
|
100
|
+
* the human's working tree, so only the workstation sources must be clean
|
|
101
|
+
* (the `agents apply` owner enforces that) and nothing else is touched.
|
|
102
|
+
*/
|
|
103
|
+
export async function managerAgentProfiles(
|
|
104
|
+
id: string,
|
|
105
|
+
workspace: string,
|
|
106
|
+
run: Runner
|
|
107
|
+
): Promise<Check[]> {
|
|
108
|
+
for (const tool of PROFILE_TOOLS) {
|
|
109
|
+
const result = await run(
|
|
110
|
+
[
|
|
111
|
+
'mise',
|
|
112
|
+
'exec',
|
|
113
|
+
'--locked',
|
|
114
|
+
'bun',
|
|
115
|
+
'--',
|
|
116
|
+
'bun',
|
|
117
|
+
'src/agents/cli.ts',
|
|
118
|
+
'--json',
|
|
119
|
+
'--yes',
|
|
120
|
+
'--non-interactive',
|
|
121
|
+
'agents',
|
|
122
|
+
'apply',
|
|
123
|
+
'--node',
|
|
124
|
+
id,
|
|
125
|
+
'--tool',
|
|
126
|
+
tool,
|
|
127
|
+
'--profile',
|
|
128
|
+
'proxy',
|
|
129
|
+
],
|
|
130
|
+
{ cwd: workspace, timeoutMs: 120_000 }
|
|
131
|
+
);
|
|
132
|
+
if (!result.ok)
|
|
133
|
+
return [
|
|
134
|
+
{
|
|
135
|
+
id: 'configuration:apply',
|
|
136
|
+
status: 'fail',
|
|
137
|
+
summary: 'MANAGER_AGENT_PROFILES_FAILED',
|
|
138
|
+
remedy: {
|
|
139
|
+
action: `Run bun src/agents/cli.ts --yes agents apply --node ${id} --tool ${tool} --profile proxy in the dev-fleet checkout on the manager and review the result.`,
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
];
|
|
143
|
+
}
|
|
144
|
+
return [
|
|
145
|
+
{
|
|
146
|
+
id: 'configuration:apply',
|
|
147
|
+
status: 'pass',
|
|
148
|
+
summary: `The manager has the same proxy agent profiles as every worker (${PROFILE_TOOLS.join(', ')}).`,
|
|
149
|
+
},
|
|
150
|
+
];
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** fleet-sync runs only on the declared manager. */
|
|
154
|
+
export async function managerIdentity(id: string, run: Runner): Promise<Check> {
|
|
155
|
+
const result = await run(['scutil', '--get', 'LocalHostName']);
|
|
156
|
+
const local = result.ok ? hostId(result.stdout.trim()) : '';
|
|
157
|
+
return local === id
|
|
158
|
+
? {
|
|
159
|
+
id: 'manager:identity',
|
|
160
|
+
status: 'pass',
|
|
161
|
+
summary: `This machine is the declared fleet manager (${id}).`,
|
|
162
|
+
}
|
|
163
|
+
: {
|
|
164
|
+
id: 'manager:identity',
|
|
165
|
+
status: 'fail',
|
|
166
|
+
summary: 'FLEET_NOT_MANAGER',
|
|
167
|
+
remedy: {
|
|
168
|
+
action: `Run fleet-sync on the manager (${id}). Workers are never controlled from another worker; change the manager only by editing manager in fleet.json and moving its singleton services.`,
|
|
169
|
+
},
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** Workers depend on the manager's services, so it never sleeps on AC power. */
|
|
174
|
+
export async function managerPower(run: Runner): Promise<Check> {
|
|
175
|
+
const result = await run(['pmset', '-g', 'custom']);
|
|
176
|
+
const ac = result.ok ? (result.stdout.split('AC Power:')[1] ?? '') : '';
|
|
177
|
+
const sleep = /^\s*sleep\s+(\d+)/m.exec(ac)?.[1];
|
|
178
|
+
return sleep === '0'
|
|
179
|
+
? {
|
|
180
|
+
id: 'manager:power',
|
|
181
|
+
status: 'pass',
|
|
182
|
+
summary: 'The manager never sleeps on AC power.',
|
|
183
|
+
}
|
|
184
|
+
: {
|
|
185
|
+
id: 'manager:power',
|
|
186
|
+
status: 'fail',
|
|
187
|
+
summary: 'MANAGER_SLEEPS_ON_POWER',
|
|
188
|
+
remedy: {
|
|
189
|
+
action:
|
|
190
|
+
'Workers lose the CLI proxy and Beszel hub while the manager sleeps. Apply the manager power policy once: ansible-playbook -K ansible/macos/manager.yml',
|
|
191
|
+
},
|
|
192
|
+
};
|
|
193
|
+
}
|