diff --git a/VENDORED.md b/VENDORED.md index b5f016f37e..f399198182 100644 --- a/VENDORED.md +++ b/VENDORED.md @@ -22,24 +22,24 @@ never a convenience. ## Ledger -| Vendored path | What was copied | Upstream repo @ commit | Why not a published package | Owner | Kill date | Kill-date test | -| ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------ | ---------- | ----------------- | -| `apps/sidecar` | Derived from upstream's own `apps/sidecar`: six modules byte-identical (`default-harness.ts`, `signing-keypair.ts`, `source-asset-delivery.ts`, `workflow-closure-apply.ts`, `workflow-probe-handler.ts`, `workflow-run-pack-restore.ts`), three near-verbatim (`atomic-write.ts` differs only by the repo-wide logger namespace), the rest (`index.ts`, `config.ts`, `tool-materialization.ts`, `step-agent-tools.ts`, `workflow-host-wiring/`, `workflow-substrate-factory/`, …) substantially rewritten, plus workbench-only modules. A living fork, not a frozen copy, so this row carries no tree hash. | [faremeter/interchange](https://github.com/faremeter/interchange) @ `692c3106` (origin/main, 2026-09-03) | An app is never npm-published, so no publish can cover the execution host; retired by consuming an upstream-published host, or by renewing this row deliberately | sawyer | 2026-11-03 | `check:killdates` | -| `vendor/intx/agent` | `@intx/agent` source (`src/`, manifest, tsconfig) | [faremeter/interchange](https://github.com/faremeter/interchange) @ `692c3106` (origin/main, 2026-09-03) | npm 0.3.0 predates the operator-configurable doom-loop threshold (`afd0c82b`, `c421c092`) the re-vendored `workflow-host` configures; one local delta (CL-7190): `ToolBundle.beforeToolExtension` (`tool.ts`) and its composition into `ResolvedTools.beforeToolExtensions` (`agent.ts`), so a tool package can contribute its own suspend-capable extension without the reactor or director special-casing it by name; retired by the next `@intx/agent` publish | sawyer | 2026-11-03 | `check:killdates` | -| `vendor/intx/db` | `@intx/db` source (`src/`, `migrations/`, drizzle config, manifest, tsconfigs) | [faremeter/interchange](https://github.com/faremeter/interchange) @ `c1b5bb73` (origin/main, 2026-09-06) | npm 0.3.0 covers the base package but not the `wire_projection` column/loader delta (CL-6324) or the `workflow_definition.origin` column separating a definition from the per-run record of one folded run's deploy (CL-6452), shipped as migrations `0089`/`0090` behind upstream's `0088`, plus `0091` rewriting the retired `onBodyFailure: "continue"` literal to upstream's `"tolerate"` in stored wire projections, plus `0092` adding `workflow_definition.schedule_claimed_minute` for the hub's native ScheduleTrigger poller (CL-4455); the pin also carries upstream's unpublished per-principal signing-key series (`principal_key` migration renumbered to `0093`, the principal/principal-key stores, and the backfill), which npm 0.3.0 lacks; and a CL-7505 serving-time refresh delta (`model-source-resolution.ts`: `buildSource`/`resolveModelSources`/`resolveInstanceModelSources` accept an optional `servingRefresh` hook, exposing `buildSource`, with a new `credential_needs_reauth` skip so an unrefreshable `oauth_token` credential is never served); retired when upstream absorbs the deltas or publishes the signing-key series | sawyer | 2026-11-03 | `check:killdates` | -| `vendor/intx/harness` | `@intx/harness` source (`src/`, manifest, tsconfig) | [faremeter/interchange](https://github.com/faremeter/interchange) @ `692c3106` (origin/main, 2026-09-03) | npm 0.3.0 predates the connector reply drain (`driveConnectorReplies`, `ConnectorReplyDrain`, `AgentEventStream`; `11590e66`) the sidecar's warm mail loop drives; no local delta; retired by the next `@intx/harness` publish | sawyer | 2026-11-03 | `check:killdates` | -| `vendor/intx/hub-agent` | `@intx/hub-agent` source (`src/`, manifest, tsconfig) | [faremeter/interchange](https://github.com/faremeter/interchange) @ `692c3106` (origin/main, 2026-09-03) | npm 0.3.0 predates the `agentDir` path export (`927556de`) the sidecar's deploy-tree lookup uses, and its own `@intx/mail-memory`/`@intx/harness` pins must resolve the vendored copies; one local delta (CL-6396): `SessionEventSink` / `publishWorkflowInferenceEvent` stamp optional `childRunId` on `agent.event`; retired by the next publish | sawyer | 2026-11-03 | `check:killdates` | -| `vendor/intx/hub-api` | `@intx/hub-api` source (`src/`, manifest, tsconfig) | [faremeter/interchange](https://github.com/faremeter/interchange) @ `c1b5bb73` (origin/main, 2026-09-06) | npm 0.3.0 covers the base package but not the null-principal `resolveApproval` for policy-resolved decisions (CL-6345), the bearer-authenticated workflow-deploy mirror (`middleware/workflow-run-deploy-auth.ts`, CL-workflow-deploy-bearer), or the upstream per-principal signing-key seam this pin re-vendors (`principalKeyStore` threaded through the tenant/invite/run/workflow routes, the grant materializer, and `createApp`); and a CL-7505 serving-time refresh delta (`run-source-resolution.ts`: `resolveDefinitionSources` accepts and threads an optional `servingRefresh` hook); retired when upstream absorbs the deltas or publishes the signing-key series | sawyer | 2026-11-03 | `check:killdates` | -| `vendor/intx/hub-sessions` | `@intx/hub-sessions` source (`src/`, manifest, tsconfig) | [faremeter/interchange](https://github.com/faremeter/interchange) @ `692c3106` (origin/main, 2026-09-03) | npm 0.3.0 covers the base package but not the pack-acceptance fixes (`ownsWorkflowRunRepo`, `anchorAddressForPackSource`, `decideTerminalRunFlip`), the wire-projection writer (CL-6324), malformed tool-call-name sanitization (CL-6478), the sealed-run terminal-status backfill (CL-6595), the CL-7190 `registerSignalCorrelation` approval-only guard (fails loud on a future `SignalKind` this RPC has no persistence for, rather than silently mis-persisting one), or the CL-6396 `childRunId` forward on `agent.event`; and a CL-7505 serving-time refresh delta (`credential-push.ts`: `pushInstanceSourceUpdate`/`pushSourceUpdates`/`pushSourceUpdatesSubtree` accept and thread an optional `servingRefresh` hook); retired when upstream absorbs the deltas | sawyer | 2026-11-03 | `check:killdates` | -| `vendor/intx/inference` | `@intx/inference` source (`src/`, manifest, tsconfig) | [faremeter/interchange](https://github.com/faremeter/interchange) @ `692c3106` (origin/main, 2026-09-03) | npm 0.3.0 predates doom-loop detection (`8da4c827`, `afd0c82b`, `c421c092`); local deltas: `providers/google-genai-files.ts` builds its upload body as `new Uint8Array(bytes)` because TS 6's lib.dom `BodyInit` rejects `Uint8Array` (upstream compiles ESNext-only under TS 5.9); and CL-7190's `message_response` resume branch in `reactor.ts`'s `resumePendingOperation`/`timeoutMessageFor`, plus its `reactor.test.ts`/`testing/fakes.ts` regression harness (this package previously had zero tests); retired by the next publish | sawyer | 2026-11-03 | `check:killdates` | -| `vendor/intx/mail-memory` | `@intx/mail-memory` source (`src/`, manifest, tsconfig) | [faremeter/interchange](https://github.com/faremeter/interchange) @ `692c3106` (origin/main, 2026-09-03) | npm 0.3.0 predates the `@intx/mailbox` extraction (`af03bb90`), on-demand body reads (`54f7c239`) and `expunge` returning the swept uids (`bcabb1f8`) that the re-vendored `workflow-host` binds against; no local delta; retired by the next publish | sawyer | 2026-11-03 | `check:killdates` | -| `vendor/intx/mailbox` | `@intx/mailbox` source (`src/`, manifest, tsconfig) | [faremeter/interchange](https://github.com/faremeter/interchange) @ `692c3106` (origin/main, 2026-09-03) | Never published: a new package at the target pin (`af03bb90`) that `workflow-host`'s substrate mailbox store and supervisor-backed transport import; no local delta; retired by its first publish | sawyer | 2026-11-03 | `check:killdates` | -| `vendor/intx/mime` | `@intx/mime` source (`src/`, manifest, tsconfig) | [faremeter/interchange](https://github.com/faremeter/interchange) @ `692c3106` (origin/main, 2026-09-03) | npm 0.3.0 predates the non-RFC message-id guard `isMessageId` (`d97e1832`), the full `References` chain (`65c6fe70`) and the lossless `decodeMail` decoder (`3b6d06b2`) that `mailbox`/`mail-memory` at the same pin import; no local delta; retired by the next publish | sawyer | 2026-11-03 | `check:killdates` | -| `vendor/intx/types` | `@intx/types` source (`src/`, manifest, tsconfig) | [faremeter/interchange](https://github.com/faremeter/interchange) @ `c1b5bb73` (origin/main, 2026-09-06) | npm 0.3.0 predates the type surface the re-vendored trees compile against: `expunge` returning `expungedUids` (`bcabb1f8`), plain-string `PackRejectReason` (`7b42f405`), the run authorization/approvals REST types (`71ad6c08`), the decoded-mail `Mail`/`MailPartReader` model (`3b6d06b2`) and the `interchange.actions`/`loops` package-json refs (`3bd5b837`, `1ea2f39b`); one local delta (CL-7190): `"message_response"` added to `signals.ts`'s `signalKinds`, alongside `signalKindToGateType`; another (CL-6396): optional `childRunId` on `AgentEventFrame`; the pin also carries the unpublished `signer-identity` contract and `principalKeyAad` domain separator from the signing-key series; retired by the next publish | sawyer | 2026-11-03 | `check:killdates` | -| `vendor/intx/hub-common` | `@intx/hub-common` source (`src/`, manifest, tsconfig) | [faremeter/interchange](https://github.com/faremeter/interchange) @ `c1b5bb73` (origin/main, 2026-09-06) | npm 0.3.0 predates `generateId`'s `pky_` principal-key prefix from the signing-key series, which the re-vendored `db`/`hub-api` import; no local delta; retired by the next `@intx/hub-common` publish | sawyer | 2026-11-03 | `check:killdates` | -| `vendor/intx/workflow` | `@intx/workflow` source (`src/`, manifest, tsconfig) | [faremeter/interchange](https://github.com/faremeter/interchange) @ `692c3106` (origin/main, 2026-09-03) | No local delta: npm 0.3.0 predates the `onBodyFailure: "tolerate"` section policy (`b977ade6`) that `@corbits/agent-runtime` authors and the action/loop primitives (`3bd5b837`, `1ea2f39b`) the re-vendored `workflow-host` runs; retired by the next `@intx/workflow` publish | sawyer | 2026-11-03 | `check:killdates` | -| `vendor/intx/workflow-deploy` | `@intx/workflow-deploy` source (`src/`, manifest, tsconfig) | [faremeter/interchange](https://github.com/faremeter/interchange) @ `692c3106` (origin/main, 2026-09-03) | No local delta: npm 0.3.0 predates `inertLoopBody` and the loop-body source pin (`1ea2f39b`) that the re-vendored `hub-sessions` imports; retired by the next `@intx/workflow-deploy` publish | sawyer | 2026-11-03 | `check:killdates` | -| `vendor/intx/workflow-host` | `@intx/workflow-host` source (`src/`, manifest, tsconfig) | [faremeter/interchange](https://github.com/faremeter/interchange) @ `692c3106` (origin/main, 2026-09-03) | npm 0.3.0 covers the base package but not the body-spawn authorize/mail-part-reader threading and grants head-collapse (CL-6448) -- the credential thread is retired at this pin, upstream now passing the run's live credential-material cell into every body spawn natively -- that let the fork run tool-bearing onTrigger bodies, or the CL-6396 event-channel `childRunId` stamp so overlapping turns HMAC-bind the occurrence they belong to; retired when upstream absorbs the deltas | sawyer | 2026-11-03 | `check:killdates` | +| Vendored path | What was copied | Upstream repo @ commit | Why not a published package | Owner | Kill date | Kill-date test | +| ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------ | ---------- | ----------------- | +| `apps/sidecar` | Derived from upstream's own `apps/sidecar`: six modules byte-identical (`default-harness.ts`, `signing-keypair.ts`, `source-asset-delivery.ts`, `workflow-closure-apply.ts`, `workflow-probe-handler.ts`, `workflow-run-pack-restore.ts`), three near-verbatim (`atomic-write.ts` differs only by the repo-wide logger namespace), the rest (`index.ts`, `config.ts`, `tool-materialization.ts`, `step-agent-tools.ts`, `workflow-host-wiring/`, `workflow-substrate-factory/`, …) substantially rewritten, plus workbench-only modules. A living fork, not a frozen copy, so this row carries no tree hash. | [faremeter/interchange](https://github.com/faremeter/interchange) @ `692c3106` (origin/main, 2026-09-03) | An app is never npm-published, so no publish can cover the execution host; retired by consuming an upstream-published host, or by renewing this row deliberately | sawyer | 2026-11-03 | `check:killdates` | +| `vendor/intx/agent` | `@intx/agent` source (`src/`, manifest, tsconfig) | [faremeter/interchange](https://github.com/faremeter/interchange) @ `692c3106` (origin/main, 2026-09-03) | npm 0.3.0 predates the operator-configurable doom-loop threshold (`afd0c82b`, `c421c092`) the re-vendored `workflow-host` configures; one local delta (CL-7190): `ToolBundle.beforeToolExtension` (`tool.ts`) and its composition into `ResolvedTools.beforeToolExtensions` (`agent.ts`), so a tool package can contribute its own suspend-capable extension without the reactor or director special-casing it by name; retired by the next `@intx/agent` publish | sawyer | 2026-11-03 | `check:killdates` | +| `vendor/intx/db` | `@intx/db` source (`src/`, `migrations/`, drizzle config, manifest, tsconfigs) | [faremeter/interchange](https://github.com/faremeter/interchange) @ `c1b5bb73` (origin/main, 2026-09-06) | npm 0.3.0 covers the base package but not the `wire_projection` column/loader delta (CL-6324) or the `workflow_definition.origin` column separating a definition from the per-run record of one folded run's deploy (CL-6452), shipped as migrations `0089`/`0090` behind upstream's `0088`, plus `0091` rewriting the retired `onBodyFailure: "continue"` literal to upstream's `"tolerate"` in stored wire projections, plus `0092` adding `workflow_definition.schedule_claimed_minute` for the hub's native ScheduleTrigger poller (CL-4455); the pin also carries upstream's unpublished per-principal signing-key series (`principal_key` migration renumbered to `0093`, the principal/principal-key stores, and the backfill), which npm 0.3.0 lacks; and a CL-7505 serving-time refresh delta (`model-source-resolution.ts`: `buildSource`/`resolveModelSources`/`resolveInstanceModelSources` accept an optional `servingRefresh` hook, exposing `buildSource`, with a new `credential_needs_reauth` skip so an unrefreshable `oauth_token` credential is never served); one more local delta (CL-7510): `"openai-responses"` added to `schema/catalog.ts`'s `modelProvider.plugin` enum, in lockstep with the `@intx/types` delta, and an adapter registry-key dispatch delta in `model-source-resolution.ts` (`buildSource` rewrites both the launched `InferenceSource.provider` and the served credential material's `providerKey` to the provider's own registry key — `ollama`, `codex`, `xai-oauth` — while the catalog `plugin` column keeps the wire-format id, so the sidecar manifest's custom factories actually resolve); retired when upstream absorbs the deltas or publishes the signing-key series | sawyer | 2026-11-03 | `check:killdates` | +| `vendor/intx/harness` | `@intx/harness` source (`src/`, manifest, tsconfig) | [faremeter/interchange](https://github.com/faremeter/interchange) @ `692c3106` (origin/main, 2026-09-03) | npm 0.3.0 predates the connector reply drain (`driveConnectorReplies`, `ConnectorReplyDrain`, `AgentEventStream`; `11590e66`) the sidecar's warm mail loop drives; no local delta; retired by the next `@intx/harness` publish | sawyer | 2026-11-03 | `check:killdates` | +| `vendor/intx/hub-agent` | `@intx/hub-agent` source (`src/`, manifest, tsconfig) | [faremeter/interchange](https://github.com/faremeter/interchange) @ `692c3106` (origin/main, 2026-09-03) | npm 0.3.0 predates the `agentDir` path export (`927556de`) the sidecar's deploy-tree lookup uses, and its own `@intx/mail-memory`/`@intx/harness` pins must resolve the vendored copies; one local delta (CL-6396): `SessionEventSink` / `publishWorkflowInferenceEvent` stamp optional `childRunId` on `agent.event`; retired by the next publish | sawyer | 2026-11-03 | `check:killdates` | +| `vendor/intx/hub-api` | `@intx/hub-api` source (`src/`, manifest, tsconfig) | [faremeter/interchange](https://github.com/faremeter/interchange) @ `c1b5bb73` (origin/main, 2026-09-06) | npm 0.3.0 covers the base package but not the null-principal `resolveApproval` for policy-resolved decisions (CL-6345), the bearer-authenticated workflow-deploy mirror (`middleware/workflow-run-deploy-auth.ts`, CL-workflow-deploy-bearer), or the upstream per-principal signing-key seam this pin re-vendors (`principalKeyStore` threaded through the tenant/invite/run/workflow routes, the grant materializer, and `createApp`); and a CL-7505 serving-time refresh delta (`run-source-resolution.ts`: `resolveDefinitionSources` accepts and threads an optional `servingRefresh` hook); retired when upstream absorbs the deltas or publishes the signing-key series | sawyer | 2026-11-03 | `check:killdates` | +| `vendor/intx/hub-sessions` | `@intx/hub-sessions` source (`src/`, manifest, tsconfig) | [faremeter/interchange](https://github.com/faremeter/interchange) @ `692c3106` (origin/main, 2026-09-03) | npm 0.3.0 covers the base package but not the pack-acceptance fixes (`ownsWorkflowRunRepo`, `anchorAddressForPackSource`, `decideTerminalRunFlip`), the wire-projection writer (CL-6324), malformed tool-call-name sanitization (CL-6478), the sealed-run terminal-status backfill (CL-6595), the CL-7190 `registerSignalCorrelation` approval-only guard (fails loud on a future `SignalKind` this RPC has no persistence for, rather than silently mis-persisting one), or the CL-6396 `childRunId` forward on `agent.event`; and a CL-7505 serving-time refresh delta (`credential-push.ts`: `pushInstanceSourceUpdate`/`pushSourceUpdates`/`pushSourceUpdatesSubtree` accept and thread an optional `servingRefresh` hook); retired when upstream absorbs the deltas | sawyer | 2026-11-03 | `check:killdates` | +| `vendor/intx/inference` | `@intx/inference` source (`src/`, manifest, tsconfig) | [faremeter/interchange](https://github.com/faremeter/interchange) @ `692c3106` (origin/main, 2026-09-03) | npm 0.3.0 predates doom-loop detection (`8da4c827`, `afd0c82b`, `c421c092`); local deltas: `providers/google-genai-files.ts` builds its upload body as `new Uint8Array(bytes)` because TS 6's lib.dom `BodyInit` rejects `Uint8Array` (upstream compiles ESNext-only under TS 5.9); and CL-7190's `message_response` resume branch in `reactor.ts`'s `resumePendingOperation`/`timeoutMessageFor`, plus its `reactor.test.ts`/`testing/fakes.ts` regression harness (this package previously had zero tests); retired by the next publish | sawyer | 2026-11-03 | `check:killdates` | +| `vendor/intx/mail-memory` | `@intx/mail-memory` source (`src/`, manifest, tsconfig) | [faremeter/interchange](https://github.com/faremeter/interchange) @ `692c3106` (origin/main, 2026-09-03) | npm 0.3.0 predates the `@intx/mailbox` extraction (`af03bb90`), on-demand body reads (`54f7c239`) and `expunge` returning the swept uids (`bcabb1f8`) that the re-vendored `workflow-host` binds against; no local delta; retired by the next publish | sawyer | 2026-11-03 | `check:killdates` | +| `vendor/intx/mailbox` | `@intx/mailbox` source (`src/`, manifest, tsconfig) | [faremeter/interchange](https://github.com/faremeter/interchange) @ `692c3106` (origin/main, 2026-09-03) | Never published: a new package at the target pin (`af03bb90`) that `workflow-host`'s substrate mailbox store and supervisor-backed transport import; no local delta; retired by its first publish | sawyer | 2026-11-03 | `check:killdates` | +| `vendor/intx/mime` | `@intx/mime` source (`src/`, manifest, tsconfig) | [faremeter/interchange](https://github.com/faremeter/interchange) @ `692c3106` (origin/main, 2026-09-03) | npm 0.3.0 predates the non-RFC message-id guard `isMessageId` (`d97e1832`), the full `References` chain (`65c6fe70`) and the lossless `decodeMail` decoder (`3b6d06b2`) that `mailbox`/`mail-memory` at the same pin import; no local delta; retired by the next publish | sawyer | 2026-11-03 | `check:killdates` | +| `vendor/intx/types` | `@intx/types` source (`src/`, manifest, tsconfig) | [faremeter/interchange](https://github.com/faremeter/interchange) @ `c1b5bb73` (origin/main, 2026-09-06) | npm 0.3.0 predates the type surface the re-vendored trees compile against: `expunge` returning `expungedUids` (`bcabb1f8`), plain-string `PackRejectReason` (`7b42f405`), the run authorization/approvals REST types (`71ad6c08`), the decoded-mail `Mail`/`MailPartReader` model (`3b6d06b2`) and the `interchange.actions`/`loops` package-json refs (`3bd5b837`, `1ea2f39b`); one local delta (CL-7190): `"message_response"` added to `signals.ts`'s `signalKinds`, alongside `signalKindToGateType`; another (CL-6396): optional `childRunId` on `AgentEventFrame`; the pin also carries the unpublished `signer-identity` contract and `principalKeyAad` domain separator from the signing-key series; one more local delta (CL-7510): `"openai-responses"` added to `catalog.ts`'s `modelProviderPlugins` so the workbench's loopback-OAuth providers (Codex, xai-oauth) can create model-provider rows; retired by the next publish | sawyer | 2026-11-03 | `check:killdates` | +| `vendor/intx/hub-common` | `@intx/hub-common` source (`src/`, manifest, tsconfig) | [faremeter/interchange](https://github.com/faremeter/interchange) @ `c1b5bb73` (origin/main, 2026-09-06) | npm 0.3.0 predates `generateId`'s `pky_` principal-key prefix from the signing-key series, which the re-vendored `db`/`hub-api` import; no local delta; retired by the next `@intx/hub-common` publish | sawyer | 2026-11-03 | `check:killdates` | +| `vendor/intx/workflow` | `@intx/workflow` source (`src/`, manifest, tsconfig) | [faremeter/interchange](https://github.com/faremeter/interchange) @ `692c3106` (origin/main, 2026-09-03) | No local delta: npm 0.3.0 predates the `onBodyFailure: "tolerate"` section policy (`b977ade6`) that `@corbits/agent-runtime` authors and the action/loop primitives (`3bd5b837`, `1ea2f39b`) the re-vendored `workflow-host` runs; retired by the next `@intx/workflow` publish | sawyer | 2026-11-03 | `check:killdates` | +| `vendor/intx/workflow-deploy` | `@intx/workflow-deploy` source (`src/`, manifest, tsconfig) | [faremeter/interchange](https://github.com/faremeter/interchange) @ `692c3106` (origin/main, 2026-09-03) | No local delta: npm 0.3.0 predates `inertLoopBody` and the loop-body source pin (`1ea2f39b`) that the re-vendored `hub-sessions` imports; retired by the next `@intx/workflow-deploy` publish | sawyer | 2026-11-03 | `check:killdates` | +| `vendor/intx/workflow-host` | `@intx/workflow-host` source (`src/`, manifest, tsconfig) | [faremeter/interchange](https://github.com/faremeter/interchange) @ `692c3106` (origin/main, 2026-09-03) | npm 0.3.0 covers the base package but not the body-spawn authorize/mail-part-reader threading and grants head-collapse (CL-6448) -- the credential thread is retired at this pin, upstream now passing the run's live credential-material cell into every body spawn natively -- that let the fork run tool-bearing onTrigger bodies, or the CL-6396 event-channel `childRunId` stamp so overlapping turns HMAC-bind the occurrence they belong to; retired when upstream absorbs the deltas | sawyer | 2026-11-03 | `check:killdates` | The pin is `692c3106` (upstream `origin/main`, 2026-09-03), 90 commits past the `v0.3.0` release tag `b5580a02`. npm is still `0.3.0`, so every tree a diff --git a/apps/sidecar/package.json b/apps/sidecar/package.json index 3ec85acec7..5ce0a3b537 100644 --- a/apps/sidecar/package.json +++ b/apps/sidecar/package.json @@ -16,9 +16,11 @@ }, "dependencies": { "@corbits/agent-lifecycle": "workspace:*", + "@corbits/codex-provider": "workspace:*", "@corbits/credential-providers": "workspace:*", "@corbits/error-sink": "workspace:*", "@corbits/ollama-adapter": "workspace:*", + "@corbits/xai-provider": "workspace:*", "@intx/agent": "workspace:*", "@intx/authz": "0.3.0", "@intx/crypto": "0.3.0", diff --git a/apps/sidecar/src/config.ts b/apps/sidecar/src/config.ts index d1ecd32993..3ce5a76861 100644 --- a/apps/sidecar/src/config.ts +++ b/apps/sidecar/src/config.ts @@ -17,7 +17,11 @@ import { parseToolRegistries } from "./tool-materialization"; * Ollama deployment's `quirks.numCtx` (see `@corbits/seeding`'s * seed and `./workflow-substrate-factory/context-budget`) actually * reaches Ollama's `options.num_ctx` instead of silently falling back - * to the built-in adapter's defaults. An operator who sets + * to the built-in adapter's defaults, and the CL-7510 loopback-OAuth + * Responses adapters (`codex`, `xai-oauth`) so a launched source — + * rewritten to the provider's own registry key at + * `@intx/db`'s `buildSource` — serves through its own adapter rather + * than the shared wire-format one. An operator who sets * `SIDECAR_ADAPTER_MANIFEST` explicitly gets exactly what they wrote -- * this default never merges with an operator value, only replaces the * unset case. @@ -28,6 +32,16 @@ const DEFAULT_ADAPTER_MANIFEST: AdapterManifest = [ specifier: "@corbits/ollama-adapter", export: "createOllamaAdapter", }, + { + provider: "codex", + specifier: "@corbits/codex-provider", + export: "createCodexResponsesAdapter", + }, + { + provider: "xai-oauth", + specifier: "@corbits/xai-provider", + export: "createXaiResponsesAdapter", + }, ]; const WsURL = type("string").narrow((url, ctx) => { diff --git a/apps/sidecar/test/config.test.ts b/apps/sidecar/test/config.test.ts index c385177776..df326b36d0 100644 --- a/apps/sidecar/test/config.test.ts +++ b/apps/sidecar/test/config.test.ts @@ -15,6 +15,16 @@ const DEFAULT_MANIFEST = [ specifier: "@corbits/ollama-adapter", export: "createOllamaAdapter", }, + { + provider: "codex", + specifier: "@corbits/codex-provider", + export: "createCodexResponsesAdapter", + }, + { + provider: "xai-oauth", + specifier: "@corbits/xai-provider", + export: "createXaiResponsesAdapter", + }, ]; test("parses a complete environment into config", () => { @@ -39,6 +49,23 @@ test("an unset SIDECAR_ADAPTER_MANIFEST defaults to the shipped Ollama adapter, expect(config.adapterManifest).toEqual(DEFAULT_MANIFEST); }); +test("the shipped default manifest registers the loopback-OAuth Responses adapters (CL-7510)", () => { + const config = readSidecarConfig(VALID_ENV); + const byProvider = new Map( + config.adapterManifest.map((entry) => [entry.provider, entry]), + ); + expect(byProvider.get("codex")).toEqual({ + provider: "codex", + specifier: "@corbits/codex-provider", + export: "createCodexResponsesAdapter", + }); + expect(byProvider.get("xai-oauth")).toEqual({ + provider: "xai-oauth", + specifier: "@corbits/xai-provider", + export: "createXaiResponsesAdapter", + }); +}); + test("an operator-set adapter manifest fully replaces the default rather than merging with it", () => { const manifest = [ { diff --git a/apps/sidecar/tsconfig.json b/apps/sidecar/tsconfig.json index 486e36c46d..7ce2ae3a6a 100644 --- a/apps/sidecar/tsconfig.json +++ b/apps/sidecar/tsconfig.json @@ -16,6 +16,9 @@ { "path": "../../packages/agent-lifecycle/tsconfig.src.json" }, + { + "path": "../../packages/codex-provider/tsconfig.src.json" + }, { "path": "../../packages/credential-providers/tsconfig.src.json" }, @@ -25,6 +28,9 @@ { "path": "../../packages/ollama-adapter/tsconfig.src.json" }, + { + "path": "../../packages/xai-provider/tsconfig.src.json" + }, { "path": "../../vendor/intx/agent/tsconfig.src.json" }, diff --git a/apps/sidecar/tsconfig.src.json b/apps/sidecar/tsconfig.src.json index e1cd317710..6163aa3b45 100644 --- a/apps/sidecar/tsconfig.src.json +++ b/apps/sidecar/tsconfig.src.json @@ -13,6 +13,9 @@ { "path": "../../packages/agent-lifecycle/tsconfig.src.json" }, + { + "path": "../../packages/codex-provider/tsconfig.src.json" + }, { "path": "../../packages/credential-providers/tsconfig.src.json" }, @@ -22,6 +25,9 @@ { "path": "../../packages/ollama-adapter/tsconfig.src.json" }, + { + "path": "../../packages/xai-provider/tsconfig.src.json" + }, { "path": "../../vendor/intx/agent/tsconfig.src.json" }, diff --git a/apps/web/test/onboarding.test.tsx b/apps/web/test/onboarding.test.tsx index 15131f8770..2439859d25 100644 --- a/apps/web/test/onboarding.test.tsx +++ b/apps/web/test/onboarding.test.tsx @@ -69,9 +69,16 @@ describe("CREDENTIAL_PROVIDERS", () => { // list, so a provider added or removed there is caught here rather than // drifting silently. test("has one card for every provider the hub can test a credential against", () => { + // The CL-7510 loopback-OAuth ids (`codex`, `xai-oauth`) are + // deliberately absent: their credential only exists after a loopback + // OAuth login, so there is no key-paste card to render until that + // flow is wired — a card here would offer a paste form no key can + // satisfy. + const LOOPBACK_OAUTH_ONLY = new Set(["codex", "xai-oauth"]); expect(CREDENTIAL_PROVIDERS.map((p) => p.id).sort()).toEqual( supportedCredentialProviders() .map((p) => p.id) + .filter((id) => !LOOPBACK_OAUTH_ONLY.has(id)) .sort(), ); }); diff --git a/bun.lock b/bun.lock index 3256e3efa5..6fa6780691 100644 --- a/bun.lock +++ b/bun.lock @@ -10,11 +10,13 @@ "devDependencies": { "@corbits/artifacts": "github:corbitsdev/corbits-artifacts#dc435ba749b4378f19e5de772159c2a57e932938", "@corbits/artifacts-hub": "workspace:*", + "@corbits/codex-provider": "workspace:*", "@corbits/connections": "workspace:*", "@corbits/evals": "workspace:*", "@corbits/ollama-adapter": "workspace:*", "@corbits/run-scope": "workspace:*", "@corbits/webhook-triggers": "workspace:*", + "@corbits/xai-provider": "workspace:*", "@eslint/js": "^10.0.0", "@intx/crypto": "workspace:*", "@intx/db": "workspace:*", @@ -110,9 +112,11 @@ }, "dependencies": { "@corbits/agent-lifecycle": "workspace:*", + "@corbits/codex-provider": "workspace:*", "@corbits/credential-providers": "workspace:*", "@corbits/error-sink": "workspace:*", "@corbits/ollama-adapter": "workspace:*", + "@corbits/xai-provider": "workspace:*", "@intx/agent": "workspace:*", "@intx/authz": "0.3.0", "@intx/crypto": "0.3.0", @@ -1527,10 +1531,12 @@ "version": "0.0.1", "dependencies": { "@corbits/code-review": "workspace:*", + "@corbits/codex-provider": "workspace:*", "@corbits/connections": "workspace:*", "@corbits/jimmy-agent": "workspace:*", "@corbits/scout-agent": "workspace:*", "@corbits/workflows": "workspace:*", + "@corbits/xai-provider": "workspace:*", "arktype": "catalog:", "simple-icons": "^16.28.0", }, @@ -3617,10 +3623,12 @@ "@babel/helper-compilation-targets/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], - "@corbits/connections/@corbits/oauth-core": ["@corbits/oauth-core@github:corbitsdev/corbits-oauth-core#e1e69e6", { "dependencies": { "arktype": "2.2.3" } }, "corbitsdev-corbits-oauth-core-e1e69e6", "sha512-wUrD73iVyk/Dtb4yRn3hCh6N8syfsEvAkRz4XYqT0FIB3sYDSMYEWHcDMJx9nq64my/HBOeN1J6PHIOtpjhtPg=="], + "@corbits/inbox/@corbits/mailbox": ["@corbits/mailbox@github:corbitsdev/corbits-mailbox#118a2cd", { "dependencies": { "@hono/standard-validator": "0.2.3", "@standard-community/standard-json": "0.3.5", "@standard-community/standard-openapi": "0.2.9", "arktype": "2.1.29", "hono-openapi": "1.3.1" }, "peerDependencies": { "@intx/log": "^0.2.2", "@intx/mime": "^0.2.2", "@intx/types": "^0.2.2", "drizzle-orm": "^0.45.2", "hono": "^4.12.0", "postgres": "^3.4.0" } }, "corbitsdev-corbits-mailbox-118a2cd", "sha512-jVRl6/IH34JBPVUz7mqWyBm1w7uJxvYAI27gjw7zL1dFO4L+D0E5q/8b+mpwKMNOKKlGXU6UF5pdHnY8ilYx8g=="], "@corbits/mailbox/hono-openapi": ["hono-openapi@1.3.1", "", { "peerDependencies": { "@hono/standard-validator": "^0.2.0", "@standard-community/standard-json": "^0.3.5", "@standard-community/standard-openapi": "^0.2.9", "@types/json-schema": "^7.0.15", "hono": "^4.11.2", "openapi-types": "^12.1.3" }, "optionalPeers": ["@hono/standard-validator", "hono"] }, "sha512-NLVeVkhKZ3drmQNEIPac8HX8Y54uf1hJAgIM/7MfDsaeVVmB+QILWQxx5x3R3NvRHgedcbEbOCGY2uR7WQYyMw=="], + "@corbits/memory-hub/@corbits/memory": ["@corbits/memory@github:corbitsdev/corbits-memory#9e6f213", { "dependencies": { "@intx/agent": "0.2.2", "@intx/authz": "0.2.2", "@intx/hub-api": "0.2.2", "@intx/log": "0.2.2", "@intx/workflow": "0.2.2", "arktype": "^2.1.29", "drizzle-orm": "^0.45.1", "hono": "^4.9.0", "hono-openapi": "^1.3.1", "postgres": "^3.4.7" } }, "corbitsdev-corbits-memory-9e6f213", "sha512-utnM4ZT2zmslcPXYWAAqxlDNLcpGsXFiTOtj8h7+OXnhCP0Eaw8yl25+yCTyHpvt3jcdeG4h5uFsSj7ou0BZCA=="], + "@esbuild-kit/core-utils/esbuild": ["esbuild@0.18.20", "", { "optionalDependencies": { "@esbuild/android-arm": "0.18.20", "@esbuild/android-arm64": "0.18.20", "@esbuild/android-x64": "0.18.20", "@esbuild/darwin-arm64": "0.18.20", "@esbuild/darwin-x64": "0.18.20", "@esbuild/freebsd-arm64": "0.18.20", "@esbuild/freebsd-x64": "0.18.20", "@esbuild/linux-arm": "0.18.20", "@esbuild/linux-arm64": "0.18.20", "@esbuild/linux-ia32": "0.18.20", "@esbuild/linux-loong64": "0.18.20", "@esbuild/linux-mips64el": "0.18.20", "@esbuild/linux-ppc64": "0.18.20", "@esbuild/linux-riscv64": "0.18.20", "@esbuild/linux-s390x": "0.18.20", "@esbuild/linux-x64": "0.18.20", "@esbuild/netbsd-x64": "0.18.20", "@esbuild/openbsd-x64": "0.18.20", "@esbuild/sunos-x64": "0.18.20", "@esbuild/win32-arm64": "0.18.20", "@esbuild/win32-ia32": "0.18.20", "@esbuild/win32-x64": "0.18.20" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA=="], "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], @@ -3643,6 +3651,10 @@ "@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.8", "", {}, "sha512-YYNsSlXBjMk92SKnkwvB5LOVSa6OznlFUGcsvrFgNJbJCd0M1XKeFVRc8ZByeCqz32FivYNHJVooLmdqrmvp/Q=="], + "@workbench/hub/@corbits/mailbox": ["@corbits/mailbox@github:corbitsdev/corbits-mailbox#118a2cd", { "dependencies": { "@hono/standard-validator": "0.2.3", "@standard-community/standard-json": "0.3.5", "@standard-community/standard-openapi": "0.2.9", "arktype": "2.1.29", "hono-openapi": "1.3.1" }, "peerDependencies": { "@intx/log": "^0.2.2", "@intx/mime": "^0.2.2", "@intx/types": "^0.2.2", "drizzle-orm": "^0.45.2", "hono": "^4.12.0", "postgres": "^3.4.0" } }, "corbitsdev-corbits-mailbox-118a2cd", "sha512-jVRl6/IH34JBPVUz7mqWyBm1w7uJxvYAI27gjw7zL1dFO4L+D0E5q/8b+mpwKMNOKKlGXU6UF5pdHnY8ilYx8g=="], + + "@workbench/hub/@corbits/memory": ["@corbits/memory@github:corbitsdev/corbits-memory#9e6f213", { "dependencies": { "@intx/agent": "0.2.2", "@intx/authz": "0.2.2", "@intx/hub-api": "0.2.2", "@intx/log": "0.2.2", "@intx/workflow": "0.2.2", "arktype": "^2.1.29", "drizzle-orm": "^0.45.1", "hono": "^4.9.0", "hono-openapi": "^1.3.1", "postgres": "^3.4.7" } }, "corbitsdev-corbits-memory-9e6f213", "sha512-utnM4ZT2zmslcPXYWAAqxlDNLcpGsXFiTOtj8h7+OXnhCP0Eaw8yl25+yCTyHpvt3jcdeG4h5uFsSj7ou0BZCA=="], + "ajv-formats/ajv": ["ajv@8.20.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA=="], "better-call/@better-auth/utils": ["@better-auth/utils@0.5.0", "", { "dependencies": { "@noble/hashes": "^2.0.1" } }, "sha512-BL8W4EfIZFwlu0r54m3v1ztjDhu6dDe/amLTm0xybmbZaNgYUqhD3SjpAsnq0q8YD6/ki4iwIgxJNLP/N3TxiA=="], @@ -3697,6 +3709,8 @@ "@babel/helper-compilation-targets/lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], + "@corbits/inbox/@corbits/mailbox/hono-openapi": ["hono-openapi@1.3.1", "", { "peerDependencies": { "@hono/standard-validator": "^0.2.0", "@standard-community/standard-json": "^0.3.5", "@standard-community/standard-openapi": "^0.2.9", "@types/json-schema": "^7.0.15", "hono": "^4.11.2", "openapi-types": "^12.1.3" }, "optionalPeers": ["@hono/standard-validator", "hono"] }, "sha512-NLVeVkhKZ3drmQNEIPac8HX8Y54uf1hJAgIM/7MfDsaeVVmB+QILWQxx5x3R3NvRHgedcbEbOCGY2uR7WQYyMw=="], + "@esbuild-kit/core-utils/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.18.20", "", { "os": "android", "cpu": "arm" }, "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw=="], "@esbuild-kit/core-utils/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.18.20", "", { "os": "android", "cpu": "arm64" }, "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ=="], @@ -3743,6 +3757,8 @@ "@modelcontextprotocol/sdk/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], + "@workbench/hub/@corbits/mailbox/hono-openapi": ["hono-openapi@1.3.1", "", { "peerDependencies": { "@hono/standard-validator": "^0.2.0", "@standard-community/standard-json": "^0.3.5", "@standard-community/standard-openapi": "^0.2.9", "@types/json-schema": "^7.0.15", "hono": "^4.11.2", "openapi-types": "^12.1.3" }, "optionalPeers": ["@hono/standard-validator", "hono"] }, "sha512-NLVeVkhKZ3drmQNEIPac8HX8Y54uf1hJAgIM/7MfDsaeVVmB+QILWQxx5x3R3NvRHgedcbEbOCGY2uR7WQYyMw=="], + "ajv-formats/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], "form-data/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], diff --git a/package.json b/package.json index 0acdf99d1d..c0e98fa170 100644 --- a/package.json +++ b/package.json @@ -32,8 +32,10 @@ "devDependencies": { "@corbits/artifacts": "github:corbitsdev/corbits-artifacts#dc435ba749b4378f19e5de772159c2a57e932938", "@corbits/artifacts-hub": "workspace:*", + "@corbits/codex-provider": "workspace:*", "@corbits/evals": "workspace:*", "@corbits/ollama-adapter": "workspace:*", + "@corbits/xai-provider": "workspace:*", "@intx/db": "workspace:*", "@intx/crypto": "workspace:*", "@intx/log": "workspace:*", diff --git a/packages/codex-provider/package.json b/packages/codex-provider/package.json index 9c00997fc8..bec79e38c2 100644 --- a/packages/codex-provider/package.json +++ b/packages/codex-provider/package.json @@ -7,7 +7,8 @@ "type": "module", "sideEffects": false, "exports": { - ".": "./src/index.ts" + ".": "./src/index.ts", + "./constants": "./src/constants.ts" }, "scripts": { "typecheck": "tsc --noEmit", diff --git a/packages/codex-provider/tsconfig.src.json b/packages/codex-provider/tsconfig.src.json index e74c588ea8..23572def72 100644 --- a/packages/codex-provider/tsconfig.src.json +++ b/packages/codex-provider/tsconfig.src.json @@ -4,7 +4,6 @@ "exclude": ["src/**/*.test.ts", "src/**/*.test.tsx"], "compilerOptions": { "types": ["bun"], - "noEmit": true, "composite": true, "emitDeclarationOnly": true, "outDir": "dist", diff --git a/packages/connections/src/credential-test.ts b/packages/connections/src/credential-test.ts index 25dfb298ae..659207923c 100644 --- a/packages/connections/src/credential-test.ts +++ b/packages/connections/src/credential-test.ts @@ -22,6 +22,8 @@ export type SupportedCredentialProvider = | "openai" | "google-genai" | "xai" + | "xai-oauth" + | "codex" | "openrouter" | "opencode-zen" | "groq" @@ -39,23 +41,27 @@ export type SupportedCredentialProvider = * at deploy time they all ride the same OpenAI-compatible wire shape, so * their catalog `plugin` value must be `"openai-compatible"` — never their * own provider id — and `ModelProviderPlugin` gets no wider for them. + * Codex and xai-oauth are the opposite: both speak OpenAI's Responses + * protocol, so they ride `"openai-responses"`. * - * This is NOT the same string as the registry key - * `byProvider.get(source.provider)` resolves against (CL-6586). For every - * provider above but Ollama the two happen to be equal, because the - * built-in `"openai-compatible"` adapter is exactly what serves them. Ollama - * is the one exception: it needs `@corbits/ollama-adapter`'s custom factory - * (registered under the key `"ollama"`, `apps/sidecar/src/config.ts`) so an - * offering's `quirks.numCtx` actually reaches `options.num_ctx` — the - * built-in adapter's stricter `quirks` schema rejects that shape outright. - * `packages/folded-runs/src/launch.ts`'s `withOllamaAdapterKey` is the one - * place that correction happens: it leaves `plugin`/`ModelSource.provider` - * as the accurate `"openai-compatible"` wire format and only rewrites the - * launched `InferenceSource.provider` — the actual registry-dispatch - * field — for an offering whose catalog provider is named `"ollama"`. + * This is NOT the same string as the registry key a launched + * `InferenceSource.provider` dispatches against (CL-6586). The launch-time + * dispatch field is resolved from the catalog provider row's own `plugin` + * column at serving time (`vendor/intx/db/src/model-source-resolution.ts`, + * `buildSource`'s `provider: provider.plugin`) — there is no separate + * rewrite step a caller must remember to run. Ollama is the one provider + * whose dispatch key differs from its wire shape: it needs + * `@corbits/ollama-adapter`'s custom factory (registered under the key + * `"ollama"`, `apps/sidecar/src/config.ts`) so an offering's + * `quirks.numCtx` actually reaches `options.num_ctx` — the built-in + * adapter's stricter `quirks` schema rejects that shape outright. */ export type AdapterPluginId = - "anthropic" | "openai" | "openai-compatible" | "google-genai"; + | "anthropic" + | "openai" + | "openai-compatible" + | "google-genai" + | "openai-responses"; export type CredentialTestResult = { readonly ok: true } | { readonly ok: false; readonly message: string }; @@ -101,7 +107,34 @@ type ProbeRequest = { readonly body?: string; }; -export type ProviderTestConfig = { +export type ProviderTestConfig = CommonProviderTestConfig & + ( + | { + /** Set for a provider whose credential can never be proven by a + * free network probe — this config exists for the catalog and + * descriptor surfaces only, and `testProviderCredential` returns + * this message as a failed result without dialing anything. */ + readonly probeless: string; + } + | { + /** Builds the free probe request that proves the key without spending a + * token: same auth layer a completion would hit, no generation cost. + * `baseURL` is the resolved origin for this probe — `config.baseURL` + * for every provider except `ollama`, which threads a caller-supplied + * override through here (see `TestProviderCredentialArgs.baseURL`). */ + readonly buildProbeRequest: ( + apiKey: string, + baseURL: string, + ) => ProbeRequest; + /** Whether this status/body pair means "the provider rejected the key," + * as opposed to a network problem or some other failure — each + * provider maps auth failures to its own status code. Only probed + * providers carry one; a `probeless` provider never fetches. */ + readonly isKeyRejected: (status: number, body: string) => boolean; + } + ); + +type CommonProviderTestConfig = { readonly displayName: string; readonly baseURL: string; readonly adapterPlugin: AdapterPluginId; @@ -109,22 +142,12 @@ export type ProviderTestConfig = { * once the credential is stored, never a claim about how this test * itself validates the key (it never calls this model). */ readonly probeModel: string; - /** Builds the free probe request that proves the key without spending a - * token: same auth layer a completion would hit, no generation cost. - * `baseURL` is the resolved origin for this probe — `config.baseURL` - * for every provider except `ollama`, which threads a caller-supplied - * override through here (see `TestProviderCredentialArgs.baseURL`). */ - readonly buildProbeRequest: (apiKey: string, baseURL: string) => ProbeRequest; /** Whether this status means "the probe proved the key," overriding the * default `response.ok` (2xx) check. Only Opencode Zen's probe needs * this: its empty-body POST can never succeed with a 2xx — the gateway * always reaches the missing-`model` validation error after a real key * clears auth — so a 400 there is the success signal, not a 2xx. */ readonly isKeyAccepted?: (status: number) => boolean; - /** Whether this status/body pair means "the provider rejected the key," - * as opposed to a network problem or some other failure — each - * provider maps auth failures to its own status code. */ - readonly isKeyRejected: (status: number, body: string) => boolean; }; const ANTHROPIC_VERSION = "2023-06-01"; @@ -248,6 +271,46 @@ export const PROVIDER_TEST_CONFIG: Readonly< return parsed.code === "invalid-argument"; }, }, + "xai-oauth": { + displayName: "xAI (Grok OAuth)", + // The grok-cli OAuth proxy, not api.x.ai: tokens minted by this login + // are rejected by the platform API surface, which expects an API key + // (the `xai` entry above). The proxy speaks the Responses protocol at + // its `/responses` path, so this provider rides `openai-responses`. + baseURL: "https://cli-chat-proxy.grok.com/v1", + adapterPlugin: "openai-responses", + // Kept in sync with the proxy's own model list (the upstream xai + // provider's XAI_DEFAULT_MODELS): the proxy rejects model ids its CLI + // does not serve. + probeModel: "grok-4.6", + // The proxy's list-models route is expected to answer 401 to a bad or + // expired OAuth token (mirroring the platform surface's behavior) — + // not yet confirmed live against a real login. + buildProbeRequest: (apiKey, _baseURL) => ({ + url: "https://cli-chat-proxy.grok.com/v1/models", + headers: { Authorization: `Bearer ${apiKey}` }, + }), + isKeyRejected: (status) => status === 401, + }, + codex: { + displayName: "Codex", + // OpenAI's consumer authorization server and ChatGPT backend, not the + // platform API-key system (the `openai` entry above) — a Codex login + // yields a ChatGPT-subscription token the platform surface rejects. + baseURL: "https://chatgpt.com/backend-api", + adapterPlugin: "openai-responses", + probeModel: "gpt-5.5", + // Probeless, and deliberately so: Codex publishes no live model + // catalog (`GET /codex/models` does not exist) and has no free + // auth-check endpoint — the cheapest authenticated route is the + // billing-bearing `/codex/responses` itself. The loopback OAuth token + // exchange that mints the credential is the proof a real token was + // obtained; there is nothing a pre-flight fetch could honestly add. + probeless: + "Codex credentials cannot be tested in advance — the ChatGPT " + + "subscription token has no free auth-check endpoint. The OAuth " + + "login that produced it is itself the proof.", + }, openrouter: { displayName: "OpenRouter", baseURL: "https://openrouter.ai/api/v1", @@ -468,6 +531,13 @@ export async function testProviderCredential( ): Promise { const config = PROVIDER_TEST_CONFIG[args.provider]; const doFetch = args.fetchImpl ?? fetch; + + // A probeless provider has no honest pre-flight probe (see `codex`): + // report the named reason without dialing anything. + if ("probeless" in config) { + return { ok: false, message: config.probeless }; + } + const probe = config.buildProbeRequest( args.apiKey, args.baseURL ?? config.baseURL, diff --git a/packages/connections/src/descriptor.ts b/packages/connections/src/descriptor.ts index 9dd2831873..e90407e3d6 100644 --- a/packages/connections/src/descriptor.ts +++ b/packages/connections/src/descriptor.ts @@ -6,7 +6,18 @@ import type { CredentialTestResult } from "./credential-test"; export type ConnectorAuthKind = - "oauth-pkce" | "oauth-code" | "api-key" | "webhook-secret"; + | "oauth-pkce" + | "oauth-code" + // A provider whose authorization server only accepts a fixed + // `http://localhost:` redirect URI captured by a local login + // server during the flow (Codex: `localhost:1455`; xai-oauth: + // `127.0.0.1:1456`) — unlike `oauth-pkce`/`oauth-code`, the browser + // never comes back to the hub, so none of `oauth-routes.ts`'s callback + // mechanics apply; the host runs the loopback server itself and only + // the descriptor's `oauth.exchange` is reused. + | "oauth-loopback" + | "api-key" + | "webhook-secret"; /** The result of trading an authorization code for real material — * shared across every OAuth connector regardless of whether the diff --git a/packages/connections/src/oauth-routes.test.ts b/packages/connections/src/oauth-routes.test.ts index 2d113add74..24284ceb56 100644 --- a/packages/connections/src/oauth-routes.test.ts +++ b/packages/connections/src/oauth-routes.test.ts @@ -77,6 +77,11 @@ function asUser(session: { userId: string }): MiddlewareHandler { }; } +function loopbackDescriptor(): ConnectorDescriptor { + const descriptor = fakeDescriptor(); + return { ...descriptor, id: "loopback", authKind: "oauth-loopback" }; +} + function mountAuthenticated( routes: Hono, session: { userId: string } = { userId: "user_1" }, @@ -827,3 +832,28 @@ describe("onConnected hook", () => { ); }); }); + +describe("oauth-loopback descriptors", () => { + test("the web start route refuses them with a typed error, never a redirect", async () => { + const app = connectRoutes({}, { loopback: loopbackDescriptor() }); + const response = await app.request("/api/connections/oauth/loopback/start"); + expect(response.status).toBe(409); + const body = (await response.json()) as { + error: { code: string; message: string }; + }; + expect(body.error.code).toBe("loopback_connector"); + expect(body.error.message).toContain("loopback"); + }); + + test("the callback route refuses them too", async () => { + const app = connectRoutes({}, { loopback: loopbackDescriptor() }); + const response = await app.request( + "/api/connections/oauth/loopback/callback?code=abc", + ); + expect(response.status).toBe(409); + const body = (await response.json()) as { + error: { code: string }; + }; + expect(body.error.code).toBe("loopback_connector"); + }); +}); diff --git a/packages/connections/src/oauth-routes.ts b/packages/connections/src/oauth-routes.ts index 0178a652bb..c804a21132 100644 --- a/packages/connections/src/oauth-routes.ts +++ b/packages/connections/src/oauth-routes.ts @@ -273,6 +273,20 @@ export function createOAuthConnectRoutes( const lastStartByKey = new Map(); + // A loopback OAuth connector's authorization server only accepts a fixed + // `http://localhost:` redirect captured by the host's own login + // server — redirecting a browser there from this web flow would be a + // dead-end consent (CL-7510). These providers connect through the + // sidecar lane instead, so both routes refuse them with a typed error. + function loopbackRefusalBody(connectorId: string) { + return { + error: { + code: "loopback_connector", + message: `${connectorId} connects through a local loopback OAuth login, not this web redirect flow.`, + }, + }; + } + function findOAuthDescriptor(connectorId: string) { const descriptor = registry[connectorId]; if (descriptor === undefined || descriptor.oauth === undefined) { @@ -314,6 +328,9 @@ export function createOAuthConnectRoutes( 404, ); } + if (descriptor.authKind === "oauth-loopback") { + return c.json(loopbackRefusalBody(connectorId), 409); + } const returnPath = sanitizeReturnPath( c.req.query("return"), @@ -427,6 +444,9 @@ export function createOAuthConnectRoutes( 404, ); } + if (descriptor.authKind === "oauth-loopback") { + return c.json(loopbackRefusalBody(connectorId), 409); + } // The cookie is never trusted any more than the query param was at // /start -- it is still attacker-influenced input (this same diff --git a/packages/connections/src/routes.test.ts b/packages/connections/src/routes.test.ts index 8897816811..4310723497 100644 --- a/packages/connections/src/routes.test.ts +++ b/packages/connections/src/routes.test.ts @@ -112,6 +112,24 @@ const FAKE_REGISTRY: Readonly> = { exchange: async () => ({ ok: true, apiKey: "unused" }), }, }, + "loopback-connector": { + id: "loopback-connector", + displayName: "Loopback Connector", + authKind: "oauth-loopback", + credentialPlugin: "http", + docsUrl: "https://example.test/docs", + feedsTools: [], + oauth: { + authorizeUrl: "https://example.test/authorize", + usesPKCE: true, + echoesState: true, + deploysDefaultWorkflows: true, + clientId: () => "public-client-id", + buildAuthorizeUrl: ({ state }) => + new URL(`https://example.test/authorize?state=${state}`), + exchange: async () => ({ ok: true, apiKey: "unused" }), + }, + }, }; function mountAs(routes: Hono): Hono { @@ -214,6 +232,13 @@ describe("GET /oauth-configured", () => { expect(body["accepting-connector"]).toBeUndefined(); expect(body["display-only-connector"]).toBeUndefined(); }); + + test("omits loopback connectors -- they never route through this web redirect flow", async () => { + const app = buildApp(); + const response = await app.request("/oauth-configured"); + const body = (await response.json()) as Record; + expect(body["loopback-connector"]).toBeUndefined(); + }); }); describe("POST /:connectorId/complete", () => { diff --git a/packages/connections/src/routes.ts b/packages/connections/src/routes.ts index 88d2823efc..8fa49114b4 100644 --- a/packages/connections/src/routes.ts +++ b/packages/connections/src/routes.ts @@ -336,6 +336,10 @@ export function createConnectionRoutes( const configured: Record = {}; for (const [id, descriptor] of Object.entries(registry)) { if (descriptor.oauth === undefined) continue; + // A loopback connector is never reachable through this web flow + // (its start/callback routes refuse it), so it is never reported + // as configured here either. + if (descriptor.authKind === "oauth-loopback") continue; const hasClientId = descriptor.oauth.clientId === undefined || descriptor.oauth.clientId(oauthEnv) !== undefined; diff --git a/packages/connections/test/credential-test.test.ts b/packages/connections/test/credential-test.test.ts index 0edfe07e59..c0a0832c4b 100644 --- a/packages/connections/test/credential-test.test.ts +++ b/packages/connections/test/credential-test.test.ts @@ -25,6 +25,7 @@ describe("supportedCredentialProviders", () => { .sort(), ).toEqual([ "anthropic", + "codex", "deepseek", "google-genai", "groq", @@ -35,6 +36,7 @@ describe("supportedCredentialProviders", () => { "opencode-zen", "openrouter", "xai", + "xai-oauth", ]); }); }); @@ -76,6 +78,62 @@ describe("providerModelSource", () => { expect(providerModelSource("openai").provider).toBe("openai"); expect(providerModelSource("google-genai").provider).toBe("google-genai"); }); + + test("maps codex and xai-oauth to the openai-responses adapter", () => { + expect(providerModelSource("codex").provider).toBe("openai-responses"); + expect(providerModelSource("codex").model).toBe("gpt-5.5"); + expect(providerModelSource("codex").baseURL).toBe( + "https://chatgpt.com/backend-api", + ); + expect(providerModelSource("xai-oauth").provider).toBe("openai-responses"); + expect(providerModelSource("xai-oauth").baseURL).toBe( + "https://cli-chat-proxy.grok.com/v1", + ); + }); +}); + +describe("codex and xai-oauth probe posture", () => { + test("codex never dials the network and reports why", async () => { + let dialed = false; + const fetchImpl: FetchLike = async () => { + dialed = true; + return new Response("{}", { status: 200 }); + }; + const result = await testProviderCredential({ + provider: "codex", + apiKey: "any", + fetchImpl, + }); + expect(dialed).toBe(false); + expect(result.ok).toBe(false); + if (!result.ok) expect(result.message).toContain("Codex"); + }); + + test("xai-oauth probes its own proxy like any bearer-token provider", async () => { + let url = ""; + const fetchImpl: FetchLike = async (requested, init) => { + url = requested; + expect((init.headers as Headers).get("authorization")).toBe( + "Bearer tok_1", + ); + return new Response(JSON.stringify({ data: [] }), { status: 200 }); + }; + const accepted = await testProviderCredential({ + provider: "xai-oauth", + apiKey: "tok_1", + fetchImpl, + }); + expect(url).toBe("https://cli-chat-proxy.grok.com/v1/models"); + expect(accepted).toEqual({ ok: true }); + + const rejected = await testProviderCredential({ + provider: "xai-oauth", + apiKey: "tok_stale", + fetchImpl: async () => + new Response(JSON.stringify({ error: "expired" }), { status: 401 }), + }); + expect(rejected.ok).toBe(false); + }); }); describe("testProviderCredential", () => { diff --git a/packages/inference-settings/src/effective-list.test.ts b/packages/inference-settings/src/effective-list.test.ts index abe2877f8c..ab8748d4c8 100644 --- a/packages/inference-settings/src/effective-list.test.ts +++ b/packages/inference-settings/src/effective-list.test.ts @@ -20,6 +20,9 @@ describe("providerDisplayName", () => { test("resolves a known provider slug to its own display name", () => { expect(providerDisplayName("ollama")).toBe("Ollama (local)"); expect(providerDisplayName("opencode-zen")).toBe("Opencode Zen"); + // The CL-7510 OAuth-only ids resolve too, not fall back to their slug. + expect(providerDisplayName("codex")).toBe("Codex"); + expect(providerDisplayName("xai-oauth")).toBe("xAI (Grok OAuth)"); }); test("falls back to the raw slug for an unrecognized provider", () => { diff --git a/packages/onboarding/src/plant-env-credentials.ts b/packages/onboarding/src/plant-env-credentials.ts index dc8598cf1a..1a4d2f495d 100644 --- a/packages/onboarding/src/plant-env-credentials.ts +++ b/packages/onboarding/src/plant-env-credentials.ts @@ -57,6 +57,11 @@ export const PROVIDER_ENV_VARS: Readonly< openai: ["OPENAI_API_KEY"], "google-genai": ["GEMINI_API_KEY", "GOOGLE_API_KEY"], xai: ["XAI_API_KEY"], + // The OAuth-only providers (CL-7510) have no env-var API-key path at + // all — a token only exists after its loopback OAuth login, so there is + // no env var an operator could plant and no name here could match. + "xai-oauth": [], + codex: [], openrouter: ["OPENROUTER_API_KEY"], "opencode-zen": ["OPENCODE_ZEN_API_KEY"], groq: ["GROQ_API_KEY"], diff --git a/packages/seeding/src/catalog-seed-data.ts b/packages/seeding/src/catalog-seed-data.ts index fc6a82ef45..3966f686d4 100644 --- a/packages/seeding/src/catalog-seed-data.ts +++ b/packages/seeding/src/catalog-seed-data.ts @@ -35,6 +35,13 @@ export type CatalogProviderSpec = { readonly name: string; readonly plugin: AdapterPluginId; readonly baseURL: string; + /** + * The `quirks` bag stored on every offering this seed creates — data the + * serving adapter needs and cannot default (Codex's host identity: + * `CodexQuirks`, upstream `@corbits/codex-provider`). Absent for every + * provider whose adapter handles its own request shape unmodified. + */ + readonly quirks?: Record; }; export type CatalogProviderSeed = { @@ -100,6 +107,39 @@ export const CATALOG_SEEDS: Readonly< { canonicalName: "grok-code-fast-1", displayName: "Grok Code Fast 1" }, ], }, + "xai-oauth": { + provider: { + name: "xai-oauth", + plugin: "openai-responses", + // The grok-cli OAuth proxy — tokens from this login are rejected by + // api.x.ai, which expects a plain API key (the `xai` seed above). + baseURL: "https://cli-chat-proxy.grok.com/v1", + }, + // Mirrors the proxy's own model list (the upstream xai provider's + // XAI_DEFAULT_MODELS); the proxy rejects any other model id. + models: [ + { canonicalName: "grok-4.6", displayName: "Grok 4.6 (Grok OAuth)" }, + { canonicalName: "grok-4.5", displayName: "Grok 4.5 (Grok OAuth)" }, + { + canonicalName: "grok-composer-2.5-fast", + displayName: "Grok Composer 2.5 Fast (Grok OAuth)", + }, + ], + }, + codex: { + provider: { + name: "codex", + plugin: "openai-responses", + // The ChatGPT subscription backend, not the platform API surface. + baseURL: "https://chatgpt.com/backend-api", + // Codex's adapter validates this host identity bag and refuses to + // default it — there is no honest generic product name. + quirks: { productName: "Codex", environmentTagName: "codex_environment" }, + }, + // Codex publishes no live model catalog, so this curated entry is the + // entire list; the backend serves the Codex CLI's current default. + models: [{ canonicalName: "gpt-5.5", displayName: "GPT-5.5 (Codex)" }], + }, openrouter: { provider: { name: "openrouter", diff --git a/packages/seeding/src/seed.ts b/packages/seeding/src/seed.ts index 6b8339f971..ec7c86ee90 100644 --- a/packages/seeding/src/seed.ts +++ b/packages/seeding/src/seed.ts @@ -2055,6 +2055,25 @@ async function ensureCatalogOffering( // a log line, a screenshot, or a bug report. export const PLACEHOLDER_CATALOG_API_KEY = "placeholder-not-a-real-key"; +/** + * Thrown by `seedCatalog` when a caller asks for a placeholder credential + * against an OAuth-only provider — `codex` and `xai-oauth` have no API-key + * path at all, so a fake `"placeholder-not-a-real-key"` secret could never + * authenticate and would only make the offering look launchable when no + * turn against it can ever succeed. A real token from the provider's own + * OAuth login (or a seeded `apiKey`) is the only way to make these + * launchable. + */ +export class PlaceholderCredentialError extends Error { + override readonly name = "PlaceholderCredentialError"; +} + +/** Providers whose only credential is a real OAuth token — + * `placeholderCredential: true` is a `PlaceholderCredentialError` for + * these, never a planted fake. */ +const OAUTH_ONLY_CATALOG_PROVIDERS: ReadonlySet = + new Set(["codex", "xai-oauth"]); + export type SeedCatalogArgs = { api: ApiCall; cookies: string[]; @@ -2167,6 +2186,17 @@ export async function seedCatalog( const { api, cookies, tenantId, log, provider = "anthropic" } = args; const seed = CATALOG_SEEDS[provider]; + if ( + args.placeholderCredential === true && + OAUTH_ONLY_CATALOG_PROVIDERS.has(provider) + ) { + throw new PlaceholderCredentialError( + `provider "${provider}" has no API-key credential path — a placeholder ` + + `secret could never authenticate. Connect it through its OAuth login ` + + `or seed a real token instead.`, + ); + } + const providerBaseURL = provider === "ollama" ? ollamaOpenAICompatBaseURL(args.baseURLOverride ?? seed.provider.baseURL) @@ -2317,11 +2347,15 @@ export async function seedCatalog( // model's real ceiling (or `undefined` for a provider outside this // mechanism's scope, or a model this catalog has not vetted a ceiling // for), landing on the offering's `quirks` column exactly the way - // `capabilitiesForDeployment` lands on its `capabilities` column. - const quirks = quirksForDeployment({ - providerName: seed.provider.name, - canonicalName: model.canonicalName, - }); + // `capabilitiesForDeployment` lands on its `capabilities` column. A + // provider whose seed declares its own bag (Codex's host identity) + // uses that instead — one quirks source per provider, never both. + const quirks = + seed.provider.quirks ?? + quirksForDeployment({ + providerName: seed.provider.name, + canonicalName: model.canonicalName, + }); await ensureCatalogOffering( api, cookies, diff --git a/packages/seeding/test/catalog-seed-data.test.ts b/packages/seeding/test/catalog-seed-data.test.ts index 489ebfae04..d407bb09e6 100644 --- a/packages/seeding/test/catalog-seed-data.test.ts +++ b/packages/seeding/test/catalog-seed-data.test.ts @@ -35,6 +35,9 @@ describe("CATALOG_SEEDS", () => { // inside the generic 2–5 bound. if (provider === "anthropic" || provider === "openai") continue; if (provider === "google-genai") continue; + // Codex publishes no live model catalog, so its curated entry is + // exactly one model — the bound below assumes a browsable spread. + if (provider === "codex") continue; expect(seed.models.length).toBeGreaterThanOrEqual(2); expect(seed.models.length).toBeLessThanOrEqual(5); } @@ -83,6 +86,39 @@ describe("CATALOG_SEEDS", () => { expect(model.displayName.length).toBeGreaterThan(0); } }); + test("codex and xai-oauth seed behind the openai-responses adapter", () => { + expect(CATALOG_SEEDS.codex.provider).toEqual({ + name: "codex", + plugin: "openai-responses", + baseURL: "https://chatgpt.com/backend-api", + // The Codex adapter validates this host identity and refuses to + // default it — an offering without it could never serve a turn. + quirks: { + productName: "Codex", + environmentTagName: "codex_environment", + }, + }); + expect(CATALOG_SEEDS.codex.models.map((m) => m.canonicalName)).toEqual([ + "gpt-5.5", + ]); + + expect(CATALOG_SEEDS["xai-oauth"].provider).toEqual({ + name: "xai-oauth", + plugin: "openai-responses", + baseURL: "https://cli-chat-proxy.grok.com/v1", + }); + expect( + CATALOG_SEEDS["xai-oauth"].models.map((m) => m.canonicalName), + ).toEqual(["grok-4.6", "grok-4.5", "grok-composer-2.5-fast"]); + expect(CATALOG_SEEDS["xai-oauth"].provider.quirks).toBeUndefined(); + }); + + test("no seed other than codex declares an offering quirks bag", () => { + for (const [provider, seed] of Object.entries(CATALOG_SEEDS)) { + if (provider === "codex") continue; + expect(seed.provider.quirks).toBeUndefined(); + } + }); }); describe("deriveWorkbenchHostInferencePreferences", () => { diff --git a/packages/seeding/test/provider-plugin-contract.test.ts b/packages/seeding/test/provider-plugin-contract.test.ts new file mode 100644 index 0000000000..db3a1ab588 --- /dev/null +++ b/packages/seeding/test/provider-plugin-contract.test.ts @@ -0,0 +1,45 @@ +// The boundary the CL-7510 OAuth-only providers actually cross: the real +// `@intx/types` CreateModelProvider validator (the one the hub's +// model-providers POST route enforces) and `@intx/db`'s +// `parseModelProviderRow` (the one every read applies) must both accept the +// `openai-responses` plugin id the codex/xai-oauth seeds carry — the fake +// API in the seedCatalog tests accepts everything, so this is the only +// test that proves a real-token seed would not 400 at create time or +// throw at read time. +import { describe, expect, test } from "bun:test"; +import { type } from "arktype"; +import { CreateModelProvider, ModelProviderPlugin } from "@intx/types"; +import { parseModelProviderRow } from "../../../vendor/intx/db/src/parse-row"; +import type { modelProvider } from "../../../vendor/intx/db/src/schema"; + +describe("openai-responses plugin id contract", () => { + test("CreateModelProvider accepts a codex-shaped provider body", () => { + const parsed = CreateModelProvider({ + name: "codex", + plugin: "openai-responses", + baseURL: "https://chatgpt.com/backend-api", + credentialId: "cre_1", + }); + expect(!(parsed instanceof type.errors)).toBe(true); + }); + + test("ModelProviderPlugin still rejects an unknown plugin id", () => { + expect( + ModelProviderPlugin("openai-responses-future") instanceof type.errors, + ).toBe(true); + }); + + test("parseModelProviderRow round-trips the new plugin id", () => { + const row = { + id: "mpr_1", + tenantId: "tnt_1", + name: "xai-oauth", + plugin: "openai-responses", + baseURL: "https://cli-chat-proxy.grok.com/v1", + credentialId: "cre_1", + createdAt: new Date(), + updatedAt: new Date(), + } as unknown as typeof modelProvider.$inferSelect; + expect(parseModelProviderRow(row).plugin).toBe("openai-responses"); + }); +}); diff --git a/packages/seeding/test/seed.test.ts b/packages/seeding/test/seed.test.ts index e3691ca3f9..a04ed6669d 100644 --- a/packages/seeding/test/seed.test.ts +++ b/packages/seeding/test/seed.test.ts @@ -14,6 +14,7 @@ import { seedCatalog, ensureProvider, seedTenant, + PlaceholderCredentialError, SETUP_AGENT_ASSET_NAME, type SeedTenantArgs, type WorkflowPusher, @@ -2919,3 +2920,64 @@ describe("seedCatalog", () => { ).rejects.toThrow(HubApiError); }); }); + +describe("seedCatalog placeholderCredential gate", () => { + test("rejects a placeholder credential for an OAuth-only provider with a named error", async () => { + for (const provider of ["codex", "xai-oauth"] as const) { + let caught: unknown; + try { + await seedCatalog({ + api: fakeAPI(() => undefined), + cookies: [], + tenantId: TENANT_ID, + provider, + placeholderCredential: true, + log: () => {}, + }); + } catch (error) { + caught = error; + } + expect(caught).toBeInstanceOf(PlaceholderCredentialError); + expect((caught as Error).message).toContain(provider); + } + }); + + test("a real apiKey still seeds an OAuth-only provider — the gate is placeholder-only", async () => { + // Nothing past the gate runs (the fake API answers nothing), but the + // failure must be the hub's, not the gate's — the gate only exists to + // stop fake credentials, not real ones. + let caught: unknown; + try { + await seedCatalog({ + api: fakeAPI(() => undefined), + cookies: [], + tenantId: TENANT_ID, + provider: "codex", + apiKey: "real-token", + log: () => {}, + }); + } catch (error) { + caught = error; + } + expect(caught).not.toBeInstanceOf(PlaceholderCredentialError); + }); + + test("leaves api-key providers' placeholder behavior untouched", async () => { + let caught: unknown; + try { + await seedCatalog({ + api: fakeAPI(() => undefined), + cookies: [], + tenantId: TENANT_ID, + provider: "xai", + placeholderCredential: true, + log: () => {}, + }); + } catch (error) { + caught = error; + } + // The xai seed proceeds past the gate (and fails later against the + // fake API) — the gate never fires for an api-key provider. + expect(caught).not.toBeInstanceOf(PlaceholderCredentialError); + }); +}); diff --git a/packages/xai-provider/package.json b/packages/xai-provider/package.json index 335fa77a9f..ce24db8526 100644 --- a/packages/xai-provider/package.json +++ b/packages/xai-provider/package.json @@ -7,7 +7,8 @@ "type": "module", "sideEffects": false, "exports": { - ".": "./src/index.ts" + ".": "./src/index.ts", + "./constants": "./src/constants.ts" }, "scripts": { "typecheck": "tsc --noEmit", diff --git a/packages/xai-provider/tsconfig.src.json b/packages/xai-provider/tsconfig.src.json index e74c588ea8..23572def72 100644 --- a/packages/xai-provider/tsconfig.src.json +++ b/packages/xai-provider/tsconfig.src.json @@ -4,7 +4,6 @@ "exclude": ["src/**/*.test.ts", "src/**/*.test.tsx"], "compilerOptions": { "types": ["bun"], - "noEmit": true, "composite": true, "emitDeclarationOnly": true, "outDir": "dist", diff --git a/scripts/checks/browser-safe-subpaths.ts b/scripts/checks/browser-safe-subpaths.ts index 0ba62e47c8..1313ae3637 100644 --- a/scripts/checks/browser-safe-subpaths.ts +++ b/scripts/checks/browser-safe-subpaths.ts @@ -78,6 +78,12 @@ export const ENTRIES: readonly BrowserSafeEntry[] = [ // wire schema, and the pure lifecycle derivation — the same read // `apps/web`'s workflow detail page (`workflow-detail-api.ts`) needs. { package: "@corbits/workflows", subpath: "./client" }, + // The loopback OAuth descriptors in templates/connectors.ts source + // their client ids, redirect URIs, and scopes from these pure-data + // constant files so the descriptor identity and the adapter refresh + // path cannot drift. Plain data, zero imports — enforced here. + { package: "@corbits/codex-provider", subpath: "./constants" }, + { package: "@corbits/xai-provider", subpath: "./constants" }, ]; const DENYLIST_PATTERNS: readonly RegExp[] = [ diff --git a/scripts/checks/kill-dates.txt b/scripts/checks/kill-dates.txt index b41a1c6e66..fdfaa36a38 100644 --- a/scripts/checks/kill-dates.txt +++ b/scripts/checks/kill-dates.txt @@ -15,7 +15,7 @@ # package's VENDORED-FROM delta line in the same change. apps/sidecar | sawyer | 2026-11-03 vendor/intx/agent | sawyer | 2026-11-03 | 9dd132fda8d7aef9d70395de2d289abeb0361416b18a5079f219372df69d81a0 -vendor/intx/db | sawyer | 2026-11-03 | 360b03bd28eaa6c24092db15d63d3445f79d56bd3dbfc2968420d8a27abff57d +vendor/intx/db | sawyer | 2026-11-03 | 2d0f5ddde3a8f9313b7d274fcc9a62d743638cb3ea4078ad4176390bde859136 vendor/intx/harness | sawyer | 2026-11-03 | 399b4e8b3b18e8969e815427625e1bfc6746f31e791fb92cdbc2aaf2c5080dbf vendor/intx/hub-agent | sawyer | 2026-11-03 | 0a24a0cf85cc0483dbeee815d89847440e2fbd1242fdfd992941379a9acc2a4a vendor/intx/hub-api | sawyer | 2026-11-03 | 90aa5cddef828d77b62adb63186dcf82b5465926cafea1ab6b64bca263031b57 @@ -25,7 +25,7 @@ vendor/intx/inference | sawyer | 2026-11-03 | 81e1b31947cd8e6cac4e336d21d8a87ba0 vendor/intx/mail-memory | sawyer | 2026-11-03 | e281498352adbc2face631744ce9a02cb838c60a6c318961022904f0fd05d192 vendor/intx/mailbox | sawyer | 2026-11-03 | 926bfef1b60a4ba7e600252e9ad6e37c17d387a200f79c2cbc0b3ceb612000ea vendor/intx/mime | sawyer | 2026-11-03 | ddf7e626172350c6a447890affc6c746b7938e04ee53af8f1d9c8cffd97d1954 -vendor/intx/types | sawyer | 2026-11-03 | 71bd6493ae6df31abd9be8ff3d2feb79260d2aae3a29a5e275945dc92ea16a12 +vendor/intx/types | sawyer | 2026-11-03 | 5d9738e8b9e6a9193a759272350edb60a1cff09c430763a77d265855117b82c2 vendor/intx/workflow | sawyer | 2026-11-03 | c8f3a7a40d4ef429b58ba913c2faa27a5e7e98baf433dc57f864e12c907eb47a vendor/intx/workflow-deploy | sawyer | 2026-11-03 | 72a4f931ba5c404a091dc46a7ed60f5b0530a63535f7cacacfe31f95c397e473 vendor/intx/workflow-host | sawyer | 2026-11-03 | b9964010920cef377ba4be777c179d414c430c1709e665bbf8c5e4a413e249e9 diff --git a/templates/connectors.test.ts b/templates/connectors.test.ts index a4a6b2da1d..de99a37b93 100644 --- a/templates/connectors.test.ts +++ b/templates/connectors.test.ts @@ -1,4 +1,5 @@ import { describe, expect, test } from "bun:test"; +import { CODEX_SCOPES } from "@corbits/codex-provider/constants"; import { connectorDescriptors } from "@corbits/connections/registry"; import { mcpPresetByName, @@ -122,6 +123,105 @@ describe("CONNECTOR_REGISTRY", () => { expect(descriptor?.feedsTools).toEqual([]); }); + test("includes Codex and xai-oauth as oauth-loopback connectors, no probe", () => { + for (const id of ["codex", "xai-oauth"]) { + const descriptor = CONNECTOR_REGISTRY[id]; + expect(descriptor?.authKind).toBe("oauth-loopback"); + expect(descriptor?.probe).toBeUndefined(); + expect(descriptor?.oauth).toBeDefined(); + expect(descriptor?.oauth?.usesPKCE).toBe(true); + expect(descriptor?.oauth?.deploysDefaultWorkflows).toBe(true); + expect(descriptor?.feedsTools).toEqual([]); + // The OAuth client ids are the provider CLIs' public identifiers. + expect(descriptor?.oauth?.clientId?.({})).toBeDefined(); + } + }); + + test("pins each loopback provider to its own fixed redirect URI", () => { + const codexUrl = CONNECTOR_REGISTRY["codex"]?.oauth?.buildAuthorizeUrl({ + callbackUrl: "https://bench.example.com/ignored", + state: "s", + codeChallenge: "c", + clientId: "app_1", + }); + expect(codexUrl?.origin).toBe("https://auth.openai.com"); + expect(codexUrl?.searchParams.get("redirect_uri")).toBe( + "http://localhost:1455/auth/callback", + ); + expect(codexUrl?.searchParams.get("originator")).toBe("codex_cli_rs"); + // Pinned against the upstream Codex CLI's scope grant — the + // api.connectors.* scopes are what let the backend serve connector + // tools to this client, and the authorization server issues them only + // to the Codex client id. Asserted against the exported constant so + // the descriptor cannot drift from the adapter's refresh path. + expect(codexUrl?.searchParams.get("scope")?.split(" ")).toEqual( + CODEX_SCOPES, + ); + + const xaiUrl = CONNECTOR_REGISTRY["xai-oauth"]?.oauth?.buildAuthorizeUrl({ + callbackUrl: "https://bench.example.com/ignored", + state: "s", + codeChallenge: "c", + clientId: "client_1", + }); + expect(xaiUrl?.origin).toBe("https://auth.x.ai"); + expect(xaiUrl?.searchParams.get("redirect_uri")).toBe( + "http://127.0.0.1:1456/callback", + ); + expect(xaiUrl?.searchParams.get("scope")).toContain("grok-cli:access"); + }); + + test("exchanges a loopback code for tokens against the provider's own token endpoint", async () => { + const originalFetch = globalThis.fetch; + const requestedURLs: string[] = []; + globalThis.fetch = (async (input: RequestInfo | URL) => { + const url = String(input); + requestedURLs.push(url); + return new Response( + JSON.stringify({ + access_token: "at_1", + refresh_token: "rt_1", + expires_in: 3600, + }), + { status: 200 }, + ); + }) as typeof fetch; + try { + const result = await CONNECTOR_REGISTRY["codex"]?.oauth?.exchange({ + code: "abc", + codeVerifier: "ver", + redirectUri: "http://localhost:1455/auth/callback", + clientId: "app_1", + }); + expect(result).toMatchObject({ + ok: true, + apiKey: "at_1", + refreshToken: "rt_1", + }); + expect(requestedURLs).toEqual(["https://auth.openai.com/oauth/token"]); + expect(requestedURLs[0]).toContain("auth.openai.com"); + } finally { + globalThis.fetch = originalFetch; + } + }); + + test("reports a failed loopback exchange without throwing", async () => { + const originalFetch = globalThis.fetch; + globalThis.fetch = (async () => + new Response("nope", { status: 400 })) as typeof fetch; + try { + const result = await CONNECTOR_REGISTRY["xai-oauth"]?.oauth?.exchange({ + code: "abc", + codeVerifier: "ver", + redirectUri: "http://127.0.0.1:1456/callback", + clientId: "client_1", + }); + expect(result?.ok).toBe(false); + } finally { + globalThis.fetch = originalFetch; + } + }); + test("includes the tool connectors with the right feedsTools", () => { expect(CONNECTOR_REGISTRY["granola"]?.feedsTools).toEqual([ "@corbits/granola-tools", diff --git a/templates/connectors.ts b/templates/connectors.ts index a9de87f7ad..15b95ac099 100644 --- a/templates/connectors.ts +++ b/templates/connectors.ts @@ -64,6 +64,21 @@ import { siRailway, siSentry, } from "simple-icons"; +import { + CODEX_AUTHORIZE_EXTRA_PARAMS, + CODEX_AUTHORIZE_URL, + CODEX_CLIENT_ID, + CODEX_REDIRECT_URI, + CODEX_SCOPES, + CODEX_TOKEN_URL, +} from "@corbits/codex-provider/constants"; +import { + XAI_AUTHORIZE_URL, + XAI_CLIENT_ID, + XAI_REDIRECT_URI, + XAI_SCOPES, + XAI_TOKEN_URL, +} from "@corbits/xai-provider/constants"; import { exchangeCodeForKey, OPENROUTER_AUTH_URL, @@ -88,6 +103,8 @@ const INFERENCE_PROVIDER_DOCS_URL: Readonly< openai: "https://platform.openai.com/api-keys", "google-genai": "https://aistudio.google.com/apikey", xai: "https://console.x.ai", + "xai-oauth": "https://x.ai", + codex: "https://developers.openai.com/codex", "opencode-zen": "https://opencode.ai/zen", groq: "https://console.groq.com/keys", deepseek: "https://platform.deepseek.com/api_keys", @@ -131,6 +148,7 @@ function inferenceProviderDescriptors(): Record { const entries: Record = {}; for (const [id, config] of Object.entries(PROVIDER_TEST_CONFIG)) { if (id === "openrouter" || id === "huggingface") continue; + if (id === "codex" || id === "xai-oauth") continue; const providerId = id as SupportedCredentialProvider; // Ollama collects a URL, not a secret — it has no auth layer at all // (see `credential-test.ts`'s own `ollama` config entry). Every @@ -257,6 +275,175 @@ function inferenceProviderDescriptors(): Record { }, }, }; + // The two loopback OAuth providers (CL-7510). Their authorization + // servers only accept the fixed `http://localhost:` redirect URI + // the provider's own CLI registers (`authKind: "oauth-loopback"`), so + // `buildAuthorizeUrl` ignores the hub callback URL the generic + // mechanics would pass and pins the provider's own redirect instead. + // Every endpoint, client id, and scope list come from the provider + // packages' own constants subpath (browser-safe, pure data — the + // exchange closures stay plain `fetch` so the registry can ship in the + // web bundle), so the descriptor can never drift from the values the + // adapter's own refresh path uses. + entries["codex"] = { + id: "codex", + displayName: PROVIDER_TEST_CONFIG.codex.displayName, + authKind: "oauth-loopback", + credentialPlugin: "http", + docsUrl: INFERENCE_PROVIDER_DOCS_URL.codex, + feedsTools: [], + oauth: { + authorizeUrl: CODEX_AUTHORIZE_URL, + usesPKCE: true, + echoesState: true, + deploysDefaultWorkflows: true, + clientId: () => CODEX_CLIENT_ID, + buildAuthorizeUrl: ({ state, codeChallenge, clientId }) => { + const url = new URL(CODEX_AUTHORIZE_URL); + url.searchParams.set("client_id", clientId ?? CODEX_CLIENT_ID); + url.searchParams.set("redirect_uri", CODEX_REDIRECT_URI); + url.searchParams.set("response_type", "code"); + url.searchParams.set("scope", CODEX_SCOPES.join(" ")); + url.searchParams.set("state", state); + if (codeChallenge !== undefined) { + url.searchParams.set("code_challenge", codeChallenge); + } + url.searchParams.set("code_challenge_method", "S256"); + for (const [name, value] of Object.entries( + CODEX_AUTHORIZE_EXTRA_PARAMS, + )) { + url.searchParams.set(name, value); + } + return url; + }, + exchange: async ({ code, codeVerifier }) => { + try { + const response = await fetch(CODEX_TOKEN_URL, { + method: "POST", + headers: { "content-type": "application/x-www-form-urlencoded" }, + signal: AbortSignal.timeout(10_000), + body: new URLSearchParams({ + grant_type: "authorization_code", + code, + code_verifier: codeVerifier ?? "", + client_id: CODEX_CLIENT_ID, + redirect_uri: CODEX_REDIRECT_URI, + }).toString(), + }); + if (!response.ok) { + return { + ok: false, + message: `Codex rejected the login (status ${response.status})`, + }; + } + const tokens = (await response.json()) as { + access_token?: unknown; + refresh_token?: unknown; + expires_in?: unknown; + }; + if (typeof tokens.access_token !== "string") { + return { ok: false, message: "Codex returned no access token" }; + } + return { + ok: true, + apiKey: tokens.access_token, + ...(typeof tokens.refresh_token === "string" + ? { refreshToken: tokens.refresh_token } + : {}), + ...(typeof tokens.expires_in === "number" + ? { + expiresAt: new Date( + Date.now() + tokens.expires_in * 1000, + ).toISOString(), + } + : {}), + }; + } catch (cause) { + return { + ok: false, + message: `Codex token exchange failed: ${cause instanceof Error ? cause.message : String(cause)}`, + }; + } + }, + }, + }; + entries["xai-oauth"] = { + id: "xai-oauth", + displayName: PROVIDER_TEST_CONFIG["xai-oauth"].displayName, + authKind: "oauth-loopback", + credentialPlugin: "http", + docsUrl: INFERENCE_PROVIDER_DOCS_URL["xai-oauth"], + feedsTools: [], + oauth: { + authorizeUrl: XAI_AUTHORIZE_URL, + usesPKCE: true, + echoesState: true, + deploysDefaultWorkflows: true, + clientId: () => XAI_CLIENT_ID, + buildAuthorizeUrl: ({ state, codeChallenge, clientId }) => { + const url = new URL(XAI_AUTHORIZE_URL); + url.searchParams.set("client_id", clientId ?? XAI_CLIENT_ID); + url.searchParams.set("redirect_uri", XAI_REDIRECT_URI); + url.searchParams.set("response_type", "code"); + url.searchParams.set("scope", XAI_SCOPES.join(" ")); + url.searchParams.set("state", state); + if (codeChallenge !== undefined) { + url.searchParams.set("code_challenge", codeChallenge); + } + url.searchParams.set("code_challenge_method", "S256"); + return url; + }, + exchange: async ({ code, codeVerifier }) => { + try { + const response = await fetch(XAI_TOKEN_URL, { + method: "POST", + headers: { "content-type": "application/x-www-form-urlencoded" }, + signal: AbortSignal.timeout(10_000), + body: new URLSearchParams({ + grant_type: "authorization_code", + code, + code_verifier: codeVerifier ?? "", + client_id: XAI_CLIENT_ID, + redirect_uri: XAI_REDIRECT_URI, + }).toString(), + }); + if (!response.ok) { + return { + ok: false, + message: `xAI rejected the login (status ${response.status})`, + }; + } + const tokens = (await response.json()) as { + access_token?: unknown; + refresh_token?: unknown; + expires_in?: unknown; + }; + if (typeof tokens.access_token !== "string") { + return { ok: false, message: "xAI returned no access token" }; + } + return { + ok: true, + apiKey: tokens.access_token, + ...(typeof tokens.refresh_token === "string" + ? { refreshToken: tokens.refresh_token } + : {}), + ...(typeof tokens.expires_in === "number" + ? { + expiresAt: new Date( + Date.now() + tokens.expires_in * 1000, + ).toISOString(), + } + : {}), + }; + } catch (cause) { + return { + ok: false, + message: `xAI token exchange failed: ${cause instanceof Error ? cause.message : String(cause)}`, + }; + } + }, + }, + }; return entries; } diff --git a/templates/package.json b/templates/package.json index 57897da0e0..cc034cc6a5 100644 --- a/templates/package.json +++ b/templates/package.json @@ -18,9 +18,11 @@ "dependencies": { "@corbits/code-review": "workspace:*", "@corbits/connections": "workspace:*", + "@corbits/codex-provider": "workspace:*", "@corbits/jimmy-agent": "workspace:*", "@corbits/scout-agent": "workspace:*", "@corbits/workflows": "workspace:*", + "@corbits/xai-provider": "workspace:*", "arktype": "catalog:", "simple-icons": "^16.28.0" }, diff --git a/vendor/intx/db/src/model-source-resolution.test.ts b/vendor/intx/db/src/model-source-resolution.test.ts index c0a94220b6..ff00fcea1c 100644 --- a/vendor/intx/db/src/model-source-resolution.test.ts +++ b/vendor/intx/db/src/model-source-resolution.test.ts @@ -32,7 +32,7 @@ mock.module("./parse-row", () => ({ parseModelOfferingRow: (row: Record) => ({ ...row, capabilities: [], - quirks: null, + quirks: (row.quirks as unknown) ?? null, }), })); @@ -121,3 +121,74 @@ describe("buildSource servingRefresh seam (CL-7505)", () => { if (built.ok) expect(built.material.secret).toBe("plain:enc:old"); }); }); + +// CL-7510 local delta: provider-name → adapter-registry-key dispatch. The +// catalog `plugin` column stays the wire-format id ("openai-responses" for +// the loopback-OAuth providers, "openai-compatible" for Ollama); only the +// launched InferenceSource.provider is rewritten to the key the sidecar's +// DEFAULT_ADAPTER_MANIFEST registers the custom factory under. +describe("buildSource adapter registry-key dispatch (CL-7510)", () => { + resolvedRows["cred_1"] = row("api_key"); + + test("a codex offering dispatches under the codex registry key with its quirks threaded", async () => { + const codexOffering = { + offering: { + id: "off_codex", + priority: 0, + capabilities: [], + quirks: { + productName: "Codex", + environmentTagName: "codex_environment", + }, + }, + provider: { + id: "prov_codex", + name: "codex", + plugin: "openai-responses", + baseURL: "https://chatgpt.com/backend-api", + credentialId: "cred_1", + walletId: null, + }, + model: { canonicalName: "gpt-5.5" }, + } as unknown as Parameters[2]; + + const built = await buildSource({} as never, "ten_1", codexOffering, cipher); + expect(built.ok).toBe(true); + if (built.ok) { + expect(built.source.provider).toBe("codex"); + expect(built.source.quirks).toEqual({ + productName: "Codex", + environmentTagName: "codex_environment", + }); + expect(built.source.baseURL).toBe("https://chatgpt.com/backend-api"); + } + }); + + test("an xai-oauth offering dispatches under the xai-oauth registry key", async () => { + const xaiOffering = { + offering: { id: "off_xai", priority: 0, capabilities: [] }, + provider: { + id: "prov_xai", + name: "xai-oauth", + plugin: "openai-responses", + baseURL: "https://cli-chat-proxy.grok.com/v1", + credentialId: "cred_1", + walletId: null, + }, + model: { canonicalName: "grok-4.6" }, + } as unknown as Parameters[2]; + + const built = await buildSource({} as never, "ten_1", xaiOffering, cipher); + expect(built.ok).toBe(true); + if (built.ok) expect(built.source.provider).toBe("xai-oauth"); + }); + + test("every other provider keeps its plugin as the dispatch key", async () => { + const built = await buildSource({} as never, "ten_1", offering, cipher); + expect(built.ok).toBe(true); + if (built.ok) { + expect(built.source.provider).toBe("test-plugin"); + expect(built.material.providerKey).toBe("test-plugin"); + } + }); +}); diff --git a/vendor/intx/db/src/model-source-resolution.ts b/vendor/intx/db/src/model-source-resolution.ts index 3060497aa2..2370e5cffd 100644 --- a/vendor/intx/db/src/model-source-resolution.ts +++ b/vendor/intx/db/src/model-source-resolution.ts @@ -20,6 +20,19 @@ import { resolveCredentialById } from "./credential-resolution"; import { parseModelOfferingRow } from "./parse-row"; import { workflowDefinition } from "./schema/workflow-definitions"; +// CL-7510 local delta: providers whose serving adapter is a workbench-side +// custom factory registered under the provider's own name +// (`apps/sidecar/src/config.ts`'s DEFAULT_ADAPTER_MANIFEST). A catalog +// provider row named here launches under that registry key instead of its +// wire-format `plugin`, the same correction `withOllamaAdapterKey` used to +// make at the retired folded-runs launch seam. +const ADAPTER_REGISTRY_KEY_BY_PROVIDER_NAME: Readonly> = + { + ollama: "ollama", + codex: "codex", + "xai-oauth": "xai-oauth", + }; + /** * Why a single offering could not be turned into a launchable source. * `wallet_backed` providers are storable in the catalog but not launchable @@ -257,11 +270,19 @@ export async function buildSource( credentialAad(credential.id, "secret"), ); const parsed = parseModelOfferingRow(offering); + // CL-7510 local delta: adapter registry-key dispatch. The catalog + // `plugin` column stays the wire-format id; a provider whose custom + // factory is registered under its own name (the ollama precedent, + // `apps/sidecar/src/config.ts`'s DEFAULT_ADAPTER_MANIFEST — codex and + // xai-oauth ride the same mechanism) launches under that registry key, + // so an offering's `quirks` reaches the adapter that understands them + // instead of the built-in wire-format adapter. + const registryKey = ADAPTER_REGISTRY_KEY_BY_PROVIDER_NAME[provider.name] ?? provider.plugin; return { ok: true, source: { id: offering.id, - provider: provider.plugin, + provider: registryKey, baseURL: provider.baseURL, credentialId: provider.credentialId, model: model.canonicalName, @@ -270,7 +291,7 @@ export async function buildSource( }, material: { credentialId: provider.credentialId, - providerKey: provider.plugin, + providerKey: registryKey, origin: provider.baseURL, secret, }, diff --git a/vendor/intx/db/src/schema/catalog.ts b/vendor/intx/db/src/schema/catalog.ts index d5ae5b307f..77a0cabc1c 100644 --- a/vendor/intx/db/src/schema/catalog.ts +++ b/vendor/intx/db/src/schema/catalog.ts @@ -43,8 +43,17 @@ export const modelProvider = pgTable( .notNull() .references(() => tenant.id, { onDelete: "cascade" }), name: text("name").notNull(), + // Local delta (CL-7510): "openai-responses" — the workbench's + // loopback-OAuth providers (Codex, xai-oauth) ride the Responses + // protocol; must stay in sync with @intx/types' modelProviderPlugins. plugin: text("plugin", { - enum: ["anthropic", "openai", "openai-compatible", "google-genai"], + enum: [ + "anthropic", + "openai", + "openai-compatible", + "google-genai", + "openai-responses", + ], }).notNull(), baseURL: text("base_url").notNull(), // A provider authenticates via exactly one of credential or wallet (the diff --git a/vendor/intx/types/src/catalog.ts b/vendor/intx/types/src/catalog.ts index 47d1c37178..222ccb0592 100644 --- a/vendor/intx/types/src/catalog.ts +++ b/vendor/intx/types/src/catalog.ts @@ -7,6 +7,9 @@ export const modelProviderPlugins = [ "openai", "openai-compatible", "google-genai", + // Local delta (CL-7510): the workbench's loopback-OAuth providers (Codex, + // xai-oauth) speak OpenAI's Responses protocol and ride this plugin id. + "openai-responses", ] as const; export type ModelProviderPlugin = (typeof modelProviderPlugins)[number]; export const ModelProviderPlugin = type