@crvouga/dev-fleet 0.0.0-stage → 0.2.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 +357 -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 +158 -0
- package/agents/t3.json +40 -0
- package/ansible/macos/manager.yml +30 -0
- package/ansible/macos/policy.json +9 -0
- package/ansible/monitoring/README.md +215 -0
- package/ansible/monitoring/agent-darwin.yml +198 -0
- package/ansible/monitoring/agent.yml +172 -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/hub-route-darwin.yml +51 -0
- package/ansible/monitoring/lock.json +16 -0
- package/ansible/net/remote-probe.sh +13 -0
- package/ansible/pyproject.toml +12 -0
- package/ansible/ubuntu/authorize-sudo.sh +51 -0
- package/ansible/ubuntu/baseline.yml +89 -0
- package/ansible/ubuntu/disk-gc.py +336 -0
- package/ansible/ubuntu/disk-guard.py +104 -0
- package/ansible/ubuntu/policy.json +52 -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 +78 -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 +312 -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/ansible/uv.lock +190 -0
- package/bin/devfleet.mjs +36 -0
- package/bootstrap/bootstrap.ps1 +50 -0
- package/bootstrap/bootstrap.sh +80 -0
- package/bootstrap/devfleet +7 -0
- package/bootstrap/docker-desktop.sh +33 -0
- package/bootstrap/doctor.ts +234 -0
- package/bootstrap/fleet-manager-run.sh +103 -0
- package/bootstrap/fleet-remote-bootstrap.sh +258 -0
- package/bootstrap/global-config.ts +171 -0
- package/bootstrap/install-launcher.sh +10 -0
- package/bootstrap/native-aws.ts +86 -0
- package/bootstrap/native-t3.ts +154 -0
- package/bootstrap/npm.ts +358 -0
- package/bootstrap/release-version.ts +157 -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/docs/consumer-setup.md +408 -0
- package/docs/declared.md +89 -0
- package/docs/machine-setup.md +399 -0
- package/docs/operations.md +276 -0
- package/docs/releasing.md +103 -0
- package/docs/state.md +254 -0
- package/docs/topology.md +67 -0
- package/docs/ubuntu-sync.md +351 -0
- package/docs/worker-contract.md +67 -0
- package/examples/fleet.json +100 -0
- package/examples/roster.json +13 -0
- package/llms.txt +35 -0
- package/mise.lock +1521 -0
- package/mise.toml +110 -0
- package/package.json +74 -4
- package/schema/fleet.schema.json +420 -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 +165 -0
- package/src/agents/launch.ts +290 -0
- package/src/agents/native-verify.ts +371 -0
- package/src/agents/provider-errors.ts +57 -0
- package/src/agents/registry.ts +61 -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 +129 -0
- package/src/cli/app.ts +122 -0
- package/src/cli/cli.ts +28 -0
- package/src/cli/command-index.ts +57 -0
- package/src/cli/contracts.ts +25 -0
- package/src/cli/fleet-reference.ts +17 -0
- package/src/cli/fleet-state.ts +499 -0
- package/src/cli/fleet-sync-checkout.ts +130 -0
- package/src/cli/fleet-sync-enrollment.ts +140 -0
- package/src/cli/fleet-sync-forward.ts +134 -0
- package/src/cli/fleet-sync-manager.ts +249 -0
- package/src/cli/fleet-sync-monitoring.ts +185 -0
- package/src/cli/fleet-sync-output.ts +317 -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 +242 -0
- package/src/cli/fleet-sync-report.ts +26 -0
- package/src/cli/fleet-sync-schedule.ts +138 -0
- package/src/cli/fleet-sync-state.ts +86 -0
- package/src/cli/fleet-sync-vault.ts +166 -0
- package/src/cli/fleet-sync.ts +375 -0
- package/src/cli/fleet.ts +165 -0
- package/src/cli/managed-health.ts +219 -0
- package/src/cli/node-deployment.ts +34 -0
- package/src/cli/node-sync.ts +121 -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 +185 -0
- package/src/cli/project.ts +51 -0
- package/src/cli/register-fleet.ts +296 -0
- package/src/cli/registry.ts +185 -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 +105 -0
- package/src/cli/search.ts +35 -0
- package/src/cli/settings.ts +67 -0
- package/src/cli/source-operations.ts +102 -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 +290 -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 +174 -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 +173 -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 +405 -0
- package/src/manager.ts +62 -0
- package/src/monitoring/agent-evidence.ts +146 -0
- package/src/monitoring/apply.ts +183 -0
- package/src/monitoring/beszel.ts +198 -0
- package/src/monitoring/enroll.ts +111 -0
- package/src/monitoring/hub-access.ts +114 -0
- package/src/monitoring/hub-backup.ts +121 -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 +248 -0
- package/src/monitoring/remote.ts +65 -0
- package/src/network-definition.ts +83 -0
- package/src/node-converge-linux.ts +374 -0
- package/src/node-converge-shared.ts +83 -0
- package/src/node-converge.ts +40 -0
- package/src/onboard.ts +35 -0
- package/src/package-info.ts +45 -0
- package/src/platform/ansible-receipt.ts +128 -0
- package/src/platform/ansible.ts +66 -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 +132 -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 +66 -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 +210 -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/state/bucket.ts +208 -0
- package/src/state/check.ts +74 -0
- package/src/state/connection.ts +92 -0
- package/src/state/enrollment.ts +45 -0
- package/src/state/legacy.ts +89 -0
- package/src/state/litestream.ts +143 -0
- package/src/state/operator.ts +20 -0
- package/src/state/paths.ts +40 -0
- package/src/state/roster.ts +62 -0
- package/src/state/store.ts +169 -0
- package/src/state/sync.ts +352 -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 +104 -0
- package/src/t3-linked-check.ts +123 -0
- package/src/tailnet-health.ts +231 -0
- package/src/tailnet-matching.ts +37 -0
- package/src/tailscale.ts +141 -0
- package/src/toolchain-checks.ts +108 -0
- package/src/topology.ts +24 -0
- package/src/transport.ts +122 -0
- package/src/types.ts +99 -0
- package/src/vault-config.ts +38 -0
- package/src/worker-access-checks.ts +168 -0
- package/src/worker-capacity.ts +34 -0
- package/src/worker-host-checks.ts +181 -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,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Platform abstraction for the `personal` CLI.
|
|
3
|
+
*
|
|
4
|
+
* Every OS-specific behavior lives behind this interface so the commands stay
|
|
5
|
+
* platform-agnostic. Concrete adapters (darwin / linux / windows) implement
|
|
6
|
+
* the same contract via polymorphism; `getPlatform()` is the factory.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export type PlatformName = 'darwin' | 'linux' | 'windows' | 'unknown';
|
|
10
|
+
|
|
11
|
+
export type NotificationKind =
|
|
12
|
+
'finished' | 'interrupted' | 'question' | 'permission' | 'error';
|
|
13
|
+
|
|
14
|
+
export type NotifierCapability = {
|
|
15
|
+
/** Native rich notifications (click actions where supported). */
|
|
16
|
+
readonly native: boolean;
|
|
17
|
+
/** Per-kind sounds. */
|
|
18
|
+
readonly sounds: boolean;
|
|
19
|
+
/** Click-to-focus a project window/tab. */
|
|
20
|
+
readonly clickToFocus: boolean;
|
|
21
|
+
/** Human reason when a capability is unavailable. */
|
|
22
|
+
readonly reason?: string;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export type NotifierTestResult = {
|
|
26
|
+
readonly ok: boolean;
|
|
27
|
+
readonly detail: string;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export type GlobalInstallResult = {
|
|
31
|
+
readonly binDir: string;
|
|
32
|
+
readonly launcherPath: string;
|
|
33
|
+
readonly created: boolean;
|
|
34
|
+
readonly alreadyOnPath: boolean;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/** One row of the OS process table (see `listProcesses`). */
|
|
38
|
+
export type ProcessInfo = {
|
|
39
|
+
readonly pid: number;
|
|
40
|
+
readonly ppid: number;
|
|
41
|
+
/** Process group (a shell pipeline / job shares one). */
|
|
42
|
+
readonly pgid: number;
|
|
43
|
+
readonly uid: number;
|
|
44
|
+
/** Resident memory in KiB. */
|
|
45
|
+
readonly rssKb: number;
|
|
46
|
+
/** Recent CPU percent (may exceed 100 on multi-core). */
|
|
47
|
+
readonly cpu: number;
|
|
48
|
+
/** `ps` state flags; a leading `Z` marks a zombie. */
|
|
49
|
+
readonly state: string;
|
|
50
|
+
/** Full command line (executable + args). */
|
|
51
|
+
readonly command: string;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
/** A periodic background job (launchd agent, systemd user timer, …). */
|
|
55
|
+
export type ScheduledJob = {
|
|
56
|
+
readonly label: string;
|
|
57
|
+
/** Absolute executable + args. */
|
|
58
|
+
readonly command: readonly string[];
|
|
59
|
+
readonly intervalSeconds: number;
|
|
60
|
+
/** Directory for the job's stdout/stderr logs. */
|
|
61
|
+
readonly logDir: string;
|
|
62
|
+
/** Extra environment (e.g. PATH so `bun` resolves outside a login shell). */
|
|
63
|
+
readonly env: Readonly<Record<string, string>>;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
export type ScheduledJobStatus = {
|
|
67
|
+
readonly supported: boolean;
|
|
68
|
+
readonly installed: boolean;
|
|
69
|
+
readonly detail: string;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
export type Platform = {
|
|
73
|
+
/** Optional source-owned native Docker installer. */
|
|
74
|
+
readonly dockerSetupScript?: string;
|
|
75
|
+
readonly name: PlatformName;
|
|
76
|
+
readonly label: string;
|
|
77
|
+
|
|
78
|
+
/** `~/.config/opencode` (or the OS equivalent). */
|
|
79
|
+
opencodeDir(): string;
|
|
80
|
+
/** `~/.cache` (or the OS equivalent) — daemon sockets live here. */
|
|
81
|
+
cacheDir(): string;
|
|
82
|
+
/** Preferred global bin dir (`~/.local/bin` or Windows equivalent). */
|
|
83
|
+
globalBinDir(): string;
|
|
84
|
+
|
|
85
|
+
/** Whether `swiftc` builds / shell shims apply on this platform. */
|
|
86
|
+
canBuildSwiftNotifier(): boolean;
|
|
87
|
+
/** Shell RC files to inspect when ensuring `~/.local/bin` is on PATH. */
|
|
88
|
+
shellRcFiles(): readonly string[];
|
|
89
|
+
|
|
90
|
+
notifierCapabilities(): NotifierCapability;
|
|
91
|
+
/** Best-effort desktop notification. Never throws — returns a result. */
|
|
92
|
+
postNotification(input: {
|
|
93
|
+
title: string;
|
|
94
|
+
message: string;
|
|
95
|
+
kind: NotificationKind;
|
|
96
|
+
}): Promise<NotifierTestResult>;
|
|
97
|
+
|
|
98
|
+
/** Daemon process running? (best-effort, always safe when unknown). */
|
|
99
|
+
isNotifierDaemonRunning(): boolean;
|
|
100
|
+
/** Best-effort daemon restart (kill so the next `--post` relaunches). */
|
|
101
|
+
restartNotifierDaemon(): void;
|
|
102
|
+
|
|
103
|
+
/** System sound names available for per-kind configuration (sorted, unique). */
|
|
104
|
+
availableSystemSounds(): string[];
|
|
105
|
+
/** Preview a system sound by name. Never throws — returns a result. */
|
|
106
|
+
playSystemSound(name: string): Promise<NotifierTestResult>;
|
|
107
|
+
|
|
108
|
+
/** Snapshot of every process, or `null` when unsupported on this OS. */
|
|
109
|
+
listProcesses(): ProcessInfo[] | null;
|
|
110
|
+
|
|
111
|
+
/** App-bundled Tailscale CLI binaries that exist (used when `tailscale` is not on PATH). */
|
|
112
|
+
tailscaleAppBinaries(): readonly string[];
|
|
113
|
+
/** VS Code user `settings.json`, or `null` when unknown on this OS. */
|
|
114
|
+
vscodeSettingsPath(): string | null;
|
|
115
|
+
/** VS Code `code` CLI binaries shipped inside the installed app. */
|
|
116
|
+
vscodeCliBinaries(): readonly string[];
|
|
117
|
+
|
|
118
|
+
/** Install (or replace) a periodic job. Never throws — returns a result. */
|
|
119
|
+
installScheduledJob(job: ScheduledJob): NotifierTestResult;
|
|
120
|
+
/** Remove a periodic job. Never throws — returns a result. */
|
|
121
|
+
removeScheduledJob(label: string): NotifierTestResult;
|
|
122
|
+
scheduledJobStatus(label: string): ScheduledJobStatus;
|
|
123
|
+
};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import policy from '../../ansible/ubuntu/policy.json';
|
|
2
|
+
import type { Runner } from '../types';
|
|
3
|
+
|
|
4
|
+
export interface UbuntuAudit {
|
|
5
|
+
supported: boolean;
|
|
6
|
+
release: string | null;
|
|
7
|
+
architecture: string | null;
|
|
8
|
+
packages: Record<string, string>;
|
|
9
|
+
desktop: boolean;
|
|
10
|
+
services: Record<string, boolean>;
|
|
11
|
+
passwordlessSudo: boolean;
|
|
12
|
+
rebootPending: boolean;
|
|
13
|
+
}
|
|
14
|
+
const PACKAGES = [...Object.keys(policy.packages), 'tailscale'];
|
|
15
|
+
|
|
16
|
+
/** Read-only audit: never emit package sources, environment, accounts or raw errors. */
|
|
17
|
+
export async function auditUbuntu(run: Runner): Promise<UbuntuAudit> {
|
|
18
|
+
const release = await run(['cat', '/etc/os-release']);
|
|
19
|
+
const arch = await run(['dpkg', '--print-architecture']);
|
|
20
|
+
const packages = await run([
|
|
21
|
+
'dpkg-query',
|
|
22
|
+
'-W',
|
|
23
|
+
'-f=${Package}\t${Version}\n',
|
|
24
|
+
...PACKAGES,
|
|
25
|
+
]);
|
|
26
|
+
const services: Record<string, boolean> = {};
|
|
27
|
+
for (const name of ['ssh', 'tailscaled', 'beszel-agent', 'docker']) {
|
|
28
|
+
services[name] = (
|
|
29
|
+
await run(['systemctl', 'is-active', `${name}.service`])
|
|
30
|
+
).ok;
|
|
31
|
+
}
|
|
32
|
+
const version =
|
|
33
|
+
/^VERSION_ID="?([0-9]+\.[0-9]+)"?$/m.exec(release.stdout)?.[1] ?? null;
|
|
34
|
+
const reported = arch.stdout.trim();
|
|
35
|
+
const architecture = policy.architectures.includes(reported)
|
|
36
|
+
? reported
|
|
37
|
+
: null;
|
|
38
|
+
return {
|
|
39
|
+
supported:
|
|
40
|
+
release.ok &&
|
|
41
|
+
/^ID=ubuntu$/m.test(release.stdout) &&
|
|
42
|
+
version === policy.release &&
|
|
43
|
+
architecture !== null,
|
|
44
|
+
release: version,
|
|
45
|
+
architecture,
|
|
46
|
+
packages: packageVersions(packages.stdout),
|
|
47
|
+
desktop: (await run(['test', '-d', '/usr/share/xsessions'])).ok,
|
|
48
|
+
services,
|
|
49
|
+
passwordlessSudo: (await run(['sudo', '-n', 'true'])).ok,
|
|
50
|
+
rebootPending: (await run(['test', '-f', '/var/run/reboot-required'])).ok,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
function packageVersions(output: string): Record<string, string> {
|
|
54
|
+
const versions: Record<string, string> = {};
|
|
55
|
+
for (const line of output.split('\n')) {
|
|
56
|
+
const [name, version] = line.split('\t');
|
|
57
|
+
if (
|
|
58
|
+
name &&
|
|
59
|
+
PACKAGES.includes(name) &&
|
|
60
|
+
version &&
|
|
61
|
+
/^[a-zA-Z0-9.+:~_-]{1,100}$/.test(version)
|
|
62
|
+
)
|
|
63
|
+
versions[name] = version;
|
|
64
|
+
}
|
|
65
|
+
return versions;
|
|
66
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import type { Check, Runner } from '../types';
|
|
2
|
+
|
|
3
|
+
const LOGIND = {
|
|
4
|
+
HandleLidSwitch: 's "ignore"',
|
|
5
|
+
HandleLidSwitchDocked: 's "ignore"',
|
|
6
|
+
HandleLidSwitchExternalPower: 's "ignore"',
|
|
7
|
+
IdleAction: 's "ignore"',
|
|
8
|
+
KillUserProcesses: 'b false',
|
|
9
|
+
StopIdleSessionUSec: 't 18446744073709551615',
|
|
10
|
+
};
|
|
11
|
+
const POWER_FILES = [
|
|
12
|
+
'/etc/systemd/logind.conf.d/60-personal-fleet.conf',
|
|
13
|
+
'/etc/systemd/sleep.conf.d/60-personal-fleet.conf',
|
|
14
|
+
];
|
|
15
|
+
/** Inspect the running manager, not an installer receipt. Reboot acceptance
|
|
16
|
+
* additionally requires the managed files to predate this kernel boot.
|
|
17
|
+
*/
|
|
18
|
+
export async function verifyUbuntuBoot(
|
|
19
|
+
user: string,
|
|
20
|
+
run: Runner
|
|
21
|
+
): Promise<Check[]> {
|
|
22
|
+
for (const [property, expected] of Object.entries(LOGIND)) {
|
|
23
|
+
const observed = await run([
|
|
24
|
+
'busctl',
|
|
25
|
+
'get-property',
|
|
26
|
+
'org.freedesktop.login1',
|
|
27
|
+
'/org/freedesktop/login1',
|
|
28
|
+
'org.freedesktop.login1.Manager',
|
|
29
|
+
property,
|
|
30
|
+
]);
|
|
31
|
+
if (!observed.ok || observed.stdout.trim() !== expected)
|
|
32
|
+
return pending(
|
|
33
|
+
'UBUNTU_SESSION_POLICY_PENDING',
|
|
34
|
+
'The running login manager does not expose the declared ' +
|
|
35
|
+
property +
|
|
36
|
+
'. Keep active work intact and schedule an explicit maintenance reboot; do not restart logind during sessions.'
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
for (const name of [
|
|
40
|
+
'sleep',
|
|
41
|
+
'suspend',
|
|
42
|
+
'hibernate',
|
|
43
|
+
'hybrid-sleep',
|
|
44
|
+
'suspend-then-hibernate',
|
|
45
|
+
]) {
|
|
46
|
+
const observed = await run(['systemctl', 'is-enabled', name + '.target']);
|
|
47
|
+
if (observed.stdout.trim() !== 'masked')
|
|
48
|
+
return pending(
|
|
49
|
+
'UBUNTU_SLEEP_POLICY_DRIFT',
|
|
50
|
+
'A declared sleep transition is not masked. Reapply the approved Ubuntu baseline; preserve user sessions.'
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
const linger = await run([
|
|
54
|
+
'loginctl',
|
|
55
|
+
'show-user',
|
|
56
|
+
user,
|
|
57
|
+
'--property=Linger',
|
|
58
|
+
'--value',
|
|
59
|
+
]);
|
|
60
|
+
if (!linger.ok || linger.stdout.trim() !== 'yes')
|
|
61
|
+
return pending(
|
|
62
|
+
'UBUNTU_LINGER_REQUIRED',
|
|
63
|
+
'The designated user is not observed as lingering. Reapply the approved user policy before claiming logout survival.'
|
|
64
|
+
);
|
|
65
|
+
if (!(await filesPredateBoot(run)))
|
|
66
|
+
return pending(
|
|
67
|
+
'UBUNTU_MAINTENANCE_REBOOT_PENDING',
|
|
68
|
+
'Effective session policy matches, but the managed power files were not observed before this boot. Perform an explicit maintenance reboot and rerun the manager command.'
|
|
69
|
+
);
|
|
70
|
+
return [
|
|
71
|
+
{
|
|
72
|
+
id: 'ubuntu:boot-policy',
|
|
73
|
+
status: 'pass',
|
|
74
|
+
summary:
|
|
75
|
+
'Managed power files predate this boot; the running login manager exposes the declared no-sleep/session policy, sleep transitions are masked, and designated-user lingering is active.',
|
|
76
|
+
},
|
|
77
|
+
];
|
|
78
|
+
}
|
|
79
|
+
async function filesPredateBoot(run: Runner): Promise<boolean> {
|
|
80
|
+
const boot = await run(['cat', '/proc/stat']);
|
|
81
|
+
const seconds = Number(/^btime (\d+)$/m.exec(boot.stdout)?.[1]);
|
|
82
|
+
if (!boot.ok || !Number.isSafeInteger(seconds) || seconds <= 0) return false;
|
|
83
|
+
for (const path of POWER_FILES) {
|
|
84
|
+
const observed = await run(['stat', '--format=%Y', path]);
|
|
85
|
+
const modified = Number(observed.stdout.trim());
|
|
86
|
+
if (
|
|
87
|
+
!observed.ok ||
|
|
88
|
+
!Number.isSafeInteger(modified) ||
|
|
89
|
+
modified <= 0 ||
|
|
90
|
+
modified >= seconds
|
|
91
|
+
)
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
function pending(code: string, action: string): Check[] {
|
|
97
|
+
return [
|
|
98
|
+
{
|
|
99
|
+
id: 'ubuntu:boot-policy',
|
|
100
|
+
status: 'fail',
|
|
101
|
+
summary: code,
|
|
102
|
+
remedy: { action },
|
|
103
|
+
},
|
|
104
|
+
];
|
|
105
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { homedir } from 'node:os';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { execFileSync, spawnSync } from 'node:child_process';
|
|
4
|
+
|
|
5
|
+
import type {
|
|
6
|
+
NotificationKind,
|
|
7
|
+
NotifierCapability,
|
|
8
|
+
NotifierTestResult,
|
|
9
|
+
Platform,
|
|
10
|
+
ProcessInfo,
|
|
11
|
+
ScheduledJob,
|
|
12
|
+
ScheduledJobStatus,
|
|
13
|
+
} from './types';
|
|
14
|
+
import { existsSync } from 'node:fs';
|
|
15
|
+
|
|
16
|
+
export class WindowsPlatform implements Platform {
|
|
17
|
+
readonly name = 'windows' as const;
|
|
18
|
+
readonly label = 'Windows';
|
|
19
|
+
|
|
20
|
+
opencodeDir(): string {
|
|
21
|
+
const base =
|
|
22
|
+
process.env['APPDATA']?.trim() || join(homedir(), 'AppData/Roaming');
|
|
23
|
+
return join(base, 'opencode');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
cacheDir(): string {
|
|
27
|
+
const base =
|
|
28
|
+
process.env['LOCALAPPDATA']?.trim() || join(homedir(), 'AppData/Local');
|
|
29
|
+
return join(base, 'opencode-cache');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
globalBinDir(): string {
|
|
33
|
+
return join(homedir(), '.local/bin');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
canBuildSwiftNotifier(): boolean {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
shellRcFiles(): readonly string[] {
|
|
41
|
+
return [];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
notifierCapabilities(): NotifierCapability {
|
|
45
|
+
return {
|
|
46
|
+
native: true,
|
|
47
|
+
sounds: false,
|
|
48
|
+
clickToFocus: false,
|
|
49
|
+
reason:
|
|
50
|
+
'Windows uses a PowerShell toast fallback (no sounds, no click-to-focus); the macOS OpenCodeNotifier app is macOS-only.',
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async postNotification(input: {
|
|
55
|
+
title: string;
|
|
56
|
+
message: string;
|
|
57
|
+
kind: NotificationKind;
|
|
58
|
+
}): Promise<NotifierTestResult> {
|
|
59
|
+
void input.kind;
|
|
60
|
+
try {
|
|
61
|
+
const ps = [
|
|
62
|
+
'-NoProfile',
|
|
63
|
+
'-NonInteractive',
|
|
64
|
+
'-Command',
|
|
65
|
+
`New-BurntToastNotification -Text ${JSON.stringify(input.title)}, ${JSON.stringify(input.message)}`,
|
|
66
|
+
];
|
|
67
|
+
execFileSync('powershell', ps, { stdio: 'ignore' });
|
|
68
|
+
return { ok: true, detail: 'posted via BurntToast' };
|
|
69
|
+
} catch {
|
|
70
|
+
// Fall through to msg.exe.
|
|
71
|
+
}
|
|
72
|
+
const r = spawnSync('msg', ['*', `${input.title}: ${input.message}`], {
|
|
73
|
+
stdio: 'ignore',
|
|
74
|
+
});
|
|
75
|
+
if (r.status === 0) return { ok: true, detail: 'posted via msg' };
|
|
76
|
+
return {
|
|
77
|
+
ok: false,
|
|
78
|
+
detail:
|
|
79
|
+
'no notifier found (install BurntToast: Install-Module BurntToast)',
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
isNotifierDaemonRunning(): boolean {
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
restartNotifierDaemon(): void {
|
|
88
|
+
// No daemon on Windows — no-op.
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
availableSystemSounds(): string[] {
|
|
92
|
+
return [];
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async playSystemSound(name: string): Promise<NotifierTestResult> {
|
|
96
|
+
void name;
|
|
97
|
+
return {
|
|
98
|
+
ok: false,
|
|
99
|
+
detail: 'sounds unsupported on Windows (toast fallback has no sound API)',
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
listProcesses(): ProcessInfo[] | null {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
tailscaleAppBinaries(): readonly string[] {
|
|
108
|
+
return ['C:\\Program Files\\Tailscale\\tailscale.exe'].filter((p) =>
|
|
109
|
+
existsSync(p)
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
vscodeSettingsPath(): string | null {
|
|
114
|
+
const base =
|
|
115
|
+
process.env['APPDATA']?.trim() || join(homedir(), 'AppData/Roaming');
|
|
116
|
+
return join(base, 'Code/User/settings.json');
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
vscodeCliBinaries(): readonly string[] {
|
|
120
|
+
const base =
|
|
121
|
+
process.env['LOCALAPPDATA']?.trim() || join(homedir(), 'AppData/Local');
|
|
122
|
+
return [join(base, 'Programs/Microsoft VS Code/bin/code.cmd')].filter((p) =>
|
|
123
|
+
existsSync(p)
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
installScheduledJob(job: ScheduledJob): NotifierTestResult {
|
|
128
|
+
return {
|
|
129
|
+
ok: false,
|
|
130
|
+
detail: `scheduled jobs are not supported on ${this.label} (${job.label})`,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
removeScheduledJob(label: string): NotifierTestResult {
|
|
135
|
+
return {
|
|
136
|
+
ok: true,
|
|
137
|
+
detail: `nothing to remove for ${label} on ${this.label}`,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
scheduledJobStatus(label: string): ScheduledJobStatus {
|
|
142
|
+
return {
|
|
143
|
+
supported: false,
|
|
144
|
+
installed: false,
|
|
145
|
+
detail: `not supported on ${this.label} (${label})`,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { record } from './config';
|
|
2
|
+
import type { Check, Runner } from './types';
|
|
3
|
+
|
|
4
|
+
/** Only recognize named firmware settings; an enabled OS service is not proof of AC recovery. */
|
|
5
|
+
export async function powerRecoveryCheck(run: Runner): Promise<Check> {
|
|
6
|
+
const result = await run(
|
|
7
|
+
['sudo', '-n', 'fwupdmgr', 'get-bios-settings', '--json'],
|
|
8
|
+
{ timeoutMs: 15_000 }
|
|
9
|
+
);
|
|
10
|
+
try {
|
|
11
|
+
const data: unknown = JSON.parse(result.stdout);
|
|
12
|
+
const settings =
|
|
13
|
+
record(data) && Array.isArray(data['BiosSettings'])
|
|
14
|
+
? data['BiosSettings']
|
|
15
|
+
: [];
|
|
16
|
+
const observed = settings.find(
|
|
17
|
+
(item) =>
|
|
18
|
+
record(item) &&
|
|
19
|
+
[
|
|
20
|
+
'RestoreACPowerLoss',
|
|
21
|
+
'AfterPowerLoss',
|
|
22
|
+
'PowerOnAfterPowerFail',
|
|
23
|
+
'ACPowerRecovery',
|
|
24
|
+
].includes(String(item['Name']))
|
|
25
|
+
);
|
|
26
|
+
if (result.ok && record(observed)) {
|
|
27
|
+
const enabled = ['Power On', 'Always On', 'PowerOn', 'On'].includes(
|
|
28
|
+
String(observed['CurrentValue'])
|
|
29
|
+
);
|
|
30
|
+
return {
|
|
31
|
+
id: 'worker:power-loss-recovery',
|
|
32
|
+
status: enabled ? 'pass' : 'fail',
|
|
33
|
+
summary: enabled
|
|
34
|
+
? 'Firmware reports an enabled restore-after-AC-loss setting; a real power-loss test remains distinct from configuration evidence.'
|
|
35
|
+
: 'Firmware restore-after-AC-loss is disabled.',
|
|
36
|
+
remedy: {
|
|
37
|
+
action:
|
|
38
|
+
'Enable the machine firmware restore-after-AC-loss setting or configure its VM host power recovery.',
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
} catch {
|
|
43
|
+
/* Firmware output may contain hardware identifiers; never expose it. */
|
|
44
|
+
}
|
|
45
|
+
return {
|
|
46
|
+
id: 'worker:power-loss-recovery',
|
|
47
|
+
status: 'skip',
|
|
48
|
+
summary:
|
|
49
|
+
'Automatic power-on after AC loss is not observable through a supported firmware adapter on this machine.',
|
|
50
|
+
remedy: {
|
|
51
|
+
action:
|
|
52
|
+
'Enable and test restore-on-AC-power in the firmware or VM host during initial enrollment. An OS unit cannot power on a switched-off machine.',
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
}
|
package/src/probe.ts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { dependencyCheck, fingerprint } from './fingerprint';
|
|
2
|
+
import { CAPABILITIES } from './config';
|
|
3
|
+
import { authCheck } from './auth-checks';
|
|
4
|
+
import type { Capability, Check, Runner, Snapshot } from './types';
|
|
5
|
+
|
|
6
|
+
export async function probe(
|
|
7
|
+
root: string,
|
|
8
|
+
required: Capability[],
|
|
9
|
+
run: Runner,
|
|
10
|
+
nodeId?: string
|
|
11
|
+
): Promise<Snapshot> {
|
|
12
|
+
const current = await fingerprint(root, run);
|
|
13
|
+
const checks: Check[] = [
|
|
14
|
+
{
|
|
15
|
+
id: 'workspace',
|
|
16
|
+
status:
|
|
17
|
+
Object.values(current.files).every(Boolean) && current.head
|
|
18
|
+
? 'pass'
|
|
19
|
+
: 'fail',
|
|
20
|
+
summary:
|
|
21
|
+
'Checkout revision and shared dependency/toolchain input availability.',
|
|
22
|
+
},
|
|
23
|
+
];
|
|
24
|
+
const dirty = await run(['git', 'status', '--porcelain'], { cwd: root });
|
|
25
|
+
checks.push({
|
|
26
|
+
id: 'git:clean',
|
|
27
|
+
status: dirty.ok ? (dirty.stdout.trim() ? 'warn' : 'pass') : 'fail',
|
|
28
|
+
summary:
|
|
29
|
+
dirty.ok && !dirty.stdout.trim()
|
|
30
|
+
? 'Working tree is clean.'
|
|
31
|
+
: 'Working tree is dirty or unreadable; sync --pull will refuse.',
|
|
32
|
+
});
|
|
33
|
+
checks.push(await dependencyCheck(root, current));
|
|
34
|
+
for (const capability of CAPABILITIES) {
|
|
35
|
+
if (required.includes(capability))
|
|
36
|
+
checks.push(...(await authCheck(capability, run, root, nodeId)));
|
|
37
|
+
else
|
|
38
|
+
checks.push({
|
|
39
|
+
id: `auth:${capability}`,
|
|
40
|
+
status: 'skip',
|
|
41
|
+
required: false,
|
|
42
|
+
summary: 'Not required by this node configuration.',
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
return {
|
|
46
|
+
version: 1,
|
|
47
|
+
platform: process.platform,
|
|
48
|
+
fingerprint: current,
|
|
49
|
+
checks,
|
|
50
|
+
};
|
|
51
|
+
}
|
package/src/protocol.ts
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { record } from './config';
|
|
2
|
+
import { FINGERPRINT_FILES } from './input-hashes';
|
|
3
|
+
import type { Check, Snapshot } from './types';
|
|
4
|
+
|
|
5
|
+
const STATUSES = ['pass', 'warn', 'fail', 'skip'];
|
|
6
|
+
|
|
7
|
+
export function validCheck(value: unknown): value is Check {
|
|
8
|
+
if (
|
|
9
|
+
!record(value) ||
|
|
10
|
+
typeof value['id'] !== 'string' ||
|
|
11
|
+
!/^[a-z0-9:._-]{1,100}$/.test(value['id']) ||
|
|
12
|
+
!STATUSES.includes(String(value['status'])) ||
|
|
13
|
+
typeof value['summary'] !== 'string' ||
|
|
14
|
+
value['summary'].length > 2000
|
|
15
|
+
)
|
|
16
|
+
return false;
|
|
17
|
+
if (!validDuration(value['phaseDurationMs'])) return false;
|
|
18
|
+
if (!validRequired(value['required'])) return false;
|
|
19
|
+
const remedy = value['remedy'];
|
|
20
|
+
return remedy === undefined || validRemedy(remedy);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function validRemedy(value: unknown): boolean {
|
|
24
|
+
return (
|
|
25
|
+
record(value) &&
|
|
26
|
+
typeof value['action'] === 'string' &&
|
|
27
|
+
value['action'].length < 2000 &&
|
|
28
|
+
(value['command'] === undefined || typeof value['command'] === 'string')
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function validSnapshot(value: unknown): value is Snapshot {
|
|
33
|
+
if (
|
|
34
|
+
!record(value) ||
|
|
35
|
+
value['version'] !== 1 ||
|
|
36
|
+
typeof value['platform'] !== 'string' ||
|
|
37
|
+
!Array.isArray(value['checks']) ||
|
|
38
|
+
value['checks'].length > 200 ||
|
|
39
|
+
!value['checks'].every(validCheck)
|
|
40
|
+
)
|
|
41
|
+
return false;
|
|
42
|
+
return validFingerprint(value['fingerprint']);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function validFingerprint(value: unknown): boolean {
|
|
46
|
+
if (!record(value) || !record(value['files'])) return false;
|
|
47
|
+
if (
|
|
48
|
+
!Object.keys(value['files']).every((key) => FINGERPRINT_FILES.includes(key))
|
|
49
|
+
)
|
|
50
|
+
return false;
|
|
51
|
+
const head = value['head'];
|
|
52
|
+
return (
|
|
53
|
+
(head === null ||
|
|
54
|
+
(typeof head === 'string' && /^[a-f0-9]{40,64}$/.test(head))) &&
|
|
55
|
+
Object.values(value['files']).every(validHash)
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function validHash(value: unknown): boolean {
|
|
60
|
+
return (
|
|
61
|
+
value === null ||
|
|
62
|
+
(typeof value === 'string' && /^[a-f0-9]{64}$/.test(value))
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function decodeSnapshot(output: string): Snapshot | null {
|
|
67
|
+
try {
|
|
68
|
+
const data: unknown = JSON.parse(output);
|
|
69
|
+
return validSnapshot(data) ? cleanSnapshot(data) : null;
|
|
70
|
+
} catch {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function cleanCheck(check: Check): Check {
|
|
76
|
+
return {
|
|
77
|
+
id: check.id,
|
|
78
|
+
status: check.status,
|
|
79
|
+
summary: check.summary,
|
|
80
|
+
...(check.required !== undefined ? { required: check.required } : {}),
|
|
81
|
+
...(check.phaseDurationMs !== undefined
|
|
82
|
+
? { phaseDurationMs: check.phaseDurationMs }
|
|
83
|
+
: {}),
|
|
84
|
+
...(check.remedy
|
|
85
|
+
? {
|
|
86
|
+
remedy: {
|
|
87
|
+
action: check.remedy.action,
|
|
88
|
+
...(check.remedy.command ? { command: check.remedy.command } : {}),
|
|
89
|
+
},
|
|
90
|
+
}
|
|
91
|
+
: {}),
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function cleanSnapshot(snapshot: Snapshot): Snapshot {
|
|
96
|
+
return {
|
|
97
|
+
version: 1,
|
|
98
|
+
platform: snapshot.platform,
|
|
99
|
+
fingerprint: {
|
|
100
|
+
head: snapshot.fingerprint.head,
|
|
101
|
+
files: snapshot.fingerprint.files,
|
|
102
|
+
},
|
|
103
|
+
checks: snapshot.checks.map(cleanCheck),
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function validDuration(value: unknown): boolean {
|
|
108
|
+
return (
|
|
109
|
+
value === undefined ||
|
|
110
|
+
(typeof value === 'number' &&
|
|
111
|
+
Number.isSafeInteger(value) &&
|
|
112
|
+
value >= 0 &&
|
|
113
|
+
value <= 86_400_000)
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function validRequired(value: unknown): boolean {
|
|
118
|
+
return value === undefined || typeof value === 'boolean';
|
|
119
|
+
}
|