@sigrun/sigrun 0.1.4 → 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.
Files changed (76) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/README.md +24 -2
  3. package/dist/blocks/builders.js +125 -0
  4. package/dist/blocks/images.js +57 -0
  5. package/dist/blocks/index.js +20 -0
  6. package/dist/blocks/limits.js +26 -0
  7. package/dist/blocks/placements.js +99 -0
  8. package/dist/blocks/schema.js +559 -0
  9. package/dist/blocks/validate.js +602 -0
  10. package/dist/config.js +31 -0
  11. package/dist/extension.d.ts +476 -9
  12. package/dist/harness/boot.js +8 -1
  13. package/dist/harness/event-bus.js +33 -0
  14. package/dist/harness/identity/users.js +3 -2
  15. package/dist/harness/loader.js +369 -13
  16. package/dist/harness/packages.js +71 -2
  17. package/dist/harness/seam-host.js +69 -2
  18. package/dist/harness/settings.js +5 -0
  19. package/dist/jobs/fire.js +11 -0
  20. package/dist/jobs/spec.js +19 -0
  21. package/dist/serve.js +29 -39
  22. package/dist/server/auth/pipeline.js +16 -1
  23. package/dist/server/conversation-ops.js +482 -10
  24. package/dist/server/gateway.js +122 -0
  25. package/dist/server/helpers.js +26 -0
  26. package/dist/server/host-bridge.js +141 -4
  27. package/dist/server/index.js +27 -0
  28. package/dist/server/notifications.js +6 -2
  29. package/dist/server/routes/conversations.js +37 -93
  30. package/dist/server/routes/events.js +39 -0
  31. package/dist/server/routes/ext-assets.js +84 -0
  32. package/dist/server/routes/extensions.js +31 -1
  33. package/dist/server/routes/files.js +38 -83
  34. package/dist/server/routes/meta.js +35 -1
  35. package/dist/server/routes/providers.js +5 -21
  36. package/dist/server/routes/submissions.js +36 -111
  37. package/dist/server/routes/surfaces.js +387 -0
  38. package/dist/server/routes/users.js +6 -1
  39. package/dist/server/session-meta.js +12 -6
  40. package/dist/server/shutdown.js +85 -0
  41. package/dist/server/surfaces/access-cache.js +43 -0
  42. package/dist/server/surfaces/dedup.js +67 -0
  43. package/dist/server/surfaces/instance.js +42 -0
  44. package/dist/server/surfaces/mime.js +73 -0
  45. package/dist/server/surfaces/rate-limit.js +63 -0
  46. package/dist/server/surfaces/service.js +493 -0
  47. package/dist/server/surfaces/watchers.js +94 -0
  48. package/node_modules/@sigrun/shared/dist/blocks/format.d.ts +28 -0
  49. package/node_modules/@sigrun/shared/dist/blocks/format.js +199 -0
  50. package/node_modules/@sigrun/shared/dist/blocks/icons.d.ts +12 -0
  51. package/node_modules/@sigrun/shared/dist/blocks/icons.js +64 -0
  52. package/node_modules/@sigrun/shared/dist/blocks/index.d.ts +25 -0
  53. package/node_modules/@sigrun/shared/dist/blocks/index.js +21 -0
  54. package/node_modules/@sigrun/shared/dist/blocks/instance.d.ts +29 -0
  55. package/node_modules/@sigrun/shared/dist/blocks/instance.js +51 -0
  56. package/node_modules/@sigrun/shared/dist/blocks/lttb.d.ts +16 -0
  57. package/node_modules/@sigrun/shared/dist/blocks/lttb.js +67 -0
  58. package/node_modules/@sigrun/shared/dist/blocks/ops.d.ts +44 -0
  59. package/node_modules/@sigrun/shared/dist/blocks/ops.js +294 -0
  60. package/node_modules/@sigrun/shared/dist/blocks/text.d.ts +43 -0
  61. package/node_modules/@sigrun/shared/dist/blocks/text.js +790 -0
  62. package/node_modules/@sigrun/shared/dist/blocks/transport.d.ts +77 -0
  63. package/node_modules/@sigrun/shared/dist/blocks/transport.js +11 -0
  64. package/node_modules/@sigrun/shared/dist/blocks/types.d.ts +541 -0
  65. package/node_modules/@sigrun/shared/dist/blocks/types.js +17 -0
  66. package/node_modules/@sigrun/shared/dist/jobs.d.ts +9 -0
  67. package/node_modules/@sigrun/shared/dist/live.d.ts +71 -0
  68. package/node_modules/@sigrun/shared/dist/live.js +82 -0
  69. package/node_modules/@sigrun/shared/dist/session-meta.d.ts +19 -3
  70. package/node_modules/@sigrun/shared/dist/settings.d.ts +7 -0
  71. package/node_modules/@sigrun/shared/dist/views.d.ts +81 -3
  72. package/node_modules/@sigrun/shared/package.json +10 -2
  73. package/package.json +9 -3
  74. package/webui/dist/assets/index-B4-GYqEb.js +50 -0
  75. package/webui/dist/index.html +1 -1
  76. package/webui/dist/assets/index-hVLvsizV.js +0 -50
@@ -103,13 +103,16 @@ export function createSeamHost(identity, kinds, bind, sharedStores) {
103
103
  indexPending(index, conversationId, record.callId);
104
104
  }, context);
105
105
  },
106
- async answerInteraction(conversationId, callId, answer, context) {
106
+ async answerInteraction(conversationId, callId, answer, context, opts) {
107
107
  const h = await harness(context);
108
108
  return await h.commit(async (tx) => {
109
109
  const draft = await tx.doc(InteractionDoc, conversationId, callId, callId);
110
110
  if (draft.status !== "pending")
111
111
  return false;
112
- const applied = applyAnswer(draft, answer, "extension", Date.now());
112
+ // Gateway C5: a host caller naming a user answers AS that human
113
+ // (the route's path — the same applyAnswer line); without a user
114
+ // the extension answered itself.
115
+ const applied = applyAnswer(draft, answer, opts?.user === undefined ? "extension" : "human", Date.now(), opts?.user);
113
116
  if (applied) {
114
117
  const index = await tx.doc(InteractionsIndexDoc);
115
118
  indexSettled(index, conversationId, callId);
@@ -193,6 +196,7 @@ export function createSeamHost(identity, kinds, bind, sharedStores) {
193
196
  ...(opts.owner === undefined ? {} : { owner: opts.owner }),
194
197
  ...(opts.sharing === undefined ? {} : { sharing: opts.sharing }),
195
198
  ...(opts.name === undefined ? {} : { name: opts.name }),
199
+ ...(opts.agent === undefined ? {} : { agent: opts.agent }),
196
200
  // The source default: the CALLING extension's slug (spec §9.5).
197
201
  source: opts.source ?? identity.name,
198
202
  });
@@ -213,6 +217,30 @@ export function createSeamHost(identity, kinds, bind, sharedStores) {
213
217
  const bridge = await hostBridge();
214
218
  return bridge.conversations.meta(id);
215
219
  },
220
+ // The C4a ops: straight delegation (the bridge resolves the id and
221
+ // runs the op its HTTP twin runs).
222
+ async abort(id) {
223
+ return (await hostBridge()).conversations.abort(id);
224
+ },
225
+ async configure(id, opts) {
226
+ return (await hostBridge()).conversations.configure(id, opts);
227
+ },
228
+ async compact(id) {
229
+ return (await hostBridge()).conversations.compact(id);
230
+ },
231
+ async fork(id, opts) {
232
+ return (await hostBridge()).conversations.fork(id, opts);
233
+ },
234
+ async patchMeta(id, patch) {
235
+ return (await hostBridge()).conversations.patchMeta(id, patch);
236
+ },
237
+ // The C4b ops: same straight delegation.
238
+ async upload(id, file) {
239
+ return (await hostBridge()).conversations.upload(id, file);
240
+ },
241
+ async readFile(id, path) {
242
+ return (await hostBridge()).conversations.readFile(id, path);
243
+ },
216
244
  },
217
245
  users: {
218
246
  async list() {
@@ -232,6 +260,45 @@ export function createSeamHost(identity, kinds, bind, sharedStores) {
232
260
  return (await hostBridge()).identity.actingUser(conversationId);
233
261
  },
234
262
  },
263
+ // Gateway C4b: straight delegation (the bridge owns both bodies).
264
+ models: {
265
+ async list() {
266
+ return (await hostBridge()).models.list();
267
+ },
268
+ },
269
+ commands: {
270
+ async list() {
271
+ return (await hostBridge()).commands.list();
272
+ },
273
+ async run(conversationId, name, args, opts) {
274
+ return (await hostBridge()).commands.run(conversationId, name, args, opts);
275
+ },
276
+ },
277
+ // The surfaces seam (UI-blocks U3 Task 4): `changed` awaits the
278
+ // bridge (so a factory-body call waits, never resolves early) and
279
+ // delegates to the server-side SurfaceService — the rev bump + the
280
+ // hub event are the service's one code path (the actions route and
281
+ // the host seam share it).
282
+ surfaces: {
283
+ changed(id, key, update) {
284
+ const host = identity; // capture for the async lane
285
+ const ran = () => {
286
+ // The Review Focus 4 observability seam (the fixture
287
+ // surfaces-early pattern): a call that WAITED for the
288
+ // bridge is distinguishable from an early no-op. Inert in
289
+ // production (nothing sets it).
290
+ globalThis.__sigrunSurfaceChanged?.(host.name);
291
+ };
292
+ void hostBridge()
293
+ .then((bridge) => {
294
+ ran();
295
+ bridge.surfaces.changed(identity.name, id, key, update);
296
+ })
297
+ .catch((error) => {
298
+ console.error(`seam host: surfaces.changed (${identity.name}/${id}) failed:`, error);
299
+ });
300
+ },
301
+ },
235
302
  bindBridge(bridge) {
236
303
  bridgeReleased(bridge);
237
304
  },
@@ -413,6 +413,7 @@ function validateServer(server) {
413
413
  "providers",
414
414
  "auth",
415
415
  "metrics",
416
+ "dev",
416
417
  "backup",
417
418
  ]);
418
419
  // The jobs default timezone (§8.2): an IANA name — `Intl` accepts
@@ -611,6 +612,10 @@ function validateServer(server) {
611
612
  throw new SettingsValidationError("server.metrics.public", "must be a boolean");
612
613
  }
613
614
  }
615
+ // The development flag (§14.2/§14.4) — a flat boolean, no section.
616
+ if (record.dev !== undefined && typeof record.dev !== "boolean") {
617
+ throw new SettingsValidationError("server.dev", "must be a boolean");
618
+ }
614
619
  if (record.backup !== undefined) {
615
620
  const backup = asRecord(record.backup);
616
621
  if (backup === undefined) {
package/dist/jobs/fire.js CHANGED
@@ -188,6 +188,12 @@ async function firePrompt(deps, id, record, fireKey, baseRun, prior, context) {
188
188
  // conversation: "new" — the run record + conversation in ONE commit.
189
189
  let conversationId = prior?.conversationId;
190
190
  if (conversationId === undefined) {
191
+ // Gateway C7: the deliver targets need a deliverer. With no engine
192
+ // loaded the stamped targets point at nothing — ONE warning per run
193
+ // (the fire itself proceeds; the stamp stays for whoever reads it).
194
+ if (action.deliver !== undefined && !deps.gatewayLoaded) {
195
+ console.warn("deliver targets ignored: no gateway");
196
+ }
191
197
  const model = action.model !== undefined
192
198
  ? parseModelSpec(action.model, "job action model")
193
199
  : deps.defaultModel;
@@ -207,7 +213,12 @@ async function firePrompt(deps, id, record, fireKey, baseRun, prior, context) {
207
213
  await tx.doc(JobRunDoc, fireKey, run);
208
214
  const meta = await tx.doc(SessionMetaDoc, newId);
209
215
  meta.source = "cron";
216
+ // Gateway C7: the job stamp — typed SessionMeta fields (the
217
+ // pre-C7 cast is gone). Copy, never alias: the doc tracker
218
+ // takes alias-free ownership of the array.
210
219
  meta.jobId = id;
220
+ if (action.deliver !== undefined)
221
+ meta.deliver = [...action.deliver];
211
222
  if (record.owner !== undefined)
212
223
  meta.owner = record.owner;
213
224
  if (action.runSharing !== undefined)
package/dist/jobs/spec.js CHANGED
@@ -170,6 +170,23 @@ function validateAction(value) {
170
170
  if (record.runSharing !== undefined && !isSharing(record.runSharing)) {
171
171
  return invalid("action.runSharing", "runSharing must be a Sharing value");
172
172
  }
173
+ // Gateway C7 (spec §8.0): deliver targets ride ONLY new-conversation
174
+ // prompt jobs — a conversation bound to a chat already delivers
175
+ // there, so deliver on a fixed id is a spec error naming this field.
176
+ // Shape-only: the target grammar belongs to the gateway.
177
+ if (record.deliver !== undefined) {
178
+ if (record.conversation !== "new") {
179
+ return invalid("action.deliver", 'deliver requires conversation "new"');
180
+ }
181
+ if (!Array.isArray(record.deliver) ||
182
+ record.deliver.length < 1 ||
183
+ record.deliver.length > 8 ||
184
+ record.deliver.some((target) => typeof target !== "string" ||
185
+ target.length < 1 ||
186
+ target.length > 256)) {
187
+ return invalid("action.deliver", "deliver must be an array of 1-8 strings, each 1-256 chars");
188
+ }
189
+ }
173
190
  const action = {
174
191
  kind: "prompt",
175
192
  prompt: record.prompt,
@@ -182,6 +199,8 @@ function validateAction(value) {
182
199
  action.thinking = record.thinking;
183
200
  if (record.runSharing !== undefined)
184
201
  action.runSharing = record.runSharing;
202
+ if (record.deliver !== undefined)
203
+ action.deliver = [...record.deliver];
185
204
  return { ok: true, action };
186
205
  }
187
206
  if (record.kind === "task") {
package/dist/serve.js CHANGED
@@ -28,8 +28,10 @@ import { envServerSection, loadConfig } from "./config.js";
28
28
  import { openHarness } from "./harness/boot.js";
29
29
  import { packagesDirOf, syncPackages } from "./harness/packages.js";
30
30
  import { defaultSettingsDir, SettingsStore } from "./harness/settings.js";
31
+ import { startGateway } from "./server/gateway.js";
31
32
  import { SIGRUN_VERSION } from "./server/schema-fixture.js";
32
33
  import { createServer } from "./server/index.js";
34
+ import { createShutdown } from "./server/shutdown.js";
33
35
  /**
34
36
  * Boot the harness + server composition and listen on
35
37
  * `options.host ?? "0.0.0.0"` + `<port>` (the default is the container's
@@ -88,44 +90,43 @@ export async function serve(env, options) {
88
90
  const { mkdir } = await import("node:fs/promises");
89
91
  await mkdir(config.workspace.path, { recursive: true });
90
92
  const opened = await openHarness(config, { extensionEntries: extensionResolutions });
93
+ // The gateway handle (gateway C2) exists BEFORE the server: the meta
94
+ // route reaches it through ServerContext, so the engine's health is
95
+ // reportable from the first request. The ENGINE STARTS after listen
96
+ // (below) — an engine supervising chat transports starts once the
97
+ // server it delivers through is up.
98
+ const gateway = startGateway(opened);
91
99
  // The contributed-bundle registry (build-and-webui Task 6): boot folded
92
100
  // the loader's extension contributions into the name-keyed registry;
93
101
  // the server's serve-time NAME-selector resolution reads it here.
94
102
  const app = await createServer(opened, config, opened.webuiBundles, {
95
103
  listenHost: options?.host ?? "0.0.0.0",
96
104
  env,
105
+ gateway,
97
106
  });
98
107
  const host = options?.host ?? "0.0.0.0";
99
108
  await app.listen({ port: config.port, host });
109
+ // The engine starts with the listener live (gateway C2's lifecycle:
110
+ // start after listen — a failed start latches degraded inside the
111
+ // handle, never wedges boot). AWAITED: serve() resolves only once the
112
+ // engine start completed (or failed-and-latched) — the constraint's
113
+ // intent; the lazy `started` never rejects, so this await cannot throw.
114
+ await gateway.started;
100
115
  // Machine-greppable: the deploy-entry test waits for this line, and an
101
116
  // operator reading pod logs sees the bound port without guessing. The
102
117
  // version rides the same line (what an operator quotes in a bug report).
103
118
  console.log(`sigrun v${SIGRUN_VERSION} listening on http://${host}:${config.port}`);
104
- const shutdown = async () => {
105
- const failures = [];
106
- // SSE streams FIRST (the ctrl+c fix): hijacked event streams never
107
- // end on their own, and app.close()'s drain waits for in-flight
108
- // requests — with any webui tab open, shutdown hung forever on its
109
- // /v1/events connection. End every live stream BEFORE the drain.
110
- try {
111
- app.sse.endAll();
112
- }
113
- catch (error) {
114
- failures.push(error);
115
- }
116
- try {
117
- await app.close();
118
- }
119
- catch (error) {
120
- failures.push(error);
121
- }
122
- try {
123
- await opened.eventBus.shutdownSequence(BACKGROUND_CONTEXT);
124
- }
125
- catch (error) {
126
- failures.push(error);
127
- }
128
- try {
119
+ // The ordered shutdown (gateway C2's extraction of the inline original
120
+ // into src/server/shutdown.ts): SSE streams FIRST, the app drain, the
121
+ // engine's intake close, the event-bus sequence (drain + flush), the
122
+ // engine stop, the refresh stop + bounded boot-refresh join, the
123
+ // harness close — each failure collected, all steps always run.
124
+ const shutdown = createShutdown({
125
+ endStreams: () => app.sse.endAll(),
126
+ closeApp: () => app.close(),
127
+ gateway,
128
+ eventBusShutdown: () => opened.eventBus.shutdownSequence(BACKGROUND_CONTEXT),
129
+ stopProviderRefresh: async () => {
129
130
  // The interval refresh scheduler (provider plugins Task 5) stops
130
131
  // BEFORE the harness closes. The T4 boot refresh promise is raced
131
132
  // against a bounded join: a wedged catalog fetch must not hold the
@@ -150,20 +151,9 @@ export async function serve(env, options) {
150
151
  if (!joined) {
151
152
  console.warn(`sigrun: boot refresh still in flight after ${BOOT_REFRESH_JOIN_MS}ms at shutdown — abandoning the join`);
152
153
  }
153
- }
154
- catch (error) {
155
- failures.push(error);
156
- }
157
- try {
158
- await opened.harness.close(BACKGROUND_CONTEXT);
159
- }
160
- catch (error) {
161
- failures.push(error);
162
- }
163
- if (failures.length > 0) {
164
- throw new AggregateError(failures, "sigrun shutdown failed");
165
- }
166
- };
154
+ },
155
+ closeHarness: () => opened.harness.close(BACKGROUND_CONTEXT),
156
+ });
167
157
  let stopping;
168
158
  return {
169
159
  port: config.port,
@@ -209,6 +209,7 @@ async function resolveProxy(request, ctx) {
209
209
  if (existing === undefined && !proxy.autoProvision) {
210
210
  return { status: 403, error: "user_not_provisioned" };
211
211
  }
212
+ const groupAdmin = groups.some((group) => adminGroups.includes(group));
212
213
  const record = existing ??
213
214
  (await users.add({
214
215
  user,
@@ -219,9 +220,23 @@ async function resolveProxy(request, ctx) {
219
220
  ? {}
220
221
  : { email: headerOf(request, proxy.emailHeader) }),
221
222
  admin: false,
223
+ // Gateway C8: the grant is stamped at provision — host-side
224
+ // checks (chat commands, no headers) read it from the record.
225
+ ...(adminGroups.length > 0 ? { groupAdmin } : {}),
222
226
  source: "proxy",
223
227
  }));
224
- const groupAdmin = groups.some((group) => adminGroups.includes(group));
228
+ // Gateway C8 write-through: host-side code has no request headers, so
229
+ // the computed group grant must live on the directory record. Only a
230
+ // CHANGE writes (the steady state never rewrites the file), and only
231
+ // while `adminGroups` is configured — an empty list leaves the field
232
+ // untouched (a group-exit clears it at this user's next request under
233
+ // a non-empty list, never under an empty one). Absent counts as false;
234
+ // the auto-provisioned record was added with the flag already set, so
235
+ // the comparison is against the RESOLVED record, never `existing`.
236
+ if (adminGroups.length > 0 &&
237
+ (record.groupAdmin ?? false) !== groupAdmin) {
238
+ await users.update(user, { groupAdmin });
239
+ }
225
240
  return {
226
241
  user: record.user,
227
242
  ...(record.name === undefined ? {} : { name: record.name }),