@zocomputer/agent-sdk 0.5.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 (88) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +673 -0
  3. package/dist/attachments.js +52 -0
  4. package/dist/gateway-fetch.js +67 -0
  5. package/dist/index.js +4169 -0
  6. package/dist/initiator-auth.js +49 -0
  7. package/dist/platform/agent-sandbox/index.js +691 -0
  8. package/dist/platform/cloud-tools/image.js +498 -0
  9. package/dist/platform/cloud-tools/index.js +507 -0
  10. package/dist/platform/cloud-tools/web-search.js +87 -0
  11. package/dist/platform/runtime-ai/gateway.js +87 -0
  12. package/dist/platform/runtime-ai/index.js +91 -0
  13. package/dist/platform/runtime-ai/register.js +88 -0
  14. package/dist/platform/runtime-ai/session-fetch.js +54 -0
  15. package/dist/platform/runtime-auth/index.js +141 -0
  16. package/dist/state-files.js +287 -0
  17. package/dist/state-sandbox.js +383 -0
  18. package/dist/state.js +29 -0
  19. package/dist/steer-inbox.js +84 -0
  20. package/dist/steer.js +83 -0
  21. package/package.json +143 -0
  22. package/platform/agent-sandbox/api-client.ts +196 -0
  23. package/platform/agent-sandbox/index.ts +27 -0
  24. package/platform/agent-sandbox/pure.ts +83 -0
  25. package/platform/agent-sandbox/sftp.ts +141 -0
  26. package/platform/agent-sandbox/ssh-connection.ts +165 -0
  27. package/platform/agent-sandbox/ssh-exec.ts +98 -0
  28. package/platform/agent-sandbox/ssh-session.ts +487 -0
  29. package/platform/agent-sandbox/zo-backend.ts +88 -0
  30. package/platform/agent-sandbox/zo-sandbox.ts +39 -0
  31. package/platform/cloud-tools/image-path.ts +44 -0
  32. package/platform/cloud-tools/image.ts +225 -0
  33. package/platform/cloud-tools/index.ts +22 -0
  34. package/platform/cloud-tools/state-files.ts +368 -0
  35. package/platform/cloud-tools/tool-meta.ts +32 -0
  36. package/platform/cloud-tools/web-search.ts +15 -0
  37. package/platform/runtime-ai/gateway.ts +76 -0
  38. package/platform/runtime-ai/index.ts +20 -0
  39. package/platform/runtime-ai/register.ts +26 -0
  40. package/platform/runtime-ai/session-fetch.ts +124 -0
  41. package/platform/runtime-auth/index.ts +331 -0
  42. package/src/async-tasks.ts +273 -0
  43. package/src/attachments.ts +109 -0
  44. package/src/backgroundable.ts +88 -0
  45. package/src/bounded-output.ts +159 -0
  46. package/src/dir-conventions.ts +238 -0
  47. package/src/extract/cache.ts +40 -0
  48. package/src/extract/docx.ts +18 -0
  49. package/src/extract/pdf.ts +54 -0
  50. package/src/extract/sheet.ts +56 -0
  51. package/src/file-kind.ts +258 -0
  52. package/src/file-view.ts +80 -0
  53. package/src/gateway-fetch.ts +115 -0
  54. package/src/glob-match.ts +13 -0
  55. package/src/hooks.ts +213 -0
  56. package/src/index.ts +419 -0
  57. package/src/initiator-auth.ts +81 -0
  58. package/src/instructions.ts +224 -0
  59. package/src/list-files.ts +41 -0
  60. package/src/mock-model.ts +572 -0
  61. package/src/park-delivery.ts +247 -0
  62. package/src/path-locks.ts +52 -0
  63. package/src/read-file-content.ts +142 -0
  64. package/src/read-text.ts +40 -0
  65. package/src/redeliver.ts +155 -0
  66. package/src/run.ts +159 -0
  67. package/src/sandbox-io.ts +414 -0
  68. package/src/state-files.ts +460 -0
  69. package/src/state-sandbox.ts +710 -0
  70. package/src/state.ts +96 -0
  71. package/src/steer-inbox.ts +105 -0
  72. package/src/steer-tool.ts +83 -0
  73. package/src/steer.ts +146 -0
  74. package/src/task.ts +320 -0
  75. package/src/tools/bash.ts +143 -0
  76. package/src/tools/edit.ts +58 -0
  77. package/src/tools/glob.ts +56 -0
  78. package/src/tools/grep.ts +188 -0
  79. package/src/tools/read.ts +319 -0
  80. package/src/tools/tasks.ts +241 -0
  81. package/src/tools/webfetch.ts +368 -0
  82. package/src/tools/write.ts +42 -0
  83. package/src/walk.ts +112 -0
  84. package/src/watch-output.ts +104 -0
  85. package/src/web-fetch.ts +324 -0
  86. package/src/web-page.ts +179 -0
  87. package/src/workspace-io.ts +225 -0
  88. package/src/workspace.ts +41 -0
package/src/index.ts ADDED
@@ -0,0 +1,419 @@
1
+ import { join } from "node:path";
2
+ import {
3
+ DEFAULT_MAX_INLINE_IMAGE_BYTES,
4
+ DEFAULT_MAX_INLINE_MEDIA_BYTES,
5
+ } from "./attachments";
6
+ import { createTaskRegistry, type TaskRegistry } from "./async-tasks";
7
+ import { createBashOp, type BackgroundableOp } from "./backgroundable";
8
+ import { TOOL_OUTPUT_DIRNAME } from "./bounded-output";
9
+ import { createDirConventionsTracker } from "./dir-conventions";
10
+ import {
11
+ createCommunicationInstruction,
12
+ createHitlInstruction,
13
+ createParallelToolsInstruction,
14
+ createRepoConventionsInstruction,
15
+ createSubagentInstruction,
16
+ createWorkflowInstruction,
17
+ type SubagentRosterEntry,
18
+ } from "./instructions";
19
+ import { createCommandRunner, type CommandRunner } from "./run";
20
+ import { createSteerInbox, type SteerInbox } from "./steer-inbox";
21
+ import { createSteerWrapper } from "./steer-tool";
22
+ import { createBashTool } from "./tools/bash";
23
+ import { createEditTool } from "./tools/edit";
24
+ import { createGlobTool } from "./tools/glob";
25
+ import { createGrepTool } from "./tools/grep";
26
+ import { createReadTool } from "./tools/read";
27
+ import { createTasksTools } from "./tools/tasks";
28
+ import { createWebFetchTool } from "./tools/webfetch";
29
+ import { createWriteTool } from "./tools/write";
30
+ import { createWorkspace, type Workspace } from "./workspace";
31
+ import { sandboxIoProvider, type SandboxIoOptions } from "./sandbox-io";
32
+
33
+ // One call wires the whole standard library for a real-filesystem eve agent:
34
+ // a workspace-scoped toolset (read/edit/write/glob/grep + host bash + webfetch),
35
+ // the background-task machinery (run_async/check_tasks/await_task over a
36
+ // persisted registry), and the matching dynamic instructions. Each
37
+ // `agent/tools/<name>.ts` file re-exports one tool — the filename is the wire
38
+ // name, so agents keep naming control (and can vacate eve's built-in
39
+ // read_file/write_file/bash with disable shims). Everything is also exported à
40
+ // la carte for agents that want a subset.
41
+
42
+ export interface StdlibOptions {
43
+ /** Directory the agent works in; file tools refuse paths that escape it. */
44
+ workspaceRoot: string;
45
+ /**
46
+ * Directory for the stdlib's local state: the background-task store
47
+ * (`tasks.json`) and spilled oversized tool output (`tool-outputs/`).
48
+ * Typically a gitignored dot-directory inside the workspace.
49
+ */
50
+ stateDir: string;
51
+ /**
52
+ * What tool descriptions call the workspace — "repo" for a coding agent in a
53
+ * git checkout, "project", … Defaults to "workspace". Interpolated once at
54
+ * build time, so descriptions stay prompt-cache stable.
55
+ */
56
+ workspaceNoun?: string;
57
+ /**
58
+ * Replaces bash's default "avoid interactive CLIs" warning — point it at
59
+ * your agent's real-terminal tool if it has one.
60
+ */
61
+ bashInteractiveHint?: string;
62
+ /** Extra run_async-able ops beyond bash (see defineOp). */
63
+ extraBackgroundables?: (ctx: {
64
+ workspace: Workspace;
65
+ runner: CommandRunner;
66
+ }) => readonly BackgroundableOp[];
67
+ /**
68
+ * When `read` or `webfetch` hits an image, embed its bytes on the tool
69
+ * result so a client can re-inject it as a viewable attachment on the next
70
+ * turn (see ./attachments and the README). Requires a client that consumes
71
+ * the attachment (rib, Zo); generic eve consumers can leave this off and get
72
+ * the metadata-only "ask the user" note. Defaults to `true`.
73
+ */
74
+ attachImagesToChat?: boolean;
75
+ /**
76
+ * Max image size (bytes) to inline on the tool result; larger images fall
77
+ * back to the metadata-only note. Defaults to 3 MiB — eve's attachment
78
+ * staging inlines images up to that size at model-call time and text-stubs
79
+ * bigger ones, so staying under it keeps the "queued" promise truthful.
80
+ * Also bounds durable-stream bloat (the data URL rides the stream once per
81
+ * read/fetch).
82
+ */
83
+ maxInlineImageBytes?: number;
84
+ /**
85
+ * Attach video files (mp4/mov/webm/mkv/avi) the way images attach. Defaults
86
+ * to `false`: video input is provider-gated (Gemini accepts it; Claude and
87
+ * most others don't), and eve's attachment staging currently hydrates only
88
+ * images/PDFs back into the model call (see the README's eve-maintainer
89
+ * notes) — enable once both hold for your agent.
90
+ */
91
+ attachVideoToChat?: boolean;
92
+ /** Attach audio files (mp3/wav/ogg/flac/m4a). Same gating as video. */
93
+ attachAudioToChat?: boolean;
94
+ /**
95
+ * Max video/audio size (bytes) to inline on the tool result. Defaults to
96
+ * 10 MB (read's stat guard rejects bigger files outright; webfetch's 5 MB
97
+ * response cap bites first for fetches).
98
+ */
99
+ maxInlineMediaBytes?: number;
100
+ /**
101
+ * Verify command mentioned by the workflow instruction (e.g. "bun run
102
+ * check"). Interpolated once at build time; omit for a generic hint.
103
+ */
104
+ verifyCommandHint?: string;
105
+ /**
106
+ * Attach a directory's conventions file to the first `read` under it, once
107
+ * per directory per session (see ./dir-conventions.ts). The root file is
108
+ * excluded — `instructions.repoConventions` covers it. Defaults to `true`.
109
+ */
110
+ injectDirConventions?: boolean;
111
+ /**
112
+ * Conventions filename the read riders look for. Defaults to "AGENTS.md".
113
+ */
114
+ conventionsFileName?: string;
115
+ /**
116
+ * Enable steering: a directory (typically under `stateDir`) where UI
117
+ * clients queue mid-turn messages (see ./steer-inbox). Every stdlib tool is
118
+ * wrapped so queued messages ride the next completing tool result; pass the
119
+ * same dir to `createParkDeliveryHook({ steer })` so messages a turn ends
120
+ * before delivering go out on park instead.
121
+ */
122
+ steer?: { dir: string };
123
+ /**
124
+ * Declared subagents the delegation playbook should route work to (e.g. the
125
+ * model-tier task preset — see ./task.ts). Grows `instructions.subagents`
126
+ * with a "Choosing a subagent" section. Interpolated once at build time.
127
+ */
128
+ subagentRoster?: readonly SubagentRosterEntry[];
129
+ }
130
+
131
+ export function createStdlib(options: StdlibOptions) {
132
+ const noun = options.workspaceNoun ?? "workspace";
133
+ const workspace = createWorkspace(options.workspaceRoot);
134
+ const spillDir = join(options.stateDir, TOOL_OUTPUT_DIRNAME);
135
+ const runner = createCommandRunner({ workspace, spillDir });
136
+ const registry: TaskRegistry = createTaskRegistry({
137
+ storePath: join(options.stateDir, "tasks.json"),
138
+ });
139
+ const backgroundables: readonly BackgroundableOp[] = [
140
+ createBashOp(runner),
141
+ ...(options.extraBackgroundables?.({ workspace, runner }) ?? []),
142
+ ];
143
+ const conventionsFileName = options.conventionsFileName ?? "AGENTS.md";
144
+ const dirConventions =
145
+ (options.injectDirConventions ?? true)
146
+ ? {
147
+ tracker: createDirConventionsTracker({
148
+ workspaceRoot: workspace.root,
149
+ fileName: conventionsFileName,
150
+ }),
151
+ fileName: conventionsFileName,
152
+ }
153
+ : undefined;
154
+ const steerInbox: SteerInbox | null = options.steer
155
+ ? createSteerInbox({ dir: options.steer.dir })
156
+ : null;
157
+ const steer = createSteerWrapper(steerInbox);
158
+ return {
159
+ workspace,
160
+ runner,
161
+ registry,
162
+ spillDir,
163
+ backgroundables,
164
+ steerInbox,
165
+ tools: {
166
+ read: steer(
167
+ createReadTool({
168
+ workspace,
169
+ noun,
170
+ attachImagesToChat: options.attachImagesToChat ?? true,
171
+ maxInlineImageBytes:
172
+ options.maxInlineImageBytes ?? DEFAULT_MAX_INLINE_IMAGE_BYTES,
173
+ attachVideoToChat: options.attachVideoToChat ?? false,
174
+ attachAudioToChat: options.attachAudioToChat ?? false,
175
+ maxInlineMediaBytes:
176
+ options.maxInlineMediaBytes ?? DEFAULT_MAX_INLINE_MEDIA_BYTES,
177
+ dirConventions,
178
+ }),
179
+ ),
180
+ edit: steer(createEditTool({ workspace, noun })),
181
+ write: steer(createWriteTool({ workspace, noun })),
182
+ glob: steer(createGlobTool({ workspace, noun })),
183
+ grep: steer(createGrepTool({ workspace, noun, spillDir })),
184
+ bash: steer(
185
+ createBashTool({
186
+ workspace,
187
+ runner,
188
+ registry,
189
+ noun,
190
+ interactiveHint: options.bashInteractiveHint,
191
+ }),
192
+ ),
193
+ tasks: createTasksTools({ registry, backgroundables, steerInbox }),
194
+ webfetch: steer(
195
+ createWebFetchTool({
196
+ workspace,
197
+ spillDir,
198
+ attachImagesToChat: options.attachImagesToChat ?? true,
199
+ maxInlineImageBytes:
200
+ options.maxInlineImageBytes ?? DEFAULT_MAX_INLINE_IMAGE_BYTES,
201
+ attachVideoToChat: options.attachVideoToChat ?? false,
202
+ attachAudioToChat: options.attachAudioToChat ?? false,
203
+ maxInlineMediaBytes:
204
+ options.maxInlineMediaBytes ?? DEFAULT_MAX_INLINE_MEDIA_BYTES,
205
+ }),
206
+ ),
207
+ },
208
+ instructions: {
209
+ parallelTools: createParallelToolsInstruction(),
210
+ repoConventions: createRepoConventionsInstruction({ workspaceRoot: workspace.root }),
211
+ subagents: createSubagentInstruction({
212
+ workspaceNoun: noun,
213
+ roster: options.subagentRoster,
214
+ }),
215
+ workflow: createWorkflowInstruction({
216
+ workspaceNoun: noun,
217
+ verifyCommandHint: options.verifyCommandHint,
218
+ }),
219
+ communication: createCommunicationInstruction(),
220
+ hitl: createHitlInstruction(),
221
+ },
222
+ };
223
+ }
224
+
225
+ export type Stdlib = ReturnType<typeof createStdlib>;
226
+
227
+ // The sandbox-backed counterpart to the stdlib's file tools, for the hosted
228
+ // topology: the eve process runs on one machine (a Vercel Function) and the
229
+ // workspace lives in the session's sandbox (`ctx.getSandbox()`) on another.
230
+ // The stdlib's `node:fs` tools would read the harness's own disk there — the
231
+ // wrong filesystem entirely — so these route every effect through the
232
+ // sandbox session instead (see ./sandbox-io.ts). Consumers wire them exactly
233
+ // like stdlib tools (one `agent/tools/<name>.ts` re-export each) and vacate
234
+ // eve's built-in `read_file`/`write_file`/`glob`/`grep` with disable shims;
235
+ // `bash` stays eve's built-in — it is already sandbox-native.
236
+ //
237
+ // Host-process machinery that needs a local disk or shell (bash, the task
238
+ // registry, steering) is deliberately absent: on a hosted agent those either
239
+ // stay eve built-ins or don't apply.
240
+
241
+ export interface SandboxFileToolsOptions {
242
+ /**
243
+ * Absolute workspace root **inside the sandbox** (e.g. "/workspace").
244
+ * File tools refuse paths that escape it.
245
+ */
246
+ workspaceRoot: string;
247
+ /** What tool descriptions call the workspace. Defaults to "workspace". */
248
+ workspaceNoun?: string;
249
+ /**
250
+ * Resolves the sandbox session for one tool call. Defaults to
251
+ * `ctx.getSandbox()`; injectable for tests.
252
+ */
253
+ resolveSession?: SandboxIoOptions["resolveSession"];
254
+ /**
255
+ * Sandbox directory for grep's overflow match lists (spilled through the
256
+ * sandbox, so the model's follow-up `read` can reach them). Omit to keep
257
+ * the stop-at-cap behavior.
258
+ */
259
+ spillDir?: string;
260
+ /**
261
+ * See {@link StdlibOptions.attachImagesToChat}. Defaults to `false` here —
262
+ * the attachment path only works when the agent registers
263
+ * `createParkDeliveryHook` AND its runtime can reach itself over loopback
264
+ * to send the next-turn message; hosted serverless runtimes haven't
265
+ * validated that leg. Until a consumer wires and verifies it, the honest
266
+ * default is the metadata-only note (a "queued" promise that never
267
+ * delivers is the silent failure the attachment contract exists to avoid).
268
+ */
269
+ attachImagesToChat?: boolean;
270
+ /** See {@link StdlibOptions.maxInlineImageBytes}. */
271
+ maxInlineImageBytes?: number;
272
+ /** See {@link StdlibOptions.attachVideoToChat}. Defaults to `false`. */
273
+ attachVideoToChat?: boolean;
274
+ /** See {@link StdlibOptions.attachAudioToChat}. Defaults to `false`. */
275
+ attachAudioToChat?: boolean;
276
+ /** See {@link StdlibOptions.maxInlineMediaBytes}. */
277
+ maxInlineMediaBytes?: number;
278
+ /** See {@link StdlibOptions.injectDirConventions}. Defaults to `true`. */
279
+ injectDirConventions?: boolean;
280
+ /** See {@link StdlibOptions.conventionsFileName}. Defaults to "AGENTS.md". */
281
+ conventionsFileName?: string;
282
+ }
283
+
284
+ export function createSandboxFileTools(options: SandboxFileToolsOptions) {
285
+ const noun = options.workspaceNoun ?? "workspace";
286
+ const workspace = createWorkspace(options.workspaceRoot);
287
+ const io = sandboxIoProvider({
288
+ root: workspace.root,
289
+ ...(options.resolveSession !== undefined
290
+ ? { resolveSession: options.resolveSession }
291
+ : {}),
292
+ });
293
+ const conventionsFileName = options.conventionsFileName ?? "AGENTS.md";
294
+ const dirConventions =
295
+ (options.injectDirConventions ?? true)
296
+ ? {
297
+ tracker: createDirConventionsTracker({
298
+ workspaceRoot: workspace.root,
299
+ fileName: conventionsFileName,
300
+ }),
301
+ fileName: conventionsFileName,
302
+ }
303
+ : undefined;
304
+ return {
305
+ workspace,
306
+ io,
307
+ tools: {
308
+ read: createReadTool({
309
+ workspace,
310
+ noun,
311
+ io,
312
+ attachImagesToChat: options.attachImagesToChat ?? false,
313
+ maxInlineImageBytes:
314
+ options.maxInlineImageBytes ?? DEFAULT_MAX_INLINE_IMAGE_BYTES,
315
+ attachVideoToChat: options.attachVideoToChat ?? false,
316
+ attachAudioToChat: options.attachAudioToChat ?? false,
317
+ maxInlineMediaBytes:
318
+ options.maxInlineMediaBytes ?? DEFAULT_MAX_INLINE_MEDIA_BYTES,
319
+ dirConventions,
320
+ }),
321
+ edit: createEditTool({ workspace, noun, io }),
322
+ write: createWriteTool({ workspace, noun, io }),
323
+ glob: createGlobTool({ workspace, noun, io }),
324
+ grep: createGrepTool({
325
+ workspace,
326
+ noun,
327
+ io,
328
+ ...(options.spillDir !== undefined ? { spillDir: options.spillDir } : {}),
329
+ }),
330
+ },
331
+ };
332
+ }
333
+
334
+ export type SandboxFileTools = ReturnType<typeof createSandboxFileTools>;
335
+
336
+ // À la carte surface: the tool/instruction factories and every lib module the
337
+ // tools are built from, for agents that compose their own set.
338
+ export { createBashTool } from "./tools/bash";
339
+ export { createEditTool } from "./tools/edit";
340
+ export { createGlobTool } from "./tools/glob";
341
+ export { createGrepTool } from "./tools/grep";
342
+ export { createReadTool } from "./tools/read";
343
+ export { buildTasksToolset, createTasksTools } from "./tools/tasks";
344
+ export { createWebFetchTool } from "./tools/webfetch";
345
+ export { createWriteTool } from "./tools/write";
346
+
347
+ export * from "./attachments";
348
+ export {
349
+ createParkDeliveryHook,
350
+ type ParkDeliveryOptions,
351
+ } from "./hooks";
352
+ export {
353
+ buildRedeliveryMessage,
354
+ clientContinuationToken,
355
+ createRedeliveryState,
356
+ type PendingRedelivery,
357
+ redeliveryFromEvent,
358
+ type RedeliveryMessagePart,
359
+ type RedeliveryRequest,
360
+ type RedeliveryState,
361
+ } from "./redeliver";
362
+ export {
363
+ createParkDeliveryState,
364
+ type ParkDeliveryItem,
365
+ type ParkDeliveryRequest,
366
+ type ParkDeliveryState,
367
+ type ParkNotification,
368
+ postParkNotification,
369
+ setParkNotificationHandler,
370
+ } from "./park-delivery";
371
+ export * from "./async-tasks";
372
+ export * from "./backgroundable";
373
+ export * from "./bounded-output";
374
+ export * from "./dir-conventions";
375
+ export * from "./task";
376
+ export * from "./extract/cache";
377
+ export * from "./extract/docx";
378
+ export * from "./extract/pdf";
379
+ export * from "./extract/sheet";
380
+ export * from "./file-kind";
381
+ export * from "./file-view";
382
+ export * from "./glob-match";
383
+ export * from "./web-fetch";
384
+ export * from "./instructions";
385
+ export * from "./list-files";
386
+ export * from "./read-file-content";
387
+ export * from "./read-text";
388
+ export * from "./run";
389
+ export * from "./steer";
390
+ export {
391
+ createSteerInbox,
392
+ type SteerInbox,
393
+ type SteerInboxOptions,
394
+ } from "./steer-inbox";
395
+ export {
396
+ createSteerWrapper,
397
+ type SteerSource,
398
+ withSteerDelivery,
399
+ } from "./steer-tool";
400
+ export {
401
+ createMockStoryModel,
402
+ lastUserTextFrom,
403
+ markdownChunks,
404
+ MOCK_SCENARIOS,
405
+ mockScenarioFrom,
406
+ scriptActionFor,
407
+ scriptStepFrom,
408
+ toolInputFragments,
409
+ type MockScenario,
410
+ type MockScriptAction,
411
+ type MockScriptedScenario,
412
+ type MockStoryModelOptions,
413
+ type MockToolCall,
414
+ } from "./mock-model";
415
+ export * from "./sandbox-io";
416
+ export * from "./walk";
417
+ export * from "./watch-output";
418
+ export * from "./workspace";
419
+ export * from "./workspace-io";
@@ -0,0 +1,81 @@
1
+ import type { AuthFn } from "eve/channels/auth";
2
+
3
+ // The Zo channel auth for agents that live behind a Zo identity-injecting
4
+ // proxy (apps/api's builder eve-proxy and the tenant runtime router). The
5
+ // proxy verifies the browser's signed identity bearer and injects the resolved
6
+ // `{ userId, agentId }` as the trusted plaintext `x-zo-initiator` header; this
7
+ // AuthFn turns it into the session's durable `initiator` SessionAuthContext.
8
+ //
9
+ // The agent holds no secret and does NOT verify a signature — it trusts the
10
+ // header because only the edge-authenticated proxy can reach it (Vercel
11
+ // Deployment Protection; the proxy always strips a client-supplied value). An
12
+ // absent/malformed header returns null so a `none()` entry after this one in
13
+ // the channel's auth array degrades to anonymous (local dev, where there is no
14
+ // proxy to inject it) rather than 401ing.
15
+ //
16
+ // The header name + JSON shape MIRROR `@zocomputer/runtime-auth`'s
17
+ // `INITIATOR_HEADER`/`formatInitiator`/`parseInitiator` (the proxy side of the
18
+ // contract) — mirrored, not imported, so this package keeps zero
19
+ // `@zocomputer/*` deps (the same stance as `state.ts`'s vocabulary; rib's
20
+ // standalone `file:` install can't resolve workspace packages).
21
+
22
+ /** The proxy → agent initiator header (`@zocomputer/runtime-auth`'s INITIATOR_HEADER). */
23
+ export const INITIATOR_HEADER = "x-zo-initiator";
24
+
25
+ /** The initiator identity carried on `INITIATOR_HEADER`. */
26
+ export interface InitiatorIdentity {
27
+ readonly userId: string;
28
+ readonly agentId: string;
29
+ }
30
+
31
+ /** Parse-then-narrow the `INITIATOR_HEADER` value; `null` on absent/malformed. */
32
+ export function parseInitiator(value: string | null | undefined): InitiatorIdentity | null {
33
+ if (!value) return null;
34
+ let parsed: unknown;
35
+ try {
36
+ parsed = JSON.parse(value);
37
+ } catch {
38
+ return null;
39
+ }
40
+ if (typeof parsed !== "object" || parsed === null) return null;
41
+ const { userId, agentId } = parsed as Record<string, unknown>;
42
+ if (typeof userId !== "string" || !userId) return null;
43
+ if (typeof agentId !== "string" || !agentId) return null;
44
+ return { userId, agentId };
45
+ }
46
+
47
+ /** AuthFn that reads `x-zo-initiator` into the session's initiator identity. */
48
+ export const initiatorAuth: AuthFn = (request) => {
49
+ const identity = parseInitiator(request.headers.get(INITIATOR_HEADER));
50
+ if (!identity) return null;
51
+ return {
52
+ principalId: identity.userId,
53
+ principalType: "user",
54
+ authenticator: "zo-initiator",
55
+ subject: identity.userId,
56
+ attributes: { agentId: identity.agentId },
57
+ };
58
+ };
59
+
60
+ /** What a tool/hook reads off `session.auth.initiator` to authorize its work. */
61
+ interface InitiatorReadable {
62
+ readonly attributes?: Readonly<Record<string, string | readonly string[]>>;
63
+ readonly subject?: string;
64
+ }
65
+
66
+ /**
67
+ * Pull `{ userId, agentId }` off a session's `initiator` SessionAuthContext, or
68
+ * `null` when it's absent (local dev, unscoped session) or malformed —
69
+ * `agentId` from `attributes.agentId`, `userId` from `subject` (matching
70
+ * `initiatorAuth` above).
71
+ */
72
+ export function readInitiator(
73
+ initiator: InitiatorReadable | null | undefined,
74
+ ): InitiatorIdentity | null {
75
+ if (!initiator) return null;
76
+ const agentId = initiator.attributes?.agentId;
77
+ const userId = initiator.subject;
78
+ if (typeof agentId !== "string" || !agentId) return null;
79
+ if (typeof userId !== "string" || !userId) return null;
80
+ return { userId, agentId };
81
+ }