diff --git a/V2_HTTP_API_AUDIT.md b/V2_HTTP_API_AUDIT.md index cce010545ed..19ed93162b7 100644 --- a/V2_HTTP_API_AUDIT.md +++ b/V2_HTTP_API_AUDIT.md @@ -147,7 +147,7 @@ Review endpoints in document order. For each endpoint, select one disposition an | [x] 053 | `POST` | `/api/session/{sessionID}/fork` | `session.fork` | Change | Request now accepts optional branded `before` message ID; omission copies full history. | | [x] 054 | `POST` | `/api/session/{sessionID}/agent` | `session.switchAgent` | Keep | Subsequent-execution agent selection retained. | | [x] 055 | `POST` | `/api/session/{sessionID}/model` | `session.switchModel` | Keep | Subsequent-execution model and optional variant selection retained. | -| [x] 056 | `PATCH` | `/api/session/{sessionID}` | `session.rename` | Change | Title-only rename now uses the session resource path. | +| [x] 056 | `PATCH` | `/api/session/{sessionID}` | `session.update` | Change | General session patch now updates optional title and permissions. | | [x] 057 | `POST` | `/api/session/{sessionID}/move` | `session.move` | Change | Removed inaccurate local-change transfer claim; delivery behavior retained. | | [x] 058 | `POST` | `/api/session/{sessionID}/background` | `session.background` | Keep | Backgroundable foreground tools transition to background observation; idle requests remain no-ops. | @@ -213,7 +213,7 @@ Review endpoints in document order. For each endpoint, select one disposition an | [x] 098 | `GET` | `/api/session/{sessionID}/permission` | `session.permission.list` | Keep | Pending session permission list retained. | | [x] 099 | `GET` | `/api/session/{sessionID}/permission/{requestID}` | `session.permission.get` | Keep | Specific pending permission read with ownership validation retained. | | [x] 100 | `POST` | `/api/session/{sessionID}/permission/{requestID}/reply` | `session.permission.reply` | Change | Renamed request field from `reply` to `decision`. | -| [ ] 101 | `PUT` | `/api/session/{sessionID}/permission/rules` | `session.permission.rules` | | | +| [x] 101 | — | — | — | Remove | Permission rules are updated through `session.update`. | ## Group 8: Filesystem, worktrees, and VCS diff --git a/packages/app/src/home/sessions/controller.tsx b/packages/app/src/home/sessions/controller.tsx index a6dde844ea6..b8810144b7f 100644 --- a/packages/app/src/home/sessions/controller.tsx +++ b/packages/app/src/home/sessions/controller.tsx @@ -146,7 +146,7 @@ export function createHomeSessionsController(home: HomeController) { const next = title.trim() if (!next || next === sessionLabel(session)) return true return ctx.sdk.api.session - .rename({ sessionID: session.id, title: next }) + .update({ sessionID: session.id, title: next }) .then(() => { ctx.data.session.remember({ ...(ctx.data.session.get(session.id) ?? session), title: next }) // Rename advances time.updated server-side; re-sync the canonical diff --git a/packages/app/src/session/timeline/controller.tsx b/packages/app/src/session/timeline/controller.tsx index a588104027b..5de6a60adf9 100644 --- a/packages/app/src/session/timeline/controller.tsx +++ b/packages/app/src/session/timeline/controller.tsx @@ -149,7 +149,7 @@ export function createTimelineController(input: { session: TimelineSessionSource if (!next || next === (titleLabel() ?? "")) return true setPending("rename", true) const success = await serverSDK.api.session - .rename({ sessionID: id, title: next }) + .update({ sessionID: id, title: next }) .then(() => true) .catch((error) => { showToast({ title: language.t("common.requestFailed"), description: errorMessage(error) }) diff --git a/packages/app/src/shell/titlebar/tab-strip.tsx b/packages/app/src/shell/titlebar/tab-strip.tsx index 75ae584ace3..28ca9c504e6 100644 --- a/packages/app/src/shell/titlebar/tab-strip.tsx +++ b/packages/app/src/shell/titlebar/tab-strip.tsx @@ -116,7 +116,7 @@ function SessionTabEntry(props: { ctx.data.session.remember({ ...value, title }) try { - await ctx.sdk.api.session.rename({ sessionID: value.id, title }) + await ctx.sdk.api.session.update({ sessionID: value.id, title }) } catch (err) { const current = session() const currentCtx = props.serverCtx diff --git a/packages/cli/src/run/run.ts b/packages/cli/src/run/run.ts index db70d209abc..e6b3a906ddf 100644 --- a/packages/cli/src/run/run.ts +++ b/packages/cli/src/run/run.ts @@ -128,7 +128,7 @@ async function execute(input: RunCommandInput, prepared: Prepared, endpoint: End const model = target.model ? { providerID: target.model.providerID, modelID: target.model.id } : undefined const variant = target.model?.variant if (!target.resume && input.title !== undefined) { - await client.session.rename({ + await client.session.update({ sessionID: target.session.id, title: input.title || prepared.message.slice(0, 50) + (prepared.message.length > 50 ? "..." : ""), }) diff --git a/packages/client/src/effect/api/api.ts b/packages/client/src/effect/api/api.ts index 3eac1e5a1c1..32a9a2ab5ab 100644 --- a/packages/client/src/effect/api/api.ts +++ b/packages/client/src/effect/api/api.ts @@ -233,9 +233,13 @@ export type SessionSwitchModelOperation = ( input: SessionSwitchModelInput, ) => Effect.Effect -export type SessionRenameInput = { readonly sessionID: Session.ID; readonly title: string } -export type SessionRenameOutput = void -export type SessionRenameOperation = (input: SessionRenameInput) => Effect.Effect +export type SessionUpdateInput = { + readonly sessionID: Session.ID + readonly title?: string | undefined + readonly permissions?: Permission.Ruleset | undefined +} +export type SessionUpdateOutput = void +export type SessionUpdateOperation = (input: SessionUpdateInput) => Effect.Effect export type SessionMoveInput = { readonly sessionID: Session.ID @@ -1386,7 +1390,7 @@ export interface SessionApi { readonly fork: SessionForkOperation readonly switchAgent: SessionSwitchAgentOperation readonly switchModel: SessionSwitchModelOperation - readonly rename: SessionRenameOperation + readonly update: SessionUpdateOperation readonly move: SessionMoveOperation readonly prompt: SessionPromptOperation readonly command: SessionCommandOperation @@ -1789,12 +1793,6 @@ export type PermissionReplyOperation = ( input: PermissionReplyInput, ) => Effect.Effect -export type PermissionRulesInput = { readonly sessionID: Session.ID; readonly permissions: Permission.Ruleset } -export type PermissionRulesOutput = void -export type PermissionRulesOperation = ( - input: PermissionRulesInput, -) => Effect.Effect - export interface PermissionApi { readonly request: { readonly list: PermissionRequestListOperation } readonly saved: { readonly list: PermissionSavedListOperation; readonly remove: PermissionSavedRemoveOperation } @@ -1802,7 +1800,6 @@ export interface PermissionApi { readonly list: PermissionListOperation readonly get: PermissionGetOperation readonly reply: PermissionReplyOperation - readonly rules: PermissionRulesOperation } export type FileListInput = { diff --git a/packages/client/src/effect/generated/client.ts b/packages/client/src/effect/generated/client.ts index fe838b4a723..b661e486c6a 100644 --- a/packages/client/src/effect/generated/client.ts +++ b/packages/client/src/effect/generated/client.ts @@ -39,8 +39,8 @@ import type { SessionSwitchAgentOutput, SessionSwitchModelInput, SessionSwitchModelOutput, - SessionRenameInput, - SessionRenameOutput, + SessionUpdateInput, + SessionUpdateOutput, SessionMoveInput, SessionMoveOutput, SessionPromptInput, @@ -174,8 +174,6 @@ import type { PermissionGetOutput, PermissionReplyInput, PermissionReplyOutput, - PermissionRulesInput, - PermissionRulesOutput, FileListInput, FileListOutput, FileFindInput, @@ -446,11 +444,12 @@ const EndpointSessionSwitchModel = (raw: RawClient["server.session"]) => (input: ), ) -const EndpointSessionRename = (raw: RawClient["server.session"]) => (input: SessionRenameInput) => - preserveEffect()( - raw["session.rename"]({ params: { sessionID: input["sessionID"] }, payload: { title: input["title"] } }).pipe( - Effect.mapError(mapClientError), - ), +const EndpointSessionUpdate = (raw: RawClient["server.session"]) => (input: SessionUpdateInput) => + preserveEffect()( + raw["session.update"]({ + params: { sessionID: input["sessionID"] }, + payload: { title: input["title"], permissions: input["permissions"] }, + }).pipe(Effect.mapError(mapClientError)), ) const EndpointSessionMove = (raw: RawClient["server.session"]) => (input: SessionMoveInput) => @@ -745,7 +744,7 @@ const adaptGroupSession = (raw: RawClient["server.session"]) => ({ fork: EndpointSessionFork(raw), switchAgent: EndpointSessionSwitchAgent(raw), switchModel: EndpointSessionSwitchModel(raw), - rename: EndpointSessionRename(raw), + update: EndpointSessionUpdate(raw), move: EndpointSessionMove(raw), prompt: EndpointSessionPrompt(raw), command: EndpointSessionCommand(raw), @@ -1112,14 +1111,6 @@ const EndpointPermissionReply = (raw: RawClient["server.permission"]) => (input: }).pipe(Effect.mapError(mapClientError)), ) -const EndpointPermissionRules = (raw: RawClient["server.permission"]) => (input: PermissionRulesInput) => - preserveEffect()( - raw["session.permission.rules"]({ - params: { sessionID: input["sessionID"] }, - payload: { permissions: input["permissions"] }, - }).pipe(Effect.mapError(mapClientError)), - ) - const adaptGroupPermission = (raw: RawClient["server.permission"]) => ({ request: { list: EndpointPermissionRequestList(raw) }, saved: { list: EndpointPermissionSavedList(raw), remove: EndpointPermissionSavedRemove(raw) }, @@ -1127,7 +1118,6 @@ const adaptGroupPermission = (raw: RawClient["server.permission"]) => ({ list: EndpointPermissionList(raw), get: EndpointPermissionGet(raw), reply: EndpointPermissionReply(raw), - rules: EndpointPermissionRules(raw), }) const EndpointFileList = (raw: RawClient["server.fs"]) => (input?: FileListInput) => diff --git a/packages/client/src/promise/generated/client.ts b/packages/client/src/promise/generated/client.ts index 8069434a6fa..416b3ab7b09 100644 --- a/packages/client/src/promise/generated/client.ts +++ b/packages/client/src/promise/generated/client.ts @@ -33,8 +33,8 @@ import type { SessionSwitchAgentOutput, SessionSwitchModelInput, SessionSwitchModelOutput, - SessionRenameInput, - SessionRenameOutput, + SessionUpdateInput, + SessionUpdateOutput, SessionMoveInput, SessionMoveOutput, SessionPromptInput, @@ -168,8 +168,6 @@ import type { PermissionGetOutput, PermissionReplyInput, PermissionReplyOutput, - PermissionRulesInput, - PermissionRulesOutput, FileReadInput, FileReadOutput, FileListInput, @@ -640,12 +638,12 @@ export function make(options: ClientOptions) { }, requestOptions, ), - rename: (input: SessionRenameInput, requestOptions?: RequestOptions) => - request( + update: (input: SessionUpdateInput, requestOptions?: RequestOptions) => + request( { method: "PATCH", path: `/api/session/${encodeURIComponent(input.sessionID)}`, - body: { title: input["title"] }, + body: { title: input["title"], permissions: input["permissions"] }, successStatus: 204, declaredStatuses: [400, 401, 404], empty: true, @@ -1519,18 +1517,6 @@ export function make(options: ClientOptions) { }, requestOptions, ), - rules: (input: PermissionRulesInput, requestOptions?: RequestOptions) => - request( - { - method: "PUT", - path: `/api/session/${encodeURIComponent(input.sessionID)}/permission/rules`, - body: { permissions: input["permissions"] }, - successStatus: 204, - declaredStatuses: [400, 401, 404], - empty: true, - }, - requestOptions, - ), }, file: { read: (input: FileReadInput, requestOptions?: RequestOptions) => diff --git a/packages/client/src/promise/generated/types.ts b/packages/client/src/promise/generated/types.ts index b789e5c804c..6ad55c09908 100644 --- a/packages/client/src/promise/generated/types.ts +++ b/packages/client/src/promise/generated/types.ts @@ -3903,12 +3903,23 @@ export type SessionSwitchModelInput = { export type SessionSwitchModelOutput = void -export type SessionRenameInput = { +export type SessionUpdateInput = { readonly sessionID: { readonly sessionID: string }["sessionID"] - readonly title: { readonly title: string }["title"] + readonly title?: { + readonly title?: string | undefined + readonly permissions?: + | ReadonlyArray<{ readonly action: string; readonly resource: string; readonly effect: "allow" | "deny" | "ask" }> + | undefined + }["title"] + readonly permissions?: { + readonly title?: string | undefined + readonly permissions?: + | ReadonlyArray<{ readonly action: string; readonly resource: string; readonly effect: "allow" | "deny" | "ask" }> + | undefined + }["permissions"] } -export type SessionRenameOutput = void +export type SessionUpdateOutput = void export type SessionMoveInput = { readonly sessionID: { readonly sessionID: string }["sessionID"] @@ -5721,19 +5732,6 @@ export type PermissionReplyInput = { export type PermissionReplyOutput = void -export type PermissionRulesInput = { - readonly sessionID: { readonly sessionID: string }["sessionID"] - readonly permissions: { - readonly permissions: ReadonlyArray<{ - readonly action: string - readonly resource: string - readonly effect: "allow" | "deny" | "ask" - }> - }["permissions"] -} - -export type PermissionRulesOutput = void - export type FileReadInput = { readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"] readonly path: string diff --git a/packages/core/src/plugin/host.ts b/packages/core/src/plugin/host.ts index a5bfac8df5a..8d5d244c7da 100644 --- a/packages/core/src/plugin/host.ts +++ b/packages/core/src/plugin/host.ts @@ -415,7 +415,6 @@ export const make = Effect.fn("PluginHost.make")(function* ( : Effect.fail(new Error(`Permission request not found: ${input.requestID}`)), ), ), - rules: sessions.setPermissions, }, plugin: { list: () => response(plugin.list()), @@ -532,7 +531,12 @@ export const make = Effect.fn("PluginHost.make")(function* ( prompt: sessions.prompt, generate: (input) => sessions.generate(input).pipe(Effect.map((text) => ({ text }))), command: (input) => sessions.command({ ...input, command: input.name }), - rename: sessions.rename, + update: Effect.fn(function* (input) { + yield* sessions.get(input.sessionID) + if (input.title !== undefined) yield* sessions.rename({ sessionID: input.sessionID, title: input.title }) + if (input.permissions !== undefined) + yield* sessions.setPermissions({ sessionID: input.sessionID, permissions: input.permissions }) + }), move: sessions.move, synthetic: sessions.synthetic, interrupt: (input) => diff --git a/packages/core/src/tool/plugin/opencode.ts b/packages/core/src/tool/plugin/opencode.ts index 729bf173624..639cd778f1a 100644 --- a/packages/core/src/tool/plugin/opencode.ts +++ b/packages/core/src/tool/plugin/opencode.ts @@ -51,7 +51,7 @@ export const Plugin = { const sessionID = input.sessionID ?? context.sessionID const title = input.title.trim() if (!title) return Effect.fail(new ToolFailure({ message: "Session title must not be empty" })) - return ctx.session.rename({ sessionID, title }).pipe( + return ctx.session.update({ sessionID, title }).pipe( Effect.as({ output: { sessionID, title }, content: `Renamed session ${sessionID} to ${title}.`, diff --git a/packages/plugin/src/effect/permission.ts b/packages/plugin/src/effect/permission.ts index 086ba994bf2..e97fbf3ebe5 100644 --- a/packages/plugin/src/effect/permission.ts +++ b/packages/plugin/src/effect/permission.ts @@ -19,6 +19,6 @@ export interface PermissionHooks { readonly evaluate: PermissionEvaluation } -export type PermissionDomain = Pick, "list" | "get" | "reply" | "rules"> & { +export type PermissionDomain = Pick, "list" | "get" | "reply"> & { readonly hook: Hooks } diff --git a/packages/plugin/src/effect/session.ts b/packages/plugin/src/effect/session.ts index bedc41a0736..9e125a1c885 100644 --- a/packages/plugin/src/effect/session.ts +++ b/packages/plugin/src/effect/session.ts @@ -134,7 +134,7 @@ export type SessionDomain = Pick< | "command" | "synthetic" | "interrupt" - | "rename" + | "update" | "move" | "wait" | "context" diff --git a/packages/plugin/src/promise/adapter.ts b/packages/plugin/src/promise/adapter.ts index 95f840bf0cc..766fff446ef 100644 --- a/packages/plugin/src/promise/adapter.ts +++ b/packages/plugin/src/promise/adapter.ts @@ -439,7 +439,6 @@ export function fromPromise(plugin: Plugin) { list: adaptApiMethod(PermissionEndpoints["session.permission.list"], host.permission.list), get: adaptApiMethod(PermissionEndpoints["session.permission.get"], host.permission.get), reply: adaptApiMethod(PermissionEndpoints["session.permission.reply"], host.permission.reply), - rules: adaptApiMethod(PermissionEndpoints["session.permission.rules"], host.permission.rules), }, plugin: { list: adaptApiMethod(PluginEndpoints["plugin.list"], host.plugin.list), @@ -581,7 +580,7 @@ export function fromPromise(plugin: Plugin) { command: adaptApiMethod(SessionEndpoints["session.command"], host.session.command), synthetic: adaptApiMethod(SessionEndpoints["session.synthetic"], host.session.synthetic), interrupt: adaptApiMethod(SessionEndpoints["session.interrupt"], host.session.interrupt), - rename: adaptApiMethod(SessionEndpoints["session.rename"], host.session.rename), + update: adaptApiMethod(SessionEndpoints["session.update"], host.session.update), move: adaptApiMethod(SessionEndpoints["session.move"], host.session.move), wait: adaptApiMethod(SessionEndpoints["session.wait"], host.session.wait), context: adaptApiMethod(SessionEndpoints["session.context"], host.session.context), diff --git a/packages/plugin/src/promise/permission.ts b/packages/plugin/src/promise/permission.ts index daf920f5f5c..1729cac29b5 100644 --- a/packages/plugin/src/promise/permission.ts +++ b/packages/plugin/src/promise/permission.ts @@ -19,6 +19,6 @@ export interface PermissionHooks { readonly evaluate: PermissionEvaluation } -export type PermissionDomain = Pick & { +export type PermissionDomain = Pick & { readonly hook: Hooks } diff --git a/packages/plugin/src/promise/session.ts b/packages/plugin/src/promise/session.ts index fd25decf3bb..a3187246605 100644 --- a/packages/plugin/src/promise/session.ts +++ b/packages/plugin/src/promise/session.ts @@ -134,7 +134,7 @@ export type SessionDomain = Pick< | "command" | "synthetic" | "interrupt" - | "rename" + | "update" | "move" | "wait" | "context" diff --git a/packages/protocol/openapi.json b/packages/protocol/openapi.json index 630ff83a66d..cfe4ae20994 100644 --- a/packages/protocol/openapi.json +++ b/packages/protocol/openapi.json @@ -1438,7 +1438,7 @@ }, "patch": { "tags": ["session"], - "operationId": "session.rename", + "operationId": "session.update", "parameters": [ { "name": "sessionID", @@ -1493,8 +1493,8 @@ } } }, - "description": "Update the session title.", - "summary": "Rename session", + "description": "Update mutable session properties.", + "summary": "Update session", "requestBody": { "content": { "application/json": { @@ -1502,10 +1502,26 @@ "type": "object", "properties": { "title": { - "type": "string" + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "permissions": { + "anyOf": [ + { + "$ref": "#/components/schemas/Permission.Ruleset" + }, + { + "type": "null" + } + ] } }, - "required": ["title"], "additionalProperties": false } } @@ -8075,85 +8091,6 @@ } } }, - "/api/session/{sessionID}/permission/rules": { - "put": { - "tags": ["permission"], - "operationId": "session.permission.rules", - "parameters": [ - { - "name": "sessionID", - "in": "path", - "schema": { - "type": "string", - "pattern": "^ses" - }, - "required": true - } - ], - "security": [], - "responses": { - "204": { - "description": "" - }, - "400": { - "description": "InvalidRequestError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequestErrorEncoded" - } - } - } - }, - "401": { - "description": "UnauthorizedError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UnauthorizedErrorEncoded" - } - } - } - }, - "404": { - "description": "SessionNotFoundError", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" - }, - { - "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" - } - ] - } - } - } - } - }, - "description": "Replace the session-scoped permission rules. Rules are evaluated after the agent's rules, and the last matching rule wins.", - "summary": "Replace session permission rules", - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "permissions": { - "$ref": "#/components/schemas/Permission.Ruleset" - } - }, - "required": ["permissions"], - "additionalProperties": false - } - } - }, - "required": true - } - } - }, "/api/fs/read/*": { "get": { "tags": ["filesystem"], diff --git a/packages/protocol/src/groups/permission.ts b/packages/protocol/src/groups/permission.ts index bd2d6f05a23..b35a71c8f9c 100644 --- a/packages/protocol/src/groups/permission.ts +++ b/packages/protocol/src/groups/permission.ts @@ -132,21 +132,4 @@ export const makePermissionGroup = < }), ), ) - .add( - HttpApiEndpoint.put("session.permission.rules", "/api/session/:sessionID/permission/rules", { - params: { sessionID: Session.ID }, - payload: Schema.Struct({ permissions: Permission.Ruleset }), - success: HttpApiSchema.NoContent, - error: SessionNotFoundError, - }) - .middleware(sessionLocationMiddleware) - .annotateMerge( - OpenApi.annotations({ - identifier: "session.permission.rules", - summary: "Replace session permission rules", - description: - "Replace the session-scoped permission rules. Rules are evaluated after the agent's rules, and the last matching rule wins.", - }), - ), - ) .annotateMerge(OpenApi.annotations({ title: "permission", description: "Experimental permission routes." })) diff --git a/packages/protocol/src/groups/session.ts b/packages/protocol/src/groups/session.ts index 2b16ec51b79..dae63ffe64b 100644 --- a/packages/protocol/src/groups/session.ts +++ b/packages/protocol/src/groups/session.ts @@ -355,18 +355,21 @@ export const makeSessionGroup = < ), ) .add( - HttpApiEndpoint.patch("session.rename", "/api/session/:sessionID", { + HttpApiEndpoint.patch("session.update", "/api/session/:sessionID", { params: { sessionID: Session.ID }, - payload: Schema.Struct({ title: Schema.String }), + payload: Schema.Struct({ + title: Schema.String.pipe(Schema.optional), + permissions: Permission.Ruleset.pipe(Schema.optional), + }), success: HttpApiSchema.NoContent, error: SessionNotFoundError, }) .middleware(sessionLocationMiddleware) .annotateMerge( OpenApi.annotations({ - identifier: "session.rename", - summary: "Rename session", - description: "Update the session title.", + identifier: "session.update", + summary: "Update session", + description: "Update mutable session properties.", }), ), ) diff --git a/packages/sdk/test/instances-effect.test.ts b/packages/sdk/test/instances-effect.test.ts index 842e78adc87..a6d4a82bbf0 100644 --- a/packages/sdk/test/instances-effect.test.ts +++ b/packages/sdk/test/instances-effect.test.ts @@ -83,7 +83,7 @@ it.live( Effect.gen(function* () { executed.push(generation) yield* ctx.session - .rename({ sessionID: tool.sessionID, title: `${key}:${generation}` }) + .update({ sessionID: tool.sessionID, title: `${key}:${generation}` }) .pipe(Effect.orDie) return { output: key, content: key } }), diff --git a/packages/server/src/handlers/permission.ts b/packages/server/src/handlers/permission.ts index 401b5059dcd..cec7bdb9bd9 100644 --- a/packages/server/src/handlers/permission.ts +++ b/packages/server/src/handlers/permission.ts @@ -83,15 +83,6 @@ export const PermissionHandler = HttpApiBuilder.group(Api, "server.permission", return HttpApiSchema.NoContent.make() }), ) - .handle( - "session.permission.rules", - Effect.fn(function* (ctx) { - yield* sessions - .setPermissions({ sessionID: ctx.params.sessionID, permissions: ctx.payload.permissions }) - .pipe(Effect.catchTag("Session.NotFoundError", missingSession)) - return HttpApiSchema.NoContent.make() - }), - ) .handle( "permission.saved.list", Effect.fn(function* (ctx) { diff --git a/packages/server/src/handlers/session.ts b/packages/server/src/handlers/session.ts index 346a2602a53..c0d819dd728 100644 --- a/packages/server/src/handlers/session.ts +++ b/packages/server/src/handlers/session.ts @@ -253,16 +253,23 @@ export const SessionHandler = HttpApiBuilder.group(Api, "server.session", (handl }), ) .handle( - "session.rename", + "session.update", Effect.fn(function* (ctx) { - if (ctx.payload.title) { - yield* session - .rename({ sessionID: ctx.params.sessionID, title: ctx.payload.title }) - .pipe(Effect.catchTag("Session.NotFoundError", missingSession)) - return HttpApiSchema.NoContent.make() + yield* session.get(ctx.params.sessionID).pipe(Effect.catchTag("Session.NotFoundError", missingSession)) + if (ctx.payload.title !== undefined) { + if (ctx.payload.title) { + yield* session + .rename({ sessionID: ctx.params.sessionID, title: ctx.payload.title }) + .pipe(Effect.catchTag("Session.NotFoundError", missingSession)) + } else { + const title = yield* SessionTitle.Service + yield* title.generate(ctx.params.sessionID) + } } - const title = yield* SessionTitle.Service - yield* title.generate(ctx.params.sessionID) + if (ctx.payload.permissions !== undefined) + yield* session + .setPermissions({ sessionID: ctx.params.sessionID, permissions: ctx.payload.permissions }) + .pipe(Effect.catchTag("Session.NotFoundError", missingSession)) return HttpApiSchema.NoContent.make() }), ) diff --git a/packages/tui/src/component/dialog-session-rename.tsx b/packages/tui/src/component/dialog-session-rename.tsx index 034bc4bcbbb..eff40d5a3bc 100644 --- a/packages/tui/src/component/dialog-session-rename.tsx +++ b/packages/tui/src/component/dialog-session-rename.tsx @@ -18,7 +18,7 @@ export function DialogSessionRename(props: { sessionID: string; currentTitle?: s const title = value.trim() if (!title) return void client.api.session - .rename({ sessionID: props.sessionID, title }) + .update({ sessionID: props.sessionID, title }) .then(() => dialog.clear()) .catch((error) => toast.show({ diff --git a/packages/tui/src/context/data.tsx b/packages/tui/src/context/data.tsx index 3fbff477513..d773547c140 100644 --- a/packages/tui/src/context/data.tsx +++ b/packages/tui/src/context/data.tsx @@ -37,7 +37,7 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({ if (generatingTitles[sessionID]) return setGeneratingTitles(sessionID, true) await client.api.session - .rename({ sessionID, title: "" }) + .update({ sessionID, title: "" }) .then(() => { // The HTTP response can beat the renamed event. Keep pending until the new title is projected locally. data.session.invalidate(sessionID) diff --git a/packages/tui/src/routes/session/index.tsx b/packages/tui/src/routes/session/index.tsx index f71b59dc682..9e251742ed9 100644 --- a/packages/tui/src/routes/session/index.tsx +++ b/packages/tui/src/routes/session/index.tsx @@ -821,7 +821,7 @@ export function Session(props: { const title = input.trim() void ( title - ? client.api.session.rename({ sessionID: route.sessionID, title }) + ? client.api.session.update({ sessionID: route.sessionID, title }) : data.session.title.generate(route.sessionID) ).catch((error) => toast.error(error)) }, diff --git a/services/www/openapi.json b/services/www/openapi.json index 630ff83a66d..cfe4ae20994 100644 --- a/services/www/openapi.json +++ b/services/www/openapi.json @@ -1438,7 +1438,7 @@ }, "patch": { "tags": ["session"], - "operationId": "session.rename", + "operationId": "session.update", "parameters": [ { "name": "sessionID", @@ -1493,8 +1493,8 @@ } } }, - "description": "Update the session title.", - "summary": "Rename session", + "description": "Update mutable session properties.", + "summary": "Update session", "requestBody": { "content": { "application/json": { @@ -1502,10 +1502,26 @@ "type": "object", "properties": { "title": { - "type": "string" + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "permissions": { + "anyOf": [ + { + "$ref": "#/components/schemas/Permission.Ruleset" + }, + { + "type": "null" + } + ] } }, - "required": ["title"], "additionalProperties": false } } @@ -8075,85 +8091,6 @@ } } }, - "/api/session/{sessionID}/permission/rules": { - "put": { - "tags": ["permission"], - "operationId": "session.permission.rules", - "parameters": [ - { - "name": "sessionID", - "in": "path", - "schema": { - "type": "string", - "pattern": "^ses" - }, - "required": true - } - ], - "security": [], - "responses": { - "204": { - "description": "" - }, - "400": { - "description": "InvalidRequestError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequestErrorEncoded" - } - } - } - }, - "401": { - "description": "UnauthorizedError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UnauthorizedErrorEncoded" - } - } - } - }, - "404": { - "description": "SessionNotFoundError", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" - }, - { - "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" - } - ] - } - } - } - } - }, - "description": "Replace the session-scoped permission rules. Rules are evaluated after the agent's rules, and the last matching rule wins.", - "summary": "Replace session permission rules", - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "permissions": { - "$ref": "#/components/schemas/Permission.Ruleset" - } - }, - "required": ["permissions"], - "additionalProperties": false - } - } - }, - "required": true - } - } - }, "/api/fs/read/*": { "get": { "tags": ["filesystem"], diff --git a/services/www/public/openapi.json b/services/www/public/openapi.json index 630ff83a66d..cfe4ae20994 100644 --- a/services/www/public/openapi.json +++ b/services/www/public/openapi.json @@ -1438,7 +1438,7 @@ }, "patch": { "tags": ["session"], - "operationId": "session.rename", + "operationId": "session.update", "parameters": [ { "name": "sessionID", @@ -1493,8 +1493,8 @@ } } }, - "description": "Update the session title.", - "summary": "Rename session", + "description": "Update mutable session properties.", + "summary": "Update session", "requestBody": { "content": { "application/json": { @@ -1502,10 +1502,26 @@ "type": "object", "properties": { "title": { - "type": "string" + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "permissions": { + "anyOf": [ + { + "$ref": "#/components/schemas/Permission.Ruleset" + }, + { + "type": "null" + } + ] } }, - "required": ["title"], "additionalProperties": false } } @@ -8075,85 +8091,6 @@ } } }, - "/api/session/{sessionID}/permission/rules": { - "put": { - "tags": ["permission"], - "operationId": "session.permission.rules", - "parameters": [ - { - "name": "sessionID", - "in": "path", - "schema": { - "type": "string", - "pattern": "^ses" - }, - "required": true - } - ], - "security": [], - "responses": { - "204": { - "description": "" - }, - "400": { - "description": "InvalidRequestError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequestErrorEncoded" - } - } - } - }, - "401": { - "description": "UnauthorizedError", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UnauthorizedErrorEncoded" - } - } - } - }, - "404": { - "description": "SessionNotFoundError", - "content": { - "application/json": { - "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" - }, - { - "$ref": "#/components/schemas/SessionNotFoundErrorEncoded" - } - ] - } - } - } - } - }, - "description": "Replace the session-scoped permission rules. Rules are evaluated after the agent's rules, and the last matching rule wins.", - "summary": "Replace session permission rules", - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "permissions": { - "$ref": "#/components/schemas/Permission.Ruleset" - } - }, - "required": ["permissions"], - "additionalProperties": false - } - } - }, - "required": true - } - } - }, "/api/fs/read/*": { "get": { "tags": ["filesystem"],