diff --git a/packages/core/src/location-mutation.ts b/packages/core/src/location-mutation.ts index 5f410b95b1b0..277ca3595a85 100644 --- a/packages/core/src/location-mutation.ts +++ b/packages/core/src/location-mutation.ts @@ -10,8 +10,8 @@ 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 + * Mutation paths do not accept project references. Relative paths resolve + * from the active Location. Paths outside it require separate * `external_directory` approval. */ export const ResolveInput = Schema.Struct({ @@ -23,7 +23,7 @@ export type ResolveInput = typeof ResolveInput.Type export class PathError extends Schema.TaggedErrorClass()("LocationMutation.PathError", { path: Schema.String, - reason: Schema.Literals(["relative_escape", "location_escape", "non_directory_ancestor"]), + reason: Schema.Literals(["location_escape", "non_directory_ancestor"]), }) {} export interface ExternalDirectoryAuthorization { @@ -51,9 +51,9 @@ export interface Target { 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. + * Resolve a path and derive its permission resources. Relative paths resolve + * from the Location. Paths outside it require separate `external_directory` + * approval. This does not approve the mutation. */ readonly resolve: (input: ResolveInput) => Effect.Effect } @@ -118,10 +118,8 @@ const layer = Layer.effect( }) 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)) { diff --git a/packages/core/test/location-mutation.test.ts b/packages/core/test/location-mutation.test.ts index b09be42daa6c..30185ae68d81 100644 --- a/packages/core/test/location-mutation.test.ts +++ b/packages/core/test/location-mutation.test.ts @@ -60,11 +60,19 @@ describe("LocationMutation", () => { ), ) - it.live("rejects a relative lexical escape instead of promoting it to external authority", () => + it.live("requires external-directory authorization for a relative lexical escape", () => withTmp((directory) => Effect.gen(function* () { - const error = yield* Effect.flip((yield* LocationMutation.Service).resolve({ path: "../outside.txt" })) - expect(error).toMatchObject({ _tag: "LocationMutation.PathError", reason: "relative_escape" }) + const target = yield* (yield* LocationMutation.Service).resolve({ path: "../outside.txt" }) + const root = yield* Effect.promise(() => fs.realpath(path.dirname(directory))) + expect(target).toMatchObject({ + canonical: path.join(root, "outside.txt"), + resource: path.join(root, "outside.txt").replaceAll("\\", "/"), + }) + expect(target.externalDirectory).toMatchObject({ + directory: root, + resource: path.join(root, "*").replaceAll("\\", "/"), + }) }).pipe(provide(directory)), ), ) diff --git a/specs/v2/schema-changelog.md b/specs/v2/schema-changelog.md index 379c675d3dbb..e4da6775a4ea 100644 --- a/specs/v2/schema-changelog.md +++ b/specs/v2/schema-changelog.md @@ -260,8 +260,8 @@ Affected schema: Change: -- Resolve relative mutation paths within the active Location. -- Accept absolute internal paths and require explicit `external_directory` approval before leaf approval for external absolute paths. +- Resolve relative mutation paths from the active Location. +- Accept absolute internal paths and require explicit `external_directory` approval before leaf approval for external paths. - Keep named references read-oriented and reject them for mutation. - Revalidate path authority immediately before write mechanics.