| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162 |
- export * as LocationMutation from "./location-mutation"
- import { makeLocationNode } from "./effect/app-node"
- import path from "path"
- import { Context, Effect, Layer, Schema } from "effect"
- import { FSUtil } from "./fs-util"
- import { Location } from "./location"
- export const Kind = Schema.Literals(["file", "directory"])
- export type Kind = typeof Kind.Type
- /**
- * Mutation paths do not accept project references. Relative paths must stay
- * inside the active Location. Absolute paths outside it require separate
- * `external_directory` approval.
- */
- 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 class PathError extends Schema.TaggedErrorClass<PathError>()("LocationMutation.PathError", {
- path: Schema.String,
- reason: Schema.Literals(["relative_escape", "location_escape", "non_directory_ancestor"]),
- }) {}
- export interface ExternalDirectoryAuthorization {
- readonly action: "external_directory"
- /** Canonical existing directory used as the external approval boundary. */
- readonly directory: string
- /** `external_directory` permission resource. */
- readonly resource: string
- readonly save: string
- }
- export const externalDirectoryPermission = (input: ExternalDirectoryAuthorization) => ({
- action: input.action,
- resources: [input.resource],
- save: [input.save],
- })
- export interface Target {
- /** Canonical existing path, or missing path below a canonical directory. */
- readonly canonical: string
- /** Permission resource: Location-relative for internal paths, canonical for external paths. */
- readonly resource: string
- readonly externalDirectory?: ExternalDirectoryAuthorization
- }
- export interface Interface {
- /**
- * Resolve a path and derive its permission resources. Relative paths must
- * stay inside the Location. Absolute paths outside it require separate
- * `external_directory` approval. This does not approve the mutation.
- */
- readonly resolve: (input: ResolveInput) => Effect.Effect<Target, PathError | FSUtil.Error>
- }
- export class Service extends Context.Service<Service, Interface>()("@opencode/v2/LocationMutation") {}
- interface ResolvedPath {
- readonly canonical: string
- readonly type?:
- | "File"
- | "Directory"
- | "SymbolicLink"
- | "BlockDevice"
- | "CharacterDevice"
- | "FIFO"
- | "Socket"
- | "Unknown"
- readonly directory: string
- }
- const slash = (value: string) => value.replaceAll("\\", "/")
- const layer = Layer.effect(
- Service,
- Effect.gen(function* () {
- const fs = yield* FSUtil.Service
- const location = yield* Location.Service
- const locationRoot = yield* fs.realPath(location.directory)
- function notFound<A>(effect: Effect.Effect<A, FSUtil.Error>) {
- return effect.pipe(Effect.catchReason("PlatformError", "NotFound", () => Effect.succeed(undefined)))
- }
- const resolvePath = Effect.fnUntraced(function* (absolute: string) {
- const existing = yield* notFound(fs.realPath(absolute))
- if (existing !== undefined) {
- const info = yield* fs.stat(existing)
- return {
- canonical: existing,
- type: info.type,
- directory: info.type === "Directory" ? existing : path.dirname(existing),
- } satisfies ResolvedPath
- }
- let anchor = path.dirname(absolute)
- while (true) {
- const canonical = yield* notFound(fs.realPath(anchor))
- if (canonical !== undefined) {
- const info = yield* fs.stat(canonical)
- if (info.type !== "Directory") {
- return yield* new PathError({ path: absolute, reason: "non_directory_ancestor" })
- }
- return {
- canonical: path.resolve(canonical, path.relative(anchor, absolute)),
- directory: canonical,
- } satisfies ResolvedPath
- }
- const parent = path.dirname(anchor)
- if (parent === anchor) return yield* new PathError({ path: absolute, reason: "non_directory_ancestor" })
- anchor = parent
- }
- })
- const resolve = Effect.fn("LocationMutation.resolve")(function* (input: ResolveInput) {
- const relative = !path.isAbsolute(input.path)
- const absolute = path.resolve(location.directory, input.path)
- const lexicallyInternal = FSUtil.contains(location.directory, absolute)
- if (relative && !lexicallyInternal) return yield* new PathError({ path: input.path, reason: "relative_escape" })
- const resolved = yield* resolvePath(absolute)
- if (lexicallyInternal && !FSUtil.contains(locationRoot, resolved.canonical)) {
- return yield* new PathError({ path: input.path, reason: "location_escape" })
- }
- const external = !lexicallyInternal
- const resource = external
- ? slash(resolved.canonical)
- : slash(path.relative(locationRoot, resolved.canonical) || ".")
- const externalDirectory =
- input.kind === "directory" && resolved.type === "Directory" ? resolved.canonical : resolved.directory
- const externalResource = slash(path.join(externalDirectory, "*"))
- return {
- canonical: resolved.canonical,
- resource,
- externalDirectory: external
- ? {
- action: "external_directory",
- directory: externalDirectory,
- resource: externalResource,
- save: externalResource,
- }
- : undefined,
- } satisfies Target
- })
- return Service.of({ resolve })
- }),
- )
- export const locationLayer = layer
- export const node = makeLocationNode({
- service: Service,
- layer: layer.pipe(Layer.orDie),
- deps: [FSUtil.node, Location.node],
- })
|