From 6e89d9e2c36c2dc385f3d207ea631a71e3d59cab Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Wed, 23 Sep 2026 07:09:19 -0500 Subject: [PATCH] feat(core): identify the client on Console requests (#50896) --- packages/core/src/plugin/provider/opencode.ts | 6 +++++- packages/core/test/plugin/provider-opencode.test.ts | 11 ++++++++--- specs/v2/provider-policy.md | 1 + 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/packages/core/src/plugin/provider/opencode.ts b/packages/core/src/plugin/provider/opencode.ts index 226b3911d77..331e802068a 100644 --- a/packages/core/src/plugin/provider/opencode.ts +++ b/packages/core/src/plugin/provider/opencode.ts @@ -3,6 +3,7 @@ import type { Scope } from "effect" import type { IntegrationOAuthMethodRegistration } from "@opencode/plugin/effect/integration" import { define } from "@opencode/plugin/effect/plugin" import { FetchHttpClient, HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http" +import { App } from "../../app.js" import { Bus } from "../../bus.js" import { Credential } from "../../credential.js" import { Integration } from "../../integration.js" @@ -122,7 +123,10 @@ export const OpencodePlugin = define> diff --git a/packages/core/test/plugin/provider-opencode.test.ts b/packages/core/test/plugin/provider-opencode.test.ts index fe9992a7b9e..180390bfb4b 100644 --- a/packages/core/test/plugin/provider-opencode.test.ts +++ b/packages/core/test/plugin/provider-opencode.test.ts @@ -6,6 +6,7 @@ import { Money } from "@opencode/schema/money" import { Effect, Layer, Stream } from "effect" import { TestClock } from "effect/testing" import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http" +import { App } from "@opencode/core/app" import { Config } from "@opencode/core/config" import { ConfigPolicyPlugin } from "@opencode/core/config/plugin/policy" import { Credential } from "@opencode/core/credential" @@ -38,11 +39,13 @@ const noRemoteConfig = HttpClient.make((request) => function consoleServer(orgID: string | null | undefined, unavailable = false) { const config: { authorization: string | null; orgID: string | null }[] = [] const requests: string[] = [] + const agents: (string | null)[] = [] const server = Bun.serve({ port: 0, fetch: async (request) => { const path = new URL(request.url).pathname requests.push(path) + agents.push(request.headers.get("user-agent")) if (path === "/auth/device/code") { expect(await request.json()).toEqual({ client_id: "opencode-cli", supports_org_scope: true }) return Response.json({ @@ -74,7 +77,7 @@ function consoleServer(orgID: string | null | undefined, unavailable = false) { return new Response("Not found", { status: 404 }) }, }) - return { server, config, requests } + return { server, config, requests, agents } } function required(value: T | undefined): T { @@ -284,7 +287,7 @@ describe("OpencodePlugin", () => { () => Effect.acquireUseRelease( Effect.sync(() => consoleServer(scenario.orgID, scenario.unavailable)), - ({ server, config, requests }) => + ({ server, config, requests, agents }) => Effect.gen(function* () { const credentials = yield* Credential.Service const initial = yield* credentials.create({ @@ -328,7 +331,9 @@ describe("OpencodePlugin", () => { }), ).toEqual(stored.value) expect(requests).toEqual(["/auth/device/token", "/api/v2/config"]) - }), + // The refresh and the config fetch both say which OpenCode is asking. + expect(agents).toEqual(["opencode/beta/1.2.3/test", "opencode/beta/1.2.3/test"]) + }).pipe(Effect.provideService(App.Metadata, App.make({ name: "test", version: "1.2.3", channel: "beta" }))), ({ server }) => Effect.promise(() => server.stop(true)), ), ) diff --git a/specs/v2/provider-policy.md b/specs/v2/provider-policy.md index ae3be769ddb..0ac5afa1a0a 100644 --- a/specs/v2/provider-policy.md +++ b/specs/v2/provider-policy.md @@ -222,6 +222,7 @@ The OpenCode Console compiles a workspace's Providers and Tools policies into st - `experimental` is omitted when the caller has no statements; omission and an empty array are equivalent. - The list is per caller and its order is significant. The client stores it exactly as received; it never reorders, dedupes, or normalizes statements. +- Every request the Console plugin makes, this fetch and the token refresh included, carries the `User-Agent` `opencode///`. The Console reads it to tell which OpenCode a member runs and whether it evaluates the statements it is being sent; older builds that drop them are otherwise indistinguishable from ones that enforce. - `ManagedPolicy` (`packages/core/src/managed-policy.ts`) is the process-global home for the current statements and the organization name. The Console plugin (`opencode.provider.opencode`) writes it whenever its config snapshot is applied; the policy plugin reads it synchronously when evaluating. - Statements ride on the Console plugin's snapshot, so they follow the connection: a credential switch replaces them, and a disconnect or a 404 from the Console clears them. Statements from different connections never merge. - Freshness is the snapshot's freshness: the next poll (about one minute) or the next credential switch.