@workerdeck/server 0.6.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Tobias Strebitzer
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,187 @@
1
+ # @workerdeck/server
2
+
3
+ The WorkerDeck gateway: HTTP + WebSocket session server over
4
+ [`@workerdeck/core`](https://www.npmjs.com/package/@workerdeck/core). Session registry
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
7
+ filesystem (no serverless).
8
+
9
+ Want the whole thing running rather than embedded? [`workerdeck`](https://www.npmjs.com/package/workerdeck)
10
+ wraps this package and the dashboard into one command: `npx workerdeck`.
11
+
12
+ Part of [WorkerDeck](https://github.com/workerdeck/workerdeck). It speaks the
13
+ [`@workerdeck/protocol`](https://www.npmjs.com/package/@workerdeck/protocol) wire format;
14
+ pair it with [`@workerdeck/client`](https://www.npmjs.com/package/@workerdeck/client) in the
15
+ host app and [`@workerdeck/ui`](https://www.npmjs.com/package/@workerdeck/ui) for embeddable
16
+ panels. Job scheduling comes from
17
+ [`@workerdeck/queue`](https://www.npmjs.com/package/@workerdeck/queue), mounted via the
18
+ `queue` option.
19
+
20
+ ## Install
21
+
22
+ ```bash
23
+ npm install @workerdeck/server
24
+ ```
25
+
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.
28
+ The server implements no Anthropic auth: the SDK/CLI resolves credentials from the operator's
29
+ environment (`ANTHROPIC_API_KEY`, Bedrock/Vertex, or a personal `claude login`).
30
+
31
+ ## Usage
32
+
33
+ The host app supplies the authenticator — return a truthy principal to accept, null/undefined to
34
+ reject with 401. `createWorkerServer` refuses to start without `authenticate` unless you
35
+ explicitly pass `allowUnauthenticated: true` (loopback dev only — never expose that):
36
+
37
+ ```ts
38
+ import { createWorkerServer } from '@workerdeck/server'
39
+
40
+ const worker = createWorkerServer({
41
+ authenticate: async (req) => verifyMyAppToken(req.headers.authorization),
42
+ allowedCwdRoots: ['/srv/checkouts'], // clamp where sessions may run
43
+ buildRunnerConfig: (req) => ({ ...req, env: { ...process.env } }),
44
+ requireApiKey: true, // fail closed on subscription credentials
45
+ })
46
+ const { port } = await worker.listen(8787)
47
+ // worker.server (node:http), worker.registry, worker.queue, worker.close()
48
+ ```
49
+
50
+ Routes (default `basePath: '/v1'`):
51
+
52
+ | Route | What it does |
53
+ | --- | --- |
54
+ | `GET/POST /v1/sessions` | List sessions / create one (`CreateSessionRequest`, `cwd` required) |
55
+ | `GET/DELETE /v1/sessions/:id` | Session info / close and remove |
56
+ | `WS /v1/sessions/:id/ws?afterSeq=n` | Attach: `attached` frame, replay past `n`, then live events |
57
+ | `POST /v1/sessions/:id/permissions/:requestId` | Resolve a pending approval over REST |
58
+ | `GET /v1/sdk-sessions?dir=…` | List the Agent SDK's on-disk sessions to offer resume |
59
+ | `GET /v1/sessions/:id/files`, `…/files/<path>` | List and download a session's scratch-filesystem deliverables |
60
+ | `POST /v1/executions/:executionId/result` | Deliver a deferred execution's result, waking a parked session |
61
+ | `GET /v1/profiles`, `GET /v1/profiles/:name` | What sessions may run as (+ a view-only config snapshot) |
62
+ | `GET/POST /v1/jobs`, `GET/DELETE /v1/jobs/:id` | Job queue (when `queue` is configured) |
63
+ | `GET /v1/queue`, `WS /v1/queue/ws` | Queue stats / one-way live stream of job events + stats |
64
+
65
+ Requests outside `basePath` fall through to the optional `fallback` hook — which is how the
66
+ turnkey instance serves a dashboard from the same origin, so a browser's WebSocket attach can
67
+ present a cookie.
68
+
69
+ ## Profiles and the second engine
70
+
71
+ A **profile** is what a session runs as, and which engine runs it. Declared at startup (or managed
72
+ over the API with a `profileStore`), each one names either a Claude Code config directory — applied
73
+ as that session's `CLAUDE_CONFIG_DIR` — or a model provider for the model-agnostic engine:
74
+
75
+ ```ts
76
+ createWorkerServer({
77
+ authenticate: async (req) => {
78
+ const user = await verifyMyAppToken(req.headers.authorization)
79
+ return user && { allowedProfiles: user.profiles, canManageProfiles: user.isAdmin }
80
+ },
81
+ profiles: [
82
+ { name: 'ada', configDir: '/home/ada/.claude', defaults: { model: 'opus' } },
83
+ {
84
+ name: 'kimi',
85
+ engine: 'provider',
86
+ // A variable NAME — no credential is stored here or served by GET /profiles.
87
+ provider: { id: 'moonshotai', model: 'kimi-k3', apiKeyEnv: 'MOONSHOT_API_KEY' },
88
+ session: { capabilities: ['web_fetch', 'deliver_file'] },
89
+ },
90
+ ],
91
+ // The one place a model SDK and its credentials are resolved — this package imports neither.
92
+ createEngineRunner: ({ config, profile, bridge }) => buildProviderRunner({ /* … */ }),
93
+ })
94
+ ```
95
+
96
+ With more than one profile declared, every create must name its `profile`; with exactly one it is
97
+ implicit, and with the option unset a `default` is auto-detected from `~/.claude`.
98
+ `allowedProfiles` on the principal scopes who may run as what — the line between one worker serving
99
+ several people and account pooling. `SessionInfo.engine` and `supportsPermissionMode` let a UI hide
100
+ affordances the provider engine doesn't have; asking for one anyway is a 400 rather than a silent
101
+ coercion.
102
+
103
+ ## Parked sessions
104
+
105
+ A session whose tool call can't answer in the next few seconds **parks**: the runner is torn down,
106
+ its snapshot goes to a `SessionStore`, and `POST /executions/:id/result` rebuilds it — same id,
107
+ same event log, same seq numbering, mid-turn. Results are idempotent by `executionId`. The default
108
+ store is in-memory (a park survives a client disconnect, not a restart); the bundled file store
109
+ survives both, on one host:
110
+
111
+ ```ts
112
+ import { createFileSessionStore, createWorkerServer } from '@workerdeck/server'
113
+
114
+ createWorkerServer({
115
+ authenticate,
116
+ // Adopted by hydrate() inside listen(): executions re-indexed, watchdogs re-armed.
117
+ parking: { store: createFileSessionStore({ dir: '/var/lib/workerdeck/parked' }) },
118
+ })
119
+ ```
120
+
121
+ That directory holds each parked session's whole transcript in plaintext — protect it like the
122
+ SDK's own `~/.claude/projects`. Credentials never reach it. One process per directory.
123
+
124
+ ## Job queue
125
+
126
+ Pass `queue` options to mount the job routes — one-shot unattended runs with bounded concurrency,
127
+ token budgets, retries, and webhook delivery:
128
+
129
+ ```ts
130
+ const worker = createWorkerServer({
131
+ authenticate,
132
+ queue: {
133
+ maxConcurrency: 2,
134
+ sessionTokenLimit: 200_000, // per job; exceeding kills the run
135
+ dailyTokenLimit: 2_000_000, // global UTC-day budget; queued jobs held once spent
136
+ maxJobDurationMs: 1_800_000, // wall-clock watchdog
137
+ retention: { maxAgeMs: 86_400_000 }, // expire terminal jobs
138
+ // adapter: myRedisAdapter, // defaults to the bundled in-memory adapter
139
+ },
140
+ })
141
+ ```
142
+
143
+ Job sessions are ordinary registry sessions — attachable over the sessions WS — and go through
144
+ the same `buildRunnerConfig` hook and auth-provenance watcher as client sessions. The in-memory
145
+ adapter is single-process and non-persistent; implement `QueueAdapter` against a shared store for
146
+ anything beyond one trusted host.
147
+
148
+ ## Session notifications
149
+
150
+ The live WebSocket only reaches someone who has one open. `notifications` is the outbound
151
+ channel for everyone else — the four moments a person acts on, POSTed to a URL you control:
152
+
153
+ ```ts
154
+ const worker = createWorkerServer({
155
+ authenticate,
156
+ notifications: {
157
+ webhook: { url: 'https://my-app.test/hooks/session', headers: { authorization: '…' } },
158
+ // events: ['permission_requested'], // default: all four
159
+ onNotification: (n) => log(n.type, n.sessionId, n.preview), // unfiltered, in-process
160
+ },
161
+ })
162
+ ```
163
+
164
+ `permission_requested`, `turn_completed`, `session_error`, `session_closed` — delivered as a
165
+ `SessionNotification`, ordered per session, retried with exponential backoff. Server-wide, unlike
166
+ the queue's per-job `webhook`: the point is hearing about sessions you neither created nor are
167
+ attached to, and every registry session qualifies (job runs and rebuilt parked sessions included).
168
+
169
+ `permission_requested` carries the whole `PermissionRequest`, so a consumer can answer it with
170
+ `POST /sessions/:id/permissions/:requestId` — the mechanism behind an Approve button in a chat
171
+ message or on a phone's lock screen. The server holds no push credentials and knows nothing about
172
+ APNs, FCM or Slack; forwarding to one of those is a separate process's job.
173
+
174
+ ## Auth posture
175
+
176
+ Each session's credential provenance surfaces as `apiKeySource` on `SessionInfo` and the
177
+ `system_init` event; `'oauth'` means claude.ai subscription credentials. With
178
+ `requireApiKey: true` such sessions are terminated with a `session_error` — recommended for
179
+ services and any unattended use. Without it the server logs a one-time notice instead
180
+ (appropriate only for personal single-user deployments). WorkerDeck never implements claude.ai
181
+ OAuth, never reads or forwards tokens — see the repo README's
182
+ ["Auth & Anthropic's terms"](https://github.com/workerdeck/workerdeck#auth--anthropics-terms).
183
+
184
+ ## License
185
+
186
+ MIT © Tobias Strebitzer —
187
+ [LICENSE](https://github.com/workerdeck/workerdeck/blob/master/LICENSE)