@workerdeck/server 0.6.0 → 0.9.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.
package/README.md CHANGED
@@ -39,7 +39,8 @@ import { createWorkerServer } from '@workerdeck/server'
39
39
 
40
40
  const worker = createWorkerServer({
41
41
  authenticate: async (req) => verifyMyAppToken(req.headers.authorization),
42
- allowedCwdRoots: ['/srv/checkouts'], // clamp where sessions may run
42
+ allowedCwdRoots: ['/srv/checkouts'], // where sessions may run — and what /fs serves
43
+ hostFiles: { write: true }, // /fs reads follow the roots above; writing opts in
43
44
  buildRunnerConfig: (req) => ({ ...req, env: { ...process.env } }),
44
45
  requireApiKey: true, // fail closed on subscription credentials
45
46
  })
@@ -57,6 +58,9 @@ Routes (default `basePath: '/v1'`):
57
58
  | `POST /v1/sessions/:id/permissions/:requestId` | Resolve a pending approval over REST |
58
59
  | `GET /v1/sdk-sessions?dir=…` | List the Agent SDK's on-disk sessions to offer resume |
59
60
  | `GET /v1/sessions/:id/files`, `…/files/<path>` | List and download a session's scratch-filesystem deliverables |
61
+ | `GET /v1/fs/roots`, `/fs/list?path=`, `/fs/read?path=` | Browse and read the **host's** real tree (the `allowedCwdRoots` trees, unless `hostFiles.roots` narrows them; 404 when neither is set) |
62
+ | `GET /v1/fs/find?path=&q=` | Recursive fuzzy file search under one directory — what backs `@file` completion |
63
+ | `PUT /v1/fs/write` | Save a host file — needs `hostFiles.write`, and always carries the hash it replaces |
60
64
  | `POST /v1/executions/:executionId/result` | Deliver a deferred execution's result, waking a parked session |
61
65
  | `GET /v1/profiles`, `GET /v1/profiles/:name` | What sessions may run as (+ a view-only config snapshot) |
62
66
  | `GET/POST /v1/jobs`, `GET/DELETE /v1/jobs/:id` | Job queue (when `queue` is configured) |
package/build/index.d.mts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { IncomingMessage, Server, ServerResponse } from "node:http";
2
- import { BridgeAnswer, BrowserBridgeExecutor, ClaudeAuthProbe, ParkedExecution, Runner, RunnerSnapshot, SessionRunnerConfig, ToolExecutionResult } from "@workerdeck/core";
2
+ import { AttachmentInput, BridgeAnswer, BrowserBridgeExecutor, ClaudeAuthProbe, EngineAdapter, ParkedExecution, Runner, RunnerSnapshot, SessionRunnerConfig, ToolExecutionResult } from "@workerdeck/core";
3
3
  import { JobQueue, QueueAdapter } from "@workerdeck/queue";
4
- import { CreateSessionRequest, JobEvent, ProfileInfo, SdkSessionSummary, ServerFrame, SessionInfo, SessionNotification, SessionWebhookConfig } from "@workerdeck/protocol";
4
+ import { CreateSessionRequest, JobEvent, MessageAttachment, ProfileEngine, ProfileInfo, SdkSessionSummary, ServerFrame, SessionInfo, SessionNotification, SessionWebhookConfig } from "@workerdeck/protocol";
5
5
 
6
6
  //#region src/registry.d.ts
7
7
  type SessionRegistryOptions = {
@@ -327,6 +327,62 @@ type WorkerServerOptions = {
327
327
  /** Required unless `allowUnauthenticated: true` — the worker must never be exposed bare. */authenticate?: Authenticator; /** Explicit opt-in to run without auth (local dev only). */
328
328
  allowUnauthenticated?: boolean; /** If set, session cwd must resolve inside one of these roots. Strongly recommended. */
329
329
  allowedCwdRoots?: string[];
330
+ /**
331
+ * The host filesystem routes (`{basePath}/fs/*`) — browse and read the
332
+ * operator's real project tree, and optionally write to it.
333
+ *
334
+ * **Reading follows {@link allowedCwdRoots} and needs no grant of its own.**
335
+ * A caller holding the auth key can already start a session in any allowed root
336
+ * and have the agent read whatever is in it, so serving those same trees over
337
+ * `/fs` adds no authority — it only removes the absurdity of going through a
338
+ * language model to `cat` a file. Set `roots` here only to *narrow* that (or to
339
+ * expose a tree sessions may not run in).
340
+ *
341
+ * With neither set the routes 404. That is not the same as inheriting
342
+ * `allowedCwdRoots`' permissive "unset means anywhere": no cwd policy means
343
+ * there is nothing to inherit, and "anywhere" is a statement about paths the
344
+ * operator types at a keyboard, not one about what a phone may read.
345
+ *
346
+ * **Writing is a separate opt-in**, because it is the one part that is not
347
+ * already implied. An agent's writes go through the permission flow; a `PUT` to
348
+ * `/fs/write` does not. These routes are operator-privileged by design — the
349
+ * caller is the operator — but that is a reason to make the bypass deliberate,
350
+ * not a reason to skip the switch.
351
+ *
352
+ * Containment is *not* `cwdAllowed`, whichever roots are in play: these routes
353
+ * walk paths the agent may have authored, so a symlink can escape a lexical
354
+ * prefix check. See `host-files.ts` — canonicalize, then re-check.
355
+ */
356
+ hostFiles?: {
357
+ /** Absolute paths. Unset inherits {@link allowedCwdRoots}; an explicit empty
358
+ * array disables the routes (a policy, not an absence). */
359
+ roots?: string[]; /** Enable `PUT {basePath}/fs/write`. Default false — read-only. */
360
+ write?: boolean;
361
+ /** Refuse reads above this (413) rather than streaming a gigabyte to a phone.
362
+ * Default 1 MiB. Writes are bounded by {@link maxBodyBytes} instead. */
363
+ maxFileBytes?: number;
364
+ /** Cap on entries returned per directory (the response says `truncated`).
365
+ * Default 5000. */
366
+ maxEntries?: number;
367
+ /** Directory names `GET /fs/find` will not descend into. Defaults to
368
+ * `DEFAULT_IGNORED_DIRS` (`.git`, `node_modules`, build output…) — the thing
369
+ * that keeps a per-keystroke search cheap on a real source tree. */
370
+ ignore?: string[];
371
+ };
372
+ /**
373
+ * Message attachments (`{basePath}/sessions/:id/attachments`) — the photos and
374
+ * files a client sends alongside a message. Always on; these knobs only size it.
375
+ *
376
+ * There is no grant to make here the way `hostFiles.write` is one: an upload
377
+ * lands in the session's own in-memory hold and reaches the model as message
378
+ * content, which is exactly what typing does. What it *can* do is cost memory,
379
+ * so both caps default low enough that a phone camera roll cannot fill the
380
+ * gateway.
381
+ */
382
+ attachments?: {
383
+ /** Largest single upload; over it is a 413. Default 10 MiB. */maxFileBytes?: number; /** Ceiling on what one session holds at once. Default 64 MiB. */
384
+ maxSessionBytes?: number;
385
+ };
330
386
  /**
331
387
  * Named Claude Code config directories sessions can run under (each becomes the
332
388
  * session's CLAUDE_CONFIG_DIR — settings, memory, skills, and the credentials the
@@ -413,8 +469,11 @@ type WorkerServerOptions = {
413
469
  probe?: ClaudeAuthProbe;
414
470
  timeoutMs?: number;
415
471
  };
416
- /** Injectable lister for GET /sdk-sessions (tests). Defaults to the SDK's listSessions,
417
- * which reads the Agent SDK's on-disk session store. */
472
+ /** Injectable lister for GET /sdk-sessions (tests) honored for the CLAUDE
473
+ * engine only, like the injectable claude auth probe (it predates the adapter
474
+ * layer). Defaults to the claude adapter's lister (the SDK's on-disk session
475
+ * store); other engines always answer through their adapter's
476
+ * `listSessions`. */
418
477
  listSdkSessions?: SdkSessionLister;
419
478
  /** Enable the job queue (`/jobs` + `/queue` routes). Jobs run as ordinary registry
420
479
  * sessions — attachable over the sessions WS — governed by these limits. */
@@ -467,6 +526,8 @@ type WorkerServerOptions = {
467
526
  * Kept as a host hook so the server package neither imports a model SDK nor
468
527
  * decides how provider credentials are resolved: the factory reads them from
469
528
  * the operator's environment, exactly like the Claude credential chain.
529
+ * `claude` and `codex` profiles never come through here — those engines ship
530
+ * as in-repo adapters (`@workerdeck/core`'s `getEngineAdapter`).
470
531
  *
471
532
  * May be async: assembly that has to await — a per-session MCP connect, a
472
533
  * credential lookup — belongs here, with `AiSdkRunnerConfig.onClose` as the
@@ -474,6 +535,13 @@ type WorkerServerOptions = {
474
535
  * the message, a job goes straight to `failed`.
475
536
  */
476
537
  createEngineRunner?: (context: EngineRunnerContext) => Runner | Promise<Runner>;
538
+ /**
539
+ * Adapter overrides, keyed by engine — **for tests only** (the server
540
+ * integration suite injects a fake codex engine so `pnpm test` spawns no
541
+ * binary). Not a public extension point: third engines belong in core as
542
+ * adapters, or behind `createEngineRunner` as provider profiles.
543
+ */
544
+ engines?: Partial<Record<ProfileEngine, EngineAdapter>>;
477
545
  };
478
546
  type EngineRunnerContext = {
479
547
  /** The session config, with profile defaults already applied. */config: SessionRunnerConfig; /** The profile that selected this engine. */
@@ -526,5 +594,69 @@ type WorkerServer = {
526
594
  };
527
595
  declare function createWorkerServer(options?: WorkerServerOptions): WorkerServer;
528
596
  //#endregion
529
- export { type Authenticator, BridgeHub, type BridgeHubOptions, type EngineRunnerContext, type FileSessionStoreOptions, MemorySessionStore, type ParkedSessionRecord, type ProfileStore, type QueueServerOptions, type SdkSessionLister, type SessionNotificationOptions, SessionNotifier, SessionParkManager, type SessionParkOptions, SessionRegistry, type SessionRegistryOptions, type SessionStore, type WorkerServer, type WorkerServerOptions, createFileProfileStore, createFileSessionStore, createMemoryProfileStore, createWorkerServer, toDurableRecord };
597
+ //#region src/attachments.d.ts
598
+ type AttachmentStoreOptions = {
599
+ /** Largest single upload. Default 10 MiB. */maxFileBytes?: number; /** Ceiling on everything one session is holding. Default 64 MiB. */
600
+ maxSessionBytes?: number;
601
+ };
602
+ type AttachmentRejection = {
603
+ code: 'too_large';
604
+ message: string;
605
+ } | {
606
+ code: 'session_full';
607
+ message: string;
608
+ } | {
609
+ code: 'unsupported_type';
610
+ message: string;
611
+ } | {
612
+ code: 'empty';
613
+ message: string;
614
+ };
615
+ type PutResult = {
616
+ ok: true;
617
+ attachment: MessageAttachment;
618
+ } | {
619
+ ok: false;
620
+ error: AttachmentRejection;
621
+ };
622
+ /**
623
+ * Per-session hold for files the user attached to a message.
624
+ *
625
+ * In memory, and deliberately so. An attachment is only *needed* for the instant
626
+ * between the upload and the message that names it; everything after that is
627
+ * convenience (a client re-rendering a thumbnail after a reattach). That is the
628
+ * same bargain `GET /sessions/:id/files` makes — the session's lifetime, no
629
+ * durability tier — and it keeps the gateway from accumulating a photo library
630
+ * on disk that nobody asked it to look after.
631
+ *
632
+ * Both caps are enforced here rather than at the route, so a host embedding the
633
+ * server cannot forget one: a single file that is too big is a 413, and so is a
634
+ * session whose total would go over.
635
+ */
636
+ declare class AttachmentStore {
637
+ #private;
638
+ constructor(options?: AttachmentStoreOptions);
639
+ get maxFileBytes(): number;
640
+ put(sessionId: string, name: string, mediaType: string, body: Buffer): PutResult;
641
+ /** The stored record, bytes included — for the download route and for the send
642
+ * path that turns ids into content blocks. */
643
+ get(sessionId: string, id: string): AttachmentInput | undefined;
644
+ /**
645
+ * Resolve the ids a `user_message` named, in the order given.
646
+ *
647
+ * Missing ids are reported rather than skipped: a message that quietly lost its
648
+ * picture reads as the model ignoring it, which is a far worse failure than a
649
+ * command that errors.
650
+ */
651
+ resolve(sessionId: string, ids: readonly string[]): {
652
+ ok: true;
653
+ attachments: AttachmentInput[];
654
+ } | {
655
+ ok: false;
656
+ missing: string[];
657
+ };
658
+ drop(sessionId: string): void;
659
+ }
660
+ //#endregion
661
+ export { AttachmentStore, type AttachmentStoreOptions, type Authenticator, BridgeHub, type BridgeHubOptions, type EngineRunnerContext, type FileSessionStoreOptions, MemorySessionStore, type ParkedSessionRecord, type ProfileStore, type QueueServerOptions, type SdkSessionLister, type SessionNotificationOptions, SessionNotifier, SessionParkManager, type SessionParkOptions, SessionRegistry, type SessionRegistryOptions, type SessionStore, type WorkerServer, type WorkerServerOptions, createFileProfileStore, createFileSessionStore, createMemoryProfileStore, createWorkerServer, toDurableRecord };
530
662
  //# sourceMappingURL=index.d.mts.map