@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
|
@@ -0,0 +1,530 @@
|
|
|
1
|
+
import { IncomingMessage, Server, ServerResponse } from "node:http";
|
|
2
|
+
import { BridgeAnswer, BrowserBridgeExecutor, ClaudeAuthProbe, ParkedExecution, Runner, RunnerSnapshot, SessionRunnerConfig, ToolExecutionResult } from "@workerdeck/core";
|
|
3
|
+
import { JobQueue, QueueAdapter } from "@workerdeck/queue";
|
|
4
|
+
import { CreateSessionRequest, JobEvent, ProfileInfo, SdkSessionSummary, ServerFrame, SessionInfo, SessionNotification, SessionWebhookConfig } from "@workerdeck/protocol";
|
|
5
|
+
|
|
6
|
+
//#region src/registry.d.ts
|
|
7
|
+
type SessionRegistryOptions = {
|
|
8
|
+
/**
|
|
9
|
+
* Called once per runner as it enters the table, before it starts — the one
|
|
10
|
+
* seam every path goes through (create, prepare, adopt, and the rebuild of a
|
|
11
|
+
* parked session), which is what a watcher that must not miss a session needs.
|
|
12
|
+
*/
|
|
13
|
+
onRegister?: (runner: Runner) => void;
|
|
14
|
+
};
|
|
15
|
+
/** In-memory session table. Terminal sessions stay listed until removed or the process exits. */
|
|
16
|
+
declare class SessionRegistry {
|
|
17
|
+
#private;
|
|
18
|
+
constructor(options?: SessionRegistryOptions);
|
|
19
|
+
create(config: SessionRunnerConfig): Runner;
|
|
20
|
+
/** Build and list a Claude-engine runner without starting it, so watchers can
|
|
21
|
+
* subscribe first. Call `start()` once they have. */
|
|
22
|
+
prepare(config: SessionRunnerConfig): Runner;
|
|
23
|
+
/** Register an already-built runner (a non-Claude engine) and start it. */
|
|
24
|
+
adopt(runner: Runner): Runner;
|
|
25
|
+
/** List a runner without starting it — for a rehydrated session, whose watchers
|
|
26
|
+
* must be subscribed before it comes back up. */
|
|
27
|
+
register(runner: Runner): Runner;
|
|
28
|
+
get(id: string): Runner | undefined;
|
|
29
|
+
list(): SessionInfo[];
|
|
30
|
+
remove(id: string): boolean;
|
|
31
|
+
/** Drop a runner WITHOUT closing it: the session isn't ending, it parked and
|
|
32
|
+
* lives on in its snapshot. Closing here would tell every client it was over. */
|
|
33
|
+
evict(id: string): boolean;
|
|
34
|
+
closeAll(): void;
|
|
35
|
+
}
|
|
36
|
+
//#endregion
|
|
37
|
+
//#region src/notifications.d.ts
|
|
38
|
+
type SessionNotificationOptions = {
|
|
39
|
+
/** POST target for every notification. */webhook?: SessionWebhookConfig;
|
|
40
|
+
/** Local observer, invoked for every notification whether or not a webhook is
|
|
41
|
+
* configured — the in-process seam a host (or the CLI's APNs forwarder) hooks.
|
|
42
|
+
* Unfiltered: `webhook.events` narrows POST deliveries, not this. */
|
|
43
|
+
onNotification?: (notification: SessionNotification) => void; /** Delivery attempts per notification (exponential backoff). Default 3. */
|
|
44
|
+
attempts?: number; /** Initial backoff between attempts. Default 500ms. */
|
|
45
|
+
retryDelayMs?: number;
|
|
46
|
+
};
|
|
47
|
+
/**
|
|
48
|
+
* Turns session events into the handful of notifications a human away from the
|
|
49
|
+
* screen cares about, and delivers them to a webhook and/or a local observer.
|
|
50
|
+
*
|
|
51
|
+
* This is the *primitive*, deliberately transport-agnostic: the server stays
|
|
52
|
+
* credential-free and knows nothing about APNs, Slack or email. Turning a
|
|
53
|
+
* notification into a push is a forwarder's job (the turnkey CLI's), and one that
|
|
54
|
+
* needs credentials, so it does not live here.
|
|
55
|
+
*
|
|
56
|
+
* Delivery is best-effort and ordered per session, mirroring the job queue's
|
|
57
|
+
* webhook behaviour — a consumer that missed one can always attach to the session
|
|
58
|
+
* WS with `afterSeq` and see the truth.
|
|
59
|
+
*/
|
|
60
|
+
declare class SessionNotifier {
|
|
61
|
+
#private;
|
|
62
|
+
constructor(options: SessionNotificationOptions);
|
|
63
|
+
/** True when nothing is listening — lets the caller skip subscribing at all. */
|
|
64
|
+
get idle(): boolean;
|
|
65
|
+
/**
|
|
66
|
+
* Subscribe to a runner for its lifetime.
|
|
67
|
+
*
|
|
68
|
+
* `afterSeq` defaults to whatever the runner has already emitted, which is what
|
|
69
|
+
* makes this safe on a *rehydrated* session: `subscribe` replays the log from
|
|
70
|
+
* `afterSeq`, so subscribing at 0 to a session rebuilt from a park would
|
|
71
|
+
* re-announce every permission request it ever made.
|
|
72
|
+
*/
|
|
73
|
+
watch(runner: Runner, afterSeq?: number): void;
|
|
74
|
+
}
|
|
75
|
+
//#endregion
|
|
76
|
+
//#region src/bridge.d.ts
|
|
77
|
+
type BridgeHubOptions = {
|
|
78
|
+
/** How long a bridged call may stay unanswered before it fails. Default 60000. */timeoutMs?: number;
|
|
79
|
+
/** Called when a bridged execution reaches a terminal result — the host feeds
|
|
80
|
+
* it back into the runner's loop. */
|
|
81
|
+
onResult?: (sessionId: string, executionId: string, result: ToolExecutionResult) => void;
|
|
82
|
+
};
|
|
83
|
+
/**
|
|
84
|
+
* Routes tool executions between a session and the browser tabs attached to it.
|
|
85
|
+
*
|
|
86
|
+
* A session may have several clients attached (dashboard plus embedded panel);
|
|
87
|
+
* the bridge asks the **first attached** one, which is the closest thing to "the
|
|
88
|
+
* client driving this session". If none is attached, dispatch fails fast rather
|
|
89
|
+
* than hanging — an autonomous job simply never bridges, it uses the server
|
|
90
|
+
* executor instead.
|
|
91
|
+
*/
|
|
92
|
+
declare class BridgeHub {
|
|
93
|
+
#private;
|
|
94
|
+
constructor(options?: BridgeHubOptions);
|
|
95
|
+
/** The executor to hand a runner for this session. Created on first use and
|
|
96
|
+
* reused, so results routed back always reach the same pending table. */
|
|
97
|
+
executorFor(sessionId: string): BrowserBridgeExecutor;
|
|
98
|
+
/** How many clients are watching this session. Parking consults it: a session
|
|
99
|
+
* someone is watching stays live. */
|
|
100
|
+
attachedCount(sessionId: string): number;
|
|
101
|
+
/** Register an attached client. Returns a detach function. */
|
|
102
|
+
attach(sessionId: string, send: (frame: ServerFrame) => void): () => void;
|
|
103
|
+
/**
|
|
104
|
+
* Deliver a client's answer to a bridged call. Returns false when the id is
|
|
105
|
+
* unknown or already settled — late and duplicate answers are ignored.
|
|
106
|
+
*/
|
|
107
|
+
resolve(sessionId: string, executionId: string, answer: BridgeAnswer): boolean;
|
|
108
|
+
/** Drop a session's bridge, failing anything still in flight. */
|
|
109
|
+
remove(sessionId: string): void;
|
|
110
|
+
}
|
|
111
|
+
//#endregion
|
|
112
|
+
//#region src/session-store.d.ts
|
|
113
|
+
/**
|
|
114
|
+
* A session with its live runner torn down, waiting on deferred executions.
|
|
115
|
+
*
|
|
116
|
+
* Everything needed to bring it back: the wire-visible info (so it still lists and
|
|
117
|
+
* reads over REST while parked), the config to rebuild the runner, the engine's
|
|
118
|
+
* snapshot, and what it is waiting for.
|
|
119
|
+
*/
|
|
120
|
+
type ParkedSessionRecord = {
|
|
121
|
+
id: string; /** Session info as of the park, with `status: 'parked'`. */
|
|
122
|
+
info: SessionInfo;
|
|
123
|
+
profile?: string; /** The config the session was created with (profile defaults already applied). */
|
|
124
|
+
config: SessionRunnerConfig;
|
|
125
|
+
snapshot: RunnerSnapshot;
|
|
126
|
+
executions: ParkedExecution[];
|
|
127
|
+
parkedAt: number;
|
|
128
|
+
};
|
|
129
|
+
/**
|
|
130
|
+
* Where parked sessions live. Two implementations ship: {@link MemorySessionStore}
|
|
131
|
+
* (a park survives a disconnect, not a restart) and {@link createFileSessionStore}
|
|
132
|
+
* (it survives both, on one host); a redis/sqlite/table store implements the same
|
|
133
|
+
* four operations.
|
|
134
|
+
*
|
|
135
|
+
* Two things to know before writing one: the record holds the session's whole
|
|
136
|
+
* transcript and tool I/O, and `config` may carry host-injected values (env, hooks,
|
|
137
|
+
* injected functions) that a JSON round-trip silently drops or, worse, persists.
|
|
138
|
+
* {@link toDurableRecord} is the filter the bundled file store applies — reuse it.
|
|
139
|
+
*/
|
|
140
|
+
interface SessionStore {
|
|
141
|
+
save(record: ParkedSessionRecord): Promise<void>;
|
|
142
|
+
get(id: string): Promise<ParkedSessionRecord | null>;
|
|
143
|
+
list(): Promise<ParkedSessionRecord[]>;
|
|
144
|
+
delete(id: string): Promise<boolean>;
|
|
145
|
+
}
|
|
146
|
+
/** Single-process, no persistence: parks survive a client disconnect, not a restart. */
|
|
147
|
+
declare class MemorySessionStore implements SessionStore {
|
|
148
|
+
#private;
|
|
149
|
+
save(record: ParkedSessionRecord): Promise<void>;
|
|
150
|
+
get(id: string): Promise<ParkedSessionRecord | null>;
|
|
151
|
+
list(): Promise<ParkedSessionRecord[]>;
|
|
152
|
+
delete(id: string): Promise<boolean>;
|
|
153
|
+
}
|
|
154
|
+
/** The record as it may be persisted: same session, config narrowed to what is
|
|
155
|
+
* safe and meaningful to keep (see {@link EPHEMERAL_CONFIG_KEYS}). */
|
|
156
|
+
declare function toDurableRecord(record: ParkedSessionRecord): ParkedSessionRecord;
|
|
157
|
+
type FileSessionStoreOptions = {
|
|
158
|
+
/** Directory holding one JSON file per parked session.
|
|
159
|
+
* Default `<cwd>/.workerdeck/parked`. */
|
|
160
|
+
dir?: string;
|
|
161
|
+
/** A record that could not be read or written. Losing one is losing a session's
|
|
162
|
+
* way back, so this is worth logging — the store itself stays quiet and skips it. */
|
|
163
|
+
onError?: (error: unknown, context: {
|
|
164
|
+
path: string;
|
|
165
|
+
op: 'save' | 'read' | 'delete';
|
|
166
|
+
}) => void;
|
|
167
|
+
};
|
|
168
|
+
/**
|
|
169
|
+
* Durable single-host store: one JSON file per parked session under `dir`, written
|
|
170
|
+
* through a temp file and a rename so a crash mid-write cannot truncate a session.
|
|
171
|
+
* `hydrate()` at `listen()` picks them up, re-indexes their executions, and re-arms
|
|
172
|
+
* the watchdogs, so a restart no longer loses parked work.
|
|
173
|
+
*
|
|
174
|
+
* Know what is on that disk: **the record holds the session's entire transcript** —
|
|
175
|
+
* prompts, model output, and tool I/O — in plaintext. Put it somewhere with the same
|
|
176
|
+
* protection as the SDK's own transcripts (`~/.claude/projects`), not in a directory
|
|
177
|
+
* that gets served, synced, or backed up somewhere looser.
|
|
178
|
+
*
|
|
179
|
+
* Single-process by design, exactly like the bundled queue adapter and profile
|
|
180
|
+
* store: two servers sharing one directory would both hydrate the same records and
|
|
181
|
+
* race to rebuild them. That is what the seam is for.
|
|
182
|
+
*
|
|
183
|
+
* Nothing here reaps: a record leaves only when its session wakes or is deleted.
|
|
184
|
+
* An execution dispatched without a deadline (a `DeferredExecutor` with no
|
|
185
|
+
* `timeoutMs`) has no watchdog to end the wait, so its record — and its transcript
|
|
186
|
+
* — stays until `DELETE /sessions/:id`. Give deferred calls a deadline, or sweep.
|
|
187
|
+
*/
|
|
188
|
+
declare function createFileSessionStore(options?: FileSessionStoreOptions): SessionStore;
|
|
189
|
+
//#endregion
|
|
190
|
+
//#region src/parking.d.ts
|
|
191
|
+
type SessionParkOptions = {
|
|
192
|
+
registry: SessionRegistry;
|
|
193
|
+
store: SessionStore;
|
|
194
|
+
/** Rebuild a parked session's runner. The snapshot rides in on
|
|
195
|
+
* `config.restore`, so the engine adopts the id, event log, and history. */
|
|
196
|
+
rebuild: (record: ParkedSessionRecord) => Promise<Runner>;
|
|
197
|
+
/** How many clients are attached to this session. A watched session stays live:
|
|
198
|
+
* parking would pull the runner out from under the socket. */
|
|
199
|
+
attachedCount: (sessionId: string) => number;
|
|
200
|
+
/** Wait this long after the last client detaches before parking, so a reconnect
|
|
201
|
+
* (a wifi blip, a page reload) doesn't cost a teardown. Default 2000. */
|
|
202
|
+
parkDelayMs?: number;
|
|
203
|
+
/** Grace given at {@link SessionParkManager.hydrate} to an execution whose
|
|
204
|
+
* deadline passed while the server was down. Its result could not have been
|
|
205
|
+
* delivered during the outage, so failing it the instant the process is back
|
|
206
|
+
* would throw away an answer that is very likely seconds behind. Extends a
|
|
207
|
+
* deadline, never shortens one. Default 60000. */
|
|
208
|
+
expiredGraceMs?: number;
|
|
209
|
+
/** Veto + accounting hook, called before the teardown: the job queue frees the
|
|
210
|
+
* run's concurrency slot here, and refuses (false) when the run is finalizing. */
|
|
211
|
+
onParking?: (sessionId: string, executionId: string) => boolean;
|
|
212
|
+
/** The session is live again under a NEW runner object — anything holding the
|
|
213
|
+
* old reference must rebind. */
|
|
214
|
+
onResumed?: (sessionId: string, runner: Runner) => void;
|
|
215
|
+
/** Park/resume failures. These are not session errors — the session is intact,
|
|
216
|
+
* the host's storage or engine assembly isn't. */
|
|
217
|
+
onError?: (error: unknown, context: {
|
|
218
|
+
sessionId: string;
|
|
219
|
+
phase: 'park' | 'resume';
|
|
220
|
+
}) => void;
|
|
221
|
+
};
|
|
222
|
+
/**
|
|
223
|
+
* Deferred execution's other half: parking a session that is waiting on work no
|
|
224
|
+
* process in this server is doing.
|
|
225
|
+
*
|
|
226
|
+
* The runner announces the moment with `status_changed: 'parked'` — emitted only
|
|
227
|
+
* once every dispatch of the batch has been handed over, so the snapshot can never
|
|
228
|
+
* miss a call that was still being dispatched. From there this class snapshots,
|
|
229
|
+
* evicts, and persists; delivering a result rebuilds the runner under the same id
|
|
230
|
+
* and hands the result to it. The session's identity, event log, and seq numbering
|
|
231
|
+
* survive intact, so a client reattaching with `afterSeq` sees one unbroken stream.
|
|
232
|
+
*/
|
|
233
|
+
declare class SessionParkManager {
|
|
234
|
+
#private;
|
|
235
|
+
constructor(options: SessionParkOptions);
|
|
236
|
+
/** Record the config a session was created with. Only sessions the host
|
|
237
|
+
* remembers can be parked — there is no way to rebuild the others. */
|
|
238
|
+
remember(sessionId: string, config: SessionRunnerConfig): void;
|
|
239
|
+
/** Adopt the store's contents (a durable store after a restart): re-index the
|
|
240
|
+
* executions and re-arm their watchdogs, no deadline sooner than the grace
|
|
241
|
+
* window — nothing could have been delivered while the process was down. */
|
|
242
|
+
hydrate(): Promise<void>;
|
|
243
|
+
/**
|
|
244
|
+
* Follow a session's lifecycle: index its deferred executions, park it when the
|
|
245
|
+
* engine says the turn has come to rest on them, and clean up when it ends.
|
|
246
|
+
* `afterSeq` skips a rehydrated runner's replayed history (re-arming a watchdog
|
|
247
|
+
* from an event whose deadline already passed would fail the execution instantly).
|
|
248
|
+
*/
|
|
249
|
+
watch(runner: Runner, afterSeq?: number): () => void;
|
|
250
|
+
/** A client detached: park the session if that was the last one watching. */
|
|
251
|
+
onDetach(sessionId: string): void;
|
|
252
|
+
/** Which session this execution belongs to — still waiting, or already settled. */
|
|
253
|
+
sessionFor(executionId: string): string | undefined;
|
|
254
|
+
/** The parked session's record, for the read paths (GET, list, attach). */
|
|
255
|
+
get(id: string): Promise<ParkedSessionRecord | null>;
|
|
256
|
+
/** Every parked session's info, to merge into `GET {basePath}/sessions`. */
|
|
257
|
+
listInfo(): Promise<SessionInfo[]>;
|
|
258
|
+
/** The live runner for a session, rehydrating a parked one on demand. Undefined
|
|
259
|
+
* when the session is neither live nor parked. */
|
|
260
|
+
ensureLive(id: string): Promise<Runner | undefined>;
|
|
261
|
+
/**
|
|
262
|
+
* Deliver a deferred execution's result. Rehydrates the session if needed and
|
|
263
|
+
* folds the result into its agent loop.
|
|
264
|
+
*
|
|
265
|
+
* Undefined = no session is waiting on that id. `applied: false` = it was already
|
|
266
|
+
* settled: a duplicate delivery, or one racing the watchdog. Both are expected,
|
|
267
|
+
* neither is an error.
|
|
268
|
+
*/
|
|
269
|
+
submitResult(executionId: string, result: ToolExecutionResult): Promise<{
|
|
270
|
+
applied: boolean;
|
|
271
|
+
sessionId: string;
|
|
272
|
+
} | undefined>;
|
|
273
|
+
/** Drop a parked session for good: the run is over (closed, canceled, killed). */
|
|
274
|
+
discard(sessionId: string): Promise<void>;
|
|
275
|
+
close(): void;
|
|
276
|
+
}
|
|
277
|
+
//#endregion
|
|
278
|
+
//#region src/profile-store.d.ts
|
|
279
|
+
/**
|
|
280
|
+
* Where dashboard-managed profiles live. The seam exists for the same reason
|
|
281
|
+
* `QueueAdapter` does: a single-host deployment wants the bundled file store and
|
|
282
|
+
* no configuration, while an operator with a database wants their own.
|
|
283
|
+
*
|
|
284
|
+
* Profiles declared in `createWorkerServer({ profiles })` never enter a store —
|
|
285
|
+
* they are code, and stay immutable. The store holds only what the management
|
|
286
|
+
* routes created, and the two sets are unioned by name.
|
|
287
|
+
*
|
|
288
|
+
* A store holds NO credentials: `ProviderConfig.apiKeyEnv` is a variable name and
|
|
289
|
+
* a Claude profile's `configDir` is a path. Both are resolved by the server's own
|
|
290
|
+
* environment at session time, which is what keeps a stored profile safe to write
|
|
291
|
+
* to disk and safe to serve from `GET /profiles`.
|
|
292
|
+
*/
|
|
293
|
+
type ProfileStore = {
|
|
294
|
+
/** Every stored profile. Called once at `listen()` and after each mutation. */list(): ProfileInfo[] | Promise<ProfileInfo[]>; /** Create or replace by `profile.name`. */
|
|
295
|
+
save(profile: ProfileInfo): void | Promise<void>; /** Remove by name. Removing something absent is not an error. */
|
|
296
|
+
delete(name: string): void | Promise<void>;
|
|
297
|
+
};
|
|
298
|
+
/** Non-durable store for tests and ephemeral deployments. */
|
|
299
|
+
declare function createMemoryProfileStore(seed?: ProfileInfo[]): ProfileStore;
|
|
300
|
+
/**
|
|
301
|
+
* JSON-file store: one array of profiles at `path` (default
|
|
302
|
+
* `<cwd>/.workerdeck/profiles.json`). Writes go through a temp file and a
|
|
303
|
+
* rename so a crash mid-write cannot truncate the operator's profile list.
|
|
304
|
+
*
|
|
305
|
+
* Single-process by design, exactly like the bundled queue adapter — two servers
|
|
306
|
+
* sharing one file would race. That is what the seam is for.
|
|
307
|
+
*/
|
|
308
|
+
declare function createFileProfileStore(path?: string): ProfileStore;
|
|
309
|
+
//#endregion
|
|
310
|
+
//#region src/server.d.ts
|
|
311
|
+
type SdkSessionLister = (options: {
|
|
312
|
+
dir?: string;
|
|
313
|
+
limit?: number;
|
|
314
|
+
offset?: number;
|
|
315
|
+
}) => Promise<SdkSessionSummary[]>;
|
|
316
|
+
/**
|
|
317
|
+
* Return a principal (any truthy value) to accept the request, or null/undefined to
|
|
318
|
+
* reject with 401. The host app supplies this — the worker has no auth story of its
|
|
319
|
+
* own. A principal object may carry `allowedProfiles: string[]` to restrict which
|
|
320
|
+
* profiles the caller can create sessions/jobs under (and see in GET /profiles) —
|
|
321
|
+
* without it the caller may use every declared profile. It may also carry
|
|
322
|
+
* `canManageProfiles: true` to allow creating/editing/deleting managed profiles
|
|
323
|
+
* (requires the `profileStore` option); anything else means no.
|
|
324
|
+
*/
|
|
325
|
+
type Authenticator = (req: IncomingMessage) => unknown | Promise<unknown>;
|
|
326
|
+
type WorkerServerOptions = {
|
|
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
|
+
allowUnauthenticated?: boolean; /** If set, session cwd must resolve inside one of these roots. Strongly recommended. */
|
|
329
|
+
allowedCwdRoots?: string[];
|
|
330
|
+
/**
|
|
331
|
+
* Named Claude Code config directories sessions can run under (each becomes the
|
|
332
|
+
* session's CLAUDE_CONFIG_DIR — settings, memory, skills, and the credentials the
|
|
333
|
+
* SDK resolves from it). Declared here at startup; the API only reads them
|
|
334
|
+
* (GET {basePath}/profiles). With more than one declared, every session/job create
|
|
335
|
+
* must name its profile; with exactly one it is implicit. Unset: a 'default'
|
|
336
|
+
* profile is auto-created from $CLAUDE_CONFIG_DIR or ~/.claude when that directory
|
|
337
|
+
* exists. Pass [] to run without profiles (no env pinning at all).
|
|
338
|
+
*/
|
|
339
|
+
profiles?: ProfileInfo[];
|
|
340
|
+
/**
|
|
341
|
+
* Persistence for dashboard-managed profiles, which mounts the profile
|
|
342
|
+
* management routes (`POST /profiles`, `PATCH`/`DELETE /profiles/:name`).
|
|
343
|
+
* Without it the profile set is startup config and the API stays read-only.
|
|
344
|
+
*
|
|
345
|
+
* Profiles declared in `profiles` are never stored and never editable over
|
|
346
|
+
* HTTP — they are code. The two sets are unioned by name, declared winning.
|
|
347
|
+
* Callers still need `canManageProfiles` on their principal.
|
|
348
|
+
*/
|
|
349
|
+
profileStore?: ProfileStore;
|
|
350
|
+
/**
|
|
351
|
+
* Config-dir roots a *managed* Claude profile's `configDir` must resolve inside
|
|
352
|
+
* (mirrors {@link allowedCwdRoots}). Unset — the default — means the management
|
|
353
|
+
* routes create provider profiles only: naming a config directory is choosing
|
|
354
|
+
* which credential store a session runs on, so it stays operator-bounded.
|
|
355
|
+
* Declared profiles are unaffected.
|
|
356
|
+
*/
|
|
357
|
+
allowedConfigDirRoots?: string[];
|
|
358
|
+
/** Map/patch the incoming CreateSessionRequest into the runner config (inject queryFn,
|
|
359
|
+
* env, tool policy, per-skill constraints...). Defaults to identity.
|
|
360
|
+
*
|
|
361
|
+
* What this hook injects is **not** durable: a session rebuilt from a parked
|
|
362
|
+
* record is built from the stored config, and a durable store persists neither
|
|
363
|
+
* `env` nor injected functions (see `toDurableRecord` in session-store.ts). That costs the
|
|
364
|
+
* Claude engine nothing, since it cannot park — but a provider host that
|
|
365
|
+
* resolves credentials into `config.env` here for its `createEngineRunner` to
|
|
366
|
+
* read back loses them on the wake. Resolve them in the factory instead. */
|
|
367
|
+
buildRunnerConfig?: (req: CreateSessionRequest) => SessionRunnerConfig; /** URL prefix for all routes. Default '/v1'. */
|
|
368
|
+
basePath?: string;
|
|
369
|
+
/**
|
|
370
|
+
* Handle requests that fall outside `basePath` instead of 404ing them. The
|
|
371
|
+
* turnkey CLI serves the dashboard through this, which is the whole reason it
|
|
372
|
+
* exists: a browser cannot put a header on a WebSocket handshake, so the only
|
|
373
|
+
* credential a tab can present on a session attach is a cookie — and a cookie
|
|
374
|
+
* only rides requests to the origin that set it. Serving the app and the API
|
|
375
|
+
* from one origin is therefore not a convenience, it is what makes an
|
|
376
|
+
* authenticated dashboard possible without a stamping proxy in front.
|
|
377
|
+
*
|
|
378
|
+
* Upgrades are not routed here: anything outside `basePath` is still refused.
|
|
379
|
+
*/
|
|
380
|
+
fallback?: (req: IncomingMessage, res: ServerResponse) => void | Promise<void>; /** Max JSON body size in bytes. Default 1 MiB. */
|
|
381
|
+
maxBodyBytes?: number;
|
|
382
|
+
/**
|
|
383
|
+
* Server-wide bypass policy: refuse `permissionMode: 'bypassPermissions'` on
|
|
384
|
+
* session/job creation (403), and strip the `allowDangerouslySkipPermissions`
|
|
385
|
+
* pre-authorization from requests (so clients that ask for the capability by
|
|
386
|
+
* default keep working — their later switch attempt fails with the CLI's own
|
|
387
|
+
* visible error instead). Mirrors Claude Code's
|
|
388
|
+
* `permissions.disableBypassPermissionsMode` setting, enforced at the gateway.
|
|
389
|
+
*/
|
|
390
|
+
disableBypassPermissions?: boolean;
|
|
391
|
+
/**
|
|
392
|
+
* Fail closed on subscription credentials: if a session initializes with
|
|
393
|
+
* `apiKeySource: 'oauth'` (a claude.ai login rather than an API key / Bedrock / Vertex),
|
|
394
|
+
* it is terminated with a session_error. Recommended for services and any
|
|
395
|
+
* unattended/scheduled use — Anthropic's terms require API-key auth for those.
|
|
396
|
+
* Off by default: single-user personal deployments may legitimately run on the
|
|
397
|
+
* operator's own subscription; the server then logs a one-time notice instead.
|
|
398
|
+
*/
|
|
399
|
+
requireApiKey?: boolean;
|
|
400
|
+
/**
|
|
401
|
+
* Launch-time credential sanity check: once `listen()` binds, each Claude
|
|
402
|
+
* profile's session environment — exactly what `buildRunnerConfig` would hand
|
|
403
|
+
* a session, host hook included — is probed with the SDK-bundled CLI's
|
|
404
|
+
* `claude auth status`, concurrently and fire-and-forget, and a profile that
|
|
405
|
+
* reports logged-out gets one console warning. Warn, never fail: the operator
|
|
406
|
+
* may be about to log in, and a probe that cannot run at all (missing binary,
|
|
407
|
+
* a CLI without `auth status`, unparseable output) stays silent — "couldn't
|
|
408
|
+
* check" is not "not logged in". No credential material is read or logged.
|
|
409
|
+
* Off by default (this is a library; tests must spawn nothing) — the turnkey
|
|
410
|
+
* CLI turns it on. Pass an object to inject the probe (tests) or a timeout.
|
|
411
|
+
*/
|
|
412
|
+
checkCredentials?: boolean | {
|
|
413
|
+
probe?: ClaudeAuthProbe;
|
|
414
|
+
timeoutMs?: number;
|
|
415
|
+
};
|
|
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. */
|
|
418
|
+
listSdkSessions?: SdkSessionLister;
|
|
419
|
+
/** Enable the job queue (`/jobs` + `/queue` routes). Jobs run as ordinary registry
|
|
420
|
+
* sessions — attachable over the sessions WS — governed by these limits. */
|
|
421
|
+
queue?: QueueServerOptions;
|
|
422
|
+
/**
|
|
423
|
+
* Out-of-band notification for interactive sessions: the four moments a person
|
|
424
|
+
* away from the screen needs (permission requested, turn done, error, closed)
|
|
425
|
+
* POSTed to a webhook and/or handed to a local observer. Off unless configured.
|
|
426
|
+
*
|
|
427
|
+
* Server-wide, unlike the queue's per-job webhook — the point is to hear about
|
|
428
|
+
* sessions you neither created nor are attached to, which is the situation a
|
|
429
|
+
* mobile client is in permanently (iOS will not hold a WebSocket open in the
|
|
430
|
+
* background). Every registry session qualifies, job runs included, so a job
|
|
431
|
+
* carrying its own webhook is reported on both channels.
|
|
432
|
+
*
|
|
433
|
+
* This is the primitive, and it stays transport-agnostic on purpose: the OSS
|
|
434
|
+
* server holds no push credentials. Turning a notification into an APNs push is
|
|
435
|
+
* a forwarder's job — see the turnkey CLI.
|
|
436
|
+
*/
|
|
437
|
+
notifications?: SessionNotificationOptions;
|
|
438
|
+
/** Browser-bridged tool execution: how long a bridged call may go unanswered
|
|
439
|
+
* before it fails (default 60000), and where terminal results are delivered.
|
|
440
|
+
* The hub is always available on the returned server as `bridge`. */
|
|
441
|
+
bridge?: BridgeHubOptions;
|
|
442
|
+
/**
|
|
443
|
+
* Deferred execution: a session that parks on an execution nothing here is
|
|
444
|
+
* running has its state persisted and its runner torn down, and comes back when
|
|
445
|
+
* the result is POSTed to `{basePath}/executions/:executionId/result`.
|
|
446
|
+
*
|
|
447
|
+
* On by default with an in-memory store, so a park survives a disconnect but not
|
|
448
|
+
* a restart; pass `store: createFileSessionStore()` (or your own) to change that
|
|
449
|
+
* — read its doc first, the record holds the whole transcript.
|
|
450
|
+
*/
|
|
451
|
+
parking?: {
|
|
452
|
+
store?: SessionStore; /** Grace after the last client detaches before parking. Default 2000. */
|
|
453
|
+
parkDelayMs?: number;
|
|
454
|
+
/** Grace given on boot to an execution whose deadline passed while the server
|
|
455
|
+
* was down (durable stores only — nothing else survives a restart). Default 60000. */
|
|
456
|
+
expiredGraceMs?: number; /** Park/resume failures — storage or engine-assembly problems, not session errors. */
|
|
457
|
+
onError?: (error: unknown, context: {
|
|
458
|
+
sessionId: string;
|
|
459
|
+
phase: 'park' | 'resume';
|
|
460
|
+
}) => void;
|
|
461
|
+
};
|
|
462
|
+
/**
|
|
463
|
+
* Build a runner for a `provider` profile (the model-agnostic engine).
|
|
464
|
+
* Required if any such profile is declared — the server refuses to start
|
|
465
|
+
* otherwise, rather than failing at create time.
|
|
466
|
+
*
|
|
467
|
+
* Kept as a host hook so the server package neither imports a model SDK nor
|
|
468
|
+
* decides how provider credentials are resolved: the factory reads them from
|
|
469
|
+
* the operator's environment, exactly like the Claude credential chain.
|
|
470
|
+
*
|
|
471
|
+
* May be async: assembly that has to await — a per-session MCP connect, a
|
|
472
|
+
* credential lookup — belongs here, with `AiSdkRunnerConfig.onClose` as the
|
|
473
|
+
* disposer. A rejection fails the create — the session POST answers 500 with
|
|
474
|
+
* the message, a job goes straight to `failed`.
|
|
475
|
+
*/
|
|
476
|
+
createEngineRunner?: (context: EngineRunnerContext) => Runner | Promise<Runner>;
|
|
477
|
+
};
|
|
478
|
+
type EngineRunnerContext = {
|
|
479
|
+
/** The session config, with profile defaults already applied. */config: SessionRunnerConfig; /** The profile that selected this engine. */
|
|
480
|
+
profile: ProfileInfo;
|
|
481
|
+
/** Bridge hub, for handing the runner a browser-backed ToolExecutor
|
|
482
|
+
* (`bridge.executorFor(sessionId)`). */
|
|
483
|
+
bridge: BridgeHub;
|
|
484
|
+
/**
|
|
485
|
+
* Set when rebuilding a session that parked on a deferred execution. Forward it
|
|
486
|
+
* as `restore` on the engine config (`createEngineSession({ config: { ...config,
|
|
487
|
+
* restore } })`) — the engine then adopts the session's id, event log, seq
|
|
488
|
+
* numbering, history, and scratch filesystem instead of starting fresh.
|
|
489
|
+
*/
|
|
490
|
+
restore?: RunnerSnapshot;
|
|
491
|
+
};
|
|
492
|
+
type QueueServerOptions = {
|
|
493
|
+
/** Concurrent job sessions. Default 1. */maxConcurrency?: number; /** Token cap per job session (input+output+cache tokens); exceeding it kills the run. */
|
|
494
|
+
sessionTokenLimit?: number; /** Global job-token budget per UTC day; queued jobs are held once exhausted. */
|
|
495
|
+
dailyTokenLimit?: number; /** Wall-clock cap per job run — the watchdog against stuck CLIs. */
|
|
496
|
+
maxJobDurationMs?: number; /** Grace between interrupting a killed run and force-closing it. Default 5000. */
|
|
497
|
+
killGraceMs?: number;
|
|
498
|
+
/** Expire terminal jobs after `maxAgeMs` (the in-memory adapter otherwise grows
|
|
499
|
+
* unboundedly). */
|
|
500
|
+
retention?: {
|
|
501
|
+
maxAgeMs: number;
|
|
502
|
+
sweepIntervalMs?: number;
|
|
503
|
+
};
|
|
504
|
+
/** Queue backend. Defaults to the bundled in-memory adapter (single process,
|
|
505
|
+
* no persistence) — redis/bullmq/pubsub adapters implement the same interface. */
|
|
506
|
+
adapter?: QueueAdapter; /** Webhook delivery attempts per event (default 3, exponential backoff). */
|
|
507
|
+
webhookAttempts?: number;
|
|
508
|
+
webhookRetryDelayMs?: number; /** Local observer for job lifecycle events (in addition to per-job webhooks). */
|
|
509
|
+
onEvent?: (event: JobEvent) => void;
|
|
510
|
+
};
|
|
511
|
+
type WorkerServer = {
|
|
512
|
+
server: Server;
|
|
513
|
+
registry: SessionRegistry; /** The job queue, when `queue` options were provided. */
|
|
514
|
+
queue?: JobQueue;
|
|
515
|
+
/** Routes tool executions to attached browser clients. `bridge.executorFor(id)`
|
|
516
|
+
* is the `ToolExecutor` to hand a runner that should execute in the tab. */
|
|
517
|
+
bridge: BridgeHub;
|
|
518
|
+
/** Parked sessions: the store, the execution index, and the rehydration path.
|
|
519
|
+
* Deliver a deferred result with `parking.submitResult(...)` in-process, or POST
|
|
520
|
+
* it to `{basePath}/executions/:executionId/result`. */
|
|
521
|
+
parking: SessionParkManager;
|
|
522
|
+
listen: (port: number, host?: string) => Promise<{
|
|
523
|
+
port: number;
|
|
524
|
+
}>;
|
|
525
|
+
close: () => Promise<void>;
|
|
526
|
+
};
|
|
527
|
+
declare function createWorkerServer(options?: WorkerServerOptions): WorkerServer;
|
|
528
|
+
//#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 };
|
|
530
|
+
//# sourceMappingURL=index.d.mts.map
|