mirror of
https://github.com/anomalyco/opencode.git
synced 2026-11-07 16:17:25 +03:00
fix(server): reject a session location that is not an absolute path
This commit is contained in:
Generated
+2
-2
@@ -1059,7 +1059,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "Create a session at the requested location. A parentID creates a linked child session at its parent's location.",
|
||||
"description": "Create a session at the requested location. A parentID creates a linked child session at its parent's location. The location directory must be an absolute path.",
|
||||
"summary": "Create session",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
@@ -1312,7 +1312,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "Import a projected session transcript at the requested location. Accepts transcripts exported by earlier releases. If parentID is supplied, the parent session must already exist; import parents before children.",
|
||||
"description": "Import a projected session transcript at the requested location. Accepts transcripts exported by earlier releases. If parentID is supplied, the parent session must already exist; import parents before children. The location directory must be an absolute path.",
|
||||
"summary": "Import session",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
|
||||
@@ -174,13 +174,13 @@ export const makeSessionGroup = <I extends HttpApiMiddleware.AnyId, S, FormI ext
|
||||
permissions: Permission.Ruleset.pipe(Schema.optional),
|
||||
}),
|
||||
success: Schema.Struct({ data: PublicSessionInfo }),
|
||||
error: SessionNotFoundError,
|
||||
error: [InvalidRequestError, SessionNotFoundError],
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "session.create",
|
||||
summary: "Create session",
|
||||
description:
|
||||
"Create a session at the requested location. A parentID creates a linked child session at its parent's location.",
|
||||
"Create a session at the requested location. A parentID creates a linked child session at its parent's location. The location directory must be an absolute path.",
|
||||
}),
|
||||
),
|
||||
)
|
||||
@@ -192,13 +192,13 @@ export const makeSessionGroup = <I extends HttpApiMiddleware.AnyId, S, FormI ext
|
||||
location: Location.PublicRef.pipe(Schema.optional),
|
||||
}),
|
||||
success: Schema.Struct({ data: PublicSessionInfo }),
|
||||
error: [ConflictError, SessionNotFoundError],
|
||||
error: [ConflictError, InvalidRequestError, SessionNotFoundError],
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "experimental.session.import",
|
||||
summary: "Import session",
|
||||
description:
|
||||
"Import a projected session transcript at the requested location. Accepts transcripts exported by earlier releases. If parentID is supplied, the parent session must already exist; import parents before children.",
|
||||
"Import a projected session transcript at the requested location. Accepts transcripts exported by earlier releases. If parentID is supplied, the parent session must already exist; import parents before children. The location directory must be an absolute path.",
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
SessionBusyError,
|
||||
SkillNotFoundError,
|
||||
} from "@opencode/protocol/errors"
|
||||
import { storedAbsolutePath } from "@opencode/core/database/path"
|
||||
import { AbsolutePath } from "@opencode/core/schema"
|
||||
import { locationErrors } from "../location"
|
||||
import { failedMessageDecode, failedSnapshot, missingMessage, missingSession } from "./session-error"
|
||||
@@ -34,6 +35,14 @@ function missingForm(id: Form.ID) {
|
||||
return new FormNotFoundError({ id, message: `Form not found: ${id}` })
|
||||
}
|
||||
|
||||
// Session and project rows only store absolute directories, so anything else would die at insert as an empty 500.
|
||||
function requireStorableLocation(location: { readonly directory: string } | undefined) {
|
||||
if (location === undefined || storedAbsolutePath(location.directory) !== undefined) return Effect.void
|
||||
return Effect.fail(
|
||||
new InvalidRequestError({ message: "location.directory must be an absolute path", field: "location" }),
|
||||
)
|
||||
}
|
||||
|
||||
export const SessionHandler = HttpApiBuilder.group(Api, "server.session", (handlers) =>
|
||||
Effect.gen(function* () {
|
||||
const session = yield* Session.Service
|
||||
@@ -101,6 +110,7 @@ export const SessionHandler = HttpApiBuilder.group(Api, "server.session", (handl
|
||||
.handle(
|
||||
"session.create",
|
||||
Effect.fn(function* (ctx) {
|
||||
if (ctx.payload.parentID === undefined) yield* requireStorableLocation(ctx.payload.location)
|
||||
return {
|
||||
data: yield* session
|
||||
.create({
|
||||
@@ -121,6 +131,7 @@ export const SessionHandler = HttpApiBuilder.group(Api, "server.session", (handl
|
||||
.handle(
|
||||
"session.import",
|
||||
Effect.fn(function* (ctx) {
|
||||
yield* requireStorableLocation(ctx.payload.location)
|
||||
return {
|
||||
data: yield* transfer
|
||||
.import({
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
import { expect } from "bun:test"
|
||||
import { App } from "@opencode/core/app"
|
||||
import { Bus } from "@opencode/core/bus"
|
||||
import { Database } from "@opencode/core/database/database"
|
||||
import { Watcher } from "@opencode/core/filesystem/watcher"
|
||||
import { ModelsDev } from "@opencode/core/models-dev"
|
||||
import { SessionExecution } from "@opencode/core/session/execution"
|
||||
import { LayerNode } from "@opencode/util/effect/layer-node"
|
||||
import { Global } from "@opencode/util/global"
|
||||
import { Context, Effect, Layer } from "effect"
|
||||
import { HttpEffect, HttpRouter, HttpServer } from "effect/http"
|
||||
import { tempGlobalLayer } from "../../core/test/fixture/global"
|
||||
import { tmpdirScoped } from "../../core/test/fixture/tmpdir"
|
||||
import { it } from "../../core/test/lib/effect"
|
||||
import { createEmbeddedRoutes } from "../src/routes"
|
||||
|
||||
it.live("rejects a session location that cannot be stored as an absolute path", () =>
|
||||
Effect.gen(function* () {
|
||||
const dir = yield* tmpdirScoped()
|
||||
const replacements: LayerNode.Replacements = [
|
||||
Global.node.replace(tempGlobalLayer),
|
||||
Database.node.replace(Database.configured({ path: ":memory:" })),
|
||||
Bus.node.replace(Bus.node),
|
||||
App.node.replace(App.node),
|
||||
ModelsDev.node.replace(ModelsDev.configured({ fetch: false })),
|
||||
Watcher.node.replace(Watcher.configured({ enabled: false })),
|
||||
SessionExecution.node.replace(SessionExecution.noopLayer),
|
||||
]
|
||||
const context = yield* Layer.build(
|
||||
createEmbeddedRoutes({ config: { directory: dir.path, project: false, content: "{}" } }, replacements).pipe(
|
||||
Layer.provide(HttpServer.layerServices),
|
||||
),
|
||||
)
|
||||
const handler = Context.get(context, HttpRouter.HttpRouter)
|
||||
.asHttpEffect()
|
||||
.pipe(HttpEffect.toWebHandlerWith(context))
|
||||
const create = (directory: string) =>
|
||||
Effect.promise(() =>
|
||||
handler(
|
||||
new Request("http://opencode.local/api/session", {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({ location: { directory } }),
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
// A relative, home-relative or empty directory used to die while writing the session row, which reached the
|
||||
// client as an empty 500 instead of a request error it can explain.
|
||||
for (const directory of ["", "relative/path", "~/code"]) {
|
||||
const response = yield* create(directory)
|
||||
expect(response.status).toBe(400)
|
||||
expect(yield* Effect.promise(() => response.json())).toMatchObject({
|
||||
_tag: "InvalidRequestError",
|
||||
field: "location",
|
||||
})
|
||||
}
|
||||
|
||||
const response = yield* create(dir.path)
|
||||
expect(response.status).toBe(200)
|
||||
expect(yield* Effect.promise(() => response.json())).toMatchObject({ data: { location: { directory: dir.path } } })
|
||||
}),
|
||||
)
|
||||
Reference in New Issue
Block a user