@sigrun/sigrun 0.1.3 → 0.1.5
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/CHANGELOG.md +46 -0
- package/README.md +24 -2
- package/dist/blocks/builders.js +125 -0
- package/dist/blocks/images.js +57 -0
- package/dist/blocks/index.js +20 -0
- package/dist/blocks/limits.js +26 -0
- package/dist/blocks/placements.js +99 -0
- package/dist/blocks/schema.js +559 -0
- package/dist/blocks/validate.js +602 -0
- package/dist/config.js +31 -0
- package/dist/extension.d.ts +476 -9
- package/dist/harness/boot.js +8 -1
- package/dist/harness/event-bus.js +33 -0
- package/dist/harness/identity/users.js +3 -2
- package/dist/harness/loader.js +369 -13
- package/dist/harness/packages.js +71 -2
- package/dist/harness/seam-host.js +69 -2
- package/dist/harness/settings.js +5 -0
- package/dist/jobs/fire.js +11 -0
- package/dist/jobs/spec.js +19 -0
- package/dist/serve.js +33 -41
- package/dist/server/auth/pipeline.js +16 -1
- package/dist/server/conversation-ops.js +482 -10
- package/dist/server/gateway.js +122 -0
- package/dist/server/helpers.js +26 -0
- package/dist/server/host-bridge.js +141 -4
- package/dist/server/index.js +27 -0
- package/dist/server/notifications.js +6 -2
- package/dist/server/routes/conversations.js +37 -93
- package/dist/server/routes/events.js +39 -0
- package/dist/server/routes/ext-assets.js +84 -0
- package/dist/server/routes/extensions.js +31 -1
- package/dist/server/routes/files.js +38 -83
- package/dist/server/routes/meta.js +37 -2
- package/dist/server/routes/providers.js +5 -21
- package/dist/server/routes/submissions.js +36 -111
- package/dist/server/routes/surfaces.js +387 -0
- package/dist/server/routes/users.js +6 -1
- package/dist/server/schema-fixture.js +7 -0
- package/dist/server/session-meta.js +12 -6
- package/dist/server/shutdown.js +85 -0
- package/dist/server/surfaces/access-cache.js +43 -0
- package/dist/server/surfaces/dedup.js +67 -0
- package/dist/server/surfaces/instance.js +42 -0
- package/dist/server/surfaces/mime.js +73 -0
- package/dist/server/surfaces/rate-limit.js +63 -0
- package/dist/server/surfaces/service.js +493 -0
- package/dist/server/surfaces/watchers.js +94 -0
- package/node_modules/@sigrun/shared/dist/blocks/format.d.ts +28 -0
- package/node_modules/@sigrun/shared/dist/blocks/format.js +199 -0
- package/node_modules/@sigrun/shared/dist/blocks/icons.d.ts +12 -0
- package/node_modules/@sigrun/shared/dist/blocks/icons.js +64 -0
- package/node_modules/@sigrun/shared/dist/blocks/index.d.ts +25 -0
- package/node_modules/@sigrun/shared/dist/blocks/index.js +21 -0
- package/node_modules/@sigrun/shared/dist/blocks/instance.d.ts +29 -0
- package/node_modules/@sigrun/shared/dist/blocks/instance.js +51 -0
- package/node_modules/@sigrun/shared/dist/blocks/lttb.d.ts +16 -0
- package/node_modules/@sigrun/shared/dist/blocks/lttb.js +67 -0
- package/node_modules/@sigrun/shared/dist/blocks/ops.d.ts +44 -0
- package/node_modules/@sigrun/shared/dist/blocks/ops.js +294 -0
- package/node_modules/@sigrun/shared/dist/blocks/text.d.ts +43 -0
- package/node_modules/@sigrun/shared/dist/blocks/text.js +790 -0
- package/node_modules/@sigrun/shared/dist/blocks/transport.d.ts +77 -0
- package/node_modules/@sigrun/shared/dist/blocks/transport.js +11 -0
- package/node_modules/@sigrun/shared/dist/blocks/types.d.ts +541 -0
- package/node_modules/@sigrun/shared/dist/blocks/types.js +17 -0
- package/node_modules/@sigrun/shared/dist/jobs.d.ts +9 -0
- package/node_modules/@sigrun/shared/dist/live.d.ts +71 -0
- package/node_modules/@sigrun/shared/dist/live.js +82 -0
- package/node_modules/@sigrun/shared/dist/session-meta.d.ts +19 -3
- package/node_modules/@sigrun/shared/dist/settings.d.ts +7 -0
- package/node_modules/@sigrun/shared/dist/views.d.ts +83 -3
- package/node_modules/@sigrun/shared/package.json +10 -2
- package/package.json +9 -3
- package/webui/dist/assets/index-B4-GYqEb.js +50 -0
- package/webui/dist/assets/index-BiSMljer.css +1 -0
- package/webui/dist/index.html +2 -2
- package/webui/dist/assets/index-2RfqJO57.css +0 -1
- package/webui/dist/assets/index-DkZgTngD.js +0 -50
package/dist/extension.d.ts
CHANGED
|
@@ -36,10 +36,13 @@ import type { AssistantMessage, Provider } from "@earendil-works/pi-ai";
|
|
|
36
36
|
import type { ConversationDocToken, ConversationId, Cursor, EntryId, EntryQuery, EntryRecord, Extension, InputSubmissionDraft, JsonObject, ModelRef, Page, SubmissionId } from "@earendil-works/pi-durable";
|
|
37
37
|
import type { Access, Sharing } from "@sigrun/shared/access";
|
|
38
38
|
import type { AmbientBody, AmbientPlace, AmbientRecord } from "@sigrun/shared/ambient";
|
|
39
|
+
import type { Action, DataOps, IconName, Tree } from "@sigrun/shared/blocks";
|
|
39
40
|
import type { InteractionAnswer, InteractionRecord } from "@sigrun/shared/interactions";
|
|
40
41
|
import type { JobSpec } from "@sigrun/shared/jobs";
|
|
41
|
-
import type { ConversationSummary, ConversationViewWire, HealthStatus, SessionMeta, SubmissionRecordWire, UserWire as DirectoryUserWire } from "@sigrun/shared/views";
|
|
42
|
-
export type { Access, AmbientRecord, DirectoryUserWire, InteractionRecord, JobSpec, Sharing, };
|
|
42
|
+
import type { ConversationSummary, ConversationViewWire, HealthStatus, ModelEntry, SessionMeta, SlashCommand, SubmissionRecordWire, ThinkingLevelWire, UserWire as DirectoryUserWire } from "@sigrun/shared/views";
|
|
43
|
+
export type { Access, AmbientRecord, DirectoryUserWire, InteractionRecord, JobSpec, ModelEntry, Sharing, SlashCommand, };
|
|
44
|
+
type JsonParams = JsonObject;
|
|
45
|
+
type JsonParamValue = JsonParams[string];
|
|
43
46
|
/**
|
|
44
47
|
* The invocation-scoped context handlers and host APIs receive: a
|
|
45
48
|
* structural view of the runtime context (its cancellation signal).
|
|
@@ -111,6 +114,14 @@ export type ExtensionLifecyclePayload = {
|
|
|
111
114
|
readonly conversationId: ConversationId;
|
|
112
115
|
readonly idleMs: number;
|
|
113
116
|
};
|
|
117
|
+
/** Gateway C6: one terminal interaction transition — the payload mirrors
|
|
118
|
+
* the record's terminal envelope fields. */
|
|
119
|
+
interactionSettled: {
|
|
120
|
+
readonly conversationId: ConversationId;
|
|
121
|
+
readonly callId: string;
|
|
122
|
+
readonly kind: string;
|
|
123
|
+
readonly status: "answered" | "skipped" | "expired" | "cancelled";
|
|
124
|
+
};
|
|
114
125
|
};
|
|
115
126
|
/** A factory-result `runSettled` handler: called once per settled run, deadline-bounded. */
|
|
116
127
|
export type ExtensionRunSettledHandler = (event: RunSettledEvent, api: MemoryEventApi, context: HandlerContext) => void | Promise<void>;
|
|
@@ -135,6 +146,8 @@ export type SigrunLifecycleHandlers = {
|
|
|
135
146
|
readonly conversationForked?: ExtensionLifecycleHandler<"conversationForked">;
|
|
136
147
|
readonly conversationArchived?: ExtensionLifecycleHandler<"conversationArchived">;
|
|
137
148
|
readonly conversationIdle?: ExtensionLifecycleHandler<"conversationIdle">;
|
|
149
|
+
/** Terminal interaction transitions from ANY writer (gateway C6). */
|
|
150
|
+
readonly interactionSettled?: ExtensionLifecycleHandler<"interactionSettled">;
|
|
138
151
|
readonly harnessShutdown?: ExtensionHarnessShutdownHandler;
|
|
139
152
|
/** The user-removal transfer event; direct fan-out, not commit-derived. */
|
|
140
153
|
readonly userDeleted?: ExtensionUserDeletedHandler;
|
|
@@ -204,8 +217,11 @@ export interface WebuiBundleContribution {
|
|
|
204
217
|
/**
|
|
205
218
|
* The resolved caller (core hardening W1): what the auth pipeline
|
|
206
219
|
* stamps on every request. `method` names the proof — the empty-method
|
|
207
|
-
* naive path (`"none"`), a verified token (`"token"`),
|
|
208
|
-
* forward-auth header set (`"proxy"`)
|
|
220
|
+
* naive path (`"none"`), a verified token (`"token"`), a trusted
|
|
221
|
+
* forward-auth header set (`"proxy"`), or an IN-PROCESS host-built
|
|
222
|
+
* principal (`"host"`, gateway C4b: `commands.run` builds it from the
|
|
223
|
+
* directory — no request ever carries one, but a command run through
|
|
224
|
+
* the host sees exactly which proof its caller had).
|
|
209
225
|
*/
|
|
210
226
|
export interface Principal {
|
|
211
227
|
/** The directory username; `undefined` under the naive method with no header. */
|
|
@@ -215,7 +231,7 @@ export interface Principal {
|
|
|
215
231
|
readonly admin: boolean;
|
|
216
232
|
/** True for the `SIGRUN_API_TOKEN` service principal. */
|
|
217
233
|
readonly service: boolean;
|
|
218
|
-
readonly method: "none" | "token" | "proxy";
|
|
234
|
+
readonly method: "none" | "token" | "proxy" | "host";
|
|
219
235
|
}
|
|
220
236
|
/** One extension HTTP route, mounted at `/v1/ext/<slug>/<path>`. */
|
|
221
237
|
export interface ExtensionRouteContribution {
|
|
@@ -252,6 +268,9 @@ export interface NotificationEvent {
|
|
|
252
268
|
readonly type: "interaction.pending" | "job.failed" | "run.settled";
|
|
253
269
|
readonly conversationId?: number;
|
|
254
270
|
readonly jobId?: string;
|
|
271
|
+
/** Gateway C6: the pending record's id — set on `interaction.pending` only
|
|
272
|
+
* (read the record via `host.snapshotInteraction`). */
|
|
273
|
+
readonly callId?: string;
|
|
255
274
|
/** The recipient. */
|
|
256
275
|
readonly user: string | undefined;
|
|
257
276
|
readonly title: string;
|
|
@@ -266,6 +285,315 @@ export interface NotifierContribution {
|
|
|
266
285
|
readonly host: SigrunHost;
|
|
267
286
|
}): Promise<void>;
|
|
268
287
|
}
|
|
288
|
+
/** Whether an adapter's credentials/config are present; unconfigured adapters are never started. */
|
|
289
|
+
export type ChannelReadiness = {
|
|
290
|
+
readonly status: "not_configured" | "ready";
|
|
291
|
+
readonly detail?: string;
|
|
292
|
+
};
|
|
293
|
+
/** What a chat platform can render — the engine formats against these. */
|
|
294
|
+
export interface ChannelCapabilities {
|
|
295
|
+
readonly chatTypes: readonly ("dm" | "group" | "thread")[];
|
|
296
|
+
readonly maxMessageLength: number;
|
|
297
|
+
readonly format: "plain" | "markdown" | "native";
|
|
298
|
+
/** Edit-in-place streaming; `minIntervalMs` is the platform's safe edit rate. */
|
|
299
|
+
readonly edit?: {
|
|
300
|
+
readonly minIntervalMs: number;
|
|
301
|
+
};
|
|
302
|
+
/** `labelMax` bounds button text; the engine truncates, never the adapter. */
|
|
303
|
+
readonly buttons?: {
|
|
304
|
+
readonly max: number;
|
|
305
|
+
readonly perRow?: number;
|
|
306
|
+
readonly labelMax?: number;
|
|
307
|
+
};
|
|
308
|
+
readonly media?: readonly ("image" | "audio" | "video" | "document")[];
|
|
309
|
+
readonly typing?: {
|
|
310
|
+
readonly intervalMs: number;
|
|
311
|
+
};
|
|
312
|
+
readonly replyTo?: boolean;
|
|
313
|
+
readonly commandMenu?: boolean;
|
|
314
|
+
/** One short paragraph for the prompt: what renders, what doesn't. */
|
|
315
|
+
readonly promptHint?: string;
|
|
316
|
+
}
|
|
317
|
+
/** One chat on a platform — where a conversation lives. */
|
|
318
|
+
export interface ChatRef {
|
|
319
|
+
readonly chatId: string;
|
|
320
|
+
readonly chatType: "dm" | "group" | "thread";
|
|
321
|
+
readonly threadId?: string;
|
|
322
|
+
}
|
|
323
|
+
/** One platform message — the edit/delete target. */
|
|
324
|
+
export interface MessageRef {
|
|
325
|
+
readonly chat: ChatRef;
|
|
326
|
+
readonly messageId: string;
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* One inbound media attachment. `fetch` is lazy so unwanted media is
|
|
330
|
+
* never downloaded; the adapter stops reading and throws once `maxBytes`
|
|
331
|
+
* is exceeded, so an undeclared-size file is never fully downloaded
|
|
332
|
+
* either.
|
|
333
|
+
*
|
|
334
|
+
* The size-limit error is an error whose `name` is
|
|
335
|
+
* {@link AttachmentTooLargeName} (`"AttachmentTooLarge"`): the engine
|
|
336
|
+
* matches it by NAME, never `instanceof` — this types-only contract
|
|
337
|
+
* ships no runtime subpath, so class identity could never line up across
|
|
338
|
+
* an adapter's own dependency tree.
|
|
339
|
+
*/
|
|
340
|
+
export interface InboundAttachment {
|
|
341
|
+
readonly kind: "image" | "audio" | "voice" | "video" | "document" | "sticker";
|
|
342
|
+
readonly mime?: string;
|
|
343
|
+
readonly filename?: string;
|
|
344
|
+
readonly size?: number;
|
|
345
|
+
fetch(opts: {
|
|
346
|
+
maxBytes: number;
|
|
347
|
+
signal: AbortSignal;
|
|
348
|
+
}): Promise<Uint8Array>;
|
|
349
|
+
}
|
|
350
|
+
/** One inbound chat message, handed to the engine through `ChannelContext.receive`. */
|
|
351
|
+
export interface InboundEvent {
|
|
352
|
+
readonly messageId: string;
|
|
353
|
+
readonly chat: ChatRef & {
|
|
354
|
+
readonly title?: string;
|
|
355
|
+
};
|
|
356
|
+
readonly sender: {
|
|
357
|
+
readonly id: string;
|
|
358
|
+
readonly name?: string;
|
|
359
|
+
readonly isBot?: boolean;
|
|
360
|
+
};
|
|
361
|
+
/** The bot's own message echoed back; the engine drops it. */
|
|
362
|
+
readonly fromSelf?: boolean;
|
|
363
|
+
readonly text: string;
|
|
364
|
+
/** Group messages: the bot was @-mentioned or replied to. */
|
|
365
|
+
readonly mentioned?: boolean;
|
|
366
|
+
readonly replyTo?: {
|
|
367
|
+
readonly messageId: string;
|
|
368
|
+
readonly text?: string;
|
|
369
|
+
readonly authorName?: string;
|
|
370
|
+
readonly fromSelf?: boolean;
|
|
371
|
+
};
|
|
372
|
+
readonly attachments?: readonly InboundAttachment[];
|
|
373
|
+
/** Platform album/batch id (e.g. a media group); events sharing it merge into one submission. */
|
|
374
|
+
readonly groupId?: string;
|
|
375
|
+
}
|
|
376
|
+
/** A button tap: `data` is the opaque token the engine put on the button. */
|
|
377
|
+
export interface ActionEvent {
|
|
378
|
+
readonly chat: ChatRef;
|
|
379
|
+
readonly sender: {
|
|
380
|
+
readonly id: string;
|
|
381
|
+
readonly name?: string;
|
|
382
|
+
};
|
|
383
|
+
readonly message?: MessageRef;
|
|
384
|
+
readonly data: string;
|
|
385
|
+
}
|
|
386
|
+
/** One message to send: already formatted per `capabilities.format` and within `maxMessageLength`. */
|
|
387
|
+
export interface OutboundMessage {
|
|
388
|
+
readonly chat: ChatRef;
|
|
389
|
+
readonly text: string;
|
|
390
|
+
readonly replyTo?: string;
|
|
391
|
+
readonly buttons?: readonly (readonly {
|
|
392
|
+
readonly label: string;
|
|
393
|
+
readonly data: string;
|
|
394
|
+
readonly style?: "primary" | "danger" | "neutral";
|
|
395
|
+
}[])[];
|
|
396
|
+
readonly files?: readonly {
|
|
397
|
+
readonly name: string;
|
|
398
|
+
readonly mime?: string;
|
|
399
|
+
readonly bytes: Uint8Array;
|
|
400
|
+
}[];
|
|
401
|
+
}
|
|
402
|
+
/** Which platform messages one `send` produced (a media group, a split message). */
|
|
403
|
+
export interface SendReceipt {
|
|
404
|
+
readonly messageIds: readonly string[];
|
|
405
|
+
}
|
|
406
|
+
/** The engine-side context handed to a started adapter. */
|
|
407
|
+
export interface ChannelContext {
|
|
408
|
+
readonly signal: AbortSignal;
|
|
409
|
+
/**
|
|
410
|
+
* Resolves once the event is durably admitted, handled as a command,
|
|
411
|
+
* or deliberately dropped: the adapter acks the platform on any
|
|
412
|
+
* resolution. A rejection means the engine failed; the adapter does
|
|
413
|
+
* not ack, the platform redelivers, and dedupe makes the retry safe.
|
|
414
|
+
*/
|
|
415
|
+
receive(event: InboundEvent): Promise<{
|
|
416
|
+
readonly outcome: "admitted" | "dropped" | "command";
|
|
417
|
+
}>;
|
|
418
|
+
/** A button tap. `data` is the opaque token the engine put on the button. */
|
|
419
|
+
action(event: ActionEvent): Promise<void>;
|
|
420
|
+
status(state: {
|
|
421
|
+
readonly state: "connecting" | "running";
|
|
422
|
+
readonly detail?: string;
|
|
423
|
+
}): void;
|
|
424
|
+
fail(error: unknown): void;
|
|
425
|
+
readonly log: {
|
|
426
|
+
info(msg: string): void;
|
|
427
|
+
warn(msg: string): void;
|
|
428
|
+
error(msg: string, error?: unknown): void;
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
/**
|
|
432
|
+
* One channel adapter: one chat platform's transport. The engine owns
|
|
433
|
+
* supervision and delivery; the adapter owns the platform.
|
|
434
|
+
*/
|
|
435
|
+
export interface ChannelContribution {
|
|
436
|
+
/** Must match `^[a-z][a-z0-9-]*$`; unique across every loaded extension. */
|
|
437
|
+
readonly id: string;
|
|
438
|
+
readonly label: string;
|
|
439
|
+
readonly capabilities: ChannelCapabilities;
|
|
440
|
+
/** Whether credentials/config are present. Unconfigured adapters are never started. */
|
|
441
|
+
configured(): ChannelReadiness | Promise<ChannelReadiness>;
|
|
442
|
+
/**
|
|
443
|
+
* Connect and begin receiving. Resolves once connected; any receive
|
|
444
|
+
* loop keeps running until `ctx.signal` aborts. Post-connect failures
|
|
445
|
+
* go through `ctx.fail` so the engine's supervisor owns restarts
|
|
446
|
+
* (adapters never self-reconnect forever).
|
|
447
|
+
*/
|
|
448
|
+
start(ctx: ChannelContext): Promise<void>;
|
|
449
|
+
stop?(): Promise<void>;
|
|
450
|
+
send(message: OutboundMessage): Promise<SendReceipt>;
|
|
451
|
+
edit?(ref: MessageRef, message: OutboundMessage): Promise<void>;
|
|
452
|
+
delete?(ref: MessageRef): Promise<void>;
|
|
453
|
+
typing?(chat: ChatRef): Promise<void>;
|
|
454
|
+
setCommands?(commands: readonly {
|
|
455
|
+
readonly name: string;
|
|
456
|
+
readonly description: string;
|
|
457
|
+
}[]): Promise<void>;
|
|
458
|
+
/** Markdown → the platform's native markup. Required when `capabilities.format` is `"native"`. */
|
|
459
|
+
format?(markdown: string): string;
|
|
460
|
+
/** Without it every send error is `"transient"`. */
|
|
461
|
+
classifyError?(error: unknown): "transient" | "permanent" | "chat_gone";
|
|
462
|
+
}
|
|
463
|
+
/**
|
|
464
|
+
* The `name` an adapter's size-limit error carries: the engine matches
|
|
465
|
+
* `error.name === "AttachmentTooLarge"`, never `instanceof` (no runtime
|
|
466
|
+
* subpath ships with the types-only contract — see
|
|
467
|
+
* {@link InboundAttachment.fetch}).
|
|
468
|
+
*/
|
|
469
|
+
export type AttachmentTooLargeName = "AttachmentTooLarge";
|
|
470
|
+
/**
|
|
471
|
+
* The context sigrun hands the loaded engine's `start`: the flat,
|
|
472
|
+
* slug-attributed channel set (every loaded adapter's contribution —
|
|
473
|
+
* `{extension, channel}` pairs) and the shutdown signal. The signal is
|
|
474
|
+
* the `AbortController` the host's `gateway.stop()` aborts — an engine's
|
|
475
|
+
* receive loops observe it, exactly like an adapter's `ChannelContext.signal`.
|
|
476
|
+
*/
|
|
477
|
+
export interface GatewayEngineContext {
|
|
478
|
+
readonly channels: readonly {
|
|
479
|
+
readonly extension: string;
|
|
480
|
+
readonly channel: ChannelContribution;
|
|
481
|
+
}[];
|
|
482
|
+
/** Aborted by the host's `gateway.stop()` — the engine's shutdown signal. */
|
|
483
|
+
readonly signal: AbortSignal;
|
|
484
|
+
}
|
|
485
|
+
/**
|
|
486
|
+
* One gateway engine: the chat-transport supervisor the `gateway`
|
|
487
|
+
* factory-result field names. ONE engine per boot (a second declarer
|
|
488
|
+
* quarantines at load — the one-engine rule); core ships none. `start`
|
|
489
|
+
* runs after the server listens; `stopIntake` runs before the event bus's
|
|
490
|
+
* shutdown drain (intake closes first — the drain is the last admission
|
|
491
|
+
* window); `stop` runs after it (adapters stop only once every settle has
|
|
492
|
+
* been delivered). Health is reported through `health()` — NEVER the
|
|
493
|
+
* factory-result `health` field (that field is the provider-contributor
|
|
494
|
+
* surface; a result carrying both quarantines).
|
|
495
|
+
*/
|
|
496
|
+
export interface GatewayEngine {
|
|
497
|
+
/** Start supervision over the channels; resolves once started. */
|
|
498
|
+
start(ctx: GatewayEngineContext): Promise<void>;
|
|
499
|
+
/** Close inbound intake (admissions stop) — before the shutdown drain. */
|
|
500
|
+
stopIntake(): Promise<void>;
|
|
501
|
+
/** Stop the engine and its adapters — after the shutdown drain. */
|
|
502
|
+
stop(): Promise<void>;
|
|
503
|
+
/** The engine's synchronous health read ("ok" | "degraded"). */
|
|
504
|
+
health(): "ok" | "degraded";
|
|
505
|
+
}
|
|
506
|
+
/**
|
|
507
|
+
* The render/data access verdict a surface reports for one principal
|
|
508
|
+
* (spec §6.5): `none` hides the surface entirely (404 on every route).
|
|
509
|
+
*/
|
|
510
|
+
export type SurfaceAccessLevel = "none" | "read" | "write";
|
|
511
|
+
/** The per-principal, per-instance context `render`/`data` receive. */
|
|
512
|
+
export interface SurfaceContext {
|
|
513
|
+
/** Who is looking; the access verdict is this principal's. */
|
|
514
|
+
readonly principal: Principal;
|
|
515
|
+
/** From `navigate(params)`; validated as JSON, size-capped at 4 KiB. */
|
|
516
|
+
readonly params: JsonParams;
|
|
517
|
+
/** The instance identity: `instanceKey(params)` of this instance. */
|
|
518
|
+
readonly key: string;
|
|
519
|
+
/** Aborts when the request is abandoned; observe it in long work. */
|
|
520
|
+
readonly signal: AbortSignal;
|
|
521
|
+
}
|
|
522
|
+
/** The full render result: the tree, its data, and the access verdict. */
|
|
523
|
+
export interface SurfaceRenderResult {
|
|
524
|
+
readonly tree: Tree;
|
|
525
|
+
readonly data: JsonObject;
|
|
526
|
+
readonly access: SurfaceAccessLevel;
|
|
527
|
+
}
|
|
528
|
+
/** The lighter `data()` result: refreshed data plus the access verdict. */
|
|
529
|
+
export interface SurfaceDataResult {
|
|
530
|
+
readonly data: JsonObject;
|
|
531
|
+
readonly access: SurfaceAccessLevel;
|
|
532
|
+
}
|
|
533
|
+
/** The payload of an `onUpload` handler (spec §7.1 — snif... never trusted). */
|
|
534
|
+
export interface UploadedFile {
|
|
535
|
+
/** The file field's name (the `file` field that carried the upload). */
|
|
536
|
+
readonly field: string;
|
|
537
|
+
/** Client-supplied filename, sanitised to a basename. */
|
|
538
|
+
readonly name: string;
|
|
539
|
+
/** Sniffed from the bytes, never trusted from the client. */
|
|
540
|
+
readonly mime: string;
|
|
541
|
+
/** Capped by the field's `maxBytes` and `server.maxUploadBytes`. */
|
|
542
|
+
readonly bytes: Uint8Array;
|
|
543
|
+
}
|
|
544
|
+
/**
|
|
545
|
+
* What an action handler returns (spec §6.3/§7.1): accepted (with an
|
|
546
|
+
* optional data update, a navigation, and a toast) or rejected (an
|
|
547
|
+
* operator-facing error, optionally field-scoped).
|
|
548
|
+
*/
|
|
549
|
+
export type ActionResult = {
|
|
550
|
+
readonly ok: true;
|
|
551
|
+
/** Data ops the server applies to the instance's data (sharedData only keeps them; §7.3). */
|
|
552
|
+
readonly update?: DataOps;
|
|
553
|
+
/** A `navigate` action (spec §6.1): the channel routes it through the normal navigation paths. */
|
|
554
|
+
readonly navigate?: Extract<Action, {
|
|
555
|
+
type: "navigate";
|
|
556
|
+
}>;
|
|
557
|
+
readonly toast?: string;
|
|
558
|
+
} | {
|
|
559
|
+
readonly ok: false;
|
|
560
|
+
readonly error: {
|
|
561
|
+
readonly message: string;
|
|
562
|
+
readonly field?: string;
|
|
563
|
+
};
|
|
564
|
+
};
|
|
565
|
+
/**
|
|
566
|
+
* One declared surface (spec §7.1, verbatim shape): the placement is
|
|
567
|
+
* the generic parameter — `"view"` or `"panel"` (the settings sections
|
|
568
|
+
* of §13.3 are a separate workstream).
|
|
569
|
+
*/
|
|
570
|
+
export interface SurfaceDef<P extends "view" | "panel"> {
|
|
571
|
+
/** Unique within the extension. */
|
|
572
|
+
readonly id: string;
|
|
573
|
+
readonly title: string;
|
|
574
|
+
readonly icon: IconName;
|
|
575
|
+
readonly placement: P;
|
|
576
|
+
/** Data is identical for every principal with access (§7.3). */
|
|
577
|
+
readonly sharedData?: boolean;
|
|
578
|
+
/** Channels refetch data on this interval while visible (min 5000). */
|
|
579
|
+
readonly refreshMs?: number;
|
|
580
|
+
/** §7.3; default: canonical JSON of params. */
|
|
581
|
+
instanceKey?(params: JsonParams): string;
|
|
582
|
+
render(ctx: SurfaceContext): Promise<SurfaceRenderResult>;
|
|
583
|
+
data?(ctx: SurfaceContext): Promise<SurfaceDataResult>;
|
|
584
|
+
readonly actions?: Readonly<Record<string, (ctx: SurfaceContext, payload: JsonParamValue) => Promise<ActionResult>>>;
|
|
585
|
+
}
|
|
586
|
+
/**
|
|
587
|
+
* The `surfaces` field of a factory result (spec §7.1): declared views
|
|
588
|
+
* and panels. Ids are unique across BOTH lists (one namespace — the
|
|
589
|
+
* loader enforces it).
|
|
590
|
+
*/
|
|
591
|
+
export interface SurfacesContribution {
|
|
592
|
+
readonly views?: readonly SurfaceDef<"view">[];
|
|
593
|
+
readonly panels?: readonly SurfaceDef<"panel">[];
|
|
594
|
+
/** §13.3 settings sections — a separate workstream; declared for shape stability. */
|
|
595
|
+
readonly settings?: readonly never[];
|
|
596
|
+
}
|
|
269
597
|
/** One wire part of a `host.conversations.submit` input array. */
|
|
270
598
|
export type SubmissionInputPart = {
|
|
271
599
|
readonly type: "text";
|
|
@@ -313,8 +641,16 @@ export interface SigrunHost {
|
|
|
313
641
|
};
|
|
314
642
|
/** Seed (or fully replace) one interaction record; seeding a terminal record is a no-op. */
|
|
315
643
|
seedInteraction(conversationId: ConversationId, record: InteractionRecord, context: HandlerContext): Promise<void>;
|
|
316
|
-
/**
|
|
317
|
-
|
|
644
|
+
/**
|
|
645
|
+
* Answer a pending interaction from the extension side. `opts.user`
|
|
646
|
+
* (gateway C5) stamps the answer HUMAN and names the user — who
|
|
647
|
+
* answered matters to every participant a conversation is shared
|
|
648
|
+
* with; access checks stay the caller's job (host code is trusted).
|
|
649
|
+
* Without it the answer is the extension's own (`"extension"`).
|
|
650
|
+
*/
|
|
651
|
+
answerInteraction(conversationId: ConversationId, callId: string, answer: InteractionAnswer, context: HandlerContext, opts?: {
|
|
652
|
+
readonly user?: string;
|
|
653
|
+
}): Promise<boolean>;
|
|
318
654
|
/** Mark a pending interaction expired/cancelled/skipped (extension-owned semantics). */
|
|
319
655
|
settleInteraction(conversationId: ConversationId, callId: string, status: "skipped" | "expired" | "cancelled", context: HandlerContext): Promise<boolean>;
|
|
320
656
|
/** Read one interaction record (the poll loop's read). */
|
|
@@ -331,6 +667,19 @@ export interface SigrunHost {
|
|
|
331
667
|
}, context: HandlerContext): Promise<void>;
|
|
332
668
|
/** Clear one ambient surface (empty replacement; keeps ownership). */
|
|
333
669
|
clearAmbient(conversationId: ConversationId, key: string, context: HandlerContext): Promise<void>;
|
|
670
|
+
/**
|
|
671
|
+
* Publish a change for one surface instance (spec §7.3).
|
|
672
|
+
* `changed(id, key, update?)` tells the server that instance `key`
|
|
673
|
+
* of surface `id` changed: watching streams receive a `surface.changed`
|
|
674
|
+
* event and refetch. `key` is the string `instanceKey(params)` returned
|
|
675
|
+
* for that instance — params NEVER appear here. `update` carries data
|
|
676
|
+
* ops (kept only for `sharedData` surfaces, §7.3). Fire-and-forget:
|
|
677
|
+
* never awaited, and calling it for a surface without `data()` (a
|
|
678
|
+
* load-time contract violation) is rejected by the host.
|
|
679
|
+
*/
|
|
680
|
+
readonly surfaces: {
|
|
681
|
+
changed(id: string, key: string, update?: DataOps): void;
|
|
682
|
+
};
|
|
334
683
|
/**
|
|
335
684
|
* Drive conversations in-process (channel bridges, jobs) over the
|
|
336
685
|
* same code paths as the HTTP API. Extensions are trusted in-process
|
|
@@ -343,6 +692,15 @@ export interface SigrunHost {
|
|
|
343
692
|
readonly sharing?: Sharing;
|
|
344
693
|
readonly name?: string;
|
|
345
694
|
readonly source?: string;
|
|
695
|
+
/**
|
|
696
|
+
* Pin the conversation's extension/tool selection (gateway C4):
|
|
697
|
+
* names resolved against the registry — an unknown name throws a
|
|
698
|
+
* `ConversationOpError` naming `agent.extensions`/`agent.tools`.
|
|
699
|
+
*/
|
|
700
|
+
readonly agent?: {
|
|
701
|
+
readonly extensions?: readonly string[];
|
|
702
|
+
readonly tools?: readonly string[];
|
|
703
|
+
};
|
|
346
704
|
}): Promise<ConversationId>;
|
|
347
705
|
submit(id: ConversationId, input: readonly SubmissionInputPart[], opts?: {
|
|
348
706
|
readonly user?: string;
|
|
@@ -355,6 +713,69 @@ export interface SigrunHost {
|
|
|
355
713
|
readonly archived?: boolean;
|
|
356
714
|
}): Promise<readonly ConversationSummary[]>;
|
|
357
715
|
meta(id: ConversationId): Promise<SessionMeta>;
|
|
716
|
+
/**
|
|
717
|
+
* Withdraw/settle the conversation's active submission (gateway
|
|
718
|
+
* C4, the abort route's op). On an idle conversation it RESOLVES
|
|
719
|
+
* (chat `/stop` on an idle chat is not an error) — the HTTP route
|
|
720
|
+
* keeps its 404 `no_active_submission`.
|
|
721
|
+
*/
|
|
722
|
+
abort(id: ConversationId): Promise<void>;
|
|
723
|
+
/**
|
|
724
|
+
* Change the model and/or thinking level (gateway C4, the configure
|
|
725
|
+
* route's op). The inputs are typed but validated like the route's
|
|
726
|
+
* body — an unknown key or malformed model is a
|
|
727
|
+
* `ConversationOpError`.
|
|
728
|
+
*/
|
|
729
|
+
configure(id: ConversationId, opts: {
|
|
730
|
+
readonly model?: string;
|
|
731
|
+
readonly thinking?: ThinkingLevelWire;
|
|
732
|
+
}): Promise<void>;
|
|
733
|
+
/** Admit one manual compaction task (gateway C4, the compact route's op). */
|
|
734
|
+
compact(id: ConversationId): Promise<void>;
|
|
735
|
+
/**
|
|
736
|
+
* Fork the history at one entry (gateway C4, the fork route's op):
|
|
737
|
+
* absent `at` = the newest entry; `owner` stamps the fork's session
|
|
738
|
+
* owner (absent = the owner key removed). Returns the new id.
|
|
739
|
+
*/
|
|
740
|
+
fork(id: ConversationId, opts?: {
|
|
741
|
+
readonly at?: EntryId;
|
|
742
|
+
readonly owner?: string;
|
|
743
|
+
}): Promise<ConversationId>;
|
|
744
|
+
/**
|
|
745
|
+
* Patch the session meta (gateway C4, the meta PATCH route's op).
|
|
746
|
+
* Host callers are TRUSTED: the sharing patch runs with no
|
|
747
|
+
* owner-or-superuser gate (the HTTP route keeps its 403).
|
|
748
|
+
*/
|
|
749
|
+
patchMeta(id: ConversationId, patch: {
|
|
750
|
+
readonly name?: string | null;
|
|
751
|
+
readonly archived?: boolean;
|
|
752
|
+
readonly sharing?: Sharing | null;
|
|
753
|
+
}): Promise<SessionMeta>;
|
|
754
|
+
/**
|
|
755
|
+
* Upload one file into the conversation's upload namespace (gateway
|
|
756
|
+
* C4b, the raw-upload route's op): `uploads/<conversationId>/<name>`,
|
|
757
|
+
* the exact path a `file`/`image` submission part takes. The NAME is
|
|
758
|
+
* the one rule (no `/` or `\\`, not `.`/`..`, non-empty) — a bad
|
|
759
|
+
* name throws a `ConversationOpError` (`invalid_field` `name`).
|
|
760
|
+
*/
|
|
761
|
+
upload(id: ConversationId, file: {
|
|
762
|
+
readonly name: string;
|
|
763
|
+
readonly mime?: string;
|
|
764
|
+
readonly bytes: Uint8Array;
|
|
765
|
+
}): Promise<{
|
|
766
|
+
readonly path: string;
|
|
767
|
+
}>;
|
|
768
|
+
/**
|
|
769
|
+
* Read one workspace file raw (gateway C4b, the files/raw route's
|
|
770
|
+
* op): workspace-confined, capped at `server.maxFileReadBytes`, with
|
|
771
|
+
* the extension-derived mime. Over the cap throws
|
|
772
|
+
* `ConversationOpError({error: "too_large"}, 413)`; a bad path
|
|
773
|
+
* `invalid_path`; an absent file `not_found`.
|
|
774
|
+
*/
|
|
775
|
+
readFile(id: ConversationId, path: string): Promise<{
|
|
776
|
+
readonly bytes: Uint8Array;
|
|
777
|
+
readonly mime?: string;
|
|
778
|
+
}>;
|
|
358
779
|
};
|
|
359
780
|
/** The user directory (admin wire form; emails are admin material). */
|
|
360
781
|
readonly users: {
|
|
@@ -378,13 +799,42 @@ export interface SigrunHost {
|
|
|
378
799
|
readonly identity: {
|
|
379
800
|
actingUser(conversationId: ConversationId): Promise<string | undefined>;
|
|
380
801
|
};
|
|
802
|
+
/**
|
|
803
|
+
* The served model catalog (gateway C4b): the SAME list `GET
|
|
804
|
+
* /v1/models` serves (picker entries — no provider internals).
|
|
805
|
+
*/
|
|
806
|
+
readonly models: {
|
|
807
|
+
list(): Promise<readonly ModelEntry[]>;
|
|
808
|
+
};
|
|
809
|
+
/**
|
|
810
|
+
* The slash-command surface (gateway C4b): the merged catalog for
|
|
811
|
+
* listing, and the invoke route's run for driving an extension
|
|
812
|
+
* command in-process (chat `/command` handling).
|
|
813
|
+
*/
|
|
814
|
+
readonly commands: {
|
|
815
|
+
/** The merged catalog rows (core table + extension entries). */
|
|
816
|
+
list(): Promise<readonly SlashCommand[]>;
|
|
817
|
+
/**
|
|
818
|
+
* Run one EXTENSION command (the invoke route's resolution): an
|
|
819
|
+
* unknown/core name throws `command_not_found: <name>`; `opts.user`
|
|
820
|
+
* must name a directory user (absent → `unknown user "<user>"`) and
|
|
821
|
+
* becomes the run's principal with EFFECTIVE admin (the stored flag
|
|
822
|
+
* OR the persisted group grant) and `method: "host"`. A throwing
|
|
823
|
+
* run propagates (no HTTP 500 mapping — the caller is in-process).
|
|
824
|
+
*/
|
|
825
|
+
run(conversationId: ConversationId, name: string, args: string, opts: {
|
|
826
|
+
readonly user: string;
|
|
827
|
+
}): Promise<{
|
|
828
|
+
readonly notice?: string;
|
|
829
|
+
}>;
|
|
830
|
+
};
|
|
381
831
|
}
|
|
382
832
|
/**
|
|
383
833
|
* The documented factory-result contract: EVERY field an extension may
|
|
384
834
|
* return from its default-export factory. Every field is optional —
|
|
385
835
|
* a minimal extension returns only `extension`; a contributor-only
|
|
386
|
-
* result (`providers`/`webuiBundles` without `extension`)
|
|
387
|
-
* itself by `name`.
|
|
836
|
+
* result (`providers`/`webuiBundles`/`channels` without `extension`)
|
|
837
|
+
* identifies itself by `name`.
|
|
388
838
|
*
|
|
389
839
|
* `routes`, `commands`, `skills`, and `notifiers` are declared here
|
|
390
840
|
* ahead of their implementations (W5 Tasks 2, 3, 4, and 6); the loader
|
|
@@ -419,6 +869,23 @@ export interface SigrunExtensionResult extends SigrunLifecycleHandlers {
|
|
|
419
869
|
readonly skills?: readonly string[];
|
|
420
870
|
/** Notification transports (lands in Task 6). */
|
|
421
871
|
readonly notifiers?: readonly NotifierContribution[];
|
|
872
|
+
/**
|
|
873
|
+
* Channel adapters (gateway C1, spec §7): one per chat platform. A
|
|
874
|
+
* `channels`-only result is a valid contributor identified by `name`;
|
|
875
|
+
* channel-id collisions across extensions quarantine the later
|
|
876
|
+
* loader (chat bindings are keyed by channel id — never last-wins).
|
|
877
|
+
*/
|
|
878
|
+
readonly channels?: readonly ChannelContribution[];
|
|
879
|
+
/**
|
|
880
|
+
* The gateway engine (gateway C2, spec §8.0): ONE per boot — a second
|
|
881
|
+
* declarer quarantines at load naming the first. Always carried on a
|
|
882
|
+
* result WITH an `extension` (an engine owns pi-durable surface); a
|
|
883
|
+
* contributor-only `gateway` result is not a contract form. Report
|
|
884
|
+
* health through `gateway.health()`, never the `health` field.
|
|
885
|
+
*/
|
|
886
|
+
readonly gateway?: GatewayEngine;
|
|
887
|
+
/** Declared surfaces — views and panels (UI-blocks U3, spec §7.1; the service lands in Task 2). */
|
|
888
|
+
readonly surfaces?: SurfacesContribution;
|
|
422
889
|
}
|
|
423
890
|
/**
|
|
424
891
|
* The extension factory: sigrun calls it at load, passing the host
|
package/dist/harness/boot.js
CHANGED
|
@@ -277,7 +277,7 @@ export async function openHarness(config, options = {}) {
|
|
|
277
277
|
const sharedStores = sharedExtensionStores(roots, env);
|
|
278
278
|
const seamHosts = new Map();
|
|
279
279
|
const seamKinds = new Map();
|
|
280
|
-
const { outcomes: extensionOutcomes, healthProducers: extensionHealth, runSettledBindings, lifecycleBindings, workerBindings, providerContributors, webuiBundles: extensionWebuiBundles, skillRoots: extensionSkillRoots, jobBindings, routeBindings, commandBindings, notifierBindings, } = await loadExtensions(loaderInputs, registry, {
|
|
280
|
+
const { outcomes: extensionOutcomes, healthProducers: extensionHealth, runSettledBindings, lifecycleBindings, workerBindings, providerContributors, webuiBundles: extensionWebuiBundles, skillRoots: extensionSkillRoots, jobBindings, routeBindings, commandBindings, notifierBindings, channelBindings, gatewayEngine, surfaces: surfaceBindings, } = await loadExtensions(loaderInputs, registry, {
|
|
281
281
|
packagesDir,
|
|
282
282
|
hostContextFactory: (extensionName, kinds, priorHost) => {
|
|
283
283
|
// Two-phase, revised for the provisional identity (provider
|
|
@@ -430,6 +430,10 @@ export async function openHarness(config, options = {}) {
|
|
|
430
430
|
// `typed` entry — there is no user turn to attribute).
|
|
431
431
|
commandExpansion,
|
|
432
432
|
envBuilder: undefined,
|
|
433
|
+
// Gateway C7: a deliver-carrying fire warns once per run when no
|
|
434
|
+
// engine loaded (core ships none — `gatewayEngine` is undefined
|
|
435
|
+
// unless an extension declared one).
|
|
436
|
+
gatewayLoaded: gatewayEngine !== undefined,
|
|
433
437
|
};
|
|
434
438
|
registry.install({
|
|
435
439
|
name: "sigrun.jobs",
|
|
@@ -850,7 +854,10 @@ export async function openHarness(config, options = {}) {
|
|
|
850
854
|
},
|
|
851
855
|
extensionRoutes: routeBindings,
|
|
852
856
|
extensionCommands: commandBindings,
|
|
857
|
+
extensionSurfaces: surfaceBindings,
|
|
853
858
|
notifierBindings,
|
|
859
|
+
channelBindings,
|
|
860
|
+
gatewayEngine,
|
|
854
861
|
skillRoots: extensionSkillRoots,
|
|
855
862
|
commandExpansion,
|
|
856
863
|
eventBus,
|
|
@@ -648,6 +648,32 @@ export class EventBus {
|
|
|
648
648
|
callId: record.callId,
|
|
649
649
|
kind: record.kind,
|
|
650
650
|
});
|
|
651
|
+
// Gateway C6: a TERMINAL status transition additionally fans
|
|
652
|
+
// out to the extension lifecycle handlers
|
|
653
|
+
// (`interactionSettled`) from this same commit-observation
|
|
654
|
+
// site. Exactly one document change lands per transition —
|
|
655
|
+
// whatever wrote it (HTTP-route answer, host answer, host
|
|
656
|
+
// settleInteraction) — so exactly one event fires per
|
|
657
|
+
// transition; seeding (pending) fires nothing. It rides the
|
|
658
|
+
// same lifecycle queue + deadline-bounded dispatch as
|
|
659
|
+
// conversationCreated etc., NOT the host event stream. The
|
|
660
|
+
// host-event split above and this gate read the SAME
|
|
661
|
+
// imported set, so a future terminal status added to
|
|
662
|
+
// TERMINAL fires both events together by construction.
|
|
663
|
+
if (INTERACTION_TERMINAL.has(record.status)) {
|
|
664
|
+
lifecycle.push({
|
|
665
|
+
type: "interactionSettled",
|
|
666
|
+
conversationId: change.conversationId,
|
|
667
|
+
callId: record.callId,
|
|
668
|
+
kind: record.kind,
|
|
669
|
+
// SAFETY: `record.status` is a narrowed `string`, but
|
|
670
|
+
// the payload wants the terminal union — sound because
|
|
671
|
+
// the union and the TERMINAL set share one definition
|
|
672
|
+
// (src/harness/interactions.ts): membership in the set
|
|
673
|
+
// IS membership in the union.
|
|
674
|
+
status: record.status,
|
|
675
|
+
});
|
|
676
|
+
}
|
|
651
677
|
}
|
|
652
678
|
}
|
|
653
679
|
else if (change.type === "document" &&
|
|
@@ -1010,6 +1036,13 @@ export class EventBus {
|
|
|
1010
1036
|
return { conversationId: event.conversationId, archived: event.archived };
|
|
1011
1037
|
case "conversationIdle":
|
|
1012
1038
|
return { conversationId: event.conversationId, idleMs: event.idleMs };
|
|
1039
|
+
case "interactionSettled":
|
|
1040
|
+
return {
|
|
1041
|
+
conversationId: event.conversationId,
|
|
1042
|
+
callId: event.callId,
|
|
1043
|
+
kind: event.kind,
|
|
1044
|
+
status: event.status,
|
|
1045
|
+
};
|
|
1013
1046
|
}
|
|
1014
1047
|
}
|
|
1015
1048
|
/** The conversation a lifecycle event's api scopes to (the fork child on forks). */
|
|
@@ -62,8 +62,9 @@ export class UserDirectory {
|
|
|
62
62
|
}
|
|
63
63
|
/**
|
|
64
64
|
* Patch one user's mutable fields (`name`/`email`/`admin` — the
|
|
65
|
-
* username itself is immutable
|
|
66
|
-
*
|
|
65
|
+
* username itself is immutable; `groupAdmin` is the pipeline's own
|
|
66
|
+
* write-through field, never the PATCH route's surface). Absent user
|
|
67
|
+
* throws; the file is rewritten atomically.
|
|
67
68
|
*/
|
|
68
69
|
update(user, patch) {
|
|
69
70
|
return this.mutations.run(async () => {
|