(options?: {
readonly fetch?: typeof globalThis.fetch;
})
| 114 | * bearer (a desktop connection carries no auth and sends none). |
| 115 | */ |
| 116 | export const createHttpShellHost = (options?: { |
| 117 | readonly fetch?: typeof globalThis.fetch; |
| 118 | }): HttpShellHost => { |
| 119 | const doFetch = options?.fetch ?? globalThis.fetch.bind(globalThis); |
| 120 | |
| 121 | /** |
| 122 | * Every request this host makes, headed the same way as the typed API client |
| 123 | * (`api/client.tsx`'s `transformClient`): bearer when the connection carries |
| 124 | * one, plus the active org selector. |
| 125 | * |
| 126 | * The selector is what an org-scoped host (cloud) scopes the request by, and |
| 127 | * it fails CLOSED — a session request that omits it is rejected outright |
| 128 | * rather than falling back to the session's stored org. Without it here every |
| 129 | * artifact tool call 403'd with `no_organization`. Resolved per request, not |
| 130 | * captured at construction: the host is memoized for the page's lifetime, |
| 131 | * while the scope authority is set during render, so a captured value could |
| 132 | * outlive the org it named. |
| 133 | * |
| 134 | * Hosts without org scoping (local, desktop, self-host) produce no slug and |
| 135 | * so send no header, which is the same convention the neighboring clients |
| 136 | * follow. |
| 137 | */ |
| 138 | const requestHeaders = (): Record<string, string> => { |
| 139 | const headers: Record<string, string> = { |
| 140 | "content-type": "application/json", |
| 141 | ...getExecutorOrganizationHeaders(), |
| 142 | }; |
| 143 | const authorization = getExecutorServerAuthorizationHeader(); |
| 144 | if (authorization) headers.authorization = authorization; |
| 145 | return headers; |
| 146 | }; |
| 147 | |
| 148 | const post = async (path: string, payload: Record<string, unknown>): Promise<unknown> => { |
| 149 | const response = await doFetch(`${getExecutorApiBaseUrl()}${path}`, { |
| 150 | method: "POST", |
| 151 | headers: requestHeaders(), |
| 152 | body: JSON.stringify(payload), |
| 153 | }); |
| 154 | if (!response.ok) { |
| 155 | const text = await response.text(); |
| 156 | // An approval that outlived its window is an ordinary outcome, not a |
| 157 | // fault: nothing ran, and the fix is to trigger the action again. Say that |
| 158 | // instead of surfacing the transport's error body, which reached the user |
| 159 | // as a raw `ExecutionNotFoundError` inside a JSON-RPC envelope. |
| 160 | if (response.status === APPROVAL_EXPIRED_STATUS) { |
| 161 | // oxlint-disable-next-line executor/no-try-catch-or-throw, executor/no-error-constructor -- boundary: the shell's tool proxy is Promise-based and surfaces rejections as component errors |
| 162 | throw new Error(APPROVAL_EXPIRED_MESSAGE); |
| 163 | } |
| 164 | // oxlint-disable-next-line executor/no-try-catch-or-throw, executor/no-error-constructor -- boundary: the shell's tool proxy is Promise-based and surfaces rejections as component errors |
| 165 | throw new Error(text || `Executor API request failed with ${response.status}`); |
| 166 | } |
| 167 | return response.json(); |
| 168 | }; |
| 169 | |
| 170 | return { |
| 171 | getHostContext: () => ({ theme: prefersDark() ? "dark" : "light" }), |
| 172 | |
| 173 | /** |
no test coverage detected