diff --git a/.gitignore b/.gitignore index 72aae85..6af5172 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ node_modules/ out/ +claude/ +claude/ diff --git a/README.md b/README.md index aae8ee5..3c6792e 100644 --- a/README.md +++ b/README.md @@ -205,8 +205,8 @@ Commits are managed via the Vers orchestrator API at `https://api.vers.sh/api/v1 ## Development ```bash -npm install -npm test +bun install +bun test ``` ## Optional flags diff --git a/src/boot.js b/src/boot.js index 32ff11b..2e87808 100644 --- a/src/boot.js +++ b/src/boot.js @@ -17,6 +17,18 @@ function remotePublicUrl(vmId) { export function buildRuntimeEnv(vm, topology, options = {}) { const rootUrl = options.rootUrl || remotePublicUrl(topology.root.vmId); + const llmProxyKey = + options.llmProxyKey && String(options.llmProxyKey).trim() + ? shellQuote(options.llmProxyKey) + : process.env.LLM_PROXY_KEY + ? shellQuote(process.env.LLM_PROXY_KEY) + : ""; + const anthropicApiKey = + options.anthropicApiKey && String(options.anthropicApiKey).trim() + ? shellQuote(options.anthropicApiKey) + : process.env.ANTHROPIC_API_KEY + ? shellQuote(process.env.ANTHROPIC_API_KEY) + : ""; const env = { PORT: "3000", VERS_VM_ID: vm.vmId, @@ -31,20 +43,14 @@ export function buildRuntimeEnv(vm, topology, options = {}) { ? shellQuote(options.versAuthToken) : `\${${topology.env.versAuthTokenEnv}:-}`, VERS_INFRA_URL: shellQuote(rootUrl), - LLM_PROXY_KEY: - options.llmProxyKey && String(options.llmProxyKey).trim() - ? shellQuote(options.llmProxyKey) - : process.env.LLM_PROXY_KEY - ? shellQuote(process.env.LLM_PROXY_KEY) - : "", - // Punkin-pi's AI package requires ANTHROPIC_API_KEY at startup before - // the vers provider is selected via set_model. Alias it to LLM_PROXY_KEY - // so the Anthropic SDK initializes with the vers proxy key. - ANTHROPIC_API_KEY: - options.llmProxyKey && String(options.llmProxyKey).trim() - ? shellQuote(options.llmProxyKey) - : process.env.LLM_PROXY_KEY - ? shellQuote(process.env.LLM_PROXY_KEY) + LLM_PROXY_KEY: llmProxyKey, + // Optional secondary provider key for internal runtime failover. + ANTHROPIC_API_KEY: anthropicApiKey, + REEF_MODEL_PROVIDER: + options.modelProvider && String(options.modelProvider).trim() + ? shellQuote(options.modelProvider) + : process.env.REEF_MODEL_PROVIDER + ? shellQuote(process.env.REEF_MODEL_PROVIDER) : "", REEF_ROLE: vm.runtime.reefRole, REEF_CATEGORY: vm.category, diff --git a/src/orchestrate.js b/src/orchestrate.js index ec9f0ff..e79b700 100644 --- a/src/orchestrate.js +++ b/src/orchestrate.js @@ -191,29 +191,26 @@ async function registerRootFleetRecords(topology, authToken, fetchImpl = fetch) await apiRequest(rootBaseUrl, authToken, "PATCH", `/vm-tree/vms/${encodeURIComponent(topology.root.vmId)}`, { name: topology.root.name, category: topology.root.category, + status: "running", + address: `${topology.root.vmId}.vm.vers.sh`, + lastHeartbeat: Date.now(), reefConfig: topology.root.reefConfig, }, fetchImpl).catch(async () => { await apiRequest(rootBaseUrl, authToken, "POST", "/vm-tree/vms", { vmId: topology.root.vmId, name: topology.root.name, category: topology.root.category, + status: "running", + address: `${topology.root.vmId}.vm.vers.sh`, + lastHeartbeat: Date.now(), reefConfig: topology.root.reefConfig, }, fetchImpl); + await apiRequest(rootBaseUrl, authToken, "PATCH", `/vm-tree/vms/${encodeURIComponent(topology.root.vmId)}`, { + status: "running", + address: `${topology.root.vmId}.vm.vers.sh`, + lastHeartbeat: Date.now(), + }, fetchImpl); }); - - await apiRequest(rootBaseUrl, authToken, "POST", "/registry/vms", { - id: topology.root.vmId, - name: topology.root.name, - role: "infra", - address: `${topology.root.vmId}.vm.vers.sh`, - reefConfig: topology.root.reefConfig, - registeredBy: "vers-fleets", - metadata: { - category: topology.root.category, - publicUrl: rootBaseUrl, - sqliteAuthority: true, - }, - }, fetchImpl); } function writeDeployment(outDir, deployment) { @@ -457,6 +454,7 @@ export async function provisionFleet(input = {}, options = {}) { versApiKey: auth.apiKey, versAuthToken: authToken, llmProxyKey: llmProxy.key, + anthropicApiKey: options.anthropicApiKey || process.env.ANTHROPIC_API_KEY, rootCommitId, goldenCommitId, }); @@ -474,6 +472,7 @@ export async function provisionFleet(input = {}, options = {}) { versApiKey: auth.apiKey, versAuthToken: authToken, llmProxyKey: llmProxy.key, + anthropicApiKey: options.anthropicApiKey || process.env.ANTHROPIC_API_KEY, goldenCommitId, }, ); diff --git a/src/topology.js b/src/topology.js index b4367c8..74bd11f 100644 --- a/src/topology.js +++ b/src/topology.js @@ -13,14 +13,14 @@ function defaultRootVmConfig() { export function defaultSharedOperationalDna() { return { - services: ["bootloader", "cron", "docs", "github", "installer", "lieutenant", "services", "swarm", "ui", "vers-config"], + services: ["bootloader", "cron", "docs", "github", "installer", "lieutenant", "logs", "probe", "services", "signals", "swarm", "ui", "vers-config"], capabilities: ["github", "pi-vers", "punkin", "reef-extension", "vers-fleets"], }; } export function defaultRootAuthorityOverlayDna() { return { - services: ["commits", "registry", "store", "vm-tree"], + services: ["commits", "scheduled", "store", "usage", "vm-tree"], capabilities: ["reef-root", "root-lineage", "sqlite-authority"], }; } diff --git a/test/topology.test.js b/test/topology.test.js index bc6ac4f..d021483 100644 --- a/test/topology.test.js +++ b/test/topology.test.js @@ -23,9 +23,11 @@ test("buildTopology creates root-only sqlite authority topology", () => { assert.equal(topology.profiles.rootAuthorityOverlay.capabilities.includes("sqlite-authority"), true); assert.equal(topology.root.runtime.hasSqliteAuthority, true); assert.equal(topology.root.runtime.profile, "root-with-authority-overlay"); - assert.equal(topology.root.reefConfig.services.includes("registry"), true); assert.equal(topology.root.reefConfig.services.includes("vm-tree"), true); assert.equal(topology.root.reefConfig.services.includes("store"), true); + assert.equal(topology.root.reefConfig.services.includes("scheduled"), true); + assert.equal(topology.root.reefConfig.services.includes("usage"), true); + assert.equal(topology.root.reefConfig.services.includes("probe"), true); assert.equal(topology.root.reefConfig.services.includes("commits"), true); assert.equal(topology.lieutenant, null); assert.deepEqual(topology.swarm, []); @@ -73,6 +75,24 @@ test("buildBootstrapBundle can inline runtime secrets for remote bootstrap", () assert.match(bundle.scripts.root, /LLM_PROXY_KEY='sk-vers-secret'/); }); +test("buildBootstrapBundle prefers a dedicated secondary provider key when provided", () => { + const bundle = buildBootstrapBundle( + { + rootName: "reef-root", + }, + { + rootUrl: "https://infra.vm.vers.sh:3000", + versApiKey: "vers-secret", + versAuthToken: "auth-secret", + llmProxyKey: "sk-vers-secret", + anthropicApiKey: "sk-ant-secret", + }, + ); + + assert.match(bundle.scripts.root, /LLM_PROXY_KEY='sk-vers-secret'/); + assert.match(bundle.scripts.root, /ANTHROPIC_API_KEY='sk-ant-secret'/); +}); + test("buildImageScript produces a secret-free image build script", () => { const topology = buildTopology({ rootName: "reef-root" }); const script = buildImageScript(topology); @@ -89,18 +109,43 @@ test("buildImageScript produces a secret-free image build script", () => { }); test("buildRuntimeScript injects secrets and starts reef", () => { + const originalAnthropicKey = process.env.ANTHROPIC_API_KEY; + delete process.env.ANTHROPIC_API_KEY; + const topology = buildTopology({ rootName: "reef-root", rootVmId: "vm-1" }); + try { + const script = buildRuntimeScript(topology.root, topology, { + versApiKey: "vers-key", + versAuthToken: "auth-token", + llmProxyKey: "sk-vers-proxy", + goldenCommitId: "golden-abc-123", + }); + assert.match(script, /configuring runtime for reef-root/); + assert.match(script, /VERS_API_KEY='vers-key'/); + assert.match(script, /VERS_AUTH_TOKEN='auth-token'/); + assert.match(script, /LLM_PROXY_KEY='sk-vers-proxy'/); + assert.doesNotMatch(script, /ANTHROPIC_API_KEY=/); + assert.match(script, /VERS_GOLDEN_COMMIT_ID='golden-abc-123'/); + assert.match(script, /bun run src\/main\.ts/); + assert.match(script, /reef is healthy/); + } finally { + if (originalAnthropicKey === undefined) { + delete process.env.ANTHROPIC_API_KEY; + } else { + process.env.ANTHROPIC_API_KEY = originalAnthropicKey; + } + } +}); + +test("buildRuntimeScript prefers a dedicated secondary provider key", () => { const topology = buildTopology({ rootName: "reef-root", rootVmId: "vm-1" }); const script = buildRuntimeScript(topology.root, topology, { versApiKey: "vers-key", versAuthToken: "auth-token", llmProxyKey: "sk-vers-proxy", + anthropicApiKey: "sk-ant-secret", goldenCommitId: "golden-abc-123", }); - assert.match(script, /configuring runtime for reef-root/); - assert.match(script, /VERS_API_KEY='vers-key'/); - assert.match(script, /VERS_AUTH_TOKEN='auth-token'/); + assert.match(script, /LLM_PROXY_KEY='sk-vers-proxy'/); - assert.match(script, /VERS_GOLDEN_COMMIT_ID='golden-abc-123'/); - assert.match(script, /bun run src\/main\.ts/); - assert.match(script, /reef is healthy/); + assert.match(script, /ANTHROPIC_API_KEY='sk-ant-secret'/); });