@sigrun/sigrun 0.1.6 → 0.2.1
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 +192 -0
- package/README.md +445 -42
- package/dist/blocks/build-stamp.json +13 -0
- package/dist/blocks/builders.d.ts +57 -0
- package/dist/blocks/images.d.ts +26 -0
- package/dist/blocks/index.d.ts +20 -0
- package/dist/blocks/limits.d.ts +25 -0
- package/dist/blocks/placements.d.ts +26 -0
- package/dist/blocks/schema.d.ts +38 -0
- package/dist/blocks/schema.js +9 -2
- package/dist/blocks/validate.d.ts +73 -0
- package/dist/blocks/validate.js +57 -8
- package/dist/cli.js +26 -2
- package/dist/config.js +67 -1
- package/dist/extension.d.ts +230 -22
- package/dist/harness/ambient.js +39 -185
- package/dist/harness/blocks/entries.js +47 -0
- package/dist/harness/blocks/state.js +58 -0
- package/dist/harness/blocks/tools.js +274 -0
- package/dist/harness/boot.js +259 -20
- package/dist/harness/builtins.js +15 -11
- package/dist/harness/event-bus.js +105 -10
- package/dist/harness/identity/accounts.js +186 -0
- package/dist/harness/identity/groups.js +14 -0
- package/dist/harness/identity/index.js +13 -6
- package/dist/harness/identity/json-store.js +15 -4
- package/dist/harness/identity/setup-code-check.js +9 -0
- package/dist/harness/identity/tokens.js +28 -5
- package/dist/harness/identity/users.js +111 -12
- package/dist/harness/interactions.js +292 -136
- package/dist/harness/loader.js +271 -17
- package/dist/harness/packages.js +1 -1
- package/dist/harness/reserved-sections.js +21 -0
- package/dist/harness/seam-host.js +92 -6
- package/dist/harness/sections/context-files.js +1 -1
- package/dist/harness/settings.js +63 -2
- package/dist/harness/stores.js +241 -4
- package/dist/harness/user-services.js +391 -0
- package/dist/harness/user-tools.js +187 -0
- package/dist/harness/workspace.js +61 -57
- package/dist/jobs/fire.js +10 -16
- package/dist/sandbox/availability.js +211 -0
- package/dist/sandbox/command-observer.js +59 -0
- package/dist/sandbox/grants.js +159 -0
- package/dist/sandbox/helper.js +29 -0
- package/dist/sandbox/isolation.js +29 -0
- package/dist/sandbox/launch.js +31 -0
- package/dist/sandbox/os-account.js +49 -0
- package/dist/sandbox/policy.js +274 -0
- package/dist/sandbox/pool.js +548 -0
- package/dist/sandbox/protocol.js +167 -0
- package/dist/sandbox/sandboxed-env.js +245 -0
- package/dist/sandbox/unavailable-env.js +49 -0
- package/dist/sandbox/worker.js +261 -0
- package/dist/serve.js +213 -25
- package/dist/server/auth/access-principal.js +16 -0
- package/dist/server/auth/pipeline.js +99 -36
- package/dist/server/blocks/answers.js +94 -0
- package/dist/server/blocks/chat-surfaces.js +393 -0
- package/dist/server/blocks/notes.js +216 -0
- package/dist/server/blocks/summaries.js +328 -0
- package/dist/server/commands.js +5 -0
- package/dist/server/content-parts-lazy.js +13 -0
- package/dist/server/conversation-handler.js +2 -5
- package/dist/server/conversation-ops.js +138 -37
- package/dist/server/helpers.js +18 -6
- package/dist/server/host-bridge.js +40 -20
- package/dist/server/images/cache.js +213 -0
- package/dist/server/images/convert.js +13 -0
- package/dist/server/images/fetch.js +302 -0
- package/dist/server/images/ranges.js +42 -0
- package/dist/server/images/rewrite.js +94 -0
- package/dist/server/images/sign.js +84 -0
- package/dist/server/images/sniff.js +62 -0
- package/dist/server/index.js +152 -4
- package/dist/server/login/flows.js +126 -0
- package/dist/server/login/http.js +31 -0
- package/dist/server/login/resolve.js +133 -0
- package/dist/server/login/setup-claim.js +55 -0
- package/dist/server/login/usernames.js +15 -0
- package/dist/server/notifications.js +21 -4
- package/dist/server/ownership.js +13 -0
- package/dist/server/routes/admin.js +58 -0
- package/dist/server/routes/auth.js +88 -2
- package/dist/server/routes/conversations.js +3 -6
- package/dist/server/routes/events.js +28 -3
- package/dist/server/routes/extensions.js +37 -5
- package/dist/server/routes/images.js +274 -0
- package/dist/server/routes/interactions.js +57 -68
- package/dist/server/routes/jobs.js +2 -8
- package/dist/server/routes/login.js +223 -0
- package/dist/server/routes/me.js +90 -0
- package/dist/server/routes/meta.js +19 -0
- package/dist/server/routes/onboarding.js +94 -84
- package/dist/server/routes/settings-sections.js +190 -0
- package/dist/server/routes/settings.js +16 -4
- package/dist/server/routes/submissions.js +1 -1
- package/dist/server/routes/surfaces.js +175 -23
- package/dist/server/routes/users.js +116 -20
- package/dist/server/schema-fixture.js +213 -0
- package/dist/server/session-meta.js +3 -0
- package/dist/server/shutdown.js +11 -4
- package/dist/server/static.js +18 -0
- package/dist/server/surfaces/dedup.js +9 -2
- package/dist/server/surfaces/service.js +181 -13
- package/dist/server/surfaces/settings-surfaces.js +597 -0
- package/dist/server/turns/submit-turn.js +26 -0
- package/dist/server/views.js +16 -0
- package/dist/settings/core-sections.js +532 -0
- package/dist/settings/pending.js +181 -0
- package/dist/settings/registry.js +453 -0
- package/dist/skills/index.js +6 -0
- package/node_modules/@sigrun/shared/CHANGELOG.md +11 -0
- package/node_modules/@sigrun/shared/dist/access.d.ts +4 -2
- package/node_modules/@sigrun/shared/dist/access.js +11 -4
- package/node_modules/@sigrun/shared/dist/ambient.d.ts +17 -74
- package/node_modules/@sigrun/shared/dist/ambient.js +8 -6
- package/node_modules/@sigrun/shared/dist/blocks/text.js +16 -2
- package/node_modules/@sigrun/shared/dist/blocks/transport.d.ts +4 -0
- package/node_modules/@sigrun/shared/dist/blocks/types.d.ts +21 -4
- package/node_modules/@sigrun/shared/dist/interactions.d.ts +22 -48
- package/node_modules/@sigrun/shared/dist/interactions.js +9 -7
- package/node_modules/@sigrun/shared/dist/settings.d.ts +27 -2
- package/node_modules/@sigrun/shared/dist/themes.d.ts +2 -3
- package/node_modules/@sigrun/shared/dist/themes.js +2 -3
- package/node_modules/@sigrun/shared/dist/turns.d.ts +14 -0
- package/node_modules/@sigrun/shared/dist/turns.js +53 -3
- package/node_modules/@sigrun/shared/dist/views.d.ts +202 -12
- package/node_modules/@sigrun/shared/dist/views.js +16 -0
- package/node_modules/@sigrun/shared/package.json +1 -1
- package/package.json +13 -9
- package/webui/dist/assets/index-SO2W4jjk.js +55 -0
- package/webui/dist/assets/index-yqkL6BSS.css +1 -0
- package/webui/dist/index.html +2 -2
- package/CONTEXT.md +0 -163
- package/webui/dist/assets/index-CiYInlLx.css +0 -1
- package/webui/dist/assets/index-JDfOQV9z.js +0 -52
package/dist/extension.d.ts
CHANGED
|
@@ -35,12 +35,12 @@
|
|
|
35
35
|
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
|
-
import type {
|
|
38
|
+
import type { AmbientPlace, AmbientRecord } from "@sigrun/shared/ambient";
|
|
39
39
|
import type { Action, DataOps, IconName, Tree } from "@sigrun/shared/blocks";
|
|
40
40
|
import type { InteractionAnswer, InteractionRecord } from "@sigrun/shared/interactions";
|
|
41
41
|
import type { JobSpec } from "@sigrun/shared/jobs";
|
|
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, };
|
|
42
|
+
import type { AccountLinkWire, ConversationSummary, ConversationViewWire, HealthStatus, ModelEntry, SessionMeta, SlashCommand, SubmissionRecordWire, ThinkingLevelWire, UserWire as DirectoryUserWire } from "@sigrun/shared/views";
|
|
43
|
+
export type { Access, AccountLinkWire, AmbientRecord, DirectoryUserWire, InteractionRecord, JobSpec, ModelEntry, Sharing, SlashCommand, };
|
|
44
44
|
type JsonParams = JsonObject;
|
|
45
45
|
type JsonParamValue = JsonParams[string];
|
|
46
46
|
/**
|
|
@@ -63,6 +63,8 @@ export interface HandlerContext {
|
|
|
63
63
|
*/
|
|
64
64
|
export type RunSettledEvent = {
|
|
65
65
|
readonly conversationId: ConversationId;
|
|
66
|
+
/** The conversation owner at delivery; absent when the conversation is ownerless. */
|
|
67
|
+
readonly owner?: string;
|
|
66
68
|
readonly inputs: readonly SubmissionId[];
|
|
67
69
|
/** Text blocks of each input's entry (`pi.user`, or an expanded input's
|
|
68
70
|
* `sigrun.user.expanded` — the ADMITTED text), concatenated;
|
|
@@ -100,19 +102,24 @@ export type MemoryEventApi = {
|
|
|
100
102
|
export type ExtensionLifecyclePayload = {
|
|
101
103
|
conversationCreated: {
|
|
102
104
|
readonly conversationId: ConversationId;
|
|
105
|
+
readonly owner?: string;
|
|
103
106
|
};
|
|
104
107
|
conversationForked: {
|
|
105
108
|
readonly childId: ConversationId;
|
|
106
109
|
readonly parentId: ConversationId;
|
|
107
110
|
readonly at: EntryId;
|
|
111
|
+
/** The CHILD's owner at delivery; absent when the child is ownerless. */
|
|
112
|
+
readonly owner?: string;
|
|
108
113
|
};
|
|
109
114
|
conversationArchived: {
|
|
110
115
|
readonly conversationId: ConversationId;
|
|
111
116
|
readonly archived: boolean;
|
|
117
|
+
readonly owner?: string;
|
|
112
118
|
};
|
|
113
119
|
conversationIdle: {
|
|
114
120
|
readonly conversationId: ConversationId;
|
|
115
121
|
readonly idleMs: number;
|
|
122
|
+
readonly owner?: string;
|
|
116
123
|
};
|
|
117
124
|
/** Gateway C6: one terminal interaction transition — the payload mirrors
|
|
118
125
|
* the record's terminal envelope fields. */
|
|
@@ -121,6 +128,11 @@ export type ExtensionLifecyclePayload = {
|
|
|
121
128
|
readonly callId: string;
|
|
122
129
|
readonly kind: string;
|
|
123
130
|
readonly status: "answered" | "skipped" | "expired" | "cancelled";
|
|
131
|
+
/** The answering user (C5 attribution), when a human answered. */
|
|
132
|
+
readonly answeredByUser?: string;
|
|
133
|
+
/** The STORED (masked-by-construction) answer values — render-safe. */
|
|
134
|
+
readonly answer?: Readonly<Record<string, unknown>>;
|
|
135
|
+
readonly owner?: string;
|
|
124
136
|
};
|
|
125
137
|
};
|
|
126
138
|
/** A factory-result `runSettled` handler: called once per settled run, deadline-bounded. */
|
|
@@ -151,6 +163,17 @@ export type SigrunLifecycleHandlers = {
|
|
|
151
163
|
readonly harnessShutdown?: ExtensionHarnessShutdownHandler;
|
|
152
164
|
/** The user-removal transfer event; direct fan-out, not commit-derived. */
|
|
153
165
|
readonly userDeleted?: ExtensionUserDeletedHandler;
|
|
166
|
+
/**
|
|
167
|
+
* UI-blocks U4 (spec §13.2): called after a settings write COMMITS —
|
|
168
|
+
* the keys are the flattened paths the write touched (core keys in
|
|
169
|
+
* the §13.1 dotted vocabulary, this extension's own top-level keys
|
|
170
|
+
* for a `settings/<slug>.json` write). Fired for BOTH scopes: a
|
|
171
|
+
* server-scope write notifies every loaded extension; an extension's
|
|
172
|
+
* own write notifies it (and every other extension — settings are
|
|
173
|
+
* shared operator surface). Errors are caught and logged; the write
|
|
174
|
+
* already succeeded and stays successful.
|
|
175
|
+
*/
|
|
176
|
+
onSettingsChanged?(keys: readonly string[], user?: string): void | Promise<void>;
|
|
154
177
|
};
|
|
155
178
|
/**
|
|
156
179
|
* The lifecycle event names a worker's `wakeOn` may declare.
|
|
@@ -182,6 +205,31 @@ export interface ExtensionWorker {
|
|
|
182
205
|
/** The work; rejects (or overruns the worker deadline) to fail the run. */
|
|
183
206
|
run(context: HandlerContext, wake?: WorkerWake): Promise<void>;
|
|
184
207
|
}
|
|
208
|
+
/** The command and extra environment/read rights for one per-user process. */
|
|
209
|
+
export interface ServiceSpec {
|
|
210
|
+
readonly command: string;
|
|
211
|
+
readonly args: string[];
|
|
212
|
+
readonly env?: Record<string, string>;
|
|
213
|
+
readonly cwd?: string;
|
|
214
|
+
readonly readPaths?: string[];
|
|
215
|
+
}
|
|
216
|
+
/** One factory-declared long-lived per-user service. */
|
|
217
|
+
export interface UserService {
|
|
218
|
+
readonly name: string;
|
|
219
|
+
start(user: string, ctx: HandlerContext): Promise<ServiceSpec | undefined>;
|
|
220
|
+
readonly idleMinutes?: number;
|
|
221
|
+
}
|
|
222
|
+
/** A shared-stdio attachment to a running per-user service. */
|
|
223
|
+
export interface ServiceHandle {
|
|
224
|
+
readonly stdin: WritableStream<Uint8Array>;
|
|
225
|
+
readonly stdout: ReadableStream<Uint8Array>;
|
|
226
|
+
readonly stderr: ReadableStream<Uint8Array>;
|
|
227
|
+
readonly exited: Promise<{
|
|
228
|
+
code: number | null;
|
|
229
|
+
signal: string | null;
|
|
230
|
+
}>;
|
|
231
|
+
release(): void;
|
|
232
|
+
}
|
|
185
233
|
/** Three-state per-provider readiness: not configured, degraded, or ready. */
|
|
186
234
|
export type ProviderReadinessStatus = "not_configured" | "degraded" | "ready";
|
|
187
235
|
/** One readiness report: the status plus optional operator-facing detail. */
|
|
@@ -207,6 +255,42 @@ export interface ProviderContribution {
|
|
|
207
255
|
/** The per-provider readiness tracker (`/v1/auth/check` reports it verbatim). */
|
|
208
256
|
readonly readiness?: ReadinessTracker;
|
|
209
257
|
}
|
|
258
|
+
/** Core-owned state for one external login, with provider-private scratch data. */
|
|
259
|
+
export type LoginFlow = {
|
|
260
|
+
state: string;
|
|
261
|
+
callbackUrl: string;
|
|
262
|
+
data: Record<string, string>;
|
|
263
|
+
};
|
|
264
|
+
/** The identity proof a login provider returns after its callback. */
|
|
265
|
+
export type LoginResult = {
|
|
266
|
+
subject: string;
|
|
267
|
+
profile: {
|
|
268
|
+
username?: string;
|
|
269
|
+
name?: string;
|
|
270
|
+
email?: string;
|
|
271
|
+
emailVerified?: boolean;
|
|
272
|
+
};
|
|
273
|
+
groups?: string[];
|
|
274
|
+
};
|
|
275
|
+
/** One extension identity provider; core owns the flow, state and callback. */
|
|
276
|
+
export interface LoginProviderContribution {
|
|
277
|
+
readonly id: string;
|
|
278
|
+
readonly label: string;
|
|
279
|
+
readonly icon?: string;
|
|
280
|
+
/** Provider-declared provisioning policy, enforced by core. */
|
|
281
|
+
readonly policy?: {
|
|
282
|
+
readonly autoProvision?: boolean;
|
|
283
|
+
readonly allowedGroups?: string[];
|
|
284
|
+
readonly allowedDomains?: string[];
|
|
285
|
+
};
|
|
286
|
+
begin(flow: LoginFlow, ctx: HandlerContext): Promise<{
|
|
287
|
+
redirectUrl: string;
|
|
288
|
+
}>;
|
|
289
|
+
complete(flow: LoginFlow, callback: {
|
|
290
|
+
query: Record<string, string>;
|
|
291
|
+
body?: unknown;
|
|
292
|
+
}, ctx: HandlerContext): Promise<LoginResult>;
|
|
293
|
+
}
|
|
210
294
|
/** One contributed webui bundle: a selection name plus the bundle root directory. */
|
|
211
295
|
export interface WebuiBundleContribution {
|
|
212
296
|
/** The selection key (`server.webui.bundle`); must match `^[a-z][a-z0-9_-]*$`. */
|
|
@@ -231,7 +315,9 @@ export interface Principal {
|
|
|
231
315
|
readonly admin: boolean;
|
|
232
316
|
/** True for the `SIGRUN_API_TOKEN` service principal. */
|
|
233
317
|
readonly service: boolean;
|
|
234
|
-
readonly method: "none" | "token" | "proxy" | "host";
|
|
318
|
+
readonly method: "none" | "token" | "session" | "proxy" | "host";
|
|
319
|
+
/** The login provider that issued a session principal. */
|
|
320
|
+
readonly provider?: string;
|
|
235
321
|
}
|
|
236
322
|
/** One extension HTTP route, mounted at `/v1/ext/<slug>/<path>`. */
|
|
237
323
|
export interface ExtensionRouteContribution {
|
|
@@ -263,14 +349,18 @@ export interface ExtensionCommandContribution {
|
|
|
263
349
|
readonly notice?: string;
|
|
264
350
|
} | void>;
|
|
265
351
|
}
|
|
266
|
-
/**
|
|
267
|
-
|
|
268
|
-
|
|
352
|
+
/** The settled-run outcome: the notification-safe projection of the host
|
|
353
|
+
* event's `outcome`/`reason` (status and reason only — no answer text). */
|
|
354
|
+
export type RunSettledOutcome = {
|
|
355
|
+
readonly status: "answered";
|
|
356
|
+
} | {
|
|
357
|
+
readonly status: "unanswered";
|
|
358
|
+
readonly reason: string;
|
|
359
|
+
};
|
|
360
|
+
/** Fields shared by every notification variant. */
|
|
361
|
+
interface NotificationEventBase {
|
|
269
362
|
readonly conversationId?: number;
|
|
270
363
|
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;
|
|
274
364
|
/** The recipient. */
|
|
275
365
|
readonly user: string | undefined;
|
|
276
366
|
readonly title: string;
|
|
@@ -278,6 +368,21 @@ export interface NotificationEvent {
|
|
|
278
368
|
/** `${server.publicUrl}/#/c/<conversationId>` when `server.publicUrl` is set; absent otherwise. */
|
|
279
369
|
readonly url?: string;
|
|
280
370
|
}
|
|
371
|
+
/** One notification core delivers to every registered notifier (best-effort). */
|
|
372
|
+
export type NotificationEvent = NotificationEventBase & ({
|
|
373
|
+
readonly type: "interaction.pending";
|
|
374
|
+
/** Gateway C6: the pending record's id — set on `interaction.pending` only
|
|
375
|
+
* (inspect it via `host.peekInteraction`; the OWNING extension's
|
|
376
|
+
* delivery read is `host.snapshotInteraction`). */
|
|
377
|
+
readonly callId?: string;
|
|
378
|
+
} | {
|
|
379
|
+
readonly type: "job.failed";
|
|
380
|
+
} | {
|
|
381
|
+
readonly type: "run.settled";
|
|
382
|
+
/** Whether the run answered or not (model error, abort, …), with
|
|
383
|
+
* the wire reason verbatim. Never infer success from the body. */
|
|
384
|
+
readonly outcome: RunSettledOutcome;
|
|
385
|
+
});
|
|
281
386
|
/** One notifier transport (ntfy, Signal, web push — core ships none). */
|
|
282
387
|
export interface NotifierContribution {
|
|
283
388
|
readonly name: string;
|
|
@@ -564,10 +669,12 @@ export type ActionResult = {
|
|
|
564
669
|
};
|
|
565
670
|
/**
|
|
566
671
|
* One declared surface (spec §7.1, verbatim shape): the placement is
|
|
567
|
-
* the generic parameter — `"view"` or `"
|
|
568
|
-
*
|
|
672
|
+
* the generic parameter — `"view"`, `"panel"`, or `"settings"` (the
|
|
673
|
+
* §13.3 settings sections adapt to settings-placement surfaces; the
|
|
674
|
+
* host builds those — extensions declare `SettingsSectionDef`s, never
|
|
675
|
+
* `SurfaceDef<"settings">` directly).
|
|
569
676
|
*/
|
|
570
|
-
export interface SurfaceDef<P extends "view" | "panel"> {
|
|
677
|
+
export interface SurfaceDef<P extends "view" | "panel" | "settings"> {
|
|
571
678
|
/** Unique within the extension. */
|
|
572
679
|
readonly id: string;
|
|
573
680
|
readonly title: string;
|
|
@@ -577,22 +684,51 @@ export interface SurfaceDef<P extends "view" | "panel"> {
|
|
|
577
684
|
readonly sharedData?: boolean;
|
|
578
685
|
/** Channels refetch data on this interval while visible (min 5000). */
|
|
579
686
|
readonly refreshMs?: number;
|
|
580
|
-
/** §7.3; default: canonical JSON of params.
|
|
581
|
-
|
|
687
|
+
/** §7.3; default: canonical JSON of params. The principal argument
|
|
688
|
+
* lets principal-owned surfaces isolate their revision/action identity. */
|
|
689
|
+
instanceKey?(params: JsonParams, principal: Principal): string;
|
|
582
690
|
render(ctx: SurfaceContext): Promise<SurfaceRenderResult>;
|
|
583
691
|
data?(ctx: SurfaceContext): Promise<SurfaceDataResult>;
|
|
584
692
|
readonly actions?: Readonly<Record<string, (ctx: SurfaceContext, payload: JsonParamValue) => Promise<ActionResult>>>;
|
|
585
693
|
}
|
|
586
694
|
/**
|
|
587
|
-
*
|
|
588
|
-
*
|
|
589
|
-
*
|
|
695
|
+
* One declared settings section (spec §13.3, verbatim shape): a static
|
|
696
|
+
* `settings`-placement form bound to the extension's settings object
|
|
697
|
+
* (`settings/<slug>.json`). The host adapts it to a surface under the
|
|
698
|
+
* extension's own `/v1/ext/<slug>/surfaces/` mount — core's own
|
|
699
|
+
* sections use the same renderer, under the reserved `core` slug.
|
|
700
|
+
*/
|
|
701
|
+
export interface SettingsSectionDef {
|
|
702
|
+
/** Unique within the extension. */
|
|
703
|
+
readonly id: string;
|
|
704
|
+
readonly title: string;
|
|
705
|
+
readonly icon?: IconName;
|
|
706
|
+
/** A root `form` block; the placement is `settings` (spec §5.7). */
|
|
707
|
+
readonly form: Tree;
|
|
708
|
+
/** Declared secrets, rendered as `secret` fields backed by the §7.7 secrets routes. */
|
|
709
|
+
readonly secrets?: readonly {
|
|
710
|
+
key: string;
|
|
711
|
+
label: string;
|
|
712
|
+
help?: string;
|
|
713
|
+
}[];
|
|
714
|
+
/** Only admins see and edit it; default true for shared sections.
|
|
715
|
+
* A user-scope section is visible to every signed-in user unless this
|
|
716
|
+
* is explicitly true. */
|
|
717
|
+
readonly admin?: boolean;
|
|
718
|
+
/** `"user"` reads/writes the calling user's settings; absent/`"server"`
|
|
719
|
+
* keeps the extension's shared settings file. */
|
|
720
|
+
readonly scope?: "server" | "user";
|
|
721
|
+
}
|
|
722
|
+
/**
|
|
723
|
+
* The `surfaces` field of a factory result (spec §7.1): declared views,
|
|
724
|
+
* panels, and settings sections. Ids are unique across ALL THREE lists
|
|
725
|
+
* (one namespace — the loader enforces it).
|
|
590
726
|
*/
|
|
591
727
|
export interface SurfacesContribution {
|
|
592
728
|
readonly views?: readonly SurfaceDef<"view">[];
|
|
593
729
|
readonly panels?: readonly SurfaceDef<"panel">[];
|
|
594
|
-
/** §13.3 settings sections
|
|
595
|
-
readonly settings?: readonly
|
|
730
|
+
/** §13.3 settings sections (UI-blocks U4 Task 3). */
|
|
731
|
+
readonly settings?: readonly SettingsSectionDef[];
|
|
596
732
|
}
|
|
597
733
|
/** One wire part of a `host.conversations.submit` input array. */
|
|
598
734
|
export type SubmissionInputPart = {
|
|
@@ -620,8 +756,38 @@ export type WhenBusyMode = NonNullable<InputSubmissionDraft["whenBusy"]>;
|
|
|
620
756
|
* factory body.
|
|
621
757
|
*/
|
|
622
758
|
export interface SigrunHost {
|
|
759
|
+
/**
|
|
760
|
+
* Publish tools for ONE user (multi-user M2). Only factory results that
|
|
761
|
+
* declared `perUserTools: true` may call this; other extensions throw
|
|
762
|
+
* `perUserTools not declared`. An empty list uninstalls the user's entry.
|
|
763
|
+
* Core wraps every tool with a committed-owner check at execute time.
|
|
764
|
+
*/
|
|
765
|
+
readonly tools: {
|
|
766
|
+
setUserTools(user: string, tools: readonly import("@earendil-works/pi-durable").ToolRegistration[]): void;
|
|
767
|
+
};
|
|
768
|
+
/** Connect to one declared per-user service; core owns its process lifecycle. */
|
|
769
|
+
readonly services: {
|
|
770
|
+
connect(name: string, user: string, ctx: HandlerContext): Promise<ServiceHandle>;
|
|
771
|
+
};
|
|
623
772
|
/** The scoped extension-stores handle (your secrets/settings sections + raw data/cache/state paths). */
|
|
624
773
|
readonly stores: {
|
|
774
|
+
/** One directory user's secrets/settings/paths, still scoped to your slug. */
|
|
775
|
+
user(user: string): {
|
|
776
|
+
readonly secrets: {
|
|
777
|
+
get(key: string): Promise<string | undefined>;
|
|
778
|
+
set(key: string, value: string): Promise<void>;
|
|
779
|
+
delete(key: string): Promise<void>;
|
|
780
|
+
};
|
|
781
|
+
readonly settings: {
|
|
782
|
+
get(): Promise<Record<string, unknown>>;
|
|
783
|
+
set(value: Record<string, unknown>): Promise<void>;
|
|
784
|
+
};
|
|
785
|
+
readonly paths: {
|
|
786
|
+
readonly data: string;
|
|
787
|
+
readonly state: string;
|
|
788
|
+
readonly cache: string;
|
|
789
|
+
};
|
|
790
|
+
};
|
|
625
791
|
/** Your section of secrets.json. */
|
|
626
792
|
readonly secrets: {
|
|
627
793
|
get(key: string): Promise<string | undefined>;
|
|
@@ -653,7 +819,22 @@ export interface SigrunHost {
|
|
|
653
819
|
}): Promise<boolean>;
|
|
654
820
|
/** Mark a pending interaction expired/cancelled/skipped (extension-owned semantics). */
|
|
655
821
|
settleInteraction(conversationId: ConversationId, callId: string, status: "skipped" | "expired" | "cancelled", context: HandlerContext): Promise<boolean>;
|
|
656
|
-
/**
|
|
822
|
+
/**
|
|
823
|
+
* Inspect one interaction record WITHOUT consuming anything: returns
|
|
824
|
+
* the same stored projection as {@link SigrunHost.snapshotInteraction}
|
|
825
|
+
* (secret values stay `SECRET_MASK`), but never touches the one-shot
|
|
826
|
+
* secret-delivery `take` path. Safe to repeat; neither extends nor
|
|
827
|
+
* shortens secret TTL; creates, settles, and mutates nothing. Use for
|
|
828
|
+
* inspection/rendering (notifiers, outcome edits, correlation).
|
|
829
|
+
*/
|
|
830
|
+
peekInteraction(conversationId: ConversationId, callId: string, context: HandlerContext): Promise<Readonly<InteractionRecord> | undefined>;
|
|
831
|
+
/**
|
|
832
|
+
* The OWNING extension's delivery read: the first applicable read of
|
|
833
|
+
* an answered secret record merges its raw secret values back ONCE;
|
|
834
|
+
* subsequent reads see the stored mask. Inspection-only callers must
|
|
835
|
+
* use {@link SigrunHost.peekInteraction} instead — a consuming read
|
|
836
|
+
* by a non-owner silently destroys the owner's one-shot delivery.
|
|
837
|
+
*/
|
|
657
838
|
snapshotInteraction(conversationId: ConversationId, callId: string, context: HandlerContext): Promise<Readonly<InteractionRecord> | undefined>;
|
|
658
839
|
/** Fully replace one ambient surface's state (write = replacement); the key must be unowned or yours. */
|
|
659
840
|
setAmbient(conversationId: ConversationId, record: {
|
|
@@ -662,7 +843,7 @@ export interface SigrunHost {
|
|
|
662
843
|
readonly maxRows?: number;
|
|
663
844
|
readonly overflow?: "drop" | "wrap" | "summarize";
|
|
664
845
|
readonly refreshMs?: number;
|
|
665
|
-
readonly body?:
|
|
846
|
+
readonly body?: Tree;
|
|
666
847
|
readonly details?: JsonObject;
|
|
667
848
|
}, context: HandlerContext): Promise<void>;
|
|
668
849
|
/** Clear one ambient surface (empty replacement; keeps ownership). */
|
|
@@ -729,6 +910,8 @@ export interface SigrunHost {
|
|
|
729
910
|
configure(id: ConversationId, opts: {
|
|
730
911
|
readonly model?: string;
|
|
731
912
|
readonly thinking?: ThinkingLevelWire;
|
|
913
|
+
/** Exact extension selection; another user's `<slug>@<user>` entry is rejected. */
|
|
914
|
+
readonly extensions?: readonly string[];
|
|
732
915
|
}): Promise<void>;
|
|
733
916
|
/** Admit one manual compaction task (gateway C4, the compact route's op). */
|
|
734
917
|
compact(id: ConversationId): Promise<void>;
|
|
@@ -782,6 +965,25 @@ export interface SigrunHost {
|
|
|
782
965
|
list(): Promise<readonly DirectoryUserWire[]>;
|
|
783
966
|
get(user: string): Promise<DirectoryUserWire | undefined>;
|
|
784
967
|
};
|
|
968
|
+
/**
|
|
969
|
+
* External identities linked to directory users (multi-user §11.3): the
|
|
970
|
+
* one table for chat senders (`chat:<channel>`) and login subjects
|
|
971
|
+
* (`login:<slug>:<id>`), shared with the gateway.
|
|
972
|
+
*
|
|
973
|
+
* Core owns the file — extensions go through this surface, never
|
|
974
|
+
* `accounts.json` directly. One link per `(provider, subject)`: a
|
|
975
|
+
* re-link to the same user is a no-op returning the existing record, a
|
|
976
|
+
* link to a DIFFERENT user rejects with an `Error` whose message starts
|
|
977
|
+
* `account_link_conflict:` (the message never names the current
|
|
978
|
+
* holder — extensions match the prefix, as they do `command_not_found:`).
|
|
979
|
+
* An unknown user rejects `unknown user "<user>"`.
|
|
980
|
+
*/
|
|
981
|
+
readonly accounts: {
|
|
982
|
+
lookup(provider: string, subject: string): Promise<AccountLinkWire | undefined>;
|
|
983
|
+
link(provider: string, subject: string, user: string, via: AccountLinkWire["via"]): Promise<AccountLinkWire>;
|
|
984
|
+
unlink(provider: string, subject: string): Promise<boolean>;
|
|
985
|
+
listForUser(user: string): Promise<readonly AccountLinkWire[]>;
|
|
986
|
+
};
|
|
785
987
|
/**
|
|
786
988
|
* Resolve one principal's access to one shared resource (the whole
|
|
787
989
|
* sharing contract for extension-owned records — core stores nothing
|
|
@@ -841,12 +1043,18 @@ export interface SigrunHost {
|
|
|
841
1043
|
* begins reading each when its task lands.
|
|
842
1044
|
*/
|
|
843
1045
|
export interface SigrunExtensionResult extends SigrunLifecycleHandlers {
|
|
1046
|
+
/** Long-lived per-user sibling processes, launched and supervised by core. */
|
|
1047
|
+
readonly userServices?: readonly UserService[];
|
|
1048
|
+
/** Declare the per-user tool surface (`host.tools.setUserTools`). */
|
|
1049
|
+
readonly perUserTools?: boolean;
|
|
844
1050
|
/** The pi-durable `Extension` to install; its declared name is the slug source. */
|
|
845
1051
|
readonly extension?: Extension;
|
|
846
1052
|
/** The contributor-only identity (required when contributing without an `extension`). */
|
|
847
1053
|
readonly name?: string;
|
|
848
1054
|
/** Provider contributions, merged into the boot model catalog (last-registered-wins, collisions recorded). */
|
|
849
1055
|
readonly providers?: readonly ProviderContribution[];
|
|
1056
|
+
/** External identity providers; core qualifies ids as `login:<slug>:<id>`. */
|
|
1057
|
+
readonly loginProviders?: readonly LoginProviderContribution[];
|
|
850
1058
|
/** Webui-bundle contributions; each `dir` anchors at the extension ENTRY's directory. */
|
|
851
1059
|
readonly webuiBundles?: readonly WebuiBundleContribution[];
|
|
852
1060
|
/** The surface's health producer, aggregated into `/v1/meta` `health` (any `"degraded"` degrades). */
|
package/dist/harness/ambient.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The ambient seam (spec: docs/approval-ux-seams.md, "Ambient surfaces"
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
2
|
+
* The ambient seam (spec: docs/approval-ux-seams.md, "Ambient surfaces";
|
|
3
|
+
* the block mapping: UI-blocks design §9.2): core-owned,
|
|
4
|
+
* channel-neutral records for persistent read-mostly state — statusline
|
|
5
|
+
* slots and panel widgets — written by extensions through the host
|
|
6
|
+
* context and rendered by channels generically.
|
|
6
7
|
*
|
|
7
8
|
* SPLIT (build-and-webui Task 1): the canonical TYPE declarations live in
|
|
8
9
|
* the `@sigrun/shared` package (`shared/src/ambient.ts` — the records ride
|
|
@@ -16,10 +17,22 @@
|
|
|
16
17
|
* not silent composition — the pi-zentui chrome-skin lesson).
|
|
17
18
|
*
|
|
18
19
|
* Layering mirrors the interaction seam: strict envelope (key, place,
|
|
19
|
-
* budget, refresh, expires) around a body
|
|
20
|
-
* ambient
|
|
20
|
+
* budget, refresh, expires) around a block-tree body (placement
|
|
21
|
+
* `ambient`; `status`/`footer` allow one line), with opaque `details`.
|
|
22
|
+
* Legacy-shaped bodies FAIL with paths (ruling 16).
|
|
21
23
|
*/
|
|
22
24
|
import { defineDoc, defineDocFamily } from "@earendil-works/pi-durable";
|
|
25
|
+
import { validateTree } from "../blocks/validate.js";
|
|
26
|
+
/**
|
|
27
|
+
* Narrow a stored record to the public wire record. SAFETY: the two
|
|
28
|
+
* shapes are identical at runtime — a stored body passed `validateTree`
|
|
29
|
+
* (placement `ambient`) at write time, and a validated tree IS the JSON
|
|
30
|
+
* the mirror describes; the cast only restores the type-level naming
|
|
31
|
+
* `JsonObject`-erasure removed.
|
|
32
|
+
*/
|
|
33
|
+
export function asAmbientRecord(value) {
|
|
34
|
+
return value;
|
|
35
|
+
}
|
|
23
36
|
/**
|
|
24
37
|
* The ambient record family: one record per surface key, per conversation.
|
|
25
38
|
* (Session-scoped statuslines that outlive one conversation are a v2
|
|
@@ -48,9 +61,12 @@ export const AmbientOwnersDoc = defineDoc({
|
|
|
48
61
|
});
|
|
49
62
|
const PLACES = new Set(["status", "footer", "panel"]);
|
|
50
63
|
const OVERFLOWS = new Set(["drop", "wrap", "summarize"]);
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
64
|
+
/**
|
|
65
|
+
* Structural validation of one ambient record: the envelope's own checks,
|
|
66
|
+
* then the body tree through `validateTree({placement: "ambient"})` with
|
|
67
|
+
* the one-line rule for `status`/`footer` (§9.2). Returns the error text
|
|
68
|
+
* (with the tree paths) or null.
|
|
69
|
+
*/
|
|
54
70
|
export function validateAmbient(value) {
|
|
55
71
|
if (typeof value !== "object" || value === null) {
|
|
56
72
|
return "ambient record must be an object";
|
|
@@ -80,182 +96,20 @@ export function validateAmbient(value) {
|
|
|
80
96
|
const body = r.body;
|
|
81
97
|
if (body === undefined)
|
|
82
98
|
return null; // an empty write clears the surface
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
}
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
if (b.rows !== undefined) {
|
|
98
|
-
if (!Array.isArray(b.rows))
|
|
99
|
-
return "body.rows must be an array";
|
|
100
|
-
for (const row of b.rows) {
|
|
101
|
-
const problem = validateRow(row);
|
|
102
|
-
if (problem !== null)
|
|
103
|
-
return problem;
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
if (b.gauge !== undefined) {
|
|
107
|
-
const problem = validateGauge(b.gauge);
|
|
108
|
-
if (problem !== null)
|
|
109
|
-
return problem;
|
|
110
|
-
}
|
|
111
|
-
if (b.ledger !== undefined) {
|
|
112
|
-
if (!Array.isArray(b.ledger))
|
|
113
|
-
return "body.ledger must be an array";
|
|
114
|
-
for (const e of b.ledger) {
|
|
115
|
-
const problem = validateLedgerEntry(e);
|
|
116
|
-
if (problem !== null)
|
|
117
|
-
return problem;
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
return null;
|
|
121
|
-
}
|
|
122
|
-
function validateLine(value) {
|
|
123
|
-
if (typeof value !== "object" || value === null)
|
|
124
|
-
return "line must be an object";
|
|
125
|
-
const l = value;
|
|
126
|
-
if (l.text === undefined && l.segments === undefined) {
|
|
127
|
-
return "line needs text or segments";
|
|
128
|
-
}
|
|
129
|
-
if (l.text !== undefined && typeof l.text !== "string") {
|
|
130
|
-
return "line.text must be a string";
|
|
131
|
-
}
|
|
132
|
-
if (l.segments !== undefined) {
|
|
133
|
-
if (!Array.isArray(l.segments))
|
|
134
|
-
return "line.segments must be an array";
|
|
135
|
-
for (const s of l.segments) {
|
|
136
|
-
if (typeof s !== "object" || s === null)
|
|
137
|
-
return "segment must be an object";
|
|
138
|
-
const seg = s;
|
|
139
|
-
if (typeof seg.id !== "string" || seg.id.length === 0) {
|
|
140
|
-
return "segment.id must be a non-empty string";
|
|
141
|
-
}
|
|
142
|
-
if (typeof seg.text !== "string")
|
|
143
|
-
return "segment.text must be a string";
|
|
144
|
-
if (seg.place !== undefined && !ZONES.has(String(seg.place))) {
|
|
145
|
-
return `segment.place must be one of left|middle|right (got ${String(seg.place)})`;
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
return null;
|
|
150
|
-
}
|
|
151
|
-
function validateRow(value) {
|
|
152
|
-
if (typeof value !== "object" || value === null)
|
|
153
|
-
return "row must be an object";
|
|
154
|
-
const r = value;
|
|
155
|
-
if (typeof r.id !== "string" || r.id.length === 0) {
|
|
156
|
-
return "row.id must be a non-empty string";
|
|
157
|
-
}
|
|
158
|
-
if (typeof r.label !== "string")
|
|
159
|
-
return "row.label must be a string";
|
|
160
|
-
if (r.status !== undefined && typeof r.status !== "string") {
|
|
161
|
-
return "row.status must be a string";
|
|
162
|
-
}
|
|
163
|
-
if (r.severity !== undefined &&
|
|
164
|
-
r.severity !== 0 &&
|
|
165
|
-
r.severity !== 1 &&
|
|
166
|
-
r.severity !== 2) {
|
|
167
|
-
return "row.severity must be 0, 1, or 2";
|
|
168
|
-
}
|
|
169
|
-
if (r.quiet !== undefined && typeof r.quiet !== "boolean") {
|
|
170
|
-
return "row.quiet must be a boolean";
|
|
171
|
-
}
|
|
172
|
-
if (r.detail !== undefined && typeof r.detail !== "string") {
|
|
173
|
-
return "row.detail must be a string";
|
|
174
|
-
}
|
|
175
|
-
if (r.depth !== undefined && typeof r.depth !== "number") {
|
|
176
|
-
return "row.depth must be a number";
|
|
177
|
-
}
|
|
178
|
-
if (r.deps !== undefined && !Array.isArray(r.deps)) {
|
|
179
|
-
return "row.deps must be an array of ids";
|
|
180
|
-
}
|
|
181
|
-
if (Array.isArray(r.deps) && r.deps.some((d) => typeof d !== "string")) {
|
|
182
|
-
return "row.deps must be an array of ids";
|
|
183
|
-
}
|
|
184
|
-
if (r.spinnerLabel !== undefined && typeof r.spinnerLabel !== "string") {
|
|
185
|
-
return "row.spinnerLabel must be a string";
|
|
186
|
-
}
|
|
187
|
-
if (r.metrics !== undefined) {
|
|
188
|
-
const problem = validateMetrics(r.metrics);
|
|
189
|
-
if (problem !== null)
|
|
190
|
-
return problem;
|
|
191
|
-
}
|
|
192
|
-
return null;
|
|
193
|
-
}
|
|
194
|
-
function validateMetrics(value) {
|
|
195
|
-
if (typeof value !== "object" || value === null)
|
|
196
|
-
return "metrics must be an object";
|
|
197
|
-
const m = value;
|
|
198
|
-
for (const k of ["tokens", "cost", "current", "total", "completed"]) {
|
|
199
|
-
if (m[k] !== undefined && typeof m[k] !== "number") {
|
|
200
|
-
return `metrics.${k} must be a number`;
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
if (m.format !== undefined && !FORMATS.has(String(m.format))) {
|
|
204
|
-
return `metrics.format must be one of compact|usd|percent (got ${String(m.format)})`;
|
|
205
|
-
}
|
|
206
|
-
return null;
|
|
207
|
-
}
|
|
208
|
-
function validateGauge(value) {
|
|
209
|
-
if (typeof value !== "object" || value === null)
|
|
210
|
-
return "gauge must be an object";
|
|
211
|
-
const g = value;
|
|
212
|
-
if (typeof g.label !== "string")
|
|
213
|
-
return "gauge.label must be a string";
|
|
214
|
-
if (g.percent !== undefined && typeof g.percent !== "number") {
|
|
215
|
-
return "gauge.percent must be a number";
|
|
216
|
-
}
|
|
217
|
-
if (g.phase !== undefined && typeof g.phase !== "string") {
|
|
218
|
-
return "gauge.phase must be a string";
|
|
219
|
-
}
|
|
220
|
-
if (g.elapsedMs !== undefined && typeof g.elapsedMs !== "number") {
|
|
221
|
-
return "gauge.elapsedMs must be a number";
|
|
222
|
-
}
|
|
223
|
-
if (g.recentOutput !== undefined && !Array.isArray(g.recentOutput)) {
|
|
224
|
-
return "gauge.recentOutput must be an array of strings";
|
|
225
|
-
}
|
|
226
|
-
if (Array.isArray(g.recentOutput) && g.recentOutput.some((l) => typeof l !== "string")) {
|
|
227
|
-
return "gauge.recentOutput must be an array of strings";
|
|
228
|
-
}
|
|
229
|
-
if (g.checks !== undefined && !Array.isArray(g.checks)) {
|
|
230
|
-
return "gauge.checks must be row-shaped";
|
|
231
|
-
}
|
|
232
|
-
if (Array.isArray(g.checks)) {
|
|
233
|
-
for (const row of g.checks) {
|
|
234
|
-
const problem = validateRow(row);
|
|
235
|
-
if (problem !== null)
|
|
236
|
-
return `gauge.checks: ${problem}`;
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
return null;
|
|
240
|
-
}
|
|
241
|
-
function validateLedgerEntry(value) {
|
|
242
|
-
if (typeof value !== "object" || value === null)
|
|
243
|
-
return "ledger entry must be an object";
|
|
244
|
-
const e = value;
|
|
245
|
-
if (typeof e.id !== "string" || e.id.length === 0) {
|
|
246
|
-
return "ledger.id must be a non-empty string";
|
|
247
|
-
}
|
|
248
|
-
if (typeof e.at !== "number")
|
|
249
|
-
return "ledger.at must be a number";
|
|
250
|
-
if (typeof e.kind !== "string")
|
|
251
|
-
return "ledger.kind must be a string";
|
|
252
|
-
if (typeof e.summary !== "string")
|
|
253
|
-
return "ledger.summary must be a string";
|
|
254
|
-
if (e.detail !== undefined && !Array.isArray(e.detail)) {
|
|
255
|
-
return "ledger.detail must be an array of strings";
|
|
256
|
-
}
|
|
257
|
-
if (Array.isArray(e.detail) && e.detail.some((d) => typeof d !== "string")) {
|
|
258
|
-
return "ledger.detail must be an array of strings";
|
|
99
|
+
// Ruling 16 (no compatibility layer): legacy-shaped bodies (objects
|
|
100
|
+
// keyed line/rows/gauge/ledger rather than {v, root}) fail HERE, with
|
|
101
|
+
// the path — a tree is {v, root}.
|
|
102
|
+
const result = validateTree(body, {
|
|
103
|
+
placement: "ambient",
|
|
104
|
+
ambientPlace: r.place,
|
|
105
|
+
});
|
|
106
|
+
if (!result.ok) {
|
|
107
|
+
const first = result.errors[0];
|
|
108
|
+
if (first === undefined)
|
|
109
|
+
return "body must be a block tree";
|
|
110
|
+
// The tree-relative path joins the record's field ("body.root…")
|
|
111
|
+
// so the error names where the record is wrong, not just the tree.
|
|
112
|
+
return `body is not a valid ambient tree: body.${first.path} ${first.message}`;
|
|
259
113
|
}
|
|
260
114
|
return null;
|
|
261
115
|
}
|