Drop ALS fallbacks from containsPath and workspace routing (#25374)
This commit is contained in:
@@ -23,7 +23,6 @@ import { AppFileSystem } from "@opencode-ai/core/filesystem"
|
|||||||
import { InstanceState } from "@/effect/instance-state"
|
import { InstanceState } from "@/effect/instance-state"
|
||||||
import { Context, Duration, Effect, Exit, Fiber, Layer, Option, Schema } from "effect"
|
import { Context, Duration, Effect, Exit, Fiber, Layer, Option, Schema } from "effect"
|
||||||
import { EffectFlock } from "@opencode-ai/core/util/effect-flock"
|
import { EffectFlock } from "@opencode-ai/core/util/effect-flock"
|
||||||
import { InstanceRef } from "@/effect/instance-ref"
|
|
||||||
import { zod } from "@/util/effect-zod"
|
import { zod } from "@/util/effect-zod"
|
||||||
import { NonNegativeInt, PositiveInt, withStatics, type DeepMutable } from "@/util/schema"
|
import { NonNegativeInt, PositiveInt, withStatics, type DeepMutable } from "@/util/schema"
|
||||||
import { ConfigAgent } from "./agent"
|
import { ConfigAgent } from "./agent"
|
||||||
@@ -459,7 +458,7 @@ export const layer = Layer.effect(
|
|||||||
const pluginScopeForSource = Effect.fnUntraced(function* (source: string) {
|
const pluginScopeForSource = Effect.fnUntraced(function* (source: string) {
|
||||||
if (source.startsWith("http://") || source.startsWith("https://")) return "global"
|
if (source.startsWith("http://") || source.startsWith("https://")) return "global"
|
||||||
if (source === "OPENCODE_CONFIG_CONTENT") return "local"
|
if (source === "OPENCODE_CONFIG_CONTENT") return "local"
|
||||||
if (yield* InstanceRef.use((ctx) => Effect.succeed(Instance.containsPath(source, ctx)))) return "local"
|
if (Instance.containsPath(source, ctx)) return "local"
|
||||||
return "global"
|
return "global"
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -30,16 +30,15 @@ export const Instance = {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if a path is within the project boundary.
|
* Check if a path is within the project boundary.
|
||||||
* Returns true if path is inside Instance.directory OR Instance.worktree.
|
* Returns true if path is inside ctx.directory OR ctx.worktree.
|
||||||
* Paths within the worktree but outside the working directory should not trigger external_directory permission.
|
* Paths within the worktree but outside the working directory should not trigger external_directory permission.
|
||||||
*/
|
*/
|
||||||
containsPath(filepath: string, ctx?: InstanceContext) {
|
containsPath(filepath: string, ctx: InstanceContext) {
|
||||||
const instance = ctx ?? Instance
|
if (AppFileSystem.contains(ctx.directory, filepath)) return true
|
||||||
if (AppFileSystem.contains(instance.directory, filepath)) return true
|
|
||||||
// Non-git projects set worktree to "/" which would match ANY absolute path.
|
// Non-git projects set worktree to "/" which would match ANY absolute path.
|
||||||
// Skip worktree check in this case to preserve external_directory permissions.
|
// Skip worktree check in this case to preserve external_directory permissions.
|
||||||
if (instance.worktree === "/") return false
|
if (ctx.worktree === "/") return false
|
||||||
return AppFileSystem.contains(instance.worktree, filepath)
|
return AppFileSystem.contains(ctx.worktree, filepath)
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
* Captures the current instance ALS context and returns a wrapper that
|
* Captures the current instance ALS context and returns a wrapper that
|
||||||
|
|||||||
+1
-10
@@ -2,7 +2,6 @@ import { getAdapter } from "@/control-plane/adapters"
|
|||||||
import { WorkspaceID } from "@/control-plane/schema"
|
import { WorkspaceID } from "@/control-plane/schema"
|
||||||
import type { Target } from "@/control-plane/types"
|
import type { Target } from "@/control-plane/types"
|
||||||
import { Workspace } from "@/control-plane/workspace"
|
import { Workspace } from "@/control-plane/workspace"
|
||||||
import { Instance } from "@/project/instance"
|
|
||||||
import { Session } from "@/session/session"
|
import { Session } from "@/session/session"
|
||||||
import { HttpApiProxy } from "./proxy"
|
import { HttpApiProxy } from "./proxy"
|
||||||
import * as Fence from "@/server/fence"
|
import * as Fence from "@/server/fence"
|
||||||
@@ -43,14 +42,6 @@ export class WorkspaceRoutingMiddleware extends HttpApiMiddleware.Service<
|
|||||||
}
|
}
|
||||||
>()("@opencode/ExperimentalHttpApiWorkspaceRouting") {}
|
>()("@opencode/ExperimentalHttpApiWorkspaceRouting") {}
|
||||||
|
|
||||||
function currentDirectory(): string {
|
|
||||||
try {
|
|
||||||
return Instance.directory
|
|
||||||
} catch {
|
|
||||||
return process.cwd()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function requestURL(request: HttpServerRequest.HttpServerRequest): URL {
|
function requestURL(request: HttpServerRequest.HttpServerRequest): URL {
|
||||||
return new URL(request.url, "http://localhost")
|
return new URL(request.url, "http://localhost")
|
||||||
}
|
}
|
||||||
@@ -65,7 +56,7 @@ function selectedWorkspaceID(url: URL, sessionWorkspaceID?: WorkspaceID): Worksp
|
|||||||
}
|
}
|
||||||
|
|
||||||
function defaultDirectory(request: HttpServerRequest.HttpServerRequest, url: URL): string {
|
function defaultDirectory(request: HttpServerRequest.HttpServerRequest, url: URL): string {
|
||||||
return url.searchParams.get("directory") || request.headers["x-opencode-directory"] || currentDirectory()
|
return url.searchParams.get("directory") || request.headers["x-opencode-directory"] || process.cwd()
|
||||||
}
|
}
|
||||||
|
|
||||||
function shouldStayOnControlPlane(request: HttpServerRequest.HttpServerRequest, url: URL): boolean {
|
function shouldStayOnControlPlane(request: HttpServerRequest.HttpServerRequest, url: URL): boolean {
|
||||||
|
|||||||
@@ -128,8 +128,8 @@ describe("Instance.containsPath", () => {
|
|||||||
await Instance.provide({
|
await Instance.provide({
|
||||||
directory: tmp.path,
|
directory: tmp.path,
|
||||||
fn: () => {
|
fn: () => {
|
||||||
expect(Instance.containsPath(path.join(tmp.path, "foo.txt"))).toBe(true)
|
expect(Instance.containsPath(path.join(tmp.path, "foo.txt"), Instance.current)).toBe(true)
|
||||||
expect(Instance.containsPath(path.join(tmp.path, "src", "file.ts"))).toBe(true)
|
expect(Instance.containsPath(path.join(tmp.path, "src", "file.ts"), Instance.current)).toBe(true)
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -143,11 +143,11 @@ describe("Instance.containsPath", () => {
|
|||||||
directory: subdir,
|
directory: subdir,
|
||||||
fn: () => {
|
fn: () => {
|
||||||
// .opencode at worktree root, but we're running from packages/lib
|
// .opencode at worktree root, but we're running from packages/lib
|
||||||
expect(Instance.containsPath(path.join(tmp.path, ".opencode", "state"))).toBe(true)
|
expect(Instance.containsPath(path.join(tmp.path, ".opencode", "state"), Instance.current)).toBe(true)
|
||||||
// sibling package should also be accessible
|
// sibling package should also be accessible
|
||||||
expect(Instance.containsPath(path.join(tmp.path, "packages", "other", "file.ts"))).toBe(true)
|
expect(Instance.containsPath(path.join(tmp.path, "packages", "other", "file.ts"), Instance.current)).toBe(true)
|
||||||
// worktree root itself
|
// worktree root itself
|
||||||
expect(Instance.containsPath(tmp.path)).toBe(true)
|
expect(Instance.containsPath(tmp.path, Instance.current)).toBe(true)
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -158,8 +158,8 @@ describe("Instance.containsPath", () => {
|
|||||||
await Instance.provide({
|
await Instance.provide({
|
||||||
directory: tmp.path,
|
directory: tmp.path,
|
||||||
fn: () => {
|
fn: () => {
|
||||||
expect(Instance.containsPath("/etc/passwd")).toBe(false)
|
expect(Instance.containsPath("/etc/passwd", Instance.current)).toBe(false)
|
||||||
expect(Instance.containsPath("/tmp/other-project")).toBe(false)
|
expect(Instance.containsPath("/tmp/other-project", Instance.current)).toBe(false)
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -170,7 +170,7 @@ describe("Instance.containsPath", () => {
|
|||||||
await Instance.provide({
|
await Instance.provide({
|
||||||
directory: tmp.path,
|
directory: tmp.path,
|
||||||
fn: () => {
|
fn: () => {
|
||||||
expect(Instance.containsPath(path.join(tmp.path, "..", "escape.txt"))).toBe(false)
|
expect(Instance.containsPath(path.join(tmp.path, "..", "escape.txt"), Instance.current)).toBe(false)
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -182,8 +182,8 @@ describe("Instance.containsPath", () => {
|
|||||||
directory: tmp.path,
|
directory: tmp.path,
|
||||||
fn: () => {
|
fn: () => {
|
||||||
expect(Instance.directory).toBe(Instance.worktree)
|
expect(Instance.directory).toBe(Instance.worktree)
|
||||||
expect(Instance.containsPath(path.join(tmp.path, "file.txt"))).toBe(true)
|
expect(Instance.containsPath(path.join(tmp.path, "file.txt"), Instance.current)).toBe(true)
|
||||||
expect(Instance.containsPath("/etc/passwd")).toBe(false)
|
expect(Instance.containsPath("/etc/passwd", Instance.current)).toBe(false)
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -195,9 +195,9 @@ describe("Instance.containsPath", () => {
|
|||||||
directory: tmp.path,
|
directory: tmp.path,
|
||||||
fn: () => {
|
fn: () => {
|
||||||
// worktree is "/" for non-git projects, but containsPath should NOT allow all paths
|
// worktree is "/" for non-git projects, but containsPath should NOT allow all paths
|
||||||
expect(Instance.containsPath(path.join(tmp.path, "file.txt"))).toBe(true)
|
expect(Instance.containsPath(path.join(tmp.path, "file.txt"), Instance.current)).toBe(true)
|
||||||
expect(Instance.containsPath("/etc/passwd")).toBe(false)
|
expect(Instance.containsPath("/etc/passwd", Instance.current)).toBe(false)
|
||||||
expect(Instance.containsPath("/tmp/other")).toBe(false)
|
expect(Instance.containsPath("/tmp/other", Instance.current)).toBe(false)
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user