Преглед изворни кода

refactor(effect): align permission and truncate entrypoints

Kit Langton пре 1 месец
родитељ
комит
ba26993361
28 измењених фајлова са 86 додато и 83 уклоњено
  1. 2 2
      packages/opencode/src/agent/agent.ts
  2. 1 1
      packages/opencode/src/cli/cmd/debug/agent.ts
  3. 1 1
      packages/opencode/src/cli/cmd/run.ts
  4. 6 4
      packages/opencode/src/effect/runtime.ts
  5. 0 0
      packages/opencode/src/permission/index.ts
  6. 1 1
      packages/opencode/src/server/routes/permission.ts
  7. 1 1
      packages/opencode/src/server/routes/session.ts
  8. 1 1
      packages/opencode/src/session/index.ts
  9. 1 1
      packages/opencode/src/session/llm.ts
  10. 1 1
      packages/opencode/src/session/processor.ts
  11. 2 2
      packages/opencode/src/session/prompt.ts
  12. 1 1
      packages/opencode/src/session/session.sql.ts
  13. 1 1
      packages/opencode/src/session/system.ts
  14. 1 1
      packages/opencode/src/skill/skill.ts
  15. 1 1
      packages/opencode/src/tool/bash.ts
  16. 1 1
      packages/opencode/src/tool/registry.ts
  17. 1 1
      packages/opencode/src/tool/task.ts
  18. 2 2
      packages/opencode/src/tool/tool.ts
  19. 41 38
      packages/opencode/src/tool/truncate-effect.ts
  20. 4 6
      packages/opencode/src/tool/truncate.ts
  21. 5 5
      packages/opencode/test/agent/agent.test.ts
  22. 1 1
      packages/opencode/test/permission-task.test.ts
  23. 1 1
      packages/opencode/test/permission/next.test.ts
  24. 2 2
      packages/opencode/test/tool/bash.test.ts
  25. 1 1
      packages/opencode/test/tool/external-directory.test.ts
  26. 1 1
      packages/opencode/test/tool/read.test.ts
  27. 1 1
      packages/opencode/test/tool/skill.test.ts
  28. 4 4
      packages/opencode/test/tool/truncation.test.ts

+ 2 - 2
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/truncation"
+import { Truncate } from "../tool/truncate"
 import { Auth } from "../auth"
 import { ProviderTransform } from "../provider/transform"
 
@@ -14,7 +14,7 @@ import PROMPT_COMPACTION from "./prompt/compaction.txt"
 import PROMPT_EXPLORE from "./prompt/explore.txt"
 import PROMPT_SUMMARY from "./prompt/summary.txt"
 import PROMPT_TITLE from "./prompt/title.txt"
-import { PermissionNext } from "@/permission/next"
+import { PermissionNext } from "@/permission"
 import { mergeDeep, pipe, sortBy, values } from "remeda"
 import { Global } from "@/global"
 import path from "path"

+ 1 - 1
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/next"
+import { PermissionNext } from "../../../permission"
 import { iife } from "../../../util/iife"
 import { bootstrap } from "../../bootstrap"
 import { cmd } from "../cmd"

+ 1 - 1
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/next"
+import { PermissionNext } from "../../permission"
 import { Tool } from "../../tool/tool"
 import { GlobTool } from "../../tool/glob"
 import { GrepTool } from "../../tool/grep"

+ 6 - 4
packages/opencode/src/effect/runtime.ts

@@ -3,13 +3,15 @@ import { AccountService } from "@/account/service"
 import { AuthService } from "@/auth/service"
 import { Instances } from "@/effect/instances"
 import type { InstanceServices } from "@/effect/instances"
-import { TruncateService } from "@/tool/truncate-service"
+import { TruncateEffect } from "@/tool/truncate-effect"
 import { Instance } from "@/project/instance"
 
 export const runtime = ManagedRuntime.make(
-  Layer.mergeAll(AccountService.defaultLayer, Instances.layer, TruncateService.layer).pipe(
-    Layer.provideMerge(AuthService.defaultLayer),
-  ),
+  Layer.mergeAll(
+    AccountService.defaultLayer, //
+    TruncateEffect.defaultLayer,
+    Instances.layer,
+  ).pipe(Layer.provideMerge(AuthService.defaultLayer)),
 )
 
 export function runPromiseInstance<A, E>(effect: Effect.Effect<A, E, InstanceServices>) {

+ 0 - 0
packages/opencode/src/permission/next.ts → packages/opencode/src/permission/index.ts


+ 1 - 1
packages/opencode/src/server/routes/permission.ts

@@ -1,7 +1,7 @@
 import { Hono } from "hono"
 import { describeRoute, validator, resolver } from "hono-openapi"
 import z from "zod"
-import { PermissionNext } from "@/permission/next"
+import { PermissionNext } from "@/permission"
 import { PermissionID } from "@/permission/schema"
 import { errors } from "../error"
 import { lazy } from "../../util/lazy"

+ 1 - 1
packages/opencode/src/server/routes/session.ts

@@ -14,7 +14,7 @@ import { Todo } from "../../session/todo"
 import { Agent } from "../../agent/agent"
 import { Snapshot } from "@/snapshot"
 import { Log } from "../../util/log"
-import { PermissionNext } from "@/permission/next"
+import { PermissionNext } from "@/permission"
 import { PermissionID } from "@/permission/schema"
 import { ModelID, ProviderID } from "@/provider/schema"
 import { errors } from "../error"

+ 1 - 1
packages/opencode/src/session/index.ts

@@ -28,7 +28,7 @@ import { SessionID, MessageID, PartID } from "./schema"
 
 import type { Provider } from "@/provider/provider"
 import { ModelID, ProviderID } from "@/provider/schema"
-import { PermissionNext } from "@/permission/next"
+import { PermissionNext } from "@/permission"
 import { Global } from "@/global"
 import type { LanguageModelV2Usage } from "@ai-sdk/provider"
 import { iife } from "@/util/iife"

+ 1 - 1
packages/opencode/src/session/llm.ts

@@ -20,7 +20,7 @@ import type { MessageV2 } from "./message-v2"
 import { Plugin } from "@/plugin"
 import { SystemPrompt } from "./system"
 import { Flag } from "@/flag/flag"
-import { PermissionNext } from "@/permission/next"
+import { PermissionNext } from "@/permission"
 import { Auth } from "@/auth"
 
 export namespace LLM {

+ 1 - 1
packages/opencode/src/session/processor.ts

@@ -12,7 +12,7 @@ import type { Provider } from "@/provider/provider"
 import { LLM } from "./llm"
 import { Config } from "@/config/config"
 import { SessionCompaction } from "./compaction"
-import { PermissionNext } from "@/permission/next"
+import { PermissionNext } from "@/permission"
 import { Question } from "@/question"
 import { PartID } from "./schema"
 import type { SessionID, MessageID } from "./schema"

+ 2 - 2
packages/opencode/src/session/prompt.ts

@@ -41,12 +41,12 @@ import { fn } from "@/util/fn"
 import { SessionProcessor } from "./processor"
 import { TaskTool } from "@/tool/task"
 import { Tool } from "@/tool/tool"
-import { PermissionNext } from "@/permission/next"
+import { PermissionNext } from "@/permission"
 import { SessionStatus } from "./status"
 import { LLM } from "./llm"
 import { iife } from "@/util/iife"
 import { Shell } from "@/shell/shell"
-import { Truncate } from "@/tool/truncation"
+import { Truncate } from "@/tool/truncate"
 import { decodeDataUrl } from "@/util/data-url"
 
 // @ts-ignore

+ 1 - 1
packages/opencode/src/session/session.sql.ts

@@ -2,7 +2,7 @@ import { sqliteTable, text, integer, index, primaryKey } from "drizzle-orm/sqlit
 import { ProjectTable } from "../project/project.sql"
 import type { MessageV2 } from "./message-v2"
 import type { Snapshot } from "../snapshot"
-import type { PermissionNext } from "../permission/next"
+import type { PermissionNext } from "../permission"
 import type { ProjectID } from "../project/schema"
 import type { SessionID, MessageID, PartID } from "./schema"
 import type { WorkspaceID } from "../control-plane/schema"

+ 1 - 1
packages/opencode/src/session/system.ts

@@ -11,7 +11,7 @@ import PROMPT_CODEX from "./prompt/codex_header.txt"
 import PROMPT_TRINITY from "./prompt/trinity.txt"
 import type { Provider } from "@/provider/provider"
 import type { Agent } from "@/agent/agent"
-import { PermissionNext } from "@/permission/next"
+import { PermissionNext } from "@/permission"
 import { Skill } from "@/skill"
 
 export namespace SystemPrompt {

+ 1 - 1
packages/opencode/src/skill/skill.ts

@@ -14,7 +14,7 @@ import { DiscoveryService } from "./discovery"
 import { Glob } from "../util/glob"
 import { pathToFileURL } from "url"
 import type { Agent } from "@/agent/agent"
-import { PermissionNext } from "@/permission/next"
+import { PermissionNext } from "@/permission"
 import { InstanceContext } from "@/effect/instance-context"
 import { Effect, Layer, ServiceMap } from "effect"
 import { runPromiseInstance } from "@/effect/runtime"

+ 1 - 1
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 "./truncation"
+import { Truncate } from "./truncate"
 import { Plugin } from "@/plugin"
 
 const MAX_METADATA_LENGTH = 30_000

+ 1 - 1
packages/opencode/src/tool/registry.ts

@@ -26,7 +26,7 @@ import { CodeSearchTool } from "./codesearch"
 import { Flag } from "@/flag/flag"
 import { Log } from "@/util/log"
 import { LspTool } from "./lsp"
-import { Truncate } from "./truncation"
+import { Truncate } from "./truncate"
 
 import { ApplyPatchTool } from "./apply_patch"
 import { Glob } from "../util/glob"

+ 1 - 1
packages/opencode/src/tool/task.ts

@@ -10,7 +10,7 @@ import { SessionPrompt } from "../session/prompt"
 import { iife } from "@/util/iife"
 import { defer } from "@/util/defer"
 import { Config } from "../config/config"
-import { PermissionNext } from "@/permission/next"
+import { PermissionNext } from "@/permission"
 
 const parameters = z.object({
   description: z.string().describe("A short (3-5 words) description of the task"),

+ 2 - 2
packages/opencode/src/tool/tool.ts

@@ -1,9 +1,9 @@
 import z from "zod"
 import type { MessageV2 } from "../session/message-v2"
 import type { Agent } from "../agent/agent"
-import type { PermissionNext } from "../permission/next"
+import type { PermissionNext } from "../permission"
 import type { SessionID, MessageID } from "../session/schema"
-import { Truncate } from "./truncation"
+import { Truncate } from "./truncate"
 
 export namespace Tool {
   interface Metadata {

+ 41 - 38
packages/opencode/src/tool/truncate-service.ts → packages/opencode/src/tool/truncate-effect.ts

@@ -1,62 +1,64 @@
+import { NodeFileSystem, NodePath } from "@effect/platform-node"
+import { Cause, Duration, Effect, FileSystem, Layer, Schedule, ServiceMap } from "effect"
 import path from "path"
-import { Log } from "../util/log"
-import { TRUNCATION_DIR } from "./truncation-dir"
-import { Identifier } from "../id/id"
 import type { Agent } from "../agent/agent"
 import { PermissionService } from "../permission/service"
-import { NodeFileSystem, NodePath } from "@effect/platform-node"
-import { Cause, Duration, Effect, FileSystem, Layer, Schedule, ServiceMap } from "effect"
+import { Identifier } from "../id/id"
+import { Log } from "../util/log"
 import { ToolID } from "./schema"
+import { TRUNCATION_DIR } from "./truncation-dir"
 
-const log = Log.create({ service: "truncation" })
-const RETENTION = Duration.days(7)
+export namespace TruncateEffect {
+  const log = Log.create({ service: "truncation" })
+  const RETENTION = Duration.days(7)
 
-export const MAX_LINES = 2000
-export const MAX_BYTES = 50 * 1024
+  export const MAX_LINES = 2000
+  export const MAX_BYTES = 50 * 1024
+  export const DIR = TRUNCATION_DIR
+  export const GLOB = path.join(TRUNCATION_DIR, "*")
 
-export type Result = { content: string; truncated: false } | { content: string; truncated: true; outputPath: string }
+  export type Result = { content: string; truncated: false } | { content: string; truncated: true; outputPath: string }
 
-export interface Options {
-  maxLines?: number
-  maxBytes?: number
-  direction?: "head" | "tail"
-}
+  export interface Options {
+    maxLines?: number
+    maxBytes?: number
+    direction?: "head" | "tail"
+  }
 
-function hasTaskTool(agent?: Agent.Info) {
-  if (!agent?.permission) return false
-  return PermissionService.evaluate("task", "*", agent.permission).action !== "deny"
-}
+  function hasTaskTool(agent?: Agent.Info) {
+    if (!agent?.permission) return false
+    return PermissionService.evaluate("task", "*", agent.permission).action !== "deny"
+  }
 
-export namespace TruncateService {
-  export interface Service {
+  export interface Api {
     readonly cleanup: () => Effect.Effect<void>
     readonly output: (text: string, options?: Options, agent?: Agent.Info) => Effect.Effect<Result>
   }
-}
 
-export class TruncateService extends ServiceMap.Service<TruncateService, TruncateService.Service>()(
-  "@opencode/Truncate",
-) {
-  static readonly layer = Layer.effect(
-    TruncateService,
+  export class Service extends ServiceMap.Service<Service, Api>()("@opencode/Truncate") {}
+
+  export const layer = Layer.effect(
+    Service,
     Effect.gen(function* () {
       const fs = yield* FileSystem.FileSystem
 
-      const cleanup = Effect.fn("TruncateService.cleanup")(function* () {
+      const cleanup = Effect.fn("TruncateEffect.cleanup")(function* () {
         const cutoff = Identifier.timestamp(Identifier.create("tool", false, Date.now() - Duration.toMillis(RETENTION)))
-        const entries = yield* fs
-          .readDirectory(TRUNCATION_DIR)
-          .pipe(
-            Effect.map((all) => all.filter((name) => name.startsWith("tool_"))),
-            Effect.catch(() => Effect.succeed([])),
-          )
+        const entries = yield* fs.readDirectory(TRUNCATION_DIR).pipe(
+          Effect.map((all) => all.filter((name) => name.startsWith("tool_"))),
+          Effect.catch(() => Effect.succeed([])),
+        )
         for (const entry of entries) {
           if (Identifier.timestamp(entry) >= cutoff) continue
           yield* fs.remove(path.join(TRUNCATION_DIR, entry)).pipe(Effect.catch(() => Effect.void))
         }
       })
 
-      const output = Effect.fn("TruncateService.output")(function* (text: string, options: Options = {}, agent?: Agent.Info) {
+      const output = Effect.fn("TruncateEffect.output")(function* (
+        text: string,
+        options: Options = {},
+        agent?: Agent.Info,
+      ) {
         const maxLines = options.maxLines ?? MAX_LINES
         const maxBytes = options.maxBytes ?? MAX_BYTES
         const direction = options.direction ?? "head"
@@ -116,7 +118,6 @@ export class TruncateService extends ServiceMap.Service<TruncateService, Truncat
         } as const
       })
 
-      // Start delayed hourly cleanup — scoped to runtime lifetime
       yield* cleanup().pipe(
         Effect.catchCause((cause) => {
           log.error("truncation cleanup failed", { cause: Cause.pretty(cause) })
@@ -127,7 +128,9 @@ export class TruncateService extends ServiceMap.Service<TruncateService, Truncat
         Effect.forkScoped,
       )
 
-      return TruncateService.of({ cleanup, output })
+      return Service.of({ cleanup, output })
     }),
-  ).pipe(Layer.provide(NodeFileSystem.layer), Layer.provide(NodePath.layer))
+  )
+
+  export const defaultLayer = layer.pipe(Layer.provide(NodeFileSystem.layer), Layer.provide(NodePath.layer))
 }

+ 4 - 6
packages/opencode/src/tool/truncation.ts → packages/opencode/src/tool/truncate.ts

@@ -1,21 +1,19 @@
-import path from "path"
-import { TRUNCATION_DIR } from "./truncation-dir"
 import type { Agent } from "../agent/agent"
 import { runtime } from "@/effect/runtime"
-import * as S from "./truncate-service"
+import { TruncateEffect as S } from "./truncate-effect"
 
 
 export namespace Truncate {
   export const MAX_LINES = S.MAX_LINES
   export const MAX_BYTES = S.MAX_BYTES
-  export const DIR = TRUNCATION_DIR
-  export const GLOB = path.join(TRUNCATION_DIR, "*")
+  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<Result> {
-    return runtime.runPromise(S.TruncateService.use((s) => s.output(text, options, agent)))
+    return runtime.runPromise(S.Service.use((s) => s.output(text, options, agent)))
   }
 }

+ 5 - 5
packages/opencode/test/agent/agent.test.ts

@@ -3,7 +3,7 @@ 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/next"
+import { PermissionNext } from "../../src/permission"
 
 // Helper to evaluate permission for a tool with wildcard pattern
 function evalPerm(agent: Agent.Info | undefined, permission: string): PermissionNext.Action | undefined {
@@ -76,7 +76,7 @@ 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/truncation")
+  const { Truncate } = await import("../../src/tool/truncate")
   await using tmp = await tmpdir()
   await Instance.provide({
     directory: tmp.path,
@@ -463,7 +463,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/truncation")
+  const { Truncate } = await import("../../src/tool/truncate")
   await using tmp = await tmpdir({
     config: {
       permission: {
@@ -483,7 +483,7 @@ test("Truncate.GLOB is allowed even when user denies external_directory globally
 })
 
 test("Truncate.GLOB is allowed even when user denies external_directory per-agent", async () => {
-  const { Truncate } = await import("../../src/tool/truncation")
+  const { Truncate } = await import("../../src/tool/truncate")
   await using tmp = await tmpdir({
     config: {
       agent: {
@@ -507,7 +507,7 @@ test("Truncate.GLOB is allowed even when user denies external_directory per-agen
 })
 
 test("explicit Truncate.GLOB deny is respected", async () => {
-  const { Truncate } = await import("../../src/tool/truncation")
+  const { Truncate } = await import("../../src/tool/truncate")
   await using tmp = await tmpdir({
     config: {
       permission: {

+ 1 - 1
packages/opencode/test/permission-task.test.ts

@@ -1,5 +1,5 @@
 import { describe, test, expect } from "bun:test"
-import { PermissionNext } from "../src/permission/next"
+import { PermissionNext } from "../src/permission"
 import { Config } from "../src/config/config"
 import { Instance } from "../src/project/instance"
 import { tmpdir } from "./fixture/fixture"

+ 1 - 1
packages/opencode/test/permission/next.test.ts

@@ -4,7 +4,7 @@ import { Effect } from "effect"
 import { Bus } from "../../src/bus"
 import { runtime } from "../../src/effect/runtime"
 import { Instances } from "../../src/effect/instances"
-import { PermissionNext } from "../../src/permission/next"
+import { PermissionNext } from "../../src/permission"
 import * as S from "../../src/permission/service"
 import { PermissionID } from "../../src/permission/schema"
 import { Instance } from "../../src/project/instance"

+ 2 - 2
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/next"
-import { Truncate } from "../../src/tool/truncation"
+import type { PermissionNext } from "../../src/permission"
+import { Truncate } from "../../src/tool/truncate"
 import { SessionID, MessageID } from "../../src/session/schema"
 
 const ctx = {

+ 1 - 1
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/next"
+import type { PermissionNext } from "../../src/permission"
 import { SessionID, MessageID } from "../../src/session/schema"
 
 const baseCtx: Omit<Tool.Context, "ask"> = {

+ 1 - 1
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/next"
+import { PermissionNext } from "../../src/permission"
 import { Agent } from "../../src/agent/agent"
 import { SessionID, MessageID } from "../../src/session/schema"
 

+ 1 - 1
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/next"
+import type { PermissionNext } from "../../src/permission"
 import type { Tool } from "../../src/tool/tool"
 import { Instance } from "../../src/project/instance"
 import { SkillTool } from "../../src/tool/skill"

+ 4 - 4
packages/opencode/test/tool/truncation.test.ts

@@ -1,8 +1,8 @@
 import { describe, test, expect } from "bun:test"
 import { NodeFileSystem } from "@effect/platform-node"
 import { Effect, FileSystem, Layer } from "effect"
-import { Truncate } from "../../src/tool/truncation"
-import { TruncateService } from "../../src/tool/truncate-service"
+import { Truncate } from "../../src/tool/truncate"
+import { TruncateEffect } from "../../src/tool/truncate-effect"
 import { Identifier } from "../../src/id/id"
 import { Filesystem } from "../../src/util/filesystem"
 import path from "path"
@@ -129,7 +129,7 @@ describe("Truncate", () => {
 
   describe("cleanup", () => {
     const DAY_MS = 24 * 60 * 60 * 1000
-    const it = testEffect(Layer.mergeAll(TruncateService.layer, NodeFileSystem.layer))
+    const it = testEffect(Layer.mergeAll(TruncateEffect.defaultLayer, NodeFileSystem.layer))
 
     it.effect("deletes files older than 7 days and preserves recent files", () =>
       Effect.gen(function* () {
@@ -142,7 +142,7 @@ describe("Truncate", () => {
 
         yield* writeFileStringScoped(old, "old content")
         yield* writeFileStringScoped(recent, "recent content")
-        yield* TruncateService.use((s) => s.cleanup())
+        yield* TruncateEffect.Service.use((s) => s.cleanup())
 
         expect(yield* fs.exists(old)).toBe(false)
         expect(yield* fs.exists(recent)).toBe(true)