@agentproto/runtime 2.2.0 → 2.4.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/README.md +1 -0
- package/dist/catalog-models.mjs +2 -1
- package/dist/catalog-models.mjs.map +1 -1
- package/dist/config.d.ts +20 -0
- package/dist/config.mjs.map +1 -1
- package/dist/index.d.ts +261 -5
- package/dist/index.mjs +581 -38
- package/dist/index.mjs.map +1 -1
- package/dist/resume-strategies.mjs.map +1 -1
- package/package.json +6 -6
package/README.md
CHANGED
|
@@ -42,6 +42,7 @@ A per-boot bearer token is generated automatically and written into `<workspace>
|
|
|
42
42
|
| MCP | `POST /mcp` (Streamable HTTP) | Stateless mode; per-request transport |
|
|
43
43
|
| Conversations | `GET /conversations` / `GET /conversations/<id>` | Markdown bodies |
|
|
44
44
|
| Adapter discovery | `GET /adapters` / `POST /adapters/:slug/install` | When `listAgentAdapters` / `installAgentAdapter` is wired |
|
|
45
|
+
| App scope mounts | `POST /apps/:appId/apply` / `DELETE /apps/:appId/apply` / `GET /scopes/:scopeId/apps` | Mirrors MCP `app_apply` / `app_unapply` / `app_list_applied`; needs `appRegistry` |
|
|
45
46
|
| Sessions list | `GET /sessions` / `GET /sessions/:id` / `GET /sessions/summaries` | id-or-name in `:id`; summaries are lightweight + paginated |
|
|
46
47
|
| Agent spawn | `POST /sessions/agent` | Long-lived ACP agent (needs `resolveAgentAdapter`) |
|
|
47
48
|
| Interrupt turn | `POST /sessions/:id/interrupt` | Cancel the in-flight turn; session stays alive |
|
package/dist/catalog-models.mjs
CHANGED
|
@@ -75,7 +75,8 @@ function resolveModelId(id) {
|
|
|
75
75
|
}
|
|
76
76
|
function methodsForDirect(descriptor) {
|
|
77
77
|
const methods = [];
|
|
78
|
-
if (descriptor?.authSubscription
|
|
78
|
+
if (descriptor?.authSubscription || descriptor?.modelDerivedApiKey)
|
|
79
|
+
methods.push("oauth-bearer");
|
|
79
80
|
if (descriptor?.provider || descriptor?.modelDerivedApiKey) methods.push("api-key");
|
|
80
81
|
return methods;
|
|
81
82
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/catalog-models.ts"],"names":["routes"],"mappings":"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","file":"catalog-models.mjs","sourcesContent":["/**\n * Read-only catalog/vendor endpoint (`agentproto-session-config-axes`\n * SPEC §5) — `GET /catalog/models` + `catalog_models` MCP tool wire into\n * {@link buildCatalogModels}, the pure join this module owns.\n *\n * Reuses three already-shipped pieces instead of rebuilding them:\n * - the vendor/product/route model + router widening (OpenRouter/\n * Requesty/HuggingFace) from `@agentproto/model-catalog/route-identity`\n * (`resolveLlmModelRoute`, `route-identity/index.ts:396-511`) — this is\n * what keeps the catalog from being capped at any one adapter's\n * `models.allowed` list (SPEC §5.1);\n * - the profile eligibility predicate shipped in #470\n * (`@agentproto/auth`'s `eligibleProfiles`, `packages/auth/src/\n * eligibility.ts:81-89`) for the profile-aware `runnable` flag (SPEC\n * §5.3) — the old bare `hasKey` check (`packages/cli/src/commands/\n * models.ts:113-117`) is the degenerate one-profile-per-provider case\n * this predicate subsumes;\n * - `AdapterAuthDescriptor` (`spawn-defaults.ts:226`), the SAME\n * provider/authSubscription projection `resolveAuthSpec` reads, as the\n * source for which auth methods an adapter can present on its direct\n * route (SPEC §3.4's derivable replacement for a hand-maintained\n * `authSubscription` boolean).\n *\n * A gateway/router route (anything where the resolved route differs from\n * the model's vendor — `openrouter`, `requesty`, `huggingface`, or an\n * adapter's own gateway mode id like `moonshot`) always bills against the\n * route's own id and is always reached with an api-key credential — never\n * oauth-bearer, since no third-party gateway has an Anthropic-style\n * subscription bearer path (SPEC §1c: \"a moonshot profile, not the Claude\n * sub\"). That structural rule is what lets this module compute\n * `runnable`/`eligibleProfiles` for the widened, non-curated rows without\n * per-adapter gateway-vendor tables.\n */\n\nimport type { AuthMethod, AuthProfile } from \"@agentproto/auth\"\nimport { eligibleProfiles, type AdapterAuthManifest } from \"@agentproto/auth\"\nimport {\n resolveLlmModelRoute,\n tryParseModelRef,\n formatModelRef,\n} from \"@agentproto/model-catalog/route-identity\"\nimport {\n getModelProvider,\n resolvePricingExact,\n LLM_PRICING_CATALOG,\n MODEL_ALIASES,\n} from \"@agentproto/model-catalog/llm\"\nimport { getAnthropicGatewayPreset } from \"@agentproto/provider-presets\"\nimport type { AdapterAuthDescriptor } from \"./spawn-defaults.js\"\nimport type { RouteSpec } from \"./session-config.js\"\n\nexport type { RouteSpec } from \"./session-config.js\"\n\n/** Routers the catalog probes to widen beyond any adapter's declared model\n * list (SPEC §5.1) — same three route-identity widens, `route-identity/\n * index.ts:54-59`. */\nconst WIDENING_ROUTES = [\"openrouter\", \"requesty\", \"huggingface\"] as const\n\n/**\n * Vendor-specific compatibility routes: canonical protocol surfaces that can\n * bill the same model family without duplicating catalog entries. A model whose\n * pricing-catalog vendor is `xai` is legitimately reachable both on the native\n * `xai` API route and on the Anthropic-compatible `xai-anthropic` route; the\n * two routes are distinct billing endpoints so the right auth profile matches\n * the right protocol.\n */\nconst VENDOR_COMPATIBILITY_ROUTES: Readonly<Record<string, readonly string[]>> = {\n xai: [\"xai\", \"xai-anthropic\"],\n}\n\n/** One model entry as declared in an adapter's `models.allowed`\n * (`AdapterModelInfo`, `packages/cli/src/registry/resolve.ts:134-142`) —\n * the subset this module needs. */\nexport interface CatalogAdapterModelInput {\n /** Model id exactly as declared — bare (`\"claude-opus-4-8\"`) or\n * `vendor/product` form. */\n id: string\n /** The billing provider/route this model entry reaches on (`\n * AdapterModelInfo.provider`) — e.g. `\"moonshot\"` or `\"openrouter\"`.\n * Takes precedence over the id's own implied vendor route. */\n provider?: string\n /** The adapter mode id that must be applied to reach this model on a\n * non-direct route (`AdapterModelInfo.mode`) — e.g. `\"moonshot\"`.\n * Undefined ⇒ direct route (the model's own vendor, or `provider`). */\n mode?: string\n}\n\n/** One installed adapter's contribution to the catalog. */\nexport interface CatalogAdapterInput {\n slug: string\n models: readonly CatalogAdapterModelInput[]\n /** This adapter's billing-auth capability on its DIRECT route — the same\n * projection `resolveAuthSpec` reads (`spawn-defaults.ts:226`). Omitted\n * ⇒ the adapter presents no auth method, so rows it curates are\n * discoverable but never runnable through it alone. */\n authDescriptor?: AdapterAuthDescriptor\n /** How this adapter's spawn ROUTE relates to the chosen model (AIP-45\n * launch-menu drill-down). `\"free\"` = the adapter can route arbitrary\n * models through gateways (`base_url`). `\"derived-from-model\"` = the\n * endpoint falls out of the model id's vendor prefix. Absent/undefined\n * ⇒ fixed single-provider adapter; widened gateway routes are not\n * attached to it. */\n routeSelection?: \"free\" | \"derived-from-model\"\n}\n\nexport interface CatalogModelsQuery {\n /** Keep only routes reachable via this adapter slug. */\n adapter?: string\n /** Keep only this vendor's entry. */\n vendor?: string\n /** Keep only routes with this route id. */\n route?: string\n /** Drop every route with `runnable: false`. */\n runnableOnly?: boolean\n}\n\nexport interface CatalogPricing {\n inPer1M: number\n outPer1M: number\n}\n\nexport interface CatalogRoute {\n route: string\n ref: string\n baseUrl: string | null\n pricing: CatalogPricing | null\n runnable: boolean\n eligibleProfiles: string[]\n adapterModes: string[]\n adapters: string[]\n curated: boolean\n /**\n * True when MORE THAN ONE distinct model is servable on this route across\n * the whole catalog join (AIP-45 launch-menu drill-down, SPEC §3/§5.3).\n * Derived from the model×route join, never a hand-maintained table:\n * `anthropic`/`openrouter`/`llm-endpoint` serve many models (`true`),\n * a single-model gateway like `moonshot` serves one (`false`). A\n * single-model route pins every model tier to its one model downstream,\n * and gates the custom-gateway A-vs-B promotion (SPEC D5). Independent of\n * the caller's query filters — this is the route's intrinsic capacity, so\n * the same route reports the same `multiModel` in a filtered response.\n */\n multiModel: boolean\n}\n\n/**\n * One route's servable-model index (SPEC §3.9) — the flat, per-route view\n * that lets a capability-derived UI derive tier pinning without re-walking\n * the vendor/product tree. `servableModels` is the set of `vendor/product`\n * model identities reachable on this route across the whole join;\n * `multiModel` is `servableModels.length > 1` (the same value carried on\n * every {@link CatalogRoute} with this id). Always the full, unfiltered\n * catalog capacity, independent of the query — a route's model-count is\n * intrinsic, not a view of the filtered result.\n */\nexport interface CatalogRouteSummary {\n route: string\n servableModels: string[]\n multiModel: boolean\n}\n\nexport interface CatalogProduct {\n product: string\n routes: CatalogRoute[]\n}\n\nexport interface CatalogVendor {\n vendor: string\n products: CatalogProduct[]\n}\n\nexport interface CatalogModelsResponse {\n vendors: CatalogVendor[]\n /**\n * Flat servable-models-per-route index (SPEC §3.9), one entry per distinct\n * route id in the catalog, sorted by route. Exposes the same join the\n * nested `multiModel` flags derive from, so a capability layer can compute\n * tier pinning per route without re-walking the vendor tree. Always the\n * full, unfiltered catalog capacity (see {@link CatalogRouteSummary}).\n */\n routes: CatalogRouteSummary[]\n}\n\nexport interface BuildCatalogModelsInput {\n adapters: readonly CatalogAdapterInput[]\n profiles: readonly AuthProfile[]\n query?: CatalogModelsQuery\n}\n\n/** A resolved model identity + pricing, independent of which adapter (if\n * any) declared it. */\ninterface ResolvedModel {\n vendor: string\n product: string\n /** The route this id resolves to on its OWN — vendor for a direct id,\n * a router name when the id already carries `@route`. */\n directRoute: string\n ref: string\n baseUrl: string | null\n pricing: CatalogPricing | null\n}\n\n/** Rewrite a router-prefixed id (`<router>/<vendor>/<product>`) into the\n * canonical route-identity `<vendor>/<product>@<router>` the parser accepts.\n *\n * Mastra-style adapters (e.g. `adapters/mastra-agent`) declare their model\n * ids in `<provider>/<upstream-id>` form, and for a gateway router the\n * upstream id is itself `<vendor>/<product>` — so a native OpenRouter id like\n * `z-ai/glm-5.2` is advertised as the 3-segment `openrouter/z-ai/glm-5.2`.\n * `parseModelRef` splits on the FIRST `/` and rejects a product that still\n * contains one (`route-identity/index.ts` SEGMENT_RE), so feeding it the raw\n * 3-segment string throws and, before this normalization, 500'd the whole\n * catalog. The route-identity grammar's canonical form for such a model is\n * `<vendor>/<product>@<router>` (`z-ai/glm-5.2@openrouter`) — the `@route`\n * suffix, NOT a leading route segment — so we recompose to that. Only the\n * known gateway routers (whose native ids are `<vendor>/<product>`) are\n * peeled; every other id is returned untouched. A `:pin` variant/provider\n * suffix on the upstream id is preserved (it rides along in the remainder).\n */\nfunction normalizeRouterPrefixedId(id: string): string {\n const firstSlash = id.indexOf(\"/\")\n if (firstSlash === -1) return id\n const head = id.slice(0, firstSlash)\n if (!(WIDENING_ROUTES as readonly string[]).includes(head)) return id\n const remainder = id.slice(firstSlash + 1)\n // Only a genuine `<vendor>/<product>` upstream id (still carrying a `/`) is\n // the router-prefixed shape; a 2-segment `<router>/<product>` is left alone.\n if (!remainder.includes(\"/\") || remainder.includes(\"@\")) return id\n return `${remainder}@${head}`\n}\n\n/** {@link resolveLlmModelRoute} that never throws. `resolveLlmModelRoute`\n * parses through the strict `parseModelRef`, which throws on a still-\n * unparseable ref (e.g. an unrecognised 3-segment id that normalization\n * didn't rewrite) — defense-in-depth so a single bad id can never 500 the\n * catalog. */\nfunction tryResolveLlmModelRoute(id: string): ReturnType<typeof resolveLlmModelRoute> {\n try {\n return resolveLlmModelRoute(id)\n } catch {\n return undefined\n }\n}\n\n/** Infer a vendor from a bare id family prefix — ports `providerFromIdPrefix`\n * (`packages/cli/src/commands/models.ts:47-54`) so a model id that predates\n * its pricing-catalog entry still gets a vendor instead of `\"unknown\"`. */\nfunction vendorFromIdPrefix(bareId: string): string | undefined {\n if (/^claude[-/]/.test(bareId)) return \"anthropic\"\n if (/^(gpt[-/]|o[1-9](-|$)|chatgpt)/.test(bareId)) return \"openai\"\n if (/^gemini[-/]/.test(bareId)) return \"google\"\n if (/^grok[-/]/.test(bareId)) return \"x-ai\"\n if (/^deepseek[-/]/.test(bareId)) return \"deepseek\"\n return undefined\n}\n\n/** Resolve a model id to its vendor/product/route/pricing, in three tiers:\n * the route-identity resolver (handles both `vendor/product[@route]` and\n * legacy bare ids with pricing), then a bare vendor/product parse with no\n * pricing, then the id-prefix heuristic as a last resort. Never throws —\n * every id gets SOME vendor rather than being dropped from the catalog. */\nfunction resolveModelId(id: string): ResolvedModel {\n // A router-prefixed Mastra id (`openrouter/z-ai/glm-5.2`) becomes its\n // canonical route-identity (`z-ai/glm-5.2@openrouter`) BEFORE any parse, so\n // it resolves to a real vendor/product/route row instead of throwing.\n const normalized = normalizeRouterPrefixedId(id)\n const resolved = tryResolveLlmModelRoute(normalized)\n if (resolved) {\n return {\n vendor: resolved.vendor,\n product: resolved.product,\n directRoute: resolved.route,\n ref: formatModelRef(resolved.ref),\n baseUrl: resolved.transport.baseUrl ?? null,\n pricing: {\n inPer1M: resolved.pricing.inputPer1M,\n outPer1M: resolved.pricing.outputPer1M,\n },\n }\n }\n const parsed = tryParseModelRef(normalized)\n if (parsed) {\n return {\n vendor: parsed.vendor,\n product: parsed.product,\n directRoute: parsed.route,\n ref: formatModelRef(parsed),\n baseUrl: null,\n pricing: null,\n }\n }\n const vendor = vendorFromIdPrefix(id) ?? \"unknown\"\n return {\n vendor,\n product: id,\n directRoute: vendor,\n ref: `${vendor}/${id}`,\n baseUrl: null,\n pricing: null,\n }\n}\n\n/** Which auth methods are presentable on a model's DIRECT route — whatever\n * the adapter's descriptor declares, derivable from `authSubscription`/\n * `provider` exactly as SPEC §3.4 calls for. Only called for a genuinely\n * direct route (see `isDirectRoute`); a gateway/router route is always\n * api-key only (SPEC §1c — no third-party gateway has an oauth-bearer\n * path), regardless of what the underlying model's own vendor is. */\nfunction methodsForDirect(\n descriptor: AdapterAuthDescriptor | undefined,\n): AuthMethod[] {\n const methods: AuthMethod[] = []\n if (descriptor?.authSubscription) methods.push(\"oauth-bearer\")\n if (descriptor?.provider || descriptor?.modelDerivedApiKey) methods.push(\"api-key\")\n return methods\n}\n\n/** True iff this model entry resolves to its OWN vendor with no adapter\n * mode override — i.e. reached without any gateway/router redirection.\n * An explicit `model.mode` (adapter gateway mode, e.g. `\"moonshot\"`) is\n * ALWAYS a redirection even when the mode id happens to equal the\n * model's resolved vendor (e.g. a `moonshot/kimi-…` model routed via\n * claude-code's `moonshot` mode) — SPEC §1c's \"moonshot profile, not the\n * Claude sub\" holds regardless of whose model is being served. An id\n * that already carries its own `@route` suffix (`resolved.directRoute !==\n * resolved.vendor`) is equally a router path even with no adapter mode.\n * A `model.provider` that matches the model's own vendor is still a\n * direct route; only a provider/route that differs is a redirection. */\nfunction isDirectRoute(route: string, mode: string | undefined, resolved: ResolvedModel): boolean {\n return mode === undefined && route === resolved.vendor && resolved.directRoute === resolved.vendor\n}\n\ninterface RouteContribution {\n vendor: string\n product: string\n route: string\n ref: string\n baseUrl: string | null\n pricing: CatalogPricing | null\n curated: boolean\n adapterSlug?: string\n adapterMode?: string\n methods: readonly AuthMethod[]\n}\n\n/** Curated contributions — one per adapter-declared model entry. */\nfunction curatedContributions(\n adapters: readonly CatalogAdapterInput[],\n): RouteContribution[] {\n const out: RouteContribution[] = []\n for (const adapter of adapters) {\n for (const model of adapter.models) {\n const resolved = resolveModelId(model.id)\n const route = model.mode ?? model.provider ?? resolved.directRoute\n const methods: AuthMethod[] = isDirectRoute(route, model.mode, resolved)\n ? methodsForDirect(adapter.authDescriptor)\n : [\"api-key\"]\n out.push({\n vendor: resolved.vendor,\n product: resolved.product,\n route,\n ref: resolved.ref,\n baseUrl: resolved.baseUrl,\n pricing: resolved.pricing,\n curated: true,\n adapterSlug: adapter.slug,\n ...(model.mode ? { adapterMode: model.mode } : {}),\n methods,\n })\n }\n }\n return out\n}\n\n/** Widen beyond every adapter's declared list (SPEC §5.1): for each\n * distinct (vendor, product) already known from a curated contribution,\n * probe the router routes route-identity knows about and add a\n * non-curated contribution for any that resolve and aren't already\n * covered by a curated row.\n *\n * Widened routes are only attached to adapters that explicitly declare\n * `routeSelection: \"free\"` — those are the adapters that can route an\n * arbitrary model through a gateway via `base_url`. Fixed-provider and\n * `derived-from-model` adapters must not be advertised on routes they\n * cannot truthfully choose. */\nfunction widenedContributions(\n adapters: readonly CatalogAdapterInput[],\n curated: readonly RouteContribution[],\n): RouteContribution[] {\n const freeAdapters = adapters\n .filter(a => a.routeSelection === \"free\")\n .map(a => a.slug)\n if (freeAdapters.length === 0) return []\n\n const seenProducts = new Map<string, Set<string>>() // \"vendor/product\" -> routes already present\n for (const c of curated) {\n const key = `${c.vendor}/${c.product}`\n const routes = seenProducts.get(key) ?? new Set<string>()\n routes.add(c.route)\n seenProducts.set(key, routes)\n }\n\n const out: RouteContribution[] = []\n for (const [key, existingRoutes] of seenProducts) {\n const [vendor, product] = key.split(\"/\", 2) as [string, string]\n for (const router of WIDENING_ROUTES) {\n if (existingRoutes.has(router)) continue\n const resolved = resolveLlmModelRoute(`${vendor}/${product}@${router}`)\n if (!resolved) continue\n for (const adapterSlug of freeAdapters) {\n out.push({\n vendor,\n product,\n route: router,\n ref: formatModelRef(resolved.ref),\n baseUrl: resolved.transport.baseUrl ?? null,\n pricing: {\n inPer1M: resolved.pricing.inputPer1M,\n outPer1M: resolved.pricing.outputPer1M,\n },\n curated: false,\n adapterSlug,\n methods: [\"api-key\"],\n })\n }\n }\n }\n return out\n}\n\n/**\n * Compatibility routes: canonical protocol surfaces that bill a model family\n * without duplicating pricing entries. For xAI, the same Grok models are\n * reachable on the native OpenAI-flavor `xai` route and on the Anthropic-\n * compatible `xai-anthropic` route; the catalog joins both so the matching\n * auth profile (`xai` vs `xai-anthropic`) can be endpoint-eligible for the\n * right protocol.\n *\n * Rows are generated straight from the static pricing catalog (vendor `xai`),\n * not from adapter curation, because the existing adapters only declare the\n * OpenRouter xAI ids (`x-ai/grok-*@openrouter`). The `xai-anthropic` rows are\n * attached to adapters that can route arbitrary models through an Anthropic-\n * compatible gateway (`routeSelection: \"free\"`). The direct `xai` rows carry\n * no adapter attachment because there is no agent adapter in the registry that\n * speaks the native xAI OpenAI surface today; they are still listed so the\n * `xai` profile can see the models it bills and whether they are runnable.\n */\nfunction compatibilityContributions(\n adapters: readonly CatalogAdapterInput[],\n): RouteContribution[] {\n const freeAdapters = adapters\n .filter(a => a.routeSelection === \"free\")\n .map(a => a.slug)\n const out: RouteContribution[] = []\n\n for (const [product, pricing] of Object.entries(LLM_PRICING_CATALOG)) {\n if (pricing.provider !== \"xai\") continue\n const vendor = pricing.vendor ?? \"xai\"\n const compatRoutes = VENDOR_COMPATIBILITY_ROUTES[vendor]\n if (!compatRoutes) continue\n\n const direct = resolveLlmModelRoute(`${vendor}/${product}`)\n if (!direct) continue\n\n for (const route of compatRoutes) {\n const ref = route === vendor ? `${vendor}/${product}` : `${vendor}/${product}@${route}`\n const baseUrl =\n route === \"xai-anthropic\"\n ? getAnthropicGatewayPreset(\"xai-anthropic\").baseUrl\n : null\n const rowBase: RouteContribution = {\n vendor,\n product,\n route,\n ref,\n baseUrl,\n pricing: {\n inPer1M: direct.pricing.inputPer1M,\n outPer1M: direct.pricing.outputPer1M,\n },\n curated: false,\n methods: [\"api-key\"],\n }\n if (route === \"xai-anthropic\" && freeAdapters.length > 0) {\n for (const adapterSlug of freeAdapters) {\n out.push({ ...rowBase, adapterSlug })\n }\n } else {\n out.push(rowBase)\n }\n }\n }\n return out\n}\n\ninterface MergedRow {\n vendor: string\n product: string\n route: string\n ref: string\n baseUrl: string | null\n pricing: CatalogPricing | null\n curated: boolean\n adapters: string[]\n adapterModes: string[]\n methods: AuthMethod[]\n}\n\n/** Merge contributions sharing a (vendor, product, route) key — multiple\n * adapters can curate the same route, and a widened route always merges\n * into whichever curated row already claimed it. */\nfunction mergeContributions(\n contributions: readonly RouteContribution[],\n): MergedRow[] {\n const rows = new Map<string, MergedRow>()\n for (const c of contributions) {\n const key = `${c.vendor}\u0000${c.product}\u0000${c.route}`\n const existing = rows.get(key)\n if (!existing) {\n rows.set(key, {\n vendor: c.vendor,\n product: c.product,\n route: c.route,\n ref: c.ref,\n baseUrl: c.baseUrl,\n pricing: c.pricing,\n curated: c.curated,\n adapters: c.adapterSlug ? [c.adapterSlug] : [],\n adapterModes: c.adapterMode ? [c.adapterMode] : [],\n methods: [...c.methods],\n })\n continue\n }\n existing.curated = existing.curated || c.curated\n existing.baseUrl = existing.baseUrl ?? c.baseUrl\n existing.pricing = existing.pricing ?? c.pricing\n if (c.adapterSlug && !existing.adapters.includes(c.adapterSlug)) {\n existing.adapters.push(c.adapterSlug)\n }\n if (c.adapterMode && !existing.adapterModes.includes(c.adapterMode)) {\n existing.adapterModes.push(c.adapterMode)\n }\n for (const m of c.methods) {\n if (!existing.methods.includes(m)) existing.methods.push(m)\n }\n }\n return [...rows.values()]\n}\n\n/** The billed vendor for a (vendor, route) pair — the model's own vendor on\n * its direct route, else the route's own id (SPEC §1c). */\nfunction billedVendor(vendor: string, route: string): string {\n return route === vendor ? vendor : route\n}\n\n/**\n * Per-model curation gate (WS3): does `profile`'s allowlist admit this model?\n * An ABSENT `models` field, or `mode: \"all\"`, admits everything — the exact\n * pre-curation behavior, so a profile that predates the field is unchanged\n * (the byte-identical invariant). A `mode: \"allow\"` profile admits the model\n * only when its catalog identity is in the list — matched against BOTH the\n * route-qualified `ref` (`z-ai/glm-5.2@openrouter`) and the route-independent\n * `vendor/product` (`z-ai/glm-5.2`), so a curated id in either form is honored.\n * It ALSO honors the BARE product (`claude-opus-4-8` — everything after the\n * first `/` of `vendorProduct`), but ONLY on a DIRECT route (a `ref` with no\n * `@route` suffix). The vscode \"+ Models\" picker writes bare pricing-catalog\n * ids verbatim into an allowlist for single-vendor DIRECT endpoints (anthropic,\n * moonshot, …), and every existing user allowlist for those is in that form, so\n * the bare tolerance keeps them working with zero migration. It is deliberately\n * NOT extended to multi-vendor GATEWAY endpoints (openrouter/requesty host the\n * same bare product under many vendor prefixes — `sference/glm-5.2` and\n * `z-ai/glm-5.2` are distinct rows on the same endpoint), where a bare id would\n * over-widen across sibling vendors. For those the `ref` (`z-ai/glm-5.2@openrouter`)\n * or the `vendor/product` form (`z-ai/glm-5.2`) is required — which is exactly\n * what the picker writes for gateway endpoints anyway.\n * This is deliberately downstream of `eligibleProfiles` (the endpoint/method\n * gate): a curated profile stays endpoint-eligible but services only its\n * chosen model refs.\n */\nfunction profileAllowsModel(profile: AuthProfile, ref: string, vendorProduct: string): boolean {\n const curation = profile.models\n if (!curation || curation.mode === \"all\") return true\n // The bare product is everything after the FIRST `/` (the vendor is always\n // the first segment; the product may itself contain `/`). No slash → treat\n // the whole string as the product.\n const slash = vendorProduct.indexOf(\"/\")\n const product = slash === -1 ? vendorProduct : vendorProduct.slice(slash + 1)\n // A gateway/router route carries an `@route` suffix in its ref\n // (`z-ai/glm-5.2@openrouter`); a direct route ref is bare `vendor/product`.\n // Bare-product tolerance is DIRECT-only — on a multi-vendor gateway a bare id\n // would admit every sibling vendor's same-named product.\n const isDirect = !ref.includes(\"@\")\n return (\n curation.ids.includes(ref) ||\n curation.ids.includes(vendorProduct) ||\n (isDirect && curation.ids.includes(product))\n )\n}\n\n/** The pure join (SPEC §5): adapter-declared models + router widening +\n * the #470 eligibility predicate → the vendor/product/route tree. No I/O —\n * callers (the HTTP route / MCP tool) own loading adapters + profiles. */\nexport function buildCatalogModels(\n input: BuildCatalogModelsInput,\n): CatalogModelsResponse {\n const curated = curatedContributions(input.adapters)\n const contributions = [\n ...curated,\n ...widenedContributions(input.adapters, curated),\n ...compatibilityContributions(input.adapters),\n ]\n const merged = mergeContributions(contributions)\n const query = input.query ?? {}\n\n // Servable-models-per-route (SPEC §3.9), over the FULL join — a route's\n // model-count is an intrinsic capability, not a view of the caller's\n // query, so `multiModel` stays stable under filtering. A model identity is\n // its `vendor/product` (the same key `widenedContributions` dedupes on), so\n // the same product served by several adapters counts once.\n const servableByRoute = new Map<string, Set<string>>()\n for (const row of merged) {\n const set = servableByRoute.get(row.route) ?? new Set<string>()\n set.add(`${row.vendor}/${row.product}`)\n servableByRoute.set(row.route, set)\n }\n const isMultiModel = (route: string): boolean =>\n (servableByRoute.get(route)?.size ?? 0) > 1\n\n const vendors = new Map<string, Map<string, CatalogRoute[]>>()\n for (const row of merged) {\n if (query.vendor && row.vendor !== query.vendor) continue\n if (query.route && row.route !== query.route) continue\n if (query.adapter && !row.adapters.includes(query.adapter)) continue\n\n // Per-model wallet-serviceability gate (SPEC §1c parity with the spawn\n // guard, `checkModelWalletEligibility` / session-spawn.ts:1143): a DIRECT\n // vendor route — `row.route === row.vendor` with no adapter-mode override\n // in play (`row.adapterModes.length === 0`; an override can coincidentally\n // route === vendor, e.g. a `moonshot` mode reached serving a moonshot-\n // vendor model — see `isDirectRoute`'s own doc comment) — is the catalog's\n // equivalent of spawning with no `route.gateway` named: the adapter's\n // fixed wallet is billed, and that wallet might not actually service this\n // specific model (`claude-fable-5` bills `openrouter`/`requesty`, never\n // `anthropic`, even though the `claude-code` adapter curates it). Reuses\n // the SAME predicate the spawn guard uses — no parallel per-model table.\n // A gateway/mode/router route is the catalog's equivalent of an EXPLICIT\n // `route.gateway` — the spawn guard deliberately never second-guesses\n // that (routing any model through any named gateway's api-key is\n // legitimate), so it is left ungated here too.\n const isDirectVendorRoute = row.route === row.vendor && row.adapterModes.length === 0\n const walletEligible =\n !isDirectVendorRoute ||\n checkModelWalletEligibility(`${row.vendor}/${row.product}`, row.route).ok\n\n const manifest: AdapterAuthManifest = {\n id: `${row.vendor}/${row.product}@${row.route}`,\n endpointByRoute: { [row.route]: billedVendor(row.vendor, row.route) },\n methodsByRoute: { [row.route]: row.methods },\n }\n // The endpoint/method-eligible profiles for this route (whole-profile\n // enable/disable already applied inside `eligibleProfiles`), then narrowed\n // by each profile's per-model curation allowlist (WS3). A profile with no\n // `models` field passes through untouched, so the non-curated join is\n // byte-identical to before.\n const eligible = walletEligible\n ? eligibleProfiles(input.profiles, manifest, row.route).filter(p =>\n profileAllowsModel(p, row.ref, `${row.vendor}/${row.product}`),\n )\n : []\n const runnable = eligible.length > 0\n if (query.runnableOnly && !runnable) continue\n\n const route: CatalogRoute = {\n route: row.route,\n ref: row.ref,\n baseUrl: row.baseUrl,\n pricing: row.pricing,\n runnable,\n eligibleProfiles: eligible.map(p => p.id),\n adapterModes: row.adapterModes,\n adapters: row.adapters,\n curated: row.curated,\n multiModel: isMultiModel(row.route),\n }\n\n const products = vendors.get(row.vendor) ?? new Map<string, CatalogRoute[]>()\n vendors.set(row.vendor, products)\n const routes = products.get(row.product) ?? []\n products.set(row.product, routes)\n routes.push(route)\n }\n\n const result: CatalogVendor[] = [...vendors.entries()]\n .sort(([a], [b]) => a.localeCompare(b))\n .map(([vendor, products]) => ({\n vendor,\n products: [...products.entries()]\n .sort(([a], [b]) => a.localeCompare(b))\n .map(([product, routes]) => ({\n product,\n routes: [...routes].sort((a, b) => a.route.localeCompare(b.route)),\n })),\n }))\n\n const routes: CatalogRouteSummary[] = [...servableByRoute.entries()]\n .sort(([a], [b]) => a.localeCompare(b))\n .map(([route, models]) => ({\n route,\n servableModels: [...models].sort((a, b) => a.localeCompare(b)),\n multiModel: models.size > 1,\n }))\n\n return { vendors: result, routes }\n}\n\n/**\n * Resolve the effective billing route for a session: a parseable model ref's\n * own route is authoritative, `routeGateway` is only consulted when the model\n * carries no explicit `@route` or is unparseable (including router-prefixed ids\n * like `openrouter/vendor/product`, which are normalized to the canonical\n * suffix form first).\n */\nexport function resolveEffectiveRoute(\n model: string | undefined,\n routeGateway: string | undefined,\n): string | undefined {\n if (!model) return routeGateway\n const normalized = normalizeRouterPrefixedId(model)\n const parsed = tryParseModelRef(normalized)\n if (!parsed) return routeGateway\n if (parsed.route !== parsed.vendor) return parsed.route\n return routeGateway ?? parsed.route\n}\n\n/** Rewrite `model`'s `@route` suffix to `route`. Bare/unparseable ids are\n * returned unchanged because they have no suffix to rewrite. */\nexport function modelWithRoute(model: string, route: string): string {\n const parsed = tryParseModelRef(model)\n if (!parsed) return model\n return formatModelRef({ ...parsed, route })\n}\n\n/**\n * Reconcile independent `model` and `route` overrides so they never describe\n * two different billing endpoints. Returns the effective values; mutating\n * callers replace their effective model/route with these results.\n *\n * Rules:\n * - explicit `@route` on the model wins over a stale/conflicting route.gateway;\n * - a route-only override rewrites a parseable model string to match;\n * - a model-only override synthesizes a route field whenever the model's\n * resolved route differs from the previous route.gateway;\n * - passing BOTH overrides that contradict each other is a caller bug → throw.\n */\nexport function reconcileModelRoute(input: {\n prevModel?: string\n prevRoute?: RouteSpec\n model?: string\n route?: RouteSpec\n}): { model?: string; route?: RouteSpec } {\n const { prevModel, prevRoute, model: overrideModel, route: overrideRoute } = input\n\n if (overrideModel !== undefined && overrideRoute !== undefined) {\n const parsed = tryParseModelRef(overrideModel)\n if (parsed && parsed.route !== parsed.vendor && parsed.route !== overrideRoute.gateway) {\n throw new Error(\n `reconcileModelRoute: model \"${overrideModel}\" pins route \"${parsed.route}\" ` +\n `but route override is \"${overrideRoute.gateway}\"`,\n )\n }\n return { model: overrideModel, route: overrideRoute }\n }\n\n if (overrideModel !== undefined) {\n const parsed = tryParseModelRef(overrideModel)\n if (parsed) {\n const hasExplicitRoute = parsed.route !== parsed.vendor\n // An explicit @route always deserves a matching route field; a vendor-\n // implied route only rewrites a stale/conflicting prevRoute.\n if (hasExplicitRoute && prevRoute?.gateway !== parsed.route) {\n return { model: overrideModel, route: { gateway: parsed.route } }\n }\n if (!hasExplicitRoute && prevRoute && prevRoute.gateway !== parsed.route) {\n return { model: overrideModel, route: { gateway: parsed.route } }\n }\n }\n return { model: overrideModel, route: prevRoute }\n }\n\n if (overrideRoute !== undefined) {\n if (prevModel !== undefined) {\n return { model: modelWithRoute(prevModel, overrideRoute.gateway), route: overrideRoute }\n }\n return { route: overrideRoute }\n }\n\n return { model: prevModel, route: prevRoute }\n}\n\n/**\n * The billing routes that can service `model`, reusing the SAME route-identity\n * resolution this module's catalog join uses (SPEC §1c serviceability) — never\n * a parallel per-model provider table. A route R services the model iff:\n * - R is the model's catalog billing provider (`getModelProvider` — the native\n * vendor SDK for a bare/direct id, or the router for a gateway-only slash id\n * like `deepseek/deepseek-v4-pro` ⇒ `openrouter`),\n * - R is an explicit `@route` carried in the id (`…@openrouter`), or\n * - the model resolves on R as one of the {@link WIDENING_ROUTES} router routes\n * (catches requesty-only ids like `sference/…` that carry no pricing-catalog\n * `provider` for `getModelProvider` to return).\n *\n * Returns the DISTINCT serviceable routes; EMPTY when the model is unknown to\n * the catalog — a mismatch cannot then be positively proven, so a money-safety\n * caller MUST NOT reject on an empty result (only on a non-empty set that\n * excludes the resolved wallet). See {@link checkModelWalletEligibility}.\n */\nexport function serviceableModelRoutes(model: string): string[] {\n const routes = new Set<string>()\n const provider = getModelProvider(model)\n if (provider) routes.add(provider)\n const parsed = tryParseModelRef(normalizeRouterPrefixedId(model))\n if (parsed) {\n // An explicit `@route` in the id is itself a serviceable route (and keeps\n // the `getModelProvider` quirk on `<vendor>/<product>@<router>` forms from\n // hiding the route the operator literally named).\n if (parsed.route !== parsed.vendor) routes.add(parsed.route)\n // A first-party vendor model whose `<vendor>/<product>` form COLLIDES with a\n // router-namespaced pricing key hides its own direct vendor route: OpenRouter\n // keys e.g. `anthropic/claude-sonnet-5` / `anthropic/claude-fable-5` with the\n // SAME dash spelling Anthropic uses, tagged `provider:\"openrouter\"`, so\n // `getModelProvider(\"anthropic/claude-sonnet-5\")` resolves to the ROUTER and\n // `anthropic` never enters `routes` above. Restore it: when the BARE product\n // is itself a first-party model of this vendor — a VERBATIM first-party\n // pricing key (`resolvePricingExact(product).provider === vendor`) — the\n // vendor's own SDK/wallet can bill it, so the direct vendor route genuinely\n // IS serviceable. This MUST be the exact lookup, never `resolvePricing`'s\n // substring fallback: an openrouter-only sibling variant like\n // `google/gemini-2.5-flash-image` substring-hits the unrelated\n // `gemini-2.5-flash` row (provider `google`) and would spuriously earn a\n // `google` direct route it cannot actually be billed on. A gateway-only\n // slash id (`deepseek/deepseek-v4-pro` — no bare first-party pricing row)\n // likewise yields no exact match and is left untouched, preserving the\n // money-safety guard's reject on a true router-only model. Scoped to this\n // function: `resolvePricingExact`/`getModelProvider` semantics are unchanged\n // for every other caller.\n if (resolvePricingExact(parsed.product)?.provider === parsed.vendor) {\n routes.add(parsed.vendor)\n }\n for (const router of WIDENING_ROUTES) {\n if (tryResolveLlmModelRoute(`${parsed.vendor}/${parsed.product}@${router}`)) {\n routes.add(router)\n }\n }\n }\n return [...routes]\n}\n\n/** The bare product of a model id — the segment after the last `/`, stripped\n * of any `@route` / `:pin` suffix, lower-cased. `deepseek/deepseek-chat` →\n * `deepseek-chat`; `z-ai/glm-5.2@openrouter` → `glm-5.2`; `claude-opus-4-8`\n * (no slash) → `claude-opus-4-8`. This is the identity the \"did you mean\"\n * matcher keys on, so a wrong-or-missing vendor/route prefix collapses onto\n * the same product. */\nfunction bareProduct(id: string): string {\n const noSuffix = id.split(\"@\")[0]!.split(\":\")[0]!\n const slash = noSuffix.lastIndexOf(\"/\")\n return (slash === -1 ? noSuffix : noSuffix.slice(slash + 1)).toLowerCase()\n}\n\n/**\n * Closest known catalog model ids for a slug UNKNOWN to the local catalog —\n * the \"did you mean\" set behind the spawn-time model advisory (session-spawn.ts).\n *\n * Deliberately narrow to keep false positives near zero: a known id qualifies\n * ONLY when its {@link bareProduct} equals the input's while the full id\n * differs — i.e. the caller used the wrong-or-missing vendor/route prefix\n * (`deepseek-chat` → `deepseek/deepseek-chat`, `glm-5.2` → `z-ai/glm-5.2`,\n * `moonshot/kimi-k2` → `moonshotai/kimi-k2`). A genuinely-new model on a known\n * vendor (`qwen/qwen4-max` when only `qwen3-max` is catalogued) shares no bare\n * product and yields nothing — so a new / free-form slug never earns a spurious\n * suggestion, matching the money-safety guard's own never-reject-an-unknown-\n * model rule. Sourced from the catalog itself (`LLM_PRICING_CATALOG` +\n * `MODEL_ALIASES` keys), never a hand-maintained second list.\n */\nexport function suggestModelSlugs(model: string): string[] {\n const target = bareProduct(model)\n if (!target) return []\n const out = new Set<string>()\n for (const id of [...Object.keys(LLM_PRICING_CATALOG), ...Object.keys(MODEL_ALIASES)]) {\n if (id !== model && bareProduct(id) === target) out.add(id)\n }\n return [...out].sort().slice(0, 5)\n}\n\n/** Verdict of the spawn-time money-safety guard ({@link checkModelWalletEligibility}). */\nexport interface ModelWalletEligibility {\n ok: boolean\n /** Serviceable routes for the model that DIFFER from the resolved wallet —\n * the actionable set to re-spawn onto. Empty when `ok`. */\n suggestedRoutes: string[]\n}\n\n/**\n * Money-safety spawn guard (SPEC §1c): can the resolved billing wallet\n * `walletRoute` (the gateway id when a `route.gateway` is set, else the resolved\n * billing provider) service `model`? Reuses {@link serviceableModelRoutes} — no\n * parallel table. Returns `ok:true` when the model is serviceable on the wallet,\n * OR when the model is unknown to the catalog (empty serviceable set — a mismatch\n * cannot be positively proven, so the guard must not reject a possibly-legitimate\n * new model). Returns `ok:false` with the serviceable alternative routes ONLY\n * when the model IS serviceable on some route but NOT the resolved wallet — the\n * exact 404-upstream case (`deepseek/deepseek-v4-pro` on the Anthropic sub). The\n * guard only REJECTS; it never substitutes a wallet the operator didn't name.\n */\nexport function checkModelWalletEligibility(\n model: string,\n walletRoute: string,\n): ModelWalletEligibility {\n const serviceable = serviceableModelRoutes(model)\n if (serviceable.length === 0 || serviceable.includes(walletRoute)) {\n return { ok: true, suggestedRoutes: [] }\n }\n return { ok: false, suggestedRoutes: serviceable.filter(r => r !== walletRoute) }\n}\n\n/**\n * The actionable fail-fast message shared by both spawn paths (session-spawn +\n * session-restart-core) so they never drift. Names the wallet that couldn't\n * service the model AND the required route + api-key profile to re-spawn onto —\n * never a wallet the guard picked for the operator.\n */\nexport function modelWalletIneligibleMessage(opts: {\n prefix: string\n adapter: string\n model: string\n walletRoute: string\n walletMode?: \"subscription\" | \"api-key\"\n suggestedRoutes: string[]\n}): string {\n const wallet = opts.walletMode\n ? `\"${opts.walletRoute}\" ${opts.walletMode} wallet`\n : `\"${opts.walletRoute}\" wallet`\n const primary = opts.suggestedRoutes[0] ?? \"a gateway route\"\n const also =\n opts.suggestedRoutes.length > 1\n ? ` (also serviceable via ${opts.suggestedRoutes\n .slice(1)\n .map(r => `\"${r}\"`)\n .join(\", \")})`\n : \"\"\n return (\n `${opts.prefix}: model \"${opts.model}\" is not serviceable on the resolved ${wallet} ` +\n `(adapter \"${opts.adapter}\") and would 404 upstream. This model bills route \"${primary}\"${also} — ` +\n `re-spawn on it: set route.gateway=\"${primary}\" with an eligible \"${primary}\" api-key profile ` +\n `(access.profileRef). This guard only rejects; it never switches wallets for you.`\n )\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/catalog-models.ts"],"names":["routes"],"mappings":"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","file":"catalog-models.mjs","sourcesContent":["/**\n * Read-only catalog/vendor endpoint (`agentproto-session-config-axes`\n * SPEC §5) — `GET /catalog/models` + `catalog_models` MCP tool wire into\n * {@link buildCatalogModels}, the pure join this module owns.\n *\n * Reuses three already-shipped pieces instead of rebuilding them:\n * - the vendor/product/route model + router widening (OpenRouter/\n * Requesty/HuggingFace) from `@agentproto/model-catalog/route-identity`\n * (`resolveLlmModelRoute`, `route-identity/index.ts:396-511`) — this is\n * what keeps the catalog from being capped at any one adapter's\n * `models.allowed` list (SPEC §5.1);\n * - the profile eligibility predicate shipped in #470\n * (`@agentproto/auth`'s `eligibleProfiles`, `packages/auth/src/\n * eligibility.ts:81-89`) for the profile-aware `runnable` flag (SPEC\n * §5.3) — the old bare `hasKey` check (`packages/cli/src/commands/\n * models.ts:113-117`) is the degenerate one-profile-per-provider case\n * this predicate subsumes;\n * - `AdapterAuthDescriptor` (`spawn-defaults.ts:226`), the SAME\n * provider/authSubscription projection `resolveAuthSpec` reads, as the\n * source for which auth methods an adapter can present on its direct\n * route (SPEC §3.4's derivable replacement for a hand-maintained\n * `authSubscription` boolean).\n *\n * A gateway/router route (anything where the resolved route differs from\n * the model's vendor — `openrouter`, `requesty`, `huggingface`, or an\n * adapter's own gateway mode id like `moonshot`) always bills against the\n * route's own id and is always reached with an api-key credential — never\n * oauth-bearer, since no third-party gateway has an Anthropic-style\n * subscription bearer path (SPEC §1c: \"a moonshot profile, not the Claude\n * sub\"). That structural rule is what lets this module compute\n * `runnable`/`eligibleProfiles` for the widened, non-curated rows without\n * per-adapter gateway-vendor tables.\n */\n\nimport type { AuthMethod, AuthProfile } from \"@agentproto/auth\"\nimport { eligibleProfiles, type AdapterAuthManifest } from \"@agentproto/auth\"\nimport {\n resolveLlmModelRoute,\n tryParseModelRef,\n formatModelRef,\n} from \"@agentproto/model-catalog/route-identity\"\nimport {\n getModelProvider,\n resolvePricingExact,\n LLM_PRICING_CATALOG,\n MODEL_ALIASES,\n} from \"@agentproto/model-catalog/llm\"\nimport { getAnthropicGatewayPreset } from \"@agentproto/provider-presets\"\nimport type { AdapterAuthDescriptor } from \"./spawn-defaults.js\"\nimport type { RouteSpec } from \"./session-config.js\"\n\nexport type { RouteSpec } from \"./session-config.js\"\n\n/** Routers the catalog probes to widen beyond any adapter's declared model\n * list (SPEC §5.1) — same three route-identity widens, `route-identity/\n * index.ts:54-59`. */\nconst WIDENING_ROUTES = [\"openrouter\", \"requesty\", \"huggingface\"] as const\n\n/**\n * Vendor-specific compatibility routes: canonical protocol surfaces that can\n * bill the same model family without duplicating catalog entries. A model whose\n * pricing-catalog vendor is `xai` is legitimately reachable both on the native\n * `xai` API route and on the Anthropic-compatible `xai-anthropic` route; the\n * two routes are distinct billing endpoints so the right auth profile matches\n * the right protocol.\n */\nconst VENDOR_COMPATIBILITY_ROUTES: Readonly<Record<string, readonly string[]>> = {\n xai: [\"xai\", \"xai-anthropic\"],\n}\n\n/** One model entry as declared in an adapter's `models.allowed`\n * (`AdapterModelInfo`, `packages/cli/src/registry/resolve.ts:134-142`) —\n * the subset this module needs. */\nexport interface CatalogAdapterModelInput {\n /** Model id exactly as declared — bare (`\"claude-opus-4-8\"`) or\n * `vendor/product` form. */\n id: string\n /** The billing provider/route this model entry reaches on (`\n * AdapterModelInfo.provider`) — e.g. `\"moonshot\"` or `\"openrouter\"`.\n * Takes precedence over the id's own implied vendor route. */\n provider?: string\n /** The adapter mode id that must be applied to reach this model on a\n * non-direct route (`AdapterModelInfo.mode`) — e.g. `\"moonshot\"`.\n * Undefined ⇒ direct route (the model's own vendor, or `provider`). */\n mode?: string\n}\n\n/** One installed adapter's contribution to the catalog. */\nexport interface CatalogAdapterInput {\n slug: string\n models: readonly CatalogAdapterModelInput[]\n /** This adapter's billing-auth capability on its DIRECT route — the same\n * projection `resolveAuthSpec` reads (`spawn-defaults.ts:226`). Omitted\n * ⇒ the adapter presents no auth method, so rows it curates are\n * discoverable but never runnable through it alone. */\n authDescriptor?: AdapterAuthDescriptor\n /** How this adapter's spawn ROUTE relates to the chosen model (AIP-45\n * launch-menu drill-down). `\"free\"` = the adapter can route arbitrary\n * models through gateways (`base_url`). `\"derived-from-model\"` = the\n * endpoint falls out of the model id's vendor prefix. Absent/undefined\n * ⇒ fixed single-provider adapter; widened gateway routes are not\n * attached to it. */\n routeSelection?: \"free\" | \"derived-from-model\"\n}\n\nexport interface CatalogModelsQuery {\n /** Keep only routes reachable via this adapter slug. */\n adapter?: string\n /** Keep only this vendor's entry. */\n vendor?: string\n /** Keep only routes with this route id. */\n route?: string\n /** Drop every route with `runnable: false`. */\n runnableOnly?: boolean\n}\n\nexport interface CatalogPricing {\n inPer1M: number\n outPer1M: number\n}\n\nexport interface CatalogRoute {\n route: string\n ref: string\n baseUrl: string | null\n pricing: CatalogPricing | null\n runnable: boolean\n eligibleProfiles: string[]\n adapterModes: string[]\n adapters: string[]\n curated: boolean\n /**\n * True when MORE THAN ONE distinct model is servable on this route across\n * the whole catalog join (AIP-45 launch-menu drill-down, SPEC §3/§5.3).\n * Derived from the model×route join, never a hand-maintained table:\n * `anthropic`/`openrouter`/`llm-endpoint` serve many models (`true`),\n * a single-model gateway like `moonshot` serves one (`false`). A\n * single-model route pins every model tier to its one model downstream,\n * and gates the custom-gateway A-vs-B promotion (SPEC D5). Independent of\n * the caller's query filters — this is the route's intrinsic capacity, so\n * the same route reports the same `multiModel` in a filtered response.\n */\n multiModel: boolean\n}\n\n/**\n * One route's servable-model index (SPEC §3.9) — the flat, per-route view\n * that lets a capability-derived UI derive tier pinning without re-walking\n * the vendor/product tree. `servableModels` is the set of `vendor/product`\n * model identities reachable on this route across the whole join;\n * `multiModel` is `servableModels.length > 1` (the same value carried on\n * every {@link CatalogRoute} with this id). Always the full, unfiltered\n * catalog capacity, independent of the query — a route's model-count is\n * intrinsic, not a view of the filtered result.\n */\nexport interface CatalogRouteSummary {\n route: string\n servableModels: string[]\n multiModel: boolean\n}\n\nexport interface CatalogProduct {\n product: string\n routes: CatalogRoute[]\n}\n\nexport interface CatalogVendor {\n vendor: string\n products: CatalogProduct[]\n}\n\nexport interface CatalogModelsResponse {\n vendors: CatalogVendor[]\n /**\n * Flat servable-models-per-route index (SPEC §3.9), one entry per distinct\n * route id in the catalog, sorted by route. Exposes the same join the\n * nested `multiModel` flags derive from, so a capability layer can compute\n * tier pinning per route without re-walking the vendor tree. Always the\n * full, unfiltered catalog capacity (see {@link CatalogRouteSummary}).\n */\n routes: CatalogRouteSummary[]\n}\n\nexport interface BuildCatalogModelsInput {\n adapters: readonly CatalogAdapterInput[]\n profiles: readonly AuthProfile[]\n query?: CatalogModelsQuery\n}\n\n/** A resolved model identity + pricing, independent of which adapter (if\n * any) declared it. */\ninterface ResolvedModel {\n vendor: string\n product: string\n /** The route this id resolves to on its OWN — vendor for a direct id,\n * a router name when the id already carries `@route`. */\n directRoute: string\n ref: string\n baseUrl: string | null\n pricing: CatalogPricing | null\n}\n\n/** Rewrite a router-prefixed id (`<router>/<vendor>/<product>`) into the\n * canonical route-identity `<vendor>/<product>@<router>` the parser accepts.\n *\n * Mastra-style adapters (e.g. `adapters/mastra-agent`) declare their model\n * ids in `<provider>/<upstream-id>` form, and for a gateway router the\n * upstream id is itself `<vendor>/<product>` — so a native OpenRouter id like\n * `z-ai/glm-5.2` is advertised as the 3-segment `openrouter/z-ai/glm-5.2`.\n * `parseModelRef` splits on the FIRST `/` and rejects a product that still\n * contains one (`route-identity/index.ts` SEGMENT_RE), so feeding it the raw\n * 3-segment string throws and, before this normalization, 500'd the whole\n * catalog. The route-identity grammar's canonical form for such a model is\n * `<vendor>/<product>@<router>` (`z-ai/glm-5.2@openrouter`) — the `@route`\n * suffix, NOT a leading route segment — so we recompose to that. Only the\n * known gateway routers (whose native ids are `<vendor>/<product>`) are\n * peeled; every other id is returned untouched. A `:pin` variant/provider\n * suffix on the upstream id is preserved (it rides along in the remainder).\n */\nfunction normalizeRouterPrefixedId(id: string): string {\n const firstSlash = id.indexOf(\"/\")\n if (firstSlash === -1) return id\n const head = id.slice(0, firstSlash)\n if (!(WIDENING_ROUTES as readonly string[]).includes(head)) return id\n const remainder = id.slice(firstSlash + 1)\n // Only a genuine `<vendor>/<product>` upstream id (still carrying a `/`) is\n // the router-prefixed shape; a 2-segment `<router>/<product>` is left alone.\n if (!remainder.includes(\"/\") || remainder.includes(\"@\")) return id\n return `${remainder}@${head}`\n}\n\n/** {@link resolveLlmModelRoute} that never throws. `resolveLlmModelRoute`\n * parses through the strict `parseModelRef`, which throws on a still-\n * unparseable ref (e.g. an unrecognised 3-segment id that normalization\n * didn't rewrite) — defense-in-depth so a single bad id can never 500 the\n * catalog. */\nfunction tryResolveLlmModelRoute(id: string): ReturnType<typeof resolveLlmModelRoute> {\n try {\n return resolveLlmModelRoute(id)\n } catch {\n return undefined\n }\n}\n\n/** Infer a vendor from a bare id family prefix — ports `providerFromIdPrefix`\n * (`packages/cli/src/commands/models.ts:47-54`) so a model id that predates\n * its pricing-catalog entry still gets a vendor instead of `\"unknown\"`. */\nfunction vendorFromIdPrefix(bareId: string): string | undefined {\n if (/^claude[-/]/.test(bareId)) return \"anthropic\"\n if (/^(gpt[-/]|o[1-9](-|$)|chatgpt)/.test(bareId)) return \"openai\"\n if (/^gemini[-/]/.test(bareId)) return \"google\"\n if (/^grok[-/]/.test(bareId)) return \"x-ai\"\n if (/^deepseek[-/]/.test(bareId)) return \"deepseek\"\n return undefined\n}\n\n/** Resolve a model id to its vendor/product/route/pricing, in three tiers:\n * the route-identity resolver (handles both `vendor/product[@route]` and\n * legacy bare ids with pricing), then a bare vendor/product parse with no\n * pricing, then the id-prefix heuristic as a last resort. Never throws —\n * every id gets SOME vendor rather than being dropped from the catalog. */\nfunction resolveModelId(id: string): ResolvedModel {\n // A router-prefixed Mastra id (`openrouter/z-ai/glm-5.2`) becomes its\n // canonical route-identity (`z-ai/glm-5.2@openrouter`) BEFORE any parse, so\n // it resolves to a real vendor/product/route row instead of throwing.\n const normalized = normalizeRouterPrefixedId(id)\n const resolved = tryResolveLlmModelRoute(normalized)\n if (resolved) {\n return {\n vendor: resolved.vendor,\n product: resolved.product,\n directRoute: resolved.route,\n ref: formatModelRef(resolved.ref),\n baseUrl: resolved.transport.baseUrl ?? null,\n pricing: {\n inPer1M: resolved.pricing.inputPer1M,\n outPer1M: resolved.pricing.outputPer1M,\n },\n }\n }\n const parsed = tryParseModelRef(normalized)\n if (parsed) {\n return {\n vendor: parsed.vendor,\n product: parsed.product,\n directRoute: parsed.route,\n ref: formatModelRef(parsed),\n baseUrl: null,\n pricing: null,\n }\n }\n const vendor = vendorFromIdPrefix(id) ?? \"unknown\"\n return {\n vendor,\n product: id,\n directRoute: vendor,\n ref: `${vendor}/${id}`,\n baseUrl: null,\n pricing: null,\n }\n}\n\n/** Which auth methods are presentable on a model's DIRECT route — whatever\n * the adapter's descriptor declares, derivable from `authSubscription`/\n * `provider` exactly as SPEC §3.4 calls for. Only called for a genuinely\n * direct route (see `isDirectRoute`); a gateway/router route is always\n * api-key only (SPEC §1c — no third-party gateway has an oauth-bearer\n * path), regardless of what the underlying model's own vendor is. */\nfunction methodsForDirect(\n descriptor: AdapterAuthDescriptor | undefined,\n): AuthMethod[] {\n const methods: AuthMethod[] = []\n if (descriptor?.authSubscription || descriptor?.modelDerivedApiKey)\n methods.push(\"oauth-bearer\")\n if (descriptor?.provider || descriptor?.modelDerivedApiKey) methods.push(\"api-key\")\n return methods\n}\n\n/** True iff this model entry resolves to its OWN vendor with no adapter\n * mode override — i.e. reached without any gateway/router redirection.\n * An explicit `model.mode` (adapter gateway mode, e.g. `\"moonshot\"`) is\n * ALWAYS a redirection even when the mode id happens to equal the\n * model's resolved vendor (e.g. a `moonshot/kimi-…` model routed via\n * claude-code's `moonshot` mode) — SPEC §1c's \"moonshot profile, not the\n * Claude sub\" holds regardless of whose model is being served. An id\n * that already carries its own `@route` suffix (`resolved.directRoute !==\n * resolved.vendor`) is equally a router path even with no adapter mode.\n * A `model.provider` that matches the model's own vendor is still a\n * direct route; only a provider/route that differs is a redirection. */\nfunction isDirectRoute(route: string, mode: string | undefined, resolved: ResolvedModel): boolean {\n return mode === undefined && route === resolved.vendor && resolved.directRoute === resolved.vendor\n}\n\ninterface RouteContribution {\n vendor: string\n product: string\n route: string\n ref: string\n baseUrl: string | null\n pricing: CatalogPricing | null\n curated: boolean\n adapterSlug?: string\n adapterMode?: string\n methods: readonly AuthMethod[]\n}\n\n/** Curated contributions — one per adapter-declared model entry. */\nfunction curatedContributions(\n adapters: readonly CatalogAdapterInput[],\n): RouteContribution[] {\n const out: RouteContribution[] = []\n for (const adapter of adapters) {\n for (const model of adapter.models) {\n const resolved = resolveModelId(model.id)\n const route = model.mode ?? model.provider ?? resolved.directRoute\n const methods: AuthMethod[] = isDirectRoute(route, model.mode, resolved)\n ? methodsForDirect(adapter.authDescriptor)\n : [\"api-key\"]\n out.push({\n vendor: resolved.vendor,\n product: resolved.product,\n route,\n ref: resolved.ref,\n baseUrl: resolved.baseUrl,\n pricing: resolved.pricing,\n curated: true,\n adapterSlug: adapter.slug,\n ...(model.mode ? { adapterMode: model.mode } : {}),\n methods,\n })\n }\n }\n return out\n}\n\n/** Widen beyond every adapter's declared list (SPEC §5.1): for each\n * distinct (vendor, product) already known from a curated contribution,\n * probe the router routes route-identity knows about and add a\n * non-curated contribution for any that resolve and aren't already\n * covered by a curated row.\n *\n * Widened routes are only attached to adapters that explicitly declare\n * `routeSelection: \"free\"` — those are the adapters that can route an\n * arbitrary model through a gateway via `base_url`. Fixed-provider and\n * `derived-from-model` adapters must not be advertised on routes they\n * cannot truthfully choose. */\nfunction widenedContributions(\n adapters: readonly CatalogAdapterInput[],\n curated: readonly RouteContribution[],\n): RouteContribution[] {\n const freeAdapters = adapters\n .filter(a => a.routeSelection === \"free\")\n .map(a => a.slug)\n if (freeAdapters.length === 0) return []\n\n const seenProducts = new Map<string, Set<string>>() // \"vendor/product\" -> routes already present\n for (const c of curated) {\n const key = `${c.vendor}/${c.product}`\n const routes = seenProducts.get(key) ?? new Set<string>()\n routes.add(c.route)\n seenProducts.set(key, routes)\n }\n\n const out: RouteContribution[] = []\n for (const [key, existingRoutes] of seenProducts) {\n const [vendor, product] = key.split(\"/\", 2) as [string, string]\n for (const router of WIDENING_ROUTES) {\n if (existingRoutes.has(router)) continue\n const resolved = resolveLlmModelRoute(`${vendor}/${product}@${router}`)\n if (!resolved) continue\n for (const adapterSlug of freeAdapters) {\n out.push({\n vendor,\n product,\n route: router,\n ref: formatModelRef(resolved.ref),\n baseUrl: resolved.transport.baseUrl ?? null,\n pricing: {\n inPer1M: resolved.pricing.inputPer1M,\n outPer1M: resolved.pricing.outputPer1M,\n },\n curated: false,\n adapterSlug,\n methods: [\"api-key\"],\n })\n }\n }\n }\n return out\n}\n\n/**\n * Compatibility routes: canonical protocol surfaces that bill a model family\n * without duplicating pricing entries. For xAI, the same Grok models are\n * reachable on the native OpenAI-flavor `xai` route and on the Anthropic-\n * compatible `xai-anthropic` route; the catalog joins both so the matching\n * auth profile (`xai` vs `xai-anthropic`) can be endpoint-eligible for the\n * right protocol.\n *\n * Rows are generated straight from the static pricing catalog (vendor `xai`),\n * not from adapter curation, because the existing adapters only declare the\n * OpenRouter xAI ids (`x-ai/grok-*@openrouter`). The `xai-anthropic` rows are\n * attached to adapters that can route arbitrary models through an Anthropic-\n * compatible gateway (`routeSelection: \"free\"`). The direct `xai` rows carry\n * no adapter attachment because there is no agent adapter in the registry that\n * speaks the native xAI OpenAI surface today; they are still listed so the\n * `xai` profile can see the models it bills and whether they are runnable.\n */\nfunction compatibilityContributions(\n adapters: readonly CatalogAdapterInput[],\n): RouteContribution[] {\n const freeAdapters = adapters\n .filter(a => a.routeSelection === \"free\")\n .map(a => a.slug)\n const out: RouteContribution[] = []\n\n for (const [product, pricing] of Object.entries(LLM_PRICING_CATALOG)) {\n if (pricing.provider !== \"xai\") continue\n const vendor = pricing.vendor ?? \"xai\"\n const compatRoutes = VENDOR_COMPATIBILITY_ROUTES[vendor]\n if (!compatRoutes) continue\n\n const direct = resolveLlmModelRoute(`${vendor}/${product}`)\n if (!direct) continue\n\n for (const route of compatRoutes) {\n const ref = route === vendor ? `${vendor}/${product}` : `${vendor}/${product}@${route}`\n const baseUrl =\n route === \"xai-anthropic\"\n ? getAnthropicGatewayPreset(\"xai-anthropic\").baseUrl\n : null\n const rowBase: RouteContribution = {\n vendor,\n product,\n route,\n ref,\n baseUrl,\n pricing: {\n inPer1M: direct.pricing.inputPer1M,\n outPer1M: direct.pricing.outputPer1M,\n },\n curated: false,\n methods: [\"api-key\"],\n }\n if (route === \"xai-anthropic\" && freeAdapters.length > 0) {\n for (const adapterSlug of freeAdapters) {\n out.push({ ...rowBase, adapterSlug })\n }\n } else {\n out.push(rowBase)\n }\n }\n }\n return out\n}\n\ninterface MergedRow {\n vendor: string\n product: string\n route: string\n ref: string\n baseUrl: string | null\n pricing: CatalogPricing | null\n curated: boolean\n adapters: string[]\n adapterModes: string[]\n methods: AuthMethod[]\n}\n\n/** Merge contributions sharing a (vendor, product, route) key — multiple\n * adapters can curate the same route, and a widened route always merges\n * into whichever curated row already claimed it. */\nfunction mergeContributions(\n contributions: readonly RouteContribution[],\n): MergedRow[] {\n const rows = new Map<string, MergedRow>()\n for (const c of contributions) {\n const key = `${c.vendor}\u0000${c.product}\u0000${c.route}`\n const existing = rows.get(key)\n if (!existing) {\n rows.set(key, {\n vendor: c.vendor,\n product: c.product,\n route: c.route,\n ref: c.ref,\n baseUrl: c.baseUrl,\n pricing: c.pricing,\n curated: c.curated,\n adapters: c.adapterSlug ? [c.adapterSlug] : [],\n adapterModes: c.adapterMode ? [c.adapterMode] : [],\n methods: [...c.methods],\n })\n continue\n }\n existing.curated = existing.curated || c.curated\n existing.baseUrl = existing.baseUrl ?? c.baseUrl\n existing.pricing = existing.pricing ?? c.pricing\n if (c.adapterSlug && !existing.adapters.includes(c.adapterSlug)) {\n existing.adapters.push(c.adapterSlug)\n }\n if (c.adapterMode && !existing.adapterModes.includes(c.adapterMode)) {\n existing.adapterModes.push(c.adapterMode)\n }\n for (const m of c.methods) {\n if (!existing.methods.includes(m)) existing.methods.push(m)\n }\n }\n return [...rows.values()]\n}\n\n/** The billed vendor for a (vendor, route) pair — the model's own vendor on\n * its direct route, else the route's own id (SPEC §1c). */\nfunction billedVendor(vendor: string, route: string): string {\n return route === vendor ? vendor : route\n}\n\n/**\n * Per-model curation gate (WS3): does `profile`'s allowlist admit this model?\n * An ABSENT `models` field, or `mode: \"all\"`, admits everything — the exact\n * pre-curation behavior, so a profile that predates the field is unchanged\n * (the byte-identical invariant). A `mode: \"allow\"` profile admits the model\n * only when its catalog identity is in the list — matched against BOTH the\n * route-qualified `ref` (`z-ai/glm-5.2@openrouter`) and the route-independent\n * `vendor/product` (`z-ai/glm-5.2`), so a curated id in either form is honored.\n * It ALSO honors the BARE product (`claude-opus-4-8` — everything after the\n * first `/` of `vendorProduct`), but ONLY on a DIRECT route (a `ref` with no\n * `@route` suffix). The vscode \"+ Models\" picker writes bare pricing-catalog\n * ids verbatim into an allowlist for single-vendor DIRECT endpoints (anthropic,\n * moonshot, …), and every existing user allowlist for those is in that form, so\n * the bare tolerance keeps them working with zero migration. It is deliberately\n * NOT extended to multi-vendor GATEWAY endpoints (openrouter/requesty host the\n * same bare product under many vendor prefixes — `sference/glm-5.2` and\n * `z-ai/glm-5.2` are distinct rows on the same endpoint), where a bare id would\n * over-widen across sibling vendors. For those the `ref` (`z-ai/glm-5.2@openrouter`)\n * or the `vendor/product` form (`z-ai/glm-5.2`) is required — which is exactly\n * what the picker writes for gateway endpoints anyway.\n * This is deliberately downstream of `eligibleProfiles` (the endpoint/method\n * gate): a curated profile stays endpoint-eligible but services only its\n * chosen model refs.\n */\nfunction profileAllowsModel(profile: AuthProfile, ref: string, vendorProduct: string): boolean {\n const curation = profile.models\n if (!curation || curation.mode === \"all\") return true\n // The bare product is everything after the FIRST `/` (the vendor is always\n // the first segment; the product may itself contain `/`). No slash → treat\n // the whole string as the product.\n const slash = vendorProduct.indexOf(\"/\")\n const product = slash === -1 ? vendorProduct : vendorProduct.slice(slash + 1)\n // A gateway/router route carries an `@route` suffix in its ref\n // (`z-ai/glm-5.2@openrouter`); a direct route ref is bare `vendor/product`.\n // Bare-product tolerance is DIRECT-only — on a multi-vendor gateway a bare id\n // would admit every sibling vendor's same-named product.\n const isDirect = !ref.includes(\"@\")\n return (\n curation.ids.includes(ref) ||\n curation.ids.includes(vendorProduct) ||\n (isDirect && curation.ids.includes(product))\n )\n}\n\n/** The pure join (SPEC §5): adapter-declared models + router widening +\n * the #470 eligibility predicate → the vendor/product/route tree. No I/O —\n * callers (the HTTP route / MCP tool) own loading adapters + profiles. */\nexport function buildCatalogModels(\n input: BuildCatalogModelsInput,\n): CatalogModelsResponse {\n const curated = curatedContributions(input.adapters)\n const contributions = [\n ...curated,\n ...widenedContributions(input.adapters, curated),\n ...compatibilityContributions(input.adapters),\n ]\n const merged = mergeContributions(contributions)\n const query = input.query ?? {}\n\n // Servable-models-per-route (SPEC §3.9), over the FULL join — a route's\n // model-count is an intrinsic capability, not a view of the caller's\n // query, so `multiModel` stays stable under filtering. A model identity is\n // its `vendor/product` (the same key `widenedContributions` dedupes on), so\n // the same product served by several adapters counts once.\n const servableByRoute = new Map<string, Set<string>>()\n for (const row of merged) {\n const set = servableByRoute.get(row.route) ?? new Set<string>()\n set.add(`${row.vendor}/${row.product}`)\n servableByRoute.set(row.route, set)\n }\n const isMultiModel = (route: string): boolean =>\n (servableByRoute.get(route)?.size ?? 0) > 1\n\n const vendors = new Map<string, Map<string, CatalogRoute[]>>()\n for (const row of merged) {\n if (query.vendor && row.vendor !== query.vendor) continue\n if (query.route && row.route !== query.route) continue\n if (query.adapter && !row.adapters.includes(query.adapter)) continue\n\n // Per-model wallet-serviceability gate (SPEC §1c parity with the spawn\n // guard, `checkModelWalletEligibility` / session-spawn.ts:1143): a DIRECT\n // vendor route — `row.route === row.vendor` with no adapter-mode override\n // in play (`row.adapterModes.length === 0`; an override can coincidentally\n // route === vendor, e.g. a `moonshot` mode reached serving a moonshot-\n // vendor model — see `isDirectRoute`'s own doc comment) — is the catalog's\n // equivalent of spawning with no `route.gateway` named: the adapter's\n // fixed wallet is billed, and that wallet might not actually service this\n // specific model (`claude-fable-5` bills `openrouter`/`requesty`, never\n // `anthropic`, even though the `claude-code` adapter curates it). Reuses\n // the SAME predicate the spawn guard uses — no parallel per-model table.\n // A gateway/mode/router route is the catalog's equivalent of an EXPLICIT\n // `route.gateway` — the spawn guard deliberately never second-guesses\n // that (routing any model through any named gateway's api-key is\n // legitimate), so it is left ungated here too.\n const isDirectVendorRoute = row.route === row.vendor && row.adapterModes.length === 0\n const walletEligible =\n !isDirectVendorRoute ||\n checkModelWalletEligibility(`${row.vendor}/${row.product}`, row.route).ok\n\n const manifest: AdapterAuthManifest = {\n id: `${row.vendor}/${row.product}@${row.route}`,\n endpointByRoute: { [row.route]: billedVendor(row.vendor, row.route) },\n methodsByRoute: { [row.route]: row.methods },\n }\n // The endpoint/method-eligible profiles for this route (whole-profile\n // enable/disable already applied inside `eligibleProfiles`), then narrowed\n // by each profile's per-model curation allowlist (WS3). A profile with no\n // `models` field passes through untouched, so the non-curated join is\n // byte-identical to before.\n const eligible = walletEligible\n ? eligibleProfiles(input.profiles, manifest, row.route).filter(p =>\n profileAllowsModel(p, row.ref, `${row.vendor}/${row.product}`),\n )\n : []\n const runnable = eligible.length > 0\n if (query.runnableOnly && !runnable) continue\n\n const route: CatalogRoute = {\n route: row.route,\n ref: row.ref,\n baseUrl: row.baseUrl,\n pricing: row.pricing,\n runnable,\n eligibleProfiles: eligible.map(p => p.id),\n adapterModes: row.adapterModes,\n adapters: row.adapters,\n curated: row.curated,\n multiModel: isMultiModel(row.route),\n }\n\n const products = vendors.get(row.vendor) ?? new Map<string, CatalogRoute[]>()\n vendors.set(row.vendor, products)\n const routes = products.get(row.product) ?? []\n products.set(row.product, routes)\n routes.push(route)\n }\n\n const result: CatalogVendor[] = [...vendors.entries()]\n .sort(([a], [b]) => a.localeCompare(b))\n .map(([vendor, products]) => ({\n vendor,\n products: [...products.entries()]\n .sort(([a], [b]) => a.localeCompare(b))\n .map(([product, routes]) => ({\n product,\n routes: [...routes].sort((a, b) => a.route.localeCompare(b.route)),\n })),\n }))\n\n const routes: CatalogRouteSummary[] = [...servableByRoute.entries()]\n .sort(([a], [b]) => a.localeCompare(b))\n .map(([route, models]) => ({\n route,\n servableModels: [...models].sort((a, b) => a.localeCompare(b)),\n multiModel: models.size > 1,\n }))\n\n return { vendors: result, routes }\n}\n\n/**\n * Resolve the effective billing route for a session: a parseable model ref's\n * own route is authoritative, `routeGateway` is only consulted when the model\n * carries no explicit `@route` or is unparseable (including router-prefixed ids\n * like `openrouter/vendor/product`, which are normalized to the canonical\n * suffix form first).\n */\nexport function resolveEffectiveRoute(\n model: string | undefined,\n routeGateway: string | undefined,\n): string | undefined {\n if (!model) return routeGateway\n const normalized = normalizeRouterPrefixedId(model)\n const parsed = tryParseModelRef(normalized)\n if (!parsed) return routeGateway\n if (parsed.route !== parsed.vendor) return parsed.route\n return routeGateway ?? parsed.route\n}\n\n/** Rewrite `model`'s `@route` suffix to `route`. Bare/unparseable ids are\n * returned unchanged because they have no suffix to rewrite. */\nexport function modelWithRoute(model: string, route: string): string {\n const parsed = tryParseModelRef(model)\n if (!parsed) return model\n return formatModelRef({ ...parsed, route })\n}\n\n/**\n * Reconcile independent `model` and `route` overrides so they never describe\n * two different billing endpoints. Returns the effective values; mutating\n * callers replace their effective model/route with these results.\n *\n * Rules:\n * - explicit `@route` on the model wins over a stale/conflicting route.gateway;\n * - a route-only override rewrites a parseable model string to match;\n * - a model-only override synthesizes a route field whenever the model's\n * resolved route differs from the previous route.gateway;\n * - passing BOTH overrides that contradict each other is a caller bug → throw.\n */\nexport function reconcileModelRoute(input: {\n prevModel?: string\n prevRoute?: RouteSpec\n model?: string\n route?: RouteSpec\n}): { model?: string; route?: RouteSpec } {\n const { prevModel, prevRoute, model: overrideModel, route: overrideRoute } = input\n\n if (overrideModel !== undefined && overrideRoute !== undefined) {\n const parsed = tryParseModelRef(overrideModel)\n if (parsed && parsed.route !== parsed.vendor && parsed.route !== overrideRoute.gateway) {\n throw new Error(\n `reconcileModelRoute: model \"${overrideModel}\" pins route \"${parsed.route}\" ` +\n `but route override is \"${overrideRoute.gateway}\"`,\n )\n }\n return { model: overrideModel, route: overrideRoute }\n }\n\n if (overrideModel !== undefined) {\n const parsed = tryParseModelRef(overrideModel)\n if (parsed) {\n const hasExplicitRoute = parsed.route !== parsed.vendor\n // An explicit @route always deserves a matching route field; a vendor-\n // implied route only rewrites a stale/conflicting prevRoute.\n if (hasExplicitRoute && prevRoute?.gateway !== parsed.route) {\n return { model: overrideModel, route: { gateway: parsed.route } }\n }\n if (!hasExplicitRoute && prevRoute && prevRoute.gateway !== parsed.route) {\n return { model: overrideModel, route: { gateway: parsed.route } }\n }\n }\n return { model: overrideModel, route: prevRoute }\n }\n\n if (overrideRoute !== undefined) {\n if (prevModel !== undefined) {\n return { model: modelWithRoute(prevModel, overrideRoute.gateway), route: overrideRoute }\n }\n return { route: overrideRoute }\n }\n\n return { model: prevModel, route: prevRoute }\n}\n\n/**\n * The billing routes that can service `model`, reusing the SAME route-identity\n * resolution this module's catalog join uses (SPEC §1c serviceability) — never\n * a parallel per-model provider table. A route R services the model iff:\n * - R is the model's catalog billing provider (`getModelProvider` — the native\n * vendor SDK for a bare/direct id, or the router for a gateway-only slash id\n * like `deepseek/deepseek-v4-pro` ⇒ `openrouter`),\n * - R is an explicit `@route` carried in the id (`…@openrouter`), or\n * - the model resolves on R as one of the {@link WIDENING_ROUTES} router routes\n * (catches requesty-only ids like `sference/…` that carry no pricing-catalog\n * `provider` for `getModelProvider` to return).\n *\n * Returns the DISTINCT serviceable routes; EMPTY when the model is unknown to\n * the catalog — a mismatch cannot then be positively proven, so a money-safety\n * caller MUST NOT reject on an empty result (only on a non-empty set that\n * excludes the resolved wallet). See {@link checkModelWalletEligibility}.\n */\nexport function serviceableModelRoutes(model: string): string[] {\n const routes = new Set<string>()\n const provider = getModelProvider(model)\n if (provider) routes.add(provider)\n const parsed = tryParseModelRef(normalizeRouterPrefixedId(model))\n if (parsed) {\n // An explicit `@route` in the id is itself a serviceable route (and keeps\n // the `getModelProvider` quirk on `<vendor>/<product>@<router>` forms from\n // hiding the route the operator literally named).\n if (parsed.route !== parsed.vendor) routes.add(parsed.route)\n // A first-party vendor model whose `<vendor>/<product>` form COLLIDES with a\n // router-namespaced pricing key hides its own direct vendor route: OpenRouter\n // keys e.g. `anthropic/claude-sonnet-5` / `anthropic/claude-fable-5` with the\n // SAME dash spelling Anthropic uses, tagged `provider:\"openrouter\"`, so\n // `getModelProvider(\"anthropic/claude-sonnet-5\")` resolves to the ROUTER and\n // `anthropic` never enters `routes` above. Restore it: when the BARE product\n // is itself a first-party model of this vendor — a VERBATIM first-party\n // pricing key (`resolvePricingExact(product).provider === vendor`) — the\n // vendor's own SDK/wallet can bill it, so the direct vendor route genuinely\n // IS serviceable. This MUST be the exact lookup, never `resolvePricing`'s\n // substring fallback: an openrouter-only sibling variant like\n // `google/gemini-2.5-flash-image` substring-hits the unrelated\n // `gemini-2.5-flash` row (provider `google`) and would spuriously earn a\n // `google` direct route it cannot actually be billed on. A gateway-only\n // slash id (`deepseek/deepseek-v4-pro` — no bare first-party pricing row)\n // likewise yields no exact match and is left untouched, preserving the\n // money-safety guard's reject on a true router-only model. Scoped to this\n // function: `resolvePricingExact`/`getModelProvider` semantics are unchanged\n // for every other caller.\n if (resolvePricingExact(parsed.product)?.provider === parsed.vendor) {\n routes.add(parsed.vendor)\n }\n for (const router of WIDENING_ROUTES) {\n if (tryResolveLlmModelRoute(`${parsed.vendor}/${parsed.product}@${router}`)) {\n routes.add(router)\n }\n }\n }\n return [...routes]\n}\n\n/** The bare product of a model id — the segment after the last `/`, stripped\n * of any `@route` / `:pin` suffix, lower-cased. `deepseek/deepseek-chat` →\n * `deepseek-chat`; `z-ai/glm-5.2@openrouter` → `glm-5.2`; `claude-opus-4-8`\n * (no slash) → `claude-opus-4-8`. This is the identity the \"did you mean\"\n * matcher keys on, so a wrong-or-missing vendor/route prefix collapses onto\n * the same product. */\nfunction bareProduct(id: string): string {\n const noSuffix = id.split(\"@\")[0]!.split(\":\")[0]!\n const slash = noSuffix.lastIndexOf(\"/\")\n return (slash === -1 ? noSuffix : noSuffix.slice(slash + 1)).toLowerCase()\n}\n\n/**\n * Closest known catalog model ids for a slug UNKNOWN to the local catalog —\n * the \"did you mean\" set behind the spawn-time model advisory (session-spawn.ts).\n *\n * Deliberately narrow to keep false positives near zero: a known id qualifies\n * ONLY when its {@link bareProduct} equals the input's while the full id\n * differs — i.e. the caller used the wrong-or-missing vendor/route prefix\n * (`deepseek-chat` → `deepseek/deepseek-chat`, `glm-5.2` → `z-ai/glm-5.2`,\n * `moonshot/kimi-k2` → `moonshotai/kimi-k2`). A genuinely-new model on a known\n * vendor (`qwen/qwen4-max` when only `qwen3-max` is catalogued) shares no bare\n * product and yields nothing — so a new / free-form slug never earns a spurious\n * suggestion, matching the money-safety guard's own never-reject-an-unknown-\n * model rule. Sourced from the catalog itself (`LLM_PRICING_CATALOG` +\n * `MODEL_ALIASES` keys), never a hand-maintained second list.\n */\nexport function suggestModelSlugs(model: string): string[] {\n const target = bareProduct(model)\n if (!target) return []\n const out = new Set<string>()\n for (const id of [...Object.keys(LLM_PRICING_CATALOG), ...Object.keys(MODEL_ALIASES)]) {\n if (id !== model && bareProduct(id) === target) out.add(id)\n }\n return [...out].sort().slice(0, 5)\n}\n\n/** Verdict of the spawn-time money-safety guard ({@link checkModelWalletEligibility}). */\nexport interface ModelWalletEligibility {\n ok: boolean\n /** Serviceable routes for the model that DIFFER from the resolved wallet —\n * the actionable set to re-spawn onto. Empty when `ok`. */\n suggestedRoutes: string[]\n}\n\n/**\n * Money-safety spawn guard (SPEC §1c): can the resolved billing wallet\n * `walletRoute` (the gateway id when a `route.gateway` is set, else the resolved\n * billing provider) service `model`? Reuses {@link serviceableModelRoutes} — no\n * parallel table. Returns `ok:true` when the model is serviceable on the wallet,\n * OR when the model is unknown to the catalog (empty serviceable set — a mismatch\n * cannot be positively proven, so the guard must not reject a possibly-legitimate\n * new model). Returns `ok:false` with the serviceable alternative routes ONLY\n * when the model IS serviceable on some route but NOT the resolved wallet — the\n * exact 404-upstream case (`deepseek/deepseek-v4-pro` on the Anthropic sub). The\n * guard only REJECTS; it never substitutes a wallet the operator didn't name.\n */\nexport function checkModelWalletEligibility(\n model: string,\n walletRoute: string,\n): ModelWalletEligibility {\n const serviceable = serviceableModelRoutes(model)\n if (serviceable.length === 0 || serviceable.includes(walletRoute)) {\n return { ok: true, suggestedRoutes: [] }\n }\n return { ok: false, suggestedRoutes: serviceable.filter(r => r !== walletRoute) }\n}\n\n/**\n * The actionable fail-fast message shared by both spawn paths (session-spawn +\n * session-restart-core) so they never drift. Names the wallet that couldn't\n * service the model AND the required route + api-key profile to re-spawn onto —\n * never a wallet the guard picked for the operator.\n */\nexport function modelWalletIneligibleMessage(opts: {\n prefix: string\n adapter: string\n model: string\n walletRoute: string\n walletMode?: \"subscription\" | \"api-key\"\n suggestedRoutes: string[]\n}): string {\n const wallet = opts.walletMode\n ? `\"${opts.walletRoute}\" ${opts.walletMode} wallet`\n : `\"${opts.walletRoute}\" wallet`\n const primary = opts.suggestedRoutes[0] ?? \"a gateway route\"\n const also =\n opts.suggestedRoutes.length > 1\n ? ` (also serviceable via ${opts.suggestedRoutes\n .slice(1)\n .map(r => `\"${r}\"`)\n .join(\", \")})`\n : \"\"\n return (\n `${opts.prefix}: model \"${opts.model}\" is not serviceable on the resolved ${wallet} ` +\n `(adapter \"${opts.adapter}\") and would 404 upstream. This model bills route \"${primary}\"${also} — ` +\n `re-spawn on it: set route.gateway=\"${primary}\" with an eligible \"${primary}\" api-key profile ` +\n `(access.profileRef). This guard only rejects; it never switches wallets for you.`\n )\n}\n"]}
|
package/dist/config.d.ts
CHANGED
|
@@ -106,6 +106,26 @@ interface DaemonConfig {
|
|
|
106
106
|
* `agentproto config set daemon.restartSweepIntervalMs <ms>`. Surfaced in
|
|
107
107
|
* `daemon_health` / `GET /health`. */
|
|
108
108
|
restartSweepIntervalMs?: number;
|
|
109
|
+
/** Turn-liveness watchdog threshold in ms (turn-liveness-watchdog
|
|
110
|
+
* chantier). Same shape as `crashDetectIntervalMs`: DEFAULT ON
|
|
111
|
+
* (non-destructive observability). The daemon periodically sweeps every
|
|
112
|
+
* BUSY agent-cli session and, for one that is mid-turn, NOT legitimately
|
|
113
|
+
* `blockedOn` a subagent/command, and has had no adapter activity
|
|
114
|
+
* (`lastActivityAt`) for longer than this threshold, stamps
|
|
115
|
+
* `stalledSinceMs` on the descriptor and emits `session:stalled` —
|
|
116
|
+
* surfacing a dead adapter stream (network drop, hung child — zero
|
|
117
|
+
* frames mid-turn) that would otherwise sit indistinguishable from
|
|
118
|
+
* healthy long work: `status:"running"`, `lastError:null`, no other
|
|
119
|
+
* signal. Conservative by design: a session legitimately blocked on a
|
|
120
|
+
* long tool call is NEVER a candidate, no matter how long it's silent —
|
|
121
|
+
* see `stall-watchdog.ts`'s docblock for why. Detects only; never kills
|
|
122
|
+
* or restarts (this is a later concern, if ever). Set to a non-positive
|
|
123
|
+
* value to disable the sweep entirely. Resolution order mirrors
|
|
124
|
+
* `idleReapAfterMs`: `AGENTPROTO_TURN_STALL_AFTER_MS` env > this field >
|
|
125
|
+
* the hardcoded default (5 min). Set via `agentproto config set
|
|
126
|
+
* daemon.turnStallAfterMs <ms>`. Surfaced in `daemon_health` /
|
|
127
|
+
* `GET /health`. */
|
|
128
|
+
turnStallAfterMs?: number;
|
|
109
129
|
}
|
|
110
130
|
interface TunnelConfig {
|
|
111
131
|
/** Cloud WS URL. When set + autoconnect=true, `agentproto serve`
|
package/dist/config.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/config.ts"],"names":["fs"],"mappings":"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","file":"config.mjs","sourcesContent":["/**\n * `~/.agentproto/config.json` — single hand-editable JSON for the\n * agentproto control plane's defaults. Sits alongside the existing\n * surface files (workspaces.json, credentials.json, sessions.json):\n *\n * workspaces.json which directories are workspaces + which is active\n * credentials.json tunnel host bearer tokens (mode 0600)\n * sessions.json last-known snapshot of the registry (informational)\n * config.json daemon defaults: port, bind, allowed origins,\n * tunnel host, feature toggles\n *\n * Resolution order for every daemon knob is:\n * 1. CLI flag (e.g. --port)\n * 2. Env var (where one exists, e.g. AGENTPROTO_TOKEN)\n * 3. config.json\n * 4. Hardcoded default\n *\n * This means a user can call `agentproto config set daemon.port 18791`\n * once and never re-pass `--port 18791` to `serve install` etc. CLI\n * flags still win for one-off overrides.\n *\n * Schema is intentionally narrow + extensible — unknown keys are\n * preserved on save (deep-merge), so a newer CLI writing a new\n * field won't drop one an older CLI doesn't know about. No secrets\n * here; credentials stay in credentials.json (mode 0600).\n */\n\nimport { promises as fs } from \"node:fs\"\nimport { homedir } from \"node:os\"\nimport { dirname, join } from \"node:path\"\nimport type { SpawnDefaultsConfig } from \"./spawn-defaults.js\"\n\nexport const CONFIG_VERSION = 1 as const\n\nexport interface DaemonConfig {\n /** Absolute path to the workspace the daemon binds to at boot. */\n workspace?: string\n /** HTTP port. Default 18790. */\n port?: number\n /** Bind addr. Default 127.0.0.1. */\n bind?: string\n /** Trusted browser origins for mutating /sessions/* routes (in\n * addition to the hardcoded localhost defaults). */\n allowedOrigins?: string[]\n /** When true, the daemon does NOT auto-trust localhost-on-any-port.\n * Only origins explicitly listed in `allowedOrigins` are allowed.\n * Pair with a curated list (e.g. `[\"http://localhost:3000\"]`) for\n * hardened setups. Default false. */\n strictOrigins?: boolean\n /** Server label sent in tunnel hello frames. */\n label?: string\n /** Bearer token gating the gateway at boot (`AuthOptions` with\n * `mode: \"bearer\"`). Unlike `remote_enable`'s ephemeral quick-tunnel\n * token, this one lives in config.json and survives daemon restarts.\n * Set via `agentproto config set daemon.authToken <token>` (e.g.\n * `$(openssl rand -hex 32)`). Unset ⇒ the gateway boots with\n * `mode: \"none\"` — fully open on loopback, same as today. */\n authToken?: string\n /** Opt-in eager resume-on-boot (session-survivability plan §5\n * \"Opt-in vs automatic\", PR-4). When true, a daemon restart doesn't just\n * leave agent-cli sessions dead-but-lazy-resumable — the boot pass eagerly\n * re-spawns the eligible ones (those that died with\n * `endedReason: \"daemon-restart\"` and still pass `canResume`) IN PLACE,\n * restoring liveness without waiting for a prompt. Lazy resume-on-prompt is\n * always on regardless of this flag (it costs nothing until someone acts);\n * this flag only controls the *automatic* boot-time pass. Default false: a\n * box-wide restart should not silently relaunch every adapter (cost, spawn\n * storm, credential re-resolution), and default-off also keeps two daemons\n * sharing the workspace buckets from racing to resume the same rows. Set via\n * `agentproto config set daemon.resumeSessionsOnBoot true`. Surfaced in\n * `daemon_health` / `GET /health`. */\n resumeSessionsOnBoot?: boolean\n /** Idle agent-session reaper (PR-6). When set to a positive number of\n * milliseconds, the daemon periodically retires agent-cli sessions that have\n * been idle (not busy, not awaiting input, `status:\"running\"`) longer than\n * this: it SIGTERMs the adapter child to free the process and flips the row\n * to `killed`/`endedReason:\"idle-reaped\"` — dead-but-lazy-resumable (a later\n * prompt revives it) and, critically, excluded from eager resume-on-boot\n * (#638 gates on `endedReason === \"daemon-restart\"`), so enabling this makes\n * eager resume safe: only genuinely-recent work survives a restart. Default\n * OFF (unset / 0 / negative ⇒ the reaper never runs). Resolution order\n * mirrors the module docblock: `AGENTPROTO_IDLE_REAP_AFTER_MS` env > this\n * field > off. Set via `agentproto config set daemon.idleReapAfterMs\n * <ms>`. Surfaced in `daemon_health` / `GET /health`. */\n idleReapAfterMs?: number\n /** Crash-detect sweep interval in ms (crash-detect PR-1). Unlike\n * `idleReapAfterMs`, this is DEFAULT ON (non-destructive observability):\n * the daemon periodically probes every live agent-cli session's OS\n * process (`process.kill(pid, 0)`) and, when it's provably gone with no\n * exit event ever emitted for it, flips the row to\n * `error`/`endedReason:\"crashed\"` with a `lastError` string — surfacing a\n * death that would otherwise sit silently as `status:\"running\"` until the\n * next prompt's RPC throws (or forever, for a parked session with no next\n * prompt). Detects only; never restarts or notifies (later PRs). Set to a\n * non-positive value to disable the sweep entirely. Resolution order\n * mirrors `idleReapAfterMs`: `AGENTPROTO_CRASH_DETECT_INTERVAL_MS` env >\n * this field > the hardcoded default (30s). Set via `agentproto config\n * set daemon.crashDetectIntervalMs <ms>`. Surfaced in `daemon_health` /\n * `GET /health`. */\n crashDetectIntervalMs?: number\n /** Restart-scheduler sweep interval in ms (restart-scheduler PR-2). OFF by\n * default (unset / 0 / negative ⇒ the sweep never runs), unlike\n * `crashDetectIntervalMs` — a positive value here only arms the periodic\n * sweep that EXECUTES an already-scheduled restart; it never opts a\n * session in by itself (that's the per-session `agent_start.restartPolicy`\n * field). Resolution order mirrors `idleReapAfterMs`:\n * `AGENTPROTO_RESTART_SWEEP_INTERVAL_MS` env > this field > off. Set via\n * `agentproto config set daemon.restartSweepIntervalMs <ms>`. Surfaced in\n * `daemon_health` / `GET /health`. */\n restartSweepIntervalMs?: number\n}\n\nexport interface TunnelConfig {\n /** Cloud WS URL. When set + autoconnect=true, `agentproto serve`\n * bootstraps with `--connect <host>`. */\n host?: string\n /** apt_ daemon token to present at the tunnel upgrade. When set,\n * `agentproto serve` uses this BEFORE falling back to\n * credentials.json — handy in profiles where the token-per-host\n * mapping in credentials.json doesn't fit (e.g. host = tunnel URL\n * but credentials were minted against the api URL). */\n token?: string\n /** Whether `agentproto daemon start` connects the tunnel by\n * default. v0 only — implementer can ignore until daemon needs it. */\n autoconnect?: boolean\n /**\n * Opt into end-to-end encryption of the outbound `serve --connect` tunnel\n * (design: tunnel-e2e/v1). When true, the daemon negotiates a\n * token-authenticated ephemeral handshake with the host and wraps the tunnel\n * frames in an AEAD box, so even the trusted host loses plaintext visibility.\n * The handshake authenticates both ends against the shared `tunnel.token`, so\n * `token` MUST also be set. Fully backward-compatible: if the host doesn't\n * advertise e2e (an older host), the daemon falls back to today's plaintext\n * tunnel. Unset/false ⇒ plaintext, byte-identical to today. */\n e2e?: boolean\n}\n\nexport interface FeaturesConfig {\n /** Hint that PTY is desired — informational; the daemon still\n * detects node-pty's presence at runtime. */\n pty?: boolean\n}\n\n/**\n * Policy for `agentproto worktree new` (PLAN.md §1.4 — config carries\n * policy, never state; git itself is the authority for which worktrees\n * exist). This is the fix for the sprawl the plan measured: 31 linked\n * worktrees across 6 different parent directories, because there was no\n * `worktree new` verb and therefore no convention to converge on.\n */\n/**\n * How the daemon isolates a freshly-spawned agent session into its own git\n * worktree (`agent_start.worktree`):\n * - `\"always\"` — every depth-0 spawn is provisioned into a worktree,\n * whether or not the caller asked. A cwd that is not in\n * a git repo has nothing to isolate, so it spawns plain.\n * - `\"on-request\"` — isolate ONLY when the caller passes `worktree`. This\n * is the default and the back-compatible behaviour:\n * today's callers pass nothing and spawn exactly where\n * they asked.\n * - `\"never\"` — isolation is off; an explicit `worktree` field is\n * REJECTED (loud, not silently ignored) so a caller\n * never believes it got an isolated tree it didn't.\n */\nexport type WorktreeIsolationMode = \"always\" | \"on-request\" | \"never\"\n\nexport interface WorktreesConfig {\n /**\n * Absolute path new worktrees are created under. Layout:\n * `<root>/<repoName>/<slug>`. Resolution order (mirrors every other\n * knob in this file, see the module docblock): `--root` flag >\n * `AGENTPROTO_WORKTREES_ROOT` env > this field > the hardcoded default\n * `~/.agentproto/worktrees`. The default is a real single root, not\n * \"unconfigured\" — `worktree new` converges to one place with zero\n * setup, which is the only way the sprawl actually stops (the 6 roots\n * that exist today are 6 people each inventing a default by hand).\n */\n root?: string\n /**\n * Policy for `agent_start.worktree` isolation. Resolution order mirrors\n * the module docblock (there is no CLI flag — this is a daemon-side\n * policy read at spawn, not a per-invocation flag):\n * `AGENTPROTO_WORKTREES_ISOLATION` env > this field > the hardcoded\n * default `\"on-request\"`. `\"on-request\"` is deliberately the default:\n * any other would break back-compat by isolating callers that never\n * asked (see `worktree-isolation.ts`).\n */\n isolation?: WorktreeIsolationMode\n}\n\n/**\n * Policy for `agent_start.attach` — whether a spawn AUTO-attaches to its\n * calling session as parent lineage. `\"always\"` (the default) nests a\n * spawned child under the session that spawned it whenever that identity is\n * derivable (the trusted `?callerSessionId=` on the daemon self-ref URL, or\n * an explicit `parentSessionId` hint), so a supervisor's executors stop\n * landing as depth-0 orphans. `\"on-request\"` reverts to the pre-attach\n * behaviour: auto-attribution is off, and a child nests only when the caller\n * explicitly opts in (`attach: true` / `attach: { parent }` / an explicit\n * `parentSessionId`). Either way a per-call `attach: false` forces an\n * independent root, exactly as `worktree: false` opts out of isolation. See\n * `spawn-attach.ts`.\n */\nexport type SpawnAttachMode = \"always\" | \"on-request\"\n\n/**\n * Policy for `agent_start`'s IMPLICIT dedupe — what happens when a caller\n * spawns with NO `idempotencyKey` at all (see `spawn-dedupe.ts`):\n * - `\"always\"` — the daemon DERIVES an implicit key from the spawn's\n * `label` (required — see below) plus a hash of the\n * initial `prompt`, and dedupes a same-adapter/cwd\n * repeat against it exactly as an explicit key would.\n * This is the default, mirroring `attach`'s own\n * \"opt-in-only guard is not a guard\" precedent above.\n * Deriving needs a `label` to produce anything at all\n * — an unlabelled spawn is untouched, which is what\n * keeps this safe for the fan-out pattern this repo\n * exercises (several agents into one cwd with no\n * shared label): see `spawn-dedupe.ts`'s docblock for\n * the full false-dedup analysis, and PR #803's own\n * no-opt-in label+cwd warning backstop in\n * `session-spawn.ts`, which independently landed on\n * the same label-is-the-signal boundary.\n * - `\"on-request\"` — no implicit derivation; only an explicit\n * `idempotencyKey` dedupes (today's behaviour,\n * unchanged). A per-call `dedupe: true` still opts in\n * under this policy, mirroring `attach: true`.\n * Either way a per-call `dedupe: false` disables implicit derivation for\n * that one spawn regardless of policy — the escape hatch, mirroring\n * `attach: false` / `worktree: false`. An explicit `idempotencyKey` always\n * wins over a derived one (derivation is only attempted when the caller\n * supplied none).\n */\nexport type SpawnDedupeMode = \"always\" | \"on-request\"\n\nexport interface SpawnConfig {\n /**\n * Attach policy for `agent_start`. Resolution order mirrors the module\n * docblock (no CLI flag — a daemon-side policy read at spawn):\n * `AGENTPROTO_SPAWN_ATTACH` env > this field > the hardcoded default\n * `\"always\"`. Unlike `worktrees.isolation` (whose default preserves\n * back-compat by NOT isolating), attach defaults ON: an orphaned executor\n * is a bug, not a feature, and the auto-parent is descriptor-only lineage\n * that never relaxes a privilege gate.\n */\n attach?: SpawnAttachMode\n /**\n * Implicit-dedupe policy for `agent_start`. Resolution order mirrors\n * `attach` above (no CLI flag — a daemon-side policy read at spawn):\n * `AGENTPROTO_SPAWN_DEDUPE` env > this field > the hardcoded default\n * `\"always\"`. See {@link SpawnDedupeMode} and `spawn-dedupe.ts` for the\n * full reasoning: a retry-safety guard that only works when a caller\n * remembers to ask for it (`idempotencyKey`) is not a guard — the same\n * argument `attach` already settled for parent lineage.\n */\n dedupe?: SpawnDedupeMode\n}\n\nexport interface PairingConfig {\n /** Rendezvous broker WS URL (ws:// or wss://) used by `pair offer` and by\n * autoconnect on boot. When unset, `pair offer` requires an explicit\n * `--rendezvous`. Mirrors `tunnel.host`. */\n rendezvous?: string\n /** Whether the daemon opens standing rendezvous connections for every\n * persisted pairing on boot (so a paired client can reconnect anytime).\n * Mirrors `tunnel.autoconnect`. Default true when a rendezvous is set. */\n autoconnect?: boolean\n}\n\n/**\n * A user-defined generic ACP agent — the config-file half of\n * `AcpAgentSpec` (the slug is the record key in `acpAgents`, so it's\n * omitted here). Any CLI that already speaks the Agent Client Protocol\n * can be wired with zero code by declaring one of these under\n * `acpAgents.<slug>` in `~/.agentproto/config.json`; the CLI's\n * `acpHandleFromSpec` mints a runnable `AgentCliHandle` from it at\n * resolve time (see `packages/cli/src/registry/acp-generic.ts`). Kept\n * in this package (not the CLI's) so `config.ts` stays the single\n * source of truth for the config surface without a cli→runtime→cli\n * import cycle — the CLI's `AcpAgentSpec` extends this shape.\n */\nexport interface AcpAgentConfigEntry {\n /** Display name. Defaults to the slug when omitted. */\n name?: string\n /** One-line description surfaced in `agentproto acp ls`. */\n description?: string\n /** Executable to spawn, e.g. \"gemini\". */\n bin: string\n /** Extra argv appended after `bin`, e.g. [\"--experimental-acp\"]. */\n bin_args?: string[]\n /** Extra environment variables for the spawned process. */\n env?: Record<string, string>\n /** Flag the CLI uses to receive the working directory, if it needs\n * one passed explicitly (most ACP agents take cwd over the wire). */\n cwd_flag?: string\n /** When true, advertise resumable + native-resume continuation. */\n resumable?: boolean\n /** Known model ids for the agent (informational + validation hints). */\n models?: { default?: string; allowed?: string[] }\n /** Shown when `bin` is missing from PATH (how to install the CLI). */\n install_hint?: string\n}\n\n/**\n * Per-environment connection bundle. A profile overrides specific\n * fields of the top-level `daemon` / `tunnel` / `features` blocks\n * when selected via `--profile <name>` (or the top-level\n * `activeProfile` setting). Missing fields fall through to the\n * top-level config, so a profile only needs to declare what's\n * different — typically just `tunnel.host` + `tunnel.token`.\n *\n * Example:\n * {\n * \"daemon\": { \"workspace\": \"/code\", \"port\": 18790 },\n * \"activeProfile\": \"local\",\n * \"profiles\": {\n * \"local\": { \"tunnel\": { \"host\": \"ws://localhost:3200/connect\",\n * \"token\": \"apt_local\", \"autoconnect\": true } },\n * \"prod\": { \"tunnel\": { \"host\": \"wss://tunnel.guilde.work/connect\",\n * \"token\": \"apt_prod\", \"autoconnect\": true },\n * \"daemon\": { \"port\": 18791 } }\n * }\n * }\n *\n * Sandbox daemons generate per-sandbox profile entries at provision\n * time so the daemon inside the sandbox boots with\n * `agentproto serve --profile sandbox-<id>` and no extra plumbing.\n */\nexport interface ProfileConfig {\n daemon?: DaemonConfig\n tunnel?: TunnelConfig\n features?: FeaturesConfig\n}\n\n/**\n * A user-defined named terminal/TUI preset stored in\n * `~/.agentproto/config.json` under `terminalPresets`. Presets keep\n * local launch recipes (argv, env, cwd, name/label) out of shared\n * adapter manifests — e.g. pointing a Claude Code TUI at a local\n * LLM gateway without retyping proxy env vars every spawn.\n */\nexport interface TerminalPreset {\n /** Command + args to spawn. When provided, `sessions terminal` can\n * be used without `-- <argv...>`. */\n argv?: string[]\n /** Extra environment variables layered on top of the daemon's\n * inherited process.env. Values MUST be strings. */\n env?: Record<string, string>\n /** Working directory for the PTY session. Relative paths are\n * resolved against the current working directory at CLI time. */\n cwd?: string\n /** Workspace slug used for cwd fallback when `cwd` is omitted. */\n workspace?: string\n /** Stable session name passed to the registry (`name` field). */\n name?: string\n /** Human-readable label surfaced in session listings. */\n label?: string\n}\n\nexport interface AgentprotoConfig {\n version?: number\n daemon?: DaemonConfig\n tunnel?: TunnelConfig\n features?: FeaturesConfig\n /** E2E daemon-pairing defaults (rendezvous URL + autoconnect). */\n pairing?: PairingConfig\n /** Where `agentproto worktree new` creates worktrees. See\n * {@link WorktreesConfig}. */\n worktrees?: WorktreesConfig\n /** Spawn-time policy (`agent_start`). See {@link SpawnConfig}. */\n spawn?: SpawnConfig\n /** Named connection profiles. See `ProfileConfig` for the merge\n * semantics — a profile's fields shallow-override the top-level\n * defaults for the selected run. */\n profiles?: Record<string, ProfileConfig>\n /** Profile name to use when `--profile` isn't passed. When unset,\n * the top-level `daemon` / `tunnel` blocks are used directly. */\n activeProfile?: string\n /** Default `skills` + `options` auto-applied to every `agent_start`\n * spawn — global and per-adapter. See `resolveSpawnDefaults` in\n * `spawn-defaults.ts` for the merge precedence with an explicit call.\n * Absent ⇒ current behaviour exactly (no regression). */\n defaults?: SpawnDefaultsConfig\n /** User-defined generic ACP agents, keyed by adapter slug. Each entry\n * is minted into a runnable handle by the CLI's `acpHandleFromSpec`\n * when `resolveAdapter(slug)` finds no npm adapter package. User\n * entries shadow the curated `ACP_CATALOG` on slug collision. */\n acpAgents?: Record<string, AcpAgentConfigEntry>\n /** User-defined named terminal/TUI presets. Local-only; never\n * packaged in shared adapter manifests or defaults. */\n terminalPresets?: Record<string, TerminalPreset>\n /** Unknown keys preserved across save round-trips. */\n [unknown: string]: unknown\n}\n\nexport const CONFIG_FILE_PATH = (): string =>\n join(homedir(), \".agentproto\", \"config.json\")\n\n/**\n * Drop any `acpAgents` entries that aren't a shape we can turn into a\n * handle. The one hard requirement is a non-empty string `bin` (the\n * executable to spawn); everything else is optional and defaulted\n * downstream. Invalid entries are removed rather than throwing so the\n * daemon still boots — one warning names the offending slug so the\n * user can fix their config. Returns `undefined` when nothing valid\n * remains, keeping the key absent (== \"no generic agents\").\n */\nfunction sanitizeAcpAgents(\n raw: unknown,\n target: string,\n): Record<string, AcpAgentConfigEntry> | undefined {\n if (!raw || typeof raw !== \"object\" || Array.isArray(raw)) {\n console.warn(\n `[runtime/config] ${target}: 'acpAgents' is not an object — ignoring`,\n )\n return undefined\n }\n const out: Record<string, AcpAgentConfigEntry> = {}\n for (const [slug, value] of Object.entries(raw as Record<string, unknown>)) {\n if (\n value &&\n typeof value === \"object\" &&\n !Array.isArray(value) &&\n typeof (value as { bin?: unknown }).bin === \"string\" &&\n (value as { bin: string }).bin.length > 0\n ) {\n out[slug] = value as AcpAgentConfigEntry\n } else {\n console.warn(\n `[runtime/config] ${target}: acpAgents.${slug} is missing a string 'bin' — ignoring`,\n )\n }\n }\n return Object.keys(out).length > 0 ? out : undefined\n}\n\n/**\n * Load config.json. Returns an empty object (NOT null) when the file\n * is missing, malformed, or unreadable — callers can `cfg.daemon?.port`\n * safely without null-guards. Errors during a malformed-read are\n * logged once so the user notices the file is broken without the\n * daemon refusing to boot.\n */\nexport async function loadConfig(path?: string): Promise<AgentprotoConfig> {\n const target = path ?? CONFIG_FILE_PATH()\n try {\n const raw = await fs.readFile(target, \"utf8\")\n const parsed = JSON.parse(raw) as unknown\n if (parsed && typeof parsed === \"object\" && !Array.isArray(parsed)) {\n const cfg = parsed as AgentprotoConfig\n // Sanitize `acpAgents` in the same tolerant spirit as the rest of\n // this loader: a malformed entry is dropped (with one warning) so a\n // single bad hand-edit can't make every generic ACP agent\n // unresolvable. Full AIP-45 validation happens later, at\n // `acpHandleFromSpec` time, with precise field-level messages.\n if (cfg.acpAgents !== undefined) {\n cfg.acpAgents = sanitizeAcpAgents(cfg.acpAgents, target)\n }\n return cfg\n }\n console.warn(\n `[runtime/config] ${target}: top-level value is not an object — ignoring`,\n )\n return {}\n } catch (err) {\n // ENOENT is the common case; only warn on other shapes.\n const code = (err as NodeJS.ErrnoException).code\n if (code && code !== \"ENOENT\") {\n console.warn(\n `[runtime/config] failed to read ${target}: ${\n err instanceof Error ? err.message : String(err)\n }`,\n )\n }\n return {}\n }\n}\n\n/**\n * Write config.json atomically (tmp + rename) so a concurrent\n * `agentproto config edit` can't half-truncate the file. Writes\n * `next` AS-IS — callers are expected to pass the full desired\n * state (loaded the existing config, mutated, passed it back).\n *\n * Earlier versions deep-merged with the on-disk file, but that made\n * deletions impossible: `setConfigKey(cfg, \"x\", undefined)` would\n * remove the key from memory, then the deep-merge would silently\n * re-add it from disk. The current design trusts the caller's\n * snapshot and uses atomic rename for crash safety.\n */\nexport async function saveConfig(\n next: AgentprotoConfig,\n path?: string,\n): Promise<void> {\n const target = path ?? CONFIG_FILE_PATH()\n const payload = { ...next, version: CONFIG_VERSION }\n const dir = dirname(target)\n await fs.mkdir(dir, { recursive: true })\n const tmp = `${target}.tmp`\n await fs.writeFile(tmp, JSON.stringify(payload, null, 2) + \"\\n\", \"utf8\")\n await fs.rename(tmp, target)\n}\n\n/**\n * Read a dot-notation key (`daemon.port`) out of a config. Returns\n * `undefined` when any segment is missing.\n */\nexport function getConfigKey(\n cfg: AgentprotoConfig,\n dotted: string,\n): unknown {\n let cur: unknown = cfg\n for (const part of dotted.split(\".\")) {\n if (cur == null || typeof cur !== \"object\") return undefined\n cur = (cur as Record<string, unknown>)[part]\n }\n return cur\n}\n\n/**\n * Set a dot-notation key in a config. Returns a new object — does\n * NOT mutate. Creates intermediate objects as needed. Setting\n * `value: undefined` is treated as a delete.\n */\nexport function setConfigKey(\n cfg: AgentprotoConfig,\n dotted: string,\n value: unknown,\n): AgentprotoConfig {\n const parts = dotted.split(\".\")\n const out: AgentprotoConfig = { ...cfg }\n let cur: Record<string, unknown> = out as Record<string, unknown>\n for (let i = 0; i < parts.length - 1; i++) {\n const k = parts[i]!\n const next = cur[k]\n if (next && typeof next === \"object\" && !Array.isArray(next)) {\n cur[k] = { ...(next as Record<string, unknown>) }\n } else {\n cur[k] = {}\n }\n cur = cur[k] as Record<string, unknown>\n }\n const leaf = parts[parts.length - 1]!\n if (value === undefined) {\n delete cur[leaf]\n } else {\n cur[leaf] = value\n }\n return out\n}\n\n/**\n * Deep merge — objects are recursively combined, everything else\n * (arrays, primitives) is replaced wholesale by `b`. Mirrors what\n * `Object.assign({}, a, b)` does for shallow keys.\n */\nfunction deepMerge<A extends Record<string, unknown>, B extends Record<string, unknown>>(\n a: A,\n b: B,\n): A & B {\n const out: Record<string, unknown> = { ...a }\n for (const [k, v] of Object.entries(b)) {\n const cur = out[k]\n if (\n v &&\n typeof v === \"object\" &&\n !Array.isArray(v) &&\n cur &&\n typeof cur === \"object\" &&\n !Array.isArray(cur)\n ) {\n out[k] = deepMerge(\n cur as Record<string, unknown>,\n v as Record<string, unknown>,\n )\n } else {\n out[k] = v\n }\n }\n return out as A & B\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/config.ts"],"names":["fs"],"mappings":"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","file":"config.mjs","sourcesContent":["/**\n * `~/.agentproto/config.json` — single hand-editable JSON for the\n * agentproto control plane's defaults. Sits alongside the existing\n * surface files (workspaces.json, credentials.json, sessions.json):\n *\n * workspaces.json which directories are workspaces + which is active\n * credentials.json tunnel host bearer tokens (mode 0600)\n * sessions.json last-known snapshot of the registry (informational)\n * config.json daemon defaults: port, bind, allowed origins,\n * tunnel host, feature toggles\n *\n * Resolution order for every daemon knob is:\n * 1. CLI flag (e.g. --port)\n * 2. Env var (where one exists, e.g. AGENTPROTO_TOKEN)\n * 3. config.json\n * 4. Hardcoded default\n *\n * This means a user can call `agentproto config set daemon.port 18791`\n * once and never re-pass `--port 18791` to `serve install` etc. CLI\n * flags still win for one-off overrides.\n *\n * Schema is intentionally narrow + extensible — unknown keys are\n * preserved on save (deep-merge), so a newer CLI writing a new\n * field won't drop one an older CLI doesn't know about. No secrets\n * here; credentials stay in credentials.json (mode 0600).\n */\n\nimport { promises as fs } from \"node:fs\"\nimport { homedir } from \"node:os\"\nimport { dirname, join } from \"node:path\"\nimport type { SpawnDefaultsConfig } from \"./spawn-defaults.js\"\n\nexport const CONFIG_VERSION = 1 as const\n\nexport interface DaemonConfig {\n /** Absolute path to the workspace the daemon binds to at boot. */\n workspace?: string\n /** HTTP port. Default 18790. */\n port?: number\n /** Bind addr. Default 127.0.0.1. */\n bind?: string\n /** Trusted browser origins for mutating /sessions/* routes (in\n * addition to the hardcoded localhost defaults). */\n allowedOrigins?: string[]\n /** When true, the daemon does NOT auto-trust localhost-on-any-port.\n * Only origins explicitly listed in `allowedOrigins` are allowed.\n * Pair with a curated list (e.g. `[\"http://localhost:3000\"]`) for\n * hardened setups. Default false. */\n strictOrigins?: boolean\n /** Server label sent in tunnel hello frames. */\n label?: string\n /** Bearer token gating the gateway at boot (`AuthOptions` with\n * `mode: \"bearer\"`). Unlike `remote_enable`'s ephemeral quick-tunnel\n * token, this one lives in config.json and survives daemon restarts.\n * Set via `agentproto config set daemon.authToken <token>` (e.g.\n * `$(openssl rand -hex 32)`). Unset ⇒ the gateway boots with\n * `mode: \"none\"` — fully open on loopback, same as today. */\n authToken?: string\n /** Opt-in eager resume-on-boot (session-survivability plan §5\n * \"Opt-in vs automatic\", PR-4). When true, a daemon restart doesn't just\n * leave agent-cli sessions dead-but-lazy-resumable — the boot pass eagerly\n * re-spawns the eligible ones (those that died with\n * `endedReason: \"daemon-restart\"` and still pass `canResume`) IN PLACE,\n * restoring liveness without waiting for a prompt. Lazy resume-on-prompt is\n * always on regardless of this flag (it costs nothing until someone acts);\n * this flag only controls the *automatic* boot-time pass. Default false: a\n * box-wide restart should not silently relaunch every adapter (cost, spawn\n * storm, credential re-resolution), and default-off also keeps two daemons\n * sharing the workspace buckets from racing to resume the same rows. Set via\n * `agentproto config set daemon.resumeSessionsOnBoot true`. Surfaced in\n * `daemon_health` / `GET /health`. */\n resumeSessionsOnBoot?: boolean\n /** Idle agent-session reaper (PR-6). When set to a positive number of\n * milliseconds, the daemon periodically retires agent-cli sessions that have\n * been idle (not busy, not awaiting input, `status:\"running\"`) longer than\n * this: it SIGTERMs the adapter child to free the process and flips the row\n * to `killed`/`endedReason:\"idle-reaped\"` — dead-but-lazy-resumable (a later\n * prompt revives it) and, critically, excluded from eager resume-on-boot\n * (#638 gates on `endedReason === \"daemon-restart\"`), so enabling this makes\n * eager resume safe: only genuinely-recent work survives a restart. Default\n * OFF (unset / 0 / negative ⇒ the reaper never runs). Resolution order\n * mirrors the module docblock: `AGENTPROTO_IDLE_REAP_AFTER_MS` env > this\n * field > off. Set via `agentproto config set daemon.idleReapAfterMs\n * <ms>`. Surfaced in `daemon_health` / `GET /health`. */\n idleReapAfterMs?: number\n /** Crash-detect sweep interval in ms (crash-detect PR-1). Unlike\n * `idleReapAfterMs`, this is DEFAULT ON (non-destructive observability):\n * the daemon periodically probes every live agent-cli session's OS\n * process (`process.kill(pid, 0)`) and, when it's provably gone with no\n * exit event ever emitted for it, flips the row to\n * `error`/`endedReason:\"crashed\"` with a `lastError` string — surfacing a\n * death that would otherwise sit silently as `status:\"running\"` until the\n * next prompt's RPC throws (or forever, for a parked session with no next\n * prompt). Detects only; never restarts or notifies (later PRs). Set to a\n * non-positive value to disable the sweep entirely. Resolution order\n * mirrors `idleReapAfterMs`: `AGENTPROTO_CRASH_DETECT_INTERVAL_MS` env >\n * this field > the hardcoded default (30s). Set via `agentproto config\n * set daemon.crashDetectIntervalMs <ms>`. Surfaced in `daemon_health` /\n * `GET /health`. */\n crashDetectIntervalMs?: number\n /** Restart-scheduler sweep interval in ms (restart-scheduler PR-2). OFF by\n * default (unset / 0 / negative ⇒ the sweep never runs), unlike\n * `crashDetectIntervalMs` — a positive value here only arms the periodic\n * sweep that EXECUTES an already-scheduled restart; it never opts a\n * session in by itself (that's the per-session `agent_start.restartPolicy`\n * field). Resolution order mirrors `idleReapAfterMs`:\n * `AGENTPROTO_RESTART_SWEEP_INTERVAL_MS` env > this field > off. Set via\n * `agentproto config set daemon.restartSweepIntervalMs <ms>`. Surfaced in\n * `daemon_health` / `GET /health`. */\n restartSweepIntervalMs?: number\n /** Turn-liveness watchdog threshold in ms (turn-liveness-watchdog\n * chantier). Same shape as `crashDetectIntervalMs`: DEFAULT ON\n * (non-destructive observability). The daemon periodically sweeps every\n * BUSY agent-cli session and, for one that is mid-turn, NOT legitimately\n * `blockedOn` a subagent/command, and has had no adapter activity\n * (`lastActivityAt`) for longer than this threshold, stamps\n * `stalledSinceMs` on the descriptor and emits `session:stalled` —\n * surfacing a dead adapter stream (network drop, hung child — zero\n * frames mid-turn) that would otherwise sit indistinguishable from\n * healthy long work: `status:\"running\"`, `lastError:null`, no other\n * signal. Conservative by design: a session legitimately blocked on a\n * long tool call is NEVER a candidate, no matter how long it's silent —\n * see `stall-watchdog.ts`'s docblock for why. Detects only; never kills\n * or restarts (this is a later concern, if ever). Set to a non-positive\n * value to disable the sweep entirely. Resolution order mirrors\n * `idleReapAfterMs`: `AGENTPROTO_TURN_STALL_AFTER_MS` env > this field >\n * the hardcoded default (5 min). Set via `agentproto config set\n * daemon.turnStallAfterMs <ms>`. Surfaced in `daemon_health` /\n * `GET /health`. */\n turnStallAfterMs?: number\n}\n\nexport interface TunnelConfig {\n /** Cloud WS URL. When set + autoconnect=true, `agentproto serve`\n * bootstraps with `--connect <host>`. */\n host?: string\n /** apt_ daemon token to present at the tunnel upgrade. When set,\n * `agentproto serve` uses this BEFORE falling back to\n * credentials.json — handy in profiles where the token-per-host\n * mapping in credentials.json doesn't fit (e.g. host = tunnel URL\n * but credentials were minted against the api URL). */\n token?: string\n /** Whether `agentproto daemon start` connects the tunnel by\n * default. v0 only — implementer can ignore until daemon needs it. */\n autoconnect?: boolean\n /**\n * Opt into end-to-end encryption of the outbound `serve --connect` tunnel\n * (design: tunnel-e2e/v1). When true, the daemon negotiates a\n * token-authenticated ephemeral handshake with the host and wraps the tunnel\n * frames in an AEAD box, so even the trusted host loses plaintext visibility.\n * The handshake authenticates both ends against the shared `tunnel.token`, so\n * `token` MUST also be set. Fully backward-compatible: if the host doesn't\n * advertise e2e (an older host), the daemon falls back to today's plaintext\n * tunnel. Unset/false ⇒ plaintext, byte-identical to today. */\n e2e?: boolean\n}\n\nexport interface FeaturesConfig {\n /** Hint that PTY is desired — informational; the daemon still\n * detects node-pty's presence at runtime. */\n pty?: boolean\n}\n\n/**\n * Policy for `agentproto worktree new` (PLAN.md §1.4 — config carries\n * policy, never state; git itself is the authority for which worktrees\n * exist). This is the fix for the sprawl the plan measured: 31 linked\n * worktrees across 6 different parent directories, because there was no\n * `worktree new` verb and therefore no convention to converge on.\n */\n/**\n * How the daemon isolates a freshly-spawned agent session into its own git\n * worktree (`agent_start.worktree`):\n * - `\"always\"` — every depth-0 spawn is provisioned into a worktree,\n * whether or not the caller asked. A cwd that is not in\n * a git repo has nothing to isolate, so it spawns plain.\n * - `\"on-request\"` — isolate ONLY when the caller passes `worktree`. This\n * is the default and the back-compatible behaviour:\n * today's callers pass nothing and spawn exactly where\n * they asked.\n * - `\"never\"` — isolation is off; an explicit `worktree` field is\n * REJECTED (loud, not silently ignored) so a caller\n * never believes it got an isolated tree it didn't.\n */\nexport type WorktreeIsolationMode = \"always\" | \"on-request\" | \"never\"\n\nexport interface WorktreesConfig {\n /**\n * Absolute path new worktrees are created under. Layout:\n * `<root>/<repoName>/<slug>`. Resolution order (mirrors every other\n * knob in this file, see the module docblock): `--root` flag >\n * `AGENTPROTO_WORKTREES_ROOT` env > this field > the hardcoded default\n * `~/.agentproto/worktrees`. The default is a real single root, not\n * \"unconfigured\" — `worktree new` converges to one place with zero\n * setup, which is the only way the sprawl actually stops (the 6 roots\n * that exist today are 6 people each inventing a default by hand).\n */\n root?: string\n /**\n * Policy for `agent_start.worktree` isolation. Resolution order mirrors\n * the module docblock (there is no CLI flag — this is a daemon-side\n * policy read at spawn, not a per-invocation flag):\n * `AGENTPROTO_WORKTREES_ISOLATION` env > this field > the hardcoded\n * default `\"on-request\"`. `\"on-request\"` is deliberately the default:\n * any other would break back-compat by isolating callers that never\n * asked (see `worktree-isolation.ts`).\n */\n isolation?: WorktreeIsolationMode\n}\n\n/**\n * Policy for `agent_start.attach` — whether a spawn AUTO-attaches to its\n * calling session as parent lineage. `\"always\"` (the default) nests a\n * spawned child under the session that spawned it whenever that identity is\n * derivable (the trusted `?callerSessionId=` on the daemon self-ref URL, or\n * an explicit `parentSessionId` hint), so a supervisor's executors stop\n * landing as depth-0 orphans. `\"on-request\"` reverts to the pre-attach\n * behaviour: auto-attribution is off, and a child nests only when the caller\n * explicitly opts in (`attach: true` / `attach: { parent }` / an explicit\n * `parentSessionId`). Either way a per-call `attach: false` forces an\n * independent root, exactly as `worktree: false` opts out of isolation. See\n * `spawn-attach.ts`.\n */\nexport type SpawnAttachMode = \"always\" | \"on-request\"\n\n/**\n * Policy for `agent_start`'s IMPLICIT dedupe — what happens when a caller\n * spawns with NO `idempotencyKey` at all (see `spawn-dedupe.ts`):\n * - `\"always\"` — the daemon DERIVES an implicit key from the spawn's\n * `label` (required — see below) plus a hash of the\n * initial `prompt`, and dedupes a same-adapter/cwd\n * repeat against it exactly as an explicit key would.\n * This is the default, mirroring `attach`'s own\n * \"opt-in-only guard is not a guard\" precedent above.\n * Deriving needs a `label` to produce anything at all\n * — an unlabelled spawn is untouched, which is what\n * keeps this safe for the fan-out pattern this repo\n * exercises (several agents into one cwd with no\n * shared label): see `spawn-dedupe.ts`'s docblock for\n * the full false-dedup analysis, and PR #803's own\n * no-opt-in label+cwd warning backstop in\n * `session-spawn.ts`, which independently landed on\n * the same label-is-the-signal boundary.\n * - `\"on-request\"` — no implicit derivation; only an explicit\n * `idempotencyKey` dedupes (today's behaviour,\n * unchanged). A per-call `dedupe: true` still opts in\n * under this policy, mirroring `attach: true`.\n * Either way a per-call `dedupe: false` disables implicit derivation for\n * that one spawn regardless of policy — the escape hatch, mirroring\n * `attach: false` / `worktree: false`. An explicit `idempotencyKey` always\n * wins over a derived one (derivation is only attempted when the caller\n * supplied none).\n */\nexport type SpawnDedupeMode = \"always\" | \"on-request\"\n\nexport interface SpawnConfig {\n /**\n * Attach policy for `agent_start`. Resolution order mirrors the module\n * docblock (no CLI flag — a daemon-side policy read at spawn):\n * `AGENTPROTO_SPAWN_ATTACH` env > this field > the hardcoded default\n * `\"always\"`. Unlike `worktrees.isolation` (whose default preserves\n * back-compat by NOT isolating), attach defaults ON: an orphaned executor\n * is a bug, not a feature, and the auto-parent is descriptor-only lineage\n * that never relaxes a privilege gate.\n */\n attach?: SpawnAttachMode\n /**\n * Implicit-dedupe policy for `agent_start`. Resolution order mirrors\n * `attach` above (no CLI flag — a daemon-side policy read at spawn):\n * `AGENTPROTO_SPAWN_DEDUPE` env > this field > the hardcoded default\n * `\"always\"`. See {@link SpawnDedupeMode} and `spawn-dedupe.ts` for the\n * full reasoning: a retry-safety guard that only works when a caller\n * remembers to ask for it (`idempotencyKey`) is not a guard — the same\n * argument `attach` already settled for parent lineage.\n */\n dedupe?: SpawnDedupeMode\n}\n\nexport interface PairingConfig {\n /** Rendezvous broker WS URL (ws:// or wss://) used by `pair offer` and by\n * autoconnect on boot. When unset, `pair offer` requires an explicit\n * `--rendezvous`. Mirrors `tunnel.host`. */\n rendezvous?: string\n /** Whether the daemon opens standing rendezvous connections for every\n * persisted pairing on boot (so a paired client can reconnect anytime).\n * Mirrors `tunnel.autoconnect`. Default true when a rendezvous is set. */\n autoconnect?: boolean\n}\n\n/**\n * A user-defined generic ACP agent — the config-file half of\n * `AcpAgentSpec` (the slug is the record key in `acpAgents`, so it's\n * omitted here). Any CLI that already speaks the Agent Client Protocol\n * can be wired with zero code by declaring one of these under\n * `acpAgents.<slug>` in `~/.agentproto/config.json`; the CLI's\n * `acpHandleFromSpec` mints a runnable `AgentCliHandle` from it at\n * resolve time (see `packages/cli/src/registry/acp-generic.ts`). Kept\n * in this package (not the CLI's) so `config.ts` stays the single\n * source of truth for the config surface without a cli→runtime→cli\n * import cycle — the CLI's `AcpAgentSpec` extends this shape.\n */\nexport interface AcpAgentConfigEntry {\n /** Display name. Defaults to the slug when omitted. */\n name?: string\n /** One-line description surfaced in `agentproto acp ls`. */\n description?: string\n /** Executable to spawn, e.g. \"gemini\". */\n bin: string\n /** Extra argv appended after `bin`, e.g. [\"--experimental-acp\"]. */\n bin_args?: string[]\n /** Extra environment variables for the spawned process. */\n env?: Record<string, string>\n /** Flag the CLI uses to receive the working directory, if it needs\n * one passed explicitly (most ACP agents take cwd over the wire). */\n cwd_flag?: string\n /** When true, advertise resumable + native-resume continuation. */\n resumable?: boolean\n /** Known model ids for the agent (informational + validation hints). */\n models?: { default?: string; allowed?: string[] }\n /** Shown when `bin` is missing from PATH (how to install the CLI). */\n install_hint?: string\n}\n\n/**\n * Per-environment connection bundle. A profile overrides specific\n * fields of the top-level `daemon` / `tunnel` / `features` blocks\n * when selected via `--profile <name>` (or the top-level\n * `activeProfile` setting). Missing fields fall through to the\n * top-level config, so a profile only needs to declare what's\n * different — typically just `tunnel.host` + `tunnel.token`.\n *\n * Example:\n * {\n * \"daemon\": { \"workspace\": \"/code\", \"port\": 18790 },\n * \"activeProfile\": \"local\",\n * \"profiles\": {\n * \"local\": { \"tunnel\": { \"host\": \"ws://localhost:3200/connect\",\n * \"token\": \"apt_local\", \"autoconnect\": true } },\n * \"prod\": { \"tunnel\": { \"host\": \"wss://tunnel.guilde.work/connect\",\n * \"token\": \"apt_prod\", \"autoconnect\": true },\n * \"daemon\": { \"port\": 18791 } }\n * }\n * }\n *\n * Sandbox daemons generate per-sandbox profile entries at provision\n * time so the daemon inside the sandbox boots with\n * `agentproto serve --profile sandbox-<id>` and no extra plumbing.\n */\nexport interface ProfileConfig {\n daemon?: DaemonConfig\n tunnel?: TunnelConfig\n features?: FeaturesConfig\n}\n\n/**\n * A user-defined named terminal/TUI preset stored in\n * `~/.agentproto/config.json` under `terminalPresets`. Presets keep\n * local launch recipes (argv, env, cwd, name/label) out of shared\n * adapter manifests — e.g. pointing a Claude Code TUI at a local\n * LLM gateway without retyping proxy env vars every spawn.\n */\nexport interface TerminalPreset {\n /** Command + args to spawn. When provided, `sessions terminal` can\n * be used without `-- <argv...>`. */\n argv?: string[]\n /** Extra environment variables layered on top of the daemon's\n * inherited process.env. Values MUST be strings. */\n env?: Record<string, string>\n /** Working directory for the PTY session. Relative paths are\n * resolved against the current working directory at CLI time. */\n cwd?: string\n /** Workspace slug used for cwd fallback when `cwd` is omitted. */\n workspace?: string\n /** Stable session name passed to the registry (`name` field). */\n name?: string\n /** Human-readable label surfaced in session listings. */\n label?: string\n}\n\nexport interface AgentprotoConfig {\n version?: number\n daemon?: DaemonConfig\n tunnel?: TunnelConfig\n features?: FeaturesConfig\n /** E2E daemon-pairing defaults (rendezvous URL + autoconnect). */\n pairing?: PairingConfig\n /** Where `agentproto worktree new` creates worktrees. See\n * {@link WorktreesConfig}. */\n worktrees?: WorktreesConfig\n /** Spawn-time policy (`agent_start`). See {@link SpawnConfig}. */\n spawn?: SpawnConfig\n /** Named connection profiles. See `ProfileConfig` for the merge\n * semantics — a profile's fields shallow-override the top-level\n * defaults for the selected run. */\n profiles?: Record<string, ProfileConfig>\n /** Profile name to use when `--profile` isn't passed. When unset,\n * the top-level `daemon` / `tunnel` blocks are used directly. */\n activeProfile?: string\n /** Default `skills` + `options` auto-applied to every `agent_start`\n * spawn — global and per-adapter. See `resolveSpawnDefaults` in\n * `spawn-defaults.ts` for the merge precedence with an explicit call.\n * Absent ⇒ current behaviour exactly (no regression). */\n defaults?: SpawnDefaultsConfig\n /** User-defined generic ACP agents, keyed by adapter slug. Each entry\n * is minted into a runnable handle by the CLI's `acpHandleFromSpec`\n * when `resolveAdapter(slug)` finds no npm adapter package. User\n * entries shadow the curated `ACP_CATALOG` on slug collision. */\n acpAgents?: Record<string, AcpAgentConfigEntry>\n /** User-defined named terminal/TUI presets. Local-only; never\n * packaged in shared adapter manifests or defaults. */\n terminalPresets?: Record<string, TerminalPreset>\n /** Unknown keys preserved across save round-trips. */\n [unknown: string]: unknown\n}\n\nexport const CONFIG_FILE_PATH = (): string =>\n join(homedir(), \".agentproto\", \"config.json\")\n\n/**\n * Drop any `acpAgents` entries that aren't a shape we can turn into a\n * handle. The one hard requirement is a non-empty string `bin` (the\n * executable to spawn); everything else is optional and defaulted\n * downstream. Invalid entries are removed rather than throwing so the\n * daemon still boots — one warning names the offending slug so the\n * user can fix their config. Returns `undefined` when nothing valid\n * remains, keeping the key absent (== \"no generic agents\").\n */\nfunction sanitizeAcpAgents(\n raw: unknown,\n target: string,\n): Record<string, AcpAgentConfigEntry> | undefined {\n if (!raw || typeof raw !== \"object\" || Array.isArray(raw)) {\n console.warn(\n `[runtime/config] ${target}: 'acpAgents' is not an object — ignoring`,\n )\n return undefined\n }\n const out: Record<string, AcpAgentConfigEntry> = {}\n for (const [slug, value] of Object.entries(raw as Record<string, unknown>)) {\n if (\n value &&\n typeof value === \"object\" &&\n !Array.isArray(value) &&\n typeof (value as { bin?: unknown }).bin === \"string\" &&\n (value as { bin: string }).bin.length > 0\n ) {\n out[slug] = value as AcpAgentConfigEntry\n } else {\n console.warn(\n `[runtime/config] ${target}: acpAgents.${slug} is missing a string 'bin' — ignoring`,\n )\n }\n }\n return Object.keys(out).length > 0 ? out : undefined\n}\n\n/**\n * Load config.json. Returns an empty object (NOT null) when the file\n * is missing, malformed, or unreadable — callers can `cfg.daemon?.port`\n * safely without null-guards. Errors during a malformed-read are\n * logged once so the user notices the file is broken without the\n * daemon refusing to boot.\n */\nexport async function loadConfig(path?: string): Promise<AgentprotoConfig> {\n const target = path ?? CONFIG_FILE_PATH()\n try {\n const raw = await fs.readFile(target, \"utf8\")\n const parsed = JSON.parse(raw) as unknown\n if (parsed && typeof parsed === \"object\" && !Array.isArray(parsed)) {\n const cfg = parsed as AgentprotoConfig\n // Sanitize `acpAgents` in the same tolerant spirit as the rest of\n // this loader: a malformed entry is dropped (with one warning) so a\n // single bad hand-edit can't make every generic ACP agent\n // unresolvable. Full AIP-45 validation happens later, at\n // `acpHandleFromSpec` time, with precise field-level messages.\n if (cfg.acpAgents !== undefined) {\n cfg.acpAgents = sanitizeAcpAgents(cfg.acpAgents, target)\n }\n return cfg\n }\n console.warn(\n `[runtime/config] ${target}: top-level value is not an object — ignoring`,\n )\n return {}\n } catch (err) {\n // ENOENT is the common case; only warn on other shapes.\n const code = (err as NodeJS.ErrnoException).code\n if (code && code !== \"ENOENT\") {\n console.warn(\n `[runtime/config] failed to read ${target}: ${\n err instanceof Error ? err.message : String(err)\n }`,\n )\n }\n return {}\n }\n}\n\n/**\n * Write config.json atomically (tmp + rename) so a concurrent\n * `agentproto config edit` can't half-truncate the file. Writes\n * `next` AS-IS — callers are expected to pass the full desired\n * state (loaded the existing config, mutated, passed it back).\n *\n * Earlier versions deep-merged with the on-disk file, but that made\n * deletions impossible: `setConfigKey(cfg, \"x\", undefined)` would\n * remove the key from memory, then the deep-merge would silently\n * re-add it from disk. The current design trusts the caller's\n * snapshot and uses atomic rename for crash safety.\n */\nexport async function saveConfig(\n next: AgentprotoConfig,\n path?: string,\n): Promise<void> {\n const target = path ?? CONFIG_FILE_PATH()\n const payload = { ...next, version: CONFIG_VERSION }\n const dir = dirname(target)\n await fs.mkdir(dir, { recursive: true })\n const tmp = `${target}.tmp`\n await fs.writeFile(tmp, JSON.stringify(payload, null, 2) + \"\\n\", \"utf8\")\n await fs.rename(tmp, target)\n}\n\n/**\n * Read a dot-notation key (`daemon.port`) out of a config. Returns\n * `undefined` when any segment is missing.\n */\nexport function getConfigKey(\n cfg: AgentprotoConfig,\n dotted: string,\n): unknown {\n let cur: unknown = cfg\n for (const part of dotted.split(\".\")) {\n if (cur == null || typeof cur !== \"object\") return undefined\n cur = (cur as Record<string, unknown>)[part]\n }\n return cur\n}\n\n/**\n * Set a dot-notation key in a config. Returns a new object — does\n * NOT mutate. Creates intermediate objects as needed. Setting\n * `value: undefined` is treated as a delete.\n */\nexport function setConfigKey(\n cfg: AgentprotoConfig,\n dotted: string,\n value: unknown,\n): AgentprotoConfig {\n const parts = dotted.split(\".\")\n const out: AgentprotoConfig = { ...cfg }\n let cur: Record<string, unknown> = out as Record<string, unknown>\n for (let i = 0; i < parts.length - 1; i++) {\n const k = parts[i]!\n const next = cur[k]\n if (next && typeof next === \"object\" && !Array.isArray(next)) {\n cur[k] = { ...(next as Record<string, unknown>) }\n } else {\n cur[k] = {}\n }\n cur = cur[k] as Record<string, unknown>\n }\n const leaf = parts[parts.length - 1]!\n if (value === undefined) {\n delete cur[leaf]\n } else {\n cur[leaf] = value\n }\n return out\n}\n\n/**\n * Deep merge — objects are recursively combined, everything else\n * (arrays, primitives) is replaced wholesale by `b`. Mirrors what\n * `Object.assign({}, a, b)` does for shallow keys.\n */\nfunction deepMerge<A extends Record<string, unknown>, B extends Record<string, unknown>>(\n a: A,\n b: B,\n): A & B {\n const out: Record<string, unknown> = { ...a }\n for (const [k, v] of Object.entries(b)) {\n const cur = out[k]\n if (\n v &&\n typeof v === \"object\" &&\n !Array.isArray(v) &&\n cur &&\n typeof cur === \"object\" &&\n !Array.isArray(cur)\n ) {\n out[k] = deepMerge(\n cur as Record<string, unknown>,\n v as Record<string, unknown>,\n )\n } else {\n out[k] = v\n }\n }\n return out as A & B\n}\n"]}
|