@inferencesh/sdk 0.20.1 → 0.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/CHANGELOG.md +104 -0
  2. package/dist/agent/actions.js +1 -4
  3. package/dist/agent/api.d.ts +37 -6
  4. package/dist/agent/api.js +37 -3
  5. package/dist/agent/reducer.js +23 -7
  6. package/dist/agent/types.d.ts +30 -5
  7. package/dist/api/agents.d.ts +9 -3
  8. package/dist/api/agents.js +87 -65
  9. package/dist/api/api-keys.d.ts +2 -5
  10. package/dist/api/apps.d.ts +9 -2
  11. package/dist/api/apps.js +9 -1
  12. package/dist/api/chats.d.ts +13 -3
  13. package/dist/api/chats.js +13 -2
  14. package/dist/api/engines.d.ts +4 -2
  15. package/dist/api/engines.js +3 -2
  16. package/dist/api/flow-runs.d.ts +7 -2
  17. package/dist/api/flow-runs.js +7 -2
  18. package/dist/api/flows.d.ts +13 -2
  19. package/dist/api/flows.js +15 -1
  20. package/dist/api/knowledge.d.ts +7 -4
  21. package/dist/api/knowledge.js +6 -4
  22. package/dist/api/mcp-servers.d.ts +2 -1
  23. package/dist/api/projects.d.ts +5 -3
  24. package/dist/api/projects.js +3 -2
  25. package/dist/api/secrets.d.ts +2 -2
  26. package/dist/api/tasks.js +39 -60
  27. package/dist/api/teams.d.ts +4 -2
  28. package/dist/api/teams.js +3 -2
  29. package/dist/api/write-bodies.d.ts +110 -0
  30. package/dist/api/write-bodies.js +1 -0
  31. package/dist/http/client.d.ts +68 -7
  32. package/dist/http/client.js +138 -97
  33. package/dist/http/index.d.ts +2 -2
  34. package/dist/http/loopback.d.ts +8 -0
  35. package/dist/http/loopback.js +18 -0
  36. package/dist/http/stream-until-terminal.d.ts +55 -0
  37. package/dist/http/stream-until-terminal.js +88 -0
  38. package/dist/http/stream.d.ts +1 -0
  39. package/dist/http/stream.js +7 -3
  40. package/dist/http/streamable.d.ts +25 -5
  41. package/dist/http/streamable.js +28 -40
  42. package/dist/index.d.ts +4 -2
  43. package/dist/index.js +1 -0
  44. package/dist/proxy/endpoints.json +156 -0
  45. package/dist/proxy/express.d.ts +5 -6
  46. package/dist/proxy/express.js +18 -2
  47. package/dist/proxy/hono.d.ts +5 -3
  48. package/dist/proxy/hono.js +4 -3
  49. package/dist/proxy/index.d.ts +165 -6
  50. package/dist/proxy/index.js +409 -25
  51. package/dist/proxy/nextjs.d.ts +29 -17
  52. package/dist/proxy/nextjs.js +72 -62
  53. package/dist/proxy/remix.d.ts +5 -3
  54. package/dist/proxy/remix.js +4 -3
  55. package/dist/proxy/svelte.d.ts +5 -4
  56. package/dist/proxy/svelte.js +4 -4
  57. package/dist/types.d.ts +591 -271
  58. package/dist/types.js +117 -252
  59. package/dist/version.d.ts +1 -0
  60. package/dist/version.js +7 -0
  61. package/package.json +2 -1
package/CHANGELOG.md CHANGED
@@ -7,6 +7,110 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.22.0] - 2026-10-10
11
+
12
+ Breaking: the server proxy forwards only app runs and agent chats and refuses every other API call with 403 (see Security); `update()` on projects, knowledge, skills, chats, engines and teams sends PATCH and needs an API that answers PATCH (see Changed); the Next.js proxy handler has no `PUT`.
13
+
14
+ ### Added
15
+
16
+ - Server proxy options (`@inferencesh/sdk/proxy` and every adapter): `allowedEndpoints` (`namespace/name` globs, e.g. `"ana/helper"`, `"ana/*"`) limits which apps and agents visitors may run; `isAuthenticated(request)` decides per request whether the API key may be used (401 otherwise) and receives the framework's request; `allowUnauthorizedRequests` (default `true`) set to `false` refuses every request unless `isAuthenticated` is given. `@inferencesh/sdk/proxy/nextjs` gains `createHandler(options)` (App Router) and `createPageHandler(options)` (Page Router); `handlers` and `pageHandler` are those with the default options. `PROXY_ENDPOINTS` lists what the proxy forwards. `apiUrl` (default `https://api.inference.sh`) names the API the frontend's client calls; `maxUploadBytes` (default 100 MiB) caps a file uploaded through the proxy.
17
+ - Server proxy: `endpoints` forwards a subset of `PROXY_ENDPOINTS`, built with `pickProxyEndpoint("POST /chats/{id}/messages", { onlyFields, validate })`, which can narrow an entry (fewer body fields, an extra body check) but never widen it; `matchProxyEndpoint(method, path)` finds the entry a request takes. `exactRefs: true` matches `allowedEndpoints` entries against the whole ref, version included ("ana/helper@v2" lets only that version through). `isLoopbackHost` / `isHttpsOrLoopback` export the proxy's https-or-loopback rule.
18
+ - Server proxy: `PROXY_ENDPOINTS` is built from the API's proxy manifest (`src/proxy/endpoints.json`, generated by go/api from its route table for the run-only preset and copied by its `make types`), exported as `PROXY_MANIFEST`. Each entry gains `route` ("POST /chats/{id}/messages") and `scopes` (what the site's key must hold for it); `PROXY_ENDPOINT_RULES` holds the proxy's own body checks by route. `pickProxyEndpoint` takes the manifest route, any placeholder names.
19
+ - `AppsAPI.patch(appId, AppPatchBody)`: PATCH /apps/{id}, changes the app's copy fields sent and keeps the others. `update()` stays POST, since it also edits the version in place.
20
+ - `ChatsAPI.updateSettings()` (POST /chats/{id}/settings: name, visibility, tool approval, hooks, memory), `FlowsAPI.patch()` (PATCH /flows/{id}, only the fields sent) and `FlowsAPI.saveViewport()` (POST /flows/{id}/viewport). `HttpClient` takes `'patch'`.
21
+ - Write body types (`AgentUpdateBody`, `AppUpdateBody`, `ChatUpdateBody`, `EngineUpdateBody`, `FlowUpdateBody`, `FlowPatchBody`, `FlowViewportBody`, `ProjectWriteBody`, `KnowledgeUpdateBody`, `TeamUpdateBody`, `MCPServerUpdateBody`) name the fields each route writes. The update methods take them in place of `Partial<DTO>`: a loaded DTO spread into the call still type-checks; an explicit field the route drops does not.
22
+ - `ApiKeysAPI.create()` takes the generated `CreateApiKeyRequest`, so `scope` (personal or workspace key) and `expires_at` type-check. Existing `{ name, scopes }` calls are unchanged.
23
+ - `ProxyAdapter` takes an optional `signal` that the proxy passes to the upstream fetch.
24
+ - Types: `ScopeRefusedMeta` (`required_scope`, the meta of an `insufficient_scope` or `admin_scopes_not_approved` error), `ErrorCodeAdminScopesNotApproved` and the other admin session error codes, `AgentVersionDTO.visitor_reads` (the scope groups a publication of the agent answers), `ScopesResponse.account_scopes` and `login_preset`, `AppSandbox` on `AppVersionDTO`.
25
+
26
+ ### Changed
27
+
28
+ - `update()` on `ProjectsAPI`, `KnowledgeAPI`, `SkillsAPI`, `ChatsAPI`, `EnginesAPI` and `TeamsAPI` sends PATCH instead of POST: a field left out of the body keeps its value. With POST the API wrote every field left out as empty. Needs an API that answers PATCH on these routes; an older API answers 405.
29
+ - `@inferencesh/sdk/proxy/nextjs` `createHandler()` (and `handlers`) return `{ GET, POST }`: the `PUT` handler is gone, since the proxy forwards no PUT call and answered every one with 403. Drop `PUT` from `export const { GET, POST, PUT } = ...`; Next.js now answers PUT with 405.
30
+
31
+ - Tool approvals (`approveTool`, `rejectTool`, `alwaysAllowTool`, `resolveInterrupt`) and widening a chat's approval settings document that they need the account holder's own sign-in: with an API key, an app's token or the server proxy the API answers 403 `requires_sign_in` (`ErrorCodeRequiresSignIn`).
32
+ - Types regenerated from the API: admin elevation types (`AdminElevate*`, `AdminElevation*`), `MeResponse.platform_power` and `scopes`, scope metadata (`sign_in_only`, `not_for_apps`, `costs_credits`, `automation`, `public_rows`, `staff_only`), `ScopePreset.default` and `grants`, `ApiKeyDTO.grants`, the approvals, secrets:reveal and admin scopes (`admin:pricing` included), and error codes including `requires_sign_in`, `insufficient_scope`, `invalid_ttl`, `invalid_code`, `already_processed` and `expired`.
33
+
34
+ ### Fixed
35
+
36
+ - `@inferencesh/sdk/proxy/express` cancels the upstream request when the client disconnects. Before, a disconnected chat or task stream kept its upstream connection open, which kept the host process alive after `server.close()`.
37
+ - `tasks.watch().done`, `client.run({ wait: true })` and the agent's stream-until-idle no longer hang when a stream closes cleanly before a terminal status (load balancer idle timeout, deploy): the SDK reads the task or chat once and settles from it, rejecting when it is still not terminal.
38
+ - Agent chat state always holds `content` as an array: a message the API serializes with `content: null`, or a partial update without content, no longer crashes views that read it. A partial update keeps the content the message had.
39
+ - `SDK_VERSION` reads `package.json`, so it cannot drift from the published version.
40
+
41
+ ### Deprecated
42
+
43
+ - `FlowRunsAPI.update()`: the API refuses it, because a flow run has no caller-writable fields. Use `FlowRunsAPI.updateVisibility()` to change who can see a run. The method stays for compile compatibility.
44
+
45
+ ### Security
46
+
47
+ - The server proxy forwards only what a frontend needs to run things, listed in `PROXY_ENDPOINTS`: `POST /apps/run`, `POST /files` (uploads of run inputs), `POST /agents/run`, `POST /chats`, `POST /chats/{id}/messages`, `POST /chats/{id}/agent`, `GET /agents/{namespace}/{name}`, reading, polling, streaming, stopping and cancelling the resulting task or chat, and client tool results (`POST /tools/{id}`). Every other call (secrets, API keys, billing, account, file listings, app/agent/flow management, admin, task and chat listings and feeds) answers 403 before the API key is attached, whatever the options. Before, it forwarded any path on `*.inference.sh` with the site's key. A server that proxied other calls for its frontend must make them from its own backend.
48
+ - Ids in proxied paths must have the shape the API mints (a lowercase 26-character ULID), so a literal route beside an id is never taken for one: `GET /tasks/stream` (the live feed of every task the key can read), `/tasks/featured`, `/tasks/queue-stats`, `/chats/list` and the like answer 403.
49
+ - Tool approvals are not forwarded: approve, reject, always allow, explain, resolving an interrupt, listing a run's interrupts, and chat settings (`allow_all_tools`). The API takes an approval only from the account holder's own sign-in, never an API key, so these could not work through a proxy, and "allow all tools" would have let any visitor holding a chat id approve every call in it. Agents served through a proxy should not use approval-gated tools; use a published agent (embed) for that.
50
+ - A target URL with credentials (`https://user:pass@...`) is refused with 400.
51
+ - The proxy forwards only to the configured API origin: `apiBaseUrl` / `INFERENCE_API_BASE_URL` when set, else `apiUrl` (default `https://api.inference.sh`). Other `*.inference.sh` hosts are refused with 412. `allowedDomains` patterns must match the whole host.
52
+ - `POST /apps/run` through the proxy takes only `app`, `app_id`, `version_id`, `function`, `input`, `stream` and `wait`: a visitor can no longer set `workers`, `infra`, `session`, `session_timeout`, `webhook`, `run_at`, `setup` or `metadata` (403).
53
+ - `POST /files` through the proxy takes one file per request, with no `category`, and a declared `size` up to `maxUploadBytes` (413 above it). Uploads count against the API key owner's storage quota.
54
+ - With `allowedEndpoints` set, a request that names an app or agent must name an allowed one by its `namespace/name` ref: `app_id`/`version_id` runs and ad-hoc `agent_config` runs are refused, and body keys are matched case-insensitively as the API reads them. Requests about an existing task or chat name it by id and are not checked against `allowedEndpoints`: whoever holds a task or chat id can read, stream, cancel or stop it and send messages in the chat. All visitors share the key, so the id is the only boundary between them.
55
+ - The server proxy (`@inferencesh/sdk/proxy`) refuses a target that is not `https:` with 412 before attaching the API key, so a client can no longer make it send `INFERENCE_API_KEY` over plain http. Plain `http:` is allowed only to a loopback host (`localhost`, `*.localhost`, `127.0.0.0/8`, `[::1]`), for a local API in development. A malformed target with `apiBaseUrl` set answers 400 instead of throwing.
56
+
57
+ ## [0.21.0] - 2026-10-08
58
+
59
+ Breaking: `AgentClient.http` requires `fetch`, the `AppTag*` constants are removed, and `StreamableManagerOptions` is a type alias. See Changed and Removed.
60
+
61
+ ### Added
62
+
63
+ - `HttpClient.fetch(endpoint, init?)`: fetches an endpoint with the client's auth, headers and credentials mode and resolves with the raw `Response`, for bodies `request()` does not parse (streams, text). A refused response throws the error `request()` would (`InferenceError`, `RequirementsNotMetException`). Type `HttpFetchInit`.
64
+ - `StreamRequest` and `StreamRequestInit`: a function that sends a stream's request. `StreamableManager` takes `{ request }` in place of `{ url, headers, credentials }`, and `streamable()` / `streamableRaw()` take one in place of the URL. With `(init) => http.fetch(endpoint, init)` the stream carries the token of the moment it connects and a refusal goes through `onError`. Type `StreamableSource`.
65
+ - `onError` gets a third argument, `FailedRequest` (`{ token }`): the bearer token the failed request carried, so a handler that refreshes credentials can tell a request sent before the refresh from one sent after it.
66
+ - `getToken` may be async (`() => string | null | undefined | Promise<string | null | undefined>`). `request()`, `fetch()` and the EventSource handshake wait for it.
67
+ - `HttpClient.request()` takes `handleErrors` (default `true`). With `handleErrors: false` the caller handles the request's failure itself: `onError` is not called for it, nothing is retried, and the error is thrown as it is.
68
+
69
+ - Types: `StoreCategoryDTO` and `StoreTagDTO` (`GET /store/categories`, `GET /store/tags`); the decision app contract (`DecisionInput`, `DecisionVisionInput`, `DecisionOutput` and its question and answer types); `ErrorCodeImpersonationReasonRequired`.
70
+
71
+ ### Changed
72
+
73
+ - `AgentClient.http` requires `fetch` and no longer requires `getStreamableConfig`. A hand-built `AgentClient` must add `http.fetch`; `Inference` and `HttpClient` already have it.
74
+ - The streams the SDK opens itself (`tasks.run()` and `tasks.watch()`, `Agent` chat streams, the agent chat in `@inferencesh/sdk/agent`) go through `HttpClient.fetch`. A stream the api refuses now reaches `onError` like a refused request, and fails with an `InferenceError` (`statusCode`, `responseBody`) where it failed with `Error('HTTP <status>: <body>')`.
75
+ - A request that got no response (the network failed) goes to `onError` from `fetch()` and the EventSource handshake too, as it already did from `request()`. The failure is thrown unless the handler's retry resolves with a response. A request aborted through its signal is not handed to `onError`.
76
+ - The EventSource handshake and `fetch()` take what `onError` resolves with only when it is a `Response`; anything else leaves the refused response in place.
77
+ - `StreamableManagerOptions` is a type alias (`StreamableSource & { ... }`), no longer an interface: it cannot be extended with `interface X extends StreamableManagerOptions<T>`; use an intersection.
78
+
79
+ ### Removed
80
+
81
+ - The `AppTag*` constants (`AppTagTextToImage` and the rest). App tags come from the store: `GET /store/tags`, typed `StoreTagDTO`.
82
+
83
+ ### Deprecated
84
+
85
+ - `HttpClient.getStreamableConfig()`: a request made from the config bypasses `onError` and carries the token of the moment the config was read. Use `fetch()` or a `StreamRequest`. It throws when `getToken` is async.
86
+
87
+ ## [0.20.1] - 2026-10-08
88
+
89
+ ### Added
90
+
91
+ - Types: app tags and the decision app category.
92
+
93
+ ## [0.20.0] - 2026-10-07
94
+
95
+ ### Added
96
+
97
+ - `tasks.files(id)`, `tasks.deleteFiles(id)` and `tasks.delete(id, { files })`: list a task's files and delete them with or without the task.
98
+ - Types: task files, MCP Apps (`_meta.ui`, `AppUIRef`, the MCP tool call UI fields), remote tags and policy rules, `AppDTO.resolved_function`, `DescriptionLimit`.
99
+
100
+ ### Changed
101
+
102
+ - Types: forms lose the bounty and reward fields; a bounty's proof form drops `proof_min_length`.
103
+
104
+ ### Security
105
+
106
+ - `next` ^16.3.6 (resolves 16.3.8) for GHSA-vcvr-r3jv-pc5j.
107
+
108
+ ## [0.19.0] - 2026-10-04
109
+
110
+ ### Changed
111
+
112
+ - Types regenerated; the run parameters docs drop `params.variant`.
113
+
10
114
  ## [0.18.0] - 2026-10-04
11
115
 
12
116
  ### Added
@@ -109,11 +109,8 @@ export function createActions(ctx) {
109
109
  return;
110
110
  }
111
111
  // Single unified stream with TypedEvents (both Chat and ChatMessage events)
112
- const { url, headers, credentials } = api.getChatStreamConfig(client, id);
113
112
  const manager = new StreamableManager({
114
- url,
115
- headers,
116
- credentials,
113
+ request: api.chatStreamRequest(client, id),
117
114
  onError: (error) => {
118
115
  console.warn('[AgentSDK] Stream error:', error);
119
116
  callbacks.onError?.(error);
@@ -9,6 +9,7 @@
9
9
  */
10
10
  import type { AlwaysAllowOptionsDTO, AlwaysAllowResultDTO, ChatAgentDTO, ChatDTO, ChatMessageDTO, ChatSettingsDTO, ChatSettingsRequest, InterruptDTO, ToolExplanationDTO } from '../types';
11
11
  import type { AgentOptions, AgentClient, AgentInfo, FileRef } from './types';
12
+ import type { StreamRequest } from '../http/streamable';
12
13
  export interface SendResult {
13
14
  chatId: string;
14
15
  userMessage: ChatMessageDTO;
@@ -45,7 +46,23 @@ export declare function submitToolResult(client: AgentClient, toolInvocationId:
45
46
  };
46
47
  form_data?: Record<string, unknown>;
47
48
  }): Promise<void>;
49
+ /**
50
+ * Approve a call awaiting approval; the api then runs it.
51
+ *
52
+ * Needs approvals:write, which only the account holder's own sign-in holds
53
+ * (the web app, or a `belt login` session). An API key, an app's OAuth token
54
+ * or the server proxy is refused with 403 `requires_sign_in`
55
+ * (ErrorCodeRequiresSignIn); the error's message says what to do.
56
+ */
48
57
  export declare function approveTool(client: AgentClient, toolInvocationId: string): Promise<void>;
58
+ /**
59
+ * Reject a call awaiting approval, with an optional reason the agent sees.
60
+ *
61
+ * Needs approvals:write, which only the account holder's own sign-in holds
62
+ * (the web app, or a `belt login` session). An API key, an app's OAuth token
63
+ * or the server proxy is refused with 403 `requires_sign_in`
64
+ * (ErrorCodeRequiresSignIn); the error's message says what to do.
65
+ */
49
66
  export declare function rejectTool(client: AgentClient, toolInvocationId: string, reason?: string): Promise<void>;
50
67
  /**
51
68
  * What "always allow" can save for a call awaiting approval, narrowest first.
@@ -56,6 +73,11 @@ export declare function getAlwaysAllowOptions(client: AgentClient, chatId: strin
56
73
  * Save an "always allow" option (a key from getAlwaysAllowOptions) as chat
57
74
  * rules and approve the call once. Without an option the api saves its old
58
75
  * default: the command exactly for remote_exec, the whole tool otherwise.
76
+ *
77
+ * Needs approvals:write, which only the account holder's own sign-in holds
78
+ * (the web app, or a `belt login` session). An API key, an app's OAuth token
79
+ * or the server proxy is refused with 403 `requires_sign_in`
80
+ * (ErrorCodeRequiresSignIn); the error's message says what to do.
59
81
  */
60
82
  export declare function alwaysAllowTool(client: AgentClient, chatId: string, toolInvocationId: string, option?: string): Promise<AlwaysAllowResultDTO>;
61
83
  /**
@@ -64,13 +86,22 @@ export declare function alwaysAllowTool(client: AgentClient, chatId: string, too
64
86
  * for the call, so asking again is free.
65
87
  */
66
88
  export declare function explainTool(client: AgentClient, chatId: string, toolInvocationId: string): Promise<ToolExplanationDTO>;
67
- /** Change the chat's settings. Answers with the settings as they now are. */
89
+ /**
90
+ * Change the chat's settings. Answers with the settings as they now are.
91
+ * Widening what runs without approval (allow_all_tools) needs approvals:write,
92
+ * held only by the account holder's own sign-in: other credentials are
93
+ * refused with 403 `requires_sign_in` (ErrorCodeRequiresSignIn).
94
+ */
68
95
  export declare function updateChatSettings(client: AgentClient, chatId: string, settings: ChatSettingsRequest): Promise<ChatSettingsDTO>;
96
+ /**
97
+ * Allow or deny an interrupt gate on an agent run.
98
+ *
99
+ * Needs approvals:write, which only the account holder's own sign-in holds
100
+ * (the web app, or a `belt login` session). An API key, an app's OAuth token
101
+ * or the server proxy is refused with 403 `requires_sign_in`
102
+ * (ErrorCodeRequiresSignIn); the error's message says what to do.
103
+ */
69
104
  export declare function resolveInterrupt(client: AgentClient, interruptId: string, decision: 'allow' | 'deny'): Promise<InterruptDTO>;
70
105
  export declare function listRunInterrupts(client: AgentClient, runId: string): Promise<InterruptDTO[]>;
71
106
  export declare function uploadFile(client: AgentClient, file: globalThis.File): Promise<FileRef>;
72
- export declare function getChatStreamConfig(client: AgentClient, chatId: string): {
73
- url: string;
74
- headers: Record<string, string>;
75
- credentials: RequestCredentials;
76
- };
107
+ export declare function chatStreamRequest(client: AgentClient, chatId: string): StreamRequest;
package/dist/agent/api.js CHANGED
@@ -146,9 +146,25 @@ export async function submitToolResult(client, toolInvocationId, resultOrAction)
146
146
  const data = typeof resultOrAction === 'string' ? { result: resultOrAction } : resultOrAction;
147
147
  await client.http.request('post', `/tools/${toolInvocationId}`, { data });
148
148
  }
149
+ /**
150
+ * Approve a call awaiting approval; the api then runs it.
151
+ *
152
+ * Needs approvals:write, which only the account holder's own sign-in holds
153
+ * (the web app, or a `belt login` session). An API key, an app's OAuth token
154
+ * or the server proxy is refused with 403 `requires_sign_in`
155
+ * (ErrorCodeRequiresSignIn); the error's message says what to do.
156
+ */
149
157
  export async function approveTool(client, toolInvocationId) {
150
158
  await client.http.request('post', `/tools/${toolInvocationId}/invoke`);
151
159
  }
160
+ /**
161
+ * Reject a call awaiting approval, with an optional reason the agent sees.
162
+ *
163
+ * Needs approvals:write, which only the account holder's own sign-in holds
164
+ * (the web app, or a `belt login` session). An API key, an app's OAuth token
165
+ * or the server proxy is refused with 403 `requires_sign_in`
166
+ * (ErrorCodeRequiresSignIn); the error's message says what to do.
167
+ */
152
168
  export async function rejectTool(client, toolInvocationId, reason) {
153
169
  await client.http.request('post', `/tools/${toolInvocationId}/reject`, { data: { reason } });
154
170
  }
@@ -164,6 +180,11 @@ export async function getAlwaysAllowOptions(client, chatId, toolInvocationId) {
164
180
  * Save an "always allow" option (a key from getAlwaysAllowOptions) as chat
165
181
  * rules and approve the call once. Without an option the api saves its old
166
182
  * default: the command exactly for remote_exec, the whole tool otherwise.
183
+ *
184
+ * Needs approvals:write, which only the account holder's own sign-in holds
185
+ * (the web app, or a `belt login` session). An API key, an app's OAuth token
186
+ * or the server proxy is refused with 403 `requires_sign_in`
187
+ * (ErrorCodeRequiresSignIn); the error's message says what to do.
167
188
  */
168
189
  export async function alwaysAllowTool(client, chatId, toolInvocationId, option) {
169
190
  const data = option ? { option } : {};
@@ -179,7 +200,12 @@ export async function explainTool(client, chatId, toolInvocationId) {
179
200
  const resp = await client.http.request('post', `/chats/${chatId}/tools/${toolInvocationId}/explain`);
180
201
  return resp.data;
181
202
  }
182
- /** Change the chat's settings. Answers with the settings as they now are. */
203
+ /**
204
+ * Change the chat's settings. Answers with the settings as they now are.
205
+ * Widening what runs without approval (allow_all_tools) needs approvals:write,
206
+ * held only by the account holder's own sign-in: other credentials are
207
+ * refused with 403 `requires_sign_in` (ErrorCodeRequiresSignIn).
208
+ */
183
209
  export async function updateChatSettings(client, chatId, settings) {
184
210
  const resp = await client.http.request('post', `/chats/${chatId}/settings`, { data: settings });
185
211
  return resp.data;
@@ -187,6 +213,14 @@ export async function updateChatSettings(client, chatId, settings) {
187
213
  // =========================================================================
188
214
  // Interrupt operations
189
215
  // =========================================================================
216
+ /**
217
+ * Allow or deny an interrupt gate on an agent run.
218
+ *
219
+ * Needs approvals:write, which only the account holder's own sign-in holds
220
+ * (the web app, or a `belt login` session). An API key, an app's OAuth token
221
+ * or the server proxy is refused with 403 `requires_sign_in`
222
+ * (ErrorCodeRequiresSignIn); the error's message says what to do.
223
+ */
190
224
  export async function resolveInterrupt(client, interruptId, decision) {
191
225
  const resp = await client.http.request('post', `/interrupts/${interruptId}/resolve`, {
192
226
  data: { decision }
@@ -203,6 +237,6 @@ export async function listRunInterrupts(client, runId) {
203
237
  export async function uploadFile(client, file) {
204
238
  return client.files.upload(file);
205
239
  }
206
- export function getChatStreamConfig(client, chatId) {
207
- return client.http.getStreamableConfig(`/chats/${chatId}/stream`);
240
+ export function chatStreamRequest(client, chatId) {
241
+ return (init) => client.http.fetch(`/chats/${chatId}/stream`, init);
208
242
  }
@@ -14,6 +14,20 @@ function deriveChatStatus(run) {
14
14
  return ChatStatusAwaitingInput;
15
15
  return ChatStatusIdle;
16
16
  }
17
+ /**
18
+ * Every message enters state through here. The API serializes a message with
19
+ * no content as `content: null` (a nil Go slice), and a partial stream update
20
+ * carries the whole DTO with `content: null` when content is not among its
21
+ * fields. State always holds an array, so ChatMessageDTO.content is what its
22
+ * type says for every consumer; a partial update with null content keeps the
23
+ * content the message already had.
24
+ */
25
+ function normalizeMessage(message, existing) {
26
+ if (message.content != null)
27
+ return message;
28
+ return { ...message, content: existing?.content ?? [] };
29
+ }
30
+ const normalizeMessages = (messages) => messages.map((m) => normalizeMessage(m));
17
31
  // =============================================================================
18
32
  // Initial State
19
33
  // =============================================================================
@@ -38,7 +52,7 @@ export function chatReducer(state, action) {
38
52
  if (!chat) {
39
53
  return { ...state, chat: null, messages: [], connectionStatus: 'idle', messageCursor: undefined, hasOlderMessages: undefined };
40
54
  }
41
- const messages = [...(chat.chat_messages || [])].sort((a, b) => a.order - b.order);
55
+ const messages = normalizeMessages(chat.chat_messages || []).sort((a, b) => a.order - b.order);
42
56
  const cursor = chat?._messageCursor;
43
57
  const hasOlder = chat?._hasOlderMessages;
44
58
  return { ...state, chat, messages, messageCursor: cursor, hasOlderMessages: hasOlder };
@@ -93,11 +107,11 @@ export function chatReducer(state, action) {
93
107
  return { ...state, chat: { ...state.chat, active_run: action.payload, status: deriveChatStatus(action.payload) } };
94
108
  }
95
109
  case 'SET_MESSAGES':
96
- return { ...state, messages: action.payload };
110
+ return { ...state, messages: normalizeMessages(action.payload) };
97
111
  case 'PREPEND_MESSAGES': {
98
112
  const { messages: older, cursor, hasMore } = action.payload;
99
113
  const existingIds = new Set(state.messages.map(m => m.id));
100
- const deduped = older.filter(m => !existingIds.has(m.id));
114
+ const deduped = normalizeMessages(older.filter(m => !existingIds.has(m.id)));
101
115
  const merged = [...deduped, ...state.messages].sort((a, b) => a.order - b.order);
102
116
  return { ...state, messages: merged, messageCursor: cursor, hasOlderMessages: hasMore };
103
117
  }
@@ -106,7 +120,9 @@ export function chatReducer(state, action) {
106
120
  const existingIndex = state.messages.findIndex((m) => m.id === message.id);
107
121
  if (existingIndex !== -1) {
108
122
  const existing = state.messages[existingIndex];
109
- const updated = action.partial ? { ...existing, ...message } : message;
123
+ const updated = action.partial
124
+ ? normalizeMessage({ ...existing, ...message }, existing)
125
+ : normalizeMessage(message);
110
126
  if (existing === updated)
111
127
  return state;
112
128
  const newMessages = [...state.messages];
@@ -115,12 +131,12 @@ export function chatReducer(state, action) {
115
131
  }
116
132
  if (action.partial)
117
133
  return state;
118
- return { ...state, messages: [...state.messages, message].sort((a, b) => a.order - b.order) };
134
+ return { ...state, messages: [...state.messages, normalizeMessage(message)].sort((a, b) => a.order - b.order) };
119
135
  }
120
136
  case 'ADD_MESSAGE':
121
137
  return {
122
138
  ...state,
123
- messages: [...state.messages, action.payload].sort((a, b) => a.order - b.order),
139
+ messages: [...state.messages, normalizeMessage(action.payload)].sort((a, b) => a.order - b.order),
124
140
  };
125
141
  case 'DELTA_TOKEN': {
126
142
  const { messageId, output } = action.payload;
@@ -131,7 +147,7 @@ export function chatReducer(state, action) {
131
147
  const target = msgs.find(m => m.id === messageId);
132
148
  if (!target)
133
149
  return state;
134
- const textBlock = target.content?.find(c => c.type === 'text');
150
+ const textBlock = target.content.find(c => c.type === 'text');
135
151
  const newContent = textBlock
136
152
  ? target.content.map(c => c.type === 'text' ? { ...c, text: output.response } : c)
137
153
  : [{ type: 'text', text: output.response }, ...target.content];
@@ -19,7 +19,7 @@ export interface AlwaysAllowChoice {
19
19
  */
20
20
  export interface AgentClient {
21
21
  /** HTTP client for API requests */
22
- http: Pick<HttpClient, 'request' | 'getStreamableConfig' | 'getStreamDefault' | 'getPollIntervalMs'>;
22
+ http: Pick<HttpClient, 'request' | 'fetch' | 'getStreamDefault' | 'getPollIntervalMs'>;
23
23
  /** Files API for uploads */
24
24
  files: {
25
25
  upload: (data: string | Blob | globalThis.File) => Promise<FileRef>;
@@ -132,9 +132,21 @@ export interface AgentChatActions {
132
132
  submitToolResult: (toolInvocationId: string, result: string) => Promise<void>;
133
133
  /** Answer an MCP tool call's input requests (AwaitingInput status with MCPInputState data) */
134
134
  submitMCPInput: (toolInvocationId: string, responses: Record<string, ElicitResult>) => Promise<void>;
135
- /** Approve a tool (for HIL approval - AwaitingApproval status) */
135
+ /**
136
+ * Approve a tool (for HIL approval - AwaitingApproval status)
137
+ *
138
+ * Needs the account holder's own sign-in (approvals:write): with an API
139
+ * key, an app's token or the server proxy it rejects with 403
140
+ * `requires_sign_in` (ErrorCodeRequiresSignIn).
141
+ */
136
142
  approveTool: (toolInvocationId: string) => Promise<void>;
137
- /** Reject a tool (for HIL approval - AwaitingApproval status) */
143
+ /**
144
+ * Reject a tool (for HIL approval - AwaitingApproval status)
145
+ *
146
+ * Needs the account holder's own sign-in (approvals:write): with an API
147
+ * key, an app's token or the server proxy it rejects with 403
148
+ * `requires_sign_in` (ErrorCodeRequiresSignIn).
149
+ */
138
150
  rejectTool: (toolInvocationId: string, reason?: string) => Promise<void>;
139
151
  /**
140
152
  * What "always allow" can save for a call awaiting approval, narrowest
@@ -154,15 +166,28 @@ export interface AgentChatActions {
154
166
  * api saves its default. A string is accepted from older callers (it was the
155
167
  * tool name) and means the default. Rejects with a 409 when the option is
156
168
  * stale: read the options again.
169
+ *
170
+ * Needs the account holder's own sign-in (approvals:write): with an API
171
+ * key, an app's token or the server proxy it rejects with 403
172
+ * `requires_sign_in` (ErrorCodeRequiresSignIn).
157
173
  */
158
174
  alwaysAllowTool: (toolInvocationId: string, choice?: AlwaysAllowChoice | string) => Promise<AlwaysAllowResultDTO | undefined>;
159
- /** Change this chat's settings, e.g. allow_all_tools (switching it on approves the calls waiting) */
175
+ /**
176
+ * Change this chat's settings, e.g. allow_all_tools (switching it on approves the calls waiting).
177
+ * Widening approvals needs the account holder's own sign-in: other credentials get 403 `requires_sign_in`.
178
+ */
160
179
  updateChatSettings: (settings: ChatSettingsRequest) => Promise<void>;
161
180
  /** Hand this chat to another agent (namespace/name); the next message goes to it. Agents our loop runs only. */
162
181
  switchAgent: (agentRef: string) => Promise<void>;
163
182
  /** Cancel a queued message before the agent processes it */
164
183
  cancelMessage: (messageId: string) => Promise<void>;
165
- /** Resolve an interrupt gate (allow or deny) */
184
+ /**
185
+ * Resolve an interrupt gate (allow or deny)
186
+ *
187
+ * Needs the account holder's own sign-in (approvals:write): with an API
188
+ * key, an app's token or the server proxy it rejects with 403
189
+ * `requires_sign_in` (ErrorCodeRequiresSignIn).
190
+ */
166
191
  resolveInterrupt: (interruptId: string, decision: 'allow' | 'deny') => Promise<void>;
167
192
  /** Load older messages (scroll-up pagination). Returns true if more exist. */
168
193
  loadOlderMessages: () => Promise<boolean>;
@@ -1,5 +1,6 @@
1
1
  import { HttpClient } from '../http/client';
2
2
  import type { Response } from '../http/response';
3
+ import type { AgentUpdateBody } from './write-bodies';
3
4
  import { FilesAPI } from './files';
4
5
  import { ChatDTO, ChatMessageDTO, ChannelContext, LLMDelta, LLMOutput, AgentConfigInput as AgentConfig, AgentDTO, AgentVersionDTO, CreateAgentRequest, FileDTO as File, InterruptDTO, CursorListRequest, CursorListResponse } from '../types';
5
6
  /** Internal tool definition returned by getInternalTools */
@@ -159,9 +160,10 @@ export declare class AgentsAPI {
159
160
  */
160
161
  createAgent(data: CreateAgentRequest): Promise<Response<AgentDTO>>;
161
162
  /**
162
- * Update an agent template
163
+ * Update an agent template: its visibility and images, and a new version
164
+ * when `version` is set (see AgentUpdateBody).
163
165
  */
164
- update(agentId: string, data: Partial<AgentDTO>): Promise<Response<AgentDTO>>;
166
+ update(agentId: string, data: AgentUpdateBody): Promise<Response<AgentDTO>>;
165
167
  /**
166
168
  * Delete an agent template
167
169
  */
@@ -215,7 +217,11 @@ export declare class AgentsAPI {
215
217
  form_data?: Record<string, unknown>;
216
218
  }): Promise<void>;
217
219
  /**
218
- * Resolve an interrupt gate on an agent run
220
+ * Resolve an interrupt gate on an agent run.
221
+ *
222
+ * Needs approvals:write, which only the account holder's own sign-in holds:
223
+ * with an API key, an app's OAuth token or the server proxy it rejects with
224
+ * 403 `requires_sign_in` (ErrorCodeRequiresSignIn).
219
225
  */
220
226
  resolveInterrupt(interruptId: string, decision: 'allow' | 'deny'): Promise<Response<InterruptDTO>>;
221
227
  /**