Skip to content

Commit 1571933

Browse files
authored
Drop ALS fallbacks from containsPath and workspace routing (#25374)
1 parent 160928a commit 1571933

4 files changed

Lines changed: 20 additions & 31 deletions

File tree

packages/opencode/src/config/config.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@ import { AppFileSystem } from "@opencode-ai/core/filesystem"
2323
import { InstanceState } from "@/effect/instance-state"
2424
import { Context, Duration, Effect, Exit, Fiber, Layer, Option, Schema } from "effect"
2525
import { EffectFlock } from "@opencode-ai/core/util/effect-flock"
26-
import { InstanceRef } from "@/effect/instance-ref"
2726
import { zod } from "@/util/effect-zod"
2827
import { NonNegativeInt, PositiveInt, withStatics, type DeepMutable } from "@/util/schema"
2928
import { ConfigAgent } from "./agent"
@@ -459,7 +458,7 @@ export const layer = Layer.effect(
459458
const pluginScopeForSource = Effect.fnUntraced(function* (source: string) {
460459
if (source.startsWith("http://") || source.startsWith("https://")) return "global"
461460
if (source === "OPENCODE_CONFIG_CONTENT") return "local"
462-
if (yield* InstanceRef.use((ctx) => Effect.succeed(Instance.containsPath(source, ctx)))) return "local"
461+
if (Instance.containsPath(source, ctx)) return "local"
463462
return "global"
464463
})
465464

packages/opencode/src/project/instance.ts

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,16 +30,15 @@ export const Instance = {
3030

3131
/**
3232
* Check if a path is within the project boundary.
33-
* Returns true if path is inside Instance.directory OR Instance.worktree.
33+
* Returns true if path is inside ctx.directory OR ctx.worktree.
3434
* Paths within the worktree but outside the working directory should not trigger external_directory permission.
3535
*/
36-
containsPath(filepath: string, ctx?: InstanceContext) {
37-
const instance = ctx ?? Instance
38-
if (AppFileSystem.contains(instance.directory, filepath)) return true
36+
containsPath(filepath: string, ctx: InstanceContext) {
37+
if (AppFileSystem.contains(ctx.directory, filepath)) return true
3938
// Non-git projects set worktree to "/" which would match ANY absolute path.
4039
// Skip worktree check in this case to preserve external_directory permissions.
41-
if (instance.worktree === "/") return false
42-
return AppFileSystem.contains(instance.worktree, filepath)
40+
if (ctx.worktree === "/") return false
41+
return AppFileSystem.contains(ctx.worktree, filepath)
4342
},
4443
/**
4544
* Captures the current instance ALS context and returns a wrapper that

packages/opencode/src/server/routes/instance/httpapi/middleware/workspace-routing.ts

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import { getAdapter } from "@/control-plane/adapters"
22
import { WorkspaceID } from "@/control-plane/schema"
33
import type { Target } from "@/control-plane/types"
44
import { Workspace } from "@/control-plane/workspace"
5-
import { Instance } from "@/project/instance"
65
import { Session } from "@/session/session"
76
import { HttpApiProxy } from "./proxy"
87
import * as Fence from "@/server/fence"
@@ -43,14 +42,6 @@ export class WorkspaceRoutingMiddleware extends HttpApiMiddleware.Service<
4342
}
4443
>()("@opencode/ExperimentalHttpApiWorkspaceRouting") {}
4544

46-
function currentDirectory(): string {
47-
try {
48-
return Instance.directory
49-
} catch {
50-
return process.cwd()
51-
}
52-
}
53-
5445
function requestURL(request: HttpServerRequest.HttpServerRequest): URL {
5546
return new URL(request.url, "http://localhost")
5647
}
@@ -65,7 +56,7 @@ function selectedWorkspaceID(url: URL, sessionWorkspaceID?: WorkspaceID): Worksp
6556
}
6657

6758
function defaultDirectory(request: HttpServerRequest.HttpServerRequest, url: URL): string {
68-
return url.searchParams.get("directory") || request.headers["x-opencode-directory"] || currentDirectory()
59+
return url.searchParams.get("directory") || request.headers["x-opencode-directory"] || process.cwd()
6960
}
7061

7162
function shouldStayOnControlPlane(request: HttpServerRequest.HttpServerRequest, url: URL): boolean {

packages/opencode/test/file/path-traversal.test.ts

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -128,8 +128,8 @@ describe("Instance.containsPath", () => {
128128
await Instance.provide({
129129
directory: tmp.path,
130130
fn: () => {
131-
expect(Instance.containsPath(path.join(tmp.path, "foo.txt"))).toBe(true)
132-
expect(Instance.containsPath(path.join(tmp.path, "src", "file.ts"))).toBe(true)
131+
expect(Instance.containsPath(path.join(tmp.path, "foo.txt"), Instance.current)).toBe(true)
132+
expect(Instance.containsPath(path.join(tmp.path, "src", "file.ts"), Instance.current)).toBe(true)
133133
},
134134
})
135135
})
@@ -143,11 +143,11 @@ describe("Instance.containsPath", () => {
143143
directory: subdir,
144144
fn: () => {
145145
// .opencode at worktree root, but we're running from packages/lib
146-
expect(Instance.containsPath(path.join(tmp.path, ".opencode", "state"))).toBe(true)
146+
expect(Instance.containsPath(path.join(tmp.path, ".opencode", "state"), Instance.current)).toBe(true)
147147
// sibling package should also be accessible
148-
expect(Instance.containsPath(path.join(tmp.path, "packages", "other", "file.ts"))).toBe(true)
148+
expect(Instance.containsPath(path.join(tmp.path, "packages", "other", "file.ts"), Instance.current)).toBe(true)
149149
// worktree root itself
150-
expect(Instance.containsPath(tmp.path)).toBe(true)
150+
expect(Instance.containsPath(tmp.path, Instance.current)).toBe(true)
151151
},
152152
})
153153
})
@@ -158,8 +158,8 @@ describe("Instance.containsPath", () => {
158158
await Instance.provide({
159159
directory: tmp.path,
160160
fn: () => {
161-
expect(Instance.containsPath("/etc/passwd")).toBe(false)
162-
expect(Instance.containsPath("/tmp/other-project")).toBe(false)
161+
expect(Instance.containsPath("/etc/passwd", Instance.current)).toBe(false)
162+
expect(Instance.containsPath("/tmp/other-project", Instance.current)).toBe(false)
163163
},
164164
})
165165
})
@@ -170,7 +170,7 @@ describe("Instance.containsPath", () => {
170170
await Instance.provide({
171171
directory: tmp.path,
172172
fn: () => {
173-
expect(Instance.containsPath(path.join(tmp.path, "..", "escape.txt"))).toBe(false)
173+
expect(Instance.containsPath(path.join(tmp.path, "..", "escape.txt"), Instance.current)).toBe(false)
174174
},
175175
})
176176
})
@@ -182,8 +182,8 @@ describe("Instance.containsPath", () => {
182182
directory: tmp.path,
183183
fn: () => {
184184
expect(Instance.directory).toBe(Instance.worktree)
185-
expect(Instance.containsPath(path.join(tmp.path, "file.txt"))).toBe(true)
186-
expect(Instance.containsPath("/etc/passwd")).toBe(false)
185+
expect(Instance.containsPath(path.join(tmp.path, "file.txt"), Instance.current)).toBe(true)
186+
expect(Instance.containsPath("/etc/passwd", Instance.current)).toBe(false)
187187
},
188188
})
189189
})
@@ -195,9 +195,9 @@ describe("Instance.containsPath", () => {
195195
directory: tmp.path,
196196
fn: () => {
197197
// worktree is "/" for non-git projects, but containsPath should NOT allow all paths
198-
expect(Instance.containsPath(path.join(tmp.path, "file.txt"))).toBe(true)
199-
expect(Instance.containsPath("/etc/passwd")).toBe(false)
200-
expect(Instance.containsPath("/tmp/other")).toBe(false)
198+
expect(Instance.containsPath(path.join(tmp.path, "file.txt"), Instance.current)).toBe(true)
199+
expect(Instance.containsPath("/etc/passwd", Instance.current)).toBe(false)
200+
expect(Instance.containsPath("/tmp/other", Instance.current)).toBe(false)
201201
},
202202
})
203203
})

0 commit comments

Comments
 (0)