From 552269d8f9441335c1e4efffe7afa437d842aeb1 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Fri, 20 Mar 2026 18:38:57 -0400 Subject: [PATCH 1/2] wip: lazy adapter imports to break bundle cycles --- packages/opencode/src/account/index.ts | 24 +++++-------- packages/opencode/src/auth/index.ts | 16 ++++----- packages/opencode/src/config/config.ts | 17 +++++---- packages/opencode/src/effect/run.ts | 21 +++++++++++ packages/opencode/src/effect/runtime.ts | 2 +- packages/opencode/src/file/index.ts | 21 +++++------ packages/opencode/src/file/time.ts | 18 +++++----- packages/opencode/src/format/index.ts | 15 +++----- packages/opencode/src/permission/index.ts | 29 ++++++--------- packages/opencode/src/permission/service.ts | 10 +++--- packages/opencode/src/provider/auth.ts | 14 ++++---- packages/opencode/src/question/index.ts | 23 +++++------- packages/opencode/src/server/routes/file.ts | 11 +++--- .../opencode/src/server/routes/permission.ts | 5 +-- .../opencode/src/server/routes/provider.ts | 9 ++--- .../opencode/src/server/routes/question.ts | 9 ++--- packages/opencode/src/server/server.ts | 8 +++-- packages/opencode/src/share/share-next.ts | 2 +- packages/opencode/src/skill/index.ts | 31 +++++++++++++++- packages/opencode/src/skill/skill.ts | 35 ------------------- packages/opencode/src/snapshot/index.ts | 21 ++++++----- .../opencode/test/share/share-next.test.ts | 6 ++-- 22 files changed, 165 insertions(+), 182 deletions(-) create mode 100644 packages/opencode/src/effect/run.ts delete mode 100644 packages/opencode/src/skill/skill.ts diff --git a/packages/opencode/src/account/index.ts b/packages/opencode/src/account/index.ts index 753b80c5f1..22341f481a 100644 --- a/packages/opencode/src/account/index.ts +++ b/packages/opencode/src/account/index.ts @@ -1,34 +1,26 @@ -import { Effect, Option } from "effect" - -import { Account as S, type AccountError, type AccessToken, AccountID, Info as Model, OrgID } from "./effect" +import { Option } from "effect" +import { run } from "@/effect/run" +import { type AccessToken, AccountID, Info as Model, OrgID } from "./effect" export { AccessToken, AccountID, OrgID } from "./effect" -import { runtime } from "@/effect/runtime" - -function runSync(f: (service: S.Interface) => Effect.Effect) { - return runtime.runSync(S.Service.use(f)) -} - -function runPromise(f: (service: S.Interface) => Effect.Effect) { - return runtime.runPromise(S.Service.use(f)) -} +const svc = () => import("./effect").then((m) => m.Account.Service) export namespace Account { export const Info = Model export type Info = Model - export function active(): Info | undefined { - return Option.getOrUndefined(runSync((service) => service.active())) + export async function active(): Promise { + return Option.getOrUndefined(await run((await svc()).use((s) => s.active()))) } export async function config(accountID: AccountID, orgID: OrgID): Promise | undefined> { - const config = await runPromise((service) => service.config(accountID, orgID)) + const config = await run((await svc()).use((s) => s.config(accountID, orgID))) return Option.getOrUndefined(config) } export async function token(accountID: AccountID): Promise { - const token = await runPromise((service) => service.token(accountID)) + const token = await run((await svc()).use((s) => s.token(accountID))) return Option.getOrUndefined(token) } } diff --git a/packages/opencode/src/auth/index.ts b/packages/opencode/src/auth/index.ts index 411d9dccc0..b8fb00243a 100644 --- a/packages/opencode/src/auth/index.ts +++ b/packages/opencode/src/auth/index.ts @@ -1,13 +1,9 @@ -import { Effect } from "effect" import z from "zod" -import { runtime } from "@/effect/runtime" -import * as S from "./effect" +import { run } from "@/effect/run" export { OAUTH_DUMMY_KEY } from "./effect" -function runPromise(f: (service: S.Auth.Interface) => Effect.Effect) { - return runtime.runPromise(S.Auth.Service.use(f)) -} +const svc = () => import("./effect").then((m) => m.Auth.Service) export namespace Auth { export const Oauth = z @@ -40,18 +36,18 @@ export namespace Auth { export type Info = z.infer export async function get(providerID: string) { - return runPromise((service) => service.get(providerID)) + return run((await svc()).use((s) => s.get(providerID))) } export async function all(): Promise> { - return runPromise((service) => service.all()) + return run((await svc()).use((s) => s.all())) } export async function set(key: string, info: Info) { - return runPromise((service) => service.set(key, info)) + return run((await svc()).use((s) => s.set(key, info))) } export async function remove(key: string) { - return runPromise((service) => service.remove(key)) + return run((await svc()).use((s) => s.remove(key))) } } diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index 47afdfd7d0..09fbcfe19a 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -11,7 +11,6 @@ import fs from "fs/promises" import { lazy } from "../util/lazy" import { NamedError } from "@opencode-ai/util/error" import { Flag } from "../flag/flag" -import { Auth } from "../auth" import { Env } from "../env" import { type ParseError as JsoncParseError, @@ -33,12 +32,14 @@ import { Glob } from "../util/glob" import { PackageRegistry } from "@/bun/registry" import { proxied } from "@/util/proxied" import { iife } from "@/util/iife" -import { Account } from "@/account" import { ConfigPaths } from "./paths" import { Filesystem } from "@/util/filesystem" import { Process } from "@/util/process" import { Lock } from "@/util/lock" +const auth = lazy(() => import("../auth").then((x) => x.Auth)) +const account = lazy(() => import("@/account").then((x) => x.Account)) + export namespace Config { const ModelId = z.string().meta({ $ref: "https://models.dev/model-schema.json#/$defs/Model" }) @@ -76,7 +77,7 @@ export namespace Config { } export const state = Instance.state(async () => { - const auth = await Auth.all() + const entries = await (await auth()).all() // Config loading order (low -> high precedence): https://opencode.ai/docs/config#precedence-order // 1) Remote .well-known/opencode (org defaults) @@ -87,7 +88,7 @@ export namespace Config { // 6) Inline config (OPENCODE_CONFIG_CONTENT) // Managed config directory is enterprise-only and always overrides everything above. let result: Info = {} - for (const [key, value] of Object.entries(auth)) { + for (const [key, value] of Object.entries(entries)) { if (value.type === "wellknown") { const url = key.replace(/\/+$/, "") process.env[value.key] = value.token @@ -177,13 +178,11 @@ export namespace Config { log.debug("loaded custom config from OPENCODE_CONFIG_CONTENT") } - const active = Account.active() + const acct = await account() + const active = await acct.active() if (active?.active_org_id) { try { - const [config, token] = await Promise.all([ - Account.config(active.id, active.active_org_id), - Account.token(active.id), - ]) + const [config, token] = await Promise.all([acct.config(active.id, active.active_org_id), acct.token(active.id)]) if (token) { process.env["OPENCODE_CONSOLE_TOKEN"] = token Env.set("OPENCODE_CONSOLE_TOKEN", token) diff --git a/packages/opencode/src/effect/run.ts b/packages/opencode/src/effect/run.ts new file mode 100644 index 0000000000..3f843a1856 --- /dev/null +++ b/packages/opencode/src/effect/run.ts @@ -0,0 +1,21 @@ +import type { Effect } from "effect" + +/** + * Lazy wrappers that defer the import of @/effect/runtime to call time. + * + * Adapter modules must not eagerly import @/effect/runtime — or even + * their own service modules — because bun's bundler can evaluate them + * before their dependencies have finished initializing. + */ + +/** For global services (Auth, Account, etc.) */ +export async function run(effect: Effect.Effect): Promise { + const { runtime } = await import("@/effect/runtime") + return runtime.runPromise(effect) +} + +/** For instance-scoped services (Skill, Snapshot, Question, etc.) */ +export async function runInstance(effect: Effect.Effect): Promise { + const { runPromiseInstance } = await import("@/effect/runtime") + return runPromiseInstance(effect) +} diff --git a/packages/opencode/src/effect/runtime.ts b/packages/opencode/src/effect/runtime.ts index e6f1f32626..c1d60d48d0 100644 --- a/packages/opencode/src/effect/runtime.ts +++ b/packages/opencode/src/effect/runtime.ts @@ -9,7 +9,7 @@ import { Instance } from "@/project/instance" export const runtime = ManagedRuntime.make( Layer.mergeAll( - Account.defaultLayer, // + Account.defaultLayer, Installation.defaultLayer, Truncate.defaultLayer, Instances.layer, diff --git a/packages/opencode/src/file/index.ts b/packages/opencode/src/file/index.ts index 35a5b5e204..f97fef5f3c 100644 --- a/packages/opencode/src/file/index.ts +++ b/packages/opencode/src/file/index.ts @@ -1,40 +1,37 @@ -import { runPromiseInstance } from "@/effect/runtime" +import { runInstance } from "@/effect/run" import { File as S } from "./service" +const svc = () => import("./service").then((m) => m.File.Service) + export namespace File { export const Info = S.Info export type Info = S.Info - export const Node = S.Node export type Node = S.Node - export const Content = S.Content export type Content = S.Content - export const Event = S.Event - export type Interface = S.Interface - export const Service = S.Service export const layer = S.layer - export function init() { - return runPromiseInstance(S.Service.use((svc) => svc.init())) + export async function init() { + return runInstance((await svc()).use((s) => s.init())) } export async function status() { - return runPromiseInstance(S.Service.use((svc) => svc.status())) + return runInstance((await svc()).use((s) => s.status())) } export async function read(file: string): Promise { - return runPromiseInstance(S.Service.use((svc) => svc.read(file))) + return runInstance((await svc()).use((s) => s.read(file))) } export async function list(dir?: string) { - return runPromiseInstance(S.Service.use((svc) => svc.list(dir))) + return runInstance((await svc()).use((s) => s.list(dir))) } export async function search(input: { query: string; limit?: number; dirs?: boolean; type?: "file" | "directory" }) { - return runPromiseInstance(S.Service.use((svc) => svc.search(input))) + return runInstance((await svc()).use((s) => s.search(input))) } } diff --git a/packages/opencode/src/file/time.ts b/packages/opencode/src/file/time.ts index b6d572fe8b..45fc9dd22f 100644 --- a/packages/opencode/src/file/time.ts +++ b/packages/opencode/src/file/time.ts @@ -1,28 +1,28 @@ -import { runPromiseInstance } from "@/effect/runtime" +import { runInstance } from "@/effect/run" import type { SessionID } from "@/session/schema" import { FileTime as S } from "./time-service" +const svc = () => import("./time-service").then((m) => m.FileTime.Service) + export namespace FileTime { export type Stamp = S.Stamp - export type Interface = S.Interface - export const Service = S.Service export const layer = S.layer - export function read(sessionID: SessionID, file: string) { - return runPromiseInstance(S.Service.use((s) => s.read(sessionID, file))) + export async function read(sessionID: SessionID, file: string) { + return runInstance((await svc()).use((s) => s.read(sessionID, file))) } - export function get(sessionID: SessionID, file: string) { - return runPromiseInstance(S.Service.use((s) => s.get(sessionID, file))) + export async function get(sessionID: SessionID, file: string) { + return runInstance((await svc()).use((s) => s.get(sessionID, file))) } export async function assert(sessionID: SessionID, filepath: string) { - return runPromiseInstance(S.Service.use((s) => s.assert(sessionID, filepath))) + return runInstance((await svc()).use((s) => s.assert(sessionID, filepath))) } export async function withLock(filepath: string, fn: () => Promise): Promise { - return runPromiseInstance(S.Service.use((s) => s.withLock(filepath, fn))) + return runInstance((await svc()).use((s) => s.withLock(filepath, fn))) } } diff --git a/packages/opencode/src/format/index.ts b/packages/opencode/src/format/index.ts index e4381c69b2..33e3e53082 100644 --- a/packages/opencode/src/format/index.ts +++ b/packages/opencode/src/format/index.ts @@ -1,16 +1,9 @@ -import { runPromiseInstance } from "@/effect/runtime" -import { Format as S } from "./service" +import { runInstance } from "@/effect/run" + +const svc = () => import("./service").then((m) => m.Format.Service) export namespace Format { - export const Status = S.Status - export type Status = S.Status - - export type Interface = S.Interface - - export const Service = S.Service - export const layer = S.layer - export async function status() { - return runPromiseInstance(S.Service.use((s) => s.status())) + return runInstance((await svc()).use((s) => s.status())) } } diff --git a/packages/opencode/src/permission/index.ts b/packages/opencode/src/permission/index.ts index 01ac768971..8229be5e13 100644 --- a/packages/opencode/src/permission/index.ts +++ b/packages/opencode/src/permission/index.ts @@ -1,52 +1,43 @@ -import { runPromiseInstance } from "@/effect/runtime" -import { fn } from "@/util/fn" -import z from "zod" +import { runInstance } from "@/effect/run" import { Permission as S } from "./service" +const svc = () => import("./service").then((m) => m.Permission.Service) + export namespace PermissionNext { export const Action = S.Action export type Action = S.Action - export const Rule = S.Rule export type Rule = S.Rule - - export const Ruleset = S.Ruleset export type Ruleset = S.Ruleset - export const Request = S.Request export type Request = S.Request - export const Reply = S.Reply export type Reply = S.Reply - export const Approval = S.Approval - export type Approval = z.infer - export const Event = S.Event - export const RejectedError = S.RejectedError export const CorrectedError = S.CorrectedError export const DeniedError = S.DeniedError export type Error = S.Error - export const AskInput = S.AskInput export const ReplyInput = S.ReplyInput - export type Interface = S.Interface - export const Service = S.Service export const layer = S.layer - export const evaluate = S.evaluate export const fromConfig = S.fromConfig export const merge = S.merge export const disabled = S.disabled - export const ask = fn(S.AskInput, async (input) => runPromiseInstance(S.Service.use((s) => s.ask(input)))) + export async function ask(input: S.AskInput) { + return runInstance((await svc()).use((s) => s.ask(input))) + } - export const reply = fn(S.ReplyInput, async (input) => runPromiseInstance(S.Service.use((s) => s.reply(input)))) + export async function reply(input: S.ReplyInput) { + return runInstance((await svc()).use((s) => s.reply(input))) + } export async function list() { - return runPromiseInstance(S.Service.use((s) => s.list())) + return runInstance((await svc()).use((s) => s.list())) } } diff --git a/packages/opencode/src/permission/service.ts b/packages/opencode/src/permission/service.ts index 08475520b2..0bbacf1932 100644 --- a/packages/opencode/src/permission/service.ts +++ b/packages/opencode/src/permission/service.ts @@ -105,16 +105,18 @@ export namespace Permission { export const AskInput = Request.partial({ id: true }).extend({ ruleset: Ruleset, }) + export type AskInput = z.infer export const ReplyInput = z.object({ requestID: PermissionID.zod, reply: Reply, message: z.string().optional(), }) + export type ReplyInput = z.infer export interface Interface { - readonly ask: (input: z.infer) => Effect.Effect - readonly reply: (input: z.infer) => Effect.Effect + readonly ask: (input: AskInput) => Effect.Effect + readonly reply: (input: ReplyInput) => Effect.Effect readonly list: () => Effect.Effect } @@ -140,7 +142,7 @@ export namespace Permission { const pending = new Map() const approved: Ruleset = row?.data ?? [] - const ask = Effect.fn("Permission.ask")(function* (input: z.infer) { + const ask = Effect.fn("Permission.ask")(function* (input: AskInput) { const { ruleset, ...request } = input let needsAsk = false @@ -176,7 +178,7 @@ export namespace Permission { ) }) - const reply = Effect.fn("Permission.reply")(function* (input: z.infer) { + const reply = Effect.fn("Permission.reply")(function* (input: ReplyInput) { const existing = pending.get(input.requestID) if (!existing) return diff --git a/packages/opencode/src/provider/auth.ts b/packages/opencode/src/provider/auth.ts index 8ede977a59..4e026a7dc0 100644 --- a/packages/opencode/src/provider/auth.ts +++ b/packages/opencode/src/provider/auth.ts @@ -1,30 +1,28 @@ -import { runPromiseInstance } from "@/effect/runtime" +import { runInstance } from "@/effect/run" import { fn } from "@/util/fn" import { ProviderID } from "./schema" import z from "zod" import { ProviderAuth as S } from "./auth-service" +const svc = () => import("./auth-service").then((m) => m.ProviderAuth.Service) + export namespace ProviderAuth { export const Method = S.Method export type Method = S.Method - export const Authorization = S.Authorization export type Authorization = S.Authorization - export const OauthMissing = S.OauthMissing export const OauthCodeMissing = S.OauthCodeMissing export const OauthCallbackFailed = S.OauthCallbackFailed export const ValidationFailed = S.ValidationFailed export type Error = S.Error - export type Interface = S.Interface - export const Service = S.Service export const layer = S.layer export const defaultLayer = S.defaultLayer export async function methods() { - return runPromiseInstance(S.Service.use((svc) => svc.methods())) + return runInstance((await svc()).use((s) => s.methods())) } export const authorize = fn( @@ -34,7 +32,7 @@ export namespace ProviderAuth { inputs: z.record(z.string(), z.string()).optional(), }), async (input): Promise => - runPromiseInstance(S.Service.use((svc) => svc.authorize(input))), + runInstance((await svc()).use((s) => s.authorize(input))), ) export const callback = fn( @@ -43,6 +41,6 @@ export namespace ProviderAuth { method: z.number(), code: z.string().optional(), }), - async (input) => runPromiseInstance(S.Service.use((svc) => svc.callback(input))), + async (input) => runInstance((await svc()).use((s) => s.callback(input))), ) } diff --git a/packages/opencode/src/question/index.ts b/packages/opencode/src/question/index.ts index de00951908..57b6494240 100644 --- a/packages/opencode/src/question/index.ts +++ b/packages/opencode/src/question/index.ts @@ -1,29 +1,24 @@ -import { runPromiseInstance } from "@/effect/runtime" +import { runInstance } from "@/effect/run" import type { MessageID, SessionID } from "@/session/schema" import type { QuestionID } from "./schema" import { Question as S } from "./service" -export namespace Question { - export const Option = S.Option - export type Option = S.Option +const svc = () => import("./service").then((m) => m.Question.Service) +export namespace Question { export const Info = S.Info export type Info = S.Info - export const Request = S.Request export type Request = S.Request - export const Answer = S.Answer export type Answer = S.Answer - export const Reply = S.Reply export type Reply = S.Reply - + export const Option = S.Option + export type Option = S.Option export const Event = S.Event export const RejectedError = S.RejectedError - export type Interface = S.Interface - export const Service = S.Service export const layer = S.layer @@ -32,18 +27,18 @@ export namespace Question { questions: Info[] tool?: { messageID: MessageID; callID: string } }): Promise { - return runPromiseInstance(S.Service.use((s) => s.ask(input))) + return runInstance((await svc()).use((s) => s.ask(input))) } export async function reply(input: { requestID: QuestionID; answers: Answer[] }) { - return runPromiseInstance(S.Service.use((s) => s.reply(input))) + return runInstance((await svc()).use((s) => s.reply(input))) } export async function reject(requestID: QuestionID) { - return runPromiseInstance(S.Service.use((s) => s.reject(requestID))) + return runInstance((await svc()).use((s) => s.reject(requestID))) } export async function list() { - return runPromiseInstance(S.Service.use((s) => s.list())) + return runInstance((await svc()).use((s) => s.list())) } } diff --git a/packages/opencode/src/server/routes/file.ts b/packages/opencode/src/server/routes/file.ts index 60789ef4b7..66d67e61ef 100644 --- a/packages/opencode/src/server/routes/file.ts +++ b/packages/opencode/src/server/routes/file.ts @@ -1,7 +1,8 @@ import { Hono } from "hono" import { describeRoute, validator, resolver } from "hono-openapi" import z from "zod" -import { File } from "../../file" +import { File } from "../../file/service" +import { File as FileApi } from "../../file" import { Ripgrep } from "../../file/ripgrep" import { LSP } from "../../lsp" import { Instance } from "../../project/instance" @@ -73,7 +74,7 @@ export const FileRoutes = lazy(() => const dirs = c.req.valid("query").dirs const type = c.req.valid("query").type const limit = c.req.valid("query").limit - const results = await File.search({ + const results = await FileApi.search({ query, limit: limit ?? 10, dirs: dirs !== "false", @@ -139,7 +140,7 @@ export const FileRoutes = lazy(() => ), async (c) => { const path = c.req.valid("query").path - const content = await File.list(path) + const content = await FileApi.list(path) return c.json(content) }, ) @@ -168,7 +169,7 @@ export const FileRoutes = lazy(() => ), async (c) => { const path = c.req.valid("query").path - const content = await File.read(path) + const content = await FileApi.read(path) return c.json(content) }, ) @@ -190,7 +191,7 @@ export const FileRoutes = lazy(() => }, }), async (c) => { - const content = await File.status() + const content = await FileApi.status() return c.json(content) }, ), diff --git a/packages/opencode/src/server/routes/permission.ts b/packages/opencode/src/server/routes/permission.ts index cc6c26d435..bf340042bd 100644 --- a/packages/opencode/src/server/routes/permission.ts +++ b/packages/opencode/src/server/routes/permission.ts @@ -1,6 +1,7 @@ import { Hono } from "hono" import { describeRoute, validator, resolver } from "hono-openapi" import z from "zod" +import { Permission } from "@/permission/service" import { PermissionNext } from "@/permission" import { PermissionID } from "@/permission/schema" import { errors } from "../error" @@ -32,7 +33,7 @@ export const PermissionRoutes = lazy(() => requestID: PermissionID.zod, }), ), - validator("json", z.object({ reply: PermissionNext.Reply, message: z.string().optional() })), + validator("json", z.object({ reply: Permission.Reply, message: z.string().optional() })), async (c) => { const params = c.req.valid("param") const json = c.req.valid("json") @@ -55,7 +56,7 @@ export const PermissionRoutes = lazy(() => description: "List of pending permissions", content: { "application/json": { - schema: resolver(PermissionNext.Request.array()), + schema: resolver(Permission.Request.array()), }, }, }, diff --git a/packages/opencode/src/server/routes/provider.ts b/packages/opencode/src/server/routes/provider.ts index 64fe34f450..4495451d28 100644 --- a/packages/opencode/src/server/routes/provider.ts +++ b/packages/opencode/src/server/routes/provider.ts @@ -4,7 +4,8 @@ import z from "zod" import { Config } from "../../config/config" import { Provider } from "../../provider/provider" import { ModelsDev } from "../../provider/models" -import { ProviderAuth } from "../../provider/auth" +import { ProviderAuth } from "../../provider/auth-service" +import { ProviderAuth as ProviderAuthApi } from "../../provider/auth" import { ProviderID } from "../../provider/schema" import { mapValues } from "remeda" import { errors } from "../error" @@ -81,7 +82,7 @@ export const ProviderRoutes = lazy(() => }, }), async (c) => { - return c.json(await ProviderAuth.methods()) + return c.json(await ProviderAuthApi.methods()) }, ) .post( @@ -118,7 +119,7 @@ export const ProviderRoutes = lazy(() => async (c) => { const providerID = c.req.valid("param").providerID const { method, inputs } = c.req.valid("json") - const result = await ProviderAuth.authorize({ + const result = await ProviderAuthApi.authorize({ providerID, method, inputs, @@ -160,7 +161,7 @@ export const ProviderRoutes = lazy(() => async (c) => { const providerID = c.req.valid("param").providerID const { method, code } = c.req.valid("json") - await ProviderAuth.callback({ + await ProviderAuthApi.callback({ providerID, method, code, diff --git a/packages/opencode/src/server/routes/question.ts b/packages/opencode/src/server/routes/question.ts index 3fff895fa6..357e51a396 100644 --- a/packages/opencode/src/server/routes/question.ts +++ b/packages/opencode/src/server/routes/question.ts @@ -2,7 +2,8 @@ import { Hono } from "hono" import { describeRoute, validator } from "hono-openapi" import { resolver } from "hono-openapi" import { QuestionID } from "@/question/schema" -import { Question } from "../../question" +import { Question } from "../../question/service" +import { Question as QuestionApi } from "../../question" import z from "zod" import { errors } from "../error" import { lazy } from "../../util/lazy" @@ -27,7 +28,7 @@ export const QuestionRoutes = lazy(() => }, }), async (c) => { - const questions = await Question.list() + const questions = await QuestionApi.list() return c.json(questions) }, ) @@ -59,7 +60,7 @@ export const QuestionRoutes = lazy(() => async (c) => { const params = c.req.valid("param") const json = c.req.valid("json") - await Question.reply({ + await QuestionApi.reply({ requestID: params.requestID, answers: json.answers, }) @@ -92,7 +93,7 @@ export const QuestionRoutes = lazy(() => ), async (c) => { const params = c.req.valid("param") - await Question.reject(params.requestID) + await QuestionApi.reject(params.requestID) return c.json(true) }, ), diff --git a/packages/opencode/src/server/server.ts b/packages/opencode/src/server/server.ts index a68becb1fb..07425c7762 100644 --- a/packages/opencode/src/server/server.ts +++ b/packages/opencode/src/server/server.ts @@ -8,13 +8,15 @@ import z from "zod" import { Provider } from "../provider/provider" import { NamedError } from "@opencode-ai/util/error" import { LSP } from "../lsp" +import { Format as FormatService } from "../format/service" import { Format } from "../format" import { TuiRoutes } from "./routes/tui" import { Instance } from "../project/instance" import { Vcs } from "../project/vcs" import { runPromiseInstance } from "@/effect/runtime" import { Agent } from "../agent/agent" -import { Skill } from "../skill/skill" +import { Skill as SkillService } from "../skill/service" +import { Skill } from "../skill" import { Auth } from "../auth" import { Flag } from "../flag/flag" import { Command } from "../command" @@ -444,7 +446,7 @@ export namespace Server { description: "List of skills", content: { "application/json": { - schema: resolver(Skill.Info.array()), + schema: resolver(SkillService.Info.array()), }, }, }, @@ -487,7 +489,7 @@ export namespace Server { description: "Formatter status", content: { "application/json": { - schema: resolver(Format.Status.array()), + schema: resolver(FormatService.Status.array()), }, }, }, diff --git a/packages/opencode/src/share/share-next.ts b/packages/opencode/src/share/share-next.ts index e911656c90..e331e8fc6a 100644 --- a/packages/opencode/src/share/share-next.ts +++ b/packages/opencode/src/share/share-next.ts @@ -45,7 +45,7 @@ export namespace ShareNext { }> { const headers: Record = {} - const active = Account.active() + const active = await Account.active() if (!active?.active_org_id) { const baseUrl = await Config.get().then((x) => x.enterprise?.url ?? "https://opncd.ai") return { headers, api: legacyApi, baseUrl } diff --git a/packages/opencode/src/skill/index.ts b/packages/opencode/src/skill/index.ts index 67bef3bd38..8dcf5d8044 100644 --- a/packages/opencode/src/skill/index.ts +++ b/packages/opencode/src/skill/index.ts @@ -1 +1,30 @@ -export * from "./skill" +import type { Agent } from "@/agent/agent" +import { runInstance } from "@/effect/run" + +const svc = () => import("./service").then((m) => m.Skill.Service) +const mod = () => import("./service").then((m) => m.Skill) + +export namespace Skill { + export type Info = import("./service").Skill.Info + export type Interface = import("./service").Skill.Interface + + export async function get(name: string) { + return runInstance((await svc()).use((s) => s.get(name))) + } + + export async function all() { + return runInstance((await svc()).use((s) => s.all())) + } + + export async function dirs() { + return runInstance((await svc()).use((s) => s.dirs())) + } + + export async function available(agent?: Agent.Info) { + return runInstance((await svc()).use((s) => s.available(agent))) + } + + export async function fmt(list: Info[], opts: { verbose: boolean }) { + return (await mod()).fmt(list, opts) + } +} diff --git a/packages/opencode/src/skill/skill.ts b/packages/opencode/src/skill/skill.ts deleted file mode 100644 index ed3e0a4b75..0000000000 --- a/packages/opencode/src/skill/skill.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { runPromiseInstance } from "@/effect/runtime" -import type { Agent } from "@/agent/agent" -import { Skill as S } from "./service" - -export namespace Skill { - export const Info = S.Info - export type Info = S.Info - - export const InvalidError = S.InvalidError - export const NameMismatchError = S.NameMismatchError - - export type Interface = S.Interface - - export const Service = S.Service - export const layer = S.layer - export const defaultLayer = S.defaultLayer - - export const fmt = S.fmt - - export async function get(name: string) { - return runPromiseInstance(S.Service.use((skill) => skill.get(name))) - } - - export async function all() { - return runPromiseInstance(S.Service.use((skill) => skill.all())) - } - - export async function dirs() { - return runPromiseInstance(S.Service.use((skill) => skill.dirs())) - } - - export async function available(agent?: Agent.Info) { - return runPromiseInstance(S.Service.use((skill) => skill.available(agent))) - } -} diff --git a/packages/opencode/src/snapshot/index.ts b/packages/opencode/src/snapshot/index.ts index 4f845ca2de..b955c0d8d0 100644 --- a/packages/opencode/src/snapshot/index.ts +++ b/packages/opencode/src/snapshot/index.ts @@ -1,44 +1,43 @@ -import { runPromiseInstance } from "@/effect/runtime" +import { runInstance } from "@/effect/run" import { Snapshot as S } from "./service" +const svc = () => import("./service").then((m) => m.Snapshot.Service) + export namespace Snapshot { export const Patch = S.Patch export type Patch = S.Patch - export const FileDiff = S.FileDiff export type FileDiff = S.FileDiff - export type Interface = S.Interface - export const Service = S.Service export const layer = S.layer export const defaultLayer = S.defaultLayer export async function cleanup() { - return runPromiseInstance(S.Service.use((svc) => svc.cleanup())) + return runInstance((await svc()).use((s) => s.cleanup())) } export async function track() { - return runPromiseInstance(S.Service.use((svc) => svc.track())) + return runInstance((await svc()).use((s) => s.track())) } export async function patch(hash: string) { - return runPromiseInstance(S.Service.use((svc) => svc.patch(hash))) + return runInstance((await svc()).use((s) => s.patch(hash))) } export async function restore(snapshot: string) { - return runPromiseInstance(S.Service.use((svc) => svc.restore(snapshot))) + return runInstance((await svc()).use((s) => s.restore(snapshot))) } export async function revert(patches: Patch[]) { - return runPromiseInstance(S.Service.use((svc) => svc.revert(patches))) + return runInstance((await svc()).use((s) => s.revert(patches))) } export async function diff(hash: string) { - return runPromiseInstance(S.Service.use((svc) => svc.diff(hash))) + return runInstance((await svc()).use((s) => s.diff(hash))) } export async function diffFull(from: string, to: string) { - return runPromiseInstance(S.Service.use((svc) => svc.diffFull(from, to))) + return runInstance((await svc()).use((s) => s.diffFull(from, to))) } } diff --git a/packages/opencode/test/share/share-next.test.ts b/packages/opencode/test/share/share-next.test.ts index 5be5d02450..fc8d511509 100644 --- a/packages/opencode/test/share/share-next.test.ts +++ b/packages/opencode/test/share/share-next.test.ts @@ -7,7 +7,7 @@ test("ShareNext.request uses legacy share API without active org account", async const originalActive = Account.active const originalConfigGet = Config.get - Account.active = mock(() => undefined) + Account.active = mock(async () => undefined) Config.get = mock(async () => ({ enterprise: { url: "https://legacy-share.example.com" } })) try { @@ -29,7 +29,7 @@ test("ShareNext.request uses org share API with auth headers when account is act const originalActive = Account.active const originalToken = Account.token - Account.active = mock(() => ({ + Account.active = mock(async () => ({ id: AccountID.make("account-1"), email: "user@example.com", url: "https://control.example.com", @@ -59,7 +59,7 @@ test("ShareNext.request fails when org account has no token", async () => { const originalActive = Account.active const originalToken = Account.token - Account.active = mock(() => ({ + Account.active = mock(async () => ({ id: AccountID.make("account-1"), email: "user@example.com", url: "https://control.example.com", From e07589ec026e123759a597a91a42a1bb342932bd Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Fri, 20 Mar 2026 18:57:23 -0400 Subject: [PATCH 2/2] fix: lazy runtime imports in facades to break bundle cycles All service facades now use @/effect/run (lazy runtime import) instead of directly importing @/effect/runtime. This breaks the circular dependency chain that caused "undefined is not an object" crashes in bun's bundled binary. - Add src/effect/run.ts with run() and runInstance() lazy wrappers - Strip all facades to runtime-only functions (no schema re-exports) - Consumers that need schemas import from service modules directly - Update specs/effect-migration.md with facade rules and full list --- packages/opencode/specs/effect-migration.md | 67 ++++++-- packages/opencode/src/account/index.ts | 3 +- packages/opencode/src/agent/agent.ts | 2 +- packages/opencode/src/auth/index.ts | 3 +- packages/opencode/src/cli/cmd/account.ts | 10 +- packages/opencode/src/cli/cmd/debug/agent.ts | 2 +- packages/opencode/src/cli/cmd/run.ts | 2 +- packages/opencode/src/config/config.ts | 11 +- packages/opencode/src/effect/run.ts | 15 +- packages/opencode/src/effect/runtime.ts | 2 + packages/opencode/src/file/index.ts | 18 +- packages/opencode/src/file/time.ts | 9 +- packages/opencode/src/format/index.ts | 3 +- packages/opencode/src/installation/index.ts | 17 +- packages/opencode/src/permission/index.ts | 34 +--- packages/opencode/src/provider/auth.ts | 50 ++---- packages/opencode/src/question/index.ts | 27 +-- .../opencode/src/server/routes/session.ts | 3 +- packages/opencode/src/server/server.ts | 2 +- packages/opencode/src/session/processor.ts | 5 +- packages/opencode/src/session/prompt.ts | 7 +- packages/opencode/src/session/revert.ts | 3 +- packages/opencode/src/session/summary.ts | 3 +- packages/opencode/src/session/system.ts | 3 +- packages/opencode/src/skill/index.ts | 11 +- packages/opencode/src/snapshot/index.ts | 16 +- packages/opencode/src/tool/bash.ts | 2 +- packages/opencode/src/tool/plan.ts | 5 +- packages/opencode/src/tool/skill.ts | 3 +- packages/opencode/src/tool/truncate.ts | 20 +-- packages/opencode/test/agent/agent.test.ts | 40 ++--- packages/opencode/test/config/config.test.ts | 2 +- packages/opencode/test/effect/runtime.test.ts | 4 +- packages/opencode/test/format/format.test.ts | 2 +- .../opencode/test/permission-task.test.ts | 108 ++++++------ .../opencode/test/permission/next.test.ts | 162 +++++++++--------- .../opencode/test/question/question.test.ts | 3 +- .../opencode/test/session/message-v2.test.ts | 2 +- packages/opencode/test/tool/bash.test.ts | 44 ++--- packages/opencode/test/tool/edit.test.ts | 4 +- .../test/tool/external-directory.test.ts | 12 +- packages/opencode/test/tool/read.test.ts | 24 +-- packages/opencode/test/tool/skill.test.ts | 4 +- .../opencode/test/tool/truncation.test.ts | 12 +- 44 files changed, 357 insertions(+), 424 deletions(-) diff --git a/packages/opencode/specs/effect-migration.md b/packages/opencode/specs/effect-migration.md index 4f195917fd..00ad0b2555 100644 --- a/packages/opencode/specs/effect-migration.md +++ b/packages/opencode/specs/effect-migration.md @@ -46,35 +46,64 @@ Rules: - Export `defaultLayer` only when wiring dependencies is useful - Use the direct namespace form once the module is fully migrated -## Temporary mixed-mode pattern +## Service / Facade split -Prefer a single namespace whenever possible. +Migrated services are split into two files: -Use a `*Effect` namespace only when there is a real mixed-mode split, usually because a legacy boundary facade still exists or because merging everything immediately would create awkward cycles. +- **Service module** (`service.ts`, `*-service.ts`, or `*-effect.ts`) — contains `Interface`, `Service`, `layer`, `defaultLayer`, schemas, types, errors, and pure helpers. Must **never** import `@/effect/runtime`. +- **Facade** (`index.ts`) — thin async wrapper that calls `runInstance()` or `run()` from `@/effect/run`. Contains **only** runtime-backed convenience functions. No re-exports of schemas, types, Service, layer, or anything else. + +### Facade rules (critical for bundle safety) + +1. **No eager import of `@/effect/runtime`** — use `run()` / `runInstance()` from `@/effect/run` instead, which lazy-imports the runtime. +2. **No eager import of the service module** if the service is in the circular dependency SCC (auth, account, skill, truncate). Use the lazy `svc()` pattern: + ```ts + const svc = () => import("./service").then((m) => m.Foo.Service) + ``` +3. **No value re-exports** — consumers that need schemas, types, `Service`, or `layer` import from the service module directly. +4. **Only async wrapper functions** — each function awaits `svc()` and passes an Effect to `run()` / `runInstance()`. + +### Why + +Bun's bundler flattens all modules into a single file. When a circular dependency exists (`runtime → instances → services → config → auth → runtime`), the bundler picks an arbitrary evaluation order. If a facade eagerly imports `@/effect/runtime` or re-exports values from a service in the SCC, those values may be `undefined` when accessed at module load time — causing `undefined is not an object` crashes. + +The lazy `svc()` + `run()` pattern defers all access to call time, when all modules have finished initializing. + +### Example facade ```ts -export namespace FooEffect { - export interface Interface { - readonly get: (id: FooID) => Effect.Effect +// src/question/index.ts (facade) +import { runInstance } from "@/effect/run" +import type { Question as S } from "./service" + +const svc = () => import("./service").then((m) => m.Question.Service) + +export namespace Question { + export async function ask(input: { ... }): Promise { + return runInstance((await svc()).use((s) => s.ask(input))) } - export class Service extends ServiceMap.Service()("@opencode/Foo") {} - - export const layer = Layer.effect(...) -} -``` - -Then keep the old boundary thin: - -```ts -export namespace Foo { - export function get(id: FooID) { - return runtime.runPromise(FooEffect.Service.use((svc) => svc.get(id))) + export async function list() { + return runInstance((await svc()).use((s) => s.list())) } } ``` -Remove the `Effect` suffix when the boundary split is gone. +### Current facades + +| Facade | Service module | Scope | +|---|---|---| +| `src/question/index.ts` | `src/question/service.ts` | instance | +| `src/permission/index.ts` | `src/permission/service.ts` | instance | +| `src/format/index.ts` | `src/format/service.ts` | instance | +| `src/file/index.ts` | `src/file/service.ts` | instance | +| `src/file/time.ts` | `src/file/time-service.ts` | instance | +| `src/provider/auth.ts` | `src/provider/auth-service.ts` | instance | +| `src/skill/index.ts` | `src/skill/service.ts` | instance | +| `src/snapshot/index.ts` | `src/snapshot/service.ts` | instance | +| `src/auth/index.ts` | `src/auth/effect.ts` | global | +| `src/account/index.ts` | `src/account/effect.ts` | global | +| `src/tool/truncate.ts` | `src/tool/truncate-effect.ts` | global | ## Scheduled Tasks diff --git a/packages/opencode/src/account/index.ts b/packages/opencode/src/account/index.ts index 22341f481a..5a1d90ac2a 100644 --- a/packages/opencode/src/account/index.ts +++ b/packages/opencode/src/account/index.ts @@ -1,10 +1,11 @@ import { Option } from "effect" import { run } from "@/effect/run" +import { lazy } from "@/util/lazy" import { type AccessToken, AccountID, Info as Model, OrgID } from "./effect" export { AccessToken, AccountID, OrgID } from "./effect" -const svc = () => import("./effect").then((m) => m.Account.Service) +const svc = lazy(() => import("./effect").then((m) => m.Account.Service)) export namespace Account { export const Info = Model diff --git a/packages/opencode/src/agent/agent.ts b/packages/opencode/src/agent/agent.ts index 5a629c73e1..ddf1625fb9 100644 --- a/packages/opencode/src/agent/agent.ts +++ b/packages/opencode/src/agent/agent.ts @@ -5,7 +5,7 @@ import { ModelID, ProviderID } from "../provider/schema" import { generateObject, streamObject, type ModelMessage } from "ai" import { SystemPrompt } from "../session/system" import { Instance } from "../project/instance" -import { Truncate } from "../tool/truncate" +import { Truncate } from "../tool/truncate-effect" import { Auth } from "../auth" import { ProviderTransform } from "../provider/transform" diff --git a/packages/opencode/src/auth/index.ts b/packages/opencode/src/auth/index.ts index b8fb00243a..11cc8386e6 100644 --- a/packages/opencode/src/auth/index.ts +++ b/packages/opencode/src/auth/index.ts @@ -1,9 +1,10 @@ import z from "zod" import { run } from "@/effect/run" +import { lazy } from "@/util/lazy" export { OAUTH_DUMMY_KEY } from "./effect" -const svc = () => import("./effect").then((m) => m.Auth.Service) +const svc = lazy(() => import("./effect").then((m) => m.Auth.Service)) export namespace Auth { export const Oauth = z diff --git a/packages/opencode/src/cli/cmd/account.ts b/packages/opencode/src/cli/cmd/account.ts index fb702c95a5..9146b22abd 100644 --- a/packages/opencode/src/cli/cmd/account.ts +++ b/packages/opencode/src/cli/cmd/account.ts @@ -1,7 +1,7 @@ import { cmd } from "./cmd" import { Duration, Effect, Match, Option } from "effect" import { UI } from "../ui" -import { runtime } from "@/effect/runtime" +import { run } from "@/effect/run" import { AccountID, Account, OrgID, PollExpired, type PollResult } from "@/account/effect" import { type AccountError } from "@/account/schema" import * as Prompt from "../effect/prompt" @@ -160,7 +160,7 @@ export const LoginCommand = cmd({ }), async handler(args) { UI.empty() - await runtime.runPromise(loginEffect(args.url)) + await run(loginEffect(args.url)) }, }) @@ -174,7 +174,7 @@ export const LogoutCommand = cmd({ }), async handler(args) { UI.empty() - await runtime.runPromise(logoutEffect(args.email)) + await run(logoutEffect(args.email)) }, }) @@ -183,7 +183,7 @@ export const SwitchCommand = cmd({ describe: false, async handler() { UI.empty() - await runtime.runPromise(switchEffect()) + await run(switchEffect()) }, }) @@ -192,7 +192,7 @@ export const OrgsCommand = cmd({ describe: false, async handler() { UI.empty() - await runtime.runPromise(orgsEffect()) + await run(orgsEffect()) }, }) diff --git a/packages/opencode/src/cli/cmd/debug/agent.ts b/packages/opencode/src/cli/cmd/debug/agent.ts index f33dcc5582..e2389820c9 100644 --- a/packages/opencode/src/cli/cmd/debug/agent.ts +++ b/packages/opencode/src/cli/cmd/debug/agent.ts @@ -7,7 +7,7 @@ import type { MessageV2 } from "../../../session/message-v2" import { MessageID, PartID } from "../../../session/schema" import { ToolRegistry } from "../../../tool/registry" import { Instance } from "../../../project/instance" -import { PermissionNext } from "../../../permission" +import { Permission as PermissionNext } from "../../../permission/service" import { iife } from "../../../util/iife" import { bootstrap } from "../../bootstrap" import { cmd } from "../cmd" diff --git a/packages/opencode/src/cli/cmd/run.ts b/packages/opencode/src/cli/cmd/run.ts index 85b5689daa..5e831404da 100644 --- a/packages/opencode/src/cli/cmd/run.ts +++ b/packages/opencode/src/cli/cmd/run.ts @@ -11,7 +11,7 @@ import { createOpencodeClient, type Message, type OpencodeClient, type ToolPart import { Server } from "../../server/server" import { Provider } from "../../provider/provider" import { Agent } from "../../agent/agent" -import { PermissionNext } from "../../permission" +import { Permission as PermissionNext } from "../../permission/service" import { Tool } from "../../tool/tool" import { GlobTool } from "../../tool/glob" import { GrepTool } from "../../tool/grep" diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index 09fbcfe19a..06302f2ebf 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -37,8 +37,8 @@ import { Filesystem } from "@/util/filesystem" import { Process } from "@/util/process" import { Lock } from "@/util/lock" -const auth = lazy(() => import("../auth").then((x) => x.Auth)) -const account = lazy(() => import("@/account").then((x) => x.Account)) +import { Auth } from "../auth" +import { Account } from "@/account" export namespace Config { const ModelId = z.string().meta({ $ref: "https://models.dev/model-schema.json#/$defs/Model" }) @@ -77,7 +77,7 @@ export namespace Config { } export const state = Instance.state(async () => { - const entries = await (await auth()).all() + const entries = await Auth.all() // Config loading order (low -> high precedence): https://opencode.ai/docs/config#precedence-order // 1) Remote .well-known/opencode (org defaults) @@ -178,11 +178,10 @@ export namespace Config { log.debug("loaded custom config from OPENCODE_CONFIG_CONTENT") } - const acct = await account() - const active = await acct.active() + const active = await Account.active() if (active?.active_org_id) { try { - const [config, token] = await Promise.all([acct.config(active.id, active.active_org_id), acct.token(active.id)]) + const [config, token] = await Promise.all([Account.config(active.id, active.active_org_id), Account.token(active.id)]) if (token) { process.env["OPENCODE_CONSOLE_TOKEN"] = token Env.set("OPENCODE_CONSOLE_TOKEN", token) diff --git a/packages/opencode/src/effect/run.ts b/packages/opencode/src/effect/run.ts index 3f843a1856..526c87a6bf 100644 --- a/packages/opencode/src/effect/run.ts +++ b/packages/opencode/src/effect/run.ts @@ -1,4 +1,7 @@ import type { Effect } from "effect" +import type { GlobalServices } from "@/effect/runtime" +import type { InstanceServices } from "@/effect/instances" +import { lazy } from "@/util/lazy" /** * Lazy wrappers that defer the import of @/effect/runtime to call time. @@ -8,14 +11,14 @@ import type { Effect } from "effect" * before their dependencies have finished initializing. */ +const runtime = lazy(() => import("@/effect/runtime")) + /** For global services (Auth, Account, etc.) */ -export async function run(effect: Effect.Effect): Promise { - const { runtime } = await import("@/effect/runtime") - return runtime.runPromise(effect) +export async function run(effect: Effect.Effect): Promise { + return (await runtime()).runtime.runPromise(effect) } /** For instance-scoped services (Skill, Snapshot, Question, etc.) */ -export async function runInstance(effect: Effect.Effect): Promise { - const { runPromiseInstance } = await import("@/effect/runtime") - return runPromiseInstance(effect) +export async function runInstance(effect: Effect.Effect): Promise { + return (await runtime()).runPromiseInstance(effect) } diff --git a/packages/opencode/src/effect/runtime.ts b/packages/opencode/src/effect/runtime.ts index c1d60d48d0..b5e86f451b 100644 --- a/packages/opencode/src/effect/runtime.ts +++ b/packages/opencode/src/effect/runtime.ts @@ -7,6 +7,8 @@ import { Installation } from "@/installation" import { Truncate } from "@/tool/truncate-effect" import { Instance } from "@/project/instance" +export type GlobalServices = Account.Service | Installation.Service | Truncate.Service | Instances | Auth.Service + export const runtime = ManagedRuntime.make( Layer.mergeAll( Account.defaultLayer, diff --git a/packages/opencode/src/file/index.ts b/packages/opencode/src/file/index.ts index f97fef5f3c..453d4203eb 100644 --- a/packages/opencode/src/file/index.ts +++ b/packages/opencode/src/file/index.ts @@ -1,20 +1,10 @@ import { runInstance } from "@/effect/run" -import { File as S } from "./service" +import { lazy } from "@/util/lazy" +import type { File as S } from "./service" -const svc = () => import("./service").then((m) => m.File.Service) +const svc = lazy(() => import("./service").then((m) => m.File.Service)) export namespace File { - export const Info = S.Info - export type Info = S.Info - export const Node = S.Node - export type Node = S.Node - export const Content = S.Content - export type Content = S.Content - export const Event = S.Event - export type Interface = S.Interface - export const Service = S.Service - export const layer = S.layer - export async function init() { return runInstance((await svc()).use((s) => s.init())) } @@ -23,7 +13,7 @@ export namespace File { return runInstance((await svc()).use((s) => s.status())) } - export async function read(file: string): Promise { + export async function read(file: string): Promise { return runInstance((await svc()).use((s) => s.read(file))) } diff --git a/packages/opencode/src/file/time.ts b/packages/opencode/src/file/time.ts index 45fc9dd22f..595895e92e 100644 --- a/packages/opencode/src/file/time.ts +++ b/packages/opencode/src/file/time.ts @@ -1,15 +1,10 @@ import { runInstance } from "@/effect/run" import type { SessionID } from "@/session/schema" -import { FileTime as S } from "./time-service" +import { lazy } from "@/util/lazy" -const svc = () => import("./time-service").then((m) => m.FileTime.Service) +const svc = lazy(() => import("./time-service").then((m) => m.FileTime.Service)) export namespace FileTime { - export type Stamp = S.Stamp - export type Interface = S.Interface - export const Service = S.Service - export const layer = S.layer - export async function read(sessionID: SessionID, file: string) { return runInstance((await svc()).use((s) => s.read(sessionID, file))) } diff --git a/packages/opencode/src/format/index.ts b/packages/opencode/src/format/index.ts index 33e3e53082..32d0cc8af7 100644 --- a/packages/opencode/src/format/index.ts +++ b/packages/opencode/src/format/index.ts @@ -1,6 +1,7 @@ import { runInstance } from "@/effect/run" +import { lazy } from "@/util/lazy" -const svc = () => import("./service").then((m) => m.Format.Service) +const svc = lazy(() => import("./service").then((m) => m.Format.Service)) export namespace Format { export async function status() { diff --git a/packages/opencode/src/installation/index.ts b/packages/opencode/src/installation/index.ts index d0bd103296..e2babc53e3 100644 --- a/packages/opencode/src/installation/index.ts +++ b/packages/opencode/src/installation/index.ts @@ -1,4 +1,5 @@ import { NodeChildProcessSpawner, NodeFileSystem, NodePath } from "@effect/platform-node" +import { run } from "@/effect/run" import { Effect, Layer, Schema, ServiceMap, Stream } from "effect" import { FetchHttpClient, HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http" import { withTransientReadRetry } from "@/util/effect-http-client" @@ -329,27 +330,19 @@ export namespace Installation { Layer.provide(NodePath.layer), ) - // Legacy adapters — dynamic import avoids circular dependency since - // foundational modules (db.ts, provider/models.ts) import Installation - // at load time, and runtime transitively loads those same modules. - async function runPromise(f: (service: Interface) => Effect.Effect) { - const { runtime } = await import("@/effect/runtime") - return runtime.runPromise(Service.use(f)) - } - export function info(): Promise { - return runPromise((svc) => svc.info()) + return run(Service.use((svc) => svc.info())) } export function method(): Promise { - return runPromise((svc) => svc.method()) + return run(Service.use((svc) => svc.method())) } export function latest(installMethod?: Method): Promise { - return runPromise((svc) => svc.latest(installMethod)) + return run(Service.use((svc) => svc.latest(installMethod))) } export function upgrade(m: Method, target: string): Promise { - return runPromise((svc) => svc.upgrade(m, target)) + return run(Service.use((svc) => svc.upgrade(m, target))) } } diff --git a/packages/opencode/src/permission/index.ts b/packages/opencode/src/permission/index.ts index 8229be5e13..65cf3bf0d8 100644 --- a/packages/opencode/src/permission/index.ts +++ b/packages/opencode/src/permission/index.ts @@ -1,39 +1,15 @@ import { runInstance } from "@/effect/run" -import { Permission as S } from "./service" +import { lazy } from "@/util/lazy" +import type { Permission } from "./service" -const svc = () => import("./service").then((m) => m.Permission.Service) +const svc = lazy(() => import("./service").then((m) => m.Permission.Service)) export namespace PermissionNext { - export const Action = S.Action - export type Action = S.Action - export const Rule = S.Rule - export type Rule = S.Rule - export type Ruleset = S.Ruleset - export const Request = S.Request - export type Request = S.Request - export const Reply = S.Reply - export type Reply = S.Reply - export const Approval = S.Approval - export const Event = S.Event - export const RejectedError = S.RejectedError - export const CorrectedError = S.CorrectedError - export const DeniedError = S.DeniedError - export type Error = S.Error - export const AskInput = S.AskInput - export const ReplyInput = S.ReplyInput - export type Interface = S.Interface - export const Service = S.Service - export const layer = S.layer - export const evaluate = S.evaluate - export const fromConfig = S.fromConfig - export const merge = S.merge - export const disabled = S.disabled - - export async function ask(input: S.AskInput) { + export async function ask(input: Permission.AskInput) { return runInstance((await svc()).use((s) => s.ask(input))) } - export async function reply(input: S.ReplyInput) { + export async function reply(input: Permission.ReplyInput) { return runInstance((await svc()).use((s) => s.reply(input))) } diff --git a/packages/opencode/src/provider/auth.ts b/packages/opencode/src/provider/auth.ts index 4e026a7dc0..23bd9776e3 100644 --- a/packages/opencode/src/provider/auth.ts +++ b/packages/opencode/src/provider/auth.ts @@ -1,46 +1,24 @@ import { runInstance } from "@/effect/run" -import { fn } from "@/util/fn" -import { ProviderID } from "./schema" -import z from "zod" -import { ProviderAuth as S } from "./auth-service" +import { lazy } from "@/util/lazy" +import type { ProviderAuth as S } from "./auth-service" +import type { ProviderID } from "./schema" -const svc = () => import("./auth-service").then((m) => m.ProviderAuth.Service) +const svc = lazy(() => import("./auth-service").then((m) => m.ProviderAuth.Service)) export namespace ProviderAuth { - export const Method = S.Method - export type Method = S.Method - export const Authorization = S.Authorization - export type Authorization = S.Authorization - export const OauthMissing = S.OauthMissing - export const OauthCodeMissing = S.OauthCodeMissing - export const OauthCallbackFailed = S.OauthCallbackFailed - export const ValidationFailed = S.ValidationFailed - export type Error = S.Error - export type Interface = S.Interface - export const Service = S.Service - export const layer = S.layer - export const defaultLayer = S.defaultLayer - export async function methods() { return runInstance((await svc()).use((s) => s.methods())) } - export const authorize = fn( - z.object({ - providerID: ProviderID.zod, - method: z.number(), - inputs: z.record(z.string(), z.string()).optional(), - }), - async (input): Promise => - runInstance((await svc()).use((s) => s.authorize(input))), - ) + export async function authorize(input: { + providerID: ProviderID + method: number + inputs?: Record + }): Promise { + return runInstance((await svc()).use((s) => s.authorize(input))) + } - export const callback = fn( - z.object({ - providerID: ProviderID.zod, - method: z.number(), - code: z.string().optional(), - }), - async (input) => runInstance((await svc()).use((s) => s.callback(input))), - ) + export async function callback(input: { providerID: ProviderID; method: number; code?: string }) { + return runInstance((await svc()).use((s) => s.callback(input))) + } } diff --git a/packages/opencode/src/question/index.ts b/packages/opencode/src/question/index.ts index 57b6494240..7641bcf6bc 100644 --- a/packages/opencode/src/question/index.ts +++ b/packages/opencode/src/question/index.ts @@ -1,36 +1,21 @@ import { runInstance } from "@/effect/run" import type { MessageID, SessionID } from "@/session/schema" +import { lazy } from "@/util/lazy" import type { QuestionID } from "./schema" -import { Question as S } from "./service" +import type { Question as S } from "./service" -const svc = () => import("./service").then((m) => m.Question.Service) +const svc = lazy(() => import("./service").then((m) => m.Question.Service)) export namespace Question { - export const Info = S.Info - export type Info = S.Info - export const Request = S.Request - export type Request = S.Request - export const Answer = S.Answer - export type Answer = S.Answer - export const Reply = S.Reply - export type Reply = S.Reply - export const Option = S.Option - export type Option = S.Option - export const Event = S.Event - export const RejectedError = S.RejectedError - export type Interface = S.Interface - export const Service = S.Service - export const layer = S.layer - export async function ask(input: { sessionID: SessionID - questions: Info[] + questions: S.Info[] tool?: { messageID: MessageID; callID: string } - }): Promise { + }): Promise { return runInstance((await svc()).use((s) => s.ask(input))) } - export async function reply(input: { requestID: QuestionID; answers: Answer[] }) { + export async function reply(input: { requestID: QuestionID; answers: S.Answer[] }) { return runInstance((await svc()).use((s) => s.reply(input))) } diff --git a/packages/opencode/src/server/routes/session.ts b/packages/opencode/src/server/routes/session.ts index 564bb496b5..53c42ecd7d 100644 --- a/packages/opencode/src/server/routes/session.ts +++ b/packages/opencode/src/server/routes/session.ts @@ -14,6 +14,7 @@ import { Todo } from "../../session/todo" import { Agent } from "../../agent/agent" import { Snapshot } from "@/snapshot/service" import { Log } from "../../util/log" +import { Permission } from "@/permission/service" import { PermissionNext } from "@/permission" import { PermissionID } from "@/permission/schema" import { ModelID, ProviderID } from "@/provider/schema" @@ -1010,7 +1011,7 @@ export const SessionRoutes = lazy(() => permissionID: PermissionID.zod, }), ), - validator("json", z.object({ response: PermissionNext.Reply })), + validator("json", z.object({ response: Permission.Reply })), async (c) => { const params = c.req.valid("param") PermissionNext.reply({ diff --git a/packages/opencode/src/server/server.ts b/packages/opencode/src/server/server.ts index 07425c7762..c060fd517b 100644 --- a/packages/opencode/src/server/server.ts +++ b/packages/opencode/src/server/server.ts @@ -13,7 +13,7 @@ import { Format } from "../format" import { TuiRoutes } from "./routes/tui" import { Instance } from "../project/instance" import { Vcs } from "../project/vcs" -import { runPromiseInstance } from "@/effect/runtime" +import { runInstance as runPromiseInstance } from "@/effect/run" import { Agent } from "../agent/agent" import { Skill as SkillService } from "../skill/service" import { Skill } from "../skill" diff --git a/packages/opencode/src/session/processor.ts b/packages/opencode/src/session/processor.ts index 2fe3310ca6..5e9f42bd83 100644 --- a/packages/opencode/src/session/processor.ts +++ b/packages/opencode/src/session/processor.ts @@ -12,7 +12,8 @@ import type { Provider } from "@/provider/provider" import { LLM } from "./llm" import { Config } from "@/config/config" import { SessionCompaction } from "./compaction" -import { PermissionNext } from "@/permission" +import { Permission as PermissionNext } from "@/permission/service" +import { PermissionNext as PermissionNextApi } from "@/permission" import { Question } from "@/question/service" import { PartID } from "./schema" import type { SessionID, MessageID } from "./schema" @@ -163,7 +164,7 @@ export namespace SessionProcessor { ) ) { const agent = await Agent.get(input.assistantMessage.agent) - await PermissionNext.ask({ + await PermissionNextApi.ask({ permission: "doom_loop", patterns: [value.toolName], sessionID: input.assistantMessage.sessionID, diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index bac958ec10..f64bd3bf67 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -41,7 +41,8 @@ import { fn } from "@/util/fn" import { SessionProcessor } from "./processor" import { TaskTool } from "@/tool/task" import { Tool } from "@/tool/tool" -import { PermissionNext } from "@/permission" +import { Permission as PermissionNext } from "@/permission/service" +import { PermissionNext as PermissionNextApi } from "@/permission" import { SessionStatus } from "./status" import { LLM } from "./llm" import { iife } from "@/util/iife" @@ -437,7 +438,7 @@ export namespace SessionPrompt { } satisfies MessageV2.ToolPart)) as MessageV2.ToolPart }, async ask(req) { - await PermissionNext.ask({ + await PermissionNextApi.ask({ ...req, sessionID: sessionID, ruleset: PermissionNext.merge(taskAgent.permission, session.permission ?? []), @@ -781,7 +782,7 @@ export namespace SessionPrompt { } }, async ask(req) { - await PermissionNext.ask({ + await PermissionNextApi.ask({ ...req, sessionID: input.session.id, tool: { messageID: input.processor.message.id, callID: options.toolCallId }, diff --git a/packages/opencode/src/session/revert.ts b/packages/opencode/src/session/revert.ts index c5c9edbbdf..5e122c64fa 100644 --- a/packages/opencode/src/session/revert.ts +++ b/packages/opencode/src/session/revert.ts @@ -1,5 +1,6 @@ import z from "zod" import { SessionID, MessageID, PartID } from "./schema" +import { Snapshot as SnapshotService } from "../snapshot/service" import { Snapshot } from "../snapshot" import { MessageV2 } from "./message-v2" import { Session } from "." @@ -28,7 +29,7 @@ export namespace SessionRevert { const session = await Session.get(input.sessionID) let revert: Session.Info["revert"] - const patches: Snapshot.Patch[] = [] + const patches: SnapshotService.Patch[] = [] for (const msg of all) { if (msg.info.role === "user") lastUser = msg.info const remaining = [] diff --git a/packages/opencode/src/session/summary.ts b/packages/opencode/src/session/summary.ts index 898b93f3f9..860b69100d 100644 --- a/packages/opencode/src/session/summary.ts +++ b/packages/opencode/src/session/summary.ts @@ -5,6 +5,7 @@ import { Session } from "." import { MessageV2 } from "./message-v2" import { Identifier } from "@/id/id" import { SessionID, MessageID } from "./schema" +import { Snapshot as SnapshotService } from "@/snapshot/service" import { Snapshot } from "@/snapshot" import { Storage } from "@/storage/storage" @@ -126,7 +127,7 @@ export namespace SessionSummary { messageID: MessageID.zod.optional(), }), async (input) => { - const diffs = await Storage.read(["session_diff", input.sessionID]).catch(() => []) + const diffs = await Storage.read(["session_diff", input.sessionID]).catch(() => []) const next = diffs.map((item) => { const file = unquoteGitPath(item.file) if (file === item.file) return item diff --git a/packages/opencode/src/session/system.ts b/packages/opencode/src/session/system.ts index ead715cfb7..21c9fe7520 100644 --- a/packages/opencode/src/session/system.ts +++ b/packages/opencode/src/session/system.ts @@ -13,6 +13,7 @@ import type { Provider } from "@/provider/provider" import type { Agent } from "@/agent/agent" import { Permission as PermissionNext } from "@/permission/service" import { Skill } from "@/skill" +import { Skill as SkillService } from "@/skill/service" export namespace SystemPrompt { export function provider(model: Provider.Model) { @@ -62,7 +63,7 @@ export namespace SystemPrompt { "Use the skill tool to load a skill when a task matches its description.", // the agents seem to ingest the information about skills a bit better if we present a more verbose // version of them here and a less verbose version in tool description, rather than vice versa. - Skill.fmt(list, { verbose: true }), + SkillService.fmt(list, { verbose: true }), ].join("\n") } } diff --git a/packages/opencode/src/skill/index.ts b/packages/opencode/src/skill/index.ts index 8dcf5d8044..21c217347d 100644 --- a/packages/opencode/src/skill/index.ts +++ b/packages/opencode/src/skill/index.ts @@ -1,13 +1,10 @@ import type { Agent } from "@/agent/agent" import { runInstance } from "@/effect/run" +import { lazy } from "@/util/lazy" -const svc = () => import("./service").then((m) => m.Skill.Service) -const mod = () => import("./service").then((m) => m.Skill) +const svc = lazy(() => import("./service").then((m) => m.Skill.Service)) export namespace Skill { - export type Info = import("./service").Skill.Info - export type Interface = import("./service").Skill.Interface - export async function get(name: string) { return runInstance((await svc()).use((s) => s.get(name))) } @@ -23,8 +20,4 @@ export namespace Skill { export async function available(agent?: Agent.Info) { return runInstance((await svc()).use((s) => s.available(agent))) } - - export async function fmt(list: Info[], opts: { verbose: boolean }) { - return (await mod()).fmt(list, opts) - } } diff --git a/packages/opencode/src/snapshot/index.ts b/packages/opencode/src/snapshot/index.ts index b955c0d8d0..76367961a9 100644 --- a/packages/opencode/src/snapshot/index.ts +++ b/packages/opencode/src/snapshot/index.ts @@ -1,18 +1,10 @@ import { runInstance } from "@/effect/run" -import { Snapshot as S } from "./service" +import { lazy } from "@/util/lazy" +import type { Snapshot as S } from "./service" -const svc = () => import("./service").then((m) => m.Snapshot.Service) +const svc = lazy(() => import("./service").then((m) => m.Snapshot.Service)) export namespace Snapshot { - export const Patch = S.Patch - export type Patch = S.Patch - export const FileDiff = S.FileDiff - export type FileDiff = S.FileDiff - export type Interface = S.Interface - export const Service = S.Service - export const layer = S.layer - export const defaultLayer = S.defaultLayer - export async function cleanup() { return runInstance((await svc()).use((s) => s.cleanup())) } @@ -29,7 +21,7 @@ export namespace Snapshot { return runInstance((await svc()).use((s) => s.restore(snapshot))) } - export async function revert(patches: Patch[]) { + export async function revert(patches: S.Patch[]) { return runInstance((await svc()).use((s) => s.revert(patches))) } diff --git a/packages/opencode/src/tool/bash.ts b/packages/opencode/src/tool/bash.ts index 50ae4abac8..88a71a010a 100644 --- a/packages/opencode/src/tool/bash.ts +++ b/packages/opencode/src/tool/bash.ts @@ -15,7 +15,7 @@ import { Flag } from "@/flag/flag.ts" import { Shell } from "@/shell/shell" import { BashArity } from "@/permission/arity" -import { Truncate } from "./truncate" +import { Truncate } from "./truncate-effect" import { Plugin } from "@/plugin" const MAX_METADATA_LENGTH = 30_000 diff --git a/packages/opencode/src/tool/plan.ts b/packages/opencode/src/tool/plan.ts index e91bc3faa2..8244c213c0 100644 --- a/packages/opencode/src/tool/plan.ts +++ b/packages/opencode/src/tool/plan.ts @@ -1,6 +1,7 @@ import z from "zod" import path from "path" import { Tool } from "./tool" +import { Question as QuestionService } from "../question/service" import { Question } from "../question" import { Session } from "../session" import { MessageV2 } from "../session/message-v2" @@ -39,7 +40,7 @@ export const PlanExitTool = Tool.define("plan_exit", { }) const answer = answers[0]?.[0] - if (answer === "No") throw new Question.RejectedError() + if (answer === "No") throw new QuestionService.RejectedError() const model = await getLastModel(ctx.sessionID) @@ -97,7 +98,7 @@ export const PlanEnterTool = Tool.define("plan_enter", { const answer = answers[0]?.[0] - if (answer === "No") throw new Question.RejectedError() + if (answer === "No") throw new QuestionService.RejectedError() const model = await getLastModel(ctx.sessionID) diff --git a/packages/opencode/src/tool/skill.ts b/packages/opencode/src/tool/skill.ts index 17016b06f8..ba35de21a0 100644 --- a/packages/opencode/src/tool/skill.ts +++ b/packages/opencode/src/tool/skill.ts @@ -3,6 +3,7 @@ import { pathToFileURL } from "url" import z from "zod" import { Tool } from "./tool" import { Skill } from "../skill" +import { Skill as SkillService } from "../skill/service" import { Ripgrep } from "../file/ripgrep" import { iife } from "@/util/iife" @@ -24,7 +25,7 @@ export const SkillTool = Tool.define("skill", async (ctx) => { "The following skills provide specialized sets of instructions for particular tasks", "Invoke this tool to load a skill when a task matches one of the available skills listed below:", "", - Skill.fmt(list, { verbose: false }), + SkillService.fmt(list, { verbose: false }), ].join("\n") const examples = list diff --git a/packages/opencode/src/tool/truncate.ts b/packages/opencode/src/tool/truncate.ts index 1710546383..82ed9f3eee 100644 --- a/packages/opencode/src/tool/truncate.ts +++ b/packages/opencode/src/tool/truncate.ts @@ -1,18 +1,12 @@ import type { Agent } from "../agent/agent" -import { runtime } from "@/effect/runtime" -import { Truncate as S } from "./truncate-effect" +import type { Truncate as S } from "./truncate-effect" +import { run } from "@/effect/run" +import { lazy } from "@/util/lazy" + +const svc = lazy(() => import("./truncate-effect").then((m) => m.Truncate.Service)) export namespace Truncate { - export const MAX_LINES = S.MAX_LINES - export const MAX_BYTES = S.MAX_BYTES - export const DIR = S.DIR - export const GLOB = S.GLOB - - export type Result = S.Result - - export type Options = S.Options - - export async function output(text: string, options: Options = {}, agent?: Agent.Info): Promise { - return runtime.runPromise(S.Service.use((s) => s.output(text, options, agent))) + export async function output(text: string, options: S.Options = {}, agent?: Agent.Info): Promise { + return run((await svc()).use((s) => s.output(text, options, agent))) } } diff --git a/packages/opencode/test/agent/agent.test.ts b/packages/opencode/test/agent/agent.test.ts index 60c8e57c92..797f9fbe0a 100644 --- a/packages/opencode/test/agent/agent.test.ts +++ b/packages/opencode/test/agent/agent.test.ts @@ -3,12 +3,12 @@ import path from "path" import { tmpdir } from "../fixture/fixture" import { Instance } from "../../src/project/instance" import { Agent } from "../../src/agent/agent" -import { PermissionNext } from "../../src/permission" +import { Permission } from "../../src/permission/service" // Helper to evaluate permission for a tool with wildcard pattern -function evalPerm(agent: Agent.Info | undefined, permission: string): PermissionNext.Action | undefined { +function evalPerm(agent: Agent.Info | undefined, permission: string): Permission.Action | undefined { if (!agent) return undefined - return PermissionNext.evaluate(permission, "*", agent.permission).action + return Permission.evaluate(permission, "*", agent.permission).action } test("returns default native agents when no config", async () => { @@ -54,7 +54,7 @@ test("plan agent denies edits except .opencode/plans/*", async () => { // Wildcard is denied expect(evalPerm(plan, "edit")).toBe("deny") // But specific path is allowed - expect(PermissionNext.evaluate("edit", ".opencode/plans/foo.md", plan!.permission).action).toBe("allow") + expect(Permission.evaluate("edit", ".opencode/plans/foo.md", plan!.permission).action).toBe("allow") }, }) }) @@ -76,15 +76,15 @@ test("explore agent denies edit and write", async () => { }) test("explore agent asks for external directories and allows Truncate.GLOB", async () => { - const { Truncate } = await import("../../src/tool/truncate") + const { Truncate } = await import("../../src/tool/truncate-effect") await using tmp = await tmpdir() await Instance.provide({ directory: tmp.path, fn: async () => { const explore = await Agent.get("explore") expect(explore).toBeDefined() - expect(PermissionNext.evaluate("external_directory", "/some/other/path", explore!.permission).action).toBe("ask") - expect(PermissionNext.evaluate("external_directory", Truncate.GLOB, explore!.permission).action).toBe("allow") + expect(Permission.evaluate("external_directory", "/some/other/path", explore!.permission).action).toBe("ask") + expect(Permission.evaluate("external_directory", Truncate.GLOB, explore!.permission).action).toBe("allow") }, }) }) @@ -216,7 +216,7 @@ test("agent permission config merges with defaults", async () => { const build = await Agent.get("build") expect(build).toBeDefined() // Specific pattern is denied - expect(PermissionNext.evaluate("bash", "rm -rf *", build!.permission).action).toBe("deny") + expect(Permission.evaluate("bash", "rm -rf *", build!.permission).action).toBe("deny") // Edit still allowed expect(evalPerm(build, "edit")).toBe("allow") }, @@ -489,7 +489,7 @@ test("legacy tools config maps write/edit/patch/multiedit to edit permission", a }) test("Truncate.GLOB is allowed even when user denies external_directory globally", async () => { - const { Truncate } = await import("../../src/tool/truncate") + const { Truncate } = await import("../../src/tool/truncate-effect") await using tmp = await tmpdir({ config: { permission: { @@ -501,15 +501,15 @@ test("Truncate.GLOB is allowed even when user denies external_directory globally directory: tmp.path, fn: async () => { const build = await Agent.get("build") - expect(PermissionNext.evaluate("external_directory", Truncate.GLOB, build!.permission).action).toBe("allow") - expect(PermissionNext.evaluate("external_directory", Truncate.DIR, build!.permission).action).toBe("deny") - expect(PermissionNext.evaluate("external_directory", "/some/other/path", build!.permission).action).toBe("deny") + expect(Permission.evaluate("external_directory", Truncate.GLOB, build!.permission).action).toBe("allow") + expect(Permission.evaluate("external_directory", Truncate.DIR, build!.permission).action).toBe("deny") + expect(Permission.evaluate("external_directory", "/some/other/path", build!.permission).action).toBe("deny") }, }) }) test("Truncate.GLOB is allowed even when user denies external_directory per-agent", async () => { - const { Truncate } = await import("../../src/tool/truncate") + const { Truncate } = await import("../../src/tool/truncate-effect") await using tmp = await tmpdir({ config: { agent: { @@ -525,15 +525,15 @@ test("Truncate.GLOB is allowed even when user denies external_directory per-agen directory: tmp.path, fn: async () => { const build = await Agent.get("build") - expect(PermissionNext.evaluate("external_directory", Truncate.GLOB, build!.permission).action).toBe("allow") - expect(PermissionNext.evaluate("external_directory", Truncate.DIR, build!.permission).action).toBe("deny") - expect(PermissionNext.evaluate("external_directory", "/some/other/path", build!.permission).action).toBe("deny") + expect(Permission.evaluate("external_directory", Truncate.GLOB, build!.permission).action).toBe("allow") + expect(Permission.evaluate("external_directory", Truncate.DIR, build!.permission).action).toBe("deny") + expect(Permission.evaluate("external_directory", "/some/other/path", build!.permission).action).toBe("deny") }, }) }) test("explicit Truncate.GLOB deny is respected", async () => { - const { Truncate } = await import("../../src/tool/truncate") + const { Truncate } = await import("../../src/tool/truncate-effect") await using tmp = await tmpdir({ config: { permission: { @@ -548,8 +548,8 @@ test("explicit Truncate.GLOB deny is respected", async () => { directory: tmp.path, fn: async () => { const build = await Agent.get("build") - expect(PermissionNext.evaluate("external_directory", Truncate.GLOB, build!.permission).action).toBe("deny") - expect(PermissionNext.evaluate("external_directory", Truncate.DIR, build!.permission).action).toBe("deny") + expect(Permission.evaluate("external_directory", Truncate.GLOB, build!.permission).action).toBe("deny") + expect(Permission.evaluate("external_directory", Truncate.DIR, build!.permission).action).toBe("deny") }, }) }) @@ -582,7 +582,7 @@ description: Permission skill. const build = await Agent.get("build") const skillDir = path.join(tmp.path, ".opencode", "skill", "perm-skill") const target = path.join(skillDir, "reference", "notes.md") - expect(PermissionNext.evaluate("external_directory", target, build!.permission).action).toBe("allow") + expect(Permission.evaluate("external_directory", target, build!.permission).action).toBe("allow") }, }) } finally { diff --git a/packages/opencode/test/config/config.test.ts b/packages/opencode/test/config/config.test.ts index baf209d860..eb9c763fa7 100644 --- a/packages/opencode/test/config/config.test.ts +++ b/packages/opencode/test/config/config.test.ts @@ -251,7 +251,7 @@ test("resolves env templates in account config with account token", async () => const originalToken = Account.token const originalControlToken = process.env["OPENCODE_CONSOLE_TOKEN"] - Account.active = mock(() => ({ + Account.active = mock(async () => ({ id: AccountID.make("account-1"), email: "user@example.com", url: "https://control.example.com", diff --git a/packages/opencode/test/effect/runtime.test.ts b/packages/opencode/test/effect/runtime.test.ts index 70bf29aaf3..917afe186f 100644 --- a/packages/opencode/test/effect/runtime.test.ts +++ b/packages/opencode/test/effect/runtime.test.ts @@ -4,9 +4,9 @@ import { runtime, runPromiseInstance } from "../../src/effect/runtime" import { Auth } from "../../src/auth/effect" import { Instances } from "../../src/effect/instances" import { Instance } from "../../src/project/instance" -import { ProviderAuth } from "../../src/provider/auth" +import { ProviderAuth } from "../../src/provider/auth-service" import { Vcs } from "../../src/project/vcs" -import { Question } from "../../src/question" +import { Question } from "../../src/question/service" import { tmpdir } from "../fixture/fixture" /** diff --git a/packages/opencode/test/format/format.test.ts b/packages/opencode/test/format/format.test.ts index 2718e125d0..82b9d8edf9 100644 --- a/packages/opencode/test/format/format.test.ts +++ b/packages/opencode/test/format/format.test.ts @@ -2,7 +2,7 @@ import { Effect } from "effect" import { afterEach, describe, expect, test } from "bun:test" import { tmpdir } from "../fixture/fixture" import { withServices } from "../fixture/instance" -import { Format } from "../../src/format" +import { Format } from "../../src/format/service" import { Instance } from "../../src/project/instance" describe("Format", () => { diff --git a/packages/opencode/test/permission-task.test.ts b/packages/opencode/test/permission-task.test.ts index c78da6e6a5..3531f620b3 100644 --- a/packages/opencode/test/permission-task.test.ts +++ b/packages/opencode/test/permission-task.test.ts @@ -1,11 +1,11 @@ import { describe, test, expect } from "bun:test" -import { PermissionNext } from "../src/permission" +import { Permission } from "../src/permission/service" import { Config } from "../src/config/config" import { Instance } from "../src/project/instance" import { tmpdir } from "./fixture/fixture" -describe("PermissionNext.evaluate for permission.task", () => { - const createRuleset = (rules: Record): PermissionNext.Ruleset => +describe("Permission.evaluate for permission.task", () => { + const createRuleset = (rules: Record): Permission.Ruleset => Object.entries(rules).map(([pattern, action]) => ({ permission: "task", pattern, @@ -13,42 +13,42 @@ describe("PermissionNext.evaluate for permission.task", () => { })) test("returns ask when no match (default)", () => { - expect(PermissionNext.evaluate("task", "code-reviewer", []).action).toBe("ask") + expect(Permission.evaluate("task", "code-reviewer", []).action).toBe("ask") }) test("returns deny for explicit deny", () => { const ruleset = createRuleset({ "code-reviewer": "deny" }) - expect(PermissionNext.evaluate("task", "code-reviewer", ruleset).action).toBe("deny") + expect(Permission.evaluate("task", "code-reviewer", ruleset).action).toBe("deny") }) test("returns allow for explicit allow", () => { const ruleset = createRuleset({ "code-reviewer": "allow" }) - expect(PermissionNext.evaluate("task", "code-reviewer", ruleset).action).toBe("allow") + expect(Permission.evaluate("task", "code-reviewer", ruleset).action).toBe("allow") }) test("returns ask for explicit ask", () => { const ruleset = createRuleset({ "code-reviewer": "ask" }) - expect(PermissionNext.evaluate("task", "code-reviewer", ruleset).action).toBe("ask") + expect(Permission.evaluate("task", "code-reviewer", ruleset).action).toBe("ask") }) test("matches wildcard patterns with deny", () => { const ruleset = createRuleset({ "orchestrator-*": "deny" }) - expect(PermissionNext.evaluate("task", "orchestrator-fast", ruleset).action).toBe("deny") - expect(PermissionNext.evaluate("task", "orchestrator-slow", ruleset).action).toBe("deny") - expect(PermissionNext.evaluate("task", "general", ruleset).action).toBe("ask") + expect(Permission.evaluate("task", "orchestrator-fast", ruleset).action).toBe("deny") + expect(Permission.evaluate("task", "orchestrator-slow", ruleset).action).toBe("deny") + expect(Permission.evaluate("task", "general", ruleset).action).toBe("ask") }) test("matches wildcard patterns with allow", () => { const ruleset = createRuleset({ "orchestrator-*": "allow" }) - expect(PermissionNext.evaluate("task", "orchestrator-fast", ruleset).action).toBe("allow") - expect(PermissionNext.evaluate("task", "orchestrator-slow", ruleset).action).toBe("allow") + expect(Permission.evaluate("task", "orchestrator-fast", ruleset).action).toBe("allow") + expect(Permission.evaluate("task", "orchestrator-slow", ruleset).action).toBe("allow") }) test("matches wildcard patterns with ask", () => { const ruleset = createRuleset({ "orchestrator-*": "ask" }) - expect(PermissionNext.evaluate("task", "orchestrator-fast", ruleset).action).toBe("ask") + expect(Permission.evaluate("task", "orchestrator-fast", ruleset).action).toBe("ask") const globalRuleset = createRuleset({ "*": "ask" }) - expect(PermissionNext.evaluate("task", "code-reviewer", globalRuleset).action).toBe("ask") + expect(Permission.evaluate("task", "code-reviewer", globalRuleset).action).toBe("ask") }) test("later rules take precedence (last match wins)", () => { @@ -56,22 +56,22 @@ describe("PermissionNext.evaluate for permission.task", () => { "orchestrator-*": "deny", "orchestrator-fast": "allow", }) - expect(PermissionNext.evaluate("task", "orchestrator-fast", ruleset).action).toBe("allow") - expect(PermissionNext.evaluate("task", "orchestrator-slow", ruleset).action).toBe("deny") + expect(Permission.evaluate("task", "orchestrator-fast", ruleset).action).toBe("allow") + expect(Permission.evaluate("task", "orchestrator-slow", ruleset).action).toBe("deny") }) test("matches global wildcard", () => { - expect(PermissionNext.evaluate("task", "any-agent", createRuleset({ "*": "allow" })).action).toBe("allow") - expect(PermissionNext.evaluate("task", "any-agent", createRuleset({ "*": "deny" })).action).toBe("deny") - expect(PermissionNext.evaluate("task", "any-agent", createRuleset({ "*": "ask" })).action).toBe("ask") + expect(Permission.evaluate("task", "any-agent", createRuleset({ "*": "allow" })).action).toBe("allow") + expect(Permission.evaluate("task", "any-agent", createRuleset({ "*": "deny" })).action).toBe("deny") + expect(Permission.evaluate("task", "any-agent", createRuleset({ "*": "ask" })).action).toBe("ask") }) }) -describe("PermissionNext.disabled for task tool", () => { +describe("Permission.disabled for task tool", () => { // Note: The `disabled` function checks if a TOOL should be completely removed from the tool list. // It only disables a tool when there's a rule with `pattern: "*"` and `action: "deny"`. // It does NOT evaluate complex subagent patterns - those are handled at runtime by `evaluate`. - const createRuleset = (rules: Record): PermissionNext.Ruleset => + const createRuleset = (rules: Record): Permission.Ruleset => Object.entries(rules).map(([pattern, action]) => ({ permission: "task", pattern, @@ -85,7 +85,7 @@ describe("PermissionNext.disabled for task tool", () => { "orchestrator-*": "allow", "*": "deny", }) - const disabled = PermissionNext.disabled(["task", "bash", "read"], ruleset) + const disabled = Permission.disabled(["task", "bash", "read"], ruleset) // The task tool IS disabled because there's a pattern: "*" with action: "deny" expect(disabled.has("task")).toBe(true) }) @@ -95,14 +95,14 @@ describe("PermissionNext.disabled for task tool", () => { "orchestrator-*": "ask", "*": "deny", }) - const disabled = PermissionNext.disabled(["task"], ruleset) + const disabled = Permission.disabled(["task"], ruleset) // The task tool IS disabled because there's a pattern: "*" with action: "deny" expect(disabled.has("task")).toBe(true) }) test("task tool is disabled when global deny pattern exists", () => { const ruleset = createRuleset({ "*": "deny" }) - const disabled = PermissionNext.disabled(["task"], ruleset) + const disabled = Permission.disabled(["task"], ruleset) expect(disabled.has("task")).toBe(true) }) @@ -113,13 +113,13 @@ describe("PermissionNext.disabled for task tool", () => { "orchestrator-*": "deny", general: "deny", }) - const disabled = PermissionNext.disabled(["task"], ruleset) + const disabled = Permission.disabled(["task"], ruleset) // The task tool is NOT disabled because no rule has pattern: "*" with action: "deny" expect(disabled.has("task")).toBe(false) }) test("task tool is enabled when no task rules exist (default ask)", () => { - const disabled = PermissionNext.disabled(["task"], []) + const disabled = Permission.disabled(["task"], []) expect(disabled.has("task")).toBe(false) }) @@ -129,7 +129,7 @@ describe("PermissionNext.disabled for task tool", () => { "*": "deny", "orchestrator-coder": "allow", }) - const disabled = PermissionNext.disabled(["task"], ruleset) + const disabled = Permission.disabled(["task"], ruleset) // The disabled() function uses findLast and checks if the last matching rule // has pattern: "*" and action: "deny". In this case, the last rule matching // "task" permission has pattern "orchestrator-coder", not "*", so not disabled @@ -155,11 +155,11 @@ describe("permission.task with real config files", () => { directory: tmp.path, fn: async () => { const config = await Config.get() - const ruleset = PermissionNext.fromConfig(config.permission ?? {}) + const ruleset = Permission.fromConfig(config.permission ?? {}) // general and orchestrator-fast should be allowed, code-reviewer denied - expect(PermissionNext.evaluate("task", "general", ruleset).action).toBe("allow") - expect(PermissionNext.evaluate("task", "orchestrator-fast", ruleset).action).toBe("allow") - expect(PermissionNext.evaluate("task", "code-reviewer", ruleset).action).toBe("deny") + expect(Permission.evaluate("task", "general", ruleset).action).toBe("allow") + expect(Permission.evaluate("task", "orchestrator-fast", ruleset).action).toBe("allow") + expect(Permission.evaluate("task", "code-reviewer", ruleset).action).toBe("deny") }, }) }) @@ -180,11 +180,11 @@ describe("permission.task with real config files", () => { directory: tmp.path, fn: async () => { const config = await Config.get() - const ruleset = PermissionNext.fromConfig(config.permission ?? {}) + const ruleset = Permission.fromConfig(config.permission ?? {}) // general and code-reviewer should be ask, orchestrator-* denied - expect(PermissionNext.evaluate("task", "general", ruleset).action).toBe("ask") - expect(PermissionNext.evaluate("task", "code-reviewer", ruleset).action).toBe("ask") - expect(PermissionNext.evaluate("task", "orchestrator-fast", ruleset).action).toBe("deny") + expect(Permission.evaluate("task", "general", ruleset).action).toBe("ask") + expect(Permission.evaluate("task", "code-reviewer", ruleset).action).toBe("ask") + expect(Permission.evaluate("task", "orchestrator-fast", ruleset).action).toBe("deny") }, }) }) @@ -205,11 +205,11 @@ describe("permission.task with real config files", () => { directory: tmp.path, fn: async () => { const config = await Config.get() - const ruleset = PermissionNext.fromConfig(config.permission ?? {}) - expect(PermissionNext.evaluate("task", "general", ruleset).action).toBe("allow") - expect(PermissionNext.evaluate("task", "code-reviewer", ruleset).action).toBe("deny") + const ruleset = Permission.fromConfig(config.permission ?? {}) + expect(Permission.evaluate("task", "general", ruleset).action).toBe("allow") + expect(Permission.evaluate("task", "code-reviewer", ruleset).action).toBe("deny") // Unspecified agents default to "ask" - expect(PermissionNext.evaluate("task", "unknown-agent", ruleset).action).toBe("ask") + expect(Permission.evaluate("task", "unknown-agent", ruleset).action).toBe("ask") }, }) }) @@ -232,18 +232,18 @@ describe("permission.task with real config files", () => { directory: tmp.path, fn: async () => { const config = await Config.get() - const ruleset = PermissionNext.fromConfig(config.permission ?? {}) + const ruleset = Permission.fromConfig(config.permission ?? {}) // Verify task permissions - expect(PermissionNext.evaluate("task", "general", ruleset).action).toBe("allow") - expect(PermissionNext.evaluate("task", "code-reviewer", ruleset).action).toBe("deny") + expect(Permission.evaluate("task", "general", ruleset).action).toBe("allow") + expect(Permission.evaluate("task", "code-reviewer", ruleset).action).toBe("deny") // Verify other tool permissions - expect(PermissionNext.evaluate("bash", "*", ruleset).action).toBe("allow") - expect(PermissionNext.evaluate("edit", "*", ruleset).action).toBe("ask") + expect(Permission.evaluate("bash", "*", ruleset).action).toBe("allow") + expect(Permission.evaluate("edit", "*", ruleset).action).toBe("ask") // Verify disabled tools - const disabled = PermissionNext.disabled(["bash", "edit", "task"], ruleset) + const disabled = Permission.disabled(["bash", "edit", "task"], ruleset) expect(disabled.has("bash")).toBe(false) expect(disabled.has("edit")).toBe(false) // task is NOT disabled because disabled() uses findLast, and the last rule @@ -270,16 +270,16 @@ describe("permission.task with real config files", () => { directory: tmp.path, fn: async () => { const config = await Config.get() - const ruleset = PermissionNext.fromConfig(config.permission ?? {}) + const ruleset = Permission.fromConfig(config.permission ?? {}) // Last matching rule wins - "*" deny is last, so all agents are denied - expect(PermissionNext.evaluate("task", "general", ruleset).action).toBe("deny") - expect(PermissionNext.evaluate("task", "code-reviewer", ruleset).action).toBe("deny") - expect(PermissionNext.evaluate("task", "unknown", ruleset).action).toBe("deny") + expect(Permission.evaluate("task", "general", ruleset).action).toBe("deny") + expect(Permission.evaluate("task", "code-reviewer", ruleset).action).toBe("deny") + expect(Permission.evaluate("task", "unknown", ruleset).action).toBe("deny") // Since "*": "deny" is the last rule, disabled() finds it with findLast // and sees pattern: "*" with action: "deny", so task is disabled - const disabled = PermissionNext.disabled(["task"], ruleset) + const disabled = Permission.disabled(["task"], ruleset) expect(disabled.has("task")).toBe(true) }, }) @@ -301,17 +301,17 @@ describe("permission.task with real config files", () => { directory: tmp.path, fn: async () => { const config = await Config.get() - const ruleset = PermissionNext.fromConfig(config.permission ?? {}) + const ruleset = Permission.fromConfig(config.permission ?? {}) // Evaluate uses findLast - "general" allow comes after "*" deny - expect(PermissionNext.evaluate("task", "general", ruleset).action).toBe("allow") + expect(Permission.evaluate("task", "general", ruleset).action).toBe("allow") // Other agents still denied by the earlier "*" deny - expect(PermissionNext.evaluate("task", "code-reviewer", ruleset).action).toBe("deny") + expect(Permission.evaluate("task", "code-reviewer", ruleset).action).toBe("deny") // disabled() uses findLast and checks if the last rule has pattern: "*" with action: "deny" // In this case, the last rule is {pattern: "general", action: "allow"}, not pattern: "*" // So the task tool is NOT disabled (even though most subagents are denied) - const disabled = PermissionNext.disabled(["task"], ruleset) + const disabled = Permission.disabled(["task"], ruleset) expect(disabled.has("task")).toBe(false) }, }) diff --git a/packages/opencode/test/permission/next.test.ts b/packages/opencode/test/permission/next.test.ts index 2a6b6e0baf..dfe30933fe 100644 --- a/packages/opencode/test/permission/next.test.ts +++ b/packages/opencode/test/permission/next.test.ts @@ -5,7 +5,7 @@ import { Bus } from "../../src/bus" import { runtime } from "../../src/effect/runtime" import { Instances } from "../../src/effect/instances" import { PermissionNext } from "../../src/permission" -import { PermissionNext as S } from "../../src/permission" +import { Permission as Svc } from "../../src/permission/service" import { PermissionID } from "../../src/permission/schema" import { Instance } from "../../src/project/instance" import { tmpdir } from "../fixture/fixture" @@ -37,12 +37,12 @@ async function waitForPending(count: number) { // fromConfig tests test("fromConfig - string value becomes wildcard rule", () => { - const result = PermissionNext.fromConfig({ bash: "allow" }) + const result = Svc.fromConfig({ bash: "allow" }) expect(result).toEqual([{ permission: "bash", pattern: "*", action: "allow" }]) }) test("fromConfig - object value converts to rules array", () => { - const result = PermissionNext.fromConfig({ bash: { "*": "allow", rm: "deny" } }) + const result = Svc.fromConfig({ bash: { "*": "allow", rm: "deny" } }) expect(result).toEqual([ { permission: "bash", pattern: "*", action: "allow" }, { permission: "bash", pattern: "rm", action: "deny" }, @@ -50,7 +50,7 @@ test("fromConfig - object value converts to rules array", () => { }) test("fromConfig - mixed string and object values", () => { - const result = PermissionNext.fromConfig({ + const result = Svc.fromConfig({ bash: { "*": "allow", rm: "deny" }, edit: "allow", webfetch: "ask", @@ -64,51 +64,51 @@ test("fromConfig - mixed string and object values", () => { }) test("fromConfig - empty object", () => { - const result = PermissionNext.fromConfig({}) + const result = Svc.fromConfig({}) expect(result).toEqual([]) }) test("fromConfig - expands tilde to home directory", () => { - const result = PermissionNext.fromConfig({ external_directory: { "~/projects/*": "allow" } }) + const result = Svc.fromConfig({ external_directory: { "~/projects/*": "allow" } }) expect(result).toEqual([{ permission: "external_directory", pattern: `${os.homedir()}/projects/*`, action: "allow" }]) }) test("fromConfig - expands $HOME to home directory", () => { - const result = PermissionNext.fromConfig({ external_directory: { "$HOME/projects/*": "allow" } }) + const result = Svc.fromConfig({ external_directory: { "$HOME/projects/*": "allow" } }) expect(result).toEqual([{ permission: "external_directory", pattern: `${os.homedir()}/projects/*`, action: "allow" }]) }) test("fromConfig - expands $HOME without trailing slash", () => { - const result = PermissionNext.fromConfig({ external_directory: { $HOME: "allow" } }) + const result = Svc.fromConfig({ external_directory: { $HOME: "allow" } }) expect(result).toEqual([{ permission: "external_directory", pattern: os.homedir(), action: "allow" }]) }) test("fromConfig - does not expand tilde in middle of path", () => { - const result = PermissionNext.fromConfig({ external_directory: { "/some/~/path": "allow" } }) + const result = Svc.fromConfig({ external_directory: { "/some/~/path": "allow" } }) expect(result).toEqual([{ permission: "external_directory", pattern: "/some/~/path", action: "allow" }]) }) test("fromConfig - expands exact tilde to home directory", () => { - const result = PermissionNext.fromConfig({ external_directory: { "~": "allow" } }) + const result = Svc.fromConfig({ external_directory: { "~": "allow" } }) expect(result).toEqual([{ permission: "external_directory", pattern: os.homedir(), action: "allow" }]) }) test("evaluate - matches expanded tilde pattern", () => { - const ruleset = PermissionNext.fromConfig({ external_directory: { "~/projects/*": "allow" } }) - const result = PermissionNext.evaluate("external_directory", `${os.homedir()}/projects/file.txt`, ruleset) + const ruleset = Svc.fromConfig({ external_directory: { "~/projects/*": "allow" } }) + const result = Svc.evaluate("external_directory", `${os.homedir()}/projects/file.txt`, ruleset) expect(result.action).toBe("allow") }) test("evaluate - matches expanded $HOME pattern", () => { - const ruleset = PermissionNext.fromConfig({ external_directory: { "$HOME/projects/*": "allow" } }) - const result = PermissionNext.evaluate("external_directory", `${os.homedir()}/projects/file.txt`, ruleset) + const ruleset = Svc.fromConfig({ external_directory: { "$HOME/projects/*": "allow" } }) + const result = Svc.evaluate("external_directory", `${os.homedir()}/projects/file.txt`, ruleset) expect(result.action).toBe("allow") }) // merge tests test("merge - simple concatenation", () => { - const result = PermissionNext.merge( + const result = Svc.merge( [{ permission: "bash", pattern: "*", action: "allow" }], [{ permission: "bash", pattern: "*", action: "deny" }], ) @@ -119,7 +119,7 @@ test("merge - simple concatenation", () => { }) test("merge - adds new permission", () => { - const result = PermissionNext.merge( + const result = Svc.merge( [{ permission: "bash", pattern: "*", action: "allow" }], [{ permission: "edit", pattern: "*", action: "deny" }], ) @@ -130,7 +130,7 @@ test("merge - adds new permission", () => { }) test("merge - concatenates rules for same permission", () => { - const result = PermissionNext.merge( + const result = Svc.merge( [{ permission: "bash", pattern: "foo", action: "ask" }], [{ permission: "bash", pattern: "*", action: "deny" }], ) @@ -141,7 +141,7 @@ test("merge - concatenates rules for same permission", () => { }) test("merge - multiple rulesets", () => { - const result = PermissionNext.merge( + const result = Svc.merge( [{ permission: "bash", pattern: "*", action: "allow" }], [{ permission: "bash", pattern: "rm", action: "ask" }], [{ permission: "edit", pattern: "*", action: "allow" }], @@ -154,12 +154,12 @@ test("merge - multiple rulesets", () => { }) test("merge - empty ruleset does nothing", () => { - const result = PermissionNext.merge([{ permission: "bash", pattern: "*", action: "allow" }], []) + const result = Svc.merge([{ permission: "bash", pattern: "*", action: "allow" }], []) expect(result).toEqual([{ permission: "bash", pattern: "*", action: "allow" }]) }) test("merge - preserves rule order", () => { - const result = PermissionNext.merge( + const result = Svc.merge( [ { permission: "edit", pattern: "src/*", action: "allow" }, { permission: "edit", pattern: "src/secret/*", action: "deny" }, @@ -175,40 +175,40 @@ test("merge - preserves rule order", () => { test("merge - config permission overrides default ask", () => { // Simulates: defaults have "*": "ask", config sets bash: "allow" - const defaults: PermissionNext.Ruleset = [{ permission: "*", pattern: "*", action: "ask" }] - const config: PermissionNext.Ruleset = [{ permission: "bash", pattern: "*", action: "allow" }] - const merged = PermissionNext.merge(defaults, config) + const defaults: Svc.Ruleset = [{ permission: "*", pattern: "*", action: "ask" }] + const config: Svc.Ruleset = [{ permission: "bash", pattern: "*", action: "allow" }] + const merged = Svc.merge(defaults, config) // Config's bash allow should override default ask - expect(PermissionNext.evaluate("bash", "ls", merged).action).toBe("allow") + expect(Svc.evaluate("bash", "ls", merged).action).toBe("allow") // Other permissions should still be ask (from defaults) - expect(PermissionNext.evaluate("edit", "foo.ts", merged).action).toBe("ask") + expect(Svc.evaluate("edit", "foo.ts", merged).action).toBe("ask") }) test("merge - config ask overrides default allow", () => { // Simulates: defaults have bash: "allow", config sets bash: "ask" - const defaults: PermissionNext.Ruleset = [{ permission: "bash", pattern: "*", action: "allow" }] - const config: PermissionNext.Ruleset = [{ permission: "bash", pattern: "*", action: "ask" }] - const merged = PermissionNext.merge(defaults, config) + const defaults: Svc.Ruleset = [{ permission: "bash", pattern: "*", action: "allow" }] + const config: Svc.Ruleset = [{ permission: "bash", pattern: "*", action: "ask" }] + const merged = Svc.merge(defaults, config) // Config's ask should override default allow - expect(PermissionNext.evaluate("bash", "ls", merged).action).toBe("ask") + expect(Svc.evaluate("bash", "ls", merged).action).toBe("ask") }) // evaluate tests test("evaluate - exact pattern match", () => { - const result = PermissionNext.evaluate("bash", "rm", [{ permission: "bash", pattern: "rm", action: "deny" }]) + const result = Svc.evaluate("bash", "rm", [{ permission: "bash", pattern: "rm", action: "deny" }]) expect(result.action).toBe("deny") }) test("evaluate - wildcard pattern match", () => { - const result = PermissionNext.evaluate("bash", "rm", [{ permission: "bash", pattern: "*", action: "allow" }]) + const result = Svc.evaluate("bash", "rm", [{ permission: "bash", pattern: "*", action: "allow" }]) expect(result.action).toBe("allow") }) test("evaluate - last matching rule wins", () => { - const result = PermissionNext.evaluate("bash", "rm", [ + const result = Svc.evaluate("bash", "rm", [ { permission: "bash", pattern: "*", action: "allow" }, { permission: "bash", pattern: "rm", action: "deny" }, ]) @@ -216,7 +216,7 @@ test("evaluate - last matching rule wins", () => { }) test("evaluate - last matching rule wins (wildcard after specific)", () => { - const result = PermissionNext.evaluate("bash", "rm", [ + const result = Svc.evaluate("bash", "rm", [ { permission: "bash", pattern: "rm", action: "deny" }, { permission: "bash", pattern: "*", action: "allow" }, ]) @@ -224,14 +224,12 @@ test("evaluate - last matching rule wins (wildcard after specific)", () => { }) test("evaluate - glob pattern match", () => { - const result = PermissionNext.evaluate("edit", "src/foo.ts", [ - { permission: "edit", pattern: "src/*", action: "allow" }, - ]) + const result = Svc.evaluate("edit", "src/foo.ts", [{ permission: "edit", pattern: "src/*", action: "allow" }]) expect(result.action).toBe("allow") }) test("evaluate - last matching glob wins", () => { - const result = PermissionNext.evaluate("edit", "src/components/Button.tsx", [ + const result = Svc.evaluate("edit", "src/components/Button.tsx", [ { permission: "edit", pattern: "src/*", action: "deny" }, { permission: "edit", pattern: "src/components/*", action: "allow" }, ]) @@ -240,7 +238,7 @@ test("evaluate - last matching glob wins", () => { test("evaluate - order matters for specificity", () => { // If more specific rule comes first, later wildcard overrides it - const result = PermissionNext.evaluate("edit", "src/components/Button.tsx", [ + const result = Svc.evaluate("edit", "src/components/Button.tsx", [ { permission: "edit", pattern: "src/components/*", action: "allow" }, { permission: "edit", pattern: "src/*", action: "deny" }, ]) @@ -248,31 +246,27 @@ test("evaluate - order matters for specificity", () => { }) test("evaluate - unknown permission returns ask", () => { - const result = PermissionNext.evaluate("unknown_tool", "anything", [ - { permission: "bash", pattern: "*", action: "allow" }, - ]) + const result = Svc.evaluate("unknown_tool", "anything", [{ permission: "bash", pattern: "*", action: "allow" }]) expect(result.action).toBe("ask") }) test("evaluate - empty ruleset returns ask", () => { - const result = PermissionNext.evaluate("bash", "rm", []) + const result = Svc.evaluate("bash", "rm", []) expect(result.action).toBe("ask") }) test("evaluate - no matching pattern returns ask", () => { - const result = PermissionNext.evaluate("edit", "etc/passwd", [ - { permission: "edit", pattern: "src/*", action: "allow" }, - ]) + const result = Svc.evaluate("edit", "etc/passwd", [{ permission: "edit", pattern: "src/*", action: "allow" }]) expect(result.action).toBe("ask") }) test("evaluate - empty rules array returns ask", () => { - const result = PermissionNext.evaluate("bash", "rm", []) + const result = Svc.evaluate("bash", "rm", []) expect(result.action).toBe("ask") }) test("evaluate - multiple matching patterns, last wins", () => { - const result = PermissionNext.evaluate("edit", "src/secret.ts", [ + const result = Svc.evaluate("edit", "src/secret.ts", [ { permission: "edit", pattern: "*", action: "ask" }, { permission: "edit", pattern: "src/*", action: "allow" }, { permission: "edit", pattern: "src/secret.ts", action: "deny" }, @@ -281,7 +275,7 @@ test("evaluate - multiple matching patterns, last wins", () => { }) test("evaluate - non-matching patterns are skipped", () => { - const result = PermissionNext.evaluate("edit", "src/foo.ts", [ + const result = Svc.evaluate("edit", "src/foo.ts", [ { permission: "edit", pattern: "*", action: "ask" }, { permission: "edit", pattern: "test/*", action: "deny" }, { permission: "edit", pattern: "src/*", action: "allow" }, @@ -290,7 +284,7 @@ test("evaluate - non-matching patterns are skipped", () => { }) test("evaluate - exact match at end wins over earlier wildcard", () => { - const result = PermissionNext.evaluate("bash", "/bin/rm", [ + const result = Svc.evaluate("bash", "/bin/rm", [ { permission: "bash", pattern: "*", action: "allow" }, { permission: "bash", pattern: "/bin/rm", action: "deny" }, ]) @@ -298,7 +292,7 @@ test("evaluate - exact match at end wins over earlier wildcard", () => { }) test("evaluate - wildcard at end overrides earlier exact match", () => { - const result = PermissionNext.evaluate("bash", "/bin/rm", [ + const result = Svc.evaluate("bash", "/bin/rm", [ { permission: "bash", pattern: "/bin/rm", action: "deny" }, { permission: "bash", pattern: "*", action: "allow" }, ]) @@ -308,24 +302,22 @@ test("evaluate - wildcard at end overrides earlier exact match", () => { // wildcard permission tests test("evaluate - wildcard permission matches any permission", () => { - const result = PermissionNext.evaluate("bash", "rm", [{ permission: "*", pattern: "*", action: "deny" }]) + const result = Svc.evaluate("bash", "rm", [{ permission: "*", pattern: "*", action: "deny" }]) expect(result.action).toBe("deny") }) test("evaluate - wildcard permission with specific pattern", () => { - const result = PermissionNext.evaluate("bash", "rm", [{ permission: "*", pattern: "rm", action: "deny" }]) + const result = Svc.evaluate("bash", "rm", [{ permission: "*", pattern: "rm", action: "deny" }]) expect(result.action).toBe("deny") }) test("evaluate - glob permission pattern", () => { - const result = PermissionNext.evaluate("mcp_server_tool", "anything", [ - { permission: "mcp_*", pattern: "*", action: "allow" }, - ]) + const result = Svc.evaluate("mcp_server_tool", "anything", [{ permission: "mcp_*", pattern: "*", action: "allow" }]) expect(result.action).toBe("allow") }) test("evaluate - specific permission and wildcard permission combined", () => { - const result = PermissionNext.evaluate("bash", "rm", [ + const result = Svc.evaluate("bash", "rm", [ { permission: "*", pattern: "*", action: "deny" }, { permission: "bash", pattern: "*", action: "allow" }, ]) @@ -333,7 +325,7 @@ test("evaluate - specific permission and wildcard permission combined", () => { }) test("evaluate - wildcard permission does not match when specific exists", () => { - const result = PermissionNext.evaluate("edit", "src/foo.ts", [ + const result = Svc.evaluate("edit", "src/foo.ts", [ { permission: "*", pattern: "*", action: "deny" }, { permission: "edit", pattern: "src/*", action: "allow" }, ]) @@ -341,7 +333,7 @@ test("evaluate - wildcard permission does not match when specific exists", () => }) test("evaluate - multiple matching permission patterns combine rules", () => { - const result = PermissionNext.evaluate("mcp_dangerous", "anything", [ + const result = Svc.evaluate("mcp_dangerous", "anything", [ { permission: "*", pattern: "*", action: "ask" }, { permission: "mcp_*", pattern: "*", action: "allow" }, { permission: "mcp_dangerous", pattern: "*", action: "deny" }, @@ -350,7 +342,7 @@ test("evaluate - multiple matching permission patterns combine rules", () => { }) test("evaluate - wildcard permission fallback for unknown tool", () => { - const result = PermissionNext.evaluate("unknown_tool", "anything", [ + const result = Svc.evaluate("unknown_tool", "anything", [ { permission: "*", pattern: "*", action: "ask" }, { permission: "bash", pattern: "*", action: "allow" }, ]) @@ -359,7 +351,7 @@ test("evaluate - wildcard permission fallback for unknown tool", () => { test("evaluate - permission patterns sorted by length regardless of object order", () => { // specific permission listed before wildcard, but specific should still win - const result = PermissionNext.evaluate("bash", "rm", [ + const result = Svc.evaluate("bash", "rm", [ { permission: "bash", pattern: "*", action: "allow" }, { permission: "*", pattern: "*", action: "deny" }, ]) @@ -368,22 +360,22 @@ test("evaluate - permission patterns sorted by length regardless of object order }) test("evaluate - merges multiple rulesets", () => { - const config: PermissionNext.Ruleset = [{ permission: "bash", pattern: "*", action: "allow" }] - const approved: PermissionNext.Ruleset = [{ permission: "bash", pattern: "rm", action: "deny" }] + const config: Svc.Ruleset = [{ permission: "bash", pattern: "*", action: "allow" }] + const approved: Svc.Ruleset = [{ permission: "bash", pattern: "rm", action: "deny" }] // approved comes after config, so rm should be denied - const result = PermissionNext.evaluate("bash", "rm", config, approved) + const result = Svc.evaluate("bash", "rm", config, approved) expect(result.action).toBe("deny") }) // disabled tests test("disabled - returns empty set when all tools allowed", () => { - const result = PermissionNext.disabled(["bash", "edit", "read"], [{ permission: "*", pattern: "*", action: "allow" }]) + const result = Svc.disabled(["bash", "edit", "read"], [{ permission: "*", pattern: "*", action: "allow" }]) expect(result.size).toBe(0) }) test("disabled - disables tool when denied", () => { - const result = PermissionNext.disabled( + const result = Svc.disabled( ["bash", "edit", "read"], [ { permission: "*", pattern: "*", action: "allow" }, @@ -396,7 +388,7 @@ test("disabled - disables tool when denied", () => { }) test("disabled - disables edit/write/apply_patch/multiedit when edit denied", () => { - const result = PermissionNext.disabled( + const result = Svc.disabled( ["edit", "write", "apply_patch", "multiedit", "bash"], [ { permission: "*", pattern: "*", action: "allow" }, @@ -411,7 +403,7 @@ test("disabled - disables edit/write/apply_patch/multiedit when edit denied", () }) test("disabled - does not disable when partially denied", () => { - const result = PermissionNext.disabled( + const result = Svc.disabled( ["bash"], [ { permission: "bash", pattern: "*", action: "allow" }, @@ -422,14 +414,14 @@ test("disabled - does not disable when partially denied", () => { }) test("disabled - does not disable when action is ask", () => { - const result = PermissionNext.disabled(["bash", "edit"], [{ permission: "*", pattern: "*", action: "ask" }]) + const result = Svc.disabled(["bash", "edit"], [{ permission: "*", pattern: "*", action: "ask" }]) expect(result.size).toBe(0) }) test("disabled - does not disable when specific allow after wildcard deny", () => { // Tool is NOT disabled because a specific allow after wildcard deny means // there's at least some usage allowed - const result = PermissionNext.disabled( + const result = Svc.disabled( ["bash"], [ { permission: "bash", pattern: "*", action: "deny" }, @@ -440,7 +432,7 @@ test("disabled - does not disable when specific allow after wildcard deny", () = }) test("disabled - does not disable when wildcard allow after deny", () => { - const result = PermissionNext.disabled( + const result = Svc.disabled( ["bash"], [ { permission: "bash", pattern: "rm *", action: "deny" }, @@ -451,7 +443,7 @@ test("disabled - does not disable when wildcard allow after deny", () => { }) test("disabled - disables multiple tools", () => { - const result = PermissionNext.disabled( + const result = Svc.disabled( ["bash", "edit", "webfetch"], [ { permission: "bash", pattern: "*", action: "deny" }, @@ -465,14 +457,14 @@ test("disabled - disables multiple tools", () => { }) test("disabled - wildcard permission denies all tools", () => { - const result = PermissionNext.disabled(["bash", "edit", "read"], [{ permission: "*", pattern: "*", action: "deny" }]) + const result = Svc.disabled(["bash", "edit", "read"], [{ permission: "*", pattern: "*", action: "deny" }]) expect(result.has("bash")).toBe(true) expect(result.has("edit")).toBe(true) expect(result.has("read")).toBe(true) }) test("disabled - specific allow overrides wildcard deny", () => { - const result = PermissionNext.disabled( + const result = Svc.disabled( ["bash", "edit", "read"], [ { permission: "*", pattern: "*", action: "deny" }, @@ -518,7 +510,7 @@ test("ask - throws RejectedError when action is deny", async () => { always: [], ruleset: [{ permission: "bash", pattern: "*", action: "deny" }], }), - ).rejects.toBeInstanceOf(PermissionNext.DeniedError) + ).rejects.toBeInstanceOf(Svc.DeniedError) }, }) }) @@ -588,8 +580,8 @@ test("ask - publishes asked event", async () => { await Instance.provide({ directory: tmp.path, fn: async () => { - let seen: PermissionNext.Request | undefined - const unsub = Bus.subscribe(PermissionNext.Event.Asked, (event) => { + let seen: Svc.Request | undefined + const unsub = Bus.subscribe(Svc.Event.Asked, (event) => { seen = event.properties }) @@ -672,7 +664,7 @@ test("reply - reject throws RejectedError", async () => { reply: "reject", }) - await expect(askPromise).rejects.toBeInstanceOf(PermissionNext.RejectedError) + await expect(askPromise).rejects.toBeInstanceOf(Svc.RejectedError) }, }) }) @@ -701,7 +693,7 @@ test("reply - reject with message throws CorrectedError", async () => { }) const err = await ask.catch((err) => err) - expect(err).toBeInstanceOf(PermissionNext.CorrectedError) + expect(err).toBeInstanceOf(Svc.CorrectedError) expect(err.message).toContain("Use a safer command") }, }) @@ -788,8 +780,8 @@ test("reply - reject cancels all pending for same session", async () => { }) // Both should be rejected - expect(await result1).toBeInstanceOf(PermissionNext.RejectedError) - expect(await result2).toBeInstanceOf(PermissionNext.RejectedError) + expect(await result1).toBeInstanceOf(Svc.RejectedError) + expect(await result2).toBeInstanceOf(Svc.RejectedError) }, }) }) @@ -895,10 +887,10 @@ test("reply - publishes replied event", async () => { | { sessionID: SessionID requestID: PermissionID - reply: PermissionNext.Reply + reply: Svc.Reply } | undefined - const unsub = Bus.subscribe(PermissionNext.Event.Replied, (event) => { + const unsub = Bus.subscribe(Svc.Event.Replied, (event) => { seen = event.properties }) @@ -949,7 +941,7 @@ test("ask - checks all patterns and stops on first deny", async () => { { permission: "bash", pattern: "rm *", action: "deny" }, ], }), - ).rejects.toBeInstanceOf(PermissionNext.DeniedError) + ).rejects.toBeInstanceOf(Svc.DeniedError) }, }) }) @@ -992,7 +984,7 @@ test("ask - should deny even when an earlier pattern is ask", async () => { (err) => err, ) - expect(err).toBeInstanceOf(PermissionNext.DeniedError) + expect(err).toBeInstanceOf(Svc.DeniedError) expect(await PermissionNext.list()).toHaveLength(0) }, }) @@ -1005,7 +997,7 @@ test("ask - abort should clear pending request", async () => { fn: async () => { const ctl = new AbortController() const ask = runtime.runPromise( - S.Service.use((svc) => + Svc.Service.use((svc) => svc.ask({ sessionID: SessionID.make("session_test"), permission: "bash", diff --git a/packages/opencode/test/question/question.test.ts b/packages/opencode/test/question/question.test.ts index 45e0d3c318..e485ea058c 100644 --- a/packages/opencode/test/question/question.test.ts +++ b/packages/opencode/test/question/question.test.ts @@ -1,5 +1,6 @@ import { afterEach, test, expect } from "bun:test" import { Question } from "../../src/question" +import { Question as QuestionService } from "../../src/question/service" import { Instance } from "../../src/project/instance" import { QuestionID } from "../../src/question/schema" import { tmpdir } from "../fixture/fixture" @@ -181,7 +182,7 @@ test("reject - throws RejectedError", async () => { const pending = await Question.list() await Question.reject(pending[0].id) - await expect(askPromise).rejects.toBeInstanceOf(Question.RejectedError) + await expect(askPromise).rejects.toBeInstanceOf(QuestionService.RejectedError) }, }) }) diff --git a/packages/opencode/test/session/message-v2.test.ts b/packages/opencode/test/session/message-v2.test.ts index 0d5b89730a..b5d81aab41 100644 --- a/packages/opencode/test/session/message-v2.test.ts +++ b/packages/opencode/test/session/message-v2.test.ts @@ -4,7 +4,7 @@ import { MessageV2 } from "../../src/session/message-v2" import type { Provider } from "../../src/provider/provider" import { ModelID, ProviderID } from "../../src/provider/schema" import { SessionID, MessageID, PartID } from "../../src/session/schema" -import { Question } from "../../src/question" +import { Question } from "../../src/question/service" const sessionID = SessionID.make("session") const providerID = ProviderID.make("test") diff --git a/packages/opencode/test/tool/bash.test.ts b/packages/opencode/test/tool/bash.test.ts index a5c7cec917..1fa4ae8ff4 100644 --- a/packages/opencode/test/tool/bash.test.ts +++ b/packages/opencode/test/tool/bash.test.ts @@ -5,8 +5,8 @@ import { BashTool } from "../../src/tool/bash" import { Instance } from "../../src/project/instance" import { Filesystem } from "../../src/util/filesystem" import { tmpdir } from "../fixture/fixture" -import type { PermissionNext } from "../../src/permission" -import { Truncate } from "../../src/tool/truncate" +import type { Permission } from "../../src/permission/service" +import { Truncate } from "../../src/tool/truncate-effect" import { SessionID, MessageID } from "../../src/session/schema" const ctx = { @@ -49,10 +49,10 @@ describe("tool.bash permissions", () => { directory: tmp.path, fn: async () => { const bash = await BashTool.init() - const requests: Array> = [] + const requests: Array> = [] const testCtx = { ...ctx, - ask: async (req: Omit) => { + ask: async (req: Omit) => { requests.push(req) }, } @@ -76,10 +76,10 @@ describe("tool.bash permissions", () => { directory: tmp.path, fn: async () => { const bash = await BashTool.init() - const requests: Array> = [] + const requests: Array> = [] const testCtx = { ...ctx, - ask: async (req: Omit) => { + ask: async (req: Omit) => { requests.push(req) }, } @@ -104,10 +104,10 @@ describe("tool.bash permissions", () => { directory: tmp.path, fn: async () => { const bash = await BashTool.init() - const requests: Array> = [] + const requests: Array> = [] const testCtx = { ...ctx, - ask: async (req: Omit) => { + ask: async (req: Omit) => { requests.push(req) }, } @@ -130,10 +130,10 @@ describe("tool.bash permissions", () => { directory: tmp.path, fn: async () => { const bash = await BashTool.init() - const requests: Array> = [] + const requests: Array> = [] const testCtx = { ...ctx, - ask: async (req: Omit) => { + ask: async (req: Omit) => { requests.push(req) }, } @@ -163,10 +163,10 @@ describe("tool.bash permissions", () => { directory: tmp.path, fn: async () => { const bash = await BashTool.init() - const requests: Array> = [] + const requests: Array> = [] const testCtx = { ...ctx, - ask: async (req: Omit) => { + ask: async (req: Omit) => { requests.push(req) }, } @@ -193,10 +193,10 @@ describe("tool.bash permissions", () => { directory: tmp.path, fn: async () => { const bash = await BashTool.init() - const requests: Array> = [] + const requests: Array> = [] const testCtx = { ...ctx, - ask: async (req: Omit) => { + ask: async (req: Omit) => { requests.push(req) }, } @@ -223,10 +223,10 @@ describe("tool.bash permissions", () => { directory: tmp.path, fn: async () => { const bash = await BashTool.init() - const requests: Array> = [] + const requests: Array> = [] const testCtx = { ...ctx, - ask: async (req: Omit) => { + ask: async (req: Omit) => { requests.push(req) }, } @@ -250,10 +250,10 @@ describe("tool.bash permissions", () => { directory: tmp.path, fn: async () => { const bash = await BashTool.init() - const requests: Array> = [] + const requests: Array> = [] const testCtx = { ...ctx, - ask: async (req: Omit) => { + ask: async (req: Omit) => { requests.push(req) }, } @@ -276,10 +276,10 @@ describe("tool.bash permissions", () => { directory: tmp.path, fn: async () => { const bash = await BashTool.init() - const requests: Array> = [] + const requests: Array> = [] const testCtx = { ...ctx, - ask: async (req: Omit) => { + ask: async (req: Omit) => { requests.push(req) }, } @@ -297,10 +297,10 @@ describe("tool.bash permissions", () => { directory: tmp.path, fn: async () => { const bash = await BashTool.init() - const requests: Array> = [] + const requests: Array> = [] const testCtx = { ...ctx, - ask: async (req: Omit) => { + ask: async (req: Omit) => { requests.push(req) }, } diff --git a/packages/opencode/test/tool/edit.test.ts b/packages/opencode/test/tool/edit.test.ts index 7b6784cf49..a5c6694288 100644 --- a/packages/opencode/test/tool/edit.test.ts +++ b/packages/opencode/test/tool/edit.test.ts @@ -81,7 +81,7 @@ describe("tool.edit", () => { directory: tmp.path, fn: async () => { const { Bus } = await import("../../src/bus") - const { File } = await import("../../src/file") + const { File } = await import("../../src/file/service") const { FileWatcher } = await import("../../src/file/watcher") const events: string[] = [] @@ -301,7 +301,7 @@ describe("tool.edit", () => { await FileTime.read(ctx.sessionID, filepath) const { Bus } = await import("../../src/bus") - const { File } = await import("../../src/file") + const { File } = await import("../../src/file/service") const { FileWatcher } = await import("../../src/file/watcher") const events: string[] = [] diff --git a/packages/opencode/test/tool/external-directory.test.ts b/packages/opencode/test/tool/external-directory.test.ts index 229901a722..4042853f53 100644 --- a/packages/opencode/test/tool/external-directory.test.ts +++ b/packages/opencode/test/tool/external-directory.test.ts @@ -3,7 +3,7 @@ import path from "path" import type { Tool } from "../../src/tool/tool" import { Instance } from "../../src/project/instance" import { assertExternalDirectory } from "../../src/tool/external-directory" -import type { PermissionNext } from "../../src/permission" +import type { Permission } from "../../src/permission/service" import { SessionID, MessageID } from "../../src/session/schema" const baseCtx: Omit = { @@ -18,7 +18,7 @@ const baseCtx: Omit = { describe("tool.assertExternalDirectory", () => { test("no-ops for empty target", async () => { - const requests: Array> = [] + const requests: Array> = [] const ctx: Tool.Context = { ...baseCtx, ask: async (req) => { @@ -37,7 +37,7 @@ describe("tool.assertExternalDirectory", () => { }) test("no-ops for paths inside Instance.directory", async () => { - const requests: Array> = [] + const requests: Array> = [] const ctx: Tool.Context = { ...baseCtx, ask: async (req) => { @@ -56,7 +56,7 @@ describe("tool.assertExternalDirectory", () => { }) test("asks with a single canonical glob", async () => { - const requests: Array> = [] + const requests: Array> = [] const ctx: Tool.Context = { ...baseCtx, ask: async (req) => { @@ -82,7 +82,7 @@ describe("tool.assertExternalDirectory", () => { }) test("uses target directory when kind=directory", async () => { - const requests: Array> = [] + const requests: Array> = [] const ctx: Tool.Context = { ...baseCtx, ask: async (req) => { @@ -108,7 +108,7 @@ describe("tool.assertExternalDirectory", () => { }) test("skips prompting when bypass=true", async () => { - const requests: Array> = [] + const requests: Array> = [] const ctx: Tool.Context = { ...baseCtx, ask: async (req) => { diff --git a/packages/opencode/test/tool/read.test.ts b/packages/opencode/test/tool/read.test.ts index cfeb597fce..536c4ada12 100644 --- a/packages/opencode/test/tool/read.test.ts +++ b/packages/opencode/test/tool/read.test.ts @@ -4,7 +4,7 @@ import { ReadTool } from "../../src/tool/read" import { Instance } from "../../src/project/instance" import { Filesystem } from "../../src/util/filesystem" import { tmpdir } from "../fixture/fixture" -import { PermissionNext } from "../../src/permission" +import { Permission } from "../../src/permission/service" import { Agent } from "../../src/agent/agent" import { SessionID, MessageID } from "../../src/session/schema" @@ -65,10 +65,10 @@ describe("tool.read external_directory permission", () => { directory: tmp.path, fn: async () => { const read = await ReadTool.init() - const requests: Array> = [] + const requests: Array> = [] const testCtx = { ...ctx, - ask: async (req: Omit) => { + ask: async (req: Omit) => { requests.push(req) }, } @@ -91,10 +91,10 @@ describe("tool.read external_directory permission", () => { directory: tmp.path, fn: async () => { const read = await ReadTool.init() - const requests: Array> = [] + const requests: Array> = [] const testCtx = { ...ctx, - ask: async (req: Omit) => { + ask: async (req: Omit) => { requests.push(req) }, } @@ -112,10 +112,10 @@ describe("tool.read external_directory permission", () => { directory: tmp.path, fn: async () => { const read = await ReadTool.init() - const requests: Array> = [] + const requests: Array> = [] const testCtx = { ...ctx, - ask: async (req: Omit) => { + ask: async (req: Omit) => { requests.push(req) }, } @@ -138,10 +138,10 @@ describe("tool.read external_directory permission", () => { directory: tmp.path, fn: async () => { const read = await ReadTool.init() - const requests: Array> = [] + const requests: Array> = [] const testCtx = { ...ctx, - ask: async (req: Omit) => { + ask: async (req: Omit) => { requests.push(req) }, } @@ -176,14 +176,14 @@ describe("tool.read env file permissions", () => { let askedForEnv = false const ctxWithPermissions = { ...ctx, - ask: async (req: Omit) => { + ask: async (req: Omit) => { for (const pattern of req.patterns) { - const rule = PermissionNext.evaluate(req.permission, pattern, agent.permission) + const rule = Permission.evaluate(req.permission, pattern, agent.permission) if (rule.action === "ask" && req.permission === "read") { askedForEnv = true } if (rule.action === "deny") { - throw new PermissionNext.DeniedError({ ruleset: agent.permission }) + throw new Permission.DeniedError({ ruleset: agent.permission }) } } }, diff --git a/packages/opencode/test/tool/skill.test.ts b/packages/opencode/test/tool/skill.test.ts index f622341d33..e71169faea 100644 --- a/packages/opencode/test/tool/skill.test.ts +++ b/packages/opencode/test/tool/skill.test.ts @@ -1,7 +1,7 @@ import { describe, expect, test } from "bun:test" import path from "path" import { pathToFileURL } from "url" -import type { PermissionNext } from "../../src/permission" +import type { Permission } from "../../src/permission/service" import type { Tool } from "../../src/tool/tool" import { Instance } from "../../src/project/instance" import { SkillTool } from "../../src/tool/skill" @@ -133,7 +133,7 @@ Use this skill. directory: tmp.path, fn: async () => { const tool = await SkillTool.init() - const requests: Array> = [] + const requests: Array> = [] const ctx: Tool.Context = { ...baseCtx, ask: async (req) => { diff --git a/packages/opencode/test/tool/truncation.test.ts b/packages/opencode/test/tool/truncation.test.ts index 032f0bfee2..5c49d368fd 100644 --- a/packages/opencode/test/tool/truncation.test.ts +++ b/packages/opencode/test/tool/truncation.test.ts @@ -71,8 +71,8 @@ describe("Truncate", () => { }) test("uses default MAX_LINES and MAX_BYTES", () => { - expect(Truncate.MAX_LINES).toBe(2000) - expect(Truncate.MAX_BYTES).toBe(50 * 1024) + expect(TruncateSvc.MAX_LINES).toBe(2000) + expect(TruncateSvc.MAX_BYTES).toBe(50 * 1024) }) test("large single-line file truncates with byte message", async () => { @@ -81,7 +81,7 @@ describe("Truncate", () => { expect(result.truncated).toBe(true) expect(result.content).toContain("bytes truncated...") - expect(Buffer.byteLength(content, "utf-8")).toBeGreaterThan(Truncate.MAX_BYTES) + expect(Buffer.byteLength(content, "utf-8")).toBeGreaterThan(TruncateSvc.MAX_BYTES) }) test("writes full output to file when truncated", async () => { @@ -145,10 +145,10 @@ describe("Truncate", () => { Effect.gen(function* () { const fs = yield* FileSystem.FileSystem - yield* fs.makeDirectory(Truncate.DIR, { recursive: true }) + yield* fs.makeDirectory(TruncateSvc.DIR, { recursive: true }) - const old = path.join(Truncate.DIR, Identifier.create("tool", false, Date.now() - 10 * DAY_MS)) - const recent = path.join(Truncate.DIR, Identifier.create("tool", false, Date.now() - 3 * DAY_MS)) + const old = path.join(TruncateSvc.DIR, Identifier.create("tool", false, Date.now() - 10 * DAY_MS)) + const recent = path.join(TruncateSvc.DIR, Identifier.create("tool", false, Date.now() - 3 * DAY_MS)) yield* writeFileStringScoped(old, "old content") yield* writeFileStringScoped(recent, "recent content")