feat(core): identify the client on Console requests (#50896)

This commit is contained in:
Adam
2026-09-23 07:09:19 -05:00
committed by GitHub
parent f526727178
commit 6e89d9e2c3
3 changed files with 14 additions and 4 deletions
@@ -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<HttpClient.HttpClient | Bus.Service | Manag
id: "opencode.provider.opencode",
effect: Effect.fn(function* (ctx) {
const bus = yield* Bus.Service
const http = yield* HttpClient.HttpClient
const client = yield* HttpClient.HttpClient
// Every request here goes to the Console, which reads the User-Agent to tell which OpenCode a member runs
// and whether it evaluates the policies it is being sent.
const http = HttpClient.mapRequest(client, HttpClientRequest.setHeader("User-Agent", App.useragent(ctx.app)))
const managed = yield* ManagedPolicy.Service
const loading = Semaphore.makeUnsafe(1)
type ActiveConnection = Effect.Success<ReturnType<typeof ctx.integration.connection.active>>
@@ -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<T>(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)),
),
)
+1
View File
@@ -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/<channel>/<version>/<app>`. 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.