feat(opencode): cut plugin system and remote behavior feeds; add SessionContext assembly and Runner

- delete plugin machinery (loader, install, meta, hooks) and all trigger sites
- fold first-party provider auth into static registry (provider/hooks.ts)
- remove remote instruction URL fetch, skills remote puller, TUI plugin host
- add session/context.ts: single provenance-tagged context assembly point
- add session/runner.ts: admit/context/stream/tools loop with compaction policy
- relocate audit artifacts to audit/ (FORK-AUDIT, instruction docs as evidence)
This commit is contained in:
2026-08-22 16:13:02 -05:00
parent 5645a12361
commit cfca4cec19
77 changed files with 929 additions and 12054 deletions
@@ -10,7 +10,6 @@ import { disposeAllInstances, provideTmpdirInstance } from "../fixture/fixture"
import { testEffect } from "../lib/effect"
import { testProviderConfig } from "../lib/test-provider"
import { Env } from "@/env"
import { Plugin } from "@/plugin"
import { Provider } from "@/provider/provider"
import { ProviderError } from "@/provider/error"
@@ -19,7 +18,7 @@ afterEach(async () => {
})
const it = testEffect(
LayerNode.compile(LayerNode.group([Provider.node, Env.node, Plugin.node, CrossSpawnSpawner.node])),
LayerNode.compile(LayerNode.group([Provider.node, Env.node, CrossSpawnSpawner.node])),
)
it.live("headerTimeout does not abort delayed SSE body after headers arrive", () =>
@@ -9,11 +9,9 @@ import { FSUtil } from "@opencode-ai/core/fs-util"
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
import { Global } from "@opencode-ai/core/global"
import { disposeAllInstances, provideInstanceEffect, tmpdirScoped, TestInstance } from "../fixture/fixture"
import { markPluginDependenciesReady } from "../fixture/plugin"
import { Auth } from "@/auth"
import { Config } from "@/config/config"
import { Env } from "../../src/env"
import { Plugin } from "../../src/plugin/index"
import { Provider } from "@/provider/provider"
import { RuntimeFlags } from "@/effect/runtime-flags"
@@ -67,7 +65,6 @@ const providerLayer = (flags: Partial<RuntimeFlags.Info> = {}) =>
Env.node,
Config.node,
Auth.node,
Plugin.node,
ModelsDev.node,
RuntimeFlags.node,
]),
@@ -84,7 +81,7 @@ const paid = (providers: Record<string, { models: Record<string, { cost: { input
const languageBaseURL = (language: unknown) => (language as { config: { baseURL: string } }).config.baseURL
const it = testEffect(LayerNode.compile(LayerNode.group([Provider.node, Env.node, Plugin.node])))
const it = testEffect(LayerNode.compile(LayerNode.group([Provider.node, Env.node])))
const experimentalModels = testEffect(providerLayer({ enableExperimentalModels: true }))
const alphaProviderConfig = {
@@ -1842,96 +1839,6 @@ const instanceStoreLayer = LayerNode.compile(InstanceStore.node, [
const provideMultiInstance = <A, E, R>(eff: Effect.Effect<A, E, R>) =>
eff.pipe(Effect.provide(instanceStoreLayer), Effect.provide(AppNodeBuilder.build(CrossSpawnSpawner.node)))
it.effect("plugin config providers persist after instance dispose", () =>
Effect.gen(function* () {
const dir = yield* tmpdirScoped()
const configDir = path.join(dir, ".opencode")
const root = path.join(configDir, "plugin")
yield* Effect.promise(() => mkdir(root, { recursive: true }))
yield* Effect.promise(() => markPluginDependenciesReady(configDir))
yield* Effect.promise(() => markPluginDependenciesReady(Global.Path.config))
yield* Effect.promise(() =>
Bun.write(
path.join(root, "demo-provider.ts"),
[
"export default {",
' id: "demo.plugin-provider",',
" server: async () => ({",
" async config(cfg) {",
" cfg.provider ??= {}",
" cfg.provider.demo = {",
' name: "Demo Provider",',
' npm: "@ai-sdk/openai-compatible",',
' api: "https://example.com/v1",',
" models: {",
" chat: {",
' name: "Demo Chat",',
" tool_call: true,",
" limit: { context: 128000, output: 4096 },",
" },",
" },",
" }",
" },",
" }),",
"}",
"",
].join("\n"),
),
)
const loadAndList = Effect.gen(function* () {
const plugin = yield* Plugin.Service
const provider = yield* Provider.Service
yield* plugin.init()
return yield* provider.list()
}).pipe(provideInstanceEffect(dir))
const first = yield* loadAndList
expect(first[ProviderV2.ID.make("demo")]).toBeDefined()
expect(first[ProviderV2.ID.make("demo")].models[ModelV2.ID.make("chat")]).toBeDefined()
yield* Effect.promise(() => disposeAllInstances())
const second = yield* loadAndList
expect(second[ProviderV2.ID.make("demo")]).toBeDefined()
expect(second[ProviderV2.ID.make("demo")].models[ModelV2.ID.make("chat")]).toBeDefined()
}).pipe(provideMultiInstance),
)
it.instance(
"plugin config enabled and disabled providers are honored",
Effect.gen(function* () {
const instance = yield* TestInstance
const configDir = path.join(instance.directory, ".opencode")
const root = path.join(configDir, "plugin")
yield* Effect.promise(() => mkdir(root, { recursive: true }))
yield* Effect.promise(() => markPluginDependenciesReady(configDir))
yield* Effect.promise(() =>
Bun.write(
path.join(root, "provider-filter.ts"),
[
"export default {",
' id: "demo.provider-filter",',
" server: async () => ({",
" async config(cfg) {",
' cfg.enabled_providers = ["anthropic", "openai"]',
' cfg.disabled_providers = ["openai"]',
" },",
" }),",
"}",
"",
].join("\n"),
),
)
yield* set("ANTHROPIC_API_KEY", "test-anthropic-key")
yield* set("OPENAI_API_KEY", "test-openai-key")
const providers = yield* list
expect(providers[ProviderV2.ID.anthropic]).toBeDefined()
expect(providers[ProviderV2.ID.openai]).toBeUndefined()
}),
)
it.effect("opencode loader keeps paid models when config apiKey is present", () =>
Effect.gen(function* () {
const noneDir = yield* tmpdirScoped()
@@ -571,11 +571,6 @@ describe("ProviderTransform.options - gpt-5 textVerbosity", () => {
},
provider: { id: "azure", options: { useCompletionUrls: true } } as any,
auth: undefined,
plugin: {
trigger: (_name: string, _input: unknown, output: unknown) => Effect.succeed(output),
list: () => Effect.succeed([]),
init: () => Effect.void,
} as any,
flags: { outputTokenMax: 32_000, client: "test" } as any,
isWorkflow: false,
}),