@ixo/oracle-runtime-workers 0.2.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 (215) hide show
  1. package/README.md +110 -0
  2. package/package.json +77 -0
  3. package/src/attachments/classify.ts +114 -0
  4. package/src/attachments/content-blocks.ts +51 -0
  5. package/src/attachments/download.ts +235 -0
  6. package/src/attachments/extract.ts +241 -0
  7. package/src/attachments/index.ts +84 -0
  8. package/src/attachments/magic.ts +133 -0
  9. package/src/attachments/pipeline.ts +409 -0
  10. package/src/attachments/retention.ts +205 -0
  11. package/src/attachments/route.ts +41 -0
  12. package/src/attachments/sandbox-archive.ts +90 -0
  13. package/src/attachments/sanitize.ts +35 -0
  14. package/src/attachments/types.ts +86 -0
  15. package/src/attachments/view.ts +107 -0
  16. package/src/core/env.ts +200 -0
  17. package/src/core/index.ts +513 -0
  18. package/src/core/llm.ts +842 -0
  19. package/src/core/main-agent-types.ts +123 -0
  20. package/src/core/main-agent.ts +399 -0
  21. package/src/core/manifest.ts +365 -0
  22. package/src/core/meta-tools.ts +232 -0
  23. package/src/core/middlewares/byo-history-sanitizer.ts +165 -0
  24. package/src/core/middlewares/capability-gate.ts +76 -0
  25. package/src/core/middlewares/index.ts +18 -0
  26. package/src/core/middlewares/page-context.ts +98 -0
  27. package/src/core/middlewares/safety-guardrail.ts +117 -0
  28. package/src/core/middlewares/summarization.ts +99 -0
  29. package/src/core/middlewares/tool-repetition-guard.ts +125 -0
  30. package/src/core/middlewares/tool-validation.ts +83 -0
  31. package/src/core/middlewares/work-status.ts +57 -0
  32. package/src/core/openrouter-pricing.ts +124 -0
  33. package/src/core/plugin-loader.ts +192 -0
  34. package/src/core/plugins/index.ts +6 -0
  35. package/src/core/plugins/skills/index.ts +8 -0
  36. package/src/core/plugins/skills/skills-tools.ts +356 -0
  37. package/src/core/plugins/skills/skills.plugin.ts +113 -0
  38. package/src/core/plugins/weather/index.ts +8 -0
  39. package/src/core/plugins/weather/weather-client.ts +153 -0
  40. package/src/core/plugins/weather/weather-middleware.ts +27 -0
  41. package/src/core/plugins/weather/weather-sub-agent.ts +65 -0
  42. package/src/core/plugins/weather/weather-tools.ts +103 -0
  43. package/src/core/plugins/weather/weather.plugin.ts +227 -0
  44. package/src/core/prompt-composer.ts +469 -0
  45. package/src/core/registries.ts +642 -0
  46. package/src/core/runtime-context.ts +619 -0
  47. package/src/core/state.ts +108 -0
  48. package/src/core/sub-agent-fallback.ts +187 -0
  49. package/src/core/subagent-as-tool.ts +322 -0
  50. package/src/core/template.ts +245 -0
  51. package/src/core/utils.ts +246 -0
  52. package/src/core/wrap-plugin-tool.ts +100 -0
  53. package/src/do/ambient.ts +266 -0
  54. package/src/do/contracts.ts +562 -0
  55. package/src/do/debug-timers.ts +120 -0
  56. package/src/do/fresh-stub.ts +32 -0
  57. package/src/do/gateway-retry.ts +73 -0
  58. package/src/do/secrets-adapter.ts +32 -0
  59. package/src/do/size-alerts.ts +76 -0
  60. package/src/do/sse-stream.ts +472 -0
  61. package/src/do/transcript.ts +192 -0
  62. package/src/do/turn-body.ts +84 -0
  63. package/src/do/turn-metadata.ts +146 -0
  64. package/src/do/ucan-service.ts +339 -0
  65. package/src/do/user-oracle-do.ts +3035 -0
  66. package/src/gateway-worker.ts +82 -0
  67. package/src/index.ts +237 -0
  68. package/src/llm/byo-adapter.ts +59 -0
  69. package/src/llm/byo-catalog.ts +520 -0
  70. package/src/llm/byo-client.ts +280 -0
  71. package/src/llm/byo-routes.ts +255 -0
  72. package/src/llm/byo-service.ts +836 -0
  73. package/src/llm/chatgpt-oauth.ts +430 -0
  74. package/src/llm/provider-error.ts +283 -0
  75. package/src/matrix/gateway-do.ts +918 -0
  76. package/src/matrix/ingest.ts +209 -0
  77. package/src/matrix/replay-format.ts +34 -0
  78. package/src/matrix/reply-chain.ts +109 -0
  79. package/src/matrix/room-state-codec.ts +115 -0
  80. package/src/matrix/user-homeserver.ts +106 -0
  81. package/src/matrix/work-status.ts +294 -0
  82. package/src/memory/session-history-indexer.ts +328 -0
  83. package/src/owner-store/ixo-vfs-store.ts +570 -0
  84. package/src/owner-store/matrix-media-store.ts +136 -0
  85. package/src/owner-store/migrating-store.ts +185 -0
  86. package/src/owner-store/owner-copy-errors.ts +175 -0
  87. package/src/owner-store/retry.ts +45 -0
  88. package/src/owner-store/tus-upload.ts +333 -0
  89. package/src/owner-store/types.ts +0 -0
  90. package/src/plugin-api/define-plugin.ts +45 -0
  91. package/src/plugin-api/index.ts +56 -0
  92. package/src/plugin-api/oracle-plugin.ts +145 -0
  93. package/src/plugin-api/tool-helper.ts +82 -0
  94. package/src/plugin-api/types.ts +816 -0
  95. package/src/plugin-api/ucan-errors.ts +16 -0
  96. package/src/plugins/agui/agui-agent.ts +115 -0
  97. package/src/plugins/agui/agui.plugin.ts +148 -0
  98. package/src/plugins/agui/index.ts +2 -0
  99. package/src/plugins/attachments/attachments.plugin.ts +54 -0
  100. package/src/plugins/attachments/index.ts +6 -0
  101. package/src/plugins/attachments/view-attachment-tool.ts +91 -0
  102. package/src/plugins/base64.ts +26 -0
  103. package/src/plugins/composio/composio-tools.ts +365 -0
  104. package/src/plugins/composio/composio-ucan.ts +43 -0
  105. package/src/plugins/composio/composio.plugin.ts +214 -0
  106. package/src/plugins/composio/index.ts +15 -0
  107. package/src/plugins/domain-indexer/domain-indexer-agent.ts +57 -0
  108. package/src/plugins/domain-indexer/domain-indexer-tools.ts +253 -0
  109. package/src/plugins/domain-indexer/domain-indexer.plugin.ts +100 -0
  110. package/src/plugins/domain-indexer/index.ts +3 -0
  111. package/src/plugins/editor/blocknote-bridge.ts +120 -0
  112. package/src/plugins/editor/content-session.ts +331 -0
  113. package/src/plugins/editor/content-tools.ts +686 -0
  114. package/src/plugins/editor/document-model.ts +0 -0
  115. package/src/plugins/editor/editor-access-denied-tool.ts +58 -0
  116. package/src/plugins/editor/editor-agent.ts +34 -0
  117. package/src/plugins/editor/editor-config.ts +64 -0
  118. package/src/plugins/editor/editor-mx.ts +128 -0
  119. package/src/plugins/editor/editor.plugin.ts +143 -0
  120. package/src/plugins/editor/failures.ts +154 -0
  121. package/src/plugins/editor/index.ts +31 -0
  122. package/src/plugins/editor/jsdom-shim.ts +55 -0
  123. package/src/plugins/editor/prompts.ts +59 -0
  124. package/src/plugins/editor/prop-policy.ts +147 -0
  125. package/src/plugins/editor/provider.ts +386 -0
  126. package/src/plugins/editor/room-membership.ts +95 -0
  127. package/src/plugins/editor/standalone-editor-tool.ts +200 -0
  128. package/src/plugins/firecrawl/firecrawl-agent.ts +93 -0
  129. package/src/plugins/firecrawl/firecrawl-tools.ts +220 -0
  130. package/src/plugins/firecrawl/firecrawl.plugin.ts +105 -0
  131. package/src/plugins/firecrawl/index.ts +15 -0
  132. package/src/plugins/flows/action-metadata.ts +3505 -0
  133. package/src/plugins/flows/actions.ts +190 -0
  134. package/src/plugins/flows/edit.ts +314 -0
  135. package/src/plugins/flows/errors.ts +38 -0
  136. package/src/plugins/flows/explain.ts +43 -0
  137. package/src/plugins/flows/flow-doc.ts +111 -0
  138. package/src/plugins/flows/flows.plugin.ts +129 -0
  139. package/src/plugins/flows/forms.ts +330 -0
  140. package/src/plugins/flows/index.ts +2 -0
  141. package/src/plugins/flows/linkage.ts +147 -0
  142. package/src/plugins/flows/port-types.ts +53 -0
  143. package/src/plugins/flows/prompts.ts +65 -0
  144. package/src/plugins/flows/read.ts +392 -0
  145. package/src/plugins/flows/references.ts +66 -0
  146. package/src/plugins/flows/test-support.ts +165 -0
  147. package/src/plugins/flows/tools/authoring.ts +516 -0
  148. package/src/plugins/flows/tools/discovery.ts +119 -0
  149. package/src/plugins/flows/tools/forms.ts +142 -0
  150. package/src/plugins/flows/tools/inspect.ts +165 -0
  151. package/src/plugins/flows/tools/linkage.ts +117 -0
  152. package/src/plugins/flows/tools/settings.ts +184 -0
  153. package/src/plugins/flows/translator.ts +328 -0
  154. package/src/plugins/flows/types.ts +210 -0
  155. package/src/plugins/flows/ydoc-helpers.ts +543 -0
  156. package/src/plugins/index.ts +70 -0
  157. package/src/plugins/mcp-call-timeout.ts +38 -0
  158. package/src/plugins/mcp-tool-adapter.ts +87 -0
  159. package/src/plugins/memory/index.ts +17 -0
  160. package/src/plugins/memory/memory-tools.ts +463 -0
  161. package/src/plugins/memory/memory-ucan.ts +51 -0
  162. package/src/plugins/memory/memory.plugin.ts +122 -0
  163. package/src/plugins/memory/types.ts +147 -0
  164. package/src/plugins/portal/action-log.ts +34 -0
  165. package/src/plugins/portal/index.ts +2 -0
  166. package/src/plugins/portal/portal.plugin.ts +203 -0
  167. package/src/plugins/sandbox/index.ts +34 -0
  168. package/src/plugins/sandbox/sandbox-bridge.ts +295 -0
  169. package/src/plugins/sandbox/sandbox-mcp.ts +127 -0
  170. package/src/plugins/sandbox/sandbox-write-blob.ts +103 -0
  171. package/src/plugins/sandbox/sandbox.plugin.ts +599 -0
  172. package/src/plugins/tasks/index.ts +17 -0
  173. package/src/plugins/tasks/manifest.ts +81 -0
  174. package/src/plugins/tasks/middleware.ts +219 -0
  175. package/src/plugins/tasks/tasks-tools.ts +414 -0
  176. package/src/plugins/tasks/tasks.plugin.ts +74 -0
  177. package/src/plugins/ucan-failure.ts +133 -0
  178. package/src/plugins/user-preferences/index.ts +22 -0
  179. package/src/plugins/user-preferences/schema.ts +31 -0
  180. package/src/plugins/user-preferences/user-preferences-store.ts +172 -0
  181. package/src/plugins/user-preferences/user-preferences-tool.ts +131 -0
  182. package/src/plugins/user-preferences/user-preferences.plugin.ts +137 -0
  183. package/src/plugins/vfs/index.ts +40 -0
  184. package/src/plugins/vfs/vfs-auth.ts +56 -0
  185. package/src/plugins/vfs/vfs-client.ts +655 -0
  186. package/src/plugins/vfs/vfs-content.ts +181 -0
  187. package/src/plugins/vfs/vfs-errors.ts +163 -0
  188. package/src/plugins/vfs/vfs-sandbox-tools.ts +272 -0
  189. package/src/plugins/vfs/vfs-tools.ts +635 -0
  190. package/src/plugins/vfs/vfs.plugin.ts +201 -0
  191. package/src/realtime/frontend-call-registry.ts +163 -0
  192. package/src/realtime/realtime-endpoint.ts +619 -0
  193. package/src/realtime/session-socket-hub.ts +249 -0
  194. package/src/realtime/socket-io-codec.ts +187 -0
  195. package/src/secrets/jwe.ts +74 -0
  196. package/src/secrets/pin-cipher.ts +43 -0
  197. package/src/secrets/secrets-service.ts +319 -0
  198. package/src/shell/app.ts +564 -0
  199. package/src/shell/auth.ts +290 -0
  200. package/src/sqlite/blob-codec.ts +100 -0
  201. package/src/sqlite/blob-compactor.ts +146 -0
  202. package/src/sqlite/cache-config.ts +45 -0
  203. package/src/sqlite/database.ts +643 -0
  204. package/src/sqlite/do-vfs.ts +1485 -0
  205. package/src/sqlite/index.ts +61 -0
  206. package/src/sqlite/serialization.ts +370 -0
  207. package/src/sqlite/sessions-store.ts +311 -0
  208. package/src/sqlite/sqlite-saver.ts +919 -0
  209. package/src/sqlite/vacuum-policy.ts +77 -0
  210. package/src/sqlite/wa-sqlite-loader.ts +123 -0
  211. package/src/sqlite/wasm.d.ts +10 -0
  212. package/src/tasks/schedule.ts +174 -0
  213. package/src/tasks/scheduler.ts +753 -0
  214. package/src/tasks/spec.ts +125 -0
  215. package/src/tasks/store.ts +351 -0
@@ -0,0 +1,295 @@
1
+ import { MultiServerMCPClient } from '@langchain/mcp-adapters';
2
+ import type { RuntimeContext } from '../../plugin-api/types';
3
+ import { base64ToBytes } from '../base64';
4
+ import { adaptMcpClientTools } from '../mcp-tool-adapter';
5
+ import { createDefaultAuthBuilder, SANDBOX_RUN_TOOL_NAME } from './sandbox-mcp';
6
+ import type {
7
+ SandboxMcpClientFactory,
8
+ SandboxMcpClientLike,
9
+ SandboxMcpTool,
10
+ } from './sandbox.plugin';
11
+
12
+ /** Upstream tool name the sandbox MCP surfaces for byte-perfect file writes. */
13
+ export const SANDBOX_WRITE_FILE_TOOL_NAME = 'sandbox_write_file';
14
+
15
+ /** Per-call timeout for the sandbox MCP client (matches sandbox.plugin.ts). */
16
+ const SANDBOX_MCP_TIMEOUT_MS = 180_000;
17
+
18
+ /** Only destination the sandbox accepts for `sandbox_write_file` writes. */
19
+ export const WORKSPACE_DATA_PREFIX = '/workspace/data/';
20
+
21
+ /** Sentinel echoed by the read command when the source file is absent. */
22
+ export const SANDBOX_NO_FILE_SENTINEL = '__SANDBOX_NOFILE__';
23
+
24
+ /**
25
+ * Shown when the user hasn't authorized the oracle to use the sandbox — the
26
+ * default auth builder can't mint an `ixo:sandbox` invocation, so there's no
27
+ * `Authorization` header to connect with. Non-throwing degradation.
28
+ */
29
+ export const SANDBOX_NOT_AUTHORIZED_MESSAGE =
30
+ "You haven't authorized the sandbox yet — grant sandbox access in the portal so I can move files between it and your files.";
31
+
32
+ /**
33
+ * Default sandbox MCP-client factory. Wraps a real `MultiServerMCPClient` in
34
+ * the minimal {@link SandboxMcpClientLike} surface the bridge needs, adapting
35
+ * each upstream tool into a plain {@link SandboxMcpTool} (JSON-Schema →
36
+ * Zod via the shared adapter). Tests inject a stub instead so no real MCP
37
+ * connection is opened.
38
+ */
39
+ export const defaultSandboxMcpClientFactory: SandboxMcpClientFactory = (
40
+ config,
41
+ ) => {
42
+ const client = new MultiServerMCPClient(config);
43
+ const wrapper: SandboxMcpClientLike = {
44
+ getTools: async () => adaptMcpClientTools(await client.getTools(), console),
45
+ close: () => client.close(),
46
+ };
47
+ return wrapper;
48
+ };
49
+
50
+ /** Live sandbox `sandbox_run` + `sandbox_write_file` handles plus a closer. */
51
+ export interface SandboxBridge {
52
+ run: SandboxMcpTool;
53
+ writeFile: SandboxMcpTool;
54
+ /** Always safe — swallows and logs any close error. */
55
+ close: () => Promise<void>;
56
+ }
57
+
58
+ function isRecord(v: unknown): v is Record<string, unknown> {
59
+ return typeof v === 'object' && v !== null && !Array.isArray(v);
60
+ }
61
+
62
+ /** MIME map skewed toward binary outputs; unknown extensions stay opaque. */
63
+ const EXT_MIME: Record<string, string> = {
64
+ md: 'text/markdown',
65
+ markdown: 'text/markdown',
66
+ txt: 'text/plain',
67
+ text: 'text/plain',
68
+ log: 'text/plain',
69
+ json: 'application/json',
70
+ csv: 'text/csv',
71
+ tsv: 'text/tab-separated-values',
72
+ xml: 'application/xml',
73
+ yaml: 'application/yaml',
74
+ yml: 'application/yaml',
75
+ html: 'text/html',
76
+ htm: 'text/html',
77
+ css: 'text/css',
78
+ js: 'text/javascript',
79
+ ts: 'text/plain',
80
+ py: 'text/x-python',
81
+ sh: 'application/x-sh',
82
+ pdf: 'application/pdf',
83
+ png: 'image/png',
84
+ jpg: 'image/jpeg',
85
+ jpeg: 'image/jpeg',
86
+ gif: 'image/gif',
87
+ webp: 'image/webp',
88
+ svg: 'image/svg+xml',
89
+ };
90
+
91
+ /** Best-effort MIME type for a path, from its extension. */
92
+ export function inferMimeFromPath(path: string): string {
93
+ const dot = path.lastIndexOf('.');
94
+ const ext = dot >= 0 ? path.slice(dot + 1).toLowerCase() : '';
95
+ return EXT_MIME[ext] ?? 'application/octet-stream';
96
+ }
97
+
98
+ /** `true` when the path sits under the sandbox's writable data root. */
99
+ export function isUnderWorkspaceData(path: string): boolean {
100
+ return path.startsWith(WORKSPACE_DATA_PREFIX);
101
+ }
102
+
103
+ /**
104
+ * `true` when a sandbox path can't be safely single-quoted into a shell
105
+ * command (`sandbox_run`). We reject rather than risk breaking the command.
106
+ */
107
+ export function hasShellUnsafeChars(path: string): boolean {
108
+ return path.includes("'") || path.includes('\n') || path.includes('\0');
109
+ }
110
+
111
+ function safeStringify(v: unknown): string {
112
+ try {
113
+ return JSON.stringify(v) ?? String(v);
114
+ } catch {
115
+ return String(v);
116
+ }
117
+ }
118
+
119
+ function truncate(s: string, max = 300): string {
120
+ return s.length > max ? `${s.slice(0, max)}…` : s;
121
+ }
122
+
123
+ export interface SandboxOutcome {
124
+ /** `false` when the envelope reports `success:false` or a non-zero exit. */
125
+ ok: boolean;
126
+ /** Captured stdout when the envelope exposes it, else the whole result. */
127
+ output: string;
128
+ /** The stringified result, for surfacing in an error message. */
129
+ text: string;
130
+ }
131
+
132
+ /**
133
+ * Read a sandbox MCP result defensively. The upstream returns either a JSON
134
+ * envelope (`{ success, exitCode, output, ... }`) or a bare string; both are
135
+ * handled. `output` is the captured stdout when present, otherwise the whole
136
+ * stringified result.
137
+ *
138
+ * ASSUMPTION (verify against a live sandbox): stdout lives in the `output`
139
+ * field and `success` / `exitCode` are top-level — the shape the sandbox
140
+ * manifest documents.
141
+ */
142
+ export function readSandboxResult(result: unknown): SandboxOutcome {
143
+ const text = typeof result === 'string' ? result : safeStringify(result);
144
+
145
+ let envelope: unknown = isRecord(result) ? result : undefined;
146
+ if (typeof result === 'string') {
147
+ try {
148
+ envelope = JSON.parse(result);
149
+ } catch {
150
+ envelope = undefined;
151
+ }
152
+ }
153
+
154
+ let output: string | undefined;
155
+ let success: boolean | undefined;
156
+ let exitCode: number | undefined;
157
+ if (isRecord(envelope)) {
158
+ if (typeof envelope.output === 'string') output = envelope.output;
159
+ if (typeof envelope.success === 'boolean') success = envelope.success;
160
+ if (typeof envelope.exitCode === 'number') exitCode = envelope.exitCode;
161
+ }
162
+
163
+ const failedByFlag = success === false || /"success"\s*:\s*false/i.test(text);
164
+ const failedByExit = exitCode !== undefined && exitCode !== 0;
165
+ return { ok: !failedByFlag && !failedByExit, output: output ?? text, text };
166
+ }
167
+
168
+ /**
169
+ * Mint sandbox auth headers, connect an MCP client, and resolve the two tools
170
+ * the bridge needs. Non-throwing: every failure comes back as `{ error }` with
171
+ * an agent-facing message. On success the caller MUST `close()` in a finally.
172
+ */
173
+ export async function getSandboxBridge(
174
+ rtCtx: RuntimeContext,
175
+ sandboxMcpUrl: string,
176
+ mcpClientFactory: SandboxMcpClientFactory = defaultSandboxMcpClientFactory,
177
+ ): Promise<SandboxBridge | { error: string }> {
178
+ let headers: Record<string, string>;
179
+ try {
180
+ headers = await createDefaultAuthBuilder()(
181
+ { sandboxMcpUrl, oracleSecrets: {}, userSecrets: {} },
182
+ rtCtx,
183
+ );
184
+ } catch (err) {
185
+ rtCtx.logger.warn(
186
+ `[sandbox-bridge] sandbox auth failed: ${err instanceof Error ? err.message : String(err)}`,
187
+ );
188
+ return { error: SANDBOX_NOT_AUTHORIZED_MESSAGE };
189
+ }
190
+
191
+ if (!headers.Authorization) {
192
+ return { error: SANDBOX_NOT_AUTHORIZED_MESSAGE };
193
+ }
194
+
195
+ const client = mcpClientFactory({
196
+ mcpServers: {
197
+ sandbox: {
198
+ type: 'http',
199
+ url: sandboxMcpUrl,
200
+ transport: 'http',
201
+ headers,
202
+ },
203
+ },
204
+ defaultToolTimeout: SANDBOX_MCP_TIMEOUT_MS,
205
+ useStandardContentBlocks: true,
206
+ });
207
+
208
+ const close = async (): Promise<void> => {
209
+ try {
210
+ await client.close();
211
+ } catch (err) {
212
+ rtCtx.logger.warn(
213
+ `[sandbox-bridge] failed to close sandbox MCP client: ${err instanceof Error ? err.message : String(err)}`,
214
+ );
215
+ }
216
+ };
217
+
218
+ let tools: SandboxMcpTool[];
219
+ try {
220
+ tools = await client.getTools();
221
+ } catch (err) {
222
+ await close();
223
+ rtCtx.logger.warn(
224
+ `[sandbox-bridge] could not list sandbox tools: ${err instanceof Error ? err.message : String(err)}`,
225
+ );
226
+ return { error: 'Could not connect to the sandbox right now.' };
227
+ }
228
+
229
+ const run = tools.find((t) => t.name === SANDBOX_RUN_TOOL_NAME);
230
+ const writeFile = tools.find((t) => t.name === SANDBOX_WRITE_FILE_TOOL_NAME);
231
+ if (!run || !writeFile) {
232
+ await close();
233
+ return {
234
+ error:
235
+ 'The sandbox is not exposing the file tools needed to move files right now.',
236
+ };
237
+ }
238
+
239
+ return { run, writeFile, close };
240
+ }
241
+
242
+ /**
243
+ * Read a file out of the sandbox as bytes, base64-hopping over `sandbox_run`
244
+ * so binary content survives the transport. Non-throwing — a missing file, a
245
+ * failed command, and undecodable output all come back as `{ error }` with an
246
+ * agent-facing message.
247
+ */
248
+ export async function readSandboxFile(
249
+ bridge: Pick<SandboxBridge, 'run'>,
250
+ sandboxPath: string,
251
+ ): Promise<{ bytes: Uint8Array } | { error: string }> {
252
+ const read = readSandboxResult(
253
+ await bridge.run.invoke({
254
+ code: `test -f '${sandboxPath}' && base64 -w0 '${sandboxPath}' || echo ${SANDBOX_NO_FILE_SENTINEL}`,
255
+ }),
256
+ );
257
+ if (!read.ok) {
258
+ return {
259
+ error: `Couldn't read \`${sandboxPath}\` from the sandbox: ${truncate(read.text)}`,
260
+ };
261
+ }
262
+ const stdout = read.output.trim();
263
+ if (stdout === SANDBOX_NO_FILE_SENTINEL) {
264
+ return { error: `No file at \`${sandboxPath}\` in the sandbox.` };
265
+ }
266
+ try {
267
+ return { bytes: base64ToBytes(stdout) };
268
+ } catch {
269
+ return {
270
+ error: `Couldn't decode \`${sandboxPath}\` from the sandbox (output was not valid base64).`,
271
+ };
272
+ }
273
+ }
274
+
275
+ /**
276
+ * Write a file into the sandbox via `sandbox_write_file`. `encoding` selects
277
+ * how `content` is interpreted upstream — `base64` for binary payloads.
278
+ * Non-throwing: a failed write comes back as `{ error }`.
279
+ */
280
+ export async function writeSandboxFile(
281
+ bridge: Pick<SandboxBridge, 'writeFile'>,
282
+ sandboxPath: string,
283
+ content: string,
284
+ encoding: 'utf8' | 'base64',
285
+ ): Promise<{ ok: true } | { error: string }> {
286
+ const write = readSandboxResult(
287
+ await bridge.writeFile.invoke({ path: sandboxPath, content, encoding }),
288
+ );
289
+ if (!write.ok) {
290
+ return {
291
+ error: `Couldn't write \`${sandboxPath}\` in the sandbox: ${truncate(write.text)}`,
292
+ };
293
+ }
294
+ return { ok: true };
295
+ }
@@ -0,0 +1,127 @@
1
+ import type { RuntimeContext } from '../../plugin-api/types';
2
+
3
+ /** Tool name surfaced by the upstream sandbox MCP. */
4
+ export const SANDBOX_RUN_TOOL_NAME = 'sandbox_run';
5
+
6
+ /**
7
+ * Parse the `ORACLE_SECRETS` env value (format: `KEY1=value1,KEY2=value2`).
8
+ *
9
+ * Whitespace around keys and values is trimmed; entries lacking an `=` or
10
+ * an empty key/value are skipped silently so a stray comma can't break boot.
11
+ */
12
+ export function parseOracleSecrets(raw: string): Record<string, string> {
13
+ const out: Record<string, string> = {};
14
+ if (!raw) return out;
15
+ for (const pair of raw.split(',')) {
16
+ const eqIdx = pair.indexOf('=');
17
+ if (eqIdx <= 0) continue;
18
+ const key = pair.slice(0, eqIdx).trim();
19
+ const val = pair.slice(eqIdx + 1).trim();
20
+ if (key && val) out[key] = val;
21
+ }
22
+ return out;
23
+ }
24
+
25
+ /** Inputs to a {@link SandboxAuthBuilder} invocation. */
26
+ export interface SandboxHeaderInputs {
27
+ /** Sandbox MCP base URL — resolved to a did:web audience. */
28
+ sandboxMcpUrl: string;
29
+ /** Optional skills-service base URL for the X-Skills-Invocation header. */
30
+ skillsServiceUrl?: string;
31
+ /** Oracle operator secrets — mapped to `x-os-{key}` headers. */
32
+ oracleSecrets: Record<string, string>;
33
+ /** Per-room user secrets — mapped to `x-us-{key}` headers. */
34
+ userSecrets: Record<string, string>;
35
+ }
36
+
37
+ /**
38
+ * Mints the headers needed to call the sandbox MCP server on behalf of
39
+ * the current user. Encapsulates the four security-sensitive steps:
40
+ *
41
+ * 1. Resolve the sandbox service DID via did:web.
42
+ * 2. Mint an `ixo:sandbox` UCAN invocation → `Authorization: Bearer ...`.
43
+ * 3. Optionally mint a parallel `ixo:skills` invocation →
44
+ * `X-Skills-Invocation: ...` (forwarded by sandbox to the skills service).
45
+ * 4. Layer oracle + user secrets as `x-os-*` / `x-us-*` headers.
46
+ *
47
+ * UCAN-only — no Matrix-OpenID fallback. If minting fails for any reason
48
+ * (no signing key, no cached delegation, did:web unresolved) the returned
49
+ * header set simply omits the relevant token and the downstream service
50
+ * surfaces a clean unauthorized error instead of leaking a half-built
51
+ * request.
52
+ */
53
+ export type SandboxAuthBuilder = (
54
+ inputs: SandboxHeaderInputs,
55
+ runCtx: RuntimeContext,
56
+ ) => Promise<Record<string, string>>;
57
+
58
+ /**
59
+ * Default {@link SandboxAuthBuilder}. Service-DID resolution and caching is
60
+ * delegated to `runCtx.ucan.resolveServiceDid` (cached per origin inside the
61
+ * UCAN service), so this builder is stateless.
62
+ */
63
+ export function createDefaultAuthBuilder(): SandboxAuthBuilder {
64
+ return async (inputs, runCtx) => {
65
+ const headers: Record<string, string> = {};
66
+
67
+ const sandboxDid = await runCtx.ucan.resolveServiceDid(
68
+ inputs.sandboxMcpUrl,
69
+ );
70
+ if (sandboxDid) {
71
+ try {
72
+ const invocation = await runCtx.ucan.mintInvocation(
73
+ { did: sandboxDid, capability: 'ixo:sandbox' },
74
+ // Claim the ability the user's delegation actually grants. A `'*'`
75
+ // claim is satisfiable only by a `'*'` grant.
76
+ { can: 'sandbox/*' },
77
+ );
78
+ if (invocation) {
79
+ headers.Authorization = `Bearer ${invocation}`;
80
+ headers['X-Auth-Type'] = 'ucan';
81
+ }
82
+ } catch (error) {
83
+ const detail =
84
+ error instanceof Error
85
+ ? `${error.name}: ${error.message}`
86
+ : String(error);
87
+ runCtx.logger.warn(
88
+ `[sandbox] failed to mint sandbox UCAN invocation: ${detail}`,
89
+ );
90
+ }
91
+ }
92
+
93
+ if (inputs.skillsServiceUrl) {
94
+ const skillsDid = await runCtx.ucan.resolveServiceDid(
95
+ inputs.skillsServiceUrl,
96
+ );
97
+ if (skillsDid) {
98
+ try {
99
+ const skillsInvocation = await runCtx.ucan.mintInvocation(
100
+ { did: skillsDid, capability: 'ixo:skills' },
101
+ { can: 'skills/*' },
102
+ );
103
+ if (skillsInvocation) {
104
+ headers['X-Skills-Invocation'] = skillsInvocation;
105
+ }
106
+ } catch (error) {
107
+ const detail =
108
+ error instanceof Error
109
+ ? `${error.name}: ${error.message}`
110
+ : String(error);
111
+ runCtx.logger.warn(
112
+ `[sandbox] failed to mint skills UCAN invocation: ${detail}`,
113
+ );
114
+ }
115
+ }
116
+ }
117
+
118
+ for (const [k, v] of Object.entries(inputs.oracleSecrets)) {
119
+ headers[`x-os-${k.toLowerCase()}`] = v;
120
+ }
121
+ for (const [k, v] of Object.entries(inputs.userSecrets)) {
122
+ headers[`x-us-${k.toLowerCase()}`] = v;
123
+ }
124
+
125
+ return headers;
126
+ };
127
+ }
@@ -0,0 +1,103 @@
1
+ import { z } from 'zod';
2
+ import { tool as pluginTool } from '../../plugin-api/tool-helper';
3
+ import type { PluginTool, RuntimeContext } from '../../plugin-api/types';
4
+ import type { SandboxMcpTool } from './sandbox.plugin';
5
+
6
+ const sandboxWriteBlobSchema = z.object({
7
+ blobId: z
8
+ .string()
9
+ .describe(
10
+ 'The blobId returned by the tool that produced the value (e.g. mint_invocation). Format: `blob_<16 hex chars>`.',
11
+ ),
12
+ path: z
13
+ .string()
14
+ .describe(
15
+ 'Destination path in the sandbox, e.g. `/workspace/data/<skill>/ucan_token`. Must be under /workspace/data/.',
16
+ ),
17
+ });
18
+
19
+ const SANDBOX_WRITE_BLOB_DESCRIPTION = `Write a server-stored blob to a file in the sandbox without ever paste-relaying the value through the LLM.
20
+
21
+ Use this whenever a tool returns a \`blobId\` (e.g. \`mint_invocation\`'s response). Pass the blobId + the destination path; the runtime looks the value up server-side and calls sandbox_write_file with its content. The long opaque value (base64 CARs, JWTs, etc.) never enters the LLM context — eliminates string-corruption-in-relay as a failure mode.
22
+
23
+ Inputs:
24
+ - blobId: short hex identifier returned by the producing tool (format \`blob_<16 hex chars>\`).
25
+ - path: destination path in the sandbox. Must be under \`/workspace/data/\` (the sandbox enforces this; writes elsewhere are rejected).
26
+
27
+ Returns: \`{ success: true, path, bytesWritten }\` on success, \`{ success: false, error }\` on failure. Common errors: blob not found (re-mint and retry — for invocations this can happen on a multi-replica deployment if the lookup lands on a different process), invalid blobId format, path outside /workspace/data/.`;
28
+
29
+ export interface CreateSandboxWriteBlobToolParams {
30
+ /** Upstream MCP `sandbox_write_file` tool — looked up once per request from
31
+ * the MCP client's tool list. Required; without it the wrapper has nothing
32
+ * to forward to. */
33
+ sandboxWriteFileTool: SandboxMcpTool;
34
+ }
35
+
36
+ /**
37
+ * `sandbox_write_blob` — companion to `mint_invocation`. Takes a `blobId`
38
+ * (server-side stored long string) and a sandbox path; looks up the blob in
39
+ * the `BlobStore` and calls the underlying `sandbox_write_file` MCP tool
40
+ * with the value. The LLM only ever passes the short blobId + path — long
41
+ * base64 strings (UCAN CARs etc.) never enter its context, so they can't be
42
+ * corrupted in transit.
43
+ *
44
+ * The plugin must only register this tool when the upstream `sandbox_write_file`
45
+ * tool exists AND the request has a known user DID — without either the
46
+ * wrapper degrades into an always-failing stub, which is worse than absent.
47
+ */
48
+ export function createSandboxWriteBlobTool(
49
+ params: CreateSandboxWriteBlobToolParams,
50
+ ): PluginTool {
51
+ return pluginTool(
52
+ async (rawArgs, ctx: RuntimeContext) => {
53
+ const { blobId, path } = sandboxWriteBlobSchema.parse(rawArgs);
54
+
55
+ if (!ctx.blobStore.isValidBlobId(blobId)) {
56
+ return JSON.stringify({
57
+ success: false,
58
+ error: `Invalid blobId format. Expected blob_<16 hex chars>, got "${blobId}".`,
59
+ });
60
+ }
61
+ if (typeof path !== 'string' || !path.startsWith('/workspace/data/')) {
62
+ return JSON.stringify({
63
+ success: false,
64
+ error: `path must be under /workspace/data/. Got: "${path}".`,
65
+ });
66
+ }
67
+
68
+ const userDid = ctx.user.did;
69
+ const blob = await ctx.blobStore.get({ userDid, blobId });
70
+ if (!blob) {
71
+ return JSON.stringify({
72
+ success: false,
73
+ error: `Blob "${blobId}" not found (expired, never existed, or owned by another user). Re-mint and retry — for single-use invocations this is expected if the blob's TTL elapsed.`,
74
+ });
75
+ }
76
+
77
+ try {
78
+ const result = await params.sandboxWriteFileTool.invoke({
79
+ path,
80
+ content: blob.value,
81
+ encoding: 'utf8',
82
+ });
83
+ return JSON.stringify({
84
+ success: true,
85
+ path,
86
+ bytesWritten: blob.value.length,
87
+ blobName: blob.name,
88
+ note: typeof result === 'string' ? result : undefined,
89
+ });
90
+ } catch (err) {
91
+ return JSON.stringify({
92
+ success: false,
93
+ error: `sandbox_write_file failed: ${err instanceof Error ? err.message : String(err)}`,
94
+ });
95
+ }
96
+ },
97
+ {
98
+ name: 'sandbox_write_blob',
99
+ description: SANDBOX_WRITE_BLOB_DESCRIPTION,
100
+ schema: sandboxWriteBlobSchema,
101
+ },
102
+ );
103
+ }