Something went wrong. Try again.
opencode v2 mirror
Something went wrong. Try again.
TypeScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174export * as FileAccess from "./file-access.js"
import { makeLocationNode } from "@opencode/util/effect/app-node"import { FSUtil } from "@opencode/util/fs-util"import { Global } from "@opencode/util/global"import { Array, Context, Effect, Layer, Schema } from "effect"import path from "path"import { Location } from "./location.js"import { Permission } from "./permission.js"import { Project } from "./project.js"import { AbsolutePath } from "./schema.js"import type { SessionErrors } from "./session/error.js"import type { Tool } from "./tool.js"
export const Kind = Schema.Literals(["file", "directory"])export type Kind = typeof Kind.Type
export const ResolveInput = Schema.Struct({ path: Schema.String, /** Selects the external approval boundary; it does not validate the target type. */ kind: Kind.pipe(Schema.optional),})export type ResolveInput = typeof ResolveInput.Type
export interface ExternalDirectoryAuthorization { readonly action: "external_directory" /** Lexical directory used as the external approval boundary. */ readonly directory: AbsolutePath readonly resource: string readonly save: string}
export const externalDirectoryPermission = (input: ExternalDirectoryAuthorization) => ({ action: input.action, resources: [input.resource], save: [input.save],})
export interface Target { readonly absolute: AbsolutePath /** Location-relative for internal paths, absolute for external paths. */ readonly resource: string readonly externalDirectory?: ExternalDirectoryAuthorization}
export type Invocation = Pick<Tool.Context, "sessionID" | "agent" | "messageID" | "id">
export interface ReadOptions { /** A target already authorized by this invocation, used for filename recovery. */ readonly siblingOf: Target}
export interface Interface { /** Resolve a lexical path and its permission resources, without requesting approval. */ readonly resolve: (input: ResolveInput) => Effect.Effect<Target, FSUtil.Error> /** Approve external directories in one batch, preserving first-seen resource order. */ readonly authorizeExternal: ( targets: readonly Target[], context: Invocation, metadata?: Permission.AssertInput["metadata"], ) => Effect.Effect<void, Error | SessionErrors.NotFoundError> /** Resolve a read target and obtain external-directory approval before read approval. */ readonly authorizeRead: ( file: string, context: Invocation, options?: ReadOptions, ) => Effect.Effect<Target, FSUtil.Error | Error | SessionErrors.NotFoundError>}
export class Service extends Context.Service<Service, Interface>()("@opencode/FileAccess") {}
/** Expand a leading ~ and normalize Windows shell paths before lexical resolution. */export const resolvePath = (directory: string, input: string, home = Global.Path.home) => { const normalized = FSUtil.windowsPath(input) return path.resolve( directory, normalized === "~" ? home : normalized.startsWith("~/") || (process.platform === "win32" && normalized.startsWith("~\\")) ? path.join(home, normalized.slice(2)) : normalized, )}
const slash = (value: string) => value.replaceAll("\\", "/")const invocation = (context: Invocation) => ({ sessionID: context.sessionID, agent: context.agent, source: { type: "tool" as const, messageID: context.messageID, id: context.id },})
const layer = Layer.effect( Service, Effect.gen(function* () { const fs = yield* FSUtil.Service const location = yield* Location.Service const permission = yield* Permission.Service
const resolve = Effect.fn("FileAccess.resolve")(function* (input: ResolveInput) { const absolute = AbsolutePath.make(resolvePath(location.directory, input.path)) const worktree = path.resolve(location.project.directory) const internal = FSUtil.contains(location.directory, absolute) || (worktree !== path.parse(worktree).root && FSUtil.contains(worktree, absolute)) if (internal) { return { absolute, resource: slash(path.relative(location.directory, absolute) || "."), } satisfies Target } const type = input.kind === "directory" ? "Directory" : input.kind === "file" ? "File" : (yield* fs.stat(absolute).pipe(Effect.catchReason("PlatformError", "NotFound", () => Effect.undefined))) ?.type const directory = AbsolutePath.make(type === "Directory" ? absolute : path.dirname(absolute)) return { absolute, resource: slash(absolute), externalDirectory: { action: "external_directory", directory, resource: slash(path.join(directory, "*")), save: slash(path.join((yield* Project.root(fs, directory)) ?? directory, "*")), }, } satisfies Target })
const authorizeExternal = Effect.fn("FileAccess.authorizeExternal")(function* ( targets: readonly Target[], context: Invocation, metadata?: Permission.AssertInput["metadata"], ) { const external = Array.dedupeWith( targets.flatMap((target) => (target.externalDirectory ? [target.externalDirectory] : [])), (left, right) => left.resource === right.resource, ) if (external.length === 0) return yield* permission.assert({ action: "external_directory", resources: external.map((item) => item.resource), save: external.map((item) => item.save), ...(metadata === undefined ? {} : { metadata }), ...invocation(context), }) })
const authorizeRead = Effect.fn("FileAccess.authorizeRead")(function* ( file: string, context: Invocation, options?: ReadOptions, ) { const target = yield* resolve({ path: file, kind: options ? "file" : undefined }) const sibling = options && path.dirname(target.absolute) === path.dirname(options.siblingOf.absolute)
// Filename recovery shares the directory approval, but checks the recovered file's own read rules. if (!sibling) yield* authorizeExternal([target], context) yield* permission.assert({ action: "read", resources: [target.resource], save: ["*"], ...invocation(context), }) return target })
return Service.of({ resolve, authorizeExternal, authorizeRead }) }),)
export const node = makeLocationNode({ service: Service, layer, deps: [FSUtil.node, Location.node, Permission.node] })