Compare commits

...
36 changed files with 497 additions and 222 deletions

No files matched your search

+105 -93
View File
@@ -447,7 +447,7 @@ export function make(options: ClientOptions) {
path: `/api/location`,
query: { location: input?.["location"] },
successStatus: 200,
declaredStatuses: [400, 401],
declaredStatuses: [400, 401, 403, 404],
empty: false,
},
requestOptions,
@@ -472,7 +472,7 @@ export function make(options: ClientOptions) {
path: `/api/agent`,
query: { location: input?.["location"] },
successStatus: 200,
declaredStatuses: [400, 401],
declaredStatuses: [400, 401, 403, 404],
empty: false,
},
requestOptions,
@@ -484,7 +484,7 @@ export function make(options: ClientOptions) {
path: `/api/agent/${encodeURIComponent(input.agentID)}`,
query: { location: input["location"] },
successStatus: 200,
declaredStatuses: [400, 401, 404],
declaredStatuses: [400, 401, 403, 404],
empty: false,
},
requestOptions,
@@ -498,7 +498,7 @@ export function make(options: ClientOptions) {
path: `/api/plugin`,
query: { location: input?.["location"] },
successStatus: 200,
declaredStatuses: [400, 401],
declaredStatuses: [400, 401, 403, 404],
empty: false,
},
requestOptions,
@@ -511,7 +511,7 @@ export function make(options: ClientOptions) {
query: { location: input?.["location"] },
body: { target: input?.["target"] },
successStatus: 200,
declaredStatuses: [400, 401],
declaredStatuses: [400, 401, 403, 404],
empty: false,
},
requestOptions,
@@ -524,7 +524,7 @@ export function make(options: ClientOptions) {
query: { location: input["location"] },
body: { targets: input["targets"] },
successStatus: 204,
declaredStatuses: [400, 401, 503],
declaredStatuses: [400, 401, 403, 404, 503],
empty: true,
},
requestOptions,
@@ -655,7 +655,7 @@ export function make(options: ClientOptions) {
path: `/api/session/${encodeURIComponent(input.sessionID)}/fork`,
body: { before: input["before"] },
successStatus: 200,
declaredStatuses: [400, 401, 404],
declaredStatuses: [400, 401, 403, 404],
empty: false,
},
requestOptions,
@@ -667,7 +667,7 @@ export function make(options: ClientOptions) {
path: `/api/session/${encodeURIComponent(input.sessionID)}/agent`,
body: { agent: input["agent"] },
successStatus: 204,
declaredStatuses: [400, 401, 404],
declaredStatuses: [400, 401, 403, 404],
empty: true,
},
requestOptions,
@@ -679,7 +679,7 @@ export function make(options: ClientOptions) {
path: `/api/session/${encodeURIComponent(input.sessionID)}/model`,
body: { model: input["model"] },
successStatus: 204,
declaredStatuses: [400, 401, 404],
declaredStatuses: [400, 401, 403, 404],
empty: true,
},
requestOptions,
@@ -691,7 +691,7 @@ export function make(options: ClientOptions) {
path: `/api/session/${encodeURIComponent(input.sessionID)}`,
body: { title: input["title"], metadata: input["metadata"], permissions: input["permissions"] },
successStatus: 204,
declaredStatuses: [400, 401, 404],
declaredStatuses: [400, 401, 403, 404],
empty: true,
},
requestOptions,
@@ -703,7 +703,7 @@ export function make(options: ClientOptions) {
path: `/api/session/${encodeURIComponent(input.sessionID)}/move`,
body: { directory: input["directory"], delivery: input["delivery"] },
successStatus: 204,
declaredStatuses: [400, 401, 404],
declaredStatuses: [400, 401, 403, 404],
empty: true,
},
requestOptions,
@@ -724,7 +724,7 @@ export function make(options: ClientOptions) {
resume: input["resume"],
},
successStatus: 200,
declaredStatuses: [400, 401, 404, 409],
declaredStatuses: [400, 401, 403, 404, 409],
empty: false,
},
requestOptions,
@@ -743,7 +743,7 @@ export function make(options: ClientOptions) {
delivery: input["delivery"],
},
successStatus: 204,
declaredStatuses: [400, 401, 404, 500],
declaredStatuses: [400, 401, 403, 404, 500],
empty: true,
},
requestOptions,
@@ -755,7 +755,7 @@ export function make(options: ClientOptions) {
path: `/api/experimental/session/${encodeURIComponent(input.sessionID)}/skill`,
body: { id: input["id"], resume: input["resume"] },
successStatus: 204,
declaredStatuses: [400, 401, 404],
declaredStatuses: [400, 401, 403, 404],
empty: true,
},
requestOptions,
@@ -774,7 +774,7 @@ export function make(options: ClientOptions) {
resume: input["resume"],
},
successStatus: 200,
declaredStatuses: [400, 401, 404, 409],
declaredStatuses: [400, 401, 403, 404, 409],
empty: false,
},
requestOptions,
@@ -786,7 +786,7 @@ export function make(options: ClientOptions) {
path: `/api/session/${encodeURIComponent(input.sessionID)}/shell`,
body: { id: input["id"], command: input["command"] },
successStatus: 204,
declaredStatuses: [400, 401, 404],
declaredStatuses: [400, 401, 403, 404],
empty: true,
},
requestOptions,
@@ -798,7 +798,7 @@ export function make(options: ClientOptions) {
path: `/api/session/${encodeURIComponent(input.sessionID)}/compact`,
body: { id: input["id"], delivery: input["delivery"] },
successStatus: 200,
declaredStatuses: [400, 401, 404, 409],
declaredStatuses: [400, 401, 403, 404, 409],
empty: false,
},
requestOptions,
@@ -809,7 +809,7 @@ export function make(options: ClientOptions) {
method: "POST",
path: `/api/experimental/session/${encodeURIComponent(input.sessionID)}/wait`,
successStatus: 204,
declaredStatuses: [400, 401, 404, 503],
declaredStatuses: [400, 401, 403, 404, 503],
empty: true,
},
requestOptions,
@@ -822,7 +822,7 @@ export function make(options: ClientOptions) {
path: `/api/session/${encodeURIComponent(input.sessionID)}/revert/stage`,
body: { messageID: input["messageID"], files: input["files"] },
successStatus: 200,
declaredStatuses: [400, 401, 404, 409, 500],
declaredStatuses: [400, 401, 403, 404, 409, 500],
empty: false,
},
requestOptions,
@@ -833,7 +833,7 @@ export function make(options: ClientOptions) {
method: "DELETE",
path: `/api/session/${encodeURIComponent(input.sessionID)}/revert`,
successStatus: 204,
declaredStatuses: [400, 401, 404, 409, 500],
declaredStatuses: [400, 401, 403, 404, 409, 500],
empty: true,
},
requestOptions,
@@ -844,7 +844,7 @@ export function make(options: ClientOptions) {
method: "POST",
path: `/api/session/${encodeURIComponent(input.sessionID)}/revert/commit`,
successStatus: 204,
declaredStatuses: [400, 401, 404, 409],
declaredStatuses: [400, 401, 403, 404, 409],
empty: true,
},
requestOptions,
@@ -868,7 +868,7 @@ export function make(options: ClientOptions) {
path: `/api/session/${encodeURIComponent(input.sessionID)}/diff`,
query: { from: input["from"], to: input["to"], context: input["context"] },
successStatus: 200,
declaredStatuses: [400, 401, 404, 500],
declaredStatuses: [400, 401, 403, 404, 500],
empty: false,
},
requestOptions,
@@ -917,7 +917,7 @@ export function make(options: ClientOptions) {
method: "GET",
path: `/api/experimental/session/${encodeURIComponent(input.sessionID)}/instructions/entries`,
successStatus: 200,
declaredStatuses: [400, 401, 404],
declaredStatuses: [400, 401, 403, 404],
empty: false,
},
requestOptions,
@@ -929,7 +929,7 @@ export function make(options: ClientOptions) {
path: `/api/experimental/session/${encodeURIComponent(input.sessionID)}/instructions/entries/${encodeURIComponent(input.key)}`,
body: { value: input["value"] },
successStatus: 204,
declaredStatuses: [400, 401, 404, 413],
declaredStatuses: [400, 401, 403, 404, 413],
empty: true,
},
requestOptions,
@@ -940,7 +940,7 @@ export function make(options: ClientOptions) {
method: "DELETE",
path: `/api/experimental/session/${encodeURIComponent(input.sessionID)}/instructions/entries/${encodeURIComponent(input.key)}`,
successStatus: 204,
declaredStatuses: [400, 401, 404],
declaredStatuses: [400, 401, 403, 404],
empty: true,
},
requestOptions,
@@ -954,7 +954,7 @@ export function make(options: ClientOptions) {
path: `/api/session/${encodeURIComponent(input.sessionID)}/generate`,
body: { prompt: input["prompt"] },
successStatus: 200,
declaredStatuses: [400, 401, 404, 503],
declaredStatuses: [400, 401, 403, 404, 503],
empty: false,
},
requestOptions,
@@ -978,7 +978,7 @@ export function make(options: ClientOptions) {
path: `/api/session/${encodeURIComponent(input.sessionID)}/interrupt`,
query: { resume: input["resume"] },
successStatus: 200,
declaredStatuses: [400, 401, 404],
declaredStatuses: [400, 401, 403, 404],
empty: false,
},
requestOptions,
@@ -989,7 +989,7 @@ export function make(options: ClientOptions) {
method: "POST",
path: `/api/session/${encodeURIComponent(input.sessionID)}/background`,
successStatus: 204,
declaredStatuses: [400, 401, 404],
declaredStatuses: [400, 401, 403, 404],
empty: true,
},
requestOptions,
@@ -1014,7 +1014,7 @@ export function make(options: ClientOptions) {
method: "GET",
path: `/api/session/${encodeURIComponent(input.sessionID)}/form`,
successStatus: 200,
declaredStatuses: [400, 401, 404],
declaredStatuses: [400, 401, 403, 404],
empty: false,
},
requestOptions,
@@ -1026,7 +1026,7 @@ export function make(options: ClientOptions) {
path: `/api/session/${encodeURIComponent(input.sessionID)}/form`,
body: { id: input["id"], title: input["title"], metadata: input["metadata"], fields: input["fields"] },
successStatus: 200,
declaredStatuses: [400, 401, 404, 409],
declaredStatuses: [400, 401, 403, 404, 409],
empty: false,
},
requestOptions,
@@ -1037,7 +1037,7 @@ export function make(options: ClientOptions) {
method: "GET",
path: `/api/session/${encodeURIComponent(input.sessionID)}/form/${encodeURIComponent(input.formID)}`,
successStatus: 200,
declaredStatuses: [400, 401, 404],
declaredStatuses: [400, 401, 403, 404],
empty: false,
},
requestOptions,
@@ -1049,7 +1049,7 @@ export function make(options: ClientOptions) {
path: `/api/session/${encodeURIComponent(input.sessionID)}/form/${encodeURIComponent(input.formID)}/reply`,
body: { answer: input["answer"] },
successStatus: 204,
declaredStatuses: [400, 401, 404, 409],
declaredStatuses: [400, 401, 403, 404, 409],
empty: true,
},
requestOptions,
@@ -1061,7 +1061,7 @@ export function make(options: ClientOptions) {
path: `/api/session/${encodeURIComponent(input.sessionID)}/form/${encodeURIComponent(input.formID)}`,
query: { message: input["message"] },
successStatus: 204,
declaredStatuses: [400, 401, 404, 409],
declaredStatuses: [400, 401, 403, 404, 409],
empty: true,
},
requestOptions,
@@ -1114,7 +1114,7 @@ export function make(options: ClientOptions) {
path: `/api/model`,
query: { location: input?.["location"] },
successStatus: 200,
declaredStatuses: [400, 401, 503],
declaredStatuses: [400, 401, 403, 404, 503],
empty: false,
},
requestOptions,
@@ -1126,7 +1126,7 @@ export function make(options: ClientOptions) {
path: `/api/model/default`,
query: { location: input?.["location"] },
successStatus: 200,
declaredStatuses: [400, 401, 503],
declaredStatuses: [400, 401, 403, 404, 503],
empty: false,
},
requestOptions,
@@ -1140,7 +1140,7 @@ export function make(options: ClientOptions) {
path: `/api/experimental/generate`,
body: { prompt: input["prompt"], model: input["model"] },
successStatus: 200,
declaredStatuses: [400, 401, 503],
declaredStatuses: [400, 401, 403, 404, 503],
empty: false,
},
requestOptions,
@@ -1154,7 +1154,7 @@ export function make(options: ClientOptions) {
path: `/api/provider`,
query: { location: input?.["location"] },
successStatus: 200,
declaredStatuses: [400, 401, 503],
declaredStatuses: [400, 401, 403, 404, 503],
empty: false,
},
requestOptions,
@@ -1166,7 +1166,7 @@ export function make(options: ClientOptions) {
path: `/api/provider/${encodeURIComponent(input.providerID)}`,
query: { location: input["location"] },
successStatus: 200,
declaredStatuses: [400, 401, 404, 503],
declaredStatuses: [400, 401, 403, 404, 503],
empty: false,
},
requestOptions,
@@ -1180,7 +1180,7 @@ export function make(options: ClientOptions) {
path: `/api/integration`,
query: { location: input?.["location"] },
successStatus: 200,
declaredStatuses: [400, 401],
declaredStatuses: [400, 401, 403, 404],
empty: false,
},
requestOptions,
@@ -1192,7 +1192,7 @@ export function make(options: ClientOptions) {
path: `/api/integration/${encodeURIComponent(input.integrationID)}`,
query: { location: input["location"] },
successStatus: 200,
declaredStatuses: [400, 401, 404],
declaredStatuses: [400, 401, 403, 404],
empty: false,
},
requestOptions,
@@ -1206,7 +1206,7 @@ export function make(options: ClientOptions) {
query: { location: input["location"] },
body: { url: input["url"] },
successStatus: 204,
declaredStatuses: [400, 401],
declaredStatuses: [400, 401, 403, 404],
empty: true,
},
requestOptions,
@@ -1221,7 +1221,7 @@ export function make(options: ClientOptions) {
query: { location: input["location"] },
body: { key: input["key"], answer: input["answer"], label: input["label"] },
successStatus: 204,
declaredStatuses: [400, 401, 404],
declaredStatuses: [400, 401, 403, 404],
empty: true,
},
requestOptions,
@@ -1236,7 +1236,7 @@ export function make(options: ClientOptions) {
query: { location: input["location"] },
body: { methodID: input["methodID"], answer: input["answer"], label: input["label"] },
successStatus: 200,
declaredStatuses: [400, 401],
declaredStatuses: [400, 401, 403, 404],
empty: false,
},
requestOptions,
@@ -1248,7 +1248,7 @@ export function make(options: ClientOptions) {
path: `/api/integration/${encodeURIComponent(input.integrationID)}/connect/oauth/${encodeURIComponent(input.attemptID)}`,
query: { location: input["location"] },
successStatus: 200,
declaredStatuses: [400, 401, 404],
declaredStatuses: [400, 401, 403, 404],
empty: false,
},
requestOptions,
@@ -1261,7 +1261,7 @@ export function make(options: ClientOptions) {
query: { location: input["location"] },
body: { code: input["code"] },
successStatus: 204,
declaredStatuses: [400, 401, 404],
declaredStatuses: [400, 401, 403, 404],
empty: true,
},
requestOptions,
@@ -1273,7 +1273,7 @@ export function make(options: ClientOptions) {
path: `/api/integration/${encodeURIComponent(input.integrationID)}/connect/oauth/${encodeURIComponent(input.attemptID)}`,
query: { location: input["location"] },
successStatus: 204,
declaredStatuses: [400, 401],
declaredStatuses: [400, 401, 403, 404],
empty: true,
},
requestOptions,
@@ -1288,7 +1288,7 @@ export function make(options: ClientOptions) {
query: { location: input["location"] },
body: { methodID: input["methodID"], label: input["label"] },
successStatus: 200,
declaredStatuses: [400, 401, 404],
declaredStatuses: [400, 401, 403, 404],
empty: false,
},
requestOptions,
@@ -1300,7 +1300,7 @@ export function make(options: ClientOptions) {
path: `/api/integration/${encodeURIComponent(input.integrationID)}/connect/command/${encodeURIComponent(input.attemptID)}`,
query: { location: input["location"] },
successStatus: 200,
declaredStatuses: [400, 401, 404],
declaredStatuses: [400, 401, 403, 404],
empty: false,
},
requestOptions,
@@ -1312,7 +1312,7 @@ export function make(options: ClientOptions) {
path: `/api/integration/${encodeURIComponent(input.integrationID)}/connect/command/${encodeURIComponent(input.attemptID)}`,
query: { location: input["location"] },
successStatus: 204,
declaredStatuses: [400, 401],
declaredStatuses: [400, 401, 403, 404],
empty: true,
},
requestOptions,
@@ -1327,7 +1327,7 @@ export function make(options: ClientOptions) {
path: `/api/mcp`,
query: { location: input?.["location"] },
successStatus: 200,
declaredStatuses: [400, 401],
declaredStatuses: [400, 401, 403, 404],
empty: false,
},
requestOptions,
@@ -1340,7 +1340,7 @@ export function make(options: ClientOptions) {
query: { location: input["location"] },
body: { config: input["config"] },
successStatus: 204,
declaredStatuses: [400, 401],
declaredStatuses: [400, 401, 403, 404],
empty: true,
},
requestOptions,
@@ -1352,7 +1352,7 @@ export function make(options: ClientOptions) {
path: `/api/experimental/mcp/${encodeURIComponent(input.server)}`,
query: { location: input["location"] },
successStatus: 204,
declaredStatuses: [400, 401, 404],
declaredStatuses: [400, 401, 403, 404],
empty: true,
},
requestOptions,
@@ -1364,7 +1364,7 @@ export function make(options: ClientOptions) {
path: `/api/experimental/mcp/${encodeURIComponent(input.server)}/connect`,
query: { location: input["location"] },
successStatus: 204,
declaredStatuses: [400, 401, 404],
declaredStatuses: [400, 401, 403, 404],
empty: true,
},
requestOptions,
@@ -1376,7 +1376,7 @@ export function make(options: ClientOptions) {
path: `/api/experimental/mcp/${encodeURIComponent(input.server)}/disconnect`,
query: { location: input["location"] },
successStatus: 204,
declaredStatuses: [400, 401, 404],
declaredStatuses: [400, 401, 403, 404],
empty: true,
},
requestOptions,
@@ -1389,7 +1389,7 @@ export function make(options: ClientOptions) {
path: `/api/mcp/resource`,
query: { location: input?.["location"] },
successStatus: 200,
declaredStatuses: [400, 401],
declaredStatuses: [400, 401, 403, 404],
empty: false,
},
requestOptions,
@@ -1458,7 +1458,13 @@ export function make(options: ClientOptions) {
project: {
list: (requestOptions?: RequestOptions) =>
request<ProjectListOutput>(
{ method: "GET", path: `/api/project`, successStatus: 200, declaredStatuses: [400, 401], empty: false },
{
method: "GET",
path: `/api/project`,
successStatus: 200,
declaredStatuses: [400, 401, 403, 404],
empty: false,
},
requestOptions,
),
update: (input: ProjectUpdateInput, requestOptions?: RequestOptions) =>
@@ -1473,7 +1479,7 @@ export function make(options: ClientOptions) {
commands: input["commands"],
},
successStatus: 200,
declaredStatuses: [400, 401, 404],
declaredStatuses: [400, 401, 403, 404],
empty: false,
},
requestOptions,
@@ -1487,7 +1493,7 @@ export function make(options: ClientOptions) {
path: `/api/form`,
query: { location: input?.["location"] },
successStatus: 200,
declaredStatuses: [400, 401],
declaredStatuses: [400, 401, 403, 404],
empty: false,
},
requestOptions,
@@ -1502,7 +1508,7 @@ export function make(options: ClientOptions) {
path: `/api/permission/request`,
query: { location: input?.["location"] },
successStatus: 200,
declaredStatuses: [400, 401],
declaredStatuses: [400, 401, 403, 404],
empty: false,
},
requestOptions,
@@ -1516,7 +1522,7 @@ export function make(options: ClientOptions) {
path: `/api/permission/saved`,
query: { projectID: input?.["projectID"] },
successStatus: 200,
declaredStatuses: [400, 401],
declaredStatuses: [400, 401, 403, 404],
empty: false,
},
requestOptions,
@@ -1527,7 +1533,7 @@ export function make(options: ClientOptions) {
method: "DELETE",
path: `/api/permission/saved/${encodeURIComponent(input.id)}`,
successStatus: 204,
declaredStatuses: [400, 401],
declaredStatuses: [400, 401, 403, 404],
empty: true,
},
requestOptions,
@@ -1548,7 +1554,7 @@ export function make(options: ClientOptions) {
agent: input["agent"],
},
successStatus: 200,
declaredStatuses: [400, 401, 404],
declaredStatuses: [400, 401, 403, 404],
empty: false,
},
requestOptions,
@@ -1559,7 +1565,7 @@ export function make(options: ClientOptions) {
method: "GET",
path: `/api/session/${encodeURIComponent(input.sessionID)}/permission`,
successStatus: 200,
declaredStatuses: [400, 401, 404],
declaredStatuses: [400, 401, 403, 404],
empty: false,
},
requestOptions,
@@ -1570,7 +1576,7 @@ export function make(options: ClientOptions) {
method: "GET",
path: `/api/session/${encodeURIComponent(input.sessionID)}/permission/${encodeURIComponent(input.requestID)}`,
successStatus: 200,
declaredStatuses: [400, 401, 404],
declaredStatuses: [400, 401, 403, 404],
empty: false,
},
requestOptions,
@@ -1582,7 +1588,7 @@ export function make(options: ClientOptions) {
path: `/api/session/${encodeURIComponent(input.sessionID)}/permission/${encodeURIComponent(input.requestID)}/reply`,
body: { decision: input["decision"], message: input["message"] },
successStatus: 204,
declaredStatuses: [400, 401, 404],
declaredStatuses: [400, 401, 403, 404],
empty: true,
},
requestOptions,
@@ -1596,7 +1602,7 @@ export function make(options: ClientOptions) {
path: `/api/fs/read/${encodePath(input.path)}`,
query: { location: input["location"] },
successStatus: 200,
declaredStatuses: [400, 401, 404],
declaredStatuses: [400, 401, 403, 404],
empty: false,
binary: true,
},
@@ -1609,7 +1615,7 @@ export function make(options: ClientOptions) {
path: `/api/fs/list`,
query: { location: input?.["location"], path: input?.["path"] },
successStatus: 200,
declaredStatuses: [400, 401],
declaredStatuses: [400, 401, 403, 404],
empty: false,
},
requestOptions,
@@ -1621,7 +1627,7 @@ export function make(options: ClientOptions) {
path: `/api/fs/find`,
query: { location: input["location"], query: input["query"], type: input["type"], limit: input["limit"] },
successStatus: 200,
declaredStatuses: [400, 401],
declaredStatuses: [400, 401, 403, 404],
empty: false,
},
requestOptions,
@@ -1634,7 +1640,7 @@ export function make(options: ClientOptions) {
query: { location: input["location"], path: input["path"] },
body: input["payload"],
successStatus: 200,
declaredStatuses: [400, 401],
declaredStatuses: [400, 401, 403, 404],
empty: false,
binaryBody: true,
},
@@ -1649,7 +1655,7 @@ export function make(options: ClientOptions) {
path: `/api/command`,
query: { location: input?.["location"] },
successStatus: 200,
declaredStatuses: [400, 401],
declaredStatuses: [400, 401, 403, 404],
empty: false,
},
requestOptions,
@@ -1663,7 +1669,7 @@ export function make(options: ClientOptions) {
path: `/api/skill`,
query: { location: input?.["location"] },
successStatus: 200,
declaredStatuses: [400, 401],
declaredStatuses: [400, 401, 403, 404],
empty: false,
},
requestOptions,
@@ -1678,7 +1684,7 @@ export function make(options: ClientOptions) {
query: { location: input["location"] },
body: { input: input["input"] },
successStatus: 200,
declaredStatuses: [400, 401, 500],
declaredStatuses: [400, 401, 403, 404, 500],
empty: false,
},
requestOptions,
@@ -1699,7 +1705,7 @@ export function make(options: ClientOptions) {
path: `/api/pty`,
query: { location: input?.["location"] },
successStatus: 200,
declaredStatuses: [400, 401],
declaredStatuses: [400, 401, 403, 404],
empty: false,
},
requestOptions,
@@ -1718,7 +1724,7 @@ export function make(options: ClientOptions) {
env: input?.["env"],
},
successStatus: 200,
declaredStatuses: [400, 401],
declaredStatuses: [400, 401, 403, 404],
empty: false,
},
requestOptions,
@@ -1730,7 +1736,7 @@ export function make(options: ClientOptions) {
path: `/api/pty/${encodeURIComponent(input.ptyID)}`,
query: { location: input["location"] },
successStatus: 200,
declaredStatuses: [400, 401, 404],
declaredStatuses: [400, 401, 403, 404],
empty: false,
},
requestOptions,
@@ -1743,7 +1749,7 @@ export function make(options: ClientOptions) {
query: { location: input["location"] },
body: { title: input["title"], size: input["size"] },
successStatus: 200,
declaredStatuses: [400, 401, 404],
declaredStatuses: [400, 401, 403, 404],
empty: false,
},
requestOptions,
@@ -1755,7 +1761,7 @@ export function make(options: ClientOptions) {
path: `/api/pty/${encodeURIComponent(input.ptyID)}`,
query: { location: input["location"] },
successStatus: 204,
declaredStatuses: [400, 401, 404],
declaredStatuses: [400, 401, 403, 404],
empty: true,
},
requestOptions,
@@ -1909,7 +1915,7 @@ export function make(options: ClientOptions) {
path: `/api/shell`,
query: { location: input?.["location"] },
successStatus: 200,
declaredStatuses: [400, 401],
declaredStatuses: [400, 401, 403, 404],
empty: false,
},
requestOptions,
@@ -1927,7 +1933,7 @@ export function make(options: ClientOptions) {
metadata: input["metadata"],
},
successStatus: 200,
declaredStatuses: [400, 401],
declaredStatuses: [400, 401, 403, 404],
empty: false,
},
requestOptions,
@@ -1939,7 +1945,7 @@ export function make(options: ClientOptions) {
path: `/api/shell/${encodeURIComponent(input.id)}`,
query: { location: input["location"] },
successStatus: 200,
declaredStatuses: [400, 401, 404],
declaredStatuses: [400, 401, 403, 404],
empty: false,
},
requestOptions,
@@ -1951,7 +1957,7 @@ export function make(options: ClientOptions) {
path: `/api/shell/${encodeURIComponent(input.id)}/output`,
query: { location: input["location"], cursor: input["cursor"], limit: input["limit"] },
successStatus: 200,
declaredStatuses: [400, 401, 404],
declaredStatuses: [400, 401, 403, 404],
empty: false,
},
requestOptions,
@@ -1963,7 +1969,7 @@ export function make(options: ClientOptions) {
path: `/api/shell/${encodeURIComponent(input.id)}`,
query: { location: input["location"] },
successStatus: 204,
declaredStatuses: [400, 401],
declaredStatuses: [400, 401, 403, 404],
empty: true,
},
requestOptions,
@@ -1977,7 +1983,7 @@ export function make(options: ClientOptions) {
path: `/api/reference`,
query: { location: input?.["location"] },
successStatus: 200,
declaredStatuses: [400, 401],
declaredStatuses: [400, 401, 403, 404],
empty: false,
},
requestOptions,
@@ -2047,7 +2053,7 @@ export function make(options: ClientOptions) {
path: `/api/vcs`,
query: { location: input?.["location"] },
successStatus: 200,
declaredStatuses: [400, 401],
declaredStatuses: [400, 401, 403, 404],
empty: false,
},
requestOptions,
@@ -2059,7 +2065,7 @@ export function make(options: ClientOptions) {
path: `/api/vcs/base`,
query: { location: input?.["location"] },
successStatus: 200,
declaredStatuses: [400, 401, 503],
declaredStatuses: [400, 401, 403, 404, 503],
empty: false,
},
requestOptions,
@@ -2071,7 +2077,7 @@ export function make(options: ClientOptions) {
path: `/api/vcs/status`,
query: { location: input?.["location"] },
successStatus: 200,
declaredStatuses: [400, 401],
declaredStatuses: [400, 401, 403, 404],
empty: false,
},
requestOptions,
@@ -2084,7 +2090,7 @@ export function make(options: ClientOptions) {
path: `/api/vcs/branch`,
query: { location: input?.["location"], search: input?.["search"], limit: input?.["limit"] },
successStatus: 200,
declaredStatuses: [400, 401],
declaredStatuses: [400, 401, 403, 404],
empty: false,
},
requestOptions,
@@ -2097,7 +2103,7 @@ export function make(options: ClientOptions) {
path: `/api/vcs/diff`,
query: { location: input["location"], mode: input["mode"], base: input["base"], context: input["context"] },
successStatus: 200,
declaredStatuses: [400, 401, 503],
declaredStatuses: [400, 401, 403, 404, 503],
empty: false,
},
requestOptions,
@@ -2153,7 +2159,7 @@ export function make(options: ClientOptions) {
path: `/api/websearch/provider`,
query: { location: input?.["location"] },
successStatus: 200,
declaredStatuses: [400, 401, 503],
declaredStatuses: [400, 401, 403, 404, 503],
empty: false,
},
requestOptions,
@@ -2166,7 +2172,7 @@ export function make(options: ClientOptions) {
query: { location: input["location"] },
body: { query: input["query"], providerID: input["providerID"] },
successStatus: 200,
declaredStatuses: [400, 401, 503],
declaredStatuses: [400, 401, 403, 404, 503],
empty: false,
},
requestOptions,
@@ -2180,14 +2186,20 @@ export function make(options: ClientOptions) {
path: `/api/config`,
query: { location: input?.["location"] },
successStatus: 200,
declaredStatuses: [400, 401],
declaredStatuses: [400, 401, 403, 404],
empty: false,
},
requestOptions,
),
shells: (requestOptions?: RequestOptions) =>
request<ConfigShellsOutput>(
{ method: "GET", path: `/api/config/shell`, successStatus: 200, declaredStatuses: [400, 401], empty: false },
{
method: "GET",
path: `/api/config/shell`,
successStatus: 200,
declaredStatuses: [400, 401, 403, 404],
empty: false,
},
requestOptions,
),
update: (input: ConfigUpdateInput, requestOptions?: RequestOptions) =>
@@ -2197,7 +2209,7 @@ export function make(options: ClientOptions) {
path: `/api/experimental/config`,
body: { shell: input["shell"] },
successStatus: 204,
declaredStatuses: [400, 401],
declaredStatuses: [400, 401, 403, 404],
empty: true,
},
requestOptions,
@@ -2525,6 +2525,22 @@ export type UnauthorizedError = { readonly _tag: "UnauthorizedError"; readonly m
export const isUnauthorizedError = (value: unknown): value is UnauthorizedError =>
typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "UnauthorizedError"
export type DirectoryAccessDeniedError = {
readonly _tag: "DirectoryAccessDeniedError"
readonly directory: string
readonly message: string
}
export const isDirectoryAccessDeniedError = (value: unknown): value is DirectoryAccessDeniedError =>
typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "DirectoryAccessDeniedError"
export type DirectoryNotFoundError = {
readonly _tag: "DirectoryNotFoundError"
readonly directory: string
readonly message: string
}
export const isDirectoryNotFoundError = (value: unknown): value is DirectoryNotFoundError =>
typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "DirectoryNotFoundError"
export type ServiceUnavailableError = {
readonly _tag: "ServiceUnavailableError"
readonly message: string
+1 -42
View File
@@ -26,30 +26,6 @@ export class NotFoundError extends Schema.TaggedError<NotFoundError>()("FileSyst
path: RelativePath,
}) {}
export class DirectoryNotFoundError extends Schema.TaggedError<DirectoryNotFoundError>()(
"FileSystem.DirectoryNotFoundError",
{
directory: AbsolutePath,
cause: Schema.Defect(),
},
) {
override get message() {
return `Directory not found: ${this.directory}`
}
}
export class DirectoryAccessDeniedError extends Schema.TaggedError<DirectoryAccessDeniedError>()(
"FileSystem.DirectoryAccessDeniedError",
{
directory: AbsolutePath,
cause: Schema.Defect(),
},
) {
override get message() {
return `Access denied to directory: ${this.directory}`
}
}
export const Content = Schema.Struct({
uri: Schema.String,
name: Schema.String.pipe(Schema.optional),
@@ -111,24 +87,7 @@ const baseLayer = Layer.effect(
// configured directory as canonical; local placements keep symlink
// canonicalization. This skip is boot-only: resolve/read/list below still
// access the host filesystem per operation (tracked in #44568).
const root = location.workspaceID
? location.directory
: yield* fs.realPath(location.directory).pipe(
Effect.catch((cause): Effect.Effect<never, DirectoryNotFoundError | DirectoryAccessDeniedError> => {
if (cause.reason._tag === "NotFound")
return Effect.fail(new DirectoryNotFoundError({ directory: location.directory, cause }))
// macOS privacy denials arrive as Unknown with an EPERM cause.
if (
cause.reason._tag === "PermissionDenied" ||
(cause.reason._tag === "Unknown" &&
cause.reason.cause instanceof Error &&
"code" in cause.reason.cause &&
cause.reason.cause.code === "EPERM")
)
return Effect.fail(new DirectoryAccessDeniedError({ directory: location.directory, cause }))
return Effect.die(cause)
}),
)
const root = location.workspaceID ? location.directory : yield* fs.realDirectory(location.directory)
const resolve = Effect.fnUntraced(function* (input?: RelativePath) {
const absolute = path.resolve(location.directory, input ?? ".")
if (!FSUtil.contains(location.directory, absolute))
+28 -5
View File
@@ -11,6 +11,7 @@ import { FileMutation } from "./file-mutation.js"
import { Environment } from "./environment/index.js"
import { Formatter } from "./formatter.js"
import { FileSystem } from "./filesystem.js"
import { FSUtil } from "@opencode/util/fs-util"
import { FileSystemSearch } from "./filesystem/search.js"
import { Generate } from "./generate.js"
import { Form } from "./form.js"
@@ -114,7 +115,7 @@ const nodes = [
export const graph = LayerNode.group(nodes)
export type Services = LayerNode.Output<typeof graph>
export type Error = Layer.Error<ReturnType<typeof layer>>
export type Error = FSUtil.DirectoryError
export interface Options {
// Plugins this instance is born with; empty and absent are equivalent.
@@ -145,7 +146,7 @@ const vanillaReplacements: LayerNode.Replacements = [
]
// One instance is one compiled, fresh copy of the graph standing on a directory.
export function layer(ref: Location.Ref, options: Options = {}): Layer.Layer<Services> {
export function layer(ref: Location.Ref, options: Options = {}): Layer.Layer<Services, Error> {
const startedAt = performance.now()
// Ordered: vanilla defaults, then caller replacements (which win over the
// defaults), then instance bindings (which win over everything).
@@ -156,9 +157,31 @@ export function layer(ref: Location.Ref, options: Options = {}): Layer.Layer<Ser
InstancePlugins.node.replace(InstancePlugins.bound(options.plugins ?? [])),
]
return LayerNode.compile(graph, { replacements, shared: Node.tags.values.global }).pipe(
// Instance boot failures are defects; provided operations retain their typed errors.
Layer.orDie,
return Layer.unwrap(
Effect.gen(function* () {
// Validate before project/config discovery can turn directory access failures into defects.
if (!ref.workspaceID)
yield* FSUtil.Service.pipe(
Effect.flatMap((fs) => fs.realDirectory(ref.directory)),
Effect.provide(LayerNode.compile(FSUtil.node, { replacements: options.replacements })),
)
return LayerNode.compile(graph, { replacements, shared: Node.tags.values.global })
}),
).pipe(
// Unavailable directories are expected; other instance boot failures remain defects.
Layer.catchCause(
(cause): Layer.Layer<Services, Error> =>
Layer.unwrap(
Effect.failCause(cause).pipe(
Effect.catch(
(error): Effect.Effect<never, Error> =>
error instanceof FSUtil.DirectoryNotFoundError || error instanceof FSUtil.DirectoryAccessDeniedError
? Effect.fail(error)
: Effect.die(error),
),
),
),
),
Layer.tap((context) => Effect.addFinalizer(() => Context.get(context, LocationLifecycle.Service).shutdown)),
Layer.tap(() =>
Effect.logInfo("location services booted", {
+2 -2
View File
@@ -5,13 +5,13 @@ import { Context, type Effect } from "effect"
import type { Session } from "@opencode/schema/session"
import { Node } from "@opencode/util/effect/app-node"
import { LayerNode } from "@opencode/util/effect/layer-node"
import type { Services } from "../instance.js"
import type { Services, Error } from "../instance.js"
/** Selects Session capabilities; implementations own caching and lifetime. */
export interface Interface {
readonly provide: (
session: Session.Info,
) => <A, E, R>(effect: Effect.Effect<A, E, R>) => Effect.Effect<A, E, Exclude<R, Services>>
) => <A, E, R>(effect: Effect.Effect<A, E, R>) => Effect.Effect<A, E | Error, Exclude<R, Services>>
}
export class Service extends Context.Service<Service, Interface>()("@opencode/Instance") {}
+1 -1
View File
@@ -17,7 +17,7 @@ export function buildLocationServiceMap(
Effect.gen(function* () {
const owner = yield* Effect.scope
const builds = MutableHashMap.empty<Location.Ref, { close?: Effect.Effect<void> }>()
const inner: LayerMap.LayerMap<Location.Ref, LocationServices> = yield* LayerMap.make(
const inner: LayerMap.LayerMap<Location.Ref, LocationServices, Instance.Error> = yield* LayerMap.make(
(ref: Location.Ref) => {
const build: { close?: Effect.Effect<void> } = {}
MutableHashMap.set(builds, ref, build)
+12 -4
View File
@@ -145,7 +145,10 @@ export interface Interface {
readonly from?: SessionMessage.ID
readonly to?: SessionMessage.ID
readonly context?: number
}) => Effect.Effect<readonly FileDiff.Info[], NotFoundError | MessageNotFoundError | TurnRangeError | Snapshot.Error>
}) => Effect.Effect<
readonly FileDiff.Info[],
NotFoundError | MessageNotFoundError | TurnRangeError | Snapshot.Error | FSUtil.DirectoryError
>
/**
* Durable admitted session work not yet visible in projected history,
* ordered by admission. Includes unpromoted user and synthetic inputs and
@@ -195,7 +198,7 @@ export interface Interface {
agents?: PromptInput.Prompt["agents"]
skills?: PromptInput.Prompt["skills"]
delivery?: SessionInbox.Delivery
}) => Effect.Effect<void, NotFoundError | Command.NotFoundError | Command.ExecutionError>
}) => Effect.Effect<void, NotFoundError | Command.NotFoundError | Command.ExecutionError | FSUtil.DirectoryError>
readonly shell: (
input: Parameters<Session.Handle["shell"]>[0] & { sessionID: SessionSchema.ID },
) => ReturnType<Session.Handle["shell"]>
@@ -218,8 +221,13 @@ export interface Interface {
sessionID: SessionSchema.ID
messageID: SessionMessage.ID
files?: boolean
}) => Effect.Effect<SessionSchema.Revert, NotFoundError | MessageNotFoundError | BusyError | Snapshot.Error>
readonly clear: (sessionID: SessionSchema.ID) => Effect.Effect<void, NotFoundError | BusyError | Snapshot.Error>
}) => Effect.Effect<
SessionSchema.Revert,
NotFoundError | MessageNotFoundError | BusyError | Snapshot.Error | FSUtil.DirectoryError
>
readonly clear: (
sessionID: SessionSchema.ID,
) => Effect.Effect<void, NotFoundError | BusyError | Snapshot.Error | FSUtil.DirectoryError>
readonly commit: (sessionID: SessionSchema.ID) => Effect.Effect<void, NotFoundError | BusyError>
}
}
+7 -1
View File
@@ -1,5 +1,6 @@
export * as SessionGenerate from "./generate.js"
import type { FSUtil } from "@opencode/util/fs-util"
import { LLMClient, Message, type AIError } from "@opencode/ai"
import { Effect } from "effect"
import { Database } from "../database/database.js"
@@ -14,7 +15,12 @@ import { SessionModelRequest } from "./model-request.js"
import type { SessionRunnerModel } from "./runner/model.js"
import type { SessionSchema } from "./schema.js"
export type Error = AgentNotFoundError | Instructions.InitializationBlocked | SessionRunnerModel.Error | AIError
export type Error =
| AgentNotFoundError
| Instructions.InitializationBlocked
| SessionRunnerModel.Error
| AIError
| FSUtil.DirectoryError
/** Generates text from current Session context without mutating the Session. */
export const generate = Effect.fn("SessionGenerate.generate")(function* (input: {
+7 -2
View File
@@ -45,7 +45,11 @@ export interface Interface {
delivery?: SessionInbox.Delivery
}) => Effect.Effect<
void,
NotFoundError | DestinationNotFoundError | DestinationNotDirectoryError | DestinationUnavailableError
| NotFoundError
| DestinationNotFoundError
| DestinationNotDirectoryError
| DestinationUnavailableError
| FSUtil.DirectoryError
>
}
@@ -91,7 +95,8 @@ const layer = Layer.effect(
yield* locations.contextEffect(destination.location).pipe(
Effect.scoped,
Effect.catchCause((cause) => {
if (Cause.hasInterruptsOnly(cause)) return Effect.failCause(cause)
if (Cause.hasInterruptsOnly(cause))
return Effect.failCause(Cause.fromReasons<never>(cause.reasons.filter(Cause.isInterruptReason)))
return Effect.logWarning("session move destination unavailable", { directory, cause }).pipe(
Effect.andThen(Effect.fail(new DestinationUnavailableError({ directory }))),
)
@@ -1,5 +1,6 @@
export * as SessionRunner from "./index.js"
import type { FSUtil } from "@opencode/util/fs-util"
import type { AIError } from "@opencode/ai"
import { Context, Data, Effect } from "effect"
import { SessionSchema } from "../schema.js"
@@ -15,6 +16,7 @@ export type RunError =
| AgentNotFoundError
| StepFailedError
| Instructions.InitializationBlocked
| FSUtil.DirectoryError
export type Continuation = { readonly step: number }
+1
View File
@@ -68,6 +68,7 @@ export class StrategyUnavailableError extends Schema.TaggedError<StrategyUnavail
) {}
export type Error =
| FSUtil.DirectoryError
| Project.NotFoundError
| SourceDirectoryNotFoundError
| DestinationExistsError
@@ -1,3 +1,4 @@
import type { FSUtil } from "@opencode/util/fs-util"
import { Bus } from "@opencode/core/bus"
import { Image } from "@opencode/core/image"
import { LocationServiceMap } from "@opencode/core/location-service-map"
@@ -24,7 +25,7 @@ export const promptLocationNode = makeGlobalNode({
Bus.node.replace(Layer.succeed(Bus.Service, bus)),
Plugin.node.replace(Layer.mock(Plugin.Service, { awaitActivation: Effect.void })),
],
}) as Layer.Layer<LocationServices>,
}) as unknown as Layer.Layer<LocationServices, FSUtil.DirectoryError>,
)
}),
),
+18
View File
@@ -0,0 +1,18 @@
import { Effect, FileSystem, Layer, type PlatformError } from "effect"
import { LayerNodePlatform } from "@opencode/util/effect/app-node-platform"
/** Fails platform realPath for selected paths, so FSUtil's own error classification still runs. */
export function blockRealPath(
fs: FileSystem.FileSystem,
failure: (target: string) => PlatformError.PlatformError | undefined,
) {
return LayerNodePlatform.filesystem.replace(
Layer.succeed(FileSystem.FileSystem, {
...fs,
realPath: (target) => {
const error = failure(target)
return error ? Effect.fail(error) : fs.realPath(target)
},
}),
)
}
+2 -1
View File
@@ -1,3 +1,4 @@
import type { FSUtil } from "@opencode/util/fs-util"
import { describe, expect } from "bun:test"
import { Context, Deferred, Duration, Effect, Fiber, Layer, LayerMap, RcMap, Schema } from "effect"
import { TestClock } from "effect/testing"
@@ -65,7 +66,7 @@ const locations = Layer.effect(
Layer.provideMerge(Form.layer),
Layer.provide(Layer.succeed(Bus.Service, bus)),
Layer.fresh,
) as unknown as Layer.Layer<LocationServices>,
) as unknown as Layer.Layer<LocationServices, FSUtil.DirectoryError>,
{ idleTimeToLive: Duration.infinity },
)
return {
@@ -8,6 +8,7 @@ import { FileSystem } from "@opencode/core/filesystem"
import { Location } from "@opencode/core/location"
import { AbsolutePath, RelativePath } from "@opencode/core/schema"
import { Workspace } from "@opencode/core/workspace"
import { blockRealPath } from "./fixture/realpath"
import { location } from "./fixture/location"
import { tmpdir } from "./fixture/tmpdir"
import { it } from "./lib/effect"
@@ -118,12 +119,7 @@ describe("FileSystem", () => {
Location.node.replace(
Layer.succeed(Location.Service, location({ directory: AbsolutePath.make(directory) })),
),
FSUtil.node.replace(
Layer.succeed(FSUtil.Service, {
...filesystem,
realPath: (target) => (target === directory ? Effect.fail(cause) : filesystem.realPath(target)),
}),
),
blockRealPath(filesystem, (target) => (target === directory ? cause : undefined)),
],
}),
),
@@ -145,7 +141,7 @@ describe("FileSystem", () => {
}
expect(result.cause.reasons.filter(Cause.isDieReason)).toMatchObject([{ defect: cause }])
}
}).pipe(Effect.provide(LayerNode.compile(FSUtil.node))),
}).pipe(Effect.provide(LayerNode.compile(FSUtil.node).pipe(Layer.fresh))),
),
)
}
+2 -1
View File
@@ -1,3 +1,4 @@
import type { FSUtil } from "@opencode/util/fs-util"
import fs from "fs/promises"
import path from "path"
import { describe, expect } from "bun:test"
@@ -67,7 +68,7 @@ const activityLocations = Layer.effect(
workspaceID: ref.workspaceID,
project: { id: Project.ID.global, directory: ref.directory, canonical: ref.directory },
}),
) as unknown as Layer.Layer<LocationServices>,
) as unknown as Layer.Layer<LocationServices, FSUtil.DirectoryError>,
{ idleTimeToLive: Duration.infinity },
),
)
+2 -1
View File
@@ -1,3 +1,4 @@
import type { FSUtil } from "@opencode/util/fs-util"
import { describe, expect } from "bun:test"
import { LLMClient, LLMEvent, LanguageModel, type LLMRequest } from "@opencode/ai"
import { OpenAIChat } from "@opencode/ai/protocols"
@@ -58,7 +59,7 @@ const locations = Layer.effect(
Layer.provide(client),
Layer.provide(config),
Layer.provide(models),
) as unknown as Layer.Layer<LocationServices>,
) as unknown as Layer.Layer<LocationServices, FSUtil.DirectoryError>,
),
)
const it = testEffect(
+2 -1
View File
@@ -1,3 +1,4 @@
import type { FSUtil } from "@opencode/util/fs-util"
import { describe, expect, test } from "bun:test"
import { AIError, TransportError } from "@opencode/ai"
import { Database } from "@opencode/core/database/database"
@@ -1371,7 +1372,7 @@ function buildExecution(
() =>
// The local execution test only needs the Session runner from the Location graph.
// oxlint-disable-next-line typescript-eslint/no-unsafe-type-assertion
runner as unknown as Layer.Layer<LocationServices>,
runner as unknown as Layer.Layer<LocationServices, FSUtil.DirectoryError>,
),
)
return yield* Layer.buildWithScope(
+7 -2
View File
@@ -1,3 +1,4 @@
import type { FSUtil } from "@opencode/util/fs-util"
import { describe, expect } from "bun:test"
import path from "path"
import { chmod, mkdir, readdir, rm } from "fs/promises"
@@ -58,7 +59,7 @@ const itWithActiveExecution = testEffect(
replacements: [Project.node.replace(globalProjectNode), offlineModels],
}),
Layer.succeed(SessionRunner.Service, { drain: () => Effect.never }),
) as unknown as Layer.Layer<LocationServices>,
) as unknown as Layer.Layer<LocationServices, FSUtil.DirectoryError>,
),
),
),
@@ -68,7 +69,11 @@ const itWithActiveExecution = testEffect(
const unavailableLocations = Layer.effect(
LocationServiceMap.Service,
LayerMap.make(
() => Layer.effectDiscard(Effect.fail(new Error("broken location"))) as unknown as Layer.Layer<LocationServices>,
() =>
Layer.effectDiscard(Effect.die(new Error("broken location"))) as unknown as Layer.Layer<
LocationServices,
FSUtil.DirectoryError
>,
),
)
const itWithUnavailableDestination = testEffect(
+2 -1
View File
@@ -1,4 +1,5 @@
import { describe, expect } from "bun:test"
import type { FSUtil } from "@opencode/util/fs-util"
import { DateTime, Effect, Fiber, Layer, LayerMap, Schema, Stream } from "effect"
import path from "path"
import { pathToFileURL } from "url"
@@ -83,7 +84,7 @@ const locations = makeGlobalNode({
restore: () => Effect.void,
}),
Layer.mock(Plugin.Service, { awaitActivation: Effect.void }),
).pipe(Layer.fresh) as unknown as Layer.Layer<LocationServices>,
).pipe(Layer.fresh) as unknown as Layer.Layer<LocationServices, FSUtil.DirectoryError>,
)
}),
),
+2 -1
View File
@@ -1,3 +1,4 @@
import type { FSUtil } from "@opencode/util/fs-util"
import path from "path"
import { describe, expect } from "bun:test"
import { Effect, Layer, LayerMap } from "effect"
@@ -49,7 +50,7 @@ const locations = makeGlobalNode({
list: () => Effect.succeed([info]),
}),
Layer.mock(Plugin.Service, { awaitActivation: Effect.void }),
) as unknown as Layer.Layer<LocationServices>,
) as unknown as Layer.Layer<LocationServices, FSUtil.DirectoryError>,
),
),
deps: [],
+8 -2
View File
@@ -1,4 +1,9 @@
import { InvalidRequestError, SessionNotFoundError } from "./errors.js"
import {
DirectoryNotFoundError,
DirectoryAccessDeniedError,
InvalidRequestError,
SessionNotFoundError,
} from "./errors.js"
import { makeDefaultApi } from "./api.js"
import type { Api } from "./api.js"
import type { Context } from "effect"
@@ -7,11 +12,12 @@ import type { EventGroup } from "./groups/event.js"
class LocationMiddleware extends HttpApiMiddleware.Service<LocationMiddleware>()(
"@opencode/client/LocationMiddleware",
{ error: [DirectoryNotFoundError, DirectoryAccessDeniedError] },
) {}
class SessionLocationMiddleware extends HttpApiMiddleware.Service<SessionLocationMiddleware>()(
"@opencode/client/SessionLocationMiddleware",
{ error: [InvalidRequestError, SessionNotFoundError] },
{ error: [InvalidRequestError, SessionNotFoundError, DirectoryNotFoundError, DirectoryAccessDeniedError] },
) {}
type ClientApiShape = Api<
+13
View File
@@ -1,5 +1,18 @@
import { Schema } from "effect"
import { Skill } from "@opencode/schema/skill"
import { AbsolutePath } from "@opencode/schema/schema"
export class DirectoryNotFoundError extends Schema.TaggedError<DirectoryNotFoundError>()(
"DirectoryNotFoundError",
{ directory: AbsolutePath, message: Schema.String },
{ httpApiStatus: 404 },
) {}
export class DirectoryAccessDeniedError extends Schema.TaggedError<DirectoryAccessDeniedError>()(
"DirectoryAccessDeniedError",
{ directory: AbsolutePath, message: Schema.String },
{ httpApiStatus: 403 },
) {}
export class InvalidRequestError extends Schema.TaggedError<InvalidRequestError>()(
"InvalidRequestError",
+7 -2
View File
@@ -1,7 +1,12 @@
import { Model } from "@opencode/schema/model"
import { Schema } from "effect"
import { HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
import { InvalidRequestError, ServiceUnavailableError } from "../errors.js"
import {
DirectoryNotFoundError,
DirectoryAccessDeniedError,
InvalidRequestError,
ServiceUnavailableError,
} from "../errors.js"
export const GenerateGroup = HttpApiGroup.make("server.generate")
.add(
@@ -13,7 +18,7 @@ export const GenerateGroup = HttpApiGroup.make("server.generate")
success: Schema.Struct({
data: Schema.Struct({ text: Schema.String }),
}).annotate({ identifier: "GenerateTextResponse" }),
error: [InvalidRequestError, ServiceUnavailableError],
error: [InvalidRequestError, ServiceUnavailableError, DirectoryNotFoundError, DirectoryAccessDeniedError],
}).annotateMerge(
OpenApi.annotations({
identifier: "experimental.generate.text",
+7 -2
View File
@@ -6,7 +6,12 @@ import { Project } from "@opencode/schema/project"
import { Session } from "@opencode/schema/session"
import { Context, Schema } from "effect"
import { HttpApiEndpoint, HttpApiGroup, HttpApiMiddleware, HttpApiSchema, OpenApi } from "effect/unstable/httpapi"
import { PermissionNotFoundError, SessionNotFoundError } from "../errors.js"
import {
DirectoryNotFoundError,
DirectoryAccessDeniedError,
PermissionNotFoundError,
SessionNotFoundError,
} from "../errors.js"
import { LocationQuery, locationQueryOpenApi } from "./location.js"
export const makePermissionGroup = <
@@ -89,7 +94,7 @@ export const makePermissionGroup = <
HttpApiEndpoint.get("session.permission.list", "/api/session/:sessionID/permission", {
params: { sessionID: Session.ID },
success: Schema.Struct({ data: Schema.Array(Permission.Request) }),
error: SessionNotFoundError,
error: [SessionNotFoundError, DirectoryNotFoundError, DirectoryAccessDeniedError],
}).annotateMerge(
OpenApi.annotations({
identifier: "session.permission.list",
+16 -11
View File
@@ -18,6 +18,8 @@ import { Context, Effect, Encoding, Result, Schema, SchemaGetter, Struct } from
import { HttpApiEndpoint, HttpApiGroup, HttpApiMiddleware, HttpApiSchema, OpenApi } from "effect/unstable/httpapi"
import {
ConflictError,
DirectoryNotFoundError,
DirectoryAccessDeniedError,
CommandExecutionError,
CommandNotFoundError,
FormAlreadySettledError,
@@ -170,12 +172,10 @@ export const SessionsQuery = Schema.Struct({
cursor: SessionsQueryCursor.pipe(Schema.optional),
}).annotate({ identifier: "SessionsQuery" })
export const makeSessionGroup = <
I extends HttpApiMiddleware.AnyId,
S,
FormI extends HttpApiMiddleware.AnyId,
FormS,
>(sessionLocationMiddleware: Context.Key<I, S>, formLocationMiddleware: Context.Key<FormI, FormS>) =>
export const makeSessionGroup = <I extends HttpApiMiddleware.AnyId, S, FormI extends HttpApiMiddleware.AnyId, FormS>(
sessionLocationMiddleware: Context.Key<I, S>,
formLocationMiddleware: Context.Key<FormI, FormS>,
) =>
HttpApiGroup.make("server.session")
.add(
HttpApiEndpoint.get("session.list", "/api/session", {
@@ -382,7 +382,7 @@ export const makeSessionGroup = <
params: { sessionID: Session.ID },
payload: Schema.Struct({ ...Location.PublicRef.fields, delivery: SessionInbox.Delivery.pipe(Schema.optional) }),
success: HttpApiSchema.NoContent,
error: [SessionNotFoundError, InvalidRequestError],
error: [SessionNotFoundError, InvalidRequestError, DirectoryNotFoundError, DirectoryAccessDeniedError],
}).annotateMerge(
OpenApi.annotations({
identifier: "session.move",
@@ -562,9 +562,7 @@ export const makeSessionGroup = <
error: [SessionNotFoundError, SessionBusyError],
})
.middleware(sessionLocationMiddleware)
.annotateMerge(
OpenApi.annotations({ identifier: "session.revert.commit", summary: "Commit staged revert" }),
),
.annotateMerge(OpenApi.annotations({ identifier: "session.revert.commit", summary: "Commit staged revert" })),
)
.add(
HttpApiEndpoint.get("session.context", "/api/session/:sessionID/context", {
@@ -594,7 +592,14 @@ export const makeSessionGroup = <
}),
}),
success: Schema.Struct({ data: Schema.Array(FileDiff.Info) }),
error: [InvalidRequestError, MessageNotFoundError, SessionNotFoundError, UnknownError],
error: [
InvalidRequestError,
MessageNotFoundError,
SessionNotFoundError,
UnknownError,
DirectoryNotFoundError,
DirectoryAccessDeniedError,
],
}).annotateMerge(
OpenApi.annotations({
identifier: "session.diff",
+5 -1
View File
@@ -57,7 +57,11 @@ export function layer(options: Options, replacements: () => LayerNode.Replacemen
...LocationServiceMap.canonical(session.location),
})
const provide = (session: Session.Info) => Effect.provide(instances.get(key(session)))
const instances: LayerMap.LayerMap<ReturnType<typeof key>, Instance.Services> = yield* LayerMap.make(
const instances: LayerMap.LayerMap<
ReturnType<typeof key>,
Instance.Services,
Instance.Error
> = yield* LayerMap.make(
(input: ReturnType<typeof key>) =>
Layer.unwrap(
Effect.gen(function* () {
+18 -13
View File
@@ -7,6 +7,7 @@ import { Global } from "@opencode/util/global"
import { Effect } from "effect"
import { HttpApiBuilder } from "effect/unstable/httpapi"
import { Api } from "../api"
import { directoryErrors } from "../location"
export const GenerateHandler = HttpApiBuilder.group(Api, "server.generate", (handlers) =>
Effect.gen(function* () {
@@ -15,19 +16,23 @@ export const GenerateHandler = HttpApiBuilder.group(Api, "server.generate", (han
const services = locations.get(Location.Ref.make({ directory: AbsolutePath.make(global.config) }))
return handlers.handle(
"generate.text",
Effect.fn("server.generate.text")(function* (request) {
const generate = yield* Generate.Service
const text = yield* generate
.text(request.payload)
.pipe(
Effect.mapError((error) =>
error._tag === "Generate.ModelSelectionError"
? new InvalidRequestError({ message: error.message })
: new ServiceUnavailableError({ message: error.message, service: error.service }),
),
)
return { data: { text } }
}, Effect.provide(services)),
Effect.fn("server.generate.text")(
function* (request) {
const generate = yield* Generate.Service
const text = yield* generate
.text(request.payload)
.pipe(
Effect.mapError((error) =>
error._tag === "Generate.ModelSelectionError"
? new InvalidRequestError({ message: error.message })
: new ServiceUnavailableError({ message: error.message, service: error.service }),
),
)
return { data: { text } }
},
Effect.provide(services),
directoryErrors,
),
)
}),
)
+1 -1
View File
@@ -24,7 +24,7 @@ export const LocationHandler = HttpApiBuilder.group(Api, "server.location", (han
Effect.provideService(LocationServiceMap.Service, locations),
Effect.catchCause((cause) =>
Cause.hasInterruptsOnly(cause)
? Effect.failCause(cause)
? Effect.failCause(Cause.fromReasons<never>(cause.reasons.filter(Cause.isInterruptReason)))
: Effect.fail(new ServiceUnavailableError({ message: Cause.pretty(cause), service: "location" })),
),
),
+2 -2
View File
@@ -7,7 +7,7 @@ import { Effect } from "effect"
import { HttpApiBuilder, HttpApiSchema } from "effect/unstable/httpapi"
import { Api } from "../api"
import { PermissionNotFoundError } from "@opencode/protocol/errors"
import { response, sessionInfo } from "../location"
import { directoryErrors, response, sessionInfo } from "../location"
import { missingSession } from "./session-error"
function missingRequest(id: Permission.ID) {
@@ -62,7 +62,7 @@ export const PermissionHandler = HttpApiBuilder.group(Api, "server.permission",
const session = yield* sessionInfo(sessions, ctx.params.sessionID)
const requests = yield* Permission.Service.use((permission) =>
permission.forSession(ctx.params.sessionID),
).pipe(instances.provide(session))
).pipe(instances.provide(session), directoryErrors)
return { data: requests }
}),
)
+16 -9
View File
@@ -23,6 +23,7 @@ import {
SkillNotFoundError,
} from "@opencode/protocol/errors"
import { AbsolutePath } from "@opencode/core/schema"
import { directoryErrors } from "../location"
import { failedMessageDecode, failedSnapshot, missingMessage, missingSession } from "./session-error"
const DefaultSessionsLimit = 50
@@ -299,6 +300,7 @@ export const SessionHandler = HttpApiBuilder.group(Api, "server.session", (handl
Effect.catchTag("Session.DestinationUnavailableError", (error) =>
Effect.fail(new InvalidRequestError({ message: `Directory is unavailable: ${error.directory}` })),
),
directoryErrors,
)
return HttpApiSchema.NoContent.make()
}),
@@ -335,6 +337,7 @@ export const SessionHandler = HttpApiBuilder.group(Api, "server.session", (handl
Effect.catchTag("Session.SkillNotFoundError", (error) =>
Effect.fail(new InvalidRequestError({ message: `Skill not found: ${error.skill}`, field: "skills" })),
),
directoryErrors,
),
}
}),
@@ -370,6 +373,7 @@ export const SessionHandler = HttpApiBuilder.group(Api, "server.session", (handl
}),
),
),
directoryErrors,
)
return HttpApiSchema.NoContent.make()
}),
@@ -388,6 +392,7 @@ export const SessionHandler = HttpApiBuilder.group(Api, "server.session", (handl
Effect.catchTag("Session.SkillNotFoundError", (error) =>
Effect.fail(new SkillNotFoundError({ skill: error.skill, message: `Skill not found: ${error.skill}` })),
),
directoryErrors,
)
return HttpApiSchema.NoContent.make()
}),
@@ -471,6 +476,7 @@ export const SessionHandler = HttpApiBuilder.group(Api, "server.session", (handl
Effect.catchTag("Session.MessageNotFoundError", missingMessage),
Effect.catchTag("Session.BusyError", busySession),
Effect.catchTag("Snapshot.Error", failedSnapshot("stage session revert", ctx.params.sessionID)),
directoryErrors,
),
}
}),
@@ -485,6 +491,7 @@ export const SessionHandler = HttpApiBuilder.group(Api, "server.session", (handl
Effect.catchTag("Session.NotFoundError", missingSession),
Effect.catchTag("Session.BusyError", busySession),
Effect.catchTag("Snapshot.Error", failedSnapshot("clear session revert", ctx.params.sessionID)),
directoryErrors,
)
return HttpApiSchema.NoContent.make()
}),
@@ -527,6 +534,7 @@ export const SessionHandler = HttpApiBuilder.group(Api, "server.session", (handl
(error) => new InvalidRequestError({ message: error.message, field: error.field }),
),
Effect.catchTag("Snapshot.Error", failedSnapshot("diff session turn", ctx.params.sessionID)),
directoryErrors,
),
}
}),
@@ -588,15 +596,14 @@ export const SessionHandler = HttpApiBuilder.group(Api, "server.session", (handl
.handle(
"session.generate",
Effect.fn(function* (ctx) {
const text = yield* session
.generate({ sessionID: ctx.params.sessionID, prompt: ctx.payload.prompt })
.pipe(
Effect.mapError((error) =>
error._tag === "Session.NotFoundError"
? missingSession(error)
: new ServiceUnavailableError({ message: error.message, service: "session generation" }),
),
)
const text = yield* session.generate({ sessionID: ctx.params.sessionID, prompt: ctx.payload.prompt }).pipe(
directoryErrors,
Effect.mapError((error) =>
error._tag === "Session.NotFoundError"
? missingSession(error)
: new ServiceUnavailableError({ message: error.message, service: "session generation" }),
),
)
return { data: { text } }
}),
)
+19 -2
View File
@@ -1,8 +1,9 @@
import { FSUtil } from "@opencode/util/fs-util"
import { Location } from "@opencode/core/location"
import { LocationServiceMap } from "@opencode/core/location-services"
import { AbsolutePath } from "@opencode/core/schema"
import { Session } from "@opencode/core/session"
import { InvalidRequestError } from "@opencode/protocol/errors"
import { DirectoryNotFoundError, DirectoryAccessDeniedError, InvalidRequestError } from "@opencode/protocol/errors"
import { Effect, Layer, Schema } from "effect"
import { HttpServerRequest } from "effect/unstable/http"
import { HttpApiMiddleware } from "effect/unstable/httpapi"
@@ -12,8 +13,24 @@ export type LocationServices = Layer.Success<ReturnType<(typeof LocationServiceM
export class LocationMiddleware extends HttpApiMiddleware.Service<LocationMiddleware, { provides: LocationServices }>()(
"@opencode/HttpApiLocation",
{ error: [DirectoryNotFoundError, DirectoryAccessDeniedError] },
) {}
export function directoryErrors<A, E, R>(effect: Effect.Effect<A, E, R>) {
return effect.pipe(
Effect.catchIf(
(error): error is Extract<E, FSUtil.DirectoryError> =>
error instanceof FSUtil.DirectoryNotFoundError || error instanceof FSUtil.DirectoryAccessDeniedError,
(error): Effect.Effect<never, DirectoryNotFoundError | DirectoryAccessDeniedError> =>
Effect.fail(
error instanceof FSUtil.DirectoryNotFoundError
? new DirectoryNotFoundError({ directory: AbsolutePath.make(error.directory), message: error.message })
: new DirectoryAccessDeniedError({ directory: AbsolutePath.make(error.directory), message: error.message }),
),
),
)
}
export function response<A, E, R>(data: Effect.Effect<A, E, R>) {
return Effect.gen(function* () {
const location = yield* Location.Service
@@ -61,7 +78,7 @@ export const layer = Layer.effect(
return LocationMiddleware.of((effect) =>
Effect.gen(function* () {
const request = yield* HttpServerRequest.HttpServerRequest
return yield* effect.pipe(Effect.provide(locations.get(requestRef(request))))
return yield* effect.pipe(Effect.provide(locations.get(requestRef(request))), directoryErrors)
}),
)
}),
@@ -1,17 +1,22 @@
import { Instance } from "@opencode/core/instance/service"
import { LocationServiceMap } from "@opencode/core/location-services"
import { Session } from "@opencode/core/session"
import { InvalidRequestError, SessionNotFoundError } from "@opencode/protocol/errors"
import {
DirectoryNotFoundError,
DirectoryAccessDeniedError,
InvalidRequestError,
SessionNotFoundError,
} from "@opencode/protocol/errors"
import { Effect, Layer } from "effect"
import { HttpRouter, HttpServerRequest } from "effect/unstable/http"
import { HttpApiMiddleware } from "effect/unstable/httpapi"
import { requestRef, sessionInfo, type LocationServices } from "../location"
import { directoryErrors, requestRef, sessionInfo, type LocationServices } from "../location"
export class FormLocationMiddleware extends HttpApiMiddleware.Service<
FormLocationMiddleware,
{ provides: LocationServices }
>()("@opencode/HttpApiFormLocation", {
error: [InvalidRequestError, SessionNotFoundError],
error: [InvalidRequestError, SessionNotFoundError, DirectoryNotFoundError, DirectoryAccessDeniedError],
}) {}
export const formLocationLayer = Layer.effect(
@@ -29,11 +34,11 @@ export const formLocationLayer = Layer.effect(
// the session row lookup because some MCP elicitations cannot currently be attributed to
// a real session. Keep this undocumented and remove once elicitations carry session ownership.
const request = yield* HttpServerRequest.HttpServerRequest
return yield* effect.pipe(Effect.provide(locations.get(requestRef(request))))
return yield* effect.pipe(Effect.provide(locations.get(requestRef(request))), directoryErrors)
}
const session = yield* sessionInfo(sessions, route.params.sessionID)
return yield* effect.pipe(instances.provide(session))
return yield* effect.pipe(instances.provide(session), directoryErrors)
}),
)
}),
@@ -3,14 +3,19 @@ import { Session } from "@opencode/core/session"
import { Effect, Layer } from "effect"
import { HttpRouter } from "effect/unstable/http"
import { HttpApiMiddleware } from "effect/unstable/httpapi"
import { InvalidRequestError, SessionNotFoundError } from "@opencode/protocol/errors"
import { sessionInfo, type LocationServices } from "../location"
import {
DirectoryNotFoundError,
DirectoryAccessDeniedError,
InvalidRequestError,
SessionNotFoundError,
} from "@opencode/protocol/errors"
import { directoryErrors, sessionInfo, type LocationServices } from "../location"
export class SessionLocationMiddleware extends HttpApiMiddleware.Service<
SessionLocationMiddleware,
{ provides: LocationServices }
>()("@opencode/HttpApiSessionLocation", {
error: [InvalidRequestError, SessionNotFoundError],
error: [InvalidRequestError, SessionNotFoundError, DirectoryNotFoundError, DirectoryAccessDeniedError],
}) {}
export const sessionLocationLayer = Layer.effect(
@@ -23,7 +28,7 @@ export const sessionLocationLayer = Layer.effect(
Effect.gen(function* () {
const route = yield* HttpRouter.RouteContext
const session = yield* sessionInfo(sessions, route.params.sessionID)
return yield* effect.pipe(instances.provide(session))
return yield* effect.pipe(instances.provide(session), directoryErrors)
}),
)
}),
+101 -2
View File
@@ -5,7 +5,11 @@ import path from "node:path"
import { Agent } from "@opencode/schema/agent"
import { Integration } from "@opencode/schema/integration"
import { ServerInfo } from "@opencode/protocol/groups/server"
import { Effect, Schedule, Schema } from "effect"
import { Effect, Layer, PlatformError, Schedule, Schema } from "effect"
import { FSUtil } from "@opencode/util/fs-util"
import { LayerNode } from "@opencode/util/effect/layer-node"
import { Session } from "@opencode/schema/session"
import { blockRealPath } from "../../core/test/fixture/realpath"
import { tmpdir } from "../../core/test/fixture/tmpdir"
import { it } from "../../core/test/lib/effect"
import { ServerFetch } from "../src/fetch"
@@ -18,6 +22,99 @@ const options = {
fs: { filewatcher: false },
} as const
for (const input of [
{ code: "ENOENT", reason: "NotFound", status: 404, tag: "DirectoryNotFoundError", message: "Directory not found" },
{
code: "EACCES",
reason: "PermissionDenied",
status: 403,
tag: "DirectoryAccessDeniedError",
message: "Access denied to directory",
},
{
code: "EPERM",
reason: "Unknown",
status: 403,
tag: "DirectoryAccessDeniedError",
message: "Access denied to directory",
},
{ code: "EIO", reason: "Unknown", status: 500 },
] as const) {
it.live(`returns directory HTTP errors for ${input.code} and retries after recovery`, () =>
Effect.gen(function* () {
const config = yield* Effect.acquireDisposable(Effect.promise(() => tmpdir("opencode-directory-errors-")))
const directory = path.join(config.path, "project")
const filesystem = yield* FSUtil.Service.pipe(Effect.provide(LayerNode.compile(FSUtil.node)))
const blocked = new Set<string>()
const cause = PlatformError.systemError({
_tag: input.reason,
module: "FileSystem",
method: "realPath",
pathOrDescriptor: directory,
cause: Object.assign(new Error(input.code), { code: input.code }),
})
const handler = yield* ServerFetch.make(
{ ...options, config: { directory: config.path } },
{
overrides: [
blockRealPath(filesystem, (target) => (blocked.has(target) && input.code !== "ENOENT" ? cause : undefined)),
],
},
)
// Session creation only resolves its placement; it does not boot the Location graph.
const created = yield* Effect.promise(() =>
handler(
new Request("http://opencode.local/api/session", {
method: "POST",
headers: { "content-type": "application/json", "x-opencode-directory": encodeURIComponent(directory) },
body: JSON.stringify({ location: { directory } }),
}),
),
)
expect(created.status).toBe(200)
const session = Schema.decodeUnknownSync(Schema.Struct({ data: Session.Info }))(
yield* Effect.promise(() => created.json()),
).data
blocked.add(directory)
if (input.code !== "ENOENT") yield* Effect.promise(() => fs.mkdir(directory))
const endpoints = [
"/api/model",
"/api/integration",
`/api/session/${session.id}/permission`,
`/api/experimental/session/${session.id}/instructions/entries`,
`/api/session/${session.id}/form`,
"/api/session/global/form",
]
for (const endpoint of endpoints) {
const response = yield* Effect.promise(() =>
handler(
new Request(`http://opencode.local${endpoint}`, {
headers: { "x-opencode-directory": encodeURIComponent(directory) },
}),
),
)
expect({ endpoint, status: response.status }).toEqual({ endpoint, status: input.status })
if (input.status !== 500)
expect(yield* Effect.promise(() => response.json())).toEqual({
_tag: input.tag,
directory,
message: `${input.message}: ${directory}`,
})
}
blocked.clear()
if (input.code === "ENOENT") yield* Effect.promise(() => fs.mkdir(directory))
const recovered = yield* Effect.promise(() =>
handler(
new Request("http://opencode.local/api/model", {
headers: { "x-opencode-directory": encodeURIComponent(directory) },
}),
),
)
expect(recovered.status).toBe(200)
}),
)
}
type Handler = (request: Request) => Promise<Response>
function occupy(port: number, cancel = false) {
@@ -105,7 +202,9 @@ it.live("serves the HttpApi and enforces Basic auth like the Node server", () =>
),
)
expect(response.status).toBe(200)
const body = yield* Effect.promise(() => response.json()).pipe(Effect.flatMap(Schema.decodeUnknownEffect(ServerInfo)))
const body = yield* Effect.promise(() => response.json()).pipe(
Effect.flatMap(Schema.decodeUnknownEffect(ServerInfo)),
)
expect(body.version).toBe("test-version")
expect(body.paths.tmp).toEndWith("opencode")
}),
+41
View File
@@ -22,6 +22,26 @@ export namespace FSUtil {
export type Error = PlatformError | FileSystemError
export class DirectoryNotFoundError extends Schema.TaggedError<DirectoryNotFoundError>()(
"FileSystem.DirectoryNotFoundError",
{ directory: Schema.String, cause: Schema.Defect() },
) {
override get message() {
return `Directory not found: ${this.directory}`
}
}
export class DirectoryAccessDeniedError extends Schema.TaggedError<DirectoryAccessDeniedError>()(
"FileSystem.DirectoryAccessDeniedError",
{ directory: Schema.String, cause: Schema.Defect() },
) {
override get message() {
return `Access denied to directory: ${this.directory}`
}
}
export type DirectoryError = DirectoryNotFoundError | DirectoryAccessDeniedError
export interface DirEntry {
readonly name: string
readonly type: "file" | "directory" | "symlink" | "other"
@@ -47,6 +67,8 @@ export namespace FSUtil {
readonly writeWithDirs: (path: string, content: string | Uint8Array, mode?: number) => Effect.Effect<void, Error>
readonly readDirectoryEntries: (path: string) => Effect.Effect<DirEntry[], Error>
readonly resolve: (path: string) => Effect.Effect<string>
/** realPath for a directory that must be reachable; missing and denied directories fail with typed errors. */
readonly realDirectory: (path: string) => Effect.Effect<string, DirectoryError>
readonly findUp: (target: string, start: string, stop?: string) => Effect.Effect<string[], Error>
readonly up: (options: UpOptions) => Effect.Effect<string[], Error>
readonly globUp: (pattern: string, start: string, stop?: string) => Effect.Effect<string[], Error>
@@ -109,6 +131,24 @@ export namespace FSUtil {
)
})
const realDirectory = Effect.fn("FileSystem.realDirectory")(function* (directory: string) {
return yield* fs.realPath(directory).pipe(
Effect.catch((cause): Effect.Effect<never, DirectoryError> => {
if (cause.reason._tag === "NotFound") return Effect.fail(new DirectoryNotFoundError({ directory, cause }))
// macOS privacy denials arrive as Unknown with an EPERM cause.
if (
cause.reason._tag === "PermissionDenied" ||
(cause.reason._tag === "Unknown" &&
cause.reason.cause instanceof Error &&
"code" in cause.reason.cause &&
cause.reason.cause.code === "EPERM")
)
return Effect.fail(new DirectoryAccessDeniedError({ directory, cause }))
return Effect.die(cause)
}),
)
})
const readJson = Effect.fn("FileSystem.readJson")(function* (path: string) {
const text = yield* fs.readFileString(path)
return yield* Effect.try({
@@ -214,6 +254,7 @@ export namespace FSUtil {
isFile,
readDirectoryEntries,
resolve,
realDirectory,
readJson,
writeJson,
ensureDir,