@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 +21 -0
- package/README.md +187 -0
- package/build/index.d.mts +530 -0
- package/build/index.mjs +1876 -0
- package/build/index.mjs.map +1 -0
- package/package.json +62 -0
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)
|