@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,236 @@
|
|
|
1
|
+
import { verifyT3Linked } from '../t3-linked-check';
|
|
2
|
+
import { T3_RESTARTED_SUMMARY } from '../platform/t3-policy';
|
|
3
|
+
import { createHash } from 'node:crypto';
|
|
4
|
+
import { verifyT3Endpoint } from '../t3-endpoint';
|
|
5
|
+
import { authorizePrivilege } from './fleet-sync-privilege';
|
|
6
|
+
import { authorizeVault } from './fleet-sync-vault';
|
|
7
|
+
import { managerFailure } from '../manager-failure';
|
|
8
|
+
import { managerText, stdoutColor } from './fleet-sync-output';
|
|
9
|
+
import { syncProgress, type SyncProgress } from './fleet-sync-progress';
|
|
10
|
+
import { saveReport } from './fleet-sync-report';
|
|
11
|
+
import { synchronizeMonitoring } from './fleet-sync-monitoring';
|
|
12
|
+
import { convergeManager } from './fleet-sync-manager';
|
|
13
|
+
import { FleetUsageError } from '../errors';
|
|
14
|
+
import { loadConfig, selectNodes } from '../config';
|
|
15
|
+
import { enrolledRoster } from '../definition';
|
|
16
|
+
import type { FleetDefinition } from '../definition';
|
|
17
|
+
import { fleet as definition } from '../fleet-config';
|
|
18
|
+
import { synchronizeManager } from '../manager-sync';
|
|
19
|
+
import { inspectBootstrap } from '../manager-sync';
|
|
20
|
+
import { prepareBootstrap, bootstrapRemote } from '../remote-bootstrap';
|
|
21
|
+
import { machineRoot } from './fleet-sync';
|
|
22
|
+
import type { Runner } from '../types';
|
|
23
|
+
import type { DeployNode } from '../manager-sync';
|
|
24
|
+
import type { FleetSyncOptions } from './fleet-sync';
|
|
25
|
+
import type { Settings } from './settings';
|
|
26
|
+
import type { CommandOutcome } from './contracts';
|
|
27
|
+
|
|
28
|
+
export async function selected(
|
|
29
|
+
options: FleetSyncOptions,
|
|
30
|
+
settings: Settings,
|
|
31
|
+
run: Runner
|
|
32
|
+
): Promise<CommandOutcome> {
|
|
33
|
+
if (!options.revision)
|
|
34
|
+
throw new FleetUsageError('An approved source revision is required.');
|
|
35
|
+
const canonical = definition;
|
|
36
|
+
let roster;
|
|
37
|
+
try {
|
|
38
|
+
roster = enrolledRoster(canonical, await loadConfig(settings.config));
|
|
39
|
+
} catch {
|
|
40
|
+
return enrollmentRequired(canonical, options.revision);
|
|
41
|
+
}
|
|
42
|
+
const nodes = selectNodes(roster, options.node ?? []);
|
|
43
|
+
const scoped = scopeDefinition(canonical, options.node);
|
|
44
|
+
const ui = interfaceOptions(options);
|
|
45
|
+
const planFingerprint = createHash('sha256')
|
|
46
|
+
.update(
|
|
47
|
+
JSON.stringify({ revision: options.revision, definition: scoped, nodes })
|
|
48
|
+
)
|
|
49
|
+
.digest('hex');
|
|
50
|
+
if (
|
|
51
|
+
options.expectedPlanFingerprint &&
|
|
52
|
+
options.expectedPlanFingerprint !== planFingerprint
|
|
53
|
+
)
|
|
54
|
+
return failed(
|
|
55
|
+
managerFailure(
|
|
56
|
+
scoped,
|
|
57
|
+
options.revision,
|
|
58
|
+
'FLEET_PLAN_STALE',
|
|
59
|
+
'Manager connection bindings or target policy changed after approval. Inspect a fresh plan and approve it; no source preparation or node mutation occurred.'
|
|
60
|
+
)
|
|
61
|
+
);
|
|
62
|
+
const payload = options.apply
|
|
63
|
+
? await prepareBootstrap(machineRoot(), options.revision, run, {
|
|
64
|
+
definition: canonical,
|
|
65
|
+
checkout: settings.workspace,
|
|
66
|
+
})
|
|
67
|
+
: null;
|
|
68
|
+
const started = Date.now();
|
|
69
|
+
const report = await synchronizeManager(
|
|
70
|
+
{
|
|
71
|
+
definition: scoped,
|
|
72
|
+
nodes,
|
|
73
|
+
revision: options.revision,
|
|
74
|
+
apply: Boolean(options.apply),
|
|
75
|
+
...canary(canonical, options),
|
|
76
|
+
progress: (node, phase) => ui.progress.step(node, phase),
|
|
77
|
+
finished: (node) => ui.progress.finished(node),
|
|
78
|
+
manager: () =>
|
|
79
|
+
convergeManager(
|
|
80
|
+
canonical,
|
|
81
|
+
{
|
|
82
|
+
revision: options.revision!,
|
|
83
|
+
apply: Boolean(options.apply),
|
|
84
|
+
workspace: settings.workspace,
|
|
85
|
+
},
|
|
86
|
+
run
|
|
87
|
+
),
|
|
88
|
+
},
|
|
89
|
+
run,
|
|
90
|
+
deployWithMonitoring(payload, canonical, run, ui)
|
|
91
|
+
).finally(() => ui.progress.stop());
|
|
92
|
+
return outcome(report, planFingerprint, started);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** A revision new to the fleet is proven on the first declared worker before
|
|
96
|
+
* the rest; repeating one that already converged everywhere needs no canary,
|
|
97
|
+
* so every worker converges at once.
|
|
98
|
+
*/
|
|
99
|
+
function canary(
|
|
100
|
+
canonical: FleetDefinition,
|
|
101
|
+
options: FleetSyncOptions
|
|
102
|
+
): { canary?: string } {
|
|
103
|
+
const first = canonical.workers[0];
|
|
104
|
+
return first && !options.rolledOut ? { canary: first.id } : {};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Saves the full JSON report, then renders the short human summary. */
|
|
108
|
+
async function outcome(
|
|
109
|
+
report: Awaited<ReturnType<typeof synchronizeManager>>,
|
|
110
|
+
planFingerprint: string,
|
|
111
|
+
started: number
|
|
112
|
+
): Promise<CommandOutcome> {
|
|
113
|
+
const data = { ...report, planFingerprint };
|
|
114
|
+
const reportPath = await saveReport(machineRoot(), data);
|
|
115
|
+
return {
|
|
116
|
+
exitCode: report.ok ? 0 : 1,
|
|
117
|
+
data,
|
|
118
|
+
text: managerText(report, {
|
|
119
|
+
color: stdoutColor(),
|
|
120
|
+
durationMs: Date.now() - started,
|
|
121
|
+
...(reportPath ? { reportPath } : {}),
|
|
122
|
+
}),
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function enrollmentRequired(
|
|
127
|
+
canonical: FleetDefinition,
|
|
128
|
+
revision: string
|
|
129
|
+
): CommandOutcome {
|
|
130
|
+
const report = managerFailure(
|
|
131
|
+
canonical,
|
|
132
|
+
revision,
|
|
133
|
+
'FLEET_MANAGER_ENROLLMENT_REQUIRED',
|
|
134
|
+
'Create or migrate private manager connection overrides for the canonical fleet. Resolve conflicting IDs/SSH aliases; discovery cannot authorize membership.'
|
|
135
|
+
);
|
|
136
|
+
return failed(report);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function failed(report: ReturnType<typeof managerFailure>): CommandOutcome {
|
|
140
|
+
return {
|
|
141
|
+
exitCode: 1,
|
|
142
|
+
data: report,
|
|
143
|
+
text: managerText(report, { color: stdoutColor() }),
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function deployWithMonitoring(
|
|
148
|
+
payload: Awaited<ReturnType<typeof prepareBootstrap>>,
|
|
149
|
+
canonical: FleetDefinition,
|
|
150
|
+
run: Runner,
|
|
151
|
+
ui: {
|
|
152
|
+
progress: SyncProgress;
|
|
153
|
+
interactiveAuthorization: boolean;
|
|
154
|
+
}
|
|
155
|
+
): DeployNode {
|
|
156
|
+
return async (node, revision) => {
|
|
157
|
+
if (!payload || payload.revision !== revision)
|
|
158
|
+
return {
|
|
159
|
+
node: node.id,
|
|
160
|
+
checks: [
|
|
161
|
+
{
|
|
162
|
+
id: 'source:bootstrap',
|
|
163
|
+
status: 'fail' as const,
|
|
164
|
+
summary: 'FLEET_APPROVED_BOOTSTRAP_UNAVAILABLE',
|
|
165
|
+
remedy: {
|
|
166
|
+
action:
|
|
167
|
+
'Install Git on the manager and verify the approved source contains the tracked remote bootstrap and matches this package’s canonical fleet definition. Use the matching package release after a membership change. No manager credentials are forwarded.',
|
|
168
|
+
},
|
|
169
|
+
},
|
|
170
|
+
],
|
|
171
|
+
};
|
|
172
|
+
// Password prompts need a quiet terminal: no spinner while authorizing.
|
|
173
|
+
if (ui.interactiveAuthorization) {
|
|
174
|
+
ui.progress.stop();
|
|
175
|
+
const checks = await authorizePrivilege(node, payload.authorizeSudo, run);
|
|
176
|
+
if (checks.length) return { node: node.id, checks };
|
|
177
|
+
}
|
|
178
|
+
const vault = { interactive: ui.interactiveAuthorization };
|
|
179
|
+
if (ui.interactiveAuthorization) ui.progress.stop();
|
|
180
|
+
const before = await authorizeVault(node, vault, run);
|
|
181
|
+
ui.progress.step(node.id, 'Converging (remote lock, install, verify)');
|
|
182
|
+
const result = await bootstrapRemote(node, payload, run);
|
|
183
|
+
// A fresh node gets vault from the bootstrap; log it in now. Its provider
|
|
184
|
+
// checks in this run predate the login, so the next sync verifies them.
|
|
185
|
+
result.checks.push(
|
|
186
|
+
...(before.length ? before : await authorizeVault(node, vault, run))
|
|
187
|
+
);
|
|
188
|
+
// Workers are Linux by definition.
|
|
189
|
+
ui.progress.step(node.id, 'Verifying SSH, OS and T3');
|
|
190
|
+
result.checks.push(...(await inspectBootstrap(node, run, 'linux')));
|
|
191
|
+
result.checks.push(...(await verifyT3Endpoint(node, run)));
|
|
192
|
+
// A worker T3 restarted by this sync (idle policy heal) needs a minute or
|
|
193
|
+
// two before the manager's link reconnects.
|
|
194
|
+
const restarted = result.checks.some(
|
|
195
|
+
(check) => check.summary === T3_RESTARTED_SUMMARY
|
|
196
|
+
);
|
|
197
|
+
result.checks.push(
|
|
198
|
+
await verifyT3Linked(node, run, {
|
|
199
|
+
reconnectMs: restarted ? 180_000 : 0,
|
|
200
|
+
})
|
|
201
|
+
);
|
|
202
|
+
if (
|
|
203
|
+
result.checks.some(
|
|
204
|
+
(check) => check.id === 'configuration:apply' && check.status === 'pass'
|
|
205
|
+
)
|
|
206
|
+
) {
|
|
207
|
+
result.checks = result.checks.filter(
|
|
208
|
+
(check) => check.id !== 'beszel:freshness'
|
|
209
|
+
);
|
|
210
|
+
ui.progress.step(node.id, 'Enrolling monitoring');
|
|
211
|
+
result.checks.push(
|
|
212
|
+
...(await synchronizeMonitoring(node, canonical, revision, run))
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
return result;
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function scopeDefinition(canonical: FleetDefinition, ids?: string[]) {
|
|
220
|
+
return ids?.length
|
|
221
|
+
? {
|
|
222
|
+
...canonical,
|
|
223
|
+
workers: canonical.workers.filter((node) => ids.includes(node.id)),
|
|
224
|
+
}
|
|
225
|
+
: canonical;
|
|
226
|
+
}
|
|
227
|
+
function interfaceOptions(options: FleetSyncOptions) {
|
|
228
|
+
return {
|
|
229
|
+
progress: syncProgress(!options.json),
|
|
230
|
+
interactiveAuthorization: Boolean(
|
|
231
|
+
options.interactiveAuthorization &&
|
|
232
|
+
!options.json &&
|
|
233
|
+
!options.nonInteractive
|
|
234
|
+
),
|
|
235
|
+
};
|
|
236
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { chmod, mkdir, rename, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
|
|
4
|
+
/** Saves the full JSON report of the latest run (0600, manager-private) so an
|
|
5
|
+
* agent can debug without rerunning. Reports carry codes and remedies only,
|
|
6
|
+
* never secret values. Returns null when it cannot be written.
|
|
7
|
+
*/
|
|
8
|
+
export async function saveReport(
|
|
9
|
+
root: string,
|
|
10
|
+
data: unknown
|
|
11
|
+
): Promise<string | null> {
|
|
12
|
+
try {
|
|
13
|
+
const directory = join(root, 'reports');
|
|
14
|
+
await mkdir(directory, { recursive: true, mode: 0o700 });
|
|
15
|
+
const path = join(directory, 'last-sync.json');
|
|
16
|
+
const partial = `${path}.partial`;
|
|
17
|
+
await writeFile(partial, `${JSON.stringify(data, null, 2)}\n`, {
|
|
18
|
+
mode: 0o600,
|
|
19
|
+
});
|
|
20
|
+
await chmod(partial, 0o600);
|
|
21
|
+
await rename(partial, path);
|
|
22
|
+
return path;
|
|
23
|
+
} catch {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import { currentPlatform } from '../platform';
|
|
3
|
+
import type { Platform } from '../platform/types';
|
|
4
|
+
import type { Runner } from '../types';
|
|
5
|
+
import type { CommandOutcome } from './contracts';
|
|
6
|
+
import { machineRoot } from './fleet-sync';
|
|
7
|
+
|
|
8
|
+
const LABEL = 'personal-fleet-sync';
|
|
9
|
+
declare const PERSONAL_VERSION: string | undefined;
|
|
10
|
+
declare const PERSONAL_SOURCE_REVISION: string | null;
|
|
11
|
+
export type ScheduleAction =
|
|
12
|
+
'enable' | 'pause' | 'resume' | 'disable' | 'status';
|
|
13
|
+
interface ScheduleInput {
|
|
14
|
+
action: ScheduleAction;
|
|
15
|
+
revision: string;
|
|
16
|
+
config: string;
|
|
17
|
+
authorized: boolean;
|
|
18
|
+
}
|
|
19
|
+
export async function managerSchedule(
|
|
20
|
+
input: ScheduleInput,
|
|
21
|
+
run: Runner,
|
|
22
|
+
platform: Platform = currentPlatform()
|
|
23
|
+
): Promise<CommandOutcome> {
|
|
24
|
+
if (input.action === 'status')
|
|
25
|
+
return {
|
|
26
|
+
exitCode: 0,
|
|
27
|
+
data: {
|
|
28
|
+
version: 2,
|
|
29
|
+
command: 'fleet-sync',
|
|
30
|
+
execution: 'manager-push',
|
|
31
|
+
scheduling: scheduleStatus(platform),
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
if (!input.authorized)
|
|
35
|
+
return required('Manager scheduling changes require --apply --yes.');
|
|
36
|
+
if (input.action === 'pause' || input.action === 'disable')
|
|
37
|
+
return removeSchedule(input, platform);
|
|
38
|
+
return enableSchedule(input, run, platform);
|
|
39
|
+
}
|
|
40
|
+
function removeSchedule(
|
|
41
|
+
input: ScheduleInput,
|
|
42
|
+
platform: Platform
|
|
43
|
+
): CommandOutcome {
|
|
44
|
+
const result = platform.removeScheduledJob(LABEL);
|
|
45
|
+
return {
|
|
46
|
+
exitCode: result.ok ? 0 : 1,
|
|
47
|
+
data: {
|
|
48
|
+
version: 2,
|
|
49
|
+
command: 'fleet-sync',
|
|
50
|
+
ok: result.ok,
|
|
51
|
+
scheduling: input.action,
|
|
52
|
+
summary: result.ok
|
|
53
|
+
? 'Manager schedule removed; node application services are untouched.'
|
|
54
|
+
: 'Manager scheduler removal failed; inspect its platform status.',
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
async function enableSchedule(
|
|
59
|
+
input: ScheduleInput,
|
|
60
|
+
run: Runner,
|
|
61
|
+
platform: Platform
|
|
62
|
+
): Promise<CommandOutcome> {
|
|
63
|
+
if (
|
|
64
|
+
typeof PERSONAL_VERSION !== 'string' ||
|
|
65
|
+
!/^\d+\.\d+\.\d+$/.test(PERSONAL_VERSION) ||
|
|
66
|
+
typeof PERSONAL_SOURCE_REVISION !== 'string'
|
|
67
|
+
)
|
|
68
|
+
return required(
|
|
69
|
+
'Scheduling requires a published CLI with clean source provenance; dirty review artifacts cannot enable unattended deployment.'
|
|
70
|
+
);
|
|
71
|
+
const root = machineRoot();
|
|
72
|
+
const prefix = join(root, 'runtime');
|
|
73
|
+
const installed = await run(
|
|
74
|
+
[
|
|
75
|
+
'npm',
|
|
76
|
+
'install',
|
|
77
|
+
'--prefix',
|
|
78
|
+
prefix,
|
|
79
|
+
'--ignore-scripts',
|
|
80
|
+
'--no-audit',
|
|
81
|
+
'--no-fund',
|
|
82
|
+
`dev-fleet@${PERSONAL_VERSION}`,
|
|
83
|
+
],
|
|
84
|
+
{ timeoutMs: 120_000 }
|
|
85
|
+
);
|
|
86
|
+
if (!installed.ok)
|
|
87
|
+
return required(
|
|
88
|
+
'The exact CLI package could not be installed in the managed manager runtime. No schedule was enabled.'
|
|
89
|
+
);
|
|
90
|
+
const result = platform.installScheduledJob({
|
|
91
|
+
label: LABEL,
|
|
92
|
+
intervalSeconds: 1800,
|
|
93
|
+
command: [
|
|
94
|
+
process.execPath,
|
|
95
|
+
join(prefix, 'node_modules/dev-fleet/cli.mjs'),
|
|
96
|
+
'fleet-sync',
|
|
97
|
+
'--config',
|
|
98
|
+
input.config,
|
|
99
|
+
'--revision',
|
|
100
|
+
input.revision,
|
|
101
|
+
'--apply',
|
|
102
|
+
'--yes',
|
|
103
|
+
'--non-interactive',
|
|
104
|
+
'--json',
|
|
105
|
+
],
|
|
106
|
+
logDir: join(root, 'logs'),
|
|
107
|
+
env: { PATH: process.env['PATH'] ?? '/usr/bin:/bin' },
|
|
108
|
+
});
|
|
109
|
+
return {
|
|
110
|
+
exitCode: result.ok ? 0 : 1,
|
|
111
|
+
data: {
|
|
112
|
+
version: 2,
|
|
113
|
+
command: 'fleet-sync',
|
|
114
|
+
ok: result.ok,
|
|
115
|
+
scheduling: result.ok ? 'enabled' : 'action-required',
|
|
116
|
+
revision: input.revision,
|
|
117
|
+
summary:
|
|
118
|
+
'Manager schedule uses one exact package and approved revision. Linux user scheduling requires lingering; macOS LaunchAgent requires the manager user session. Inspect status and logs; boot acceptance is separate.',
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
function required(summary: string): CommandOutcome {
|
|
123
|
+
return {
|
|
124
|
+
exitCode: 1,
|
|
125
|
+
data: {
|
|
126
|
+
version: 2,
|
|
127
|
+
command: 'fleet-sync',
|
|
128
|
+
ok: false,
|
|
129
|
+
scheduling: 'action-required',
|
|
130
|
+
summary,
|
|
131
|
+
},
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function scheduleStatus(platform: Platform) {
|
|
136
|
+
const { installed, supported } = platform.scheduledJobStatus(LABEL);
|
|
137
|
+
return { installed, supported };
|
|
138
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import { fleet as definition } from '../fleet-config';
|
|
3
|
+
import { sshArgs, shellQuote } from '../ssh';
|
|
4
|
+
import { resolveVaultConfig, usesVault } from '../vault-config';
|
|
5
|
+
import type { Check, FleetNode, Runner } from '../types';
|
|
6
|
+
|
|
7
|
+
const TOOL_PATH =
|
|
8
|
+
'export PATH="$HOME/.local/share/mise/shims:$HOME/.local/bin:$PATH"';
|
|
9
|
+
|
|
10
|
+
/** Runs an interactive command with the operator's terminal attached. */
|
|
11
|
+
export type TerminalRunner = (args: string[]) => boolean;
|
|
12
|
+
|
|
13
|
+
const terminal: TerminalRunner = (args) =>
|
|
14
|
+
spawnSync(args[0]!, args.slice(1), { stdio: 'inherit' }).status === 0;
|
|
15
|
+
|
|
16
|
+
type VaultState = 'authenticated' | 'unauthenticated' | 'unavailable';
|
|
17
|
+
|
|
18
|
+
/** Vault field holding the issued token for one worker node. */
|
|
19
|
+
export function workerTokenField(node: string): string {
|
|
20
|
+
return `FLEET_WORKER_TOKEN_${node.toUpperCase().replace(/[^A-Z0-9]/g, '_')}`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* The manager reads each node's own token from Vault, so it must be logged in
|
|
25
|
+
* first. Interactive runs prompt once on this terminal; the password never
|
|
26
|
+
* passes through this process.
|
|
27
|
+
*/
|
|
28
|
+
export async function authorizeManager(
|
|
29
|
+
options: { interactive: boolean },
|
|
30
|
+
run: Runner,
|
|
31
|
+
attach: TerminalRunner = terminal
|
|
32
|
+
): Promise<boolean> {
|
|
33
|
+
if (!usesVault()) return true;
|
|
34
|
+
if ((await run(lookup(), { timeoutMs: 20_000 })).ok) return true;
|
|
35
|
+
if (!options.interactive) return false;
|
|
36
|
+
process.stderr.write(
|
|
37
|
+
'This manager is not logged in to Vault. Log in once; each machine then gets its own token.\n'
|
|
38
|
+
);
|
|
39
|
+
attach(login());
|
|
40
|
+
return (await run(lookup(), { timeoutMs: 20_000 })).ok;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Each node keeps its own periodic Vault token (renewed by the worker job). The
|
|
45
|
+
* proxy key, GitHub token and provider credentials all come from that session,
|
|
46
|
+
* so a node without one has no working proxy providers. The node's
|
|
47
|
+
* issued token is delivered once over SSH stdin, never as an argument;
|
|
48
|
+
* interactive runs fall back to a password login on the node's own terminal.
|
|
49
|
+
*/
|
|
50
|
+
export async function authorizeVault(
|
|
51
|
+
node: FleetNode,
|
|
52
|
+
options: { interactive: boolean },
|
|
53
|
+
run: Runner,
|
|
54
|
+
attach: TerminalRunner = terminal
|
|
55
|
+
): Promise<Check[]> {
|
|
56
|
+
// The other secrets providers need no per-node session.
|
|
57
|
+
if (!usesVault()) return [];
|
|
58
|
+
const before = await vaultState(node, run);
|
|
59
|
+
if (before === 'authenticated') return [pass(node)];
|
|
60
|
+
// Fresh node: the locked toolchain (including vault) is not installed yet.
|
|
61
|
+
if (before === 'unavailable') return [];
|
|
62
|
+
if (await deliverWorkerToken(node, run)) return [pass(node)];
|
|
63
|
+
if (!options.interactive) return [required(node)];
|
|
64
|
+
process.stderr.write(
|
|
65
|
+
`[${node.id}] No issued worker token found. Enter the Vault password to log this machine in.\n`
|
|
66
|
+
);
|
|
67
|
+
attach(remote(node, login(), '-t'));
|
|
68
|
+
return [
|
|
69
|
+
(await vaultState(node, run)) === 'authenticated'
|
|
70
|
+
? pass(node)
|
|
71
|
+
: required(node),
|
|
72
|
+
];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function deliverWorkerToken(
|
|
76
|
+
node: FleetNode,
|
|
77
|
+
run: Runner
|
|
78
|
+
): Promise<boolean> {
|
|
79
|
+
const { addr, mount, project, config } = resolveVaultConfig();
|
|
80
|
+
const secret = await run(
|
|
81
|
+
[
|
|
82
|
+
'vault',
|
|
83
|
+
'kv',
|
|
84
|
+
'get',
|
|
85
|
+
`-address=${addr}`,
|
|
86
|
+
`-mount=${mount}`,
|
|
87
|
+
`-field=${workerTokenField(node.id)}`,
|
|
88
|
+
`${project}/${config}`,
|
|
89
|
+
],
|
|
90
|
+
{ timeoutMs: 20_000 }
|
|
91
|
+
);
|
|
92
|
+
const token = secret.ok ? secret.stdout.trim() : '';
|
|
93
|
+
if (!/^[\x21-\x7e]{16,4096}$/.test(token)) return false;
|
|
94
|
+
const delivered = await run(
|
|
95
|
+
remote(node, ['vault', 'login', `-address=${addr}`, '-no-print', '-']),
|
|
96
|
+
{ input: token + '\n', timeoutMs: 30_000 }
|
|
97
|
+
);
|
|
98
|
+
return delivered.ok && (await vaultState(node, run)) === 'authenticated';
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async function vaultState(node: FleetNode, run: Runner): Promise<VaultState> {
|
|
102
|
+
const result = await run(
|
|
103
|
+
[
|
|
104
|
+
...sshArgs(node),
|
|
105
|
+
`${TOOL_PATH}; command -v vault >/dev/null 2>&1 || { printf vault-missing; exit 1; }; ${quote(lookup())} >/dev/null 2>&1`,
|
|
106
|
+
],
|
|
107
|
+
{ timeoutMs: 20_000 }
|
|
108
|
+
);
|
|
109
|
+
if (result.ok) return 'authenticated';
|
|
110
|
+
// Unreachable nodes are reported by the bootstrap's own SSH checks.
|
|
111
|
+
if (result.reason !== 'exit') return 'unavailable';
|
|
112
|
+
return result.stdout.includes('vault-missing')
|
|
113
|
+
? 'unavailable'
|
|
114
|
+
: 'unauthenticated';
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function lookup(): string[] {
|
|
118
|
+
const { addr } = resolveVaultConfig();
|
|
119
|
+
return ['vault', 'token', 'lookup', `-address=${addr}`, '-format=json'];
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function login(): string[] {
|
|
123
|
+
const { addr } = resolveVaultConfig();
|
|
124
|
+
return [
|
|
125
|
+
'vault',
|
|
126
|
+
'login',
|
|
127
|
+
`-address=${addr}`,
|
|
128
|
+
'-method=userpass',
|
|
129
|
+
'-no-print',
|
|
130
|
+
`username=${definition.worker!.vaultUser}`,
|
|
131
|
+
];
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Same verified connection options; `-t` attaches a terminal for prompts. */
|
|
135
|
+
export function remote(
|
|
136
|
+
node: FleetNode,
|
|
137
|
+
command: string[],
|
|
138
|
+
tty: '-T' | '-t' = '-T'
|
|
139
|
+
): string[] {
|
|
140
|
+
const args = sshArgs(node).map((arg) => (arg === '-T' ? tty : arg));
|
|
141
|
+
return [...args, `${TOOL_PATH}; ${quote(command)}`];
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function quote(command: string[]): string {
|
|
145
|
+
return command.map(shellQuote).join(' ');
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function pass(node: FleetNode): Check {
|
|
149
|
+
return {
|
|
150
|
+
id: 'auth:vault',
|
|
151
|
+
status: 'pass',
|
|
152
|
+
summary: `${node.id} has its own Vault session; proxy providers can read their key.`,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function required(node: FleetNode): Check {
|
|
157
|
+
const { addr } = resolveVaultConfig();
|
|
158
|
+
return {
|
|
159
|
+
id: 'auth:vault',
|
|
160
|
+
status: 'fail',
|
|
161
|
+
summary: 'VAULT_LOGIN_REQUIRED',
|
|
162
|
+
remedy: {
|
|
163
|
+
action: `Run devfleet fleet-sync in a terminal, or store this node's token in the shared Vault document as ${workerTokenField(node.id)}. Manual fallback on ${node.id}: vault login -address=${addr} -method=userpass username=${definition.worker!.vaultUser}. Proxy providers in T3 stay unavailable until then.`,
|
|
164
|
+
},
|
|
165
|
+
};
|
|
166
|
+
}
|