@algosuite/vo-mcp 0.2.0-beta.11 → 0.2.0-beta.13
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/dist/cli.js +6 -0
- package/dist/cli.js.map +2 -2
- package/dist/index.js +6 -0
- package/dist/index.js.map +2 -2
- package/dist/install-cli.js.map +2 -2
- package/dist/login-cli.js.map +2 -2
- package/dist/pair-cli.js.map +2 -2
- package/dist/runner-cli.js +183 -19
- package/dist/runner-cli.js.map +3 -3
- package/dist/runner-supervisor.js +890 -28
- package/dist/runner-supervisor.js.map +4 -4
- package/dist/supervisor-credential-helper.js +8 -2
- package/dist/supervisor-credential-helper.js.map +2 -2
- package/package.json +1 -1
package/dist/pair-cli.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/cloud/pairing.ts", "../src/cloud/credential-store.ts", "../src/cloud/keychain.ts", "../src/pair-cli.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * Device-code pairing for the BYO runner (M1) \u2014 the no-terminal-token onboarding.\n *\n * The runner calls the control-plane `pair/initiate`, shows the friend a short\n * `code` to type at `<dashboard>/pair`, then polls `pair/poll` with its SECRET\n * `poll_token` until the friend authorizes \u2014 at which point it receives a freshly\n * minted `vocred_` and stores it in the OS keychain (via writeStoredCredential).\n *\n * The friend's AI key NEVER flows through here \u2014 this only obtains the\n * control-plane credential the runner needs to claim THEIR tasks.\n */\nimport { hostname, platform } from 'node:os';\n\nimport { writeStoredCredential, type StoredCredential } from './credential-store.js';\n\nexport const DEFAULT_CONTROL_PLANE_URL = 'https://vo-control-plane-bzjphrajaq-uc.a.run.app';\nexport const DEFAULT_DASHBOARD_URL = 'https://algosuite.ai';\n\nexport interface RunPairingDeps {\n readonly env?: Record<string, string | undefined>;\n readonly log?: (message: string) => void;\n readonly fetchImpl?: typeof fetch;\n readonly sleep?: (ms: number) => Promise<void>;\n readonly now?: () => Date;\n /** Test seam: capture the stored credential instead of touching the real keychain. */\n readonly store?: (cred: StoredCredential, storedAtIso: string) => string;\n}\n\nexport interface PairingResult {\n readonly credentialPath: string;\n readonly expires_at: string;\n}\n\n/** ABCD-EFGH grouping for an 8-char code (easier to read aloud / type). */\nexport function formatPairingCode(code: string): string {\n return code.length === 8 ? `${code.slice(0, 4)}-${code.slice(4)}` : code;\n}\n\nasync function readJson(res: { json: () => Promise<unknown> }): Promise<Record<string, unknown>> {\n const body = await res.json().catch(() => ({}));\n return body && typeof body === 'object' ? (body as Record<string, unknown>) : {};\n}\n\n/**\n * Run the full pairing handshake. Resolves once a credential is stored; rejects\n * with a friendly message on expiry / consumption / fatal transport error.\n */\nexport async function runPairing(deps: RunPairingDeps = {}): Promise<PairingResult> {\n const env = deps.env ?? process.env;\n const log = deps.log ?? ((m: string) => console.error(m));\n const fetchImpl = deps.fetchImpl ?? fetch;\n const sleep = deps.sleep ?? ((ms: number) => new Promise<void>((r) => setTimeout(r, ms)));\n const now = deps.now ?? (() => new Date());\n const store = deps.store ?? ((cred: StoredCredential, iso: string) => writeStoredCredential(cred, iso, env));\n\n const controlPlaneUrl = env['VO_CONTROL_PLANE_URL']?.trim() || DEFAULT_CONTROL_PLANE_URL;\n const dashboardUrl = env['VO_DASHBOARD_URL']?.trim() || DEFAULT_DASHBOARD_URL;\n const deviceLabel = `${platform()} on ${hostname()}`.slice(0, 120);\n\n // 1. initiate \u2014 get a display code + a secret poll token.\n const initRes = await fetchImpl(`${controlPlaneUrl}/api/v1/pair/initiate`, {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ device_label: deviceLabel }),\n });\n if (!initRes.ok) throw new Error(`Could not start pairing (server ${initRes.status}).`);\n const init = await readJson(initRes);\n const code = String(init['code'] ?? '');\n const pollToken = String(init['poll_token'] ?? '');\n if (!code || !pollToken) throw new Error('Pairing service returned an incomplete response.');\n const intervalMs = (Number(init['poll_interval_seconds']) || 5) * 1000;\n const expiresAtMs = new Date(String(init['expires_at'] ?? '')).getTime();\n\n // 2. show the friend what to do.\n log('');\n log(' To connect this runner, open this page in your browser:');\n log(` ${dashboardUrl}/pair`);\n log(' and enter this code:');\n log('');\n log(` ${formatPairingCode(code)}`);\n log('');\n log(' Your Anthropic key never leaves this computer. Waiting for you to authorize\u2026');\n\n // 3. poll with the SECRET token until authorized / expired.\n for (;;) {\n if (Number.isFinite(expiresAtMs) && now().getTime() >= expiresAtMs) {\n throw new Error('The pairing code expired before it was authorized. Run `vo-mcp pair` again.');\n }\n await sleep(intervalMs);\n const pollRes = await fetchImpl(`${controlPlaneUrl}/api/v1/pair/poll`, {\n method: 'GET',\n headers: { 'x-vo-poll-token': pollToken },\n });\n if (pollRes.status === 404) {\n throw new Error('The pairing expired. Run `vo-mcp pair` again.');\n }\n if (pollRes.status === 410) {\n throw new Error('This code was already used. Run `vo-mcp pair` again.');\n }\n if (!pollRes.ok) {\n // Transient (rate limit / blip) \u2014 keep waiting.\n continue;\n }\n const body = await readJson(pollRes);\n if (body['status'] === 'pending') continue;\n if (body['status'] === 'authorized' && typeof body['vo_credential'] === 'string') {\n const credentialPath = store(\n {\n vo_credential: body['vo_credential'] as string,\n ...(typeof body['expires_at'] === 'string'\n ? { vo_credential_expires_at: body['expires_at'] as string }\n : {}),\n },\n now().toISOString(),\n );\n return { credentialPath, expires_at: String(body['expires_at'] ?? '') };\n }\n throw new Error('Unexpected response from the pairing service.');\n }\n}\n", "/**\n * Local credential store for the thin-client `vo-mcp login` flow (Increment 3b,\n * Option A \u2014 `docs/vo/vo-command-center-inc3b-login-design-2026-06-05.md`).\n *\n * Persists the per-user Firebase refresh token (the user's OWN credential, never\n * the god-token, never model keys) captured by `login`, so the auto-refreshing\n * token source (Inc 3a) can mint fresh ID tokens across MCP restarts.\n *\n * Storage precedence (Inc 3b.3):\n * 1. **OS keychain** (Windows Credential Manager / macOS Keychain / libsecret)\n * via the optional `@napi-rs/keyring` backend (`keychain.ts`). The DEFAULT\n * when available \u2014 the secret never lands in plaintext on disk.\n * 2. **0600 file** at `$VO_MCP_CREDENTIALS_PATH` or `~/.config/vo-mcp/credentials.json`.\n * The fallback when the keychain is unavailable or disabled\n * (`VO_MCP_DISABLE_KEYCHAIN`). `VO_MCP_CREDENTIALS_PATH` only sets the file\n * LOCATION; force file storage with `VO_MCP_DISABLE_KEYCHAIN`.\n *\n * `env`-supplied tokens (`VO_USER_REFRESH_TOKEN`, etc.) still win over BOTH\n * stores \u2014 that precedence lives upstream in `auth-token-source.ts`.\n *\n * **Single source of truth.** The credential lives in EITHER the keychain OR the\n * file, never both: a write to one store CLEARS the other, so a stale entry can\n * never shadow the current credential on read, and the secret never lingers in\n * plaintext after a migration to the keychain.\n *\n * **Keychain durability.** A keychain-stored credential is only readable while\n * the `@napi-rs/keyring` native module loads. If the module later becomes\n * unavailable (an ABI break across a Node upgrade, a corrupted install), the\n * credential can't be read and the user re-runs `vo-mcp login` \u2014 the same\n * behaviour as `gh` / `gcloud` / `firebase` keychain storage. We deliberately do\n * NOT mirror the secret to a plaintext file as a fallback: that would defeat the\n * entire point of keychain storage (keeping the secret off plaintext disk).\n */\nimport { homedir } from 'node:os';\nimport { join, dirname } from 'node:path';\nimport {\n existsSync,\n mkdirSync,\n readFileSync,\n writeFileSync,\n chmodSync,\n rmSync,\n} from 'node:fs';\n\nimport { keychainAvailable, keychainGet, keychainSet, keychainDelete } from './keychain.js';\n\nexport interface StoredCredential {\n /**\n * Firebase refresh token (long-lived; exchanged for short-lived ID tokens).\n * OPTIONAL since Inc 3b.4b: once a scoped `vo_credential` is minted, the raw\n * refresh token is dropped, so a stored credential may carry ONLY the vocred_.\n */\n readonly refresh_token?: string;\n /** Firebase Web API key (PUBLIC) needed for the securetoken refresh exchange. */\n readonly api_key?: string;\n /**\n * Scoped, revocable VO credential (`vocred_`) minted by the control-plane\n * (Inc 3b.4b). Preferred over the raw refresh token; lets the client present a\n * revocable, server-side credential instead of the Firebase refresh token.\n */\n readonly vo_credential?: string;\n /** ISO-8601 expiry of `vo_credential` (the client re-logs-in past this). */\n readonly vo_credential_expires_at?: string;\n /** The signed-in operator email (diagnostics only). */\n readonly email?: string;\n /** ISO timestamp the credential was stored. */\n readonly stored_at?: string;\n}\n\n/**\n * Pluggable OS-keychain backend. Defaults to the real `@napi-rs/keyring` wrapper;\n * tests inject a deterministic fake so they never touch the host keychain.\n */\nexport interface KeychainBackend {\n available(): boolean;\n get(): string | null;\n set(secret: string): boolean;\n delete(): boolean;\n}\n\nconst realKeychain: KeychainBackend = {\n available: keychainAvailable,\n get: keychainGet,\n set: keychainSet,\n delete: keychainDelete,\n};\n\n/** Human-readable \"location\" returned when the credential was stored in the OS keychain. */\nexport const KEYCHAIN_LOCATION = 'OS keychain (service \"vo-mcp\")';\n\n/** Resolve the credentials file path (env override \u2192 XDG-ish default under home). */\nexport function credentialPath(env: Readonly<Record<string, string | undefined>> = process.env): string {\n const override = env['VO_MCP_CREDENTIALS_PATH']?.trim();\n if (override) return override;\n return join(homedir(), '.config', 'vo-mcp', 'credentials.json');\n}\n\n/**\n * Whether the keychain should be consulted at all (read OR write). False when the\n * native backend is unavailable or `VO_MCP_DISABLE_KEYCHAIN` is set (CI/headless).\n */\nfunction keychainEnabled(\n env: Readonly<Record<string, string | undefined>>,\n keychain: KeychainBackend,\n): boolean {\n const disabled = (env['VO_MCP_DISABLE_KEYCHAIN'] ?? '').trim().toLowerCase();\n if (disabled === '1' || disabled === 'true' || disabled === 'yes') return false;\n return keychain.available();\n}\n\n/** Parse + validate a stored credential blob. Returns null on any problem (never throws). */\nfunction deserialize(raw: string): StoredCredential | null {\n try {\n const parsed = JSON.parse(raw) as Partial<StoredCredential>;\n const refresh = typeof parsed.refresh_token === 'string' ? parsed.refresh_token.trim() : '';\n const apiKey = typeof parsed.api_key === 'string' ? parsed.api_key.trim() : '';\n const voCred = typeof parsed.vo_credential === 'string' ? parsed.vo_credential.trim() : '';\n // Valid if it carries a scoped vocred_ OR a full Firebase refresh pair.\n if (!voCred && (!refresh || !apiKey)) return null;\n return {\n ...(refresh ? { refresh_token: refresh } : {}),\n ...(apiKey ? { api_key: apiKey } : {}),\n ...(voCred ? { vo_credential: voCred } : {}),\n ...(typeof parsed.vo_credential_expires_at === 'string' ? { vo_credential_expires_at: parsed.vo_credential_expires_at } : {}),\n ...(typeof parsed.email === 'string' ? { email: parsed.email } : {}),\n ...(typeof parsed.stored_at === 'string' ? { stored_at: parsed.stored_at } : {}),\n };\n } catch {\n return null;\n }\n}\n\nfunction readFromFile(env: Readonly<Record<string, string | undefined>>): StoredCredential | null {\n try {\n const p = credentialPath(env);\n if (!existsSync(p)) return null;\n return deserialize(readFileSync(p, 'utf8'));\n } catch {\n return null;\n }\n}\n\n/**\n * Read the stored credential, or `null` if absent/unreadable/invalid (never\n * throws). Consults an ENABLED keychain first (regardless of the write-target\n * flags, so a credential written to the keychain is found even if\n * `VO_MCP_CREDENTIALS_PATH` is later set), then the 0600 file.\n */\nexport function readStoredCredential(\n env: Readonly<Record<string, string | undefined>> = process.env,\n keychain: KeychainBackend = realKeychain,\n): StoredCredential | null {\n if (keychainEnabled(env, keychain)) {\n const raw = keychain.get();\n const fromKeychain = raw ? deserialize(raw) : null;\n if (fromKeychain) return fromKeychain;\n }\n return readFromFile(env);\n}\n\nfunction deleteFile(env: Readonly<Record<string, string | undefined>>): void {\n try {\n rmSync(credentialPath(env), { force: true });\n } catch {\n /* best-effort */\n }\n}\n\nfunction writeToFile(\n payload: StoredCredential,\n env: Readonly<Record<string, string | undefined>>,\n): string {\n const p = credentialPath(env);\n mkdirSync(dirname(p), { recursive: true });\n writeFileSync(p, `${JSON.stringify(payload, null, 2)}\\n`, { mode: 0o600 });\n // Best-effort tighten (no-op / throws on some Windows filesystems \u2014 ignore).\n try {\n chmodSync(p, 0o600);\n } catch {\n /* best-effort */\n }\n return p;\n}\n\n/**\n * Persist the credential. Prefers the OS keychain (secret never hits plaintext\n * disk); otherwise writes the 0600 file. Writing to one store CLEARS the other\n * (single source of truth \u2014 no stale shadow, no lingering plaintext). Returns the\n * location it was stored (`KEYCHAIN_LOCATION` or the file path).\n */\nexport function writeStoredCredential(\n cred: StoredCredential,\n storedAt: string,\n env: Readonly<Record<string, string | undefined>> = process.env,\n keychain: KeychainBackend = realKeychain,\n): string {\n const payload: StoredCredential = {\n ...(cred.refresh_token ? { refresh_token: cred.refresh_token } : {}),\n ...(cred.api_key ? { api_key: cred.api_key } : {}),\n ...(cred.vo_credential ? { vo_credential: cred.vo_credential } : {}),\n ...(cred.vo_credential_expires_at ? { vo_credential_expires_at: cred.vo_credential_expires_at } : {}),\n ...(cred.email ? { email: cred.email } : {}),\n stored_at: cred.stored_at ?? storedAt,\n };\n if (keychainEnabled(env, keychain) && keychain.set(JSON.stringify(payload))) {\n // Stored in the keychain \u2192 clear any stale plaintext file so the secret\n // doesn't linger on disk and can't shadow the keychain on read.\n deleteFile(env);\n return KEYCHAIN_LOCATION;\n }\n const p = writeToFile(payload, env);\n // Stored in the file \u2192 clear any stale keychain entry so it can't shadow the\n // newer file credential on read.\n if (keychainEnabled(env, keychain)) keychain.delete();\n return p;\n}\n", "/**\n * Optional OS-keychain backend for the thin-client credential store (Increment\n * 3b.3 \u2014 `docs/vo/vo-command-center-inc3b-login-design-2026-06-05.md` \u00A75/\u00A76).\n *\n * Loads `@napi-rs/keyring` at runtime via `createRequire`, so it is a TRUE\n * optional dependency: if the native module is absent or fails to load\n * (unsupported platform, prebuilt binary missing, headless CI), every function\n * degrades to a no-op and the caller (`credential-store.ts`) falls back to the\n * 0600 file store. `@napi-rs/keyring`'s `Entry` API is SYNCHRONOUS, so the\n * credential store stays synchronous \u2014 no async ripple into the Inc-3a token\n * source that reads it.\n *\n * Why `createRequire` and not a static/dynamic `import`: a static import would\n * make the native module a HARD dependency (a missing prebuilt would crash the\n * MCP at startup); a dynamic `import()` is async (would force the whole read\n * path async). `createRequire(...)` inside a try/catch loads it lazily and\n * synchronously, and a load failure is just \"keychain unavailable\".\n */\nimport { createRequire } from 'node:module';\n\n/** Keychain service + account the single refresh credential is stored under. */\nconst SERVICE = 'vo-mcp';\nconst ACCOUNT = 'refresh-credential';\n\ninterface KeyringEntry {\n getPassword(): string | null;\n setPassword(password: string): void;\n deletePassword(): boolean;\n}\ninterface KeyringModule {\n Entry: new (service: string, account: string) => KeyringEntry;\n}\n\n// undefined = not yet attempted; null = attempted and unavailable.\nlet cached: KeyringModule | null | undefined;\n\nfunction loadKeyring(): KeyringModule | null {\n if (cached !== undefined) return cached;\n try {\n const req = createRequire(import.meta.url);\n const mod = req('@napi-rs/keyring') as Partial<KeyringModule>;\n cached = mod && typeof mod.Entry === 'function' ? (mod as KeyringModule) : null;\n } catch {\n cached = null;\n }\n return cached;\n}\n\n/** True when the OS keychain backend is usable in this runtime. */\nexport function keychainAvailable(): boolean {\n return loadKeyring() !== null;\n}\n\n/** Read the raw stored secret string from the OS keychain, or null. Never throws. */\nexport function keychainGet(): string | null {\n const k = loadKeyring();\n if (!k) return null;\n try {\n return new k.Entry(SERVICE, ACCOUNT).getPassword();\n } catch {\n return null;\n }\n}\n\n/** Store the raw secret string in the OS keychain. Returns true on success. Never throws. */\nexport function keychainSet(secret: string): boolean {\n const k = loadKeyring();\n if (!k) return false;\n try {\n new k.Entry(SERVICE, ACCOUNT).setPassword(secret);\n return true;\n } catch {\n return false;\n }\n}\n\n/**\n * Delete the stored secret from the OS keychain. Returns true if an entry was\n * removed. Never throws \u2014 a no-op (and `false`) when the backend is unavailable\n * or the entry is absent. Used to keep ONE source of truth: when the credential\n * is (re)written to the file, any stale keychain entry is cleared so it can't\n * shadow the newer file credential on read (and vice-versa).\n */\nexport function keychainDelete(): boolean {\n const k = loadKeyring();\n if (!k) return false;\n try {\n return new k.Entry(SERVICE, ACCOUNT).deletePassword();\n } catch {\n return false;\n }\n}\n\n/** Test-only seam to reset the memoised module load. */\nexport function __resetKeychainCache(): void {\n cached = undefined;\n}\n", "#!/usr/bin/env node\n/**\n * `vo-mcp pair` CLI entry point.\n *\n * Device-code pairing \u2014 shows a short code to enter at <dashboard>/pair, waits\n * for the friend to authorize in their browser, then stores the minted\n * credential. No browser loopback, no token paste.\n */\nimport { runPairing } from './cloud/pairing.js';\n\nasync function main(): Promise<void> {\n const log = (m: string): void => console.error(m);\n try {\n const result = await runPairing({ env: process.env, log });\n log('');\n log(`\u2713 Paired! Credential stored at: ${result.credentialPath}`);\n log('');\n log('Next steps:');\n log(' 1. Start your runner: vo-mcp runner');\n log(' 2. Dispatch agents from: https://algosuite.ai/algohq');\n } catch (err: unknown) {\n log(`\u2717 Pairing failed: ${err instanceof Error ? err.message : String(err)}`);\n process.exit(1);\n }\n}\n\nmain().catch((err: unknown) => {\n console.error('[vo-mcp pair] fatal:', err);\n process.exit(1);\n});\n"],
|
|
5
|
-
"mappings": ";;;;AAWA,SAAS,UAAU,gBAAgB;;;ACsBnC,SAAS,eAAe;AACxB,SAAS,MAAM,eAAe;AAC9B;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;;;ACxBP,SAAS,qBAAqB;AAG9B,IAAM,UAAU;AAChB,IAAM,UAAU;AAYhB,IAAI;AAEJ,SAAS,cAAoC;AAC3C,MAAI,WAAW,OAAW,QAAO;AACjC,MAAI;AACF,UAAM,MAAM,cAAc,YAAY,GAAG;AACzC,UAAM,MAAM,IAAI,kBAAkB;AAClC,aAAS,OAAO,OAAO,IAAI,UAAU,aAAc,MAAwB;AAAA,EAC7E,QAAQ;AACN,aAAS;AAAA,EACX;AACA,SAAO;AACT;AAGO,SAAS,oBAA6B;AAC3C,SAAO,YAAY,MAAM;AAC3B;AAGO,SAAS,cAA6B;AAC3C,QAAM,IAAI,YAAY;AACtB,MAAI,CAAC,EAAG,QAAO;AACf,MAAI;AACF,WAAO,IAAI,EAAE,MAAM,SAAS,OAAO,EAAE,YAAY;AAAA,EACnD,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAGO,SAAS,YAAY,QAAyB;AACnD,QAAM,IAAI,YAAY;AACtB,MAAI,CAAC,EAAG,QAAO;AACf,MAAI;AACF,QAAI,EAAE,MAAM,SAAS,OAAO,EAAE,YAAY,MAAM;AAChD,WAAO;AAAA,EACT,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AASO,SAAS,iBAA0B;AACxC,QAAM,IAAI,YAAY;AACtB,MAAI,CAAC,EAAG,QAAO;AACf,MAAI;AACF,WAAO,IAAI,EAAE,MAAM,SAAS,OAAO,EAAE,eAAe;AAAA,EACtD,QAAQ;AACN,WAAO;AAAA,EACT;AACF;;;ADXA,IAAM,eAAgC;AAAA,EACpC,WAAW;AAAA,EACX,KAAK;AAAA,EACL,KAAK;AAAA,EACL,QAAQ;AACV;AAGO,IAAM,oBAAoB;AAG1B,SAAS,eAAe,MAAoD,QAAQ,KAAa;AACtG,QAAM,WAAW,IAAI,yBAAyB,GAAG,KAAK;AACtD,MAAI,SAAU,QAAO;AACrB,SAAO,KAAK,QAAQ,GAAG,WAAW,UAAU,kBAAkB;AAChE;AAMA,SAAS,gBACP,KACA,UACS;AACT,QAAM,YAAY,IAAI,yBAAyB,KAAK,IAAI,KAAK,EAAE,YAAY;AAC3E,MAAI,aAAa,OAAO,aAAa,UAAU,aAAa,MAAO,QAAO;AAC1E,SAAO,SAAS,UAAU;AAC5B;
|
|
4
|
+
"sourcesContent": ["/**\n * Device-code pairing for the BYO runner (M1) \u2014 the no-terminal-token onboarding.\n *\n * The runner calls the control-plane `pair/initiate`, shows the friend a short\n * `code` to type at `<dashboard>/pair`, then polls `pair/poll` with its SECRET\n * `poll_token` until the friend authorizes \u2014 at which point it receives a freshly\n * minted `vocred_` and stores it in the OS keychain (via writeStoredCredential).\n *\n * The friend's AI key NEVER flows through here \u2014 this only obtains the\n * control-plane credential the runner needs to claim THEIR tasks.\n */\nimport { hostname, platform } from 'node:os';\n\nimport { writeStoredCredential, type StoredCredential } from './credential-store.js';\n\nexport const DEFAULT_CONTROL_PLANE_URL = 'https://vo-control-plane-bzjphrajaq-uc.a.run.app';\nexport const DEFAULT_DASHBOARD_URL = 'https://algosuite.ai';\n\nexport interface RunPairingDeps {\n readonly env?: Record<string, string | undefined>;\n readonly log?: (message: string) => void;\n readonly fetchImpl?: typeof fetch;\n readonly sleep?: (ms: number) => Promise<void>;\n readonly now?: () => Date;\n /** Test seam: capture the stored credential instead of touching the real keychain. */\n readonly store?: (cred: StoredCredential, storedAtIso: string) => string;\n}\n\nexport interface PairingResult {\n readonly credentialPath: string;\n readonly expires_at: string;\n}\n\n/** ABCD-EFGH grouping for an 8-char code (easier to read aloud / type). */\nexport function formatPairingCode(code: string): string {\n return code.length === 8 ? `${code.slice(0, 4)}-${code.slice(4)}` : code;\n}\n\nasync function readJson(res: { json: () => Promise<unknown> }): Promise<Record<string, unknown>> {\n const body = await res.json().catch(() => ({}));\n return body && typeof body === 'object' ? (body as Record<string, unknown>) : {};\n}\n\n/**\n * Run the full pairing handshake. Resolves once a credential is stored; rejects\n * with a friendly message on expiry / consumption / fatal transport error.\n */\nexport async function runPairing(deps: RunPairingDeps = {}): Promise<PairingResult> {\n const env = deps.env ?? process.env;\n const log = deps.log ?? ((m: string) => console.error(m));\n const fetchImpl = deps.fetchImpl ?? fetch;\n const sleep = deps.sleep ?? ((ms: number) => new Promise<void>((r) => setTimeout(r, ms)));\n const now = deps.now ?? (() => new Date());\n const store = deps.store ?? ((cred: StoredCredential, iso: string) => writeStoredCredential(cred, iso, env));\n\n const controlPlaneUrl = env['VO_CONTROL_PLANE_URL']?.trim() || DEFAULT_CONTROL_PLANE_URL;\n const dashboardUrl = env['VO_DASHBOARD_URL']?.trim() || DEFAULT_DASHBOARD_URL;\n const deviceLabel = `${platform()} on ${hostname()}`.slice(0, 120);\n\n // 1. initiate \u2014 get a display code + a secret poll token.\n const initRes = await fetchImpl(`${controlPlaneUrl}/api/v1/pair/initiate`, {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ device_label: deviceLabel }),\n });\n if (!initRes.ok) throw new Error(`Could not start pairing (server ${initRes.status}).`);\n const init = await readJson(initRes);\n const code = String(init['code'] ?? '');\n const pollToken = String(init['poll_token'] ?? '');\n if (!code || !pollToken) throw new Error('Pairing service returned an incomplete response.');\n const intervalMs = (Number(init['poll_interval_seconds']) || 5) * 1000;\n const expiresAtMs = new Date(String(init['expires_at'] ?? '')).getTime();\n\n // 2. show the friend what to do.\n log('');\n log(' To connect this runner, open this page in your browser:');\n log(` ${dashboardUrl}/pair`);\n log(' and enter this code:');\n log('');\n log(` ${formatPairingCode(code)}`);\n log('');\n log(' Your Anthropic key never leaves this computer. Waiting for you to authorize\u2026');\n\n // 3. poll with the SECRET token until authorized / expired.\n for (;;) {\n if (Number.isFinite(expiresAtMs) && now().getTime() >= expiresAtMs) {\n throw new Error('The pairing code expired before it was authorized. Run `vo-mcp pair` again.');\n }\n await sleep(intervalMs);\n const pollRes = await fetchImpl(`${controlPlaneUrl}/api/v1/pair/poll`, {\n method: 'GET',\n headers: { 'x-vo-poll-token': pollToken },\n });\n if (pollRes.status === 404) {\n throw new Error('The pairing expired. Run `vo-mcp pair` again.');\n }\n if (pollRes.status === 410) {\n throw new Error('This code was already used. Run `vo-mcp pair` again.');\n }\n if (!pollRes.ok) {\n // Transient (rate limit / blip) \u2014 keep waiting.\n continue;\n }\n const body = await readJson(pollRes);\n if (body['status'] === 'pending') continue;\n if (body['status'] === 'authorized' && typeof body['vo_credential'] === 'string') {\n const credentialPath = store(\n {\n vo_credential: body['vo_credential'] as string,\n ...(typeof body['expires_at'] === 'string'\n ? { vo_credential_expires_at: body['expires_at'] as string }\n : {}),\n },\n now().toISOString(),\n );\n return { credentialPath, expires_at: String(body['expires_at'] ?? '') };\n }\n throw new Error('Unexpected response from the pairing service.');\n }\n}\n", "/**\n * Local credential store for the thin-client `vo-mcp login` flow (Increment 3b,\n * Option A \u2014 `docs/vo/vo-command-center-inc3b-login-design-2026-06-05.md`).\n *\n * Persists the per-user Firebase refresh token (the user's OWN credential, never\n * the god-token, never model keys) captured by `login`, so the auto-refreshing\n * token source (Inc 3a) can mint fresh ID tokens across MCP restarts.\n *\n * Storage precedence (Inc 3b.3):\n * 1. **OS keychain** (Windows Credential Manager / macOS Keychain / libsecret)\n * via the optional `@napi-rs/keyring` backend (`keychain.ts`). The DEFAULT\n * when available \u2014 the secret never lands in plaintext on disk.\n * 2. **0600 file** at `$VO_MCP_CREDENTIALS_PATH` or `~/.config/vo-mcp/credentials.json`.\n * The fallback when the keychain is unavailable or disabled\n * (`VO_MCP_DISABLE_KEYCHAIN`). `VO_MCP_CREDENTIALS_PATH` only sets the file\n * LOCATION; force file storage with `VO_MCP_DISABLE_KEYCHAIN`.\n *\n * `env`-supplied tokens (`VO_USER_REFRESH_TOKEN`, etc.) still win over BOTH\n * stores \u2014 that precedence lives upstream in `auth-token-source.ts`.\n *\n * **Single source of truth.** The credential lives in EITHER the keychain OR the\n * file, never both: a write to one store CLEARS the other, so a stale entry can\n * never shadow the current credential on read, and the secret never lingers in\n * plaintext after a migration to the keychain.\n *\n * **Keychain durability.** A keychain-stored credential is only readable while\n * the `@napi-rs/keyring` native module loads. If the module later becomes\n * unavailable (an ABI break across a Node upgrade, a corrupted install), the\n * credential can't be read and the user re-runs `vo-mcp login` \u2014 the same\n * behaviour as `gh` / `gcloud` / `firebase` keychain storage. We deliberately do\n * NOT mirror the secret to a plaintext file as a fallback: that would defeat the\n * entire point of keychain storage (keeping the secret off plaintext disk).\n */\nimport { homedir } from 'node:os';\nimport { join, dirname } from 'node:path';\nimport {\n existsSync,\n mkdirSync,\n readFileSync,\n writeFileSync,\n chmodSync,\n rmSync,\n} from 'node:fs';\n\nimport { keychainAvailable, keychainGet, keychainSet, keychainDelete } from './keychain.js';\n\nexport interface StoredCredential {\n /**\n * Firebase refresh token (long-lived; exchanged for short-lived ID tokens).\n * OPTIONAL since Inc 3b.4b: once a scoped `vo_credential` is minted, the raw\n * refresh token is dropped, so a stored credential may carry ONLY the vocred_.\n */\n readonly refresh_token?: string;\n /** Firebase Web API key (PUBLIC) needed for the securetoken refresh exchange. */\n readonly api_key?: string;\n /**\n * Scoped, revocable VO credential (`vocred_`) minted by the control-plane\n * (Inc 3b.4b). Preferred over the raw refresh token; lets the client present a\n * revocable, server-side credential instead of the Firebase refresh token.\n */\n readonly vo_credential?: string;\n /** ISO-8601 expiry of `vo_credential` (the client re-logs-in past this). */\n readonly vo_credential_expires_at?: string;\n /** The signed-in operator email (diagnostics only). */\n readonly email?: string;\n /** ISO timestamp the credential was stored. */\n readonly stored_at?: string;\n}\n\n/**\n * Pluggable OS-keychain backend. Defaults to the real `@napi-rs/keyring` wrapper;\n * tests inject a deterministic fake so they never touch the host keychain.\n */\nexport interface KeychainBackend {\n available(): boolean;\n get(): string | null;\n set(secret: string): boolean;\n delete(): boolean;\n}\n\nconst realKeychain: KeychainBackend = {\n available: keychainAvailable,\n get: keychainGet,\n set: keychainSet,\n delete: keychainDelete,\n};\n\n/** Human-readable \"location\" returned when the credential was stored in the OS keychain. */\nexport const KEYCHAIN_LOCATION = 'OS keychain (service \"vo-mcp\")';\n\n/** Resolve the credentials file path (env override \u2192 XDG-ish default under home). */\nexport function credentialPath(env: Readonly<Record<string, string | undefined>> = process.env): string {\n const override = env['VO_MCP_CREDENTIALS_PATH']?.trim();\n if (override) return override;\n return join(homedir(), '.config', 'vo-mcp', 'credentials.json');\n}\n\n/**\n * Whether the keychain should be consulted at all (read OR write). False when the\n * native backend is unavailable or `VO_MCP_DISABLE_KEYCHAIN` is set (CI/headless).\n */\nfunction keychainEnabled(\n env: Readonly<Record<string, string | undefined>>,\n keychain: KeychainBackend,\n): boolean {\n const disabled = (env['VO_MCP_DISABLE_KEYCHAIN'] ?? '').trim().toLowerCase();\n if (disabled === '1' || disabled === 'true' || disabled === 'yes') return false;\n return keychain.available();\n}\n\n/** Parse + validate a stored credential blob. Returns null on any problem (never throws). */\nfunction deserialize(raw: string): StoredCredential | null {\n try {\n const parsed = JSON.parse(raw) as Partial<StoredCredential>;\n const refresh = typeof parsed.refresh_token === 'string' ? parsed.refresh_token.trim() : '';\n const apiKey = typeof parsed.api_key === 'string' ? parsed.api_key.trim() : '';\n const voCred = typeof parsed.vo_credential === 'string' ? parsed.vo_credential.trim() : '';\n // Valid if it carries a scoped vocred_ OR a full Firebase refresh pair.\n if (!voCred && (!refresh || !apiKey)) return null;\n return {\n ...(refresh ? { refresh_token: refresh } : {}),\n ...(apiKey ? { api_key: apiKey } : {}),\n ...(voCred ? { vo_credential: voCred } : {}),\n ...(typeof parsed.vo_credential_expires_at === 'string' ? { vo_credential_expires_at: parsed.vo_credential_expires_at } : {}),\n ...(typeof parsed.email === 'string' ? { email: parsed.email } : {}),\n ...(typeof parsed.stored_at === 'string' ? { stored_at: parsed.stored_at } : {}),\n };\n } catch {\n return null;\n }\n}\n\nfunction readFromFile(env: Readonly<Record<string, string | undefined>>): StoredCredential | null {\n try {\n const p = credentialPath(env);\n if (!existsSync(p)) return null;\n return deserialize(readFileSync(p, 'utf8'));\n } catch {\n return null;\n }\n}\n\n/**\n * Read the stored credential, or `null` if absent/unreadable/invalid (never\n * throws). Consults an ENABLED keychain first (regardless of the write-target\n * flags, so a credential written to the keychain is found even if\n * `VO_MCP_CREDENTIALS_PATH` is later set), then the 0600 file.\n */\nexport function readStoredCredential(\n env: Readonly<Record<string, string | undefined>> = process.env,\n keychain: KeychainBackend = realKeychain,\n): StoredCredential | null {\n if (keychainEnabled(env, keychain)) {\n const raw = keychain.get();\n const fromKeychain = raw ? deserialize(raw) : null;\n if (fromKeychain) return fromKeychain;\n }\n return readFromFile(env);\n}\n\n/** Read only the OS keychain; never fall back to env-selected plaintext files. */\nexport function readStoredCredentialKeychainOnly(\n env: Readonly<Record<string, string | undefined>> = process.env,\n keychain: KeychainBackend = realKeychain,\n): StoredCredential | null {\n if (!keychainEnabled(env, keychain)) return null;\n const raw = keychain.get();\n return raw ? deserialize(raw) : null;\n}\n\nfunction deleteFile(env: Readonly<Record<string, string | undefined>>): void {\n try {\n rmSync(credentialPath(env), { force: true });\n } catch {\n /* best-effort */\n }\n}\n\nfunction writeToFile(\n payload: StoredCredential,\n env: Readonly<Record<string, string | undefined>>,\n): string {\n const p = credentialPath(env);\n mkdirSync(dirname(p), { recursive: true });\n writeFileSync(p, `${JSON.stringify(payload, null, 2)}\\n`, { mode: 0o600 });\n // Best-effort tighten (no-op / throws on some Windows filesystems \u2014 ignore).\n try {\n chmodSync(p, 0o600);\n } catch {\n /* best-effort */\n }\n return p;\n}\n\n/**\n * Persist the credential. Prefers the OS keychain (secret never hits plaintext\n * disk); otherwise writes the 0600 file. Writing to one store CLEARS the other\n * (single source of truth \u2014 no stale shadow, no lingering plaintext). Returns the\n * location it was stored (`KEYCHAIN_LOCATION` or the file path).\n */\nexport function writeStoredCredential(\n cred: StoredCredential,\n storedAt: string,\n env: Readonly<Record<string, string | undefined>> = process.env,\n keychain: KeychainBackend = realKeychain,\n): string {\n const payload: StoredCredential = {\n ...(cred.refresh_token ? { refresh_token: cred.refresh_token } : {}),\n ...(cred.api_key ? { api_key: cred.api_key } : {}),\n ...(cred.vo_credential ? { vo_credential: cred.vo_credential } : {}),\n ...(cred.vo_credential_expires_at ? { vo_credential_expires_at: cred.vo_credential_expires_at } : {}),\n ...(cred.email ? { email: cred.email } : {}),\n stored_at: cred.stored_at ?? storedAt,\n };\n if (keychainEnabled(env, keychain) && keychain.set(JSON.stringify(payload))) {\n // Stored in the keychain \u2192 clear any stale plaintext file so the secret\n // doesn't linger on disk and can't shadow the keychain on read.\n deleteFile(env);\n return KEYCHAIN_LOCATION;\n }\n const p = writeToFile(payload, env);\n // Stored in the file \u2192 clear any stale keychain entry so it can't shadow the\n // newer file credential on read.\n if (keychainEnabled(env, keychain)) keychain.delete();\n return p;\n}\n", "/**\n * Optional OS-keychain backend for the thin-client credential store (Increment\n * 3b.3 \u2014 `docs/vo/vo-command-center-inc3b-login-design-2026-06-05.md` \u00A75/\u00A76).\n *\n * Loads `@napi-rs/keyring` at runtime via `createRequire`, so it is a TRUE\n * optional dependency: if the native module is absent or fails to load\n * (unsupported platform, prebuilt binary missing, headless CI), every function\n * degrades to a no-op and the caller (`credential-store.ts`) falls back to the\n * 0600 file store. `@napi-rs/keyring`'s `Entry` API is SYNCHRONOUS, so the\n * credential store stays synchronous \u2014 no async ripple into the Inc-3a token\n * source that reads it.\n *\n * Why `createRequire` and not a static/dynamic `import`: a static import would\n * make the native module a HARD dependency (a missing prebuilt would crash the\n * MCP at startup); a dynamic `import()` is async (would force the whole read\n * path async). `createRequire(...)` inside a try/catch loads it lazily and\n * synchronously, and a load failure is just \"keychain unavailable\".\n */\nimport { createRequire } from 'node:module';\n\n/** Keychain service + account the single refresh credential is stored under. */\nconst SERVICE = 'vo-mcp';\nconst ACCOUNT = 'refresh-credential';\n\ninterface KeyringEntry {\n getPassword(): string | null;\n setPassword(password: string): void;\n deletePassword(): boolean;\n}\ninterface KeyringModule {\n Entry: new (service: string, account: string) => KeyringEntry;\n}\n\n// undefined = not yet attempted; null = attempted and unavailable.\nlet cached: KeyringModule | null | undefined;\n\nfunction loadKeyring(): KeyringModule | null {\n if (cached !== undefined) return cached;\n try {\n const req = createRequire(import.meta.url);\n const mod = req('@napi-rs/keyring') as Partial<KeyringModule>;\n cached = mod && typeof mod.Entry === 'function' ? (mod as KeyringModule) : null;\n } catch {\n cached = null;\n }\n return cached;\n}\n\n/** True when the OS keychain backend is usable in this runtime. */\nexport function keychainAvailable(): boolean {\n return loadKeyring() !== null;\n}\n\n/** Read the raw stored secret string from the OS keychain, or null. Never throws. */\nexport function keychainGet(): string | null {\n const k = loadKeyring();\n if (!k) return null;\n try {\n return new k.Entry(SERVICE, ACCOUNT).getPassword();\n } catch {\n return null;\n }\n}\n\n/** Store the raw secret string in the OS keychain. Returns true on success. Never throws. */\nexport function keychainSet(secret: string): boolean {\n const k = loadKeyring();\n if (!k) return false;\n try {\n new k.Entry(SERVICE, ACCOUNT).setPassword(secret);\n return true;\n } catch {\n return false;\n }\n}\n\n/**\n * Delete the stored secret from the OS keychain. Returns true if an entry was\n * removed. Never throws \u2014 a no-op (and `false`) when the backend is unavailable\n * or the entry is absent. Used to keep ONE source of truth: when the credential\n * is (re)written to the file, any stale keychain entry is cleared so it can't\n * shadow the newer file credential on read (and vice-versa).\n */\nexport function keychainDelete(): boolean {\n const k = loadKeyring();\n if (!k) return false;\n try {\n return new k.Entry(SERVICE, ACCOUNT).deletePassword();\n } catch {\n return false;\n }\n}\n\n/** Test-only seam to reset the memoised module load. */\nexport function __resetKeychainCache(): void {\n cached = undefined;\n}\n", "#!/usr/bin/env node\n/**\n * `vo-mcp pair` CLI entry point.\n *\n * Device-code pairing \u2014 shows a short code to enter at <dashboard>/pair, waits\n * for the friend to authorize in their browser, then stores the minted\n * credential. No browser loopback, no token paste.\n */\nimport { runPairing } from './cloud/pairing.js';\n\nasync function main(): Promise<void> {\n const log = (m: string): void => console.error(m);\n try {\n const result = await runPairing({ env: process.env, log });\n log('');\n log(`\u2713 Paired! Credential stored at: ${result.credentialPath}`);\n log('');\n log('Next steps:');\n log(' 1. Start your runner: vo-mcp runner');\n log(' 2. Dispatch agents from: https://algosuite.ai/algohq');\n } catch (err: unknown) {\n log(`\u2717 Pairing failed: ${err instanceof Error ? err.message : String(err)}`);\n process.exit(1);\n }\n}\n\nmain().catch((err: unknown) => {\n console.error('[vo-mcp pair] fatal:', err);\n process.exit(1);\n});\n"],
|
|
5
|
+
"mappings": ";;;;AAWA,SAAS,UAAU,gBAAgB;;;ACsBnC,SAAS,eAAe;AACxB,SAAS,MAAM,eAAe;AAC9B;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;;;ACxBP,SAAS,qBAAqB;AAG9B,IAAM,UAAU;AAChB,IAAM,UAAU;AAYhB,IAAI;AAEJ,SAAS,cAAoC;AAC3C,MAAI,WAAW,OAAW,QAAO;AACjC,MAAI;AACF,UAAM,MAAM,cAAc,YAAY,GAAG;AACzC,UAAM,MAAM,IAAI,kBAAkB;AAClC,aAAS,OAAO,OAAO,IAAI,UAAU,aAAc,MAAwB;AAAA,EAC7E,QAAQ;AACN,aAAS;AAAA,EACX;AACA,SAAO;AACT;AAGO,SAAS,oBAA6B;AAC3C,SAAO,YAAY,MAAM;AAC3B;AAGO,SAAS,cAA6B;AAC3C,QAAM,IAAI,YAAY;AACtB,MAAI,CAAC,EAAG,QAAO;AACf,MAAI;AACF,WAAO,IAAI,EAAE,MAAM,SAAS,OAAO,EAAE,YAAY;AAAA,EACnD,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAGO,SAAS,YAAY,QAAyB;AACnD,QAAM,IAAI,YAAY;AACtB,MAAI,CAAC,EAAG,QAAO;AACf,MAAI;AACF,QAAI,EAAE,MAAM,SAAS,OAAO,EAAE,YAAY,MAAM;AAChD,WAAO;AAAA,EACT,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AASO,SAAS,iBAA0B;AACxC,QAAM,IAAI,YAAY;AACtB,MAAI,CAAC,EAAG,QAAO;AACf,MAAI;AACF,WAAO,IAAI,EAAE,MAAM,SAAS,OAAO,EAAE,eAAe;AAAA,EACtD,QAAQ;AACN,WAAO;AAAA,EACT;AACF;;;ADXA,IAAM,eAAgC;AAAA,EACpC,WAAW;AAAA,EACX,KAAK;AAAA,EACL,KAAK;AAAA,EACL,QAAQ;AACV;AAGO,IAAM,oBAAoB;AAG1B,SAAS,eAAe,MAAoD,QAAQ,KAAa;AACtG,QAAM,WAAW,IAAI,yBAAyB,GAAG,KAAK;AACtD,MAAI,SAAU,QAAO;AACrB,SAAO,KAAK,QAAQ,GAAG,WAAW,UAAU,kBAAkB;AAChE;AAMA,SAAS,gBACP,KACA,UACS;AACT,QAAM,YAAY,IAAI,yBAAyB,KAAK,IAAI,KAAK,EAAE,YAAY;AAC3E,MAAI,aAAa,OAAO,aAAa,UAAU,aAAa,MAAO,QAAO;AAC1E,SAAO,SAAS,UAAU;AAC5B;AA8DA,SAAS,WAAW,KAAyD;AAC3E,MAAI;AACF,WAAO,eAAe,GAAG,GAAG,EAAE,OAAO,KAAK,CAAC;AAAA,EAC7C,QAAQ;AAAA,EAER;AACF;AAEA,SAAS,YACP,SACA,KACQ;AACR,QAAM,IAAI,eAAe,GAAG;AAC5B,YAAU,QAAQ,CAAC,GAAG,EAAE,WAAW,KAAK,CAAC;AACzC,gBAAc,GAAG,GAAG,KAAK,UAAU,SAAS,MAAM,CAAC,CAAC;AAAA,GAAM,EAAE,MAAM,IAAM,CAAC;AAEzE,MAAI;AACF,cAAU,GAAG,GAAK;AAAA,EACpB,QAAQ;AAAA,EAER;AACA,SAAO;AACT;AAQO,SAAS,sBACd,MACA,UACA,MAAoD,QAAQ,KAC5D,WAA4B,cACpB;AACR,QAAM,UAA4B;AAAA,IAChC,GAAI,KAAK,gBAAgB,EAAE,eAAe,KAAK,cAAc,IAAI,CAAC;AAAA,IAClE,GAAI,KAAK,UAAU,EAAE,SAAS,KAAK,QAAQ,IAAI,CAAC;AAAA,IAChD,GAAI,KAAK,gBAAgB,EAAE,eAAe,KAAK,cAAc,IAAI,CAAC;AAAA,IAClE,GAAI,KAAK,2BAA2B,EAAE,0BAA0B,KAAK,yBAAyB,IAAI,CAAC;AAAA,IACnG,GAAI,KAAK,QAAQ,EAAE,OAAO,KAAK,MAAM,IAAI,CAAC;AAAA,IAC1C,WAAW,KAAK,aAAa;AAAA,EAC/B;AACA,MAAI,gBAAgB,KAAK,QAAQ,KAAK,SAAS,IAAI,KAAK,UAAU,OAAO,CAAC,GAAG;AAG3E,eAAW,GAAG;AACd,WAAO;AAAA,EACT;AACA,QAAM,IAAI,YAAY,SAAS,GAAG;AAGlC,MAAI,gBAAgB,KAAK,QAAQ,EAAG,UAAS,OAAO;AACpD,SAAO;AACT;;;ADlNO,IAAM,4BAA4B;AAClC,IAAM,wBAAwB;AAkB9B,SAAS,kBAAkB,MAAsB;AACtD,SAAO,KAAK,WAAW,IAAI,GAAG,KAAK,MAAM,GAAG,CAAC,CAAC,IAAI,KAAK,MAAM,CAAC,CAAC,KAAK;AACtE;AAEA,eAAe,SAAS,KAAyE;AAC/F,QAAM,OAAO,MAAM,IAAI,KAAK,EAAE,MAAM,OAAO,CAAC,EAAE;AAC9C,SAAO,QAAQ,OAAO,SAAS,WAAY,OAAmC,CAAC;AACjF;AAMA,eAAsB,WAAW,OAAuB,CAAC,GAA2B;AAClF,QAAM,MAAM,KAAK,OAAO,QAAQ;AAChC,QAAM,MAAM,KAAK,QAAQ,CAAC,MAAc,QAAQ,MAAM,CAAC;AACvD,QAAM,YAAY,KAAK,aAAa;AACpC,QAAM,QAAQ,KAAK,UAAU,CAAC,OAAe,IAAI,QAAc,CAAC,MAAM,WAAW,GAAG,EAAE,CAAC;AACvF,QAAM,MAAM,KAAK,QAAQ,MAAM,oBAAI,KAAK;AACxC,QAAM,QAAQ,KAAK,UAAU,CAAC,MAAwB,QAAgB,sBAAsB,MAAM,KAAK,GAAG;AAE1G,QAAM,kBAAkB,IAAI,sBAAsB,GAAG,KAAK,KAAK;AAC/D,QAAM,eAAe,IAAI,kBAAkB,GAAG,KAAK,KAAK;AACxD,QAAM,cAAc,GAAG,SAAS,CAAC,OAAO,SAAS,CAAC,GAAG,MAAM,GAAG,GAAG;AAGjE,QAAM,UAAU,MAAM,UAAU,GAAG,eAAe,yBAAyB;AAAA,IACzE,QAAQ;AAAA,IACR,SAAS,EAAE,gBAAgB,mBAAmB;AAAA,IAC9C,MAAM,KAAK,UAAU,EAAE,cAAc,YAAY,CAAC;AAAA,EACpD,CAAC;AACD,MAAI,CAAC,QAAQ,GAAI,OAAM,IAAI,MAAM,mCAAmC,QAAQ,MAAM,IAAI;AACtF,QAAM,OAAO,MAAM,SAAS,OAAO;AACnC,QAAM,OAAO,OAAO,KAAK,MAAM,KAAK,EAAE;AACtC,QAAM,YAAY,OAAO,KAAK,YAAY,KAAK,EAAE;AACjD,MAAI,CAAC,QAAQ,CAAC,UAAW,OAAM,IAAI,MAAM,kDAAkD;AAC3F,QAAM,cAAc,OAAO,KAAK,uBAAuB,CAAC,KAAK,KAAK;AAClE,QAAM,cAAc,IAAI,KAAK,OAAO,KAAK,YAAY,KAAK,EAAE,CAAC,EAAE,QAAQ;AAGvE,MAAI,EAAE;AACN,MAAI,2DAA2D;AAC/D,MAAI,OAAO,YAAY,OAAO;AAC9B,MAAI,wBAAwB;AAC5B,MAAI,EAAE;AACN,MAAI,SAAS,kBAAkB,IAAI,CAAC,EAAE;AACtC,MAAI,EAAE;AACN,MAAI,qFAAgF;AAGpF,aAAS;AACP,QAAI,OAAO,SAAS,WAAW,KAAK,IAAI,EAAE,QAAQ,KAAK,aAAa;AAClE,YAAM,IAAI,MAAM,6EAA6E;AAAA,IAC/F;AACA,UAAM,MAAM,UAAU;AACtB,UAAM,UAAU,MAAM,UAAU,GAAG,eAAe,qBAAqB;AAAA,MACrE,QAAQ;AAAA,MACR,SAAS,EAAE,mBAAmB,UAAU;AAAA,IAC1C,CAAC;AACD,QAAI,QAAQ,WAAW,KAAK;AAC1B,YAAM,IAAI,MAAM,+CAA+C;AAAA,IACjE;AACA,QAAI,QAAQ,WAAW,KAAK;AAC1B,YAAM,IAAI,MAAM,sDAAsD;AAAA,IACxE;AACA,QAAI,CAAC,QAAQ,IAAI;AAEf;AAAA,IACF;AACA,UAAM,OAAO,MAAM,SAAS,OAAO;AACnC,QAAI,KAAK,QAAQ,MAAM,UAAW;AAClC,QAAI,KAAK,QAAQ,MAAM,gBAAgB,OAAO,KAAK,eAAe,MAAM,UAAU;AAChF,YAAMA,kBAAiB;AAAA,QACrB;AAAA,UACE,eAAe,KAAK,eAAe;AAAA,UACnC,GAAI,OAAO,KAAK,YAAY,MAAM,WAC9B,EAAE,0BAA0B,KAAK,YAAY,EAAY,IACzD,CAAC;AAAA,QACP;AAAA,QACA,IAAI,EAAE,YAAY;AAAA,MACpB;AACA,aAAO,EAAE,gBAAAA,iBAAgB,YAAY,OAAO,KAAK,YAAY,KAAK,EAAE,EAAE;AAAA,IACxE;AACA,UAAM,IAAI,MAAM,+CAA+C;AAAA,EACjE;AACF;;;AG7GA,eAAe,OAAsB;AACnC,QAAM,MAAM,CAAC,MAAoB,QAAQ,MAAM,CAAC;AAChD,MAAI;AACF,UAAM,SAAS,MAAM,WAAW,EAAE,KAAK,QAAQ,KAAK,IAAI,CAAC;AACzD,QAAI,EAAE;AACN,QAAI,wCAAmC,OAAO,cAAc,EAAE;AAC9D,QAAI,EAAE;AACN,QAAI,aAAa;AACjB,QAAI,wCAAwC;AAC5C,QAAI,wDAAwD;AAAA,EAC9D,SAAS,KAAc;AACrB,QAAI,0BAAqB,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,CAAC,EAAE;AAC3E,YAAQ,KAAK,CAAC;AAAA,EAChB;AACF;AAEA,KAAK,EAAE,MAAM,CAAC,QAAiB;AAC7B,UAAQ,MAAM,wBAAwB,GAAG;AACzC,UAAQ,KAAK,CAAC;AAChB,CAAC;",
|
|
6
6
|
"names": ["credentialPath"]
|
|
7
7
|
}
|
package/dist/runner-cli.js
CHANGED
|
@@ -74,6 +74,7 @@ __export(credential_store_exports, {
|
|
|
74
74
|
KEYCHAIN_LOCATION: () => KEYCHAIN_LOCATION,
|
|
75
75
|
credentialPath: () => credentialPath,
|
|
76
76
|
readStoredCredential: () => readStoredCredential,
|
|
77
|
+
readStoredCredentialKeychainOnly: () => readStoredCredentialKeychainOnly,
|
|
77
78
|
writeStoredCredential: () => writeStoredCredential
|
|
78
79
|
});
|
|
79
80
|
import { homedir } from "node:os";
|
|
@@ -132,6 +133,11 @@ function readStoredCredential(env2 = process.env, keychain = realKeychain) {
|
|
|
132
133
|
}
|
|
133
134
|
return readFromFile(env2);
|
|
134
135
|
}
|
|
136
|
+
function readStoredCredentialKeychainOnly(env2 = process.env, keychain = realKeychain) {
|
|
137
|
+
if (!keychainEnabled(env2, keychain)) return null;
|
|
138
|
+
const raw = keychain.get();
|
|
139
|
+
return raw ? deserialize(raw) : null;
|
|
140
|
+
}
|
|
135
141
|
function deleteFile(env2) {
|
|
136
142
|
try {
|
|
137
143
|
rmSync(credentialPath(env2), { force: true });
|
|
@@ -2125,22 +2131,41 @@ async function resolveBearer(env2) {
|
|
|
2125
2131
|
function createControlPlaneClient({
|
|
2126
2132
|
baseUrl = process.env.VO_CONTROL_PLANE_URL || "",
|
|
2127
2133
|
env: env2 = process.env,
|
|
2128
|
-
fetchImpl = fetch
|
|
2134
|
+
fetchImpl = fetch,
|
|
2135
|
+
heartbeatTimeoutMs = Math.min(
|
|
2136
|
+
Math.max(Number(env2.VO_CODE_RUNNER_HEARTBEAT_TIMEOUT_MS) || 15e3, 1e3),
|
|
2137
|
+
6e4
|
|
2138
|
+
)
|
|
2129
2139
|
} = {}) {
|
|
2130
2140
|
if (!baseUrl) {
|
|
2131
2141
|
throw new Error("VO_CONTROL_PLANE_URL is required for the code-runner daemon");
|
|
2132
2142
|
}
|
|
2133
2143
|
const root = baseUrl.replace(/\/+$/, "");
|
|
2134
|
-
async function req(method, path16, body) {
|
|
2144
|
+
async function req(method, path16, body, { timeoutMs } = {}) {
|
|
2135
2145
|
const bearer = await resolveBearer(env2);
|
|
2136
|
-
|
|
2146
|
+
const controller = timeoutMs ? new AbortController() : null;
|
|
2147
|
+
let timeoutId;
|
|
2148
|
+
const request = Promise.resolve(fetchImpl(`${root}${path16}`, {
|
|
2137
2149
|
method,
|
|
2138
2150
|
headers: {
|
|
2139
2151
|
"content-type": "application/json",
|
|
2140
2152
|
authorization: `Bearer ${bearer}`
|
|
2141
2153
|
},
|
|
2142
|
-
body: body === void 0 ? void 0 : JSON.stringify(body)
|
|
2154
|
+
body: body === void 0 ? void 0 : JSON.stringify(body),
|
|
2155
|
+
...controller ? { signal: controller.signal } : {}
|
|
2156
|
+
}));
|
|
2157
|
+
if (!timeoutMs) return request;
|
|
2158
|
+
const timeout = new Promise((_, reject) => {
|
|
2159
|
+
timeoutId = setTimeout(() => {
|
|
2160
|
+
controller.abort();
|
|
2161
|
+
reject(new Error(`control-plane ${path16} timed out after ${timeoutMs}ms`));
|
|
2162
|
+
}, timeoutMs);
|
|
2143
2163
|
});
|
|
2164
|
+
try {
|
|
2165
|
+
return await Promise.race([request, timeout]);
|
|
2166
|
+
} finally {
|
|
2167
|
+
clearTimeout(timeoutId);
|
|
2168
|
+
}
|
|
2144
2169
|
}
|
|
2145
2170
|
return {
|
|
2146
2171
|
/**
|
|
@@ -2156,6 +2181,10 @@ function createControlPlaneClient({
|
|
|
2156
2181
|
if (Array.isArray(operatorIds) && operatorIds.length > 0) body.operator_ids = operatorIds;
|
|
2157
2182
|
if (session.runnerInstanceId) body.runner_instance_id = session.runnerInstanceId;
|
|
2158
2183
|
if (session.runnerInstanceId && session.reconcileStale) body.reconcile_stale = true;
|
|
2184
|
+
if (session.defaultAgent) body.default_agent = session.defaultAgent;
|
|
2185
|
+
if (Array.isArray(session.availableAgents)) {
|
|
2186
|
+
body.available_agents = session.availableAgents.filter((entry) => entry?.installed === true && entry?.authenticated === true).map((entry) => entry.agent);
|
|
2187
|
+
}
|
|
2159
2188
|
const res = await req("POST", "/api/v1/code-task/claim", body);
|
|
2160
2189
|
if (res.status === 401) {
|
|
2161
2190
|
cachedFirebaseToken = null;
|
|
@@ -2299,13 +2328,20 @@ function createControlPlaneClient({
|
|
|
2299
2328
|
* authenticated operator so the web shows a TRUE "runner online" signal.
|
|
2300
2329
|
* Best-effort caller; throws on 401/non-ok so the daemon can log + retry.
|
|
2301
2330
|
*/
|
|
2302
|
-
async postHeartbeat({ runnerId, operatorId, uptimeSec, activeTasks, version, daemonVersion, servedRepos, servedOperators, availableAgents, accountUsage }) {
|
|
2331
|
+
async postHeartbeat({ runnerId, runnerInstanceId, operatorId, uptimeSec, activeTasks, version, daemonVersion, defaultAgent, supervisorInstanceId, supervisorVersion, supervisorCapabilities, servedRepos, servedOperators, availableAgents, accountUsage }) {
|
|
2303
2332
|
const body = { runner_id: runnerId };
|
|
2333
|
+
if (runnerInstanceId) body.runner_instance_id = runnerInstanceId;
|
|
2304
2334
|
if (operatorId) body.operator_id = operatorId;
|
|
2305
2335
|
if (typeof uptimeSec === "number") body.uptime_sec = uptimeSec;
|
|
2306
2336
|
if (typeof activeTasks === "number") body.active_tasks = activeTasks;
|
|
2307
2337
|
if (version) body.version = version;
|
|
2308
2338
|
if (daemonVersion) body.daemon_version = daemonVersion;
|
|
2339
|
+
if (defaultAgent) body.default_agent = defaultAgent;
|
|
2340
|
+
if (supervisorInstanceId) body.supervisor_instance_id = supervisorInstanceId;
|
|
2341
|
+
if (supervisorVersion) body.supervisor_version = supervisorVersion;
|
|
2342
|
+
if (Array.isArray(supervisorCapabilities) && supervisorCapabilities.length > 0) {
|
|
2343
|
+
body.supervisor_capabilities = supervisorCapabilities;
|
|
2344
|
+
}
|
|
2309
2345
|
if (Array.isArray(servedRepos) && servedRepos.length > 0) body.served_repos = servedRepos;
|
|
2310
2346
|
if (Array.isArray(servedOperators) && servedOperators.length > 0) {
|
|
2311
2347
|
body.served_operator_ids = servedOperators;
|
|
@@ -2316,7 +2352,9 @@ function createControlPlaneClient({
|
|
|
2316
2352
|
if (Array.isArray(accountUsage) && accountUsage.length > 0) {
|
|
2317
2353
|
body.account_usage = accountUsage;
|
|
2318
2354
|
}
|
|
2319
|
-
const res = await req("POST", "/api/v1/runner/heartbeat", body
|
|
2355
|
+
const res = await req("POST", "/api/v1/runner/heartbeat", body, {
|
|
2356
|
+
timeoutMs: heartbeatTimeoutMs
|
|
2357
|
+
});
|
|
2320
2358
|
if (res.status === 401) {
|
|
2321
2359
|
cachedFirebaseToken = null;
|
|
2322
2360
|
throw new Error("heartbeat unauthorized (401)");
|
|
@@ -2324,10 +2362,27 @@ function createControlPlaneClient({
|
|
|
2324
2362
|
if (!res.ok) throw new Error(`heartbeat failed: HTTP ${res.status}`);
|
|
2325
2363
|
return true;
|
|
2326
2364
|
},
|
|
2365
|
+
/** Read the server-authoritative heartbeat ledger without mutating it. */
|
|
2366
|
+
async getRunnerStatus({ operatorId } = {}) {
|
|
2367
|
+
const query = operatorId ? `?operator_id=${encodeURIComponent(operatorId)}` : "";
|
|
2368
|
+
const res = await req("GET", `/api/v1/runner/status${query}`, void 0, {
|
|
2369
|
+
timeoutMs: heartbeatTimeoutMs
|
|
2370
|
+
});
|
|
2371
|
+
if (res.status === 401) {
|
|
2372
|
+
cachedFirebaseToken = null;
|
|
2373
|
+
throw new Error("runner status unauthorized (401)");
|
|
2374
|
+
}
|
|
2375
|
+
if (!res.ok) throw new Error(`runner status failed: HTTP ${res.status}`);
|
|
2376
|
+
const body = await res.json();
|
|
2377
|
+
return Array.isArray(body?.runners) ? body.runners : [];
|
|
2378
|
+
},
|
|
2327
2379
|
/** Poll one authenticated runner's durable Mission Control action queue. */
|
|
2328
|
-
async pollRunnerControl({ runnerId, operatorId }) {
|
|
2380
|
+
async pollRunnerControl({ runnerId, operatorId, supervisorInstanceId, supervisorVersion, capabilities }) {
|
|
2329
2381
|
const body = { runner_id: runnerId };
|
|
2330
2382
|
if (operatorId) body.operator_id = operatorId;
|
|
2383
|
+
if (supervisorInstanceId) body.supervisor_instance_id = supervisorInstanceId;
|
|
2384
|
+
if (supervisorVersion) body.supervisor_version = supervisorVersion;
|
|
2385
|
+
if (Array.isArray(capabilities) && capabilities.length > 0) body.capabilities = capabilities;
|
|
2331
2386
|
const res = await req("POST", "/api/v1/runner/control/poll", body);
|
|
2332
2387
|
if (res.status === 401) {
|
|
2333
2388
|
cachedFirebaseToken = null;
|
|
@@ -2339,9 +2394,12 @@ function createControlPlaneClient({
|
|
|
2339
2394
|
return action && typeof action.action_id === "string" && action.action_id ? { ...action, actionId: action.action_id } : null;
|
|
2340
2395
|
},
|
|
2341
2396
|
/** Acknowledge a maintenance action after the host has restarted the child. */
|
|
2342
|
-
async completeRunnerControl(actionId, { runnerId, operatorId, status, detail }) {
|
|
2397
|
+
async completeRunnerControl(actionId, { runnerId, operatorId, supervisorInstanceId, supervisorVersion, capabilities, status, detail }) {
|
|
2343
2398
|
const body = { runner_id: runnerId, status };
|
|
2344
2399
|
if (operatorId) body.operator_id = operatorId;
|
|
2400
|
+
if (supervisorInstanceId) body.supervisor_instance_id = supervisorInstanceId;
|
|
2401
|
+
if (supervisorVersion) body.supervisor_version = supervisorVersion;
|
|
2402
|
+
if (Array.isArray(capabilities) && capabilities.length > 0) body.capabilities = capabilities;
|
|
2345
2403
|
if (detail) body.detail = detail;
|
|
2346
2404
|
const res = await req("POST", `/api/v1/runner/control/${encodeURIComponent(actionId)}/complete`, body);
|
|
2347
2405
|
if (res.status === 401) {
|
|
@@ -2423,17 +2481,55 @@ function cleanPathSegment(value) {
|
|
|
2423
2481
|
const trimmed = String(value || "").trim();
|
|
2424
2482
|
return trimmed.startsWith('"') && trimmed.endsWith('"') ? trimmed.slice(1, -1) : trimmed;
|
|
2425
2483
|
}
|
|
2484
|
+
function envValue(env2, name) {
|
|
2485
|
+
const exact = env2?.[name];
|
|
2486
|
+
if (typeof exact === "string") return exact.trim();
|
|
2487
|
+
const key = Object.keys(env2 || {}).find((candidate) => candidate.toLowerCase() === name.toLowerCase());
|
|
2488
|
+
return typeof env2?.[key] === "string" ? env2[key].trim() : "";
|
|
2489
|
+
}
|
|
2490
|
+
function userClaudeCandidates(bin, env2) {
|
|
2491
|
+
if (!/^claude(?:\.(?:exe|cmd|ps1))?$/iu.test(bin)) return [];
|
|
2492
|
+
const userProfile = envValue(env2, "USERPROFILE");
|
|
2493
|
+
const appData = envValue(env2, "APPDATA") || (userProfile ? path9.join(userProfile, "AppData", "Roaming") : "");
|
|
2494
|
+
const localAppData = envValue(env2, "LOCALAPPDATA") || (userProfile ? path9.join(userProfile, "AppData", "Local") : "");
|
|
2495
|
+
const candidates = [];
|
|
2496
|
+
if (appData) {
|
|
2497
|
+
const npmBin = path9.join(appData, "npm");
|
|
2498
|
+
candidates.push(
|
|
2499
|
+
path9.join(npmBin, "claude.exe"),
|
|
2500
|
+
path9.join(npmBin, "claude.cmd"),
|
|
2501
|
+
path9.join(npmBin, "claude.ps1"),
|
|
2502
|
+
path9.join(npmBin, "claude"),
|
|
2503
|
+
path9.join(npmBin, ...NATIVE_CLAUDE_PARTS)
|
|
2504
|
+
);
|
|
2505
|
+
}
|
|
2506
|
+
if (userProfile) candidates.push(path9.join(userProfile, ".local", "bin", "claude.exe"));
|
|
2507
|
+
if (localAppData) {
|
|
2508
|
+
candidates.push(
|
|
2509
|
+
path9.join(localAppData, "Microsoft", "WinGet", "Links", "claude.exe"),
|
|
2510
|
+
path9.join(localAppData, "Microsoft", "WindowsApps", "claude.exe")
|
|
2511
|
+
);
|
|
2512
|
+
}
|
|
2513
|
+
return candidates;
|
|
2514
|
+
}
|
|
2426
2515
|
function pathCandidates(bin, env2) {
|
|
2427
2516
|
if (path9.isAbsolute(bin) || /[\\/]/u.test(bin)) {
|
|
2428
2517
|
return [path9.resolve(bin)];
|
|
2429
2518
|
}
|
|
2430
2519
|
const extension = path9.extname(bin);
|
|
2431
|
-
|
|
2520
|
+
const fromPath = pathValue(env2).split(";").map(cleanPathSegment).filter(Boolean).flatMap((directory) => extension ? [path9.join(directory, bin)] : [
|
|
2432
2521
|
path9.join(directory, `${bin}.exe`),
|
|
2433
2522
|
path9.join(directory, `${bin}.cmd`),
|
|
2434
2523
|
path9.join(directory, `${bin}.ps1`),
|
|
2435
2524
|
path9.join(directory, bin)
|
|
2436
2525
|
]);
|
|
2526
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2527
|
+
return [...fromPath, ...userClaudeCandidates(bin, env2)].filter((candidate) => {
|
|
2528
|
+
const key = candidate.toLowerCase();
|
|
2529
|
+
if (seen.has(key)) return false;
|
|
2530
|
+
seen.add(key);
|
|
2531
|
+
return true;
|
|
2532
|
+
});
|
|
2437
2533
|
}
|
|
2438
2534
|
function canonicalExistingPath(candidate, exists, canonicalize) {
|
|
2439
2535
|
if (!exists(candidate)) return null;
|
|
@@ -2462,7 +2558,7 @@ function resolveWindowsClaudeExecutable({
|
|
|
2462
2558
|
if (resolvedNative) return resolvedNative;
|
|
2463
2559
|
}
|
|
2464
2560
|
const error = new Error(
|
|
2465
|
-
`Could not resolve a native claude.exe for "${requested}".
|
|
2561
|
+
`Could not resolve a native claude.exe for "${requested}". Install or update Claude Code with the native Windows installer (recommended) or npm install -g @anthropic-ai/claude-code; the HQ runner will not execute a shell-only .cmd/.ps1 shim.`
|
|
2466
2562
|
);
|
|
2467
2563
|
error.code = "ENOENT";
|
|
2468
2564
|
throw error;
|
|
@@ -4779,7 +4875,7 @@ var init_dispatch_onboarding = __esm({
|
|
|
4779
4875
|
"docs/current/virtual-office-operating-model.md",
|
|
4780
4876
|
"docs/current/virtual-office-test-architect.md",
|
|
4781
4877
|
"docs/current/evidence-grounded-consensus-testing.md",
|
|
4782
|
-
"docs/vo/ADR-001-verification-oracle-not-orchestrator-2026-05-29.md (
|
|
4878
|
+
"docs/vo/ADR-001-verification-oracle-not-orchestrator-2026-05-29.md (AlgoHQ verifies + signs; human approves merges; NO autonomous bot-merge / headless triggers)",
|
|
4783
4879
|
"docs/vo/vo-adr-002-two-plane-moat.md (fat secret server / thin dumb client)",
|
|
4784
4880
|
"docs/vo/vo-roadmap-2026-05-26.md (the live roadmap \u2014 read its Change log tail for current state)",
|
|
4785
4881
|
"the nearest scoped CLAUDE.md for any directory you edit",
|
|
@@ -5240,6 +5336,7 @@ function makeLoopTicks({
|
|
|
5240
5336
|
env: env2,
|
|
5241
5337
|
log: log3,
|
|
5242
5338
|
getActive,
|
|
5339
|
+
runnerInstanceId,
|
|
5243
5340
|
// Cached agent-availability provider (agent-availability.mjs); returns null
|
|
5244
5341
|
// until the first probe completes — the heartbeat simply omits the field.
|
|
5245
5342
|
getAgentAvailability = () => null,
|
|
@@ -5251,9 +5348,49 @@ function makeLoopTicks({
|
|
|
5251
5348
|
}) {
|
|
5252
5349
|
let lastSessionForward = 0;
|
|
5253
5350
|
let lastHeartbeat = 0;
|
|
5351
|
+
let availabilityWasReady = false;
|
|
5352
|
+
const heartbeatState = /* @__PURE__ */ new Map();
|
|
5254
5353
|
let lastResumeSchedule = 0;
|
|
5255
5354
|
let resumeRunning = false;
|
|
5355
|
+
function enqueueHeartbeat(payload) {
|
|
5356
|
+
const key = payload.operatorId || "";
|
|
5357
|
+
let state = heartbeatState.get(key);
|
|
5358
|
+
if (!state) {
|
|
5359
|
+
state = { running: false, pending: null };
|
|
5360
|
+
heartbeatState.set(key, state);
|
|
5361
|
+
}
|
|
5362
|
+
return new Promise((resolve2) => {
|
|
5363
|
+
if (state.running) {
|
|
5364
|
+
if (state.pending) state.pending.waiters.push(resolve2);
|
|
5365
|
+
else state.pending = { payload, waiters: [resolve2] };
|
|
5366
|
+
state.pending.payload = payload;
|
|
5367
|
+
return;
|
|
5368
|
+
}
|
|
5369
|
+
const launch = (nextPayload, waiters) => {
|
|
5370
|
+
state.running = true;
|
|
5371
|
+
let request;
|
|
5372
|
+
try {
|
|
5373
|
+
request = Promise.resolve(client.postHeartbeat(nextPayload));
|
|
5374
|
+
} catch (error) {
|
|
5375
|
+
request = Promise.reject(error);
|
|
5376
|
+
}
|
|
5377
|
+
request.catch((e) => log3(`heartbeat failed: ${e.message}`)).finally(() => {
|
|
5378
|
+
for (const done of waiters) done();
|
|
5379
|
+
if (state.pending) {
|
|
5380
|
+
const pending = state.pending;
|
|
5381
|
+
state.pending = null;
|
|
5382
|
+
launch(pending.payload, pending.waiters);
|
|
5383
|
+
} else {
|
|
5384
|
+
state.running = false;
|
|
5385
|
+
heartbeatState.delete(key);
|
|
5386
|
+
}
|
|
5387
|
+
});
|
|
5388
|
+
};
|
|
5389
|
+
launch(payload, [resolve2]);
|
|
5390
|
+
});
|
|
5391
|
+
}
|
|
5256
5392
|
return function tick() {
|
|
5393
|
+
const heartbeatCompletions = [];
|
|
5257
5394
|
const now = nowFn();
|
|
5258
5395
|
if (cfg.sessionForwardSec > 0 && now - lastSessionForward >= cfg.sessionForwardSec * 1e3) {
|
|
5259
5396
|
lastSessionForward = now;
|
|
@@ -5264,18 +5401,29 @@ function makeLoopTicks({
|
|
|
5264
5401
|
}).catch(() => {
|
|
5265
5402
|
});
|
|
5266
5403
|
}
|
|
5267
|
-
|
|
5404
|
+
const availableAgents = getAgentAvailability();
|
|
5405
|
+
const availabilityReady = Array.isArray(availableAgents);
|
|
5406
|
+
const availabilityJustBecameReady = availabilityReady && !availabilityWasReady;
|
|
5407
|
+
availabilityWasReady = availabilityReady;
|
|
5408
|
+
if (now - lastHeartbeat >= HEARTBEAT_MS || availabilityJustBecameReady) {
|
|
5268
5409
|
lastHeartbeat = now;
|
|
5269
5410
|
const servedRepos = Array.isArray(cfg.servedRepos) ? cfg.servedRepos.slice(0, 100) : [];
|
|
5270
5411
|
const servedOperators = Array.isArray(cfg.servedOperators) ? cfg.servedOperators.slice(0, 100) : [];
|
|
5271
|
-
const availableAgents = getAgentAvailability();
|
|
5272
5412
|
const accountUsage = getAccountUsage();
|
|
5273
5413
|
const version = String(env2.VO_CODE_RUNNER_VERSION || "").trim().slice(0, 40);
|
|
5274
5414
|
const daemonVersion = String(env2.VO_CODE_RUNNER_DAEMON_VERSION || "").trim().slice(0, 40);
|
|
5415
|
+
const supervisorInstanceId = String(env2.VO_RUNNER_SUPERVISOR_INSTANCE_ID || "").trim();
|
|
5416
|
+
const supervisorVersion = String(env2.VO_RUNNER_SUPERVISOR_VERSION || "").trim().slice(0, 40);
|
|
5417
|
+
const supervisorCapabilities = String(env2.VO_RUNNER_SUPERVISOR_CAPABILITIES || "").split(",").map((value) => value.trim()).filter(Boolean).slice(0, 8);
|
|
5275
5418
|
const baseHeartbeat = {
|
|
5276
5419
|
runnerId: cfg.runnerId,
|
|
5420
|
+
...runnerInstanceId ? { runnerInstanceId } : {},
|
|
5277
5421
|
...version ? { version } : {},
|
|
5278
5422
|
...daemonVersion ? { daemonVersion } : {},
|
|
5423
|
+
...cfg.agent ? { defaultAgent: cfg.agent } : {},
|
|
5424
|
+
...supervisorInstanceId ? { supervisorInstanceId } : {},
|
|
5425
|
+
...supervisorVersion ? { supervisorVersion } : {},
|
|
5426
|
+
...supervisorCapabilities.length > 0 ? { supervisorCapabilities } : {},
|
|
5279
5427
|
...servedRepos.length > 0 ? { servedRepos } : {},
|
|
5280
5428
|
...servedOperators.length > 0 ? { servedOperators } : {},
|
|
5281
5429
|
...Array.isArray(availableAgents) && availableAgents.length > 0 ? { availableAgents } : {},
|
|
@@ -5285,7 +5433,10 @@ function makeLoopTicks({
|
|
|
5285
5433
|
};
|
|
5286
5434
|
const operatorIds = servedOperators.length > 0 ? servedOperators : [void 0];
|
|
5287
5435
|
for (const operatorId of operatorIds) {
|
|
5288
|
-
|
|
5436
|
+
heartbeatCompletions.push(enqueueHeartbeat({
|
|
5437
|
+
...baseHeartbeat,
|
|
5438
|
+
...operatorId ? { operatorId } : {}
|
|
5439
|
+
}));
|
|
5289
5440
|
}
|
|
5290
5441
|
}
|
|
5291
5442
|
const resumeSec = Number(env2.VO_RESUME_SCHEDULE_SEC) > 0 ? Number(env2.VO_RESUME_SCHEDULE_SEC) : DEFAULT_RESUME_SCHEDULE_SEC;
|
|
@@ -5296,6 +5447,7 @@ function makeLoopTicks({
|
|
|
5296
5447
|
resumeRunning = false;
|
|
5297
5448
|
});
|
|
5298
5449
|
}
|
|
5450
|
+
return Promise.all(heartbeatCompletions).then(() => void 0);
|
|
5299
5451
|
};
|
|
5300
5452
|
}
|
|
5301
5453
|
var HEARTBEAT_MS, DEFAULT_RESUME_SCHEDULE_SEC;
|
|
@@ -5310,6 +5462,10 @@ var init_loop_ticks = __esm({
|
|
|
5310
5462
|
});
|
|
5311
5463
|
|
|
5312
5464
|
// ../../scripts/virtual-office/code-runner/agent-availability.mjs
|
|
5465
|
+
function resolveAgentClaimContext(provider, defaultAgent) {
|
|
5466
|
+
const availableAgents = provider.get();
|
|
5467
|
+
return Array.isArray(availableAgents) ? { availableAgents, defaultAgent } : null;
|
|
5468
|
+
}
|
|
5313
5469
|
async function collectAgentAvailability({
|
|
5314
5470
|
agents = listAgents(),
|
|
5315
5471
|
runnerFor = (agent) => resolveRunner({ VO_CODE_RUNNER_AGENT: agent }).runner
|
|
@@ -6085,7 +6241,7 @@ function startControlServer({ port, getStatus, requestStop, allowedOrigin, log:
|
|
|
6085
6241
|
server.unref?.();
|
|
6086
6242
|
return server;
|
|
6087
6243
|
}
|
|
6088
|
-
function startDaemonControl({ cfg, requestStop, getActiveCount, isRunning, startedAt, log: log3 = () => {
|
|
6244
|
+
function startDaemonControl({ cfg, runnerInstanceId, requestStop, getActiveCount, isRunning, startedAt, log: log3 = () => {
|
|
6089
6245
|
}, onDuplicate = null }) {
|
|
6090
6246
|
if (!cfg.controlEnabled) return null;
|
|
6091
6247
|
return startControlServer({
|
|
@@ -6097,6 +6253,7 @@ function startDaemonControl({ cfg, requestStop, getActiveCount, isRunning, start
|
|
|
6097
6253
|
running: isRunning(),
|
|
6098
6254
|
pid: process.pid,
|
|
6099
6255
|
runnerId: cfg.runnerId,
|
|
6256
|
+
runnerInstanceId,
|
|
6100
6257
|
servedRepos: cfg.servedRepos,
|
|
6101
6258
|
servedOperators: cfg.servedOperators,
|
|
6102
6259
|
watchEnabled: cfg.watchEnabled,
|
|
@@ -7626,7 +7783,7 @@ function reportsBlocker(summary) {
|
|
|
7626
7783
|
const text = String(summary || "");
|
|
7627
7784
|
const explicit = explicitTaskOutcome(text);
|
|
7628
7785
|
if (explicit) return explicit === "BLOCKED" || explicit === "FAILED";
|
|
7629
|
-
return /\btask remains\s+(?:\*\*)?BLOCKED\b/i.test(text) || /(?:^|\n)\s*(?:#{1,6}\s*)?(?:host-recovery\s+)?(?:result|outcome)\s*:\s*(?:\*\*)?BLOCKED\b/im.test(text);
|
|
7786
|
+
return /\btask remains\s+(?:\*\*)?BLOCKED\b/i.test(text) || /(?:^|\n)\s*(?:#{1,6}\s*)?(?:\*\*)?(?:host-recovery\s+)?(?:result|outcome|status)\s*:\s*(?:\*\*)?BLOCKED\b/im.test(text);
|
|
7630
7787
|
}
|
|
7631
7788
|
function isMaxTurnExhaustion(run = {}, maxTurns) {
|
|
7632
7789
|
const summary = String(run.summary || "").trim().toLowerCase();
|
|
@@ -7902,6 +8059,7 @@ async function main({ env: env2 = process.env, once: once2 = false } = {}) {
|
|
|
7902
8059
|
const startedAt = Date.now();
|
|
7903
8060
|
const controlServer = startDaemonControl({
|
|
7904
8061
|
cfg,
|
|
8062
|
+
runnerInstanceId,
|
|
7905
8063
|
requestStop: () => stop("web-control"),
|
|
7906
8064
|
getActiveCount: () => active,
|
|
7907
8065
|
isRunning: () => !stopping,
|
|
@@ -7929,18 +8087,24 @@ async function main({ env: env2 = process.env, once: once2 = false } = {}) {
|
|
|
7929
8087
|
const watchCoordinator = makeWatchCycleCoordinator({ runWatch, log: log2, intervalMs: cfg.watchIntervalSec * 1e3 });
|
|
7930
8088
|
const agentAvailability = makeAgentAvailabilityProvider({ onError: (e) => log2(`agent probe failed: ${e.message}`) });
|
|
7931
8089
|
const accountUsage = makeAccountUsageProvider();
|
|
7932
|
-
const loopTick = makeLoopTicks({ client, cfg, env: env2, log: log2, getActive: () => active, getAgentAvailability: () => agentAvailability.get(), getAccountUsage: () => accountUsage.get() });
|
|
8090
|
+
const loopTick = makeLoopTicks({ client, cfg, env: env2, log: log2, getActive: () => active, runnerInstanceId, getAgentAvailability: () => agentAvailability.get(), getAccountUsage: () => accountUsage.get() });
|
|
7933
8091
|
const backoff = makeReconnectBackoff({ baseMs: cfg.pollSec * 1e3, log: log2 });
|
|
7934
8092
|
while (!stopping) {
|
|
7935
|
-
loopTick();
|
|
8093
|
+
const heartbeatCompletion = loopTick();
|
|
7936
8094
|
if (cfg.watchEnabled) watchCoordinator.start();
|
|
7937
8095
|
if (active >= cfg.maxConcurrency) {
|
|
7938
8096
|
await sleep2(cfg.pollSec * 1e3);
|
|
7939
8097
|
continue;
|
|
7940
8098
|
}
|
|
8099
|
+
const claimAgents = resolveAgentClaimContext(agentAvailability, cfg.agent);
|
|
8100
|
+
if (!claimAgents) {
|
|
8101
|
+
await sleep2(cfg.pollSec * 1e3);
|
|
8102
|
+
continue;
|
|
8103
|
+
}
|
|
8104
|
+
await heartbeatCompletion;
|
|
7941
8105
|
let task;
|
|
7942
8106
|
try {
|
|
7943
|
-
task = await client.claim(cfg.runnerId, cfg.servedRepos, cfg.servedOperators, { runnerInstanceId, reconcileStale });
|
|
8107
|
+
task = await client.claim(cfg.runnerId, cfg.servedRepos, cfg.servedOperators, { runnerInstanceId, reconcileStale, ...claimAgents });
|
|
7944
8108
|
reconcileStale = false;
|
|
7945
8109
|
backoff.onSuccess();
|
|
7946
8110
|
} catch (err) {
|