@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
package/src/sync.ts
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { unlink } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { fingerprint, writeReceipt } from './fingerprint';
|
|
4
|
+
import type { Runner, Check, Fingerprint } from './types';
|
|
5
|
+
|
|
6
|
+
export function syncPlan(pull: boolean): string[] {
|
|
7
|
+
return [
|
|
8
|
+
'Validate the checkout and shared input files; invalidate any prior sync receipt.',
|
|
9
|
+
...(pull
|
|
10
|
+
? [
|
|
11
|
+
'Refuse dirty or detached checkouts; git pull --ff-only (no reset, stash, force or rebase).',
|
|
12
|
+
]
|
|
13
|
+
: []),
|
|
14
|
+
'Run mise exec --locked bun -- bun bootstrap/setup.ts --non-interactive (shared tools, user mise/PATH, frozen dependencies).',
|
|
15
|
+
'Record input hashes only after every step succeeds.',
|
|
16
|
+
'Run fleet-status afterward; workstation/provider configuration and logins remain explicit ws/operator actions.',
|
|
17
|
+
];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function syncWorkspace(
|
|
21
|
+
root: string,
|
|
22
|
+
pull: boolean,
|
|
23
|
+
run: Runner
|
|
24
|
+
): Promise<Check[]> {
|
|
25
|
+
const before = await fingerprint(root, run);
|
|
26
|
+
if (!completeFingerprint(before)) {
|
|
27
|
+
return failed(
|
|
28
|
+
'Not a complete workspace checkout. Follow fleet-nodes --onboard first.'
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
if (pull) {
|
|
32
|
+
const dirty = await run(['git', 'status', '--porcelain'], { cwd: root });
|
|
33
|
+
const branch = await run(['git', 'symbolic-ref', '--quiet', 'HEAD'], {
|
|
34
|
+
cwd: root,
|
|
35
|
+
});
|
|
36
|
+
if (!dirty.ok || dirty.stdout.trim() || !branch.ok)
|
|
37
|
+
return failed(
|
|
38
|
+
'Refusing pull: dirty, detached, or unreadable checkout. Reconcile manually.'
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
await unlink(join(root, 'node_modules/.fleet-sync.json')).catch(
|
|
42
|
+
() => undefined
|
|
43
|
+
);
|
|
44
|
+
if (pull) {
|
|
45
|
+
const result = await run(['git', 'pull', '--ff-only'], {
|
|
46
|
+
cwd: root,
|
|
47
|
+
timeoutMs: 120_000,
|
|
48
|
+
});
|
|
49
|
+
if (!result.ok)
|
|
50
|
+
return failed(
|
|
51
|
+
'Fast-forward pull failed. Inspect upstream/network/auth manually; nothing was reset.'
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
const expected = await fingerprint(root, run);
|
|
55
|
+
const result = await run(
|
|
56
|
+
[
|
|
57
|
+
'mise',
|
|
58
|
+
'exec',
|
|
59
|
+
'--locked',
|
|
60
|
+
'bun',
|
|
61
|
+
'--',
|
|
62
|
+
'bun',
|
|
63
|
+
'bootstrap/setup.ts',
|
|
64
|
+
'--non-interactive',
|
|
65
|
+
],
|
|
66
|
+
{ cwd: root, timeoutMs: 1_200_000 }
|
|
67
|
+
);
|
|
68
|
+
if (!result.ok)
|
|
69
|
+
return failed(
|
|
70
|
+
`Shared toolchain/dependency sync failed (${result.reason}). Inspect setup locally; partial installs are possible.`
|
|
71
|
+
);
|
|
72
|
+
const after = await fingerprint(root, run);
|
|
73
|
+
if (JSON.stringify(expected) !== JSON.stringify(after))
|
|
74
|
+
return failed(
|
|
75
|
+
'Checkout inputs changed during sync. Retry from a stable checkout.'
|
|
76
|
+
);
|
|
77
|
+
await writeReceipt(root, after);
|
|
78
|
+
return [
|
|
79
|
+
{
|
|
80
|
+
id: 'sync',
|
|
81
|
+
status: 'pass',
|
|
82
|
+
summary:
|
|
83
|
+
'Shared setup succeeded; frozen-sync receipt recorded. Auth and live connectivity still need doctor checks.',
|
|
84
|
+
},
|
|
85
|
+
];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function failed(summary: string): Check[] {
|
|
89
|
+
return [
|
|
90
|
+
{
|
|
91
|
+
id: 'sync',
|
|
92
|
+
status: 'fail',
|
|
93
|
+
summary,
|
|
94
|
+
remedy: {
|
|
95
|
+
action:
|
|
96
|
+
'Resolve locally using docs/machine-setup.md, then review and retry devfleet fleet-repair --repair workspace.',
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
];
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function completeFingerprint(value: Fingerprint): boolean {
|
|
103
|
+
return value.head !== null && Object.values(value.files).every(Boolean);
|
|
104
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import {
|
|
2
|
+
accountSlug as slug,
|
|
3
|
+
discoverAccounts,
|
|
4
|
+
type Account,
|
|
5
|
+
} from '../agents/accounts';
|
|
6
|
+
import { proxyKey } from '../agents/secrets';
|
|
7
|
+
|
|
8
|
+
export type Roster = () => Promise<Account[]>;
|
|
9
|
+
|
|
10
|
+
/** The accounts signed in on the CLI proxy, read with this node's own key.
|
|
11
|
+
* Throws when the proxy does not answer, so providers are never dropped
|
|
12
|
+
* because the roster was unreadable.
|
|
13
|
+
*/
|
|
14
|
+
export const proxyAccounts: Roster = async () =>
|
|
15
|
+
discoverAccounts(await proxyKey());
|
|
16
|
+
|
|
17
|
+
/** How T3 runs each tool for one account. The launcher pins every request to
|
|
18
|
+
* the account named by WS_AGENT_ACCOUNT; no credential is ever placed here.
|
|
19
|
+
*/
|
|
20
|
+
const TOOLS: Record<
|
|
21
|
+
string,
|
|
22
|
+
{ driver: string; label: string; config: Record<string, string> }
|
|
23
|
+
> = {
|
|
24
|
+
claude: {
|
|
25
|
+
driver: 'claudeAgent',
|
|
26
|
+
label: 'Claude',
|
|
27
|
+
config: {
|
|
28
|
+
binaryPath: '${workspace}/agents/claude-proxy',
|
|
29
|
+
homePath: '${agents}/${node}/claude/account-${account}',
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
codex: {
|
|
33
|
+
driver: 'codex',
|
|
34
|
+
label: 'Codex',
|
|
35
|
+
config: {
|
|
36
|
+
binaryPath: '${workspace}/agents/codex-proxy.ts',
|
|
37
|
+
homePath: '${agents}/${node}/codex/account-${account}',
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
opencode: {
|
|
41
|
+
driver: 'opencode',
|
|
42
|
+
label: 'AntiGravity',
|
|
43
|
+
config: {
|
|
44
|
+
binaryPath: '${workspace}/agents/opencode-proxy.ts',
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/** One `<Provider> <account email>` instance per signed-in account, identical
|
|
50
|
+
* on every machine because every machine reads the same proxy.
|
|
51
|
+
*/
|
|
52
|
+
export function accountInstances(
|
|
53
|
+
accounts: Account[],
|
|
54
|
+
variables: Record<string, string>
|
|
55
|
+
): Record<string, unknown> {
|
|
56
|
+
const instances: Record<string, unknown> = {};
|
|
57
|
+
for (const account of accounts) {
|
|
58
|
+
const tool = TOOLS[account.tool];
|
|
59
|
+
if (!tool) continue;
|
|
60
|
+
const values: Record<string, string> = {
|
|
61
|
+
...variables,
|
|
62
|
+
account: slug(account.email),
|
|
63
|
+
};
|
|
64
|
+
instances[`personal_${slug(account.prefix)}`.slice(0, 64)] = {
|
|
65
|
+
driver: tool.driver,
|
|
66
|
+
displayName: `${tool.label} ${account.email}`,
|
|
67
|
+
enabled: true,
|
|
68
|
+
config: Object.fromEntries(
|
|
69
|
+
Object.entries(tool.config).map(([key, value]) => [
|
|
70
|
+
key,
|
|
71
|
+
value.replace(/\$\{(\w+)\}/g, (_, name: string) => values[name]!),
|
|
72
|
+
])
|
|
73
|
+
),
|
|
74
|
+
environment: [
|
|
75
|
+
{ name: 'WS_AGENT_NODE', value: variables['node'], sensitive: false },
|
|
76
|
+
{ name: 'WS_AGENT_ACCOUNT', value: account.email, sensitive: false },
|
|
77
|
+
],
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
return instances;
|
|
81
|
+
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { join, resolve } from 'node:path';
|
|
2
|
+
import { homedir, hostname } from 'node:os';
|
|
3
|
+
import { FleetUsageError } from '../errors';
|
|
4
|
+
import { object, read } from './files';
|
|
5
|
+
import { proxyName } from './names';
|
|
6
|
+
import { accountInstances, proxyAccounts, type Roster } from './accounts';
|
|
7
|
+
|
|
8
|
+
export interface T3Options {
|
|
9
|
+
id?: string;
|
|
10
|
+
t3Home?: string;
|
|
11
|
+
definition?: string;
|
|
12
|
+
planHash?: string;
|
|
13
|
+
dryRun?: boolean;
|
|
14
|
+
import?: string;
|
|
15
|
+
/** Internal: the proxy account roster (read live when omitted). */
|
|
16
|
+
roster?: Roster;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function nodeId(value?: string): string {
|
|
20
|
+
const id =
|
|
21
|
+
value ??
|
|
22
|
+
hostname()
|
|
23
|
+
.toLowerCase()
|
|
24
|
+
.replace(/[^a-z0-9-]/g, '-')
|
|
25
|
+
.slice(0, 63);
|
|
26
|
+
if (!/^[a-z0-9][a-z0-9-]{0,62}$/.test(id))
|
|
27
|
+
throw new FleetUsageError('Invalid node ID. Use --id NAME.');
|
|
28
|
+
return id;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function paths(options: T3Options, workspace: string) {
|
|
32
|
+
const base = resolve(
|
|
33
|
+
options.t3Home ?? process.env['T3CODE_HOME'] ?? join(homedir(), '.t3')
|
|
34
|
+
);
|
|
35
|
+
return {
|
|
36
|
+
settings: join(base, 'userdata/settings.json'),
|
|
37
|
+
state: join(base, 'userdata/personal-managed.json'),
|
|
38
|
+
definition: resolve(
|
|
39
|
+
options.definition ?? join(workspace, 'agents/t3.json')
|
|
40
|
+
),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** The managed instances: the fixed ones in the definition plus one per
|
|
45
|
+
* provider account signed in on the CLI proxy (`roster`, read live).
|
|
46
|
+
*/
|
|
47
|
+
export async function desired(
|
|
48
|
+
path: string,
|
|
49
|
+
workspace: string,
|
|
50
|
+
id: string,
|
|
51
|
+
roster: Roster = proxyAccounts
|
|
52
|
+
) {
|
|
53
|
+
const raw = await read(path);
|
|
54
|
+
if (raw === null)
|
|
55
|
+
throw new FleetUsageError('T3 definition is missing in this checkout.');
|
|
56
|
+
const doc = object(raw);
|
|
57
|
+
if (
|
|
58
|
+
doc['version'] !== 1 ||
|
|
59
|
+
Object.keys(doc).some((key) => !['version', 'instances'].includes(key))
|
|
60
|
+
)
|
|
61
|
+
throw new FleetUsageError(
|
|
62
|
+
'Unsupported T3 definition. Expected version 1 and instances.'
|
|
63
|
+
);
|
|
64
|
+
const instances = object(JSON.stringify(doc['instances'] ?? null));
|
|
65
|
+
for (const [key, value] of Object.entries(instances))
|
|
66
|
+
validateInstance(key, value);
|
|
67
|
+
const agents =
|
|
68
|
+
process.platform === 'win32'
|
|
69
|
+
? join(homedir(), 'AppData/Local/personal/agents')
|
|
70
|
+
: join(homedir(), '.config/personal/agents');
|
|
71
|
+
const variables: Record<string, string> = {
|
|
72
|
+
workspace: resolve(workspace),
|
|
73
|
+
agents,
|
|
74
|
+
node: id,
|
|
75
|
+
};
|
|
76
|
+
// Substitute individual strings, not serialized JSON: paths may contain quotes/backslashes.
|
|
77
|
+
return {
|
|
78
|
+
...(expand(instances, variables) as Record<string, unknown>),
|
|
79
|
+
...accountInstances(await roster(), variables),
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function expand(value: unknown, variables: Record<string, string>): unknown {
|
|
84
|
+
if (typeof value === 'string')
|
|
85
|
+
return value.replace(/\$\{([^}]+)\}/g, (_, key: string) => {
|
|
86
|
+
if (!(key in variables))
|
|
87
|
+
throw new FleetUsageError('Unknown variable in T3 definition.');
|
|
88
|
+
return variables[key]!;
|
|
89
|
+
});
|
|
90
|
+
if (Array.isArray(value)) return value.map((item) => expand(item, variables));
|
|
91
|
+
if (value && typeof value === 'object')
|
|
92
|
+
return Object.fromEntries(
|
|
93
|
+
Object.entries(value).map(([key, item]) => [key, expand(item, variables)])
|
|
94
|
+
);
|
|
95
|
+
return value;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Each fixed driver accepts only the adapter fields fleet sets, under its
|
|
99
|
+
* conventional `<Provider> CLI Proxy` name. Credentials never appear here (T3
|
|
100
|
+
* refuses a managed policy that holds one): wrappers read them from Vault at
|
|
101
|
+
* launch.
|
|
102
|
+
*/
|
|
103
|
+
const DRIVERS: Record<
|
|
104
|
+
string,
|
|
105
|
+
{ required: string[]; env?: Record<string, string> }
|
|
106
|
+
> = {
|
|
107
|
+
opencode: { required: ['binaryPath'] },
|
|
108
|
+
grok: { required: ['binaryPath'], env: { XAI_API_KEY: 'fleet-cli-proxy' } },
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
function validateInstance(id: string, value: unknown): void {
|
|
112
|
+
const instance = object(JSON.stringify(value));
|
|
113
|
+
const config = object(JSON.stringify(instance['config']));
|
|
114
|
+
const driver = DRIVERS[String(instance['driver'])];
|
|
115
|
+
if (
|
|
116
|
+
!/^[a-zA-Z][a-zA-Z0-9_-]{0,63}$/.test(id) ||
|
|
117
|
+
!driver ||
|
|
118
|
+
instance['displayName'] !== proxyName(String(instance['driver'])) ||
|
|
119
|
+
typeof instance['enabled'] !== 'boolean' ||
|
|
120
|
+
Object.keys(instance).some(
|
|
121
|
+
(key) =>
|
|
122
|
+
!['driver', 'displayName', 'enabled', 'config', 'environment'].includes(
|
|
123
|
+
key
|
|
124
|
+
)
|
|
125
|
+
) ||
|
|
126
|
+
!validConfig(config, driver) ||
|
|
127
|
+
!validEnvironment(instance['environment'], driver.env ?? {})
|
|
128
|
+
)
|
|
129
|
+
throw new FleetUsageError(
|
|
130
|
+
'Invalid T3 instance definition; only the supported managed adapter fields are accepted.'
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function validConfig(
|
|
135
|
+
config: Record<string, unknown>,
|
|
136
|
+
driver: { required: string[] }
|
|
137
|
+
): boolean {
|
|
138
|
+
return (
|
|
139
|
+
Object.keys(config).every((key) => driver.required.includes(key)) &&
|
|
140
|
+
driver.required.every((key) => typeof config[key] === 'string')
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** WS_AGENT_NODE first, then exactly the driver's fixed non-secret values. */
|
|
145
|
+
function validEnvironment(
|
|
146
|
+
env: unknown,
|
|
147
|
+
fixed: Record<string, string>
|
|
148
|
+
): boolean {
|
|
149
|
+
const extra = Object.entries(fixed);
|
|
150
|
+
if (!Array.isArray(env) || env.length !== 1 + extra.length) return false;
|
|
151
|
+
const [node, ...rest] = env.map((item) => object(JSON.stringify(item)));
|
|
152
|
+
return (
|
|
153
|
+
node!['name'] === 'WS_AGENT_NODE' &&
|
|
154
|
+
node!['value'] === '${node}' &&
|
|
155
|
+
node!['sensitive'] === false &&
|
|
156
|
+
Object.keys(node!).length === 3 &&
|
|
157
|
+
rest.every(
|
|
158
|
+
(variable, index) =>
|
|
159
|
+
variable['name'] === extra[index]![0] &&
|
|
160
|
+
variable['value'] === extra[index]![1] &&
|
|
161
|
+
variable['sensitive'] === false &&
|
|
162
|
+
Object.keys(variable).length === 3
|
|
163
|
+
)
|
|
164
|
+
);
|
|
165
|
+
}
|
package/src/t3/files.ts
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import {
|
|
2
|
+
lstat,
|
|
3
|
+
mkdir,
|
|
4
|
+
readFile,
|
|
5
|
+
rename,
|
|
6
|
+
rm,
|
|
7
|
+
writeFile,
|
|
8
|
+
} from 'node:fs/promises';
|
|
9
|
+
import { dirname, parse, resolve } from 'node:path';
|
|
10
|
+
import { randomUUID, createHash } from 'node:crypto';
|
|
11
|
+
import { FleetUsageError } from '../errors';
|
|
12
|
+
|
|
13
|
+
export const hash = (value: string): string =>
|
|
14
|
+
createHash('sha256').update(value).digest('hex');
|
|
15
|
+
|
|
16
|
+
export async function safePath(path: string): Promise<void> {
|
|
17
|
+
for (
|
|
18
|
+
let current = resolve(path);
|
|
19
|
+
current !== parse(current).root;
|
|
20
|
+
current = dirname(current)
|
|
21
|
+
) {
|
|
22
|
+
try {
|
|
23
|
+
if ((await lstat(current)).isSymbolicLink())
|
|
24
|
+
throw new FleetUsageError('Managed T3 paths cannot be symlinks.');
|
|
25
|
+
} catch (error) {
|
|
26
|
+
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function read(path: string): Promise<string | null> {
|
|
32
|
+
await safePath(path);
|
|
33
|
+
try {
|
|
34
|
+
const info = await lstat(path);
|
|
35
|
+
if (!info.isFile() || info.size > 4 * 1024 * 1024)
|
|
36
|
+
throw new FleetUsageError(
|
|
37
|
+
'T3 configuration must be a regular file smaller than 4 MiB.'
|
|
38
|
+
);
|
|
39
|
+
return await readFile(path, 'utf8');
|
|
40
|
+
} catch (error) {
|
|
41
|
+
if ((error as NodeJS.ErrnoException).code === 'ENOENT') return null;
|
|
42
|
+
throw error;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export async function write(path: string, contents: string): Promise<void> {
|
|
47
|
+
await safePath(path);
|
|
48
|
+
await mkdir(dirname(path), { recursive: true, mode: 0o700 });
|
|
49
|
+
const temporary = `${path}.${randomUUID()}.tmp`;
|
|
50
|
+
try {
|
|
51
|
+
await writeFile(temporary, contents, { mode: 0o600, flag: 'wx' });
|
|
52
|
+
await rename(temporary, path);
|
|
53
|
+
} finally {
|
|
54
|
+
await rm(temporary, { force: true });
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function object(raw: string | null): Record<string, unknown> {
|
|
59
|
+
if (raw === null) return {};
|
|
60
|
+
try {
|
|
61
|
+
const value: unknown = JSON.parse(raw);
|
|
62
|
+
if (value && typeof value === 'object' && !Array.isArray(value))
|
|
63
|
+
return value as Record<string, unknown>;
|
|
64
|
+
} catch {
|
|
65
|
+
/* No contents or parser details in errors. */
|
|
66
|
+
}
|
|
67
|
+
throw new FleetUsageError(
|
|
68
|
+
'Invalid T3 JSON object. Repair the file before reconciliation.'
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function canonical(value: unknown): string {
|
|
73
|
+
if (Array.isArray(value)) return `[${value.map(canonical).join(',')}]`;
|
|
74
|
+
if (value && typeof value === 'object') {
|
|
75
|
+
return `{${Object.entries(value)
|
|
76
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
77
|
+
.map(([key, item]) => `${JSON.stringify(key)}:${canonical(item)}`)
|
|
78
|
+
.join(',')}}`;
|
|
79
|
+
}
|
|
80
|
+
return JSON.stringify(value) ?? 'null';
|
|
81
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { desired, nodeId, paths } from './desired';
|
|
2
|
+
import { conformT3Names } from './names';
|
|
3
|
+
import type { Roster } from './accounts';
|
|
4
|
+
import type { Check } from '../types';
|
|
5
|
+
|
|
6
|
+
/** Fleet check: this machine's T3 lists every provider under the convention. */
|
|
7
|
+
export async function providerNames(
|
|
8
|
+
id: string,
|
|
9
|
+
workspace: string,
|
|
10
|
+
apply: boolean,
|
|
11
|
+
{ t3Home, roster }: { t3Home?: string; roster?: Roster } = {}
|
|
12
|
+
): Promise<Check> {
|
|
13
|
+
try {
|
|
14
|
+
const target = paths(t3Home ? { t3Home } : {}, workspace);
|
|
15
|
+
const managed = Object.fromEntries(
|
|
16
|
+
Object.entries(
|
|
17
|
+
await desired(target.definition, workspace, nodeId(id), roster)
|
|
18
|
+
).map(([instance, value]) => [
|
|
19
|
+
instance,
|
|
20
|
+
String((value as { displayName?: unknown }).displayName),
|
|
21
|
+
])
|
|
22
|
+
);
|
|
23
|
+
const { changed } = await conformT3Names(target.settings, managed, {
|
|
24
|
+
apply,
|
|
25
|
+
});
|
|
26
|
+
return changed && !apply
|
|
27
|
+
? {
|
|
28
|
+
id: 't3:provider-names',
|
|
29
|
+
status: 'warn',
|
|
30
|
+
summary: 'T3_PROVIDER_NAMES_PENDING',
|
|
31
|
+
remedy: {
|
|
32
|
+
action:
|
|
33
|
+
'fleet-sync renames these providers to the convention when applied.',
|
|
34
|
+
},
|
|
35
|
+
}
|
|
36
|
+
: {
|
|
37
|
+
id: 't3:provider-names',
|
|
38
|
+
status: 'pass',
|
|
39
|
+
summary:
|
|
40
|
+
'Every T3 provider is named "<Provider> CLI Proxy" or "<Provider> <account email>".',
|
|
41
|
+
};
|
|
42
|
+
} catch {
|
|
43
|
+
return {
|
|
44
|
+
id: 't3:provider-names',
|
|
45
|
+
status: 'fail',
|
|
46
|
+
summary: 'T3_PROVIDER_NAMES_UNVERIFIED',
|
|
47
|
+
remedy: {
|
|
48
|
+
action:
|
|
49
|
+
'T3 settings were unreadable or changed during the rename. Rerun fleet-sync; only provider display names are written.',
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
}
|
package/src/t3/names.ts
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { object, read, write } from './files';
|
|
5
|
+
|
|
6
|
+
/** One naming convention for every provider T3 lists, on every machine:
|
|
7
|
+
* `<Provider> CLI Proxy` for a fleet-managed gateway instance and
|
|
8
|
+
* `<Provider> <account email>` for a native login.
|
|
9
|
+
*/
|
|
10
|
+
export const PROVIDER_NAMES: Record<string, string> = {
|
|
11
|
+
claudeAgent: 'Claude',
|
|
12
|
+
codex: 'Codex',
|
|
13
|
+
antigravity: 'AntiGravity',
|
|
14
|
+
grok: 'Grok',
|
|
15
|
+
opencode: 'OpenCode',
|
|
16
|
+
cursor: 'Cursor',
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export const proxyName = (driver: string): string | undefined =>
|
|
20
|
+
PROVIDER_NAMES[driver] && `${PROVIDER_NAMES[driver]} CLI Proxy`;
|
|
21
|
+
|
|
22
|
+
/** The account email is the only identity shown; without one the provider
|
|
23
|
+
* name stands alone rather than a guess.
|
|
24
|
+
*/
|
|
25
|
+
export const accountName = (
|
|
26
|
+
driver: string,
|
|
27
|
+
email?: string
|
|
28
|
+
): string | undefined =>
|
|
29
|
+
PROVIDER_NAMES[driver] &&
|
|
30
|
+
(email ? `${PROVIDER_NAMES[driver]} ${email}` : PROVIDER_NAMES[driver]);
|
|
31
|
+
|
|
32
|
+
export type Accounts = (
|
|
33
|
+
driver: string,
|
|
34
|
+
homePath?: string
|
|
35
|
+
) => Promise<string | undefined>;
|
|
36
|
+
|
|
37
|
+
const EMAIL = /^[^\s@]{1,64}@[^\s@]{1,255}$/;
|
|
38
|
+
const email = (value: unknown): string | undefined =>
|
|
39
|
+
typeof value === 'string' && EMAIL.test(value) ? value : undefined;
|
|
40
|
+
|
|
41
|
+
async function json(path: string): Promise<Record<string, unknown>> {
|
|
42
|
+
return object(await readFile(path, 'utf8'));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Reads only the signed-in address from each client's own login record; a
|
|
46
|
+
* token is never returned, logged or sent anywhere.
|
|
47
|
+
*/
|
|
48
|
+
export const localAccounts: Accounts = async (driver, homePath) => {
|
|
49
|
+
try {
|
|
50
|
+
if (driver === 'claudeAgent') {
|
|
51
|
+
const account = (await json(join(homePath ?? homedir(), '.claude.json')))[
|
|
52
|
+
'oauthAccount'
|
|
53
|
+
];
|
|
54
|
+
return email((account as Record<string, unknown>)?.['emailAddress']);
|
|
55
|
+
}
|
|
56
|
+
if (driver === 'codex') {
|
|
57
|
+
const home =
|
|
58
|
+
homePath ?? process.env['CODEX_HOME'] ?? join(homedir(), '.codex');
|
|
59
|
+
const tokens = (await json(join(home, 'auth.json')))['tokens'];
|
|
60
|
+
const claims = String(
|
|
61
|
+
(tokens as Record<string, unknown>)?.['id_token']
|
|
62
|
+
).split('.')[1];
|
|
63
|
+
return email(
|
|
64
|
+
object(Buffer.from(claims ?? '', 'base64url').toString('utf8'))['email']
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
} catch {
|
|
68
|
+
/* Signed out or unreadable: the provider name stands alone. */
|
|
69
|
+
}
|
|
70
|
+
return undefined;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
type Instance = Record<string, unknown>;
|
|
74
|
+
const homeOf = (instance: Instance): string | undefined => {
|
|
75
|
+
const home = (instance['config'] as Record<string, unknown> | undefined)?.[
|
|
76
|
+
'homePath'
|
|
77
|
+
];
|
|
78
|
+
return typeof home === 'string' ? home : undefined;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
/** Every instance T3 lists gets its conventional name. Managed instances
|
|
82
|
+
* (`managed`: ID to name) are named by their definition; every other instance is renamed after its own
|
|
83
|
+
* signed-in account, and a signed-in default login T3 lists without a settings
|
|
84
|
+
* entry gains one. Only `displayName` is ever written.
|
|
85
|
+
*/
|
|
86
|
+
export async function conformNames(
|
|
87
|
+
current: Record<string, unknown>,
|
|
88
|
+
managed: Record<string, string>,
|
|
89
|
+
accounts: Accounts = localAccounts
|
|
90
|
+
): Promise<Record<string, unknown>> {
|
|
91
|
+
const next: Record<string, unknown> = { ...current };
|
|
92
|
+
const named = new Set<string>();
|
|
93
|
+
for (const [id, value] of Object.entries(current)) {
|
|
94
|
+
// A worker's policy owns its managed instances; a copy left in its
|
|
95
|
+
// settings still shows until the policy loads, so it conforms too.
|
|
96
|
+
const renamed = Object.hasOwn(managed, id)
|
|
97
|
+
? rename(value, () => managed[id])
|
|
98
|
+
: rename(value, accountName, await account(value, accounts));
|
|
99
|
+
if (!renamed) continue;
|
|
100
|
+
named.add(String(renamed['displayName']));
|
|
101
|
+
next[id] = renamed;
|
|
102
|
+
}
|
|
103
|
+
// T3 lists a driver's default instance (its ID is the driver) without an entry.
|
|
104
|
+
for (const driver of Object.keys(PROVIDER_NAMES)) {
|
|
105
|
+
const account = await accounts(driver);
|
|
106
|
+
const name = account && accountName(driver, account);
|
|
107
|
+
if (!name || named.has(name) || Object.hasOwn(current, driver)) continue;
|
|
108
|
+
next[driver] = { driver, displayName: name };
|
|
109
|
+
}
|
|
110
|
+
return next;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const account = (value: unknown, accounts: Accounts) =>
|
|
114
|
+
value && typeof value === 'object'
|
|
115
|
+
? accounts(String((value as Instance)['driver']), homeOf(value as Instance))
|
|
116
|
+
: undefined;
|
|
117
|
+
|
|
118
|
+
/** The instance under its conventional name; undefined for an unknown driver. */
|
|
119
|
+
function rename(
|
|
120
|
+
value: unknown,
|
|
121
|
+
convention: (driver: string, email?: string) => string | undefined,
|
|
122
|
+
email?: string
|
|
123
|
+
): Instance | undefined {
|
|
124
|
+
if (!value || typeof value !== 'object') return undefined;
|
|
125
|
+
const instance = value as Instance;
|
|
126
|
+
const name = convention(String(instance['driver']), email);
|
|
127
|
+
if (!name) return undefined;
|
|
128
|
+
return instance['displayName'] === name
|
|
129
|
+
? instance
|
|
130
|
+
: { ...instance, displayName: name };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Applies the convention to this machine's T3 settings. T3 watches the file,
|
|
134
|
+
* so names change live; nothing is restarted. Returns whether it changed.
|
|
135
|
+
*/
|
|
136
|
+
export async function conformT3Names(
|
|
137
|
+
settingsPath: string,
|
|
138
|
+
managed: Record<string, string>,
|
|
139
|
+
options: { apply: boolean; accounts?: Accounts }
|
|
140
|
+
): Promise<{ changed: boolean }> {
|
|
141
|
+
const raw = await read(settingsPath);
|
|
142
|
+
const settings = object(raw);
|
|
143
|
+
const current = object(JSON.stringify(settings['providerInstances'] ?? {}));
|
|
144
|
+
const next = await conformNames(current, managed, options.accounts);
|
|
145
|
+
if (JSON.stringify(next) === JSON.stringify(current))
|
|
146
|
+
return { changed: false };
|
|
147
|
+
if (!options.apply) return { changed: true };
|
|
148
|
+
// T3 also writes this file: never overwrite a change made since the read.
|
|
149
|
+
if ((await read(settingsPath)) !== raw) throw new Error('T3_SETTINGS_BUSY');
|
|
150
|
+
await write(
|
|
151
|
+
settingsPath,
|
|
152
|
+
JSON.stringify({ ...settings, providerInstances: next }, null, 2) + '\n'
|
|
153
|
+
);
|
|
154
|
+
return { changed: true };
|
|
155
|
+
}
|