Explorar o código

wip: refactor permissions

Dax Raad hai 6 meses
pai
achega
a5b20f973f

+ 4 - 0
opencode.json

@@ -28,5 +28,9 @@
       "type": "local",
       "command": ["opencode", "x", "@h1deya/mcp-server-weather"]
     }
+  },
+  "permission": {
+    "edit": "ask",
+    "bash": "ask"
   }
 }

+ 1 - 0
packages/opencode/src/id/id.ts

@@ -5,6 +5,7 @@ export namespace Identifier {
   const prefixes = {
     session: "ses",
     message: "msg",
+    permission: "per",
     user: "usr",
     part: "prt",
   } as const

+ 16 - 27
packages/opencode/src/permission/index.ts

@@ -3,6 +3,7 @@ import { z } from "zod"
 import { Bus } from "../bus"
 import { Log } from "../util/log"
 import { Installation } from "../installation"
+import { Identifier } from "../id/id"
 
 export namespace Permission {
   const log = Log.create({ service: "permission" })
@@ -10,9 +11,11 @@ export namespace Permission {
   export const Info = z
     .object({
       id: z.string(),
+      type: z.string(),
+      pattern: z.string().optional(),
       sessionID: z.string(),
       messageID: z.string(),
-      toolCallID: z.string().optional(),
+      callID: z.string().optional(),
       title: z.string(),
       metadata: z.record(z.any()),
       time: z.object({
@@ -55,18 +58,19 @@ export namespace Permission {
     async (state) => {
       for (const pending of Object.values(state.pending)) {
         for (const item of Object.values(pending)) {
-          item.reject(new RejectedError(item.info.sessionID, item.info.id, item.info.toolCallID))
+          item.reject(new RejectedError(item.info.sessionID, item.info.id, item.info.callID))
         }
       }
     },
   )
 
   export function ask(input: {
-    id: Info["id"]
+    type: Info["type"]
+    title: Info["title"]
+    pattern?: Info["pattern"]
+    callID?: Info["callID"]
     sessionID: Info["sessionID"]
     messageID: Info["messageID"]
-    toolCallID?: Info["toolCallID"]
-    title: Info["title"]
     metadata: Info["metadata"]
   }) {
     // TODO: dax, remove this when you're happy with permissions
@@ -75,24 +79,16 @@ export namespace Permission {
     const { pending, approved } = state()
     log.info("asking", {
       sessionID: input.sessionID,
-      permissionID: input.id,
       messageID: input.messageID,
-      toolCallID: input.toolCallID,
+      toolCallID: input.callID,
     })
-    if (approved[input.sessionID]?.[input.id]) {
-      log.info("previously approved", {
-        sessionID: input.sessionID,
-        permissionID: input.id,
-        messageID: input.messageID,
-        toolCallID: input.toolCallID,
-      })
-      return
-    }
+    if (approved[input.sessionID]?.[input.pattern ?? input.type]) return
     const info: Info = {
-      id: input.id,
+      id: Identifier.ascending("permission"),
+      type: input.type,
       sessionID: input.sessionID,
       messageID: input.messageID,
-      toolCallID: input.toolCallID,
+      callID: input.callID,
       title: input.title,
       metadata: input.metadata,
       time: {
@@ -101,18 +97,11 @@ export namespace Permission {
     }
     pending[input.sessionID] = pending[input.sessionID] || {}
     return new Promise<void>((resolve, reject) => {
-      pending[input.sessionID][input.id] = {
+      pending[input.sessionID][info.id] = {
         info,
         resolve,
         reject,
       }
-      // setTimeout(() => {
-      //   respond({
-      //     sessionID: input.sessionID,
-      //     permissionID: input.id,
-      //     response: "always",
-      //   })
-      // }, 1000)
       Bus.publish(Event.Updated, info)
     })
   }
@@ -127,7 +116,7 @@ export namespace Permission {
     if (!match) return
     delete pending[input.sessionID][input.permissionID]
     if (input.response === "reject") {
-      match.reject(new RejectedError(input.sessionID, input.permissionID, match.info.toolCallID))
+      match.reject(new RejectedError(input.sessionID, input.permissionID, match.info.callID))
       return
     }
     match.resolve()

+ 3 - 2
packages/opencode/src/tool/bash.ts

@@ -108,10 +108,11 @@ export const BashTool = Tool.define("bash", {
     const cfg = await Config.get()
     if (cfg.permission?.bash === "ask")
       await Permission.ask({
-        id: "bash",
+        type: "bash",
+        pattern: params.command.split(" ").slice(0, 2).join(" ").trim(),
         sessionID: ctx.sessionID,
         messageID: ctx.messageID,
-        toolCallID: ctx.toolCallID,
+        callID: ctx.toolCallID,
         title: "Run this command: " + params.command,
         metadata: {
           command: params.command,

+ 4 - 4
packages/opencode/src/tool/edit.ts

@@ -50,10 +50,10 @@ export const EditTool = Tool.define("edit", {
         diff = trimDiff(createTwoFilesPatch(filePath, filePath, contentOld, contentNew))
         if (cfg.permission?.edit === "ask") {
           await Permission.ask({
-            id: "edit",
+            type: "edit",
             sessionID: ctx.sessionID,
             messageID: ctx.messageID,
-            toolCallID: ctx.toolCallID,
+            callID: ctx.toolCallID,
             title: "Edit this file: " + filePath,
             metadata: {
               filePath,
@@ -79,10 +79,10 @@ export const EditTool = Tool.define("edit", {
       diff = trimDiff(createTwoFilesPatch(filePath, filePath, contentOld, contentNew))
       if (cfg.permission?.edit === "ask") {
         await Permission.ask({
-          id: "edit",
+          type: "edit",
           sessionID: ctx.sessionID,
           messageID: ctx.messageID,
-          toolCallID: ctx.toolCallID,
+          callID: ctx.toolCallID,
           title: "Edit this file: " + filePath,
           metadata: {
             filePath,

+ 2 - 2
packages/opencode/src/tool/write.ts

@@ -31,10 +31,10 @@ export const WriteTool = Tool.define("write", {
     const cfg = await Config.get()
     if (cfg.permission?.edit === "ask")
       await Permission.ask({
-        id: "write",
+        type: "write",
         sessionID: ctx.sessionID,
         messageID: ctx.messageID,
-        toolCallID: ctx.toolCallID,
+        callID: ctx.toolCallID,
         title: exists ? "Overwrite this file: " + filepath : "Create new file: " + filepath,
         metadata: {
           filePath: filepath,

+ 4 - 4
packages/sdk/go/.stats.yml

@@ -1,4 +1,4 @@
-configured_endpoints: 26
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/opencode%2Fopencode-5bf6a39123d248d306490c1dee61b46ba113ea2c415a4de1a631c76462769c49.yml
-openapi_spec_hash: 3c5b25f121429281275ffd70c9d5cfe4
-config_hash: 1ae82c93499b9f0b9ba828b8919f9cb3
+configured_endpoints: 28
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/opencode%2Fopencode-3fa00e84a92784c0e12cf47a49cf5ac4eb5556b5b3ad8769ad7b4e7e1bf1b01a.yml
+openapi_spec_hash: 5f98ce812d7feb00e6c2eb7a15dd8887
+config_hash: 7707d73ebbd7ad7042ab70466b39348d

+ 12 - 0
packages/sdk/go/api.md

@@ -103,6 +103,7 @@ Response Types:
 - <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#ToolStatePending">ToolStatePending</a>
 - <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#ToolStateRunning">ToolStateRunning</a>
 - <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#UserMessage">UserMessage</a>
+- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionMessageResponse">SessionMessageResponse</a>
 - <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionMessagesResponse">SessionMessagesResponse</a>
 
 Methods:
@@ -113,6 +114,7 @@ Methods:
 - <code title="post /session/{id}/abort">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.Abort">Abort</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</a>) (<a href="https://pkg.go.dev/builtin#bool">bool</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
 - <code title="post /session/{id}/message">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.Chat">Chat</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</a>, body <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionChatParams">SessionChatParams</a>) (<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#AssistantMessage">AssistantMessage</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
 - <code title="post /session/{id}/init">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.Init">Init</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</a>, body <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionInitParams">SessionInitParams</a>) (<a href="https://pkg.go.dev/builtin#bool">bool</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
+- <code title="get /session/{id}/message/{messageID}">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.Message">Message</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</a>, messageID <a href="https://pkg.go.dev/builtin#string">string</a>) (<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionMessageResponse">SessionMessageResponse</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
 - <code title="get /session/{id}/message">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.Messages">Messages</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</a>) ([]<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionMessagesResponse">SessionMessagesResponse</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
 - <code title="post /session/{id}/revert">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.Revert">Revert</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</a>, body <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionRevertParams">SessionRevertParams</a>) (<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#Session">Session</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
 - <code title="post /session/{id}/share">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.Share">Share</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</a>) (<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#Session">Session</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
@@ -120,6 +122,16 @@ Methods:
 - <code title="post /session/{id}/unrevert">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.Unrevert">Unrevert</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</a>) (<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#Session">Session</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
 - <code title="delete /session/{id}/share">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.Unshare">Unshare</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</a>) (<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#Session">Session</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
 
+## Permissions
+
+Response Types:
+
+- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#Permission">Permission</a>
+
+Methods:
+
+- <code title="post /session/{id}/permissions/{permissionID}">client.Session.Permissions.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionPermissionService.Respond">Respond</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</a>, permissionID <a href="https://pkg.go.dev/builtin#string">string</a>, body <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionPermissionRespondParams">SessionPermissionRespondParams</a>) (<a href="https://pkg.go.dev/builtin#bool">bool</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
+
 # Tui
 
 Methods:

+ 4 - 55
packages/sdk/go/event.go

@@ -54,8 +54,7 @@ type EventListResponse struct {
 	// [EventListResponseEventMessageRemovedProperties],
 	// [EventListResponseEventMessagePartUpdatedProperties],
 	// [EventListResponseEventMessagePartRemovedProperties],
-	// [EventListResponseEventStorageWriteProperties],
-	// [EventListResponseEventPermissionUpdatedProperties],
+	// [EventListResponseEventStorageWriteProperties], [Permission],
 	// [EventListResponseEventFileEditedProperties],
 	// [EventListResponseEventSessionUpdatedProperties],
 	// [EventListResponseEventSessionDeletedProperties],
@@ -643,9 +642,9 @@ func (r EventListResponseEventStorageWriteType) IsKnown() bool {
 }
 
 type EventListResponseEventPermissionUpdated struct {
-	Properties EventListResponseEventPermissionUpdatedProperties `json:"properties,required"`
-	Type       EventListResponseEventPermissionUpdatedType       `json:"type,required"`
-	JSON       eventListResponseEventPermissionUpdatedJSON       `json:"-"`
+	Properties Permission                                  `json:"properties,required"`
+	Type       EventListResponseEventPermissionUpdatedType `json:"type,required"`
+	JSON       eventListResponseEventPermissionUpdatedJSON `json:"-"`
 }
 
 // eventListResponseEventPermissionUpdatedJSON contains the JSON metadata for the
@@ -667,56 +666,6 @@ func (r eventListResponseEventPermissionUpdatedJSON) RawJSON() string {
 
 func (r EventListResponseEventPermissionUpdated) implementsEventListResponse() {}
 
-type EventListResponseEventPermissionUpdatedProperties struct {
-	ID        string                                                `json:"id,required"`
-	Metadata  map[string]interface{}                                `json:"metadata,required"`
-	SessionID string                                                `json:"sessionID,required"`
-	Time      EventListResponseEventPermissionUpdatedPropertiesTime `json:"time,required"`
-	Title     string                                                `json:"title,required"`
-	JSON      eventListResponseEventPermissionUpdatedPropertiesJSON `json:"-"`
-}
-
-// eventListResponseEventPermissionUpdatedPropertiesJSON contains the JSON metadata
-// for the struct [EventListResponseEventPermissionUpdatedProperties]
-type eventListResponseEventPermissionUpdatedPropertiesJSON struct {
-	ID          apijson.Field
-	Metadata    apijson.Field
-	SessionID   apijson.Field
-	Time        apijson.Field
-	Title       apijson.Field
-	raw         string
-	ExtraFields map[string]apijson.Field
-}
-
-func (r *EventListResponseEventPermissionUpdatedProperties) UnmarshalJSON(data []byte) (err error) {
-	return apijson.UnmarshalRoot(data, r)
-}
-
-func (r eventListResponseEventPermissionUpdatedPropertiesJSON) RawJSON() string {
-	return r.raw
-}
-
-type EventListResponseEventPermissionUpdatedPropertiesTime struct {
-	Created float64                                                   `json:"created,required"`
-	JSON    eventListResponseEventPermissionUpdatedPropertiesTimeJSON `json:"-"`
-}
-
-// eventListResponseEventPermissionUpdatedPropertiesTimeJSON contains the JSON
-// metadata for the struct [EventListResponseEventPermissionUpdatedPropertiesTime]
-type eventListResponseEventPermissionUpdatedPropertiesTimeJSON struct {
-	Created     apijson.Field
-	raw         string
-	ExtraFields map[string]apijson.Field
-}
-
-func (r *EventListResponseEventPermissionUpdatedPropertiesTime) UnmarshalJSON(data []byte) (err error) {
-	return apijson.UnmarshalRoot(data, r)
-}
-
-func (r eventListResponseEventPermissionUpdatedPropertiesTimeJSON) RawJSON() string {
-	return r.raw
-}
-
 type EventListResponseEventPermissionUpdatedType string
 
 const (

+ 42 - 1
packages/sdk/go/session.go

@@ -24,7 +24,8 @@ import (
 // automatically. You should not instantiate this service directly, and instead use
 // the [NewSessionService] method instead.
 type SessionService struct {
-	Options []option.RequestOption
+	Options     []option.RequestOption
+	Permissions *SessionPermissionService
 }
 
 // NewSessionService generates a new service that applies the given options to each
@@ -33,6 +34,7 @@ type SessionService struct {
 func NewSessionService(opts ...option.RequestOption) (r *SessionService) {
 	r = &SessionService{}
 	r.Options = opts
+	r.Permissions = NewSessionPermissionService(opts...)
 	return
 }
 
@@ -100,6 +102,22 @@ func (r *SessionService) Init(ctx context.Context, id string, body SessionInitPa
 	return
 }
 
+// Get a message from a session
+func (r *SessionService) Message(ctx context.Context, id string, messageID string, opts ...option.RequestOption) (res *SessionMessageResponse, err error) {
+	opts = append(r.Options[:], opts...)
+	if id == "" {
+		err = errors.New("missing required id parameter")
+		return
+	}
+	if messageID == "" {
+		err = errors.New("missing required messageID parameter")
+		return
+	}
+	path := fmt.Sprintf("session/%s/message/%s", id, messageID)
+	err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, nil, &res, opts...)
+	return
+}
+
 // List messages for a session
 func (r *SessionService) Messages(ctx context.Context, id string, opts ...option.RequestOption) (res *[]SessionMessagesResponse, err error) {
 	opts = append(r.Options[:], opts...)
@@ -2012,6 +2030,29 @@ func (r userMessageTimeJSON) RawJSON() string {
 	return r.raw
 }
 
+type SessionMessageResponse struct {
+	Info  Message                    `json:"info,required"`
+	Parts []Part                     `json:"parts,required"`
+	JSON  sessionMessageResponseJSON `json:"-"`
+}
+
+// sessionMessageResponseJSON contains the JSON metadata for the struct
+// [SessionMessageResponse]
+type sessionMessageResponseJSON struct {
+	Info        apijson.Field
+	Parts       apijson.Field
+	raw         string
+	ExtraFields map[string]apijson.Field
+}
+
+func (r *SessionMessageResponse) UnmarshalJSON(data []byte) (err error) {
+	return apijson.UnmarshalRoot(data, r)
+}
+
+func (r sessionMessageResponseJSON) RawJSON() string {
+	return r.raw
+}
+
 type SessionMessagesResponse struct {
 	Info  Message                     `json:"info,required"`
 	Parts []Part                      `json:"parts,required"`

+ 26 - 0
packages/sdk/go/session_test.go

@@ -176,6 +176,32 @@ func TestSessionInit(t *testing.T) {
 	}
 }
 
+func TestSessionMessage(t *testing.T) {
+	t.Skip("skipped: tests are disabled for the time being")
+	baseURL := "http://localhost:4010"
+	if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok {
+		baseURL = envURL
+	}
+	if !testutil.CheckTestServer(t, baseURL) {
+		return
+	}
+	client := opencode.NewClient(
+		option.WithBaseURL(baseURL),
+	)
+	_, err := client.Session.Message(
+		context.TODO(),
+		"id",
+		"messageID",
+	)
+	if err != nil {
+		var apierr *opencode.Error
+		if errors.As(err, &apierr) {
+			t.Log(string(apierr.DumpRequest(true)))
+		}
+		t.Fatalf("err should be nil: %s", err.Error())
+	}
+}
+
 func TestSessionMessages(t *testing.T) {
 	t.Skip("skipped: tests are disabled for the time being")
 	baseURL := "http://localhost:4010"

+ 130 - 0
packages/sdk/go/sessionpermission.go

@@ -0,0 +1,130 @@
+// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+package opencode
+
+import (
+	"context"
+	"errors"
+	"fmt"
+	"net/http"
+
+	"github.com/sst/opencode-sdk-go/internal/apijson"
+	"github.com/sst/opencode-sdk-go/internal/param"
+	"github.com/sst/opencode-sdk-go/internal/requestconfig"
+	"github.com/sst/opencode-sdk-go/option"
+)
+
+// SessionPermissionService contains methods and other services that help with
+// interacting with the opencode API.
+//
+// Note, unlike clients, this service does not read variables from the environment
+// automatically. You should not instantiate this service directly, and instead use
+// the [NewSessionPermissionService] method instead.
+type SessionPermissionService struct {
+	Options []option.RequestOption
+}
+
+// NewSessionPermissionService generates a new service that applies the given
+// options to each request. These options are applied after the parent client's
+// options (if there is one), and before any request-specific options.
+func NewSessionPermissionService(opts ...option.RequestOption) (r *SessionPermissionService) {
+	r = &SessionPermissionService{}
+	r.Options = opts
+	return
+}
+
+// Respond to a permission request
+func (r *SessionPermissionService) Respond(ctx context.Context, id string, permissionID string, body SessionPermissionRespondParams, opts ...option.RequestOption) (res *bool, err error) {
+	opts = append(r.Options[:], opts...)
+	if id == "" {
+		err = errors.New("missing required id parameter")
+		return
+	}
+	if permissionID == "" {
+		err = errors.New("missing required permissionID parameter")
+		return
+	}
+	path := fmt.Sprintf("session/%s/permissions/%s", id, permissionID)
+	err = requestconfig.ExecuteNewRequest(ctx, http.MethodPost, path, body, &res, opts...)
+	return
+}
+
+type Permission struct {
+	ID        string                 `json:"id,required"`
+	MessageID string                 `json:"messageID,required"`
+	Metadata  map[string]interface{} `json:"metadata,required"`
+	SessionID string                 `json:"sessionID,required"`
+	Time      PermissionTime         `json:"time,required"`
+	Title     string                 `json:"title,required"`
+	Type      string                 `json:"type,required"`
+	CallID    string                 `json:"callID"`
+	Pattern   string                 `json:"pattern"`
+	JSON      permissionJSON         `json:"-"`
+}
+
+// permissionJSON contains the JSON metadata for the struct [Permission]
+type permissionJSON struct {
+	ID          apijson.Field
+	MessageID   apijson.Field
+	Metadata    apijson.Field
+	SessionID   apijson.Field
+	Time        apijson.Field
+	Title       apijson.Field
+	Type        apijson.Field
+	CallID      apijson.Field
+	Pattern     apijson.Field
+	raw         string
+	ExtraFields map[string]apijson.Field
+}
+
+func (r *Permission) UnmarshalJSON(data []byte) (err error) {
+	return apijson.UnmarshalRoot(data, r)
+}
+
+func (r permissionJSON) RawJSON() string {
+	return r.raw
+}
+
+type PermissionTime struct {
+	Created float64            `json:"created,required"`
+	JSON    permissionTimeJSON `json:"-"`
+}
+
+// permissionTimeJSON contains the JSON metadata for the struct [PermissionTime]
+type permissionTimeJSON struct {
+	Created     apijson.Field
+	raw         string
+	ExtraFields map[string]apijson.Field
+}
+
+func (r *PermissionTime) UnmarshalJSON(data []byte) (err error) {
+	return apijson.UnmarshalRoot(data, r)
+}
+
+func (r permissionTimeJSON) RawJSON() string {
+	return r.raw
+}
+
+type SessionPermissionRespondParams struct {
+	Response param.Field[SessionPermissionRespondParamsResponse] `json:"response,required"`
+}
+
+func (r SessionPermissionRespondParams) MarshalJSON() (data []byte, err error) {
+	return apijson.MarshalRoot(r)
+}
+
+type SessionPermissionRespondParamsResponse string
+
+const (
+	SessionPermissionRespondParamsResponseOnce   SessionPermissionRespondParamsResponse = "once"
+	SessionPermissionRespondParamsResponseAlways SessionPermissionRespondParamsResponse = "always"
+	SessionPermissionRespondParamsResponseReject SessionPermissionRespondParamsResponse = "reject"
+)
+
+func (r SessionPermissionRespondParamsResponse) IsKnown() bool {
+	switch r {
+	case SessionPermissionRespondParamsResponseOnce, SessionPermissionRespondParamsResponseAlways, SessionPermissionRespondParamsResponseReject:
+		return true
+	}
+	return false
+}

+ 43 - 0
packages/sdk/go/sessionpermission_test.go

@@ -0,0 +1,43 @@
+// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+package opencode_test
+
+import (
+	"context"
+	"errors"
+	"os"
+	"testing"
+
+	"github.com/sst/opencode-sdk-go"
+	"github.com/sst/opencode-sdk-go/internal/testutil"
+	"github.com/sst/opencode-sdk-go/option"
+)
+
+func TestSessionPermissionRespond(t *testing.T) {
+	t.Skip("skipped: tests are disabled for the time being")
+	baseURL := "http://localhost:4010"
+	if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok {
+		baseURL = envURL
+	}
+	if !testutil.CheckTestServer(t, baseURL) {
+		return
+	}
+	client := opencode.NewClient(
+		option.WithBaseURL(baseURL),
+	)
+	_, err := client.Session.Permissions.Respond(
+		context.TODO(),
+		"id",
+		"permissionID",
+		opencode.SessionPermissionRespondParams{
+			Response: opencode.F(opencode.SessionPermissionRespondParamsResponseOnce),
+		},
+	)
+	if err != nil {
+		var apierr *opencode.Error
+		if errors.As(err, &apierr) {
+			t.Log(string(apierr.DumpRequest(true)))
+		}
+		t.Fatalf("err should be nil: %s", err.Error())
+	}
+}

+ 1 - 1
packages/tui/go.mod

@@ -24,7 +24,7 @@ require (
 
 replace (
 	github.com/charmbracelet/x/input => ./input
-	github.com/sst/opencode-sdk-go => ./sdk
+	github.com/sst/opencode-sdk-go => ../sdk/go
 )
 
 require golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // indirect

+ 2 - 2
packages/tui/internal/components/chat/messages.go

@@ -469,7 +469,7 @@ func (m *messagesComponent) renderView() tea.Cmd {
 						}
 
 						permission := opencode.Permission{}
-						if m.app.CurrentPermission.ToolCallID == part.CallID {
+						if m.app.CurrentPermission.CallID == part.CallID {
 							permission = m.app.CurrentPermission
 						}
 
@@ -640,7 +640,7 @@ func (m *messagesComponent) renderView() tea.Cmd {
 				slog.Error("Failed to get message from child session", "error", err)
 			} else {
 				for _, part := range response.Parts {
-					if part.CallID == m.app.CurrentPermission.ToolCallID {
+					if part.CallID == m.app.CurrentPermission.CallID {
 						content := renderToolDetails(
 							m.app,
 							part.AsUnion().(opencode.ToolPart),