@workerdeck/server 2.7.1 → 2.8.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
@@ -3,7 +3,7 @@
3
3
  The WorkerDeck gateway: HTTP + WebSocket session server over
4
4
  [`@workerdeck/core`](https://www.npmjs.com/package/@workerdeck/core). Session registry
5
5
  (create/list/attach/interrupt/kill), pluggable auth hook, replay-from-seq attach, profiles,
6
- parked-session storage, optional job-queue routes. Runs anywhere Node runs — needs a real
6
+ parked-session storage, optional job-queue routes. Runs anywhere Node runs - needs a real
7
7
  filesystem (no serverless).
8
8
 
9
9
  Want the whole thing running rather than embedded? [`workerdeck`](https://www.npmjs.com/package/workerdeck)
@@ -24,22 +24,22 @@ npm install @workerdeck/server
24
24
  ```
25
25
 
26
26
  Node ≥ 22. The Agent SDK spawns the Claude Code CLI as a long-running subprocess with filesystem
27
- state — edge/serverless functions cannot host this; realistic targets are a VM or a container.
27
+ state - edge/serverless functions cannot host this; realistic targets are a VM or a container.
28
28
  The server implements no Anthropic auth: the SDK/CLI resolves credentials from the operator's
29
29
  environment (`ANTHROPIC_API_KEY`, Bedrock/Vertex, or a personal `claude login`).
30
30
 
31
31
  ## Usage
32
32
 
33
- The host app supplies the authenticator — return a truthy principal to accept, null/undefined to
33
+ The host app supplies the authenticator - return a truthy principal to accept, null/undefined to
34
34
  reject with 401. `createWorkerServer` refuses to start without `authenticate` unless you
35
- explicitly pass `allowUnauthenticated: true` (loopback dev only — never expose that):
35
+ explicitly pass `allowUnauthenticated: true` (loopback dev only - never expose that):
36
36
 
37
37
  ```ts
38
38
  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'], // where sessions may run — and what /fs serves
42
+ allowedCwdRoots: ['/srv/checkouts'], // where sessions may run - and what /fs serves
43
43
  hostFiles: { write: true }, // /fs reads follow the roots above; writing opts in
44
44
  buildRunnerConfig: (req) => ({ ...req, env: { ...process.env } }),
45
45
  requireApiKey: true, // fail closed on subscription credentials
@@ -60,22 +60,22 @@ Routes (default `basePath: '/v1'`):
60
60
  | `GET /v1/sdk-sessions?dir=…` | List the Agent SDK's on-disk sessions to offer resume |
61
61
  | `GET /v1/sessions/:id/files`, `…/files/<path>` | List and download a session's scratch-filesystem deliverables |
62
62
  | `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) |
63
- | `GET /v1/fs/find?path=&q=` | Recursive fuzzy file search under one directory — what backs `@file` completion |
64
- | `PUT /v1/fs/write` | Save a host file — needs `hostFiles.write`, and always carries the hash it replaces |
63
+ | `GET /v1/fs/find?path=&q=` | Recursive fuzzy file search under one directory - what backs `@file` completion |
64
+ | `PUT /v1/fs/write` | Save a host file - needs `hostFiles.write`, and always carries the hash it replaces |
65
65
  | `POST /v1/executions/:executionId/result` | Deliver a deferred execution's result, waking a parked session |
66
66
  | `GET /v1/profiles`, `GET /v1/profiles/:name` | What sessions may run as (+ a view-only config snapshot) |
67
67
  | `GET/POST /v1/jobs`, `GET/DELETE /v1/jobs/:id` | Job queue (when `queue` is configured) |
68
68
  | `GET /v1/queue`, `WS /v1/queue/ws` | Queue stats / one-way live stream of job events + stats |
69
69
 
70
- Requests outside `basePath` fall through to the optional `fallback` hook — which is how the
70
+ Requests outside `basePath` fall through to the optional `fallback` hook - which is how the
71
71
  turnkey instance serves a dashboard from the same origin, so a browser's WebSocket attach can
72
72
  present a cookie.
73
73
 
74
74
  ## Profiles and the second engine
75
75
 
76
76
  A **profile** is what a session runs as, and which engine runs it. Declared at startup (or managed
77
- over the API with a `profileStore`), each one names either a Claude Code config directory — applied
78
- as that session's `CLAUDE_CONFIG_DIR` — or a model provider for the model-agnostic engine:
77
+ over the API with a `profileStore`), each one names either a Claude Code config directory - applied
78
+ as that session's `CLAUDE_CONFIG_DIR` - or a model provider for the model-agnostic engine:
79
79
 
80
80
  ```ts
81
81
  createWorkerServer({
@@ -88,19 +88,19 @@ createWorkerServer({
88
88
  {
89
89
  name: 'kimi',
90
90
  engine: 'provider',
91
- // A variable NAME — no credential is stored here or served by GET /profiles.
91
+ // A variable NAME - no credential is stored here or served by GET /profiles.
92
92
  provider: { id: 'moonshotai', model: 'kimi-k3', apiKeyEnv: 'MOONSHOT_API_KEY' },
93
93
  session: { capabilities: ['web_fetch', 'deliver_file'] },
94
94
  },
95
95
  ],
96
- // The one place a model SDK and its credentials are resolved — this package imports neither.
96
+ // The one place a model SDK and its credentials are resolved - this package imports neither.
97
97
  createEngineRunner: ({ config, profile, bridge }) => buildProviderRunner({ /* … */ }),
98
98
  })
99
99
  ```
100
100
 
101
101
  With more than one profile declared, every create must name its `profile`; with exactly one it is
102
102
  implicit, and with the option unset a `default` is auto-detected from `~/.claude`.
103
- `allowedProfiles` on the principal scopes who may run as what — the line between one worker serving
103
+ `allowedProfiles` on the principal scopes who may run as what - the line between one worker serving
104
104
  several people and account pooling. `SessionInfo.engine` and `supportsPermissionMode` let a UI hide
105
105
  affordances the provider engine doesn't have; asking for one anyway is a 400 rather than a silent
106
106
  coercion.
@@ -108,7 +108,7 @@ coercion.
108
108
  ## Parked sessions
109
109
 
110
110
  A session whose tool call can't answer in the next few seconds **parks**: the runner is torn down,
111
- its snapshot goes to a `SessionStore`, and `POST /executions/:id/result` rebuilds it — same id,
111
+ its snapshot goes to a `SessionStore`, and `POST /executions/:id/result` rebuilds it - same id,
112
112
  same event log, same seq numbering, mid-turn. Results are idempotent by `executionId`. The default
113
113
  store is in-memory (a park survives a client disconnect, not a restart); the bundled file store
114
114
  survives both, on one host:
@@ -123,12 +123,12 @@ createWorkerServer({
123
123
  })
124
124
  ```
125
125
 
126
- That directory holds each parked session's whole transcript in plaintext — protect it like the
126
+ That directory holds each parked session's whole transcript in plaintext - protect it like the
127
127
  SDK's own `~/.claude/projects`. Credentials never reach it. One process per directory.
128
128
 
129
129
  ## Job queue
130
130
 
131
- Pass `queue` options to mount the job routes — one-shot unattended runs with bounded concurrency,
131
+ Pass `queue` options to mount the job routes - one-shot unattended runs with bounded concurrency,
132
132
  token budgets, retries, and webhook delivery:
133
133
 
134
134
  ```ts
@@ -145,7 +145,7 @@ const worker = createWorkerServer({
145
145
  })
146
146
  ```
147
147
 
148
- Job sessions are ordinary registry sessions — attachable over the sessions WS — and go through
148
+ Job sessions are ordinary registry sessions - attachable over the sessions WS - and go through
149
149
  the same `buildRunnerConfig` hook and auth-provenance watcher as client sessions. The in-memory
150
150
  adapter is single-process and non-persistent; implement `QueueAdapter` against a shared store for
151
151
  anything beyond one trusted host.
@@ -153,7 +153,7 @@ anything beyond one trusted host.
153
153
  ## Session notifications
154
154
 
155
155
  The live WebSocket only reaches someone who has one open. `notifications` is the outbound
156
- channel for everyone else — the four moments a person acts on, POSTed to a URL you control:
156
+ channel for everyone else - the four moments a person acts on, POSTed to a URL you control:
157
157
 
158
158
  ```ts
159
159
  const worker = createWorkerServer({
@@ -166,13 +166,13 @@ const worker = createWorkerServer({
166
166
  })
167
167
  ```
168
168
 
169
- `permission_requested`, `turn_completed`, `session_error`, `session_closed` — delivered as a
169
+ `permission_requested`, `turn_completed`, `session_error`, `session_closed` - delivered as a
170
170
  `SessionNotification`, ordered per session, retried with exponential backoff. Server-wide, unlike
171
171
  the queue's per-job `webhook`: the point is hearing about sessions you neither created nor are
172
172
  attached to, and every registry session qualifies (job runs and rebuilt parked sessions included).
173
173
 
174
174
  `permission_requested` carries the whole `PermissionRequest`, so a consumer can answer it with
175
- `POST /sessions/:id/permissions/:requestId` — the mechanism behind an Approve button in a chat
175
+ `POST /sessions/:id/permissions/:requestId` - the mechanism behind an Approve button in a chat
176
176
  message or on a phone's lock screen. The server holds no push credentials and knows nothing about
177
177
  APNs, FCM or Slack; forwarding to one of those is a separate process's job.
178
178
 
@@ -180,10 +180,10 @@ APNs, FCM or Slack; forwarding to one of those is a separate process's job.
180
180
 
181
181
  Each session's credential provenance surfaces as `apiKeySource` on `SessionInfo` and the
182
182
  `system_init` event; `'oauth'` means claude.ai subscription credentials. With
183
- `requireApiKey: true` such sessions are terminated with a `session_error` — recommended for
183
+ `requireApiKey: true` such sessions are terminated with a `session_error` - recommended for
184
184
  services and any unattended use. Without it the server logs a one-time notice instead
185
185
  (appropriate only for personal single-user deployments). WorkerDeck never implements claude.ai
186
- OAuth, never reads or forwards tokens — see the repo README's
186
+ OAuth, never reads or forwards tokens - see the repo README's
187
187
  ["Auth & Anthropic's terms"](https://github.com/workerdeck/workerdeck#auth--anthropics-terms).
188
188
 
189
189
  ## Rules you cannot infer from the types
@@ -192,7 +192,7 @@ OAuth, never reads or forwards tokens — see the repo README's
192
192
  caller's business, so the out-of-scope answer is byte-identical to the unknown-id one.
193
193
  - **Visibility is full control.** There is no read-only attach: a client that can see a session can
194
194
  send `user_message`, `permission_decision`, `interrupt` and `close`. `SessionPanel`'s `readOnly`
195
- removes the affordance, not the authority — enforce at the gateway or not at all.
195
+ removes the affordance, not the authority - enforce at the gateway or not at all.
196
196
  - **`authorizeSession` is synchronous on purpose.** It runs per route and per row of every list. An
197
197
  expensive lookup belongs in `authenticate`, where it happens once and lands on the principal.
198
198
  - **`sandboxedProviderProfile()`'s empty arrays are load-bearing.** `capabilities: []` and
@@ -202,15 +202,15 @@ OAuth, never reads or forwards tokens — see the repo README's
202
202
  variable at all moves the CLI off the macOS Keychain, so pinning the default directory breaks a
203
203
  working `claude login`. "Pin the default dir" and "don't pin" are different logins.
204
204
  - **`checkCredentials` is display-only** unless you also set `requireAvailableProfile`. A create
205
- against an unavailable profile otherwise proceeds and fails with the engine's own error — right
205
+ against an unavailable profile otherwise proceeds and fails with the engine's own error - right
206
206
  for an operator (the probe can be stale), wrong in front of an end user.
207
207
  - **`createEngineRunner` has four invisible obligations**: forward `restore`, adopt `id`, seed the
208
208
  VFS only when *not* restoring, and dispose per-session resources via `onClose`. Every one is a
209
209
  runtime-only failure. `createProviderRunner()` does all four; reach for the raw hook only when it
210
210
  genuinely doesn't fit.
211
211
  - **`parking.persistLive` is how a *provider* session survives a restart, and it needs a durable
212
- store to mean anything.** Claude and codex go dormant — remembered by engine session id and
213
- resumed from the engine's own store — which a provider session cannot do, so its record carries
212
+ store to mean anything.** Claude and codex go dormant - remembered by engine session id and
213
+ resumed from the engine's own store - which a provider session cannot do, so its record carries
214
214
  the state itself, written through after every turn. With the default in-memory store the option
215
215
  does nothing and says nothing. It is off by default: a library must not start writing sessions'
216
216
  transcripts to disk because someone upgraded, and the record holds the whole transcript in
@@ -220,9 +220,9 @@ OAuth, never reads or forwards tokens — see the repo README's
220
220
  implement a `SessionStore`, do not "tidy up" a record on read.
221
221
  - **One origin is not a convenience.** A browser cannot put an `Authorization` header on a
222
222
  WebSocket upgrade, so a cookie is the only credential a tab can present on an attach, and a
223
- cookie is per-origin. That is what `fallback` is for — an app served from the gateway's own port.
223
+ cookie is per-origin. That is what `fallback` is for - an app served from the gateway's own port.
224
224
 
225
225
  ## License
226
226
 
227
- MIT © Tobias Strebitzer —
227
+ MIT © Tobias Strebitzer -
228
228
  [LICENSE](https://github.com/workerdeck/workerdeck/blob/master/LICENSE)
package/build/index.mjs CHANGED
@@ -712,7 +712,7 @@ async function handleHostFiles(ctx, req, res, pathname) {
712
712
  }
713
713
  const existing = current.ok ? current.data : null;
714
714
  if (existing && !body.expectedHash) {
715
- json(res, 409, { error: "file exists — pass expectedHash to overwrite it" });
715
+ json(res, 409, { error: "file exists - pass expectedHash to overwrite it" });
716
716
  return;
717
717
  }
718
718
  if (existing && hashBytes(existing) !== body.expectedHash) {
@@ -1068,7 +1068,7 @@ function createFileSessionStore(options = {}) {
1068
1068
  path,
1069
1069
  op: "save"
1070
1070
  });
1071
- throw new Error(`parked session '${record.id}' is not JSON-serializable — a host-injected value reached its config or snapshot: ${String(error)}`, { cause: error });
1071
+ throw new Error(`parked session '${record.id}' is not JSON-serializable - a host-injected value reached its config or snapshot: ${String(error)}`, { cause: error });
1072
1072
  }
1073
1073
  try {
1074
1074
  await mkdir(dir, {
@@ -2574,7 +2574,7 @@ var SessionParkManager = class {
2574
2574
  }
2575
2575
  if (runner.id !== id) {
2576
2576
  runner.close("error");
2577
- const error = /* @__PURE__ */ new Error(`rebuilt session has id '${runner.id}', expected '${id}' — the engine factory must forward EngineRunnerContext.restore (or, without a snapshot, the session id) to the runner config`);
2577
+ const error = /* @__PURE__ */ new Error(`rebuilt session has id '${runner.id}', expected '${id}' - the engine factory must forward EngineRunnerContext.restore (or, without a snapshot, the session id) to the runner config`);
2578
2578
  this.#options.onError?.(error, {
2579
2579
  sessionId: id,
2580
2580
  phase: "resume"
@@ -2682,7 +2682,7 @@ var ProfileService = class {
2682
2682
  if (!hasEngineRunnerFactory) return `profile '${p.name}' uses engine 'provider' but no \`createEngineRunner\` was provided to build one`;
2683
2683
  } else if (p.engine === "codex") {
2684
2684
  if (p.codexHome && !existsSync(p.codexHome)) return `profile '${p.name}' codexHome does not exist: ${p.codexHome}`;
2685
- if (p.session?.instructions) return `profile '${p.name}' declares session.instructions, which the codex engine cannot deliver — put instructions in the target repo’s AGENTS.md instead`;
2685
+ if (p.session?.instructions) return `profile '${p.name}' declares session.instructions, which the codex engine cannot deliver - put instructions in the target repo’s AGENTS.md instead`;
2686
2686
  } else if (!p.configDir || !existsSync(p.configDir)) return `profile '${p.name}' configDir does not exist: ${p.configDir}`;
2687
2687
  if (disableBypassPermissions && p.defaults?.permissionMode === "bypassPermissions") return `profile '${p.name}' defaults to bypassPermissions but disableBypassPermissions is set`;
2688
2688
  const fallbackMode = p.defaults?.permissionMode;
@@ -2745,7 +2745,7 @@ var ProfileService = class {
2745
2745
  declaredGuard(profile) {
2746
2746
  return this.#declaredByName.has(profile.name) ? {
2747
2747
  status: 403,
2748
- error: `profile '${profile.name}' is declared in server options and cannot be changed over the API — edit the \`profiles\` option instead`
2748
+ error: `profile '${profile.name}' is declared in server options and cannot be changed over the API - edit the \`profiles\` option instead`
2749
2749
  } : null;
2750
2750
  }
2751
2751
  configDirGuard(profile) {
@@ -2895,13 +2895,13 @@ function createSessionFactory(deps) {
2895
2895
  };
2896
2896
  const checkPermissionMode = (mode, profile) => {
2897
2897
  if (mode === void 0 || supportsPermissionMode(profile?.engine, mode)) return null;
2898
- return `permission mode '${mode}' is not supported by profile '${profile.name}' (engine '${engineOf(profile)}') — supported: ` + adapterFor(profile?.engine).capabilities.permissionModes.join(", ");
2898
+ return `permission mode '${mode}' is not supported by profile '${profile.name}' (engine '${engineOf(profile)}') - supported: ` + adapterFor(profile?.engine).capabilities.permissionModes.join(", ");
2899
2899
  };
2900
2900
  const checkEngineGrants = (req, profile) => {
2901
2901
  const engine = engineOf(profile);
2902
2902
  const caps = adapterFor(profile?.engine).capabilities;
2903
2903
  const name = profile?.name ?? "default";
2904
- if (!caps.sessionMcpServers && req.mcpServers && Object.keys(req.mcpServers).length > 0) return `profile '${name}' runs the ${engine} engine, whose MCP servers are declared outside the session request — a request cannot add its own`;
2904
+ if (!caps.sessionMcpServers && req.mcpServers && Object.keys(req.mcpServers).length > 0) return `profile '${name}' runs the ${engine} engine, whose MCP servers are declared outside the session request - a request cannot add its own`;
2905
2905
  if (!caps.budgets && (req.maxTurns !== void 0 || req.maxBudgetUsd !== void 0)) return `the ${engine} engine does not honor maxTurns/maxBudgetUsd`;
2906
2906
  if (!caps.settingSources && req.settingSources !== void 0) return `the ${engine} engine does not load settingSources`;
2907
2907
  if (!caps.resume && req.resume !== void 0) return `the ${engine} engine cannot resume a session`;
@@ -2912,7 +2912,7 @@ function createSessionFactory(deps) {
2912
2912
  if (!req.capabilities || !granted) return null;
2913
2913
  const ungranted = req.capabilities.filter((c) => !granted.includes(c));
2914
2914
  if (ungranted.length === 0) return null;
2915
- return `profile '${profile.name}' does not grant: ${ungranted.join(", ")} (granted: ${granted.join(", ") || "none"}) — a request may narrow capabilities, not widen them`;
2915
+ return `profile '${profile.name}' does not grant: ${ungranted.join(", ")} (granted: ${granted.join(", ") || "none"}) - a request may narrow capabilities, not widen them`;
2916
2916
  };
2917
2917
  const stripInertFields = (req, profile) => {
2918
2918
  if (!adapterFor(profile?.engine).capabilities.interactiveApprovals) delete req.questionBehavior;
@@ -3006,7 +3006,7 @@ function createSessionFactory(deps) {
3006
3006
  id
3007
3007
  });
3008
3008
  const reported = runner.info().scope;
3009
- if (!sameScope(reported, config.scope)) throw new Error(`runner for session ${runner.id} reports scope ${JSON.stringify(reported)}, expected ${JSON.stringify(config.scope)} — echo config.scope from info()`);
3009
+ if (!sameScope(reported, config.scope)) throw new Error(`runner for session ${runner.id} reports scope ${JSON.stringify(reported)}, expected ${JSON.stringify(config.scope)} - echo config.scope from info()`);
3010
3010
  return runner;
3011
3011
  };
3012
3012
  const createRunner = async (config) => {
@@ -3062,7 +3062,7 @@ function createSessionFactory(deps) {
3062
3062
  if (subscriptionNoticeShown.has(profileName)) return;
3063
3063
  subscriptionNoticeShown.add(profileName);
3064
3064
  const scope = profileName ? `Sessions under profile '${profileName}'` : "Sessions";
3065
- console.warn(`[workerdeck] ${scope} are using claude.ai subscription credentials (apiKeySource 'oauth'), not an API key. That is only appropriate for personal, single-user use of your own account. Unattended/scheduled or multi-user use requires an API key under Anthropic's terms — set ANTHROPIC_API_KEY in the server environment, or set requireApiKey: true to fail closed.`);
3065
+ console.warn(`[workerdeck] ${scope} are using claude.ai subscription credentials (apiKeySource 'oauth'), not an API key. That is only appropriate for personal, single-user use of your own account. Unattended/scheduled or multi-user use requires an API key under Anthropic's terms - set ANTHROPIC_API_KEY in the server environment, or set requireApiKey: true to fail closed.`);
3066
3066
  }
3067
3067
  });
3068
3068
  };