@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,226 @@
|
|
|
1
|
+
import { mkdtemp, readFile, rm } from 'node:fs/promises';
|
|
2
|
+
import { tmpdir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import type { Check, Runner } from '../types';
|
|
5
|
+
import { pinnedPath } from './toolchain';
|
|
6
|
+
import { registry } from './registry';
|
|
7
|
+
import {
|
|
8
|
+
cacheKey,
|
|
9
|
+
cachedCheck,
|
|
10
|
+
sourceKey,
|
|
11
|
+
type VerifyCache,
|
|
12
|
+
} from './verify-cache';
|
|
13
|
+
|
|
14
|
+
const PROMPT = 'Reply with exactly: OK';
|
|
15
|
+
|
|
16
|
+
/** Every agent T3 offers on every node, launched exactly as T3 launches it:
|
|
17
|
+
* through the tracked wrapper that injects the shared credential. `pin` names
|
|
18
|
+
* the mise.toml tool whose locked version the installed binary must report.
|
|
19
|
+
*/
|
|
20
|
+
export const NATIVE_AGENTS = [
|
|
21
|
+
{
|
|
22
|
+
name: 'claude',
|
|
23
|
+
binary: 'claude',
|
|
24
|
+
pin: 'claude',
|
|
25
|
+
wrapper: 'claude-proxy.ts',
|
|
26
|
+
args: ['-p', PROMPT],
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
name: 'codex',
|
|
30
|
+
binary: 'codex',
|
|
31
|
+
pin: 'codex',
|
|
32
|
+
wrapper: 'codex-proxy.ts',
|
|
33
|
+
args: ['exec', '--skip-git-repo-check', PROMPT],
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
name: 'grok',
|
|
37
|
+
binary: 'grok',
|
|
38
|
+
pin: 'http:grok',
|
|
39
|
+
wrapper: 'grok-proxy.ts',
|
|
40
|
+
args: ['-p', PROMPT],
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
name: 'antigravity',
|
|
44
|
+
binary: 'antigravity',
|
|
45
|
+
pin: 'http:antigravity',
|
|
46
|
+
wrapper: 'antigravity-proxy.ts',
|
|
47
|
+
args: [
|
|
48
|
+
'-p',
|
|
49
|
+
PROMPT,
|
|
50
|
+
'--model',
|
|
51
|
+
'Gemini 3.8 Flash (High)',
|
|
52
|
+
'--print-timeout',
|
|
53
|
+
'90s',
|
|
54
|
+
],
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
name: 'muse',
|
|
58
|
+
binary: 'opencode',
|
|
59
|
+
pin: 'aqua:anomalyco/opencode',
|
|
60
|
+
wrapper: 'opencode-proxy.ts',
|
|
61
|
+
args: ['run', '--model', 'personal_proxy/devin/muse-wonder-1-0', PROMPT],
|
|
62
|
+
},
|
|
63
|
+
] as const;
|
|
64
|
+
|
|
65
|
+
type Agent = (typeof NATIVE_AGENTS)[number];
|
|
66
|
+
|
|
67
|
+
/** Same versions everywhere (from the shared mise.lock) and a working signed-in
|
|
68
|
+
* session through each T3 launcher. Agents run concurrently and are bounded.
|
|
69
|
+
*/
|
|
70
|
+
export async function verifyNativeAgents(
|
|
71
|
+
node: string,
|
|
72
|
+
workspace: string,
|
|
73
|
+
run: Runner,
|
|
74
|
+
options: {
|
|
75
|
+
/** Proxy routes already found out of quota; their sessions are not run. */
|
|
76
|
+
outOfQuota?: Set<string>;
|
|
77
|
+
cache?: VerifyCache;
|
|
78
|
+
} = {}
|
|
79
|
+
): Promise<Check[]> {
|
|
80
|
+
const { outOfQuota = new Set<string>(), cache = {} } = options;
|
|
81
|
+
const pins = parsePins(await readFile(join(workspace, 'mise.toml'), 'utf8'));
|
|
82
|
+
const cwd = await mkdtemp(join(tmpdir(), 'fleet-agents-'));
|
|
83
|
+
try {
|
|
84
|
+
const results = await Promise.all(
|
|
85
|
+
NATIVE_AGENTS.map(async (agent) => [
|
|
86
|
+
await versionCheck(agent, pins[agent.pin], run),
|
|
87
|
+
outOfQuota.has(agent.name)
|
|
88
|
+
? quotaPass(agent)
|
|
89
|
+
: await cachedCheck(
|
|
90
|
+
`agent-${node}-${agent.name}`,
|
|
91
|
+
cacheKey(
|
|
92
|
+
node,
|
|
93
|
+
agent,
|
|
94
|
+
pins[agent.pin] ?? '',
|
|
95
|
+
registry,
|
|
96
|
+
await sourceKey(launcherSources(workspace, agent))
|
|
97
|
+
),
|
|
98
|
+
() => sessionCheck(agent, { node, workspace, cwd }, run),
|
|
99
|
+
cache
|
|
100
|
+
),
|
|
101
|
+
])
|
|
102
|
+
);
|
|
103
|
+
return results.flat();
|
|
104
|
+
} finally {
|
|
105
|
+
await rm(cwd, { recursive: true, force: true });
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Reads `tool = "x"` and `"tool" = { version = "x", ... }` pins. */
|
|
110
|
+
export function parsePins(source: string): Record<string, string> {
|
|
111
|
+
const pins: Record<string, string> = {};
|
|
112
|
+
const pattern =
|
|
113
|
+
/^"?([A-Za-z0-9:@/._-]+)"?\s*=\s*(?:"([^"]+)"|\{[^\n]*?\bversion\s*=\s*"([^"]+)")/gm;
|
|
114
|
+
for (const match of source.matchAll(pattern))
|
|
115
|
+
pins[match[1]!] = (match[2] ?? match[3])!;
|
|
116
|
+
return pins;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Self-healing: a binary that drifted from the lock (a vendor self-updater
|
|
120
|
+
* rewrote it) is reinstalled from the locked artifact, then checked again.
|
|
121
|
+
*/
|
|
122
|
+
async function versionCheck(
|
|
123
|
+
agent: Agent,
|
|
124
|
+
pin: string | undefined,
|
|
125
|
+
run: Runner
|
|
126
|
+
): Promise<Check> {
|
|
127
|
+
const matches = async () => {
|
|
128
|
+
const result = await run([agent.binary, '--version'], {
|
|
129
|
+
env: { PATH: pinnedPath() },
|
|
130
|
+
timeoutMs: 30_000,
|
|
131
|
+
});
|
|
132
|
+
return Boolean(pin) && result.ok && result.stdout.includes(pin!);
|
|
133
|
+
};
|
|
134
|
+
let ok = await matches();
|
|
135
|
+
if (!ok && pin) {
|
|
136
|
+
await run(
|
|
137
|
+
['mise', 'install', '--locked', '--force', `${agent.pin}@${pin}`],
|
|
138
|
+
{
|
|
139
|
+
timeoutMs: 600_000,
|
|
140
|
+
}
|
|
141
|
+
);
|
|
142
|
+
ok = await matches();
|
|
143
|
+
}
|
|
144
|
+
return {
|
|
145
|
+
id: `agent:${agent.name}:version`,
|
|
146
|
+
status: ok ? 'pass' : 'fail',
|
|
147
|
+
summary: ok
|
|
148
|
+
? `${agent.binary} ${pin} matches the shared toolchain lock.`
|
|
149
|
+
: 'AGENT_VERSION_MISMATCH',
|
|
150
|
+
...(ok
|
|
151
|
+
? {}
|
|
152
|
+
: {
|
|
153
|
+
remedy: {
|
|
154
|
+
action: `Every node runs the ${agent.binary} pinned in mise.toml/mise.lock; reinstalling it from the lock failed. Inspect mise install --locked ${agent.pin} on this node; never install it with a vendor self-updater.`,
|
|
155
|
+
},
|
|
156
|
+
}),
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** Quota is the account's, not the node's: no warning (as for providers). */
|
|
161
|
+
function quotaPass(agent: Agent): Check {
|
|
162
|
+
return {
|
|
163
|
+
id: `agent:${agent.name}:session`,
|
|
164
|
+
status: 'pass',
|
|
165
|
+
summary: `The shared ${agent.name} account is rate limited or out of quota; its session is checked again once it recovers.`,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async function sessionCheck(
|
|
170
|
+
agent: Agent,
|
|
171
|
+
context: { node: string; workspace: string; cwd: string },
|
|
172
|
+
run: Runner
|
|
173
|
+
): Promise<Check> {
|
|
174
|
+
const launch = () =>
|
|
175
|
+
run(
|
|
176
|
+
['bun', join(context.workspace, 'agents', agent.wrapper), ...agent.args],
|
|
177
|
+
{
|
|
178
|
+
cwd: context.cwd,
|
|
179
|
+
env: { WS_AGENT_NODE: context.node, PATH: pinnedPath() },
|
|
180
|
+
input: '',
|
|
181
|
+
timeoutMs: 150_000,
|
|
182
|
+
}
|
|
183
|
+
);
|
|
184
|
+
const expected = /\bOK\b/;
|
|
185
|
+
let result = await launch();
|
|
186
|
+
// One retry: a CLI's first launch on a node may still be initializing.
|
|
187
|
+
if (!(result.ok && expected.test(result.stdout))) result = await launch();
|
|
188
|
+
const ok = result.ok && expected.test(result.stdout);
|
|
189
|
+
return {
|
|
190
|
+
id: `agent:${agent.name}:session`,
|
|
191
|
+
status: ok ? 'pass' : 'fail',
|
|
192
|
+
summary: ok
|
|
193
|
+
? `${agent.name} runs signed in through its T3 launcher with the shared account.`
|
|
194
|
+
: sessionFailure(result.stderr + result.stdout),
|
|
195
|
+
...(ok ? {} : { remedy: { action: sessionRemedy(agent) } }),
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** A stable code only; raw CLI output is never reported. */
|
|
200
|
+
function sessionFailure(output: string): string {
|
|
201
|
+
if (/missing or out of date/.test(output)) return 'AGENT_PROFILE_STALE';
|
|
202
|
+
if (/Required secret unavailable/.test(output))
|
|
203
|
+
return 'AGENT_VAULT_UNAVAILABLE';
|
|
204
|
+
if (/model_not_found|unknown provider for model/.test(output))
|
|
205
|
+
return 'AGENT_MODEL_UNSERVED';
|
|
206
|
+
return 'AGENT_SESSION_UNVERIFIED';
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function sessionRemedy(agent: Agent): string {
|
|
210
|
+
if (agent.name === 'antigravity')
|
|
211
|
+
return 'Antigravity sends its own catalog model ids; the proxy serves them through declared aliases that fleet-sync applies on the manager (see proxy:config-pending). Pick Gemini 3.8 Flash in T3: the default Pro model is not served by the shared accounts.';
|
|
212
|
+
return `Run the ${agent.name} launcher on this node to inspect it: WS_AGENT_NODE=<node> bun agents/${agent.wrapper}. Check this node's Vault session and the shared proxy.`;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/** The launcher and profile code a session depends on. */
|
|
216
|
+
function launcherSources(workspace: string, agent: Agent): string[] {
|
|
217
|
+
const agents = join(workspace, 'agents');
|
|
218
|
+
const lib = join(workspace, 'src/agents');
|
|
219
|
+
return [
|
|
220
|
+
join(agents, agent.wrapper),
|
|
221
|
+
join(agents, 'managed-proxy.ts'),
|
|
222
|
+
...['launch.ts', 'render.ts', 'secrets.ts', 'toolchain.ts'].map((name) =>
|
|
223
|
+
join(lib, name)
|
|
224
|
+
),
|
|
225
|
+
];
|
|
226
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { AgentConfigurationError } from './errors';
|
|
2
|
+
|
|
3
|
+
export class ProviderVerificationError extends AgentConfigurationError {
|
|
4
|
+
constructor(
|
|
5
|
+
readonly code: string,
|
|
6
|
+
message: string
|
|
7
|
+
) {
|
|
8
|
+
super(message);
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
export function providerHttpFailure(status: number): ProviderVerificationError {
|
|
12
|
+
const code =
|
|
13
|
+
status === 401 || status === 403
|
|
14
|
+
? 'PROVIDER_AUTH_REJECTED'
|
|
15
|
+
: status === 429
|
|
16
|
+
? 'PROVIDER_RATE_LIMITED'
|
|
17
|
+
: status >= 500
|
|
18
|
+
? 'PROVIDER_UPSTREAM_UNAVAILABLE'
|
|
19
|
+
: 'PROVIDER_PROTOCOL_REJECTED';
|
|
20
|
+
return new ProviderVerificationError(
|
|
21
|
+
code,
|
|
22
|
+
'The gateway rejected a bounded verification request; response content is withheld.'
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
const SAFE_CODES = new Set([
|
|
26
|
+
'PROVIDER_AUTH_REJECTED',
|
|
27
|
+
'PROVIDER_RATE_LIMITED',
|
|
28
|
+
'PROVIDER_UPSTREAM_UNAVAILABLE',
|
|
29
|
+
'PROVIDER_PROTOCOL_REJECTED',
|
|
30
|
+
'PROVIDER_MODEL_UNAVAILABLE',
|
|
31
|
+
'PROVIDER_OUTPUT_UNVERIFIED',
|
|
32
|
+
'PROVIDER_TOOL_RESULT_UNVERIFIED',
|
|
33
|
+
'PROVIDER_EMPTY_RESPONSE',
|
|
34
|
+
'PROVIDER_RESPONSE_LIMIT',
|
|
35
|
+
'PROVIDER_TOOL_MISSING',
|
|
36
|
+
'PROVIDER_TOOL_INVALID',
|
|
37
|
+
'PROVIDER_TOOL_ARGUMENTS_INVALID',
|
|
38
|
+
'PROVIDER_STREAM_INCOMPLETE',
|
|
39
|
+
]);
|
|
40
|
+
export function providerFailureCode(error: unknown): string {
|
|
41
|
+
if (error instanceof ProviderVerificationError && SAFE_CODES.has(error.code))
|
|
42
|
+
return error.code;
|
|
43
|
+
if (error instanceof Error && SAFE_CODES.has(error.message))
|
|
44
|
+
return error.message;
|
|
45
|
+
if (error instanceof Error && error.name === 'TimeoutError')
|
|
46
|
+
return 'PROVIDER_TIMEOUT';
|
|
47
|
+
if (error instanceof TypeError) return 'PROVIDER_NETWORK_UNVERIFIED';
|
|
48
|
+
if (error instanceof SyntaxError) return 'PROVIDER_RESPONSE_INVALID';
|
|
49
|
+
return 'PROVIDER_ROUTE_UNVERIFIED';
|
|
50
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { AgentConfigurationError } from './errors';
|
|
2
|
+
import source from '../../agents/registry.json';
|
|
3
|
+
import { createHash } from 'node:crypto';
|
|
4
|
+
import { fleet } from '../fleet-config';
|
|
5
|
+
|
|
6
|
+
export type Tool = keyof typeof source.defaults;
|
|
7
|
+
export type Profile = 'direct' | 'proxy';
|
|
8
|
+
export type Selection = {
|
|
9
|
+
node: string;
|
|
10
|
+
tool: Tool;
|
|
11
|
+
profile: Profile;
|
|
12
|
+
model: string;
|
|
13
|
+
};
|
|
14
|
+
/** The tracked registry plus the proxy endpoint from this fleet's config. */
|
|
15
|
+
export const registry = {
|
|
16
|
+
...source,
|
|
17
|
+
proxy: { endpoint: fleet.services.proxy.endpoint, ...source.proxy },
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export function select(node: string, tool: string, profile: string): Selection {
|
|
21
|
+
if (!/^[a-z0-9][a-z0-9-]{0,62}$/.test(node))
|
|
22
|
+
throw new AgentConfigurationError('Invalid node ID.');
|
|
23
|
+
if (!(tool in source.defaults) || !['direct', 'proxy'].includes(profile))
|
|
24
|
+
throw new AgentConfigurationError(
|
|
25
|
+
'Unsupported tool/profile; run bun src/agents/cli.ts agents inventory.'
|
|
26
|
+
);
|
|
27
|
+
const id = tool as Tool;
|
|
28
|
+
const model = resolveModel(node, id, profile as Profile);
|
|
29
|
+
if (!model || !/^[a-zA-Z0-9_./:+-]+$/.test(model))
|
|
30
|
+
throw new AgentConfigurationError(
|
|
31
|
+
'No approved model. Add an exact model ID to agents/registry.json and review it.'
|
|
32
|
+
);
|
|
33
|
+
return { node, tool: id, profile: profile as Profile, model };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function resolveModel(
|
|
37
|
+
node: string,
|
|
38
|
+
tool: Tool,
|
|
39
|
+
profile: Profile,
|
|
40
|
+
nodes = source.nodes as Record<
|
|
41
|
+
string,
|
|
42
|
+
Partial<Record<Tool, { direct?: string; proxy?: string }>>
|
|
43
|
+
>
|
|
44
|
+
) {
|
|
45
|
+
const override = nodes[node]?.[tool]?.[profile];
|
|
46
|
+
const models = source.proxy.models as Partial<Record<Tool, string | null>>;
|
|
47
|
+
return (
|
|
48
|
+
override ??
|
|
49
|
+
(profile === 'direct' ? source.defaults[tool].model : models[tool])
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function digest(value: string): string {
|
|
54
|
+
return createHash('sha256').update(value).digest('hex');
|
|
55
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { join, win32 } from 'node:path';
|
|
2
|
+
import type { PlatformName } from '../platform/types';
|
|
3
|
+
import { registry, type Selection } from './registry';
|
|
4
|
+
|
|
5
|
+
/** Dedicated outputs; native login/state directories are never copied. */
|
|
6
|
+
export function agentRoot(home: string, platform: PlatformName): string {
|
|
7
|
+
return platform === 'windows'
|
|
8
|
+
? win32.join(home, 'AppData', 'Local', 'personal', 'agents')
|
|
9
|
+
: join(home, '.config', 'personal', 'agents');
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function render(s: Selection): Record<string, string> {
|
|
13
|
+
const model = JSON.stringify(s.model);
|
|
14
|
+
if (s.tool === 'codex') {
|
|
15
|
+
const provider =
|
|
16
|
+
s.profile === 'proxy'
|
|
17
|
+
? '\nmodel_provider = "personal_proxy"\n[model_providers.personal_proxy]\nname = "Personal CLI Proxy"\nbase_url = ' +
|
|
18
|
+
JSON.stringify(registry.proxy.endpoint + '/v1') +
|
|
19
|
+
'\nenv_key = "CLIPROXY_API_KEY"\nwire_api = "responses"\nrequires_openai_auth = false\nsupports_websockets = false\n'
|
|
20
|
+
: '\n';
|
|
21
|
+
return { 'config.toml': `model = ${model}${provider}` };
|
|
22
|
+
}
|
|
23
|
+
if (s.tool === 'claude')
|
|
24
|
+
return {
|
|
25
|
+
'settings.json': JSON.stringify({ model: s.model }, null, 2) + '\n',
|
|
26
|
+
};
|
|
27
|
+
if (s.tool === 'grok') return { 'config.toml': grokConfig(s) };
|
|
28
|
+
if (s.tool === 'antigravity')
|
|
29
|
+
return {
|
|
30
|
+
// The fleet profile home for direct and one-shot runs (see launchArgs);
|
|
31
|
+
// never the human's ~/.gemini. T3 keeps its own private GEMINI_HOME.
|
|
32
|
+
'.gemini/antigravity-cli/settings.json':
|
|
33
|
+
JSON.stringify({ modelProvider: 'gemini' }, null, 2) + '\n',
|
|
34
|
+
};
|
|
35
|
+
if (s.tool === 'gemini')
|
|
36
|
+
return {
|
|
37
|
+
'.gemini/settings.json':
|
|
38
|
+
JSON.stringify(
|
|
39
|
+
{
|
|
40
|
+
model: { name: s.model },
|
|
41
|
+
...(s.profile === 'proxy'
|
|
42
|
+
? { security: { auth: { selectedType: 'gateway' } } }
|
|
43
|
+
: {}),
|
|
44
|
+
},
|
|
45
|
+
null,
|
|
46
|
+
2
|
|
47
|
+
) + '\n',
|
|
48
|
+
};
|
|
49
|
+
const config =
|
|
50
|
+
s.profile === 'proxy'
|
|
51
|
+
? {
|
|
52
|
+
$schema: 'https://opencode.ai/config.json',
|
|
53
|
+
model: `personal_proxy/${s.model}`,
|
|
54
|
+
provider: {
|
|
55
|
+
personal_proxy: {
|
|
56
|
+
npm: '@ai-sdk/openai',
|
|
57
|
+
options: {
|
|
58
|
+
baseURL: registry.proxy.endpoint + '/v1',
|
|
59
|
+
apiKey: '{env:CLIPROXY_API_KEY}',
|
|
60
|
+
},
|
|
61
|
+
models: Object.fromEntries(
|
|
62
|
+
[
|
|
63
|
+
...Object.values(registry.proxy.routes).map(
|
|
64
|
+
(route) => route.model
|
|
65
|
+
),
|
|
66
|
+
s.model,
|
|
67
|
+
].map((id) => [id, { name: id }])
|
|
68
|
+
),
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
}
|
|
72
|
+
: { $schema: 'https://opencode.ai/config.json', model: s.model };
|
|
73
|
+
return { 'opencode.json': JSON.stringify(config, null, 2) + '\n' };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Grok Build reads custom models only from GROK_HOME/config.toml. The key
|
|
77
|
+
* comes from the environment (env_key), never the file; no grok login.
|
|
78
|
+
*/
|
|
79
|
+
function grokConfig(s: Selection): string {
|
|
80
|
+
// Grok records a one-time skills migration in config.toml; declaring it up
|
|
81
|
+
// front keeps the rendered file unchanged by the CLI (no profile drift).
|
|
82
|
+
const lines = [
|
|
83
|
+
'[cli]',
|
|
84
|
+
'auto_update = false',
|
|
85
|
+
'',
|
|
86
|
+
'[marketplace]',
|
|
87
|
+
'default_skills_installs_purged = true',
|
|
88
|
+
'',
|
|
89
|
+
];
|
|
90
|
+
if (s.profile !== 'proxy')
|
|
91
|
+
return [
|
|
92
|
+
...lines,
|
|
93
|
+
'[models]',
|
|
94
|
+
`default = ${JSON.stringify(s.model)}`,
|
|
95
|
+
'',
|
|
96
|
+
].join('\n');
|
|
97
|
+
return [
|
|
98
|
+
...lines,
|
|
99
|
+
'[models]',
|
|
100
|
+
'default = "personal-proxy"',
|
|
101
|
+
'',
|
|
102
|
+
'[model.personal-proxy]',
|
|
103
|
+
`model = ${JSON.stringify(s.model)}`,
|
|
104
|
+
`name = ${JSON.stringify(`${s.model} (Personal CLI Proxy)`)}`,
|
|
105
|
+
`base_url = ${JSON.stringify(registry.proxy.endpoint + '/v1')}`,
|
|
106
|
+
'env_key = "CLIPROXY_API_KEY"',
|
|
107
|
+
'api_backend = "chat_completions"',
|
|
108
|
+
'',
|
|
109
|
+
].join('\n');
|
|
110
|
+
}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { providerHttpFailure } from './provider-errors';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import { registry } from './registry';
|
|
4
|
+
import { record } from '../config';
|
|
5
|
+
|
|
6
|
+
/** Reasoning models spend output tokens thinking before any text or tool call,
|
|
7
|
+
* so a tight cap ends the response without the tool call or text being
|
|
8
|
+
* verified. Prompts are tiny; this is a ceiling, not an expected length.
|
|
9
|
+
*/
|
|
10
|
+
export const VERIFY_OUTPUT_TOKENS = 1024;
|
|
11
|
+
|
|
12
|
+
/** Supported OpenAI Responses harness route. The only tool is a local echo;
|
|
13
|
+
* model output can never select a command, file, URL or other side effect.
|
|
14
|
+
*/
|
|
15
|
+
export async function verifyRoundTrip(
|
|
16
|
+
model: string,
|
|
17
|
+
key: string,
|
|
18
|
+
request = fetch
|
|
19
|
+
): Promise<void> {
|
|
20
|
+
await verifyStream(model, key, request);
|
|
21
|
+
const nonce = randomUUID();
|
|
22
|
+
const tool = {
|
|
23
|
+
type: 'function',
|
|
24
|
+
name: 'fleet_echo',
|
|
25
|
+
description: 'Return the provided verification nonce.',
|
|
26
|
+
parameters: {
|
|
27
|
+
type: 'object',
|
|
28
|
+
properties: { nonce: { type: 'string', enum: [nonce] } },
|
|
29
|
+
required: ['nonce'],
|
|
30
|
+
additionalProperties: false,
|
|
31
|
+
},
|
|
32
|
+
strict: true,
|
|
33
|
+
};
|
|
34
|
+
const prompt = {
|
|
35
|
+
role: 'user',
|
|
36
|
+
content: `Call fleet_echo with nonce ${nonce}, then reply with its result.`,
|
|
37
|
+
};
|
|
38
|
+
const response = await call(
|
|
39
|
+
model,
|
|
40
|
+
key,
|
|
41
|
+
{
|
|
42
|
+
input: [prompt],
|
|
43
|
+
tools: [tool],
|
|
44
|
+
tool_choice: { type: 'function', name: 'fleet_echo' },
|
|
45
|
+
max_output_tokens: VERIFY_OUTPUT_TOKENS,
|
|
46
|
+
},
|
|
47
|
+
request
|
|
48
|
+
);
|
|
49
|
+
const payload: unknown = JSON.parse(await boundedText(response));
|
|
50
|
+
const invocation = validateInvocation(payload, nonce);
|
|
51
|
+
const final = await call(
|
|
52
|
+
model,
|
|
53
|
+
key,
|
|
54
|
+
{
|
|
55
|
+
input: [
|
|
56
|
+
prompt,
|
|
57
|
+
invocation,
|
|
58
|
+
{
|
|
59
|
+
type: 'function_call_output',
|
|
60
|
+
call_id: invocation.call_id,
|
|
61
|
+
output: nonce,
|
|
62
|
+
},
|
|
63
|
+
],
|
|
64
|
+
tools: [tool],
|
|
65
|
+
tool_choice: 'none',
|
|
66
|
+
max_output_tokens: VERIFY_OUTPUT_TOKENS,
|
|
67
|
+
},
|
|
68
|
+
request
|
|
69
|
+
);
|
|
70
|
+
const completed: unknown = JSON.parse(await boundedText(final));
|
|
71
|
+
if (
|
|
72
|
+
!record(completed) ||
|
|
73
|
+
completed['status'] !== 'completed' ||
|
|
74
|
+
!Array.isArray(completed['output']) ||
|
|
75
|
+
!JSON.stringify(completed['output']).includes(nonce)
|
|
76
|
+
)
|
|
77
|
+
throw new Error('PROVIDER_TOOL_RESULT_UNVERIFIED');
|
|
78
|
+
}
|
|
79
|
+
async function call(
|
|
80
|
+
model: string,
|
|
81
|
+
key: string,
|
|
82
|
+
body: object,
|
|
83
|
+
request: typeof fetch
|
|
84
|
+
): Promise<Response> {
|
|
85
|
+
const response = await request(registry.proxy.endpoint + '/v1/responses', {
|
|
86
|
+
method: 'POST',
|
|
87
|
+
headers: {
|
|
88
|
+
Authorization: `Bearer ${key}`,
|
|
89
|
+
'Content-Type': 'application/json',
|
|
90
|
+
},
|
|
91
|
+
body: JSON.stringify({ model, ...body }),
|
|
92
|
+
redirect: 'error',
|
|
93
|
+
signal: AbortSignal.timeout(45_000),
|
|
94
|
+
});
|
|
95
|
+
if (!response.ok) throw providerHttpFailure(response.status);
|
|
96
|
+
return response;
|
|
97
|
+
}
|
|
98
|
+
async function boundedText(response: Response): Promise<string> {
|
|
99
|
+
if (!response.body) throw new Error('PROVIDER_EMPTY_RESPONSE');
|
|
100
|
+
const reader = response.body.getReader();
|
|
101
|
+
const decoder = new TextDecoder();
|
|
102
|
+
let size = 0;
|
|
103
|
+
let text = '';
|
|
104
|
+
try {
|
|
105
|
+
while (true) {
|
|
106
|
+
const chunk = await reader.read();
|
|
107
|
+
if (chunk.done) break;
|
|
108
|
+
size += chunk.value.byteLength;
|
|
109
|
+
if (size > 512 * 1024) throw new Error('PROVIDER_RESPONSE_LIMIT');
|
|
110
|
+
text += decoder.decode(chunk.value, { stream: true });
|
|
111
|
+
}
|
|
112
|
+
return text + decoder.decode();
|
|
113
|
+
} finally {
|
|
114
|
+
await reader.cancel();
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
function validateInvocation(
|
|
118
|
+
payload: unknown,
|
|
119
|
+
nonce: string
|
|
120
|
+
): {
|
|
121
|
+
type: 'function_call';
|
|
122
|
+
call_id: string;
|
|
123
|
+
name: 'fleet_echo';
|
|
124
|
+
arguments: string;
|
|
125
|
+
} {
|
|
126
|
+
if (!record(payload)) throw new Error('PROVIDER_TOOL_MISSING');
|
|
127
|
+
if (!Array.isArray(payload['output']))
|
|
128
|
+
throw new Error('PROVIDER_TOOL_MISSING');
|
|
129
|
+
const calls = payload['output'].filter(
|
|
130
|
+
(item) => record(item) && item['type'] === 'function_call'
|
|
131
|
+
);
|
|
132
|
+
const call = calls[0];
|
|
133
|
+
if (
|
|
134
|
+
calls.length !== 1 ||
|
|
135
|
+
!record(call) ||
|
|
136
|
+
call['name'] !== 'fleet_echo' ||
|
|
137
|
+
!stringFields(call, ['call_id', 'arguments'])
|
|
138
|
+
)
|
|
139
|
+
throw new Error('PROVIDER_TOOL_INVALID');
|
|
140
|
+
const args: unknown = JSON.parse(String(call['arguments']));
|
|
141
|
+
if (
|
|
142
|
+
!record(args) ||
|
|
143
|
+
Object.keys(args).length !== 1 ||
|
|
144
|
+
args['nonce'] !== nonce
|
|
145
|
+
)
|
|
146
|
+
throw new Error('PROVIDER_TOOL_ARGUMENTS_INVALID');
|
|
147
|
+
return {
|
|
148
|
+
type: 'function_call',
|
|
149
|
+
name: 'fleet_echo',
|
|
150
|
+
call_id: String(call['call_id']),
|
|
151
|
+
arguments: String(call['arguments']),
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async function verifyStream(
|
|
156
|
+
model: string,
|
|
157
|
+
key: string,
|
|
158
|
+
request: typeof fetch
|
|
159
|
+
): Promise<void> {
|
|
160
|
+
const response = await call(
|
|
161
|
+
model,
|
|
162
|
+
key,
|
|
163
|
+
{
|
|
164
|
+
input: 'Reply OK.',
|
|
165
|
+
stream: true,
|
|
166
|
+
max_output_tokens: VERIFY_OUTPUT_TOKENS,
|
|
167
|
+
},
|
|
168
|
+
request
|
|
169
|
+
);
|
|
170
|
+
const body = await boundedText(response);
|
|
171
|
+
const events: unknown[] = body
|
|
172
|
+
.split(/\r?\n/)
|
|
173
|
+
.filter((line) => line.startsWith('data:'))
|
|
174
|
+
.map((line) => line.slice(5).trim())
|
|
175
|
+
.filter((line) => line !== '[DONE]')
|
|
176
|
+
.map((line) => JSON.parse(line));
|
|
177
|
+
const types = events.filter(record).map((event) => event['type']);
|
|
178
|
+
if (
|
|
179
|
+
!types.includes('response.completed') ||
|
|
180
|
+
!types.includes('response.output_text.delta') ||
|
|
181
|
+
types.includes('response.failed') ||
|
|
182
|
+
types.includes('error')
|
|
183
|
+
)
|
|
184
|
+
throw new Error('PROVIDER_STREAM_INCOMPLETE');
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function stringFields(
|
|
188
|
+
value: Record<string, unknown>,
|
|
189
|
+
fields: string[]
|
|
190
|
+
): boolean {
|
|
191
|
+
return fields.every((field) => typeof value[field] === 'string');
|
|
192
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { AgentConfigurationError } from './errors';
|
|
2
|
+
import { secret } from '../fleet-config';
|
|
3
|
+
|
|
4
|
+
/** The key every node presents to the CLI proxy, from this fleet's secrets
|
|
5
|
+
* provider with this node's own access.
|
|
6
|
+
*/
|
|
7
|
+
export async function proxyKey(): Promise<string> {
|
|
8
|
+
const value = await secret('proxy-client-key');
|
|
9
|
+
if (value) return value;
|
|
10
|
+
throw new AgentConfigurationError(
|
|
11
|
+
"Required secret unavailable: proxy-client-key. Restore this node's access to the fleet's secrets provider (see `secrets` in the fleet configuration)."
|
|
12
|
+
);
|
|
13
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { homedir } from 'node:os';
|
|
2
|
+
import { delimiter, join } from 'node:path';
|
|
3
|
+
import { spawnSync } from 'node:child_process';
|
|
4
|
+
|
|
5
|
+
/** mise shims resolve every agent to the version pinned in the shared lock.
|
|
6
|
+
* Putting them first keeps an unmanaged copy elsewhere on PATH (a vendor
|
|
7
|
+
* installer in ~/.local/bin, an nvm global) from shadowing the pinned one.
|
|
8
|
+
*/
|
|
9
|
+
export function pinnedPath(env: NodeJS.ProcessEnv = process.env): string {
|
|
10
|
+
const data =
|
|
11
|
+
env['MISE_DATA_DIR'] ??
|
|
12
|
+
join(env['XDG_DATA_HOME'] ?? join(homedir(), '.local', 'share'), 'mise');
|
|
13
|
+
const shims = join(data, 'shims');
|
|
14
|
+
const rest = (env['PATH'] ?? '')
|
|
15
|
+
.split(delimiter)
|
|
16
|
+
.filter((entry) => entry && entry !== shims);
|
|
17
|
+
return [shims, ...rest].join(delimiter);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** The real pinned executable behind a mise shim. Needed when a launch
|
|
21
|
+
* changes HOME: a shim run under another HOME cannot find mise's trust state.
|
|
22
|
+
*/
|
|
23
|
+
export function pinnedBinary(
|
|
24
|
+
tool: string,
|
|
25
|
+
env: NodeJS.ProcessEnv = process.env
|
|
26
|
+
): string {
|
|
27
|
+
const result = spawnSync('mise', ['which', tool], {
|
|
28
|
+
encoding: 'utf8',
|
|
29
|
+
env: { ...env, PATH: pinnedPath(env) },
|
|
30
|
+
timeout: 15_000,
|
|
31
|
+
});
|
|
32
|
+
const path = result.status === 0 ? result.stdout.trim() : '';
|
|
33
|
+
return path.startsWith('/') ? path : tool;
|
|
34
|
+
}
|