diff --git a/packages/opencode/src/server/routes/instance/httpapi/middleware/instance-context.ts b/packages/opencode/src/server/routes/instance/httpapi/middleware/instance-context.ts index e39eb7394a50..58ebbc03374e 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/middleware/instance-context.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/middleware/instance-context.ts @@ -1,8 +1,10 @@ import { InstanceRef, WorkspaceRef } from "@/effect/instance-ref" +import { FSUtil } from "@opencode-ai/core/fs-util" import { InstanceStore } from "@/project/instance-store" import { Effect, Layer } from "effect" import { HttpServerResponse } from "effect/unstable/http" import { HttpApiMiddleware } from "effect/unstable/httpapi" +import { notFound } from "../errors" import { WorkspaceRouteContext } from "./workspace-routing" export class InstanceContextMiddleware extends HttpApiMiddleware.Service< @@ -23,10 +25,18 @@ function decode(input: string): string { function provideInstanceContext( effect: Effect.Effect, store: InstanceStore.Interface, + fs: FSUtil.Interface, ): Effect.Effect { return Effect.gen(function* () { const route = yield* WorkspaceRouteContext - const ctx = yield* store.load({ directory: decode(route.directory) }) + const directory = decode(route.directory) + // Validate the requested directory before loading: load may return a cached context or resolve a missing child + // through an ancestor repository. + if (!(yield* fs.isDir(directory))) { + return HttpServerResponse.jsonUnsafe(notFound(`Project directory not found: ${directory}`), { status: 404 }) + } + + const ctx = yield* store.load({ directory }) return yield* effect.pipe( Effect.provideService(InstanceRef, ctx), Effect.provideService(WorkspaceRef, route.workspaceID), @@ -38,6 +48,7 @@ export const instanceContextLayer = Layer.effect( InstanceContextMiddleware, Effect.gen(function* () { const store = yield* InstanceStore.Service - return InstanceContextMiddleware.of((effect) => provideInstanceContext(effect, store)) + const fs = yield* FSUtil.Service + return InstanceContextMiddleware.of((effect) => provideInstanceContext(effect, store, fs)) }), ) diff --git a/packages/opencode/test/server/httpapi-instance.test.ts b/packages/opencode/test/server/httpapi-instance.test.ts index 5bbd6d7cc0d8..ed3d38c2bfd1 100644 --- a/packages/opencode/test/server/httpapi-instance.test.ts +++ b/packages/opencode/test/server/httpapi-instance.test.ts @@ -262,4 +262,51 @@ describe("instance HttpApi", () => { ) }), ) + + it.live("rejects instance requests for missing project directories", () => + Effect.gen(function* () { + const parent = yield* tmpdirScoped({ git: true }) + const dir = `${parent}/deleted-project` + const response = yield* HttpClientRequest.get(InstancePaths.path).pipe(directoryHeader(dir), HttpClient.execute) + + expect(response.status).toBe(404) + expect(yield* response.json).toEqual({ + name: "NotFoundError", + data: { message: `Project directory not found: ${dir}` }, + }) + + const prompt = yield* HttpClientRequest.post(SessionPaths.promptAsync.replace(":sessionID", "ses_missing")).pipe( + directoryHeader(dir), + HttpClientRequest.bodyJson({ agent: "build", parts: [{ type: "text", text: "hello" }] }), + Effect.flatMap(HttpClient.execute), + ) + + expect(prompt.status).toBe(404) + expect(yield* prompt.json).toEqual({ + name: "NotFoundError", + data: { message: `Project directory not found: ${dir}` }, + }) + }), + ) + + it.live("rejects cached instance requests after the project directory is deleted", () => + Effect.gen(function* () { + const parent = yield* tmpdirScoped({ git: true }) + const dir = `${parent}/deleted-project` + const fs = yield* FileSystem.FileSystem + yield* fs.makeDirectory(dir, { recursive: true }) + + const initial = yield* HttpClientRequest.get(InstancePaths.path).pipe(directoryHeader(dir), HttpClient.execute) + expect(initial.status).toBe(200) + + yield* fs.remove(dir, { recursive: true }) + + const response = yield* HttpClientRequest.get(InstancePaths.path).pipe(directoryHeader(dir), HttpClient.execute) + expect(response.status).toBe(404) + expect(yield* response.json).toEqual({ + name: "NotFoundError", + data: { message: `Project directory not found: ${dir}` }, + }) + }), + ) })