Bläddra i källkod

feat(api): add experimental wellknown connections

Dax Raad 1 månad sedan
förälder
incheckning
5fb0470b44
33 ändrade filer med 1070 tillägg och 138 borttagningar
  1. 12 4
      packages/cli/src/commands/commands.ts
  2. 52 0
      packages/cli/src/commands/handlers/auth/connect.ts
  3. 3 0
      packages/cli/src/index.ts
  4. 45 34
      packages/client/src/effect/api/api.ts
  5. 52 41
      packages/client/src/effect/generated/client.ts
  6. 17 0
      packages/client/src/promise/generated/client.ts
  7. 9 0
      packages/client/src/promise/generated/types.ts
  8. 24 1
      packages/client/test/promise.test.ts
  9. 55 2
      packages/core/schema.json
  10. 87 17
      packages/core/src/config.ts
  11. 1 0
      packages/core/src/database/migration.gen.ts
  12. 18 0
      packages/core/src/database/migration/20260716020354_kv.ts
  13. 8 0
      packages/core/src/database/schema.gen.ts
  14. 23 16
      packages/core/src/integration.ts
  15. 47 0
      packages/core/src/kv.ts
  16. 9 0
      packages/core/src/kv/sql.ts
  17. 5 0
      packages/core/src/plugin/internal.ts
  18. 3 3
      packages/core/src/plugin/supervisor.ts
  19. 166 0
      packages/core/src/wellknown.ts
  20. 34 0
      packages/core/src/wellknown/plugin.ts
  21. 121 3
      packages/core/test/config/config.test.ts
  22. 2 2
      packages/core/test/integration.test.ts
  23. 25 0
      packages/core/test/kv.test.ts
  24. 82 0
      packages/core/test/wellknown.test.ts
  25. 2 2
      packages/opencode/test/session/message-v2.test.ts
  26. 1 1
      packages/plugin/src/v2/effect/integration.ts
  27. 1 1
      packages/plugin/src/v2/promise/integration.ts
  28. 16 0
      packages/protocol/src/groups/integration.ts
  29. 65 0
      packages/sdk/js/src/v2/gen/sdk.gen.ts
  30. 38 0
      packages/sdk/js/src/v2/gen/types.gen.ts
  31. 17 0
      packages/server/src/handlers/integration.ts
  32. 4 3
      packages/server/src/routes.ts
  33. 26 8
      packages/tui/src/component/dialog-integration.tsx

+ 12 - 4
packages/cli/src/commands/commands.ts

@@ -66,6 +66,17 @@ export const Commands = Spec.make(typeof OPENCODE_CLI_NAME === "string" ? OPENCO
         }),
       ],
     }),
+    Spec.make("auth", {
+      description: "Manage authentication",
+      commands: [
+        Spec.make("connect", {
+          description: "Connect to a wellknown authentication provider",
+          params: {
+            url: Argument.string("url").pipe(Argument.withDescription("Wellknown provider URL")),
+          },
+        }),
+      ],
+    }),
     Spec.make("mcp", {
       description: "Manage MCP (Model Context Protocol) servers",
       commands: [
@@ -178,10 +189,7 @@ export const Commands = Spec.make(typeof OPENCODE_CLI_NAME === "string" ? OPENCO
           Flag.atMost(100),
         ),
         title: Flag.string("title").pipe(Flag.withDescription("Session title"), Flag.optional),
-        thinking: Flag.boolean("thinking").pipe(
-          Flag.withDescription("Show thinking blocks"),
-          Flag.withDefault(false),
-        ),
+        thinking: Flag.boolean("thinking").pipe(Flag.withDescription("Show thinking blocks"), Flag.withDefault(false)),
         auto: Flag.boolean("auto").pipe(
           Flag.withDescription("Auto-approve permissions that are not explicitly denied"),
           Flag.withDefault(false),

+ 52 - 0
packages/cli/src/commands/handlers/auth/connect.ts

@@ -0,0 +1,52 @@
+import { EOL } from "node:os"
+import { Effect } from "effect"
+import { Service } from "@opencode-ai/client/effect/service"
+import { OpenCode, type IntegrationCommandStatusOutput, type OpenCodeClient } from "@opencode-ai/client/promise"
+import { Commands } from "../../commands"
+import { Runtime } from "../../../framework/runtime"
+import { ServiceConfig } from "../../../services/service-config"
+
+const location = { directory: process.cwd() }
+
+export default Runtime.handler(
+  Commands.commands.auth.commands.connect,
+  Effect.fn("cli.auth.connect")(function* (input) {
+    process.stdout.write("Connecting..." + EOL + EOL)
+    const endpoint = yield* Service.ensure(yield* ServiceConfig.options())
+    const client = OpenCode.make({ baseUrl: endpoint.url, headers: Service.headers(endpoint) })
+    yield* request(() => client.integration.wellknown.add({ url: input.url, location }))
+    const integrationID = input.url.replace(/\/+$/, "")
+    const started = yield* request(() =>
+      client.integration.command.connect({ integrationID, methodID: "login", location }),
+    )
+    yield* Effect.addFinalizer(() =>
+      request(() =>
+        client.integration.command.cancel({ integrationID, attemptID: started.data.attemptID, location }),
+      ).pipe(Effect.ignore),
+    )
+
+    const status = yield* wait(client, integrationID, started.data.attemptID)
+    if (status.status === "failed") return yield* Effect.fail(new Error(status.message))
+    if (status.status === "expired") return yield* Effect.fail(new Error("Authentication expired"))
+    process.stdout.write("Connected" + EOL)
+  }),
+)
+
+const wait = (
+  client: OpenCodeClient,
+  integrationID: string,
+  attemptID: string,
+  shown = false,
+): Effect.Effect<Exclude<IntegrationCommandStatusOutput["data"], { status: "pending" }>, unknown> =>
+  Effect.gen(function* () {
+    const response = yield* request(() => client.integration.command.status({ integrationID, attemptID, location }))
+    if (response.data.status !== "pending") return response.data
+    const output = response.data.message?.trim()
+    if (!shown && output) process.stdout.write(output + EOL + EOL)
+    yield* Effect.sleep(500)
+    return yield* wait(client, integrationID, attemptID, shown || !!output)
+  })
+
+function request<A>(task: () => Promise<A>) {
+  return Effect.tryPromise({ try: task, catch: (cause) => cause })
+}

+ 3 - 0
packages/cli/src/index.ts

@@ -17,6 +17,9 @@ import { Npm } from "@opencode-ai/core/npm"
 const Handlers = Runtime.handlers(Commands, {
   $: () => import("./commands/handlers/default"),
   api: () => import("./commands/handlers/api"),
+  auth: {
+    connect: () => import("./commands/handlers/auth/connect"),
+  },
   debug: {
     agents: () => import("./commands/handlers/debug/agents"),
   },

+ 45 - 34
packages/client/src/effect/api/api.ts

@@ -407,102 +407,113 @@ export type Endpoint10_1Input = {
 export type Endpoint10_1Output = EffectValue<ReturnType<RawClient["server.integration"]["integration.get"]>>
 export type IntegrationGetOperation<E = never> = (input: Endpoint10_1Input) => Effect.Effect<Endpoint10_1Output, E>
 
-type Endpoint10_2Request = Parameters<RawClient["server.integration"]["integration.connect.key"]>[0]
+type Endpoint10_2Request = Parameters<RawClient["server.integration"]["integration.wellknown.add"]>[0]
 export type Endpoint10_2Input = {
-  readonly integrationID: Endpoint10_2Request["params"]["integrationID"]
   readonly location?: Endpoint10_2Request["query"]["location"]
-  readonly key: Endpoint10_2Request["payload"]["key"]
-  readonly label?: Endpoint10_2Request["payload"]["label"]
+  readonly url: Endpoint10_2Request["payload"]["url"]
 }
-export type Endpoint10_2Output = EffectValue<ReturnType<RawClient["server.integration"]["integration.connect.key"]>>
-export type IntegrationConnectKeyOperation<E = never> = (
+export type Endpoint10_2Output = EffectValue<ReturnType<RawClient["server.integration"]["integration.wellknown.add"]>>
+export type IntegrationWellknownAddOperation<E = never> = (
   input: Endpoint10_2Input,
 ) => Effect.Effect<Endpoint10_2Output, E>
 
-type Endpoint10_3Request = Parameters<RawClient["server.integration"]["integration.oauth.connect"]>[0]
+type Endpoint10_3Request = Parameters<RawClient["server.integration"]["integration.connect.key"]>[0]
 export type Endpoint10_3Input = {
   readonly integrationID: Endpoint10_3Request["params"]["integrationID"]
   readonly location?: Endpoint10_3Request["query"]["location"]
-  readonly methodID: Endpoint10_3Request["payload"]["methodID"]
-  readonly inputs: Endpoint10_3Request["payload"]["inputs"]
+  readonly key: Endpoint10_3Request["payload"]["key"]
   readonly label?: Endpoint10_3Request["payload"]["label"]
 }
-export type Endpoint10_3Output = EffectValue<ReturnType<RawClient["server.integration"]["integration.oauth.connect"]>>
-export type IntegrationOauthConnectOperation<E = never> = (
+export type Endpoint10_3Output = EffectValue<ReturnType<RawClient["server.integration"]["integration.connect.key"]>>
+export type IntegrationConnectKeyOperation<E = never> = (
   input: Endpoint10_3Input,
 ) => Effect.Effect<Endpoint10_3Output, E>
 
-type Endpoint10_4Request = Parameters<RawClient["server.integration"]["integration.oauth.status"]>[0]
+type Endpoint10_4Request = Parameters<RawClient["server.integration"]["integration.oauth.connect"]>[0]
 export type Endpoint10_4Input = {
   readonly integrationID: Endpoint10_4Request["params"]["integrationID"]
-  readonly attemptID: Endpoint10_4Request["params"]["attemptID"]
   readonly location?: Endpoint10_4Request["query"]["location"]
+  readonly methodID: Endpoint10_4Request["payload"]["methodID"]
+  readonly inputs: Endpoint10_4Request["payload"]["inputs"]
+  readonly label?: Endpoint10_4Request["payload"]["label"]
 }
-export type Endpoint10_4Output = EffectValue<ReturnType<RawClient["server.integration"]["integration.oauth.status"]>>
-export type IntegrationOauthStatusOperation<E = never> = (
+export type Endpoint10_4Output = EffectValue<ReturnType<RawClient["server.integration"]["integration.oauth.connect"]>>
+export type IntegrationOauthConnectOperation<E = never> = (
   input: Endpoint10_4Input,
 ) => Effect.Effect<Endpoint10_4Output, E>
 
-type Endpoint10_5Request = Parameters<RawClient["server.integration"]["integration.oauth.complete"]>[0]
+type Endpoint10_5Request = Parameters<RawClient["server.integration"]["integration.oauth.status"]>[0]
 export type Endpoint10_5Input = {
   readonly integrationID: Endpoint10_5Request["params"]["integrationID"]
   readonly attemptID: Endpoint10_5Request["params"]["attemptID"]
   readonly location?: Endpoint10_5Request["query"]["location"]
-  readonly code?: Endpoint10_5Request["payload"]["code"]
 }
-export type Endpoint10_5Output = EffectValue<ReturnType<RawClient["server.integration"]["integration.oauth.complete"]>>
-export type IntegrationOauthCompleteOperation<E = never> = (
+export type Endpoint10_5Output = EffectValue<ReturnType<RawClient["server.integration"]["integration.oauth.status"]>>
+export type IntegrationOauthStatusOperation<E = never> = (
   input: Endpoint10_5Input,
 ) => Effect.Effect<Endpoint10_5Output, E>
 
-type Endpoint10_6Request = Parameters<RawClient["server.integration"]["integration.oauth.cancel"]>[0]
+type Endpoint10_6Request = Parameters<RawClient["server.integration"]["integration.oauth.complete"]>[0]
 export type Endpoint10_6Input = {
   readonly integrationID: Endpoint10_6Request["params"]["integrationID"]
   readonly attemptID: Endpoint10_6Request["params"]["attemptID"]
   readonly location?: Endpoint10_6Request["query"]["location"]
+  readonly code?: Endpoint10_6Request["payload"]["code"]
 }
-export type Endpoint10_6Output = EffectValue<ReturnType<RawClient["server.integration"]["integration.oauth.cancel"]>>
-export type IntegrationOauthCancelOperation<E = never> = (
+export type Endpoint10_6Output = EffectValue<ReturnType<RawClient["server.integration"]["integration.oauth.complete"]>>
+export type IntegrationOauthCompleteOperation<E = never> = (
   input: Endpoint10_6Input,
 ) => Effect.Effect<Endpoint10_6Output, E>
 
-type Endpoint10_7Request = Parameters<RawClient["server.integration"]["integration.command.connect"]>[0]
+type Endpoint10_7Request = Parameters<RawClient["server.integration"]["integration.oauth.cancel"]>[0]
 export type Endpoint10_7Input = {
   readonly integrationID: Endpoint10_7Request["params"]["integrationID"]
+  readonly attemptID: Endpoint10_7Request["params"]["attemptID"]
   readonly location?: Endpoint10_7Request["query"]["location"]
-  readonly methodID: Endpoint10_7Request["payload"]["methodID"]
-  readonly label?: Endpoint10_7Request["payload"]["label"]
 }
-export type Endpoint10_7Output = EffectValue<ReturnType<RawClient["server.integration"]["integration.command.connect"]>>
-export type IntegrationCommandConnectOperation<E = never> = (
+export type Endpoint10_7Output = EffectValue<ReturnType<RawClient["server.integration"]["integration.oauth.cancel"]>>
+export type IntegrationOauthCancelOperation<E = never> = (
   input: Endpoint10_7Input,
 ) => Effect.Effect<Endpoint10_7Output, E>
 
-type Endpoint10_8Request = Parameters<RawClient["server.integration"]["integration.command.status"]>[0]
+type Endpoint10_8Request = Parameters<RawClient["server.integration"]["integration.command.connect"]>[0]
 export type Endpoint10_8Input = {
   readonly integrationID: Endpoint10_8Request["params"]["integrationID"]
-  readonly attemptID: Endpoint10_8Request["params"]["attemptID"]
   readonly location?: Endpoint10_8Request["query"]["location"]
+  readonly methodID: Endpoint10_8Request["payload"]["methodID"]
+  readonly label?: Endpoint10_8Request["payload"]["label"]
 }
-export type Endpoint10_8Output = EffectValue<ReturnType<RawClient["server.integration"]["integration.command.status"]>>
-export type IntegrationCommandStatusOperation<E = never> = (
+export type Endpoint10_8Output = EffectValue<ReturnType<RawClient["server.integration"]["integration.command.connect"]>>
+export type IntegrationCommandConnectOperation<E = never> = (
   input: Endpoint10_8Input,
 ) => Effect.Effect<Endpoint10_8Output, E>
 
-type Endpoint10_9Request = Parameters<RawClient["server.integration"]["integration.command.cancel"]>[0]
+type Endpoint10_9Request = Parameters<RawClient["server.integration"]["integration.command.status"]>[0]
 export type Endpoint10_9Input = {
   readonly integrationID: Endpoint10_9Request["params"]["integrationID"]
   readonly attemptID: Endpoint10_9Request["params"]["attemptID"]
   readonly location?: Endpoint10_9Request["query"]["location"]
 }
-export type Endpoint10_9Output = EffectValue<ReturnType<RawClient["server.integration"]["integration.command.cancel"]>>
-export type IntegrationCommandCancelOperation<E = never> = (
+export type Endpoint10_9Output = EffectValue<ReturnType<RawClient["server.integration"]["integration.command.status"]>>
+export type IntegrationCommandStatusOperation<E = never> = (
   input: Endpoint10_9Input,
 ) => Effect.Effect<Endpoint10_9Output, E>
 
+type Endpoint10_10Request = Parameters<RawClient["server.integration"]["integration.command.cancel"]>[0]
+export type Endpoint10_10Input = {
+  readonly integrationID: Endpoint10_10Request["params"]["integrationID"]
+  readonly attemptID: Endpoint10_10Request["params"]["attemptID"]
+  readonly location?: Endpoint10_10Request["query"]["location"]
+}
+export type Endpoint10_10Output = EffectValue<ReturnType<RawClient["server.integration"]["integration.command.cancel"]>>
+export type IntegrationCommandCancelOperation<E = never> = (
+  input: Endpoint10_10Input,
+) => Effect.Effect<Endpoint10_10Output, E>
+
 export interface IntegrationApi<E = never> {
   readonly list: IntegrationListOperation<E>
   readonly get: IntegrationGetOperation<E>
+  readonly wellknown: { readonly add: IntegrationWellknownAddOperation<E> }
   readonly connect: { readonly key: IntegrationConnectKeyOperation<E> }
   readonly oauth: {
     readonly connect: IntegrationOauthConnectOperation<E>

+ 52 - 41
packages/client/src/effect/generated/client.ts

@@ -504,106 +504,116 @@ const Endpoint10_1 = (raw: RawClient["server.integration"]) => (input: Endpoint1
     query: { location: input["location"] },
   }).pipe(Effect.mapError(mapClientError))
 
-type Endpoint10_2Request = Parameters<RawClient["server.integration"]["integration.connect.key"]>[0]
+type Endpoint10_2Request = Parameters<RawClient["server.integration"]["integration.wellknown.add"]>[0]
 type Endpoint10_2Input = {
-  readonly integrationID: Endpoint10_2Request["params"]["integrationID"]
   readonly location?: Endpoint10_2Request["query"]["location"]
-  readonly key: Endpoint10_2Request["payload"]["key"]
-  readonly label?: Endpoint10_2Request["payload"]["label"]
+  readonly url: Endpoint10_2Request["payload"]["url"]
 }
 const Endpoint10_2 = (raw: RawClient["server.integration"]) => (input: Endpoint10_2Input) =>
-  raw["integration.connect.key"]({
-    params: { integrationID: input["integrationID"] },
-    query: { location: input["location"] },
-    payload: { key: input["key"], label: input["label"] },
-  }).pipe(Effect.mapError(mapClientError))
+  raw["integration.wellknown.add"]({ query: { location: input["location"] }, payload: { url: input["url"] } }).pipe(
+    Effect.mapError(mapClientError),
+  )
 
-type Endpoint10_3Request = Parameters<RawClient["server.integration"]["integration.oauth.connect"]>[0]
+type Endpoint10_3Request = Parameters<RawClient["server.integration"]["integration.connect.key"]>[0]
 type Endpoint10_3Input = {
   readonly integrationID: Endpoint10_3Request["params"]["integrationID"]
   readonly location?: Endpoint10_3Request["query"]["location"]
-  readonly methodID: Endpoint10_3Request["payload"]["methodID"]
-  readonly inputs: Endpoint10_3Request["payload"]["inputs"]
+  readonly key: Endpoint10_3Request["payload"]["key"]
   readonly label?: Endpoint10_3Request["payload"]["label"]
 }
 const Endpoint10_3 = (raw: RawClient["server.integration"]) => (input: Endpoint10_3Input) =>
-  raw["integration.oauth.connect"]({
+  raw["integration.connect.key"]({
     params: { integrationID: input["integrationID"] },
     query: { location: input["location"] },
-    payload: { methodID: input["methodID"], inputs: input["inputs"], label: input["label"] },
+    payload: { key: input["key"], label: input["label"] },
   }).pipe(Effect.mapError(mapClientError))
 
-type Endpoint10_4Request = Parameters<RawClient["server.integration"]["integration.oauth.status"]>[0]
+type Endpoint10_4Request = Parameters<RawClient["server.integration"]["integration.oauth.connect"]>[0]
 type Endpoint10_4Input = {
   readonly integrationID: Endpoint10_4Request["params"]["integrationID"]
-  readonly attemptID: Endpoint10_4Request["params"]["attemptID"]
   readonly location?: Endpoint10_4Request["query"]["location"]
+  readonly methodID: Endpoint10_4Request["payload"]["methodID"]
+  readonly inputs: Endpoint10_4Request["payload"]["inputs"]
+  readonly label?: Endpoint10_4Request["payload"]["label"]
 }
 const Endpoint10_4 = (raw: RawClient["server.integration"]) => (input: Endpoint10_4Input) =>
-  raw["integration.oauth.status"]({
-    params: { integrationID: input["integrationID"], attemptID: input["attemptID"] },
+  raw["integration.oauth.connect"]({
+    params: { integrationID: input["integrationID"] },
     query: { location: input["location"] },
+    payload: { methodID: input["methodID"], inputs: input["inputs"], label: input["label"] },
   }).pipe(Effect.mapError(mapClientError))
 
-type Endpoint10_5Request = Parameters<RawClient["server.integration"]["integration.oauth.complete"]>[0]
+type Endpoint10_5Request = Parameters<RawClient["server.integration"]["integration.oauth.status"]>[0]
 type Endpoint10_5Input = {
   readonly integrationID: Endpoint10_5Request["params"]["integrationID"]
   readonly attemptID: Endpoint10_5Request["params"]["attemptID"]
   readonly location?: Endpoint10_5Request["query"]["location"]
-  readonly code?: Endpoint10_5Request["payload"]["code"]
 }
 const Endpoint10_5 = (raw: RawClient["server.integration"]) => (input: Endpoint10_5Input) =>
-  raw["integration.oauth.complete"]({
+  raw["integration.oauth.status"]({
     params: { integrationID: input["integrationID"], attemptID: input["attemptID"] },
     query: { location: input["location"] },
-    payload: { code: input["code"] },
   }).pipe(Effect.mapError(mapClientError))
 
-type Endpoint10_6Request = Parameters<RawClient["server.integration"]["integration.oauth.cancel"]>[0]
+type Endpoint10_6Request = Parameters<RawClient["server.integration"]["integration.oauth.complete"]>[0]
 type Endpoint10_6Input = {
   readonly integrationID: Endpoint10_6Request["params"]["integrationID"]
   readonly attemptID: Endpoint10_6Request["params"]["attemptID"]
   readonly location?: Endpoint10_6Request["query"]["location"]
+  readonly code?: Endpoint10_6Request["payload"]["code"]
 }
 const Endpoint10_6 = (raw: RawClient["server.integration"]) => (input: Endpoint10_6Input) =>
-  raw["integration.oauth.cancel"]({
+  raw["integration.oauth.complete"]({
     params: { integrationID: input["integrationID"], attemptID: input["attemptID"] },
     query: { location: input["location"] },
+    payload: { code: input["code"] },
   }).pipe(Effect.mapError(mapClientError))
 
-type Endpoint10_7Request = Parameters<RawClient["server.integration"]["integration.command.connect"]>[0]
+type Endpoint10_7Request = Parameters<RawClient["server.integration"]["integration.oauth.cancel"]>[0]
 type Endpoint10_7Input = {
   readonly integrationID: Endpoint10_7Request["params"]["integrationID"]
+  readonly attemptID: Endpoint10_7Request["params"]["attemptID"]
   readonly location?: Endpoint10_7Request["query"]["location"]
-  readonly methodID: Endpoint10_7Request["payload"]["methodID"]
-  readonly label?: Endpoint10_7Request["payload"]["label"]
 }
 const Endpoint10_7 = (raw: RawClient["server.integration"]) => (input: Endpoint10_7Input) =>
-  raw["integration.command.connect"]({
-    params: { integrationID: input["integrationID"] },
+  raw["integration.oauth.cancel"]({
+    params: { integrationID: input["integrationID"], attemptID: input["attemptID"] },
     query: { location: input["location"] },
-    payload: { methodID: input["methodID"], label: input["label"] },
   }).pipe(Effect.mapError(mapClientError))
 
-type Endpoint10_8Request = Parameters<RawClient["server.integration"]["integration.command.status"]>[0]
+type Endpoint10_8Request = Parameters<RawClient["server.integration"]["integration.command.connect"]>[0]
 type Endpoint10_8Input = {
   readonly integrationID: Endpoint10_8Request["params"]["integrationID"]
-  readonly attemptID: Endpoint10_8Request["params"]["attemptID"]
   readonly location?: Endpoint10_8Request["query"]["location"]
+  readonly methodID: Endpoint10_8Request["payload"]["methodID"]
+  readonly label?: Endpoint10_8Request["payload"]["label"]
 }
 const Endpoint10_8 = (raw: RawClient["server.integration"]) => (input: Endpoint10_8Input) =>
-  raw["integration.command.status"]({
-    params: { integrationID: input["integrationID"], attemptID: input["attemptID"] },
+  raw["integration.command.connect"]({
+    params: { integrationID: input["integrationID"] },
     query: { location: input["location"] },
+    payload: { methodID: input["methodID"], label: input["label"] },
   }).pipe(Effect.mapError(mapClientError))
 
-type Endpoint10_9Request = Parameters<RawClient["server.integration"]["integration.command.cancel"]>[0]
+type Endpoint10_9Request = Parameters<RawClient["server.integration"]["integration.command.status"]>[0]
 type Endpoint10_9Input = {
   readonly integrationID: Endpoint10_9Request["params"]["integrationID"]
   readonly attemptID: Endpoint10_9Request["params"]["attemptID"]
   readonly location?: Endpoint10_9Request["query"]["location"]
 }
 const Endpoint10_9 = (raw: RawClient["server.integration"]) => (input: Endpoint10_9Input) =>
+  raw["integration.command.status"]({
+    params: { integrationID: input["integrationID"], attemptID: input["attemptID"] },
+    query: { location: input["location"] },
+  }).pipe(Effect.mapError(mapClientError))
+
+type Endpoint10_10Request = Parameters<RawClient["server.integration"]["integration.command.cancel"]>[0]
+type Endpoint10_10Input = {
+  readonly integrationID: Endpoint10_10Request["params"]["integrationID"]
+  readonly attemptID: Endpoint10_10Request["params"]["attemptID"]
+  readonly location?: Endpoint10_10Request["query"]["location"]
+}
+const Endpoint10_10 = (raw: RawClient["server.integration"]) => (input: Endpoint10_10Input) =>
   raw["integration.command.cancel"]({
     params: { integrationID: input["integrationID"], attemptID: input["attemptID"] },
     query: { location: input["location"] },
@@ -612,14 +622,15 @@ const Endpoint10_9 = (raw: RawClient["server.integration"]) => (input: Endpoint1
 const adaptGroup10 = (raw: RawClient["server.integration"]) => ({
   list: Endpoint10_0(raw),
   get: Endpoint10_1(raw),
-  connect: { key: Endpoint10_2(raw) },
+  wellknown: { add: Endpoint10_2(raw) },
+  connect: { key: Endpoint10_3(raw) },
   oauth: {
-    connect: Endpoint10_3(raw),
-    status: Endpoint10_4(raw),
-    complete: Endpoint10_5(raw),
-    cancel: Endpoint10_6(raw),
+    connect: Endpoint10_4(raw),
+    status: Endpoint10_5(raw),
+    complete: Endpoint10_6(raw),
+    cancel: Endpoint10_7(raw),
   },
-  command: { connect: Endpoint10_7(raw), status: Endpoint10_8(raw), cancel: Endpoint10_9(raw) },
+  command: { connect: Endpoint10_8(raw), status: Endpoint10_9(raw), cancel: Endpoint10_10(raw) },
 })
 
 type Endpoint11_0Request = Parameters<RawClient["server.mcp"]["mcp.list"]>[0]

+ 17 - 0
packages/client/src/promise/generated/client.ts

@@ -82,6 +82,8 @@ import type {
   IntegrationListOutput,
   IntegrationGetInput,
   IntegrationGetOutput,
+  IntegrationWellknownAddInput,
+  IntegrationWellknownAddOutput,
   IntegrationConnectKeyInput,
   IntegrationConnectKeyOutput,
   IntegrationOauthConnectInput,
@@ -893,6 +895,21 @@ export function make(options: ClientOptions) {
           },
           requestOptions,
         ),
+      wellknown: {
+        add: (input: IntegrationWellknownAddInput, requestOptions?: RequestOptions) =>
+          request<IntegrationWellknownAddOutput>(
+            {
+              method: "POST",
+              path: `/api/experimental/integration/wellknown`,
+              query: { location: input["location"] },
+              body: { url: input["url"] },
+              successStatus: 204,
+              declaredStatuses: [400, 401],
+              empty: true,
+            },
+            requestOptions,
+          ),
+      },
       connect: {
         key: (input: IntegrationConnectKeyInput, requestOptions?: RequestOptions) =>
           request<IntegrationConnectKeyOutput>(

+ 9 - 0
packages/client/src/promise/generated/types.ts

@@ -3304,6 +3304,15 @@ export type IntegrationGetOutput = {
   data: IntegrationInfo | null
 }
 
+export type IntegrationWellknownAddInput = {
+  readonly location?: {
+    readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
+  }["location"]
+  readonly url: { readonly url: string }["url"]
+}
+
+export type IntegrationWellknownAddOutput = void
+
 export type IntegrationConnectKeyInput = {
   readonly integrationID: { readonly integrationID: string }["integrationID"]
   readonly location?: {

+ 24 - 1
packages/client/test/promise.test.ts

@@ -36,7 +36,8 @@ test("exposes every standard HTTP API group", () => {
   expect(Object.keys(client.debug)).toEqual(["location"])
   expect(Object.keys(client.debug.location)).toEqual(["list", "evict"])
   expect(Object.keys(client.message)).toEqual(["list"])
-  expect(Object.keys(client.integration)).toEqual(["list", "get", "connect", "oauth", "command"])
+  expect(Object.keys(client.integration)).toEqual(["list", "get", "wellknown", "connect", "oauth", "command"])
+  expect(Object.keys(client.integration.wellknown)).toEqual(["add"])
   expect(Object.keys(client.integration.connect)).toEqual(["key"])
   expect(Object.keys(client.integration.oauth)).toEqual(["connect", "status", "complete", "cancel"])
   expect(Object.keys(client.integration.command)).toEqual(["connect", "status", "cancel"])
@@ -62,6 +63,28 @@ test("server.get uses the public HTTP contract", async () => {
   expect(request?.url).toBe("http://localhost:3000/api/server")
 })
 
+test("experimental wellknown integration add uses the public HTTP contract", async () => {
+  let request: Request | undefined
+  const client = OpenCode.make({
+    baseUrl: "http://localhost:3000",
+    fetch: async (input, init) => {
+      request = input instanceof Request ? input : new Request(input, init)
+      return new Response(null, { status: 204 })
+    },
+  })
+
+  await client.integration.wellknown.add({
+    url: "https://example.com",
+    location: { directory: "/tmp/project" },
+  })
+
+  expect(request?.method).toBe("POST")
+  expect(request?.url).toBe(
+    "http://localhost:3000/api/experimental/integration/wellknown?location%5Bdirectory%5D=%2Ftmp%2Fproject",
+  )
+  expect(await request?.json()).toEqual({ url: "https://example.com" })
+})
+
 test("health.stop sends exact replacement identity", async () => {
   let request: Request | undefined
   const client = OpenCode.make({

+ 55 - 2
packages/core/schema.json

@@ -1,9 +1,9 @@
 {
   "version": "7",
   "dialect": "sqlite",
-  "id": "5f0a1db8-d4bf-42c3-becb-96b46fe66bed",
+  "id": "a4ba73b4-21bc-41ab-a415-94e2ca38d798",
   "prevIds": [
-    "666138ef-82cb-4a9a-a765-e6669a436ff3"
+    "5f0a1db8-d4bf-42c3-becb-96b46fe66bed"
   ],
   "ddl": [
     {
@@ -38,6 +38,10 @@
       "name": "event",
       "entityType": "tables"
     },
+    {
+      "name": "kv",
+      "entityType": "tables"
+    },
     {
       "name": "permission",
       "entityType": "tables"
@@ -556,6 +560,46 @@
       "entityType": "columns",
       "table": "event"
     },
+    {
+      "type": "text",
+      "notNull": false,
+      "autoincrement": false,
+      "default": null,
+      "generated": null,
+      "name": "key",
+      "entityType": "columns",
+      "table": "kv"
+    },
+    {
+      "type": "text",
+      "notNull": true,
+      "autoincrement": false,
+      "default": null,
+      "generated": null,
+      "name": "value",
+      "entityType": "columns",
+      "table": "kv"
+    },
+    {
+      "type": "integer",
+      "notNull": true,
+      "autoincrement": false,
+      "default": null,
+      "generated": null,
+      "name": "time_created",
+      "entityType": "columns",
+      "table": "kv"
+    },
+    {
+      "type": "integer",
+      "notNull": true,
+      "autoincrement": false,
+      "default": null,
+      "generated": null,
+      "name": "time_updated",
+      "entityType": "columns",
+      "table": "kv"
+    },
     {
       "type": "text",
       "notNull": false,
@@ -1844,6 +1888,15 @@
       "table": "event",
       "entityType": "pks"
     },
+    {
+      "columns": [
+        "key"
+      ],
+      "nameExplicit": false,
+      "name": "kv_pk",
+      "table": "kv",
+      "entityType": "pks"
+    },
     {
       "columns": [
         "id"

+ 87 - 17
packages/core/src/config.ts

@@ -6,6 +6,8 @@ import { type ParseError, parse } from "jsonc-parser"
 import { Context, Effect, Fiber, Layer, Option, PubSub, Schema, Stream } from "effect"
 import { Permission } from "@opencode-ai/schema/permission"
 import { Config as ConfigSchema } from "@opencode-ai/schema/config"
+import { Integration } from "@opencode-ai/schema/integration"
+import { Credential } from "./credential"
 import { EventV2 } from "./event"
 import { Watcher } from "./filesystem/watcher"
 import { FSUtil } from "./fs-util"
@@ -28,6 +30,7 @@ import { ConfigVariable } from "./config/variable"
 import { ConfigWatcher } from "./config/watcher"
 import { ConfigV1 } from "./v1/config/config"
 import { ConfigMigrateV1 } from "./v1/config/migrate"
+import { WellKnown } from "./wellknown"
 
 export class Info extends Schema.Class<Info>("Config.Info")({
   $schema: Schema.optional(Schema.String).annotate({
@@ -157,29 +160,67 @@ const layer = Layer.effect(
     const location = yield* Location.Service
     const watcher = yield* Watcher.Service
     const events = yield* EventV2.Service
+    const credentials = yield* Credential.Service
+    const wellknown = yield* WellKnown.Service
     const names = ["opencode.json", "opencode.jsonc"]
     const decodeOptions = { errors: "all", onExcessProperty: "ignore", propertyOrder: "original" } as const
     const decodeInfo = Schema.decodeUnknownOption(Info, decodeOptions)
     const decodeV1Info = Schema.decodeUnknownOption(ConfigV1.Info, decodeOptions)
 
-    const loadFile = Effect.fnUntraced(function* (filepath: string) {
-      const text = yield* fs.readFileStringSafe(filepath)
-      if (!text) return
-      const substituted = yield* ConfigVariable.substitute({ type: "path", path: filepath, text })
-
+    const parseInfo = (text: string) => {
       const errors: ParseError[] = []
-      const input: unknown = parse(substituted, errors, { allowTrailingComma: true })
+      const input: unknown = parse(text, errors, { allowTrailingComma: true })
       if (errors.length) return
-
-      const info = Option.getOrUndefined(
+      return Option.getOrUndefined(
         ConfigMigrateV1.isV1(input)
           ? decodeV1Info(input).pipe(Option.map(ConfigMigrateV1.migrate), Option.flatMap(decodeInfo))
           : decodeInfo(input),
       )
+    }
+
+    const loadFile = Effect.fnUntraced(function* (filepath: string) {
+      const text = yield* fs.readFileStringSafe(filepath)
+      if (!text) return
+      const substituted = yield* ConfigVariable.substitute({ type: "path", path: filepath, text })
+      const info = parseInfo(substituted)
       if (!info) return
       return new Document({ type: "document", path: filepath, info })
     })
 
+    const loadWellknown = Effect.fn("Config.loadWellknown")(function* () {
+      const entries = yield* wellknown
+        .entries()
+        .pipe(
+          Effect.catch((error) =>
+            Effect.logWarning("failed to discover wellknown config", { error }).pipe(Effect.as([] as const)),
+          ),
+        )
+      return yield* Effect.forEach(entries, (entry) =>
+        Effect.gen(function* () {
+          const auth = entry.manifest.auth
+          if (!auth) return []
+          const credential = (yield* credentials.list(entry.integrationID)).findLast(
+            (credential) => credential.value.type === "key",
+          )
+          if (!credential || credential.value.type !== "key") return []
+          const variables = { [auth.env]: credential.value.key }
+          const configs = yield* wellknown.resolve(entry, variables).pipe(Effect.orDie)
+          return yield* Effect.forEach(configs, (config) =>
+            ConfigVariable.substitute({
+              type: "virtual",
+              source: entry.origin,
+              dir: entry.origin,
+              text: JSON.stringify(config),
+              env: variables,
+            }).pipe(
+              Effect.map(parseInfo),
+              Effect.map((info) => (info ? new Document({ type: "document", info }) : undefined)),
+            ),
+          ).pipe(Effect.map((documents) => documents.filter((document) => document !== undefined)))
+        }),
+      ).pipe(Effect.map((documents) => documents.flat()))
+    })
+
     const loadDirectory = Effect.fnUntraced(function* (directory: AbsolutePath) {
       return [
         ...(yield* Effect.forEach(names, (file) => loadFile(path.join(directory, file))).pipe(
@@ -201,7 +242,7 @@ const layer = Layer.effect(
               targets: [".opencode", ".claude", ".agents", ...names.toReversed()],
               start: location.directory,
             })
-          .pipe(Effect.orDie)
+            .pipe(Effect.orDie)
 
       // We load certain files from a few other folders in the ecosystem
       const claude = [
@@ -244,7 +285,14 @@ const layer = Layer.effect(
       )
 
       const supplementary = yield* Effect.forEach(directories, loadDirectory).pipe(Effect.orDie)
-      return [...claude, ...agents, ...(supplementary[0] ?? []), ...direct, ...supplementary.slice(1).flat()]
+      return [
+        ...claude,
+        ...agents,
+        ...(supplementary[0] ?? []),
+        ...direct,
+        ...supplementary.slice(1).flat(),
+        ...(yield* loadWellknown().pipe(Effect.orDie)),
+      ]
     })
 
     const initial = yield* discover()
@@ -276,15 +324,37 @@ const layer = Layer.effect(
       }
     })
 
+    const reload = Effect.fn("Config.reload")(function* () {
+      const next = yield* discover()
+      configs = next
+      yield* reconcile(next)
+      yield* events.publish(ConfigSchema.Event.Updated, {})
+    })
+
     yield* Stream.fromPubSub(updates).pipe(
       Stream.debounce("100 millis"),
       Stream.runForEach((update) =>
-        Effect.gen(function* () {
-          const next = yield* discover()
-          configs = next
-          yield* reconcile(next)
-          yield* events.publish(ConfigSchema.Event.Updated, {})
-        }).pipe(Effect.catchCause((cause) => Effect.logError("failed to reload config", { path: update.path, cause }))),
+        reload().pipe(
+          Effect.catchCause((cause) => Effect.logError("failed to reload config", { path: update.path, cause })),
+        ),
+      ),
+      Effect.forkScoped({ startImmediately: true }),
+    )
+    yield* events.subscribe(Integration.Event.ConnectionUpdated).pipe(
+      Stream.filterEffect((event) =>
+        wellknown.entries().pipe(
+          Effect.map((entries) => entries.some((entry) => entry.integrationID === event.data.integrationID)),
+          Effect.catch(() => Effect.succeed(false)),
+        ),
+      ),
+      Stream.runForEach(() =>
+        reload().pipe(Effect.catchCause((cause) => Effect.logError("failed to reload wellknown config", { cause }))),
+      ),
+      Effect.forkScoped({ startImmediately: true }),
+    )
+    yield* wellknown.changes.pipe(
+      Stream.runForEach(() =>
+        reload().pipe(Effect.catchCause((cause) => Effect.logError("failed to reload wellknown sources", { cause }))),
       ),
       Effect.forkScoped({ startImmediately: true }),
     )
@@ -301,5 +371,5 @@ const layer = Layer.effect(
 export const node = makeLocationNode({
   service: Service,
   layer,
-  deps: [Watcher.node, EventV2.node, FSUtil.node, Global.node, Location.node],
+  deps: [Watcher.node, EventV2.node, FSUtil.node, Global.node, Location.node, Credential.node, WellKnown.node],
 })

+ 1 - 0
packages/core/src/database/migration.gen.ts

@@ -54,5 +54,6 @@ export const migrations = (
     import("./migration/20260709163752_time_suspended"),
     import("./migration/20260709190621_session_pending_table"),
     import("./migration/20260710025429_instruction_sync"),
+    import("./migration/20260716020354_kv"),
   ])
 ).map((module) => module.default) satisfies DatabaseMigration.Migration[]

+ 18 - 0
packages/core/src/database/migration/20260716020354_kv.ts

@@ -0,0 +1,18 @@
+import { Effect } from "effect"
+import type { DatabaseMigration } from "../migration"
+
+export default {
+  id: "20260716020354_kv",
+  up(tx) {
+    return Effect.gen(function* () {
+      yield* tx.run(`
+        CREATE TABLE \`kv\` (
+          \`key\` text PRIMARY KEY,
+          \`value\` text NOT NULL,
+          \`time_created\` integer NOT NULL,
+          \`time_updated\` integer NOT NULL
+        );
+      `)
+    })
+  },
+} satisfies DatabaseMigration.Migration

+ 8 - 0
packages/core/src/database/schema.gen.ts

@@ -87,6 +87,14 @@ export default {
           CONSTRAINT \`fk_event_aggregate_id_event_sequence_aggregate_id_fk\` FOREIGN KEY (\`aggregate_id\`) REFERENCES \`event_sequence\`(\`aggregate_id\`) ON DELETE CASCADE
         );
       `)
+      yield* tx.run(`
+        CREATE TABLE \`kv\` (
+          \`key\` text PRIMARY KEY,
+          \`value\` text NOT NULL,
+          \`time_created\` integer NOT NULL,
+          \`time_updated\` integer NOT NULL
+        );
+      `)
       yield* tx.run(`
         CREATE TABLE \`permission\` (
           \`id\` text PRIMARY KEY,

+ 23 - 16
packages/core/src/integration.ts

@@ -8,6 +8,7 @@ import {
   Duration,
   Effect,
   Exit,
+  Fiber,
   Layer,
   Schedule,
   Schema,
@@ -617,34 +618,40 @@ const layer = Layer.effect(
         }),
       )
 
-      yield* processes
-        .runStream(
+      yield* Effect.gen(function* () {
+        const handle = yield* processes.spawn(
           ChildProcess.make(method.command[0], method.command.slice(1), {
             extendEnv: true,
             stdin: "ignore",
           }),
-          { okExitCodes: [0] },
         )
-        .pipe(
-          Stream.tap((line) =>
+        const stdout = yield* AppProcess.collectStream(handle.stdout, undefined).pipe(Effect.forkScoped)
+        yield* handle.stderr.pipe(
+          Stream.decodeText,
+          Stream.tap((chunk) =>
             SynchronizedRef.update(commandAttempts, (current) => {
               const attempt = current.get(attemptID)
               if (!attempt || attempt.status !== "pending") return current
-              const message = attempt.message ? `${attempt.message}\n${line}` : line
+              const message = (attempt.message ?? "") + chunk
               return new Map(current).set(attemptID, { ...attempt, message })
             }),
           ),
-          Stream.runCollect,
-          Effect.flatMap((lines) => {
-            const credential = Array.from(lines).at(-1)
-            return credential
-              ? Effect.succeed(credential)
-              : Effect.fail(new Error("Authentication command returned no credential"))
-          }),
-          Effect.exit,
-          Effect.flatMap((exit) => settleCommand(attemptID, exit)),
-          Effect.forkIn(attemptScope, { startImmediately: true }),
+          Stream.runDrain,
         )
+        const exitCode = yield* handle.exitCode
+        if (exitCode !== 0) {
+          const attempt = (yield* SynchronizedRef.get(commandAttempts)).get(attemptID)
+          return yield* Effect.fail(new Error(attempt?.message?.trim() || `Authentication command exited ${exitCode}`))
+        }
+        const credential = (yield* Fiber.join(stdout)).buffer.toString("utf8").trim()
+        if (!credential) return yield* Effect.fail(new Error("Authentication command returned no credential"))
+        return credential
+      }).pipe(
+        Scope.provide(attemptScope),
+        Effect.exit,
+        Effect.flatMap((exit) => settleCommand(attemptID, exit)),
+        Effect.forkIn(attemptScope, { startImmediately: true }),
+      )
 
       return CommandAttempt.make({ attemptID, time })
     })

+ 47 - 0
packages/core/src/kv.ts

@@ -0,0 +1,47 @@
+export * as KV from "./kv"
+
+import { eq } from "drizzle-orm"
+import { Context, Effect, Layer, Schema } from "effect"
+import { Database } from "./database/database"
+import { makeGlobalNode } from "./effect/app-node"
+import { KVTable } from "./kv/sql"
+
+export type Value = Schema.Json
+
+export interface Interface {
+  readonly get: (key: string) => Effect.Effect<Value | undefined>
+  readonly set: (key: string, value: Value) => Effect.Effect<void>
+  readonly remove: (key: string) => Effect.Effect<void>
+}
+
+export class Service extends Context.Service<Service, Interface>()("@opencode/v2/KV") {}
+
+const layer = Layer.effect(
+  Service,
+  Effect.gen(function* () {
+    const { db } = yield* Database.Service
+    return Service.of({
+      get: Effect.fn("KV.get")(function* (key) {
+        return (yield* db
+          .select({ value: KVTable.value })
+          .from(KVTable)
+          .where(eq(KVTable.key, key))
+          .get()
+          .pipe(Effect.orDie))?.value
+      }),
+      set: Effect.fn("KV.set")(function* (key, value) {
+        yield* db
+          .insert(KVTable)
+          .values({ key, value })
+          .onConflictDoUpdate({ target: KVTable.key, set: { value, time_updated: Date.now() } })
+          .run()
+          .pipe(Effect.orDie)
+      }),
+      remove: Effect.fn("KV.remove")(function* (key) {
+        yield* db.delete(KVTable).where(eq(KVTable.key, key)).run().pipe(Effect.orDie)
+      }),
+    })
+  }),
+)
+
+export const node = makeGlobalNode({ service: Service, layer, deps: [Database.node] })

+ 9 - 0
packages/core/src/kv/sql.ts

@@ -0,0 +1,9 @@
+import { sqliteTable, text } from "drizzle-orm/sqlite-core"
+import { Timestamps } from "../database/schema.sql"
+import type { KV } from "../kv"
+
+export const KVTable = sqliteTable("kv", {
+  key: text().primaryKey(),
+  value: text({ mode: "json" }).$type<KV.Value>().notNull(),
+  ...Timestamps,
+})

+ 5 - 0
packages/core/src/plugin/internal.ts

@@ -43,6 +43,7 @@ import { SubagentTool } from "../tool/subagent"
 import { Tools } from "../tool/tools"
 import { WebFetchTool } from "../tool/webfetch"
 import { WebSearchTool } from "../tool/websearch"
+import { WellKnown } from "../wellknown"
 import { WriteTool } from "../tool/write"
 import { AgentPlugin } from "./agent"
 import { CommandPlugin } from "./command"
@@ -52,6 +53,7 @@ import { PluginRuntime } from "./runtime"
 import { SkillPlugin } from "./skill"
 import { SystemPromptPlugin } from "./system-prompt"
 import { VariantPlugin } from "./variant"
+import { WellKnownPlugin } from "../wellknown/plugin"
 
 const services = Effect.fn("PluginInternal.services")(function* () {
   const agent = yield* AgentV2.Service
@@ -81,6 +83,7 @@ const services = Effect.fn("PluginInternal.services")(function* () {
   const skill = yield* SkillV2.Service
   const tools = yield* Tools.Service
   const websearch = yield* WebSearchTool.ConfigService
+  const wellknown = yield* WellKnown.Service
   return Context.mergeAll(
     Context.make(AgentV2.Service, agent),
     Context.make(Catalog.Service, catalog),
@@ -109,6 +112,7 @@ const services = Effect.fn("PluginInternal.services")(function* () {
     Context.make(SkillV2.Service, skill),
     Context.make(Tools.Service, tools),
     Context.make(WebSearchTool.ConfigService, websearch),
+    Context.make(WellKnown.Service, wellknown),
   )
 })
 
@@ -119,6 +123,7 @@ export type Requirements = ContextServices<Effect.Success<ReturnType<typeof serv
 export type InternalPlugin = Plugin<Requirements | Scope.Scope>
 
 const pre = [
+  WellKnownPlugin.Plugin,
   AgentPlugin.Plugin,
   CommandPlugin.Plugin,
   SkillPlugin.Plugin,

+ 3 - 3
packages/core/src/plugin/supervisor.ts

@@ -35,6 +35,7 @@ import { SkillV2 } from "../skill"
 import { ReadToolFileSystem } from "../tool/read-filesystem"
 import { ToolRegistry } from "../tool/registry"
 import { WebSearchTool } from "../tool/websearch"
+import { WellKnown } from "../wellknown"
 import { PluginInternal } from "./internal"
 import { PluginRuntime } from "./runtime"
 import { SdkPlugins } from "./sdk"
@@ -163,9 +164,7 @@ const load = Effect.fn("PluginSupervisor.load")(function* (operation: Extract<Op
   if (!entrypoint) return
   // Bun currently ignores query parameters when caching file:// imports.
   const source =
-    operation.mtime === undefined
-      ? entrypoint
-      : `${operation.target.replaceAll("\\", "/")}?mtime=${operation.mtime}`
+    operation.mtime === undefined ? entrypoint : `${operation.target.replaceAll("\\", "/")}?mtime=${operation.mtime}`
   yield* Effect.log({ msg: "loading plugin", id: operation.target, entrypoint: source })
   const mod = yield* Effect.promise(() => import(source))
   const value = (yield* Schema.decodeUnknownEffect(PluginModule)(mod)).default
@@ -296,6 +295,7 @@ export const node = makeLocationNode({
     SkillV2.node,
     ToolRegistry.toolsNode,
     WebSearchTool.configNode,
+    WellKnown.node,
   ],
 })
 

+ 166 - 0
packages/core/src/wellknown.ts

@@ -0,0 +1,166 @@
+export * as WellKnown from "./wellknown"
+
+import { Integration } from "@opencode-ai/schema/integration"
+import { Context, Effect, Layer, PubSub, Ref, Schema, Semaphore, Stream } from "effect"
+import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http"
+import { makeGlobalNode } from "./effect/app-node"
+import { httpClient } from "./effect/app-node-platform"
+import { KV } from "./kv"
+
+export interface Auth extends Schema.Schema.Type<typeof Auth> {}
+export const Auth = Schema.Struct({
+  command: Schema.Array(Schema.String),
+  env: Schema.String,
+}).annotate({ identifier: "WellKnown.Auth" })
+
+export interface RemoteConfig extends Schema.Schema.Type<typeof RemoteConfig> {}
+export const RemoteConfig = Schema.Struct({
+  url: Schema.String,
+  headers: Schema.optional(Schema.Record(Schema.String, Schema.String)),
+}).annotate({ identifier: "WellKnown.RemoteConfig" })
+
+export interface Config extends Schema.Schema.Type<typeof Config> {}
+export const Config = Schema.Record(Schema.String, Schema.Json).annotate({ identifier: "WellKnown.Config" })
+
+export interface Manifest extends Schema.Schema.Type<typeof Manifest> {}
+export const Manifest = Schema.Struct({
+  auth: Schema.optional(Auth),
+  config: Schema.optional(Schema.NullOr(Config)),
+  remote_config: Schema.optional(RemoteConfig),
+}).annotate({ identifier: "WellKnown.Manifest" })
+
+export interface ResolveInput {
+  readonly origin: string
+  readonly variables?: Readonly<Record<string, string>>
+}
+
+export interface Entry {
+  readonly origin: string
+  readonly integrationID: Integration.ID
+  readonly manifest: Manifest
+}
+
+export interface Interface {
+  readonly entries: () => Effect.Effect<readonly Entry[], Error>
+  readonly snapshot: () => readonly Entry[]
+  readonly add: (origin: string) => Effect.Effect<Entry, Error>
+  readonly remove: (origin: string) => Effect.Effect<void>
+  readonly changes: Stream.Stream<void>
+  readonly resolve: (entry: Entry, variables: Readonly<Record<string, string>>) => Effect.Effect<Config[], Error>
+}
+
+export class Service extends Context.Service<Service, Interface>()("@opencode/v2/WellKnown") {}
+
+export const inspect = Effect.fn("WellKnown.inspect")(function* (origin: string) {
+  const url = `${origin.replace(/\/+$/, "")}/.well-known/opencode`
+  const http = HttpClient.filterStatusOk(yield* HttpClient.HttpClient)
+  return yield* http.execute(HttpClientRequest.get(url).pipe(HttpClientRequest.acceptJson)).pipe(
+    Effect.flatMap(HttpClientResponse.schemaBodyJson(Manifest)),
+    Effect.mapError((cause) => new Error(`Failed to load wellknown manifest from ${url}`, { cause })),
+  )
+})
+
+export const resolve = Effect.fn("WellKnown.resolve")(function* (input: ResolveInput) {
+  const manifest = yield* inspect(input.origin)
+  return yield* resolveEntry(
+    { origin: input.origin, integrationID: Integration.ID.make(input.origin.replace(/\/+$/, "")), manifest },
+    input.variables ?? {},
+  )
+})
+
+const resolveEntry = Effect.fnUntraced(function* (entry: Entry, variables: Readonly<Record<string, string>>) {
+  const configs = entry.manifest.config ? [entry.manifest.config] : []
+  if (!entry.manifest.remote_config) return configs
+
+  const substitute = (value: string) =>
+    value.replace(/\{env:([^}]+)\}/g, (_, name: string) => variables[name] ?? process.env[name] ?? "")
+  const url = substitute(entry.manifest.remote_config.url)
+  const headers = Object.fromEntries(
+    Object.entries(entry.manifest.remote_config.headers ?? {}).map(([key, value]) => [key, substitute(value)]),
+  )
+  const http = HttpClient.filterStatusOk(yield* HttpClient.HttpClient)
+  const remote = yield* http
+    .execute(HttpClientRequest.get(url).pipe(HttpClientRequest.acceptJson, HttpClientRequest.setHeaders(headers)))
+    .pipe(
+      Effect.flatMap(HttpClientResponse.schemaBodyJson(Config)),
+      Effect.mapError((cause) => new Error(`Failed to load wellknown remote config from ${url}`, { cause })),
+    )
+  if (Schema.is(Config)(remote.config)) return [...configs, remote.config]
+  return [...configs, remote]
+})
+
+const sourcesKey = "wellknown:sources"
+const Sources = Schema.Array(Schema.String)
+
+const layer = Layer.effect(
+  Service,
+  Effect.gen(function* () {
+    const http = yield* HttpClient.HttpClient
+    const kv = yield* KV.Service
+    const cache = yield* Ref.make(new Map<string, Entry>())
+    const changes = yield* PubSub.unbounded<void>()
+    const lock = Semaphore.makeUnsafe(1)
+
+    const load = Effect.fn("WellKnown.load")(function* () {
+      const value = yield* kv.get(sourcesKey)
+      const origins = Schema.is(Sources)(value) ? value : []
+      const current = yield* Ref.get(cache)
+      const entries = yield* Effect.forEach(origins, (origin) => {
+        const cached = current.get(origin)
+        if (cached) return Effect.succeed(cached)
+        return inspect(origin).pipe(
+          Effect.provideService(HttpClient.HttpClient, http),
+          Effect.map((manifest) => ({ origin, integrationID: Integration.ID.make(origin), manifest })),
+        )
+      })
+      yield* Ref.set(cache, new Map(entries.map((entry) => [entry.origin, entry])))
+      return entries
+    })
+
+    return Service.of({
+      entries: load,
+      snapshot: () => Array.from(Ref.getUnsafe(cache).values()),
+      add: Effect.fn("WellKnown.add")(function* (value) {
+        return yield* lock.withPermit(
+          Effect.gen(function* () {
+            const origin = value.replace(/\/+$/, "")
+            const manifest = yield* inspect(origin).pipe(Effect.provideService(HttpClient.HttpClient, http))
+            if (!manifest.auth) return yield* Effect.fail(new Error(`No authentication method found at ${origin}`))
+            const entry = { origin, integrationID: Integration.ID.make(origin), manifest }
+            const sources = yield* kv.get(sourcesKey)
+            const origins = Schema.is(Sources)(sources) ? sources : []
+            yield* kv.set(sourcesKey, Array.from(new Set([...origins, origin])))
+            yield* Ref.update(cache, (current) => new Map(current).set(origin, entry))
+            yield* PubSub.publish(changes, undefined)
+            return entry
+          }),
+        )
+      }),
+      remove: Effect.fn("WellKnown.remove")(function* (value) {
+        yield* lock.withPermit(
+          Effect.gen(function* () {
+            const origin = value.replace(/\/+$/, "")
+            const sources = yield* kv.get(sourcesKey)
+            const origins = Schema.is(Sources)(sources) ? sources : []
+            yield* kv.set(
+              sourcesKey,
+              origins.filter((item) => item !== origin),
+            )
+            yield* Ref.update(cache, (current) => {
+              const next = new Map(current)
+              next.delete(origin)
+              return next
+            })
+            yield* PubSub.publish(changes, undefined)
+          }),
+        )
+      }),
+      changes: Stream.fromPubSub(changes),
+      resolve: Effect.fn("WellKnown.resolveEntry")(function* (entry, variables) {
+        return yield* resolveEntry(entry, variables).pipe(Effect.provideService(HttpClient.HttpClient, http))
+      }),
+    })
+  }),
+)
+
+export const node = makeGlobalNode({ service: Service, layer, deps: [httpClient, KV.node] })

+ 34 - 0
packages/core/src/wellknown/plugin.ts

@@ -0,0 +1,34 @@
+export * as WellKnownPlugin from "./plugin"
+
+import { define } from "@opencode-ai/plugin/v2/effect/plugin"
+import { Effect, Stream } from "effect"
+import { WellKnown } from "../wellknown"
+
+export const Plugin = define({
+  id: "opencode.wellknown",
+  effect: Effect.fn(function* (ctx) {
+    const wellknown = yield* WellKnown.Service
+    yield* wellknown.entries().pipe(Effect.orDie)
+    yield* ctx.integration.transform((draft) => {
+      wellknown.snapshot().forEach((entry) => {
+        if (!entry.manifest.auth) return
+        draft.update(entry.integrationID, (integration) => {
+          integration.name = new URL(entry.origin).hostname
+        })
+        draft.method.update({
+          integrationID: entry.integrationID,
+          method: {
+            id: "login",
+            type: "command",
+            label: "Log in",
+            command: [...entry.manifest.auth.command],
+          },
+        })
+      })
+    })
+    yield* wellknown.changes.pipe(
+      Stream.runForEach(() => ctx.integration.reload()),
+      Effect.forkScoped({ startImmediately: true }),
+    )
+  }),
+})

+ 121 - 3
packages/core/test/config/config.test.ts

@@ -8,7 +8,9 @@ import { ConfigModel } from "@opencode-ai/core/config/model"
 import { Config as ConfigSchema } from "@opencode-ai/schema/config"
 import { ConfigProvider } from "@opencode-ai/core/config/provider"
 import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
+import { makeGlobalNode } from "@opencode-ai/core/effect/app-node"
 import { LayerNode } from "@opencode-ai/core/effect/layer-node"
+import { Credential } from "@opencode-ai/core/credential"
 import { ConfigMigrateV1 } from "@opencode-ai/core/v1/config/migrate"
 import { ConfigV1 } from "@opencode-ai/core/v1/config/config"
 import { FSUtil } from "@opencode-ai/core/fs-util"
@@ -19,6 +21,8 @@ import { Location } from "@opencode-ai/core/location"
 import { Project } from "@opencode-ai/core/project"
 import { ProviderV2 } from "@opencode-ai/core/provider"
 import { AbsolutePath } from "@opencode-ai/core/schema"
+import { WellKnown } from "@opencode-ai/core/wellknown"
+import { Integration } from "@opencode-ai/schema/integration"
 import { location } from "../fixture/location"
 import { tmpdir } from "../fixture/tmpdir"
 import { testEffect } from "../lib/effect"
@@ -26,12 +30,46 @@ import { testEffect } from "../lib/effect"
 const it = testEffect(Layer.empty)
 const selection = Schema.decodeUnknownSync(ConfigModel.Selection)
 
+const emptyCredentialNode = makeGlobalNode({
+  service: Credential.Service,
+  layer: Layer.succeed(
+    Credential.Service,
+    Credential.Service.of({
+      all: () => Effect.succeed([]),
+      list: () => Effect.succeed([]),
+      get: () => Effect.succeed(undefined),
+      create: () => Effect.die("unused Credential.create"),
+      update: () => Effect.die("unused Credential.update"),
+      remove: () => Effect.die("unused Credential.remove"),
+    }),
+  ),
+  deps: [],
+})
+
+const emptyWellknownNode = makeGlobalNode({
+  service: WellKnown.Service,
+  layer: Layer.succeed(
+    WellKnown.Service,
+    WellKnown.Service.of({
+      entries: () => Effect.succeed([]),
+      snapshot: () => [],
+      add: () => Effect.die("unused Wellknown.add"),
+      remove: () => Effect.die("unused Wellknown.remove"),
+      changes: Stream.empty,
+      resolve: () => Effect.die("unused Wellknown.resolve"),
+    }),
+  ),
+  deps: [],
+})
+
 function testLayer(
   directory: string,
   globalDirectory = path.join(directory, "global"),
   projectDirectory = directory,
   vcs?: Project.Vcs,
   watcher?: Layer.Layer<Watcher.Service>,
+  credentialNode = emptyCredentialNode,
+  wellknownNode = emptyWellknownNode,
 ) {
   const locationLayer = Layer.succeed(
     Location.Service,
@@ -45,6 +83,8 @@ function testLayer(
   return AppNodeBuilder.build(LayerNode.group([Config.node, EventV2.node]), [
     [Location.node, locationLayer],
     [Global.node, Global.layerWith({ config: globalDirectory, home: path.join(globalDirectory, "home") })],
+    [Credential.node, credentialNode],
+    [WellKnown.node, wellknownNode],
     ...(watcher ? ([[Watcher.node, watcher]] as const) : []),
   ])
 }
@@ -125,6 +165,86 @@ describe("Config", () => {
     }),
   )
 
+  it.live("loads authenticated wellknown config at highest priority", () =>
+    Effect.acquireUseRelease(
+      Effect.promise(() => tmpdir()),
+      (tmp) =>
+        Effect.gen(function* () {
+          const global = path.join(tmp.path, "global")
+          const project = path.join(tmp.path, "project")
+          yield* Effect.promise(async () => {
+            await fs.mkdir(global, { recursive: true })
+            await fs.mkdir(project, { recursive: true })
+            await fs.writeFile(path.join(global, "opencode.json"), JSON.stringify({ shell: "global" }))
+            await fs.writeFile(path.join(project, "opencode.json"), JSON.stringify({ shell: "project" }))
+          })
+
+          const integrationID = Integration.ID.make("https://example.com")
+          let key = "secret"
+          const credentialNode = makeGlobalNode({
+            service: Credential.Service,
+            layer: Layer.succeed(
+              Credential.Service,
+              Credential.Service.of({
+                all: () => Effect.die("unused Credential.all"),
+                list: () =>
+                  Effect.succeed([
+                    new Credential.Info({
+                      id: Credential.ID.create(),
+                      integrationID,
+                      label: "default",
+                      value: Credential.Key.make({ type: "key", key }),
+                    }),
+                  ]),
+                get: () => Effect.die("unused Credential.get"),
+                create: () => Effect.die("unused Credential.create"),
+                update: () => Effect.die("unused Credential.update"),
+                remove: () => Effect.die("unused Credential.remove"),
+              }),
+            ),
+            deps: [],
+          })
+          const entry: WellKnown.Entry = {
+            origin: "https://example.com",
+            integrationID,
+            manifest: { auth: { command: ["login"], env: "TOKEN" } },
+          }
+          const wellknownNode = makeGlobalNode({
+            service: WellKnown.Service,
+            layer: Layer.succeed(
+              WellKnown.Service,
+              WellKnown.Service.of({
+                entries: () => Effect.succeed([entry]),
+                snapshot: () => [entry],
+                add: () => Effect.die("unused Wellknown.add"),
+                remove: () => Effect.die("unused Wellknown.remove"),
+                changes: Stream.empty,
+                resolve: (_entry, variables) => Effect.succeed([{ shell: variables.TOKEN }]),
+              }),
+            ),
+            deps: [],
+          })
+
+          return yield* Effect.gen(function* () {
+            const config = yield* Config.Service
+            const events = yield* EventV2.Service
+            expect(Config.latest(yield* config.entries(), "shell")).toBe("secret")
+            const updated = yield* events
+              .subscribe(ConfigSchema.Event.Updated)
+              .pipe(Stream.take(1), Stream.runCollect, Effect.forkScoped)
+            yield* Effect.yieldNow
+            key = "next"
+            yield* events.publish(Integration.Event.ConnectionUpdated, { integrationID })
+            expect(yield* Fiber.join(updated)).toHaveLength(1)
+            expect(Config.latest(yield* config.entries(), "shell")).toBe("next")
+          }).pipe(
+            Effect.provide(testLayer(project, global, project, undefined, undefined, credentialNode, wellknownNode)),
+          )
+        }),
+      (tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),
+    ),
+  )
+
   it.effect("detects v1 configuration from any v1-only top-level key", () =>
     Effect.sync(() => {
       expect(ConfigMigrateV1.isV1({ snapshot: false })).toBe(true)
@@ -282,9 +402,7 @@ describe("Config", () => {
             const config = yield* Config.Service
             yield* config.entries()
 
-            expect(targets).toEqual([
-              { type: "directory", path: AbsolutePath.make(path.join(tmp.path, "global")) },
-            ])
+            expect(targets).toEqual([{ type: "directory", path: AbsolutePath.make(path.join(tmp.path, "global")) }])
           }).pipe(Effect.provide(testLayer(tmp.path, undefined, undefined, undefined, watcher)))
         }),
       ),

+ 2 - 2
packages/core/test/integration.test.ts

@@ -181,7 +181,7 @@ describe("Integration", () => {
             command: [
               process.execPath,
               "-e",
-              'console.log("https://example.com/login"); await Bun.sleep(50); console.log("secret")',
+              'console.error("https://example.com/login"); await Bun.sleep(50); console.log("secret")',
             ],
           },
         }),
@@ -192,7 +192,7 @@ describe("Integration", () => {
         integrations.command.status({ integrationID, attemptID: attempt.attemptID }),
         (status) => status.status === "pending" && status.message?.includes("https://example.com/login") === true,
       )
-      expect(pending).toMatchObject({ status: "pending", message: "https://example.com/login" })
+      expect(pending).toMatchObject({ status: "pending", message: "https://example.com/login\n" })
 
       expect(
         yield* eventually(

+ 25 - 0
packages/core/test/kv.test.ts

@@ -0,0 +1,25 @@
+import { describe, expect } from "bun:test"
+import { Effect } from "effect"
+import { KV } from "@opencode-ai/core/kv"
+import { LayerNode } from "@opencode-ai/core/effect/layer-node"
+import { testEffect } from "./lib/effect"
+
+const it = testEffect(LayerNode.compile(KV.node))
+
+describe("KV", () => {
+  it.effect("stores, replaces, and removes JSON values", () =>
+    Effect.gen(function* () {
+      const kv = yield* KV.Service
+      expect(yield* kv.get("wellknown:sources")).toBeUndefined()
+
+      yield* kv.set("wellknown:sources", ["https://example.com"])
+      expect(yield* kv.get("wellknown:sources")).toEqual(["https://example.com"])
+
+      yield* kv.set("wellknown:sources", ["https://example.com", "https://example.org"])
+      expect(yield* kv.get("wellknown:sources")).toEqual(["https://example.com", "https://example.org"])
+
+      yield* kv.remove("wellknown:sources")
+      expect(yield* kv.get("wellknown:sources")).toBeUndefined()
+    }),
+  )
+})

+ 82 - 0
packages/core/test/wellknown.test.ts

@@ -0,0 +1,82 @@
+import { expect } from "bun:test"
+import { Effect, Fiber, Stream } from "effect"
+import { FetchHttpClient } from "effect/unstable/http"
+import { KV } from "@opencode-ai/core/kv"
+import { LayerNode } from "@opencode-ai/core/effect/layer-node"
+import { WellKnown } from "@opencode-ai/core/wellknown"
+import { testEffect } from "./lib/effect"
+
+const it = testEffect(FetchHttpClient.layer)
+const serviceIt = testEffect(LayerNode.compile(LayerNode.group([WellKnown.node, KV.node])))
+
+it.live("loads embedded and remote configuration", () =>
+  Effect.acquireUseRelease(
+    Effect.sync(() =>
+      Bun.serve({
+        port: 0,
+        fetch(request) {
+          const url = new URL(request.url)
+          if (url.pathname === "/.well-known/opencode") {
+            return Response.json({
+              auth: { command: ["login"], env: "TOKEN" },
+              config: { model: "embedded/model" },
+              remote_config: {
+                url: `${url.origin}/config/{env:TOKEN}`,
+                headers: { authorization: "Bearer {env:TOKEN}" },
+              },
+            })
+          }
+          if (url.pathname === "/config/secret" && request.headers.get("authorization") === "Bearer secret") {
+            return Response.json({ config: { model: "remote/model" } })
+          }
+          return new Response("Not found", { status: 404 })
+        },
+      }),
+    ),
+    (server) =>
+      Effect.gen(function* () {
+        const origin = server.url.origin
+        expect(yield* WellKnown.inspect(`${origin}/`)).toEqual({
+          auth: { command: ["login"], env: "TOKEN" },
+          config: { model: "embedded/model" },
+          remote_config: {
+            url: `${origin}/config/{env:TOKEN}`,
+            headers: { authorization: "Bearer {env:TOKEN}" },
+          },
+        })
+        expect(yield* WellKnown.resolve({ origin, variables: { TOKEN: "secret" } })).toEqual([
+          { model: "embedded/model" },
+          { model: "remote/model" },
+        ])
+      }),
+    (server) => Effect.promise(() => server.stop(true)),
+  ),
+)
+
+serviceIt.live("persists sources in one KV value", () =>
+  Effect.acquireUseRelease(
+    Effect.sync(() =>
+      Bun.serve({
+        port: 0,
+        fetch: () => Response.json({ auth: { command: ["login"], env: "TOKEN" } }),
+      }),
+    ),
+    (server) =>
+      Effect.gen(function* () {
+        const wellknown = yield* WellKnown.Service
+        const kv = yield* KV.Service
+        const changed = yield* wellknown.changes.pipe(Stream.take(1), Stream.runCollect, Effect.forkScoped)
+        const entry = yield* wellknown.add(`${server.url.origin}/`)
+
+        expect(entry.origin).toBe(server.url.origin)
+        expect(yield* kv.get("wellknown:sources")).toEqual([server.url.origin])
+        expect(yield* wellknown.entries()).toEqual([entry])
+        expect(yield* Fiber.join(changed)).toHaveLength(1)
+
+        yield* wellknown.remove(server.url.origin)
+        expect(yield* kv.get("wellknown:sources")).toEqual([])
+        expect(yield* wellknown.entries()).toEqual([])
+      }),
+    (server) => Effect.promise(() => server.stop(true)),
+  ),
+)

+ 2 - 2
packages/opencode/test/session/message-v2.test.ts

@@ -1528,7 +1528,7 @@ describe("session.message-v2.fromError", () => {
 
   test("classifies ZlibError from fetch as retryable APIError", () => {
     const zlibError = new Error(
-      'ZlibError fetching "https://opencode.cloudflare.dev/anthropic/messages". For more information, pass `verbose: true` in the second argument to fetch()',
+      'ZlibError fetching "https://example.com/anthropic/messages". For more information, pass `verbose: true` in the second argument to fetch()',
     )
     ;(zlibError as any).code = "ZlibError"
     ;(zlibError as any).errno = 0
@@ -1543,7 +1543,7 @@ describe("session.message-v2.fromError", () => {
 
   test("classifies ZlibError as AbortedError when abort context is provided", () => {
     const zlibError = new Error(
-      'ZlibError fetching "https://opencode.cloudflare.dev/anthropic/messages". For more information, pass `verbose: true` in the second argument to fetch()',
+      'ZlibError fetching "https://example.com/anthropic/messages". For more information, pass `verbose: true` in the second argument to fetch()',
     )
     ;(zlibError as any).code = "ZlibError"
     ;(zlibError as any).errno = 0

+ 1 - 1
packages/plugin/src/v2/effect/integration.ts

@@ -61,7 +61,7 @@ export interface IntegrationDraft {
   }
 }
 
-export interface IntegrationDomain extends IntegrationApi<unknown> {
+export interface IntegrationDomain extends Omit<IntegrationApi<unknown>, "wellknown"> {
   readonly transform: Transform<IntegrationDraft>
   readonly reload: () => Effect.Effect<void>
   readonly connection: {

+ 1 - 1
packages/plugin/src/v2/promise/integration.ts

@@ -5,7 +5,7 @@ import type { Transform } from "./registration.js"
 
 export type { IntegrationDraft, IntegrationMethodRegistration }
 
-export interface IntegrationDomain extends IntegrationApi {
+export interface IntegrationDomain extends Omit<IntegrationApi, "wellknown"> {
   readonly transform: Transform<IntegrationDraft>
   readonly reload: () => Promise<void>
   readonly connection: {

+ 16 - 0
packages/protocol/src/groups/integration.ts

@@ -37,6 +37,22 @@ export const IntegrationGroup = HttpApiGroup.make("server.integration")
         }),
       ),
   )
+  .add(
+    HttpApiEndpoint.post("integration.wellknown.add", "/api/experimental/integration/wellknown", {
+      query: LocationQuery,
+      payload: Schema.Struct({ url: Schema.String }),
+      success: HttpApiSchema.NoContent,
+      error: InvalidRequestError,
+    })
+      .annotateMerge(locationQueryOpenApi)
+      .annotateMerge(
+        OpenApi.annotations({
+          identifier: "v2.experimental.integration.wellknown.add",
+          summary: "Add wellknown integration",
+          description: "Discover and persist an experimental wellknown integration source.",
+        }),
+      ),
+  )
   .add(
     HttpApiEndpoint.post("integration.connect.key", "/api/integration/:integrationID/connect/key", {
       params: { integrationID: Integration.ID },

+ 65 - 0
packages/sdk/js/src/v2/gen/sdk.gen.ts

@@ -280,6 +280,8 @@ import type {
   V2DebugLocationListResponses,
   V2EventSubscribeErrors,
   V2EventSubscribeResponses,
+  V2ExperimentalIntegrationWellknownAddErrors,
+  V2ExperimentalIntegrationWellknownAddResponses,
   V2FormRequestListErrors,
   V2FormRequestListResponses,
   V2FsFindErrors,
@@ -7209,6 +7211,64 @@ export class Integration extends HeyApiClient {
   }
 }
 
+export class Wellknown extends HeyApiClient {
+  /**
+   * Add wellknown integration
+   *
+   * Discover and persist an experimental wellknown integration source.
+   */
+  public add<ThrowOnError extends boolean = false>(
+    parameters?: {
+      location?: {
+        directory?: string | null
+        workspace?: string | null
+      } | null
+      url?: string
+    },
+    options?: Options<never, ThrowOnError>,
+  ) {
+    const params = buildClientParams(
+      [parameters],
+      [
+        {
+          args: [
+            { in: "query", key: "location" },
+            { in: "body", key: "url" },
+          ],
+        },
+      ],
+    )
+    return (options?.client ?? this.client).post<
+      V2ExperimentalIntegrationWellknownAddResponses,
+      V2ExperimentalIntegrationWellknownAddErrors,
+      ThrowOnError
+    >({
+      url: "/api/experimental/integration/wellknown",
+      ...options,
+      ...params,
+      headers: {
+        "Content-Type": "application/json",
+        ...options?.headers,
+        ...params.headers,
+      },
+    })
+  }
+}
+
+export class Integration2 extends HeyApiClient {
+  private _wellknown?: Wellknown
+  get wellknown(): Wellknown {
+    return (this._wellknown ??= new Wellknown({ client: this.client }))
+  }
+}
+
+export class Experimental2 extends HeyApiClient {
+  private _integration?: Integration2
+  get integration(): Integration2 {
+    return (this._integration ??= new Integration2({ client: this.client }))
+  }
+}
+
 export class Resource2 extends HeyApiClient {
   /**
    * List MCP resources
@@ -8500,6 +8560,11 @@ export class V2 extends HeyApiClient {
     return (this._integration ??= new Integration({ client: this.client }))
   }
 
+  private _experimental?: Experimental2
+  get experimental(): Experimental2 {
+    return (this._experimental ??= new Experimental2({ client: this.client }))
+  }
+
   private _mcp?: Mcp2
   get mcp(): Mcp2 {
     return (this._mcp ??= new Mcp2({ client: this.client }))

+ 38 - 0
packages/sdk/js/src/v2/gen/types.gen.ts

@@ -16859,6 +16859,44 @@ export type V2IntegrationGetResponses = {
 
 export type V2IntegrationGetResponse = V2IntegrationGetResponses[keyof V2IntegrationGetResponses]
 
+export type V2ExperimentalIntegrationWellknownAddData = {
+  body: {
+    url: string
+  }
+  path?: never
+  query?: {
+    location?: {
+      directory?: string | null
+      workspace?: string | null
+    } | null
+  }
+  url: "/api/experimental/integration/wellknown"
+}
+
+export type V2ExperimentalIntegrationWellknownAddErrors = {
+  /**
+   * InvalidRequestError
+   */
+  400: InvalidRequestError1 | InvalidRequestErrorV2
+  /**
+   * UnauthorizedError
+   */
+  401: UnauthorizedError
+}
+
+export type V2ExperimentalIntegrationWellknownAddError =
+  V2ExperimentalIntegrationWellknownAddErrors[keyof V2ExperimentalIntegrationWellknownAddErrors]
+
+export type V2ExperimentalIntegrationWellknownAddResponses = {
+  /**
+   * <No Content>
+   */
+  204: void
+}
+
+export type V2ExperimentalIntegrationWellknownAddResponse =
+  V2ExperimentalIntegrationWellknownAddResponses[keyof V2ExperimentalIntegrationWellknownAddResponses]
+
 export type V2IntegrationConnectKeyData = {
   body: {
     key: string

+ 17 - 0
packages/server/src/handlers/integration.ts

@@ -4,6 +4,7 @@ import { HttpApiBuilder, HttpApiSchema } from "effect/unstable/httpapi"
 import { Api } from "../api"
 import { InvalidRequestError } from "@opencode-ai/protocol/errors"
 import { response } from "../location"
+import { WellKnown } from "@opencode-ai/core/wellknown"
 
 const authorize = <A, R>(effect: Effect.Effect<A, Integration.AuthorizationError, R>) =>
   effect.pipe(
@@ -33,6 +34,22 @@ export const IntegrationHandler = HttpApiBuilder.group(Api, "server.integration"
           return yield* response(service.get(ctx.params.integrationID))
         }),
       )
+      .handle(
+        "integration.wellknown.add",
+        Effect.fn(function* (ctx) {
+          const wellknown = yield* WellKnown.Service
+          const integration = yield* Integration.Service
+          yield* wellknown
+            .add(ctx.payload.url)
+            .pipe(
+              Effect.mapError(
+                (error) => new InvalidRequestError({ message: error.message, kind: "well_known_discovery" }),
+              ),
+            )
+          yield* integration.reload()
+          return HttpApiSchema.NoContent.make()
+        }),
+      )
       .handle(
         "integration.connect.key",
         Effect.fn(function* (ctx) {

+ 4 - 3
packages/server/src/routes.ts

@@ -16,6 +16,7 @@ import { SessionRestart } from "@opencode-ai/core/session/execution/restart"
 import { PluginRuntime } from "@opencode-ai/core/plugin/runtime"
 import { SdkPlugins } from "@opencode-ai/core/plugin/sdk"
 import { ToolOutputStore } from "@opencode-ai/core/tool-output-store"
+import { WellKnown } from "@opencode-ai/core/wellknown"
 import { HttpRouter, HttpServer } from "effect/unstable/http"
 import { HttpApiBuilder } from "effect/unstable/httpapi"
 import { Context, Effect, Layer, Option } from "effect"
@@ -44,6 +45,7 @@ const applicationServices = LayerNode.group([
   PermissionSaved.node,
   PtyTicket.node,
   Credential.node,
+  WellKnown.node,
   PtyEnvironment.node,
   LocationServiceMap.node,
   SessionRestart.node,
@@ -85,7 +87,7 @@ function makeRoutes<AuthError, AuthServices>(
     Layer.flatMap((context) => {
       const services = Layer.succeedContext(context)
       const requestServices = Layer.merge(
-        Layer.succeedContext(Context.pick(PermissionSaved.Service, Project.Service)(context)),
+        Layer.succeedContext(Context.pick(PermissionSaved.Service, Project.Service, WellKnown.Service)(context)),
         ServerInfo.layer(serviceURLs),
       )
       return HttpApiBuilder.layer(Api, { openapiPath: "/openapi.json" }).pipe(
@@ -109,5 +111,4 @@ function simulateEnabled() {
   return !!process.env.OPENCODE_SIMULATE
 }
 
-export const webHandler = () =>
-  HttpRouter.toWebHandler(createRoutes().pipe(Layer.provide(HttpServer.layerServices)))
+export const webHandler = () => HttpRouter.toWebHandler(createRoutes().pipe(Layer.provide(HttpServer.layerServices)))

+ 26 - 8
packages/tui/src/component/dialog-integration.tsx

@@ -185,6 +185,8 @@ function CommandStarting(props: {
   const dialog = useDialog()
   const client = useClient()
   const toast = useToast()
+  let closed = false
+  let handedOff = false
 
   onMount(() => {
     void client.api.integration.command
@@ -193,7 +195,16 @@ function CommandStarting(props: {
         methodID: props.method.id,
         location: location(data),
       })
-      .then((result) =>
+      .then((result) => {
+        if (closed) {
+          void client.api.integration.command.cancel({
+            integrationID: props.integration.id,
+            attemptID: result.data.attemptID,
+            location: location(data),
+          })
+          return
+        }
+        handedOff = true
         dialog.replace(() => (
           <CommandPending
             integration={props.integration}
@@ -201,13 +212,17 @@ function CommandStarting(props: {
             attempt={result.data}
             onConnected={props.onConnected}
           />
-        )),
-      )
+        ))
+      })
       .catch((cause) => {
+        if (closed) return
         toast.show({ variant: "error", message: message(cause) })
         dialog.clear()
       })
   })
+  onCleanup(() => {
+    if (!handedOff) closed = true
+  })
 
   return <CommandView title={props.method.label} output="" message="Starting command..." />
 }
@@ -275,9 +290,10 @@ function CommandPending(props: {
 function CommandView(props: { title: string; output: string; message: string }) {
   const dialog = useDialog()
   const { theme } = useTheme()
+  onMount(() => dialog.setSize("large"))
   return (
-    <box paddingLeft={2} paddingRight={2} gap={1} paddingBottom={1}>
-      <box flexDirection="row" justifyContent="space-between">
+    <box gap={1} paddingBottom={1}>
+      <box flexDirection="row" justifyContent="space-between" paddingLeft={2} paddingRight={2}>
         <text attributes={TextAttributes.BOLD} fg={theme.text}>
           {props.title}
         </text>
@@ -285,10 +301,12 @@ function CommandView(props: { title: string; output: string; message: string })
           esc close
         </text>
       </box>
-      <box backgroundColor={theme.backgroundElement} paddingLeft={1} paddingRight={1}>
-        <text fg={theme.text}>{props.output}</text>
+      <box backgroundColor={theme.backgroundElement} paddingLeft={2} paddingRight={2} paddingTop={1} paddingBottom={1}>
+        <text fg={theme.text}>{props.output.trim()}</text>
+      </box>
+      <box paddingLeft={2} paddingRight={2}>
+        <text fg={theme.textMuted}>{props.message}</text>
       </box>
-      <text fg={theme.textMuted}>{props.message}</text>
     </box>
   )
 }