@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
|
@@ -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
|
-
|
|
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
|
},
|
package/dist/harness/settings.js
CHANGED
|
@@ -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,7 +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";
|
|
32
|
+
import { SIGRUN_VERSION } from "./server/schema-fixture.js";
|
|
31
33
|
import { createServer } from "./server/index.js";
|
|
34
|
+
import { createShutdown } from "./server/shutdown.js";
|
|
32
35
|
/**
|
|
33
36
|
* Boot the harness + server composition and listen on
|
|
34
37
|
* `options.host ?? "0.0.0.0"` + `<port>` (the default is the container's
|
|
@@ -87,43 +90,43 @@ export async function serve(env, options) {
|
|
|
87
90
|
const { mkdir } = await import("node:fs/promises");
|
|
88
91
|
await mkdir(config.workspace.path, { recursive: true });
|
|
89
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);
|
|
90
99
|
// The contributed-bundle registry (build-and-webui Task 6): boot folded
|
|
91
100
|
// the loader's extension contributions into the name-keyed registry;
|
|
92
101
|
// the server's serve-time NAME-selector resolution reads it here.
|
|
93
102
|
const app = await createServer(opened, config, opened.webuiBundles, {
|
|
94
103
|
listenHost: options?.host ?? "0.0.0.0",
|
|
95
104
|
env,
|
|
105
|
+
gateway,
|
|
96
106
|
});
|
|
97
107
|
const host = options?.host ?? "0.0.0.0";
|
|
98
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;
|
|
99
115
|
// Machine-greppable: the deploy-entry test waits for this line, and an
|
|
100
|
-
// operator reading pod logs sees the bound port without guessing.
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
try {
|
|
115
|
-
await app.close();
|
|
116
|
-
}
|
|
117
|
-
catch (error) {
|
|
118
|
-
failures.push(error);
|
|
119
|
-
}
|
|
120
|
-
try {
|
|
121
|
-
await opened.eventBus.shutdownSequence(BACKGROUND_CONTEXT);
|
|
122
|
-
}
|
|
123
|
-
catch (error) {
|
|
124
|
-
failures.push(error);
|
|
125
|
-
}
|
|
126
|
-
try {
|
|
116
|
+
// operator reading pod logs sees the bound port without guessing. The
|
|
117
|
+
// version rides the same line (what an operator quotes in a bug report).
|
|
118
|
+
console.log(`sigrun v${SIGRUN_VERSION} listening on http://${host}:${config.port}`);
|
|
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 () => {
|
|
127
130
|
// The interval refresh scheduler (provider plugins Task 5) stops
|
|
128
131
|
// BEFORE the harness closes. The T4 boot refresh promise is raced
|
|
129
132
|
// against a bounded join: a wedged catalog fetch must not hold the
|
|
@@ -148,20 +151,9 @@ export async function serve(env, options) {
|
|
|
148
151
|
if (!joined) {
|
|
149
152
|
console.warn(`sigrun: boot refresh still in flight after ${BOOT_REFRESH_JOIN_MS}ms at shutdown — abandoning the join`);
|
|
150
153
|
}
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
}
|
|
155
|
-
try {
|
|
156
|
-
await opened.harness.close(BACKGROUND_CONTEXT);
|
|
157
|
-
}
|
|
158
|
-
catch (error) {
|
|
159
|
-
failures.push(error);
|
|
160
|
-
}
|
|
161
|
-
if (failures.length > 0) {
|
|
162
|
-
throw new AggregateError(failures, "sigrun shutdown failed");
|
|
163
|
-
}
|
|
164
|
-
};
|
|
154
|
+
},
|
|
155
|
+
closeHarness: () => opened.harness.close(BACKGROUND_CONTEXT),
|
|
156
|
+
});
|
|
165
157
|
let stopping;
|
|
166
158
|
return {
|
|
167
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
|
-
|
|
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 }),
|