diff --git a/docs/identity-handoff.md b/docs/identity-handoff.md index ed4e86558..0fdc9e7ee 100644 --- a/docs/identity-handoff.md +++ b/docs/identity-handoff.md @@ -15,13 +15,15 @@ No package, image, release asset, or source archive is published by this change. | Legal copyright owner | Luntraa, Inc. | | Release API | `https://api.diffex.ai/api/latest-version`, owned by `diffex-cloud` | | Model catalog API | `https://api.diffex.ai/api/models/providers/{providerId}`, owned by `diffex-cloud` | -| Radius services | Approved external integrations recorded in the candidate handoff | +| Radius services | Removed; prohibited in development, testing, evaluation, and production | | Security contact | Existing private access to a Diffex repository owner; no dedicated mailbox or public reporting channel claimed | | Issue authorization | Write/admin permission on the current Diffex repository | Diffex and Luntraa, Inc. are unrelated to Earendil. The former staff-team authorization, security mailbox reference, announcement, and product affiliation wording have been removed. -The owner explicitly retained OpenAI Codex and xAI OAuth in addition to the release audit's Radius and provider-attribution integrations. +The owner explicitly retained OpenAI Codex and xAI OAuth and provider attribution. +Radius is removed; the older release audit does not authorize its use. +See [migration and validation](release/radius-removal.md). The catalog authority is now Diffex; see [the catalog contract](model-catalog.md). The exact residual inventory is [identity-residual-allowlist.md](../identity-residual-allowlist.md). diff --git a/docs/release/candidate-handoff.md b/docs/release/candidate-handoff.md index c60ed370a..9f5561340 100644 --- a/docs/release/candidate-handoff.md +++ b/docs/release/candidate-handoff.md @@ -6,12 +6,17 @@ The accepted implementation is [`33704b32c5d17b849c013834fd1c56d65de843fd`](http The follow-up evidence commit changes only documents; [the artifact manifest](candidate-artifacts.json) pins the tested implementation and bytes. The companion cloud change is stacked on PR #147. +Current policy supersedes this historical candidate's Radius approval. +Radius is removed and must not be used in development, testing, evaluation, or production. +The pinned JSON manifests and identity reports below describe historical bytes containing Radius and must not be reused as release candidates. +See [Radius removal](radius-removal.md) for current migration and validation. + ## Reconciled decisions The canonical private source, package map, executable, hosted image identity, and Luntraa, Inc. copyright are recorded in [the identity handoff](../identity-handoff.md). -The release audit approves Pi-supported catalogs, Radius gateway/OAuth, and provider attribution. +The historical release audit approved external catalogs and provider attribution. The owner additionally instructed this implementation to retain OpenAI Codex and xAI OAuth. -The earlier backend plan's proposed replacement catalogs, Diffex Radius service, and replacement provider registrations are therefore not prerequisites for this candidate. +Radius gateway/OAuth approval has been revoked; no replacement Radius service is planned. No Diffex ownership of those external services or registrations is asserted. The obsolete security/staff exceptions are removed. @@ -34,18 +39,15 @@ The [exact residual inventory](../../identity-residual-allowlist.md) replaces th | Operation | Endpoint or authority | Policy | | ------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- | | Release discovery | `https://api.diffex.ai/api/latest-version` | Diffex Cloud public Worker; anonymous; fixed canonical package identity; no redirects | -| Diffex account login and product APIs | `https://api.diffex.ai`, existing Render/WorkOS routes | Separate account credentials; never implicitly reused as Radius credentials | +| Diffex account login and product APIs | `https://api.diffex.ai`, existing Render/WorkOS routes | Separate account credentials | | Built-in catalog refresh | `https://pi.dev/api/models/providers/` | Explicitly retained external catalogs for configured providers | -| Radius config and inference | `https://radius.pi.dev/v1/config`, gateway-selected message base URL | Explicitly retained Radius trust boundary; provider credentials only | -| Radius OAuth | Gateway `/v1/oauth` discovery and discovered authorization/device/token URLs; client `pi-gateway` | Retained registration; the earlier audit observed issuer `https://radius.earendil.com`; live discovery was not repeated | | OpenAI Codex | `https://auth.openai.com`, `https://chatgpt.com/backend-api`; client `app_EMoamEEZ73f0CkXaXp7hrann` | Explicit owner retention of OAuth and provider attribution | | xAI subscription | `https://auth.x.ai/oauth2/device/code`, `https://auth.x.ai/oauth2/token`; client `b1a00492-073a-47ea-816f-4c329264a828` | Explicit owner retention of OAuth and provider attribution | | Other model providers | Selected built-in provider endpoints or explicitly configured custom endpoints | Provider-specific credentials and model traffic | | Optional attribution | OpenRouter, NVIDIA NIM, Cloudflare | Existing optional provider headers; default off; separate from removed install reporting | | OpenCode attribution | Selected OpenCode endpoint | Existing client/session headers retained independently of optional attribution | -Radius discovery and model configurations remain trusted inputs that can supply endpoint URLs. -Retaining them accepts that existing trust boundary; this change does not implement the older plan's endpoint allowlisting or cache-origin migration proposal. +Radius discovery, authentication, and inference are no longer supported or allowed egress. Custom gateways remain explicit user configuration. No runtime egress permission extends to install-report routes, upstream update discovery, security reporting to upstream contacts, or external organization authorization. GitHub access remains available for explicitly requested extension/package operations and source-host operations. @@ -55,7 +57,8 @@ GitHub access remains available for explicitly requested extension/package opera The frozen built-in catalog was generated at `2026-08-25T03:02:44.635Z` with structure hash `888155e376e18c5eb02a88c79c374edaa9a517730bfe17f82f76ca51c96cca12`. The local build validates and uses that checked-in snapshot without refreshing model metadata. `packages/ai/src/providers/data/.manifest.json` records every provider shard digest. -The static provider IDs in that manifest, plus dynamic `radius`, remain supported by the source adapters. +The static provider IDs in that historical manifest describe the candidate's catalog. +Dynamic `radius` is no longer supported by the source adapters. The installed artifact manifest lists all 40 provider IDs and display names; listing a provider does not claim live authentication or inference acceptance. Configured built-in providers retain the external overlay and its four-hour refresh interval. @@ -76,7 +79,8 @@ No deployment or production metadata mutation was needed. ## Validation and explicit exclusions The full `./test.sh` suite passed without provider credentials. -This includes the retained Radius, OpenAI Codex, xAI, remote catalog, provider-attribution, install-report removal, and updater regressions. +That historical run included Radius, OpenAI Codex, xAI, remote catalog, provider-attribution, install-report removal, and updater regressions. +Current Radius tests assert rejection without service access. The new issue-workflow tests first reproduced rejection of Diffex maintainers because an inherited token was absent, then passed after repository-scoped authorization replaced it. The CLI quality gate and exact identity scan passed. The cloud/CLI contract checker passed with no public contract changes. diff --git a/docs/release/candidate-rehearsal.md b/docs/release/candidate-rehearsal.md index 0e56fe935..6a9b1574b 100644 --- a/docs/release/candidate-rehearsal.md +++ b/docs/release/candidate-rehearsal.md @@ -1,5 +1,8 @@ # Candidate release rehearsal +This is historical evidence for the pinned revision below. +Its Radius-containing artifacts are superseded and must not be reused; see [Radius removal](radius-removal.md). + The rehearsal validates the approved npm-only distribution without publishing a release. The current public inventory is `@diffexai/diffex`, with internal runtime modules bundled inside its tarball. The older eleven-package and standalone acceptance requirements are superseded by the owner-approved [distribution policy](distribution-policy.json). @@ -91,7 +94,8 @@ These application-level guards and event logs are not a whole-host packet captur Catalog preparation contacts its approved catalog inputs; the local refresh encountered a transport failure, so the existing validated local snapshot was used for source gates while the native build retained its freshly hydrated snapshot. The source identity gate accepts exactly seventeen reviewed occurrences across 915 files. -Inspection of every final tarball member finds sixteen identity matches in nine files, all mapping to approved Radius, catalog, OAuth, and provider-attribution entries. +Inspection of that historical tarball found sixteen identity matches in nine files, then approved as Radius, catalog, OAuth, and provider-attribution entries. +Radius approval has since been revoked for every environment. The final payload gate verifies required notices, runtime-file hashes, the documentation allowlist, and absence of prohibited source and embedded maps. Image inspection accepts 1,414 project files across twelve runtime workspaces and thirty approved documentation assets. All isolated installation prefixes, caches, homes, mock sessions, fixture servers, and runtime containers are removed by their harnesses. diff --git a/docs/release/client-canaries.md b/docs/release/client-canaries.md index c71436ce4..f216a6b2f 100644 --- a/docs/release/client-canaries.md +++ b/docs/release/client-canaries.md @@ -68,7 +68,8 @@ npm --prefix install -g --ignore-scripts --min-release-age=0 @ No package-manager process executed, no uninstall was attempted, and no installed software changed. The preload intercepted the separate `https://pi.dev/api/report-install` call during non-offline startup and returned a controlled failure without forwarding it. There was no Pi version-check traffic or any Pi socket connection. -Install telemetry, model catalogs, and Radius remain separate migration work. +At the time of this historical run, install telemetry, model catalogs, and Radius remained separate migration work. +Radius is now removed and prohibited in all environments; see [Radius removal](radius-removal.md). ## Regression and artifact checks diff --git a/docs/release/provider-branding-validation.md b/docs/release/provider-branding-validation.md index 0d697c929..a6b8c27e9 100644 --- a/docs/release/provider-branding-validation.md +++ b/docs/release/provider-branding-validation.md @@ -4,6 +4,7 @@ Diffex controls the local callback presentation and default application attribut Registered OAuth client IDs, provider consent pages, authentication protocols, stored credentials, inference endpoints, and payloads remain unchanged. The separate Diffex account feature is excluded. Radius was excluded from the original branding change and subsequently removed from the built-in provider and authentication integrations. +The [Radius removal record](radius-removal.md) supersedes its historical approval and documents configuration rejection and offline validation. ## Implemented defaults diff --git a/docs/release/radius-removal.md b/docs/release/radius-removal.md new file mode 100644 index 000000000..6f9555661 --- /dev/null +++ b/docs/release/radius-removal.md @@ -0,0 +1,68 @@ +# Radius removal + +Radius must not be used in development, testing, evaluation, or production. +The removal supersedes the older candidate handoff and rehearsal approvals. +Historical JSON evidence remains a record of the original bytes, not an approved release candidate or permission to use Radius. + +## Configuration behavior + +The common rejection explains that Radius support was removed and requires selecting another provider. +The runtime rejects Radius registrations before changing provider or credential state. +It does not migrate Radius traffic to another provider automatically. + +| Existing state or request | Result | +| --- | --- | +| Saved API key, OAuth credential, environment key, or model cache | Remains inactive; no refresh, discovery, authentication, or inference request | +| `providers.radius` in `models.json` | Configuration error naming the entry and file, including configurations without OAuth | +| `oauth: "radius"` under another provider ID | Same configuration error before schema validation or provider composition | +| CLI provider/model selection or Radius model scope | Explicit removal error before inference | +| Saved default provider | Startup fails with instructions to update `defaultProvider` and `defaultModel` in `settings.json` | +| Resumed Radius session | Startup fails with instructions to choose a replacement using `--model provider/model` | +| Auth check or credential printing | Explicit removal error; no Radius credential is exported | +| Extension or SDK provider registration | Rejected, including case variants of the removed provider ID | +| `/login` | Radius absent from both subscription and API-key selectors | +| `/logout` with a saved Radius credential | Local credential deletion remains available without contacting the former service | + +Invalid Radius entries invalidate the custom configuration file; built-in providers remain registered. +Unselected stale credentials and caches do not prevent use of supported providers. +See the [user migration steps](../../packages/coding-agent/docs/providers.md#removed-providers). + +## Source and artifact review + +The built-in provider factory, OAuth implementation and loader, environment-key mapping, provider type, and default-model integration were removed in `3cd84966f`. +`f1a3608e1` closes custom-configuration and registration paths and adds actionable selection errors. +`19d038f8b` adds the archive gate and checks legacy CLI and session behavior. +`27b844ad3` presents expected startup rejections without a Node stack trace. +The generic `diffex-messages` transport remains available for other backends, with neutral local test fixtures. + +The remaining source references identify the removed provider for rejection or assert that access is unavailable. +Documentation records migration policy or explicitly superseded historical evidence. +CSS `border-radius` properties are unrelated. +The evaluation configuration and workflows contain no Radius integration. + +The distribution checker rejects former provider/OAuth module paths and service, environment-key, or client-registration markers in executable payloads and declarations. +The same inspection applies to bundled internal workspaces and nested archives. +The removal explanation itself is permitted. +Old ignored local build outputs were cleaned and regenerated offline rather than edited manually. + +## Validation + +Validation uses synthetic credentials, local fixtures, and isolated configuration directories. +No Radius service, real provider credential, paid inference, or live Radius compatibility test is used. + +- `npm run check` passes, including identity, dependency, lockfile, TypeScript, and browser checks. +- `./test.sh` passes with provider credentials removed and live E2E tests disabled. +- Focused AI tests cover provider discovery, environment-key removal, registration rejection, and the generic message transport. +- Focused coding-agent tests cover stale credentials/caches, configuration, registration, selection, defaults, resumption, auth commands, both login methods, and local logout cleanup. +- The distribution payload suite passes all 47 tests, including stale modules and markers in renamed or bundled payloads. +- Fresh offline compilation and reviewed packing produce a local tarball that passes payload inspection. +- A clean npm consumer installs that tarball with lifecycle scripts disabled and verifies every runtime hash against the reviewed archive. +- The installed executable passes help, version, model listing, controlled print and terminal prompt/reply, Radius search exclusion in both login methods, and clean exit. +- Installed legacy configuration, defaults, resumed sessions, and auth commands produce the removal explanation. + +The terminal fixture answers the logical Diffex version request locally and rejects unexpected provider requests and socket connections. +This establishes removal and local package behavior; it does not publish a release or establish unrelated live-provider compatibility. + +The tested runtime source is `27b844ad3` on macOS arm64 with Node 22.19.0 and npm 10.9.3. +The local `0.3.3` tarball contains 1,243 payload files and has SHA-256 `7bacd1bf5fdebb4a0097ad683cf6cf26fc497b0a469df0e72231f318be5bfb71`. +This checksum identifies validation bytes only, not a published replacement for an existing version. diff --git a/packages/ai/src/api/diffex-messages.ts b/packages/ai/src/api/diffex-messages.ts index 9849806c1..4a8cdde59 100644 --- a/packages/ai/src/api/diffex-messages.ts +++ b/packages/ai/src/api/diffex-messages.ts @@ -4,8 +4,7 @@ * Streams Diffex's own message protocol directly to a backend: the request is a * single POST of `{ model, context, options }` to `/messages`, the * response is an SSE stream of serialized assistant-message events plus a - * terminal `done`/`error` event. This is the wire protocol spoken by the - * Radius gateway, but any backend implementing it can be used, e.g. via a + * terminal `done`/`error` event. Any backend implementing this protocol can be used via a * models.json custom provider with `"api": "diffex-messages"`. */ diff --git a/packages/ai/src/models.ts b/packages/ai/src/models.ts index 1f06228f5..bd8db51e5 100644 --- a/packages/ai/src/models.ts +++ b/packages/ai/src/models.ts @@ -14,6 +14,7 @@ import type { ProviderAuth, } from "./auth/types.ts"; import { InMemoryModelsStore, type ModelsStore, type ModelsStoreEntry } from "./models-store.ts"; +import { getRemovedProviderMessage } from "./providers/removed.ts"; import type { Api, ApiStreamOptions, @@ -267,6 +268,8 @@ class ModelsImpl implements MutableModels { } setProvider(provider: Provider): void { + const removed = getRemovedProviderMessage(provider.id); + if (removed) throw new ModelsError("provider", removed); this.supersedeProviderRefresh(provider.id); this.providers.set(provider.id, provider); } @@ -557,7 +560,8 @@ class ModelsImpl implements MutableModels { const signal = operationSignal(interaction.signal); signal.throwIfAborted(); const provider = this.providers.get(providerId); - if (!provider) throw new ModelsError("provider", `Unknown provider: ${providerId}`); + if (!provider) + throw new ModelsError("provider", getRemovedProviderMessage(providerId) ?? `Unknown provider: ${providerId}`); const method = type === "oauth" ? provider.auth.oauth : provider.auth.apiKey; if (!method?.login) { throw new ModelsError("auth", `${provider.name} does not support ${type} login`); @@ -633,7 +637,10 @@ class ModelsImpl implements MutableModels { private requireProvider(model: Model): Provider { const provider = this.providers.get(model.provider); if (!provider) { - throw new ModelsError("provider", `Unknown provider: ${model.provider}`); + throw new ModelsError( + "provider", + getRemovedProviderMessage(model.provider) ?? `Unknown provider: ${model.provider}`, + ); } return provider; } @@ -777,6 +784,8 @@ export interface CreateProviderOptions { * produces a stream error. */ export function createProvider(input: CreateProviderOptions): Provider { + const removed = getRemovedProviderMessage(input.id); + if (removed) throw new ModelsError("provider", removed); const baselineModels = input.models; let dynamicModels: readonly Model[] = []; const fetchModels = input.fetchModels; diff --git a/packages/ai/src/providers/removed.ts b/packages/ai/src/providers/removed.ts new file mode 100644 index 000000000..b9b2bf7fa --- /dev/null +++ b/packages/ai/src/providers/removed.ts @@ -0,0 +1,6 @@ +/** Explain removed providers without loading their former integrations. */ +export function getRemovedProviderMessage(providerId: string): string | undefined { + return providerId.trim().toLowerCase() === "radius" + ? "Radius support has been removed. Remove its configuration and select another provider." + : undefined; +} diff --git a/packages/ai/test/diffex-messages.test.ts b/packages/ai/test/diffex-messages.test.ts index 60b0ae429..51fea862f 100644 --- a/packages/ai/test/diffex-messages.test.ts +++ b/packages/ai/test/diffex-messages.test.ts @@ -2,6 +2,7 @@ import { createServer, type IncomingMessage, type Server, type ServerResponse } import type { AddressInfo } from "node:net"; import { afterEach, describe, expect, it } from "vitest"; import { type DiffexMessagesOptions, stream, streamSimple } from "../src/api/diffex-messages.ts"; +import { getApiProvider } from "../src/compat.ts"; import type { Api, AssistantMessageEvent, Context, Model, StopReason } from "../src/types.ts"; type RecordedRequest = { @@ -68,9 +69,9 @@ async function startServer(options: ResponderOptions): Promise<{ baseUrl: string function createModel(baseUrl: string): Model<"diffex-messages"> { return { id: "auto", - name: "Radius Auto", + name: "Test Backend", api: "diffex-messages", - provider: "radius", + provider: "test-backend", baseUrl, reasoning: false, input: ["text"], @@ -137,7 +138,7 @@ describe("diffex-messages", () => { expect(message.usage).toEqual(usage); expect(message.responseId).toBe("resp_1"); expect(message.model).toBe("auto"); - expect(message.provider).toBe("radius"); + expect(message.provider).toBe("test-backend"); expect(message.content).toEqual([ { type: "text", text: "Hello", textSignature: undefined }, { type: "toolCall", id: "call_1", name: "read", arguments: { path: "a.txt" } }, @@ -159,7 +160,7 @@ describe("diffex-messages", () => { it("appends debug=1 and reports response headers via onResponse", async () => { const { baseUrl, requests } = await startServer({ - headers: { "x-pi-gateway-upstream-provider": "anthropic" }, + headers: { "x-test-backend-upstream-provider": "anthropic" }, events: [{ type: "done", reason: "stop", usage }], }); const model = createModel(baseUrl); @@ -176,7 +177,7 @@ describe("diffex-messages", () => { expect(message.stopReason).toBe("stop"); expect(requests[0].url).toBe("/v1/messages?debug=1"); - expect(observedHeaders?.["x-pi-gateway-upstream-provider"]).toBe("anthropic"); + expect(observedHeaders?.["x-test-backend-upstream-provider"]).toBe("anthropic"); }); it("surfaces backend error responses with diagnostics", async () => { @@ -237,7 +238,6 @@ describe("diffex-messages", () => { describe("diffex-messages api registration", () => { it("is registered as a builtin api provider", async () => { - const { getApiProvider } = await import("../src/compat.ts"); expect(getApiProvider("diffex-messages")).toBeDefined(); }); diff --git a/packages/ai/test/removed-provider.test.ts b/packages/ai/test/removed-provider.test.ts new file mode 100644 index 000000000..e2bace3c2 --- /dev/null +++ b/packages/ai/test/removed-provider.test.ts @@ -0,0 +1,12 @@ +import { describe, expect, it } from "vitest"; +import { createModels, createProvider } from "../src/models.ts"; +import { anthropicProvider } from "../src/providers/anthropic.ts"; + +describe("removed provider registration", () => { + it.each(["radius", "Radius", " radius "])("rejects %s before invoking any provider hooks", (id) => { + const models = createModels(); + expect(() => models.setProvider({ ...anthropicProvider(), id })).toThrow("Radius support has been removed"); + expect(() => createProvider({ id, auth: {}, models: [], api: {} })).toThrow("Radius support has been removed"); + expect(models.getProviders()).toEqual([]); + }); +}); diff --git a/packages/coding-agent/docs/providers.md b/packages/coding-agent/docs/providers.md index e2a22dc2d..6e7808d07 100644 --- a/packages/coding-agent/docs/providers.md +++ b/packages/coding-agent/docs/providers.md @@ -11,6 +11,7 @@ Diffex supports subscription-based providers via OAuth and API key providers via - [llama.cpp](#llamacpp) - [Custom Providers](#custom-providers) - [Resolution Order](#resolution-order) +- [Removed Providers](#removed-providers) ## Subscriptions @@ -329,3 +330,20 @@ When resolving credentials for a provider: 2. `auth.json` entry (API key or OAuth token) 3. Environment variable 4. Custom provider keys from `models.json` + +## Removed Providers + +Radius is removed and must not be used in development, testing, evaluation, or production. +It is absent from subscription and API-key login menus, provider discovery, and model catalogs. +Custom registrations under the `radius` provider ID are rejected. + +Existing Radius selections explain that support was removed and require a replacement provider. +Remove `providers.radius` and any provider with `"oauth": "radius"` from `~/.diffex/agent/models.json`. +These entries invalidate the custom configuration file until removed; built-in providers remain available. +Replace Radius defaults in global or project `settings.json`, including `defaultProvider`, `defaultModel`, and any Radius entries in `enabledModels`. +Resume an old Radius session with an explicit `--model provider/model` to choose its replacement. + +Saved Radius credentials and cached models remain on disk but cannot restore access or trigger token refresh, catalog discovery, or inference. +Use `/logout` to delete a saved Radius credential locally without contacting the former service, or remove its entry from `auth.json` manually. +Remove the unused Radius entry from `models-store.json` and unset the former `RADIUS_API_KEY` environment variable. +Configure another supported provider through `/login`, then select its model with `/model`. diff --git a/packages/coding-agent/src/cli/auth-check.ts b/packages/coding-agent/src/cli/auth-check.ts index 1807f486e..d2b1e7600 100644 --- a/packages/coding-agent/src/cli/auth-check.ts +++ b/packages/coding-agent/src/cli/auth-check.ts @@ -1,4 +1,5 @@ import type { CredentialStore } from "@diffexai/diffex-ai"; +import { getRemovedProviderMessage } from "@diffexai/diffex-ai/providers/removed"; import { resolveCliModel } from "../core/model-resolver.ts"; import { ModelRuntime } from "../core/model-runtime.ts"; import { InMemoryCodingAgentModelsStore } from "../core/models-store.ts"; @@ -34,6 +35,8 @@ export async function checkProviderAuth( provider = resolved.model.provider; } if (!provider) throw new AuthCommandError("Unable to resolve an auth provider"); + const removed = getRemovedProviderMessage(provider); + if (removed) throw new AuthCommandError(removed); if (modelRuntime.getError()) { return { status: "invalid", provider, reason: "invalid_state" }; } @@ -58,6 +61,8 @@ export async function getProviderCredential( credentials: CredentialStore, options: { refresh: boolean }, ): Promise { + const removed = getRemovedProviderMessage(providerId); + if (removed) throw new AuthCommandError(removed); const credential = await credentials.read(providerId); if (!options.refresh && credential?.type === "oauth") return credential.access; return getAuthCredential(await modelRuntime.getAuth(providerId)); diff --git a/packages/coding-agent/src/cli/credential-print.ts b/packages/coding-agent/src/cli/credential-print.ts index e195e157e..5d0e05856 100644 --- a/packages/coding-agent/src/cli/credential-print.ts +++ b/packages/coding-agent/src/cli/credential-print.ts @@ -1,4 +1,5 @@ import type { Api, CredentialInfo, Model } from "@diffexai/diffex-ai"; +import { getRemovedProviderMessage } from "@diffexai/diffex-ai/providers/removed"; import { resolveCliModel } from "../core/model-resolver.ts"; import type { ModelRuntime } from "../core/model-runtime.ts"; import type { Args } from "./args.ts"; @@ -22,6 +23,8 @@ export async function resolveCredentialForPrint( signal?: AbortSignal, ): Promise { const { provider: cliProvider, model: cliModel } = validateAuthCommandArgs(args, kind); + const removed = getRemovedProviderMessage(cliProvider ?? cliModel?.split("/")[0] ?? ""); + if (removed) throw new AuthCommandError(removed); const credentialTypes = new Map( (await modelRuntime.listCredentials({ signal })).map((credential) => [credential.providerId, credential.type]), ); diff --git a/packages/coding-agent/src/core/model-config.ts b/packages/coding-agent/src/core/model-config.ts index 418ba7f8d..55e317d47 100644 --- a/packages/coding-agent/src/core/model-config.ts +++ b/packages/coding-agent/src/core/model-config.ts @@ -1,6 +1,7 @@ /** Immutable, credential-blind models.json snapshot. */ import { readFile } from "node:fs/promises"; +import { getRemovedProviderMessage } from "@diffexai/diffex-ai/providers/removed"; import { type Static, Type } from "typebox"; import { Compile } from "typebox/compile"; import type { TLocalizedValidationError } from "typebox/error"; @@ -267,6 +268,25 @@ export class ModelConfig { ); } + if (typeof parsed === "object" && parsed !== null && "providers" in parsed) { + const entries = parsed.providers; + if (typeof entries === "object" && entries !== null && !Array.isArray(entries)) { + for (const [providerId, provider] of Object.entries(entries)) { + const oauth = + typeof provider === "object" && provider !== null && "oauth" in provider ? provider.oauth : undefined; + const removed = + getRemovedProviderMessage(providerId) ?? + (typeof oauth === "string" ? getRemovedProviderMessage(oauth) : undefined); + if (removed) { + return new ModelConfig( + new Map(), + `Invalid models.json: providers.${providerId}: ${removed}\n\nFile: ${path}`, + ); + } + } + } + } + if (!validateModelsConfig.Check(parsed)) { const errors = validateModelsConfig diff --git a/packages/coding-agent/src/core/model-resolver.ts b/packages/coding-agent/src/core/model-resolver.ts index 12735b4ae..7b5994164 100644 --- a/packages/coding-agent/src/core/model-resolver.ts +++ b/packages/coding-agent/src/core/model-resolver.ts @@ -8,8 +8,10 @@ import { type AuthOperationOptions, type KnownProvider, type Model, + ModelsError, modelsAreEqual, } from "@diffexai/diffex-ai"; +import { getRemovedProviderMessage } from "@diffexai/diffex-ai/providers/removed"; import chalk from "chalk"; import { minimatch } from "minimatch"; import { isValidThinkingLevel } from "../cli/args.ts"; @@ -286,6 +288,8 @@ export function resolveModelScopeFromModels( const diagnostics: ModelScopeDiagnostic[] = []; for (const pattern of patterns) { + const removed = getRemovedProviderMessage(pattern.split("/")[0]); + if (removed) throw new ModelsError("provider", removed); // Check if pattern contains glob characters if (pattern.includes("*") || pattern.includes("?") || pattern.includes("[")) { // Extract optional thinking level suffix (e.g., "provider/*:high") @@ -408,6 +412,8 @@ export function resolveCliModel(options: { modelRuntime: ModelRuntime; }): ResolveCliModelResult { const { cliProvider, cliModel, cliThinking, modelRuntime } = options; + const removed = getRemovedProviderMessage(cliProvider ?? cliModel?.split("/")[0] ?? ""); + if (removed) return { model: undefined, warning: undefined, error: removed }; if (!cliModel) { return { model: undefined, warning: undefined, error: undefined }; @@ -667,6 +673,9 @@ export async function findInitialModel(options: { } // 3. Try saved default from settings if auth is configured. + const removedDefault = defaultProvider ? getRemovedProviderMessage(defaultProvider) : undefined; + if (removedDefault) + throw new ModelsError("provider", `${removedDefault} Update defaultProvider and defaultModel in settings.json.`); if (defaultProvider && defaultModelId) { const found = modelRuntime.getModel(defaultProvider, defaultModelId); if (found && modelRuntime.hasConfiguredAuth(found.provider)) { @@ -709,6 +718,8 @@ export async function restoreModelFromSession( shouldPrintMessages: boolean, modelRuntime: ModelRuntime, ): Promise<{ model: Model | undefined; fallbackMessage: string | undefined }> { + const removed = getRemovedProviderMessage(savedProvider); + if (removed) throw new ModelsError("provider", `${removed} Resume with an explicit --model provider/model.`); const restoredModel = modelRuntime.getModel(savedProvider, savedModelId); // Check if restored model exists and still has auth configured diff --git a/packages/coding-agent/src/core/model-runtime.ts b/packages/coding-agent/src/core/model-runtime.ts index 7b2ed4f85..27c1b35e7 100644 --- a/packages/coding-agent/src/core/model-runtime.ts +++ b/packages/coding-agent/src/core/model-runtime.ts @@ -37,6 +37,7 @@ import { type StreamOptions, } from "@diffexai/diffex-ai"; import * as builtinProviderCatalog from "@diffexai/diffex-ai/providers/all"; +import { getRemovedProviderMessage } from "@diffexai/diffex-ai/providers/removed"; import { getAgentDir } from "../config.ts"; import { operationSignal, raceWithAbortSignal } from "../utils/abort.ts"; import { AuthStorage as DefaultAuthStorage } from "./auth-storage.ts"; @@ -515,6 +516,8 @@ export class ModelRuntime implements Models { setRuntimeApiKey(providerId: string, apiKey: string, options: AuthOperationOptions = {}): Promise { const signal = operationSignal(options.signal); return this.enqueueCredentialOperation(providerId, signal, async () => { + const removed = getRemovedProviderMessage(providerId); + if (removed) throw new ModelsError("provider", removed); this.credentials.setRuntimeApiKey(providerId, apiKey); await this.synchronizeCredentialState( providerId, @@ -538,6 +541,7 @@ export class ModelRuntime implements Models { } getProviderAuthStatus(providerId: string): AuthStatus { + if (getRemovedProviderMessage(providerId)) return { configured: false }; if (this.credentials.hasRuntimeApiKey(providerId)) return { configured: true, source: "runtime" }; if (this.snapshot.storedProviders.has(providerId)) return { configured: true, source: "stored" }; const configured = configuredRequestAuthStatus( @@ -558,7 +562,11 @@ export class ModelRuntime implements Models { options: Omit & ProviderRequestOptions; }> { const provider = this.models.getProvider(model.provider); - if (!provider) throw new ModelsError("provider", `Unknown provider: ${model.provider}`); + if (!provider) + throw new ModelsError( + "provider", + getRemovedProviderMessage(model.provider) ?? `Unknown provider: ${model.provider}`, + ); const resolution = await this.getAuth(model, { apiKey: options?.apiKey, env: options?.env, @@ -724,6 +732,8 @@ export class ModelRuntime implements Models { } registerNativeProvider(provider: Provider): void { + const removed = getRemovedProviderMessage(provider.id); + if (removed) throw new ModelsError("provider", removed); if (!provider.id.trim()) throw new Error("Provider id must not be empty."); this.extensionProviders.delete(provider.id); this.nativeExtensionProviders.set(provider.id, provider); @@ -733,6 +743,8 @@ export class ModelRuntime implements Models { } registerProvider(providerId: string, config: ProviderConfigInput): void { + const removed = getRemovedProviderMessage(providerId); + if (removed) throw new ModelsError("provider", removed); // Validate the incoming registration on its own, like the legacy registry: // a broken re-registration must throw without touching the stored config. validateExtensionProvider(providerId, this.builtins.get(providerId), this.config.getProvider(providerId), config); diff --git a/packages/coding-agent/src/core/sdk.ts b/packages/coding-agent/src/core/sdk.ts index d0f536e5e..372808908 100644 --- a/packages/coding-agent/src/core/sdk.ts +++ b/packages/coding-agent/src/core/sdk.ts @@ -8,7 +8,9 @@ import { type TelemetryContext, type ThinkingLevel, } from "@diffexai/diffex-agent-core"; +import { ModelsError } from "@diffexai/diffex-ai"; import { clampThinkingLevel, type Message, type Model, streamSimple } from "@diffexai/diffex-ai/compat"; +import { getRemovedProviderMessage } from "@diffexai/diffex-ai/providers/removed"; import type { HarnessStateStore, HarnessVersionBundle } from "@diffexai/diffex-harness-state"; import { EvolutionMemoryWriter, FileEvolutionPersistence } from "#evolution"; import { getAgentDir, VERSION } from "../config.ts"; @@ -480,6 +482,8 @@ export async function createInternalAgentSession( // If session has data, try to restore model from it if (!model && hasExistingSession && existingSession.model) { + const removed = getRemovedProviderMessage(existingSession.model.provider); + if (removed) throw new ModelsError("provider", `${removed} Resume with an explicit --model provider/model.`); const restoredModel = modelRuntime.getModel(existingSession.model.provider, existingSession.model.modelId); if (restoredModel && modelRuntime.hasConfiguredAuth(restoredModel.provider)) { model = restoredModel; diff --git a/packages/coding-agent/src/main.ts b/packages/coding-agent/src/main.ts index bb7c5ccfc..7914e0b99 100644 --- a/packages/coding-agent/src/main.ts +++ b/packages/coding-agent/src/main.ts @@ -5,7 +5,7 @@ * createAgentSession() options. The SDK does the heavy lifting. */ -import { type ImageContent, modelsAreEqual } from "@diffexai/diffex-ai"; +import { type ImageContent, ModelsError, modelsAreEqual } from "@diffexai/diffex-ai"; import chalk from "chalk"; import { type Args, type Mode, parseArgs, printHelp } from "./cli/args.ts"; import { @@ -215,7 +215,8 @@ async function runAuthCommand(args: string[]): Promise { result = { status: "not_ready", provider: result.provider, reason: "credential_not_available" }; } } - } catch { + } catch (error) { + if (error instanceof AuthCommandError) throw error; result = { status: "invalid", provider: requestedAuth.provider ?? requestedAuth.model!, @@ -274,7 +275,7 @@ function buildSessionOptions( // Model from CLI // - supports --provider --model // - supports --model / - if (parsed.model) { + if (parsed.model || parsed.provider) { const resolved = resolveCliModel({ cliProvider: parsed.provider, cliModel: parsed.model, @@ -729,6 +730,10 @@ export async function main(args: string[], options?: MainOptions) { { type: "error", message: error instanceof Error ? error.message : String(error) }, ]); await invocationTrace.close("failed"); + if (error instanceof ModelsError && error.code === "provider") { + console.error(chalk.red(`Error: ${error.message}`)); + process.exit(1); + } throw error; } time("createAgentSessionRuntime"); diff --git a/packages/coding-agent/test/radius.test.ts b/packages/coding-agent/test/radius.test.ts index 77265e1a5..276a44649 100644 --- a/packages/coding-agent/test/radius.test.ts +++ b/packages/coding-agent/test/radius.test.ts @@ -1,10 +1,21 @@ -import { mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { spawnSync } from "node:child_process"; +import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; +import { fileURLToPath } from "node:url"; import { type Credential, InMemoryModelsStore, type Model } from "@diffexai/diffex-ai"; +import { anthropicProvider } from "@diffexai/diffex-ai/providers/anthropic"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { checkProviderAuth, getProviderCredential } from "../src/cli/auth-check.ts"; import { AuthStorage } from "../src/core/auth-storage.ts"; +import { + findInitialModel, + resolveCliModel, + resolveModelScopeFromModels, + restoreModelFromSession, +} from "../src/core/model-resolver.ts"; import { ModelRuntime } from "../src/core/model-runtime.ts"; +import { InteractiveMode } from "../src/modes/interactive/interactive-mode.ts"; const staleModel: Model<"diffex-messages"> = { id: "auto", @@ -69,7 +80,11 @@ describe("removed Radius integration", () => { await expect(runtime.getAvailable("radius")).resolves.toEqual([]); await runtime.refresh({ providers: ["radius"], allowNetwork: true }); const result = await runtime.completeSimple(staleModel, { messages: [] }); - expect(result).toMatchObject({ stopReason: "error", errorMessage: "Unknown provider: radius" }); + expect(result).toMatchObject({ + stopReason: "error", + errorMessage: expect.stringContaining("Radius support has been removed"), + }); + expect(runtime.getProviderAuthStatus("radius")).toEqual({ configured: false }); expect(fetch).not.toHaveBeenCalled(); await expect(credentials.read("radius")).resolves.toEqual(credential); }, @@ -85,41 +100,245 @@ describe("removed Radius integration", () => { const notify = vi.fn(); await expect( runtime.login("radius", type, { signal: new AbortController().signal, prompt, notify }), - ).rejects.toThrow("Unknown provider: radius"); + ).rejects.toThrow("Radius support has been removed"); expect(prompt).not.toHaveBeenCalled(); expect(notify).not.toHaveBeenCalled(); expect(fetch).not.toHaveBeenCalled(); }, ); - it.each(["radius", "radius-dev"])( - "rejects the Radius models.json OAuth configuration under %s", - async (providerId) => { - const modelsPath = join(tempDir, "models.json"); - writeFileSync( - modelsPath, - JSON.stringify({ - providers: { - [providerId]: { - oauth: "radius", - baseUrl: "https://radius.example", - apiKey: "stored-key", - api: "diffex-messages", - models: [{ id: "auto" }], + it.each([ + { providerId: "radius" }, + { providerId: "Radius" }, + { providerId: "radius", oauth: "radius" }, + { providerId: "radius-dev", oauth: "radius" }, + ])("rejects Radius models.json configuration: %j", async ({ providerId, ...config }) => { + const modelsPath = join(tempDir, "models.json"); + writeFileSync( + modelsPath, + JSON.stringify({ + providers: { + [providerId]: { + ...config, + baseUrl: "https://radius.example", + apiKey: "stored-key", + api: "diffex-messages", + models: [{ id: "auto" }], + }, + }, + }), + ); + const runtime = await ModelRuntime.create({ + credentials: AuthStorage.inMemory(), + modelsPath, + modelsStore: new InMemoryModelsStore(), + }); + expect(runtime.getError()).toContain("Radius support has been removed"); + expect(runtime.getError()).toContain(`providers.${providerId}`); + expect(runtime.getProvider(providerId)).toBeUndefined(); + await runtime.refresh({ providers: [providerId], allowNetwork: true }); + expect(runtime.getProvider(providerId)).toBeUndefined(); + expect(fetch).not.toHaveBeenCalled(); + }); + + it("rejects extension registrations and runtime keys before mutating state", async () => { + const runtime = await ModelRuntime.create({ credentials: AuthStorage.inMemory(), modelsPath: null }); + expect(() => runtime.registerProvider("radius", { api: "diffex-messages", baseUrl: staleModel.baseUrl })).toThrow( + "Radius support has been removed", + ); + expect(() => runtime.registerNativeProvider({ ...anthropicProvider(), id: "radius" })).toThrow( + "Radius support has been removed", + ); + await expect(runtime.setRuntimeApiKey("radius", "unused-key")).rejects.toThrow("Radius support has been removed"); + expect(runtime.getRegisteredProviderIds()).not.toContain("radius"); + expect(runtime.getProviderAuthStatus("radius")).toEqual({ configured: false }); + expect(fetch).not.toHaveBeenCalled(); + }); + + it("omits Radius from both login methods and permits local credential cleanup", async () => { + const modelRuntime = await ModelRuntime.create({ + credentials: AuthStorage.inMemory({ radius: { type: "api_key", key: "unused-key" } }), + modelsPath: null, + }); + const prototype = InteractiveMode.prototype as unknown as { + getLoginProviderOptions(this: object, authType: "oauth" | "api_key"): Array<{ id: string }>; + getLogoutProviderOptions(this: object): Promise>; + }; + const view = { session: { modelRuntime }, diffexAccountAuth: { hasSession: async () => false } }; + for (const type of ["oauth", "api_key"] as const) { + const options = prototype.getLoginProviderOptions.call(view, type); + expect(options.length).toBeGreaterThan(0); + expect(options.some((option) => option.id === "radius")).toBe(false); + } + expect(await prototype.getLogoutProviderOptions.call(view)).toMatchObject([{ id: "radius" }]); + await modelRuntime.logout("radius"); + expect(await modelRuntime.listCredentials()).toEqual([]); + expect(await prototype.getLogoutProviderOptions.call(view)).toEqual([]); + expect(fetch).not.toHaveBeenCalled(); + }); + + it("rejects auth checks and credential export without reading old tokens", async () => { + const credentials = AuthStorage.inMemory({ + radius: { type: "oauth", access: "old-token", refresh: "unused", expires: 0 }, + }); + const runtime = await ModelRuntime.create({ credentials, modelsPath: null }); + await expect( + checkProviderAuth( + { provider: "radius", messages: [], fileArgs: [], diagnostics: [], unknownFlags: new Map() }, + runtime, + ), + ).rejects.toThrow("Radius support has been removed"); + const read = vi.spyOn(credentials, "read"); + await expect(getProviderCredential("radius", runtime, credentials, { refresh: false })).rejects.toThrow( + "Radius support has been removed", + ); + expect(read).not.toHaveBeenCalled(); + expect(fetch).not.toHaveBeenCalled(); + }); + + it("explains removed CLI, default, and restored model selections", async () => { + const modelRuntime = await ModelRuntime.create({ credentials: AuthStorage.inMemory(), modelsPath: null }); + for (const selection of [ + { cliProvider: "radius", cliModel: "auto" }, + { cliModel: "Radius/auto" }, + { cliProvider: "radius" }, + ]) { + expect(resolveCliModel({ ...selection, modelRuntime })).toMatchObject({ + model: undefined, + error: expect.stringContaining("Radius support has been removed"), + }); + } + await expect( + findInitialModel({ + defaultProvider: "radius", + defaultModelId: "auto", + scopedModels: [], + isContinuing: false, + modelRuntime, + }), + ).rejects.toThrow("Update defaultProvider and defaultModel in settings.json"); + await expect(restoreModelFromSession("radius", "auto", undefined, false, modelRuntime)).rejects.toThrow( + "Resume with an explicit --model provider/model", + ); + expect(() => resolveModelScopeFromModels(["radius/*"], modelRuntime.getModels())).toThrow( + "Radius support has been removed", + ); + expect(fetch).not.toHaveBeenCalled(); + }); + + it.each(["api_key", "oauth", "cli", "cli-provider", "default", "resume", "auth", "auth-key", "auth-token"])( + "explains legacy %s configuration through the source CLI", + (kind) => { + const repo = fileURLToPath(new URL("../../..", import.meta.url)); + const agentDir = join(tempDir, "agent"); + mkdirSync(agentDir); + const settings = { + packages: [], + ...(kind === "default" ? { defaultProvider: "radius", defaultModel: "auto" } : {}), + }; + writeFileSync(join(agentDir, "settings.json"), JSON.stringify(settings)); + const authPath = join(agentDir, "auth.json"); + const auth = JSON.stringify({ radius: { type: "api_key", key: "unused-key" } }); + writeFileSync(authPath, auth); + const sessionPath = join(tempDir, "old-session.jsonl"); + if (kind === "resume") { + writeFileSync( + sessionPath, + [ + { + type: "session", + version: 3, + id: "old-session", + timestamp: new Date(0).toISOString(), + cwd: tempDir, + }, + { + type: "model_change", + id: "model", + parentId: null, + timestamp: new Date(0).toISOString(), + provider: "radius", + modelId: "auto", + }, + { + type: "message", + id: "message", + parentId: "model", + timestamp: new Date(0).toISOString(), + message: { role: "user", content: "Previous prompt", timestamp: 0 }, + }, + ] + .map((entry) => JSON.stringify(entry)) + .join("\n"), + ); + } + if (kind === "api_key" || kind === "oauth") { + writeFileSync( + join(agentDir, "models.json"), + JSON.stringify({ + providers: { + radius: { + api: "diffex-messages", + apiKey: "unused-key", + baseUrl: staleModel.baseUrl, + models: [{ id: "auto" }], + ...(kind === "oauth" ? { oauth: "radius" } : {}), + }, }, + }), + ); + } + const result = spawnSync( + process.execPath, + [ + "--import", + join(repo, "node_modules/tsx/dist/loader.mjs"), + join(repo, "packages/coding-agent/src/cli.ts"), + ...(kind.startsWith("auth") + ? [ + "auth", + kind === "auth" ? "check" : kind === "auth-key" ? "print-api-key" : "print-bearer-token", + "--provider", + "radius", + ] + : [ + "--offline", + "--no-extensions", + "--no-skills", + "--no-context-files", + "--no-prompt-templates", + ...(kind === "resume" ? ["--session", sessionPath] : ["--no-session"]), + ...(kind === "cli" + ? ["--model", "radius/auto", "-p", "Do not send"] + : kind === "cli-provider" + ? ["--provider", "radius", "-p", "Do not send"] + : kind === "default" || kind === "resume" + ? ["-p", "Do not send"] + : ["--list-models"]), + ]), + ], + { + cwd: tempDir, + env: { + PATH: process.env.PATH, + HOME: tempDir, + DIFFEX_CODING_AGENT_DIR: agentDir, + DIFFEX_OFFLINE: "1", + DIFFEX_NO_LOCAL_LLM: "1", + RADIUS_API_KEY: "unused-key", + TSX_TSCONFIG_PATH: join(repo, "tsconfig.json"), }, - }), + encoding: "utf8", + timeout: 20000, + }, ); - const runtime = await ModelRuntime.create({ - credentials: AuthStorage.inMemory(), - modelsPath, - modelsStore: new InMemoryModelsStore(), - }); - expect(runtime.getError()).toContain("oauth"); - expect(runtime.getProvider(providerId)).toBeUndefined(); - await runtime.refresh({ providers: [providerId], allowNetwork: true }); - expect(runtime.getProvider(providerId)).toBeUndefined(); - expect(fetch).not.toHaveBeenCalled(); + expect(result.error).toBeUndefined(); + expect(result.status).toBe(kind === "api_key" || kind === "oauth" ? 0 : kind === "auth" ? 2 : 1); + expect(result.stderr).toContain("Radius support has been removed"); + expect(result.stderr).not.toContain("\n at "); + expect(result.stderr).not.toContain("Node.js v"); + expect(result.stdout).not.toMatch(/^radius\s+auto/m); + expect(readFileSync(authPath, "utf8")).toBe(auth); }, ); }); diff --git a/scripts/check-distribution-payload.py b/scripts/check-distribution-payload.py index a3de8a622..c0f38de17 100644 --- a/scripts/check-distribution-payload.py +++ b/scripts/check-distribution-payload.py @@ -26,6 +26,8 @@ MAP_DIRECTIVE = re.compile(rb'(?://[#@]|/\*[#@])\s*sourceMappingURL\s*=') MAP_CONTENT = re.compile(rb'["\']sourcesContent["\']\s*:') TS_CONTENT = re.compile(rb'(?m)^\s*(?:export\s+)?(?:interface\s+\w+\s*(?:extends[^\n]+)?\{|type\s+\w+\s*=)') +REMOVED_PROVIDER_PATH = re.compile(r'(?:^|/)(?:providers/radius(?:-config)?|auth/oauth/radius|core/radius)\.(?:js|d\.ts)$', re.I) +REMOVED_PROVIDER_RUNTIME = re.compile(rb'radius\.(?:pi\.dev|earendil\.com)|\bRADIUS_API_KEY\b|\bpi-gateway\b', re.I) class PayloadError(ValueError): @@ -42,6 +44,8 @@ def check_path(name, directory=False, default_agents=False): fail(name, 'unsafe archive path') if any(part in {'.git', '.env', 'src', 'examples', '__MACOSX'} or part.startswith(('._', '.env.')) for part in path.parts): fail(name, 'prohibited source or private path') + if REMOVED_PROVIDER_PATH.search(name): + fail(name, 'removed Radius integration') if any(part.lower() == 'agents.md' for part in path.parts) and not default_agents: fail(name, 'prohibited repository instructions') if not directory and path.name.upper() == 'NOTICE': @@ -81,6 +85,8 @@ def scan_bytes(data, path, budget, depth=0, default_agents=False): fail(path, 'prohibited repository instructions: default AGENTS.md differs from AGENTS_release.md') if MAP_DIRECTIVE.search(data) or MAP_CONTENT.search(data): fail(path, 'embedded source map') + if PurePosixPath(path).suffix in {'.js', '.mjs', '.cjs', '.ts', '.node'} and REMOVED_PROVIDER_RUNTIME.search(data): + fail(path, 'removed Radius integration') if not declaration and PurePosixPath(path).suffix not in {'.md', '.html'} and TS_CONTENT.search(data): fail(path, 'TypeScript implementation content') stream = io.BytesIO(data) diff --git a/scripts/distribution-payload.test.mjs b/scripts/distribution-payload.test.mjs index b72623bdf..762b13d42 100644 --- a/scripts/distribution-payload.test.mjs +++ b/scripts/distribution-payload.test.mjs @@ -80,6 +80,28 @@ test("accepts a tarball with readable JavaScript, declarations, and complete not assert.equal(result.status, 0, result.stderr); }); +test("accepts the Radius removal explanation without restoring the integration", () => { + const result = inspect({ "dist/removed.js": 'export const message = "Radius support has been removed.";' }); + assert.equal(result.status, 0, result.stderr); +}); + +for (const [path, content] of [ + ["dist/node_modules/@diffexai/diffex-ai/dist/providers/radius.js", "export const provider = {};"], + ["dist/node_modules/@diffexai/diffex-ai/dist/providers/radius-config.d.ts", "export declare const gateway: string;"], + ["dist/node_modules/@diffexai/diffex-ai/dist/auth/oauth/radius.js", "export const login = () => {};"], + ["dist/core/radius.js", "export const provider = {};"], + ["dist/renamed.js", 'export const gateway = "https://radius.pi.dev";'], + ["dist/renamed.mjs", 'export const issuer = "https://radius.earendil.com";'], + ["dist/renamed.cjs", 'exports.key = process.env.RADIUS_API_KEY;'], + ["dist/bundle.node", Buffer.from('binary\0client_id=pi-gateway\0')], +]) { + test(`rejects removed Radius integration in ${path}`, () => { + const result = inspect({ [path]: content }); + assert.notEqual(result.status, 0); + assert.match(result.stderr, /removed Radius integration/); + }); +} + for (const notice of [ "LICENSE", "THIRD-PARTY-NOTICES.txt", diff --git a/scripts/verify-candidate-artifacts.py b/scripts/verify-candidate-artifacts.py index 10415eaf0..61ef60bb2 100644 --- a/scripts/verify-candidate-artifacts.py +++ b/scripts/verify-candidate-artifacts.py @@ -126,10 +126,26 @@ name in text for name in ["ChatGPT", "OpenRouter", "xAI"] ): assert not re.search(r"\bRadius\b", text), text + mark = len(raw) + os.write(fd, b"radius") + stage = "account-search" + elif stage == "account-search" and b"No matching providers" in raw[mark:]: mark = len(raw) os.write(fd, b"\x03") stage = "back" elif stage == "back" and b"Select authentication method:" in raw[mark:]: + mark = len(raw) + os.write(fd, b"\x1b[B\r") + stage = "api-keys" + elif stage == "api-keys" and b"Select provider to configure:" in raw[mark:]: + mark = len(raw) + os.write(fd, b"radius") + stage = "api-key-search" + elif stage == "api-key-search" and b"No matching providers" in raw[mark:]: + mark = len(raw) + os.write(fd, b"\x03") + stage = "api-key-back" + elif stage == "api-key-back" and b"Select authentication method:" in raw[mark:]: mark = len(raw) os.write(fd, b"\x03") stage = "editor" @@ -159,7 +175,7 @@ results.append( { "runtime": runtime, - "command": "interactive prompt, login selector, exit", + "command": "interactive prompt, subscription and API-key Radius exclusion, exit", "exitCode": 0, "events": events, "accounts": ["ChatGPT", "OpenRouter", "xAI"],