@sigrun/sigrun 0.1.6 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +104 -0
- package/README.md +352 -36
- package/dist/blocks/schema.js +9 -2
- package/dist/blocks/validate.js +57 -8
- package/dist/cli.js +26 -2
- package/dist/config.js +67 -1
- package/dist/extension.d.ts +189 -15
- 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 +253 -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 +80 -6
- 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 +162 -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 +20 -0
- package/dist/sandbox/os-account.js +49 -0
- package/dist/sandbox/policy.js +253 -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 +281 -0
- package/dist/serve.js +82 -7
- package/dist/server/auth/access-principal.js +16 -0
- package/dist/server/auth/pipeline.js +47 -33
- 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 +205 -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 +144 -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/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 +267 -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 +15 -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 +6 -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 +175 -12
- package/node_modules/@sigrun/shared/dist/views.js +16 -0
- package/node_modules/@sigrun/shared/package.json +1 -1
- package/package.json +11 -6
- package/webui/dist/assets/index-CxB969qp.js +55 -0
- package/webui/dist/assets/index-yqkL6BSS.css +1 -0
- package/webui/dist/index.html +2 -2
- package/webui/dist/assets/index-CiYInlLx.css +0 -1
- package/webui/dist/assets/index-JDfOQV9z.js +0 -52
package/dist/harness/boot.js
CHANGED
|
@@ -11,6 +11,7 @@ import { BACKGROUND_CONTEXT } from "@earendil-works/chord/context";
|
|
|
11
11
|
import { join } from "node:path";
|
|
12
12
|
import { Harness, } from "@earendil-works/pi-durable";
|
|
13
13
|
import { openNodeSqliteStorage } from "@earendil-works/pi-durable/storage/sqlite/node";
|
|
14
|
+
import { effectiveMethods } from "../config.js";
|
|
14
15
|
import { wireActivity } from "./activity.js";
|
|
15
16
|
import { createScheduleTask } from "../jobs/task.js";
|
|
16
17
|
import { createJobsService, jobWireOf } from "../jobs/index.js";
|
|
@@ -29,6 +30,7 @@ import { modelGuidanceExtension } from "./sections/model-guidance.js";
|
|
|
29
30
|
import { createSkillsExtension } from "../skills/index.js";
|
|
30
31
|
import { SLASH_COMMANDS } from "../server/commands.js";
|
|
31
32
|
import { packagePromptRootsOf } from "../commands/templates.js";
|
|
33
|
+
import { blocksToolsExtension } from "./blocks/tools.js";
|
|
32
34
|
import { codingToolsExtension, searchToolsExtension } from "./tools.js";
|
|
33
35
|
import { collectProviders, createProviderMergeSink, } from "./providers/index.js";
|
|
34
36
|
import { EventHub } from "../server/events/hub.js";
|
|
@@ -36,10 +38,24 @@ import { providerAuthStatus } from "../server/helpers.js";
|
|
|
36
38
|
import { bootRefresh, DEFAULT_REFRESH_TIMEOUT_MS, refreshProvider, } from "./providers/refresh.js";
|
|
37
39
|
import { startProviderRefresh } from "./providers/scheduler.js";
|
|
38
40
|
import { fileModelsStore } from "./providers/models-store.js";
|
|
39
|
-
import { packagesDirOf } from "./packages.js";
|
|
41
|
+
import { packagesDirOf, selfPackageRoot } from "./packages.js";
|
|
42
|
+
import { resolveHelper } from "../sandbox/helper.js";
|
|
43
|
+
import { launchInJail } from "../sandbox/launch.js";
|
|
44
|
+
import { buildUserPolicy, defaultModuleTrees } from "../sandbox/policy.js";
|
|
45
|
+
import { lookupOsAccount } from "../sandbox/os-account.js";
|
|
46
|
+
import { agentEnv } from "./agent-env.js";
|
|
47
|
+
import { UserServices } from "./user-services.js";
|
|
48
|
+
import { resolveWorkerEntry, sandboxLauncher, WorkerPool } from "../sandbox/pool.js";
|
|
49
|
+
import { IsolationState } from "../sandbox/isolation.js";
|
|
50
|
+
import { probeOsUsers } from "../sandbox/availability.js";
|
|
51
|
+
import { createAdmissionGuard } from "./identity/users.js";
|
|
52
|
+
import { SessionMetaDoc } from "../server/session-meta.js";
|
|
53
|
+
import { probeIsolation, runHelperProbe, selfTestHelper, } from "../sandbox/availability.js";
|
|
40
54
|
import { resolveBuiltins, } from "./builtins.js";
|
|
41
55
|
import { createRegistry } from "./registry.js";
|
|
56
|
+
import { isUserEntryName, rewriteSelection, sweepUserSelections, UserToolRegistry, } from "./user-tools.js";
|
|
42
57
|
import { defaultSettingsDir, SettingsStore } from "./settings.js";
|
|
58
|
+
import { resolveRefreshMinutes, snapshotRestartKeys, } from "../settings/pending.js";
|
|
43
59
|
import { ensureStoreRoots, resolveStoreRoots, sharedExtensionStores, } from "./stores.js";
|
|
44
60
|
import { createWorkspaceService, defaultWorkspacePrefs, toHarnessEnv, } from "./workspace.js";
|
|
45
61
|
/** The idle sweep cadence (spec §2: ONE host-owned 1s interval). */
|
|
@@ -93,6 +109,35 @@ export async function openHarness(config, options = {}) {
|
|
|
93
109
|
// store-root resolution immediately below, and the provider
|
|
94
110
|
// collector further down (`providerEnv`).
|
|
95
111
|
const env = options.env ?? process.env;
|
|
112
|
+
const coreRoot = selfPackageRoot();
|
|
113
|
+
const isolation = await (options.isolationProbe ?? probeIsolation)({
|
|
114
|
+
platform: process.platform,
|
|
115
|
+
helper: coreRoot === undefined ? undefined : resolveHelper(env, coreRoot),
|
|
116
|
+
runProbe: runHelperProbe,
|
|
117
|
+
selfTest: selfTestHelper,
|
|
118
|
+
});
|
|
119
|
+
console.log(`sandbox isolation: ${isolation.available ? `available (Landlock ABI ${isolation.abi})` : `unavailable (${isolation.reason ?? "unknown reason"})`}`);
|
|
120
|
+
if (coreRoot === undefined)
|
|
121
|
+
throw new Error("sandbox worker entry requires the sigrun package root");
|
|
122
|
+
const workerEntry = resolveWorkerEntry(coreRoot);
|
|
123
|
+
const helper = isolation.helper;
|
|
124
|
+
const osUsers = !config.isolation.osUsers
|
|
125
|
+
? { available: false, status: "off" }
|
|
126
|
+
: options.osUsersProbe !== undefined
|
|
127
|
+
? await options.osUsersProbe(helper ?? "")
|
|
128
|
+
: helper === undefined || !isolation.available
|
|
129
|
+
? { available: false, status: "osUsers requires CAP_SETUID" }
|
|
130
|
+
: await probeOsUsers(helper);
|
|
131
|
+
const sandboxPool = new WorkerPool({
|
|
132
|
+
launcher: options.sandboxLauncher ?? ((policy) => {
|
|
133
|
+
if (helper === undefined)
|
|
134
|
+
throw new Error("sandbox helper not found");
|
|
135
|
+
return sandboxLauncher(helper, workerEntry)(policy);
|
|
136
|
+
}),
|
|
137
|
+
idleMinutes: () => settingsStore?.read().server?.isolation?.workerIdleMinutes ?? config.isolation.workerIdleMinutes,
|
|
138
|
+
log: (line) => console.warn(line),
|
|
139
|
+
workerEntry,
|
|
140
|
+
});
|
|
96
141
|
// Store-root lifecycle (spec §6): every durable root except cache
|
|
97
142
|
// exists before anything touches disk — storage, the registry's
|
|
98
143
|
// first install, extension packages — so a fresh deploy (or a PVC
|
|
@@ -124,6 +169,33 @@ export async function openHarness(config, options = {}) {
|
|
|
124
169
|
// I/O here (each file materializes on its first write), so this is
|
|
125
170
|
// pure construction over paths boot already owns.
|
|
126
171
|
const identity = options.identity ?? openIdentityStores(roots);
|
|
172
|
+
const listenHost = options.listenHost ?? env.SIGRUN_LISTEN_HOST ?? "0.0.0.0";
|
|
173
|
+
const methods = effectiveMethods(config.auth, listenHost);
|
|
174
|
+
const isolationState = new IsolationState({
|
|
175
|
+
mode: config.isolation.mode,
|
|
176
|
+
availability: isolation,
|
|
177
|
+
userCount: () => identity.users.list().length,
|
|
178
|
+
log: (line) => console.warn(line),
|
|
179
|
+
});
|
|
180
|
+
// These refusals happen during openHarness, before createServer/listen.
|
|
181
|
+
if (identity.users.list().length >= 2 && methods.length === 0) {
|
|
182
|
+
throw new Error("multi-user requires authentication: configure SIGRUN_AUTH=token or SIGRUN_AUTH=proxy, or remove users until only one remains");
|
|
183
|
+
}
|
|
184
|
+
if ((isolationState.active() || config.isolation.mode === "always") &&
|
|
185
|
+
methods.includes("proxy") &&
|
|
186
|
+
config.auth.proxy.secretHeader === undefined) {
|
|
187
|
+
throw new Error("isolation with proxy authentication requires server.auth.proxy.secretHeader; configure a shared-secret header or disable isolation");
|
|
188
|
+
}
|
|
189
|
+
identity.users.configureAdmission({
|
|
190
|
+
firstWorkspace: config.workspace.path,
|
|
191
|
+
admit: createAdmissionGuard({
|
|
192
|
+
isolation,
|
|
193
|
+
mode: config.isolation.mode,
|
|
194
|
+
methods,
|
|
195
|
+
proxySecretHeader: config.auth.proxy.secretHeader,
|
|
196
|
+
}),
|
|
197
|
+
});
|
|
198
|
+
isolationState.active();
|
|
127
199
|
// The unclaimed onboarding posture (W1 Task 4, spec §5.8): every
|
|
128
200
|
// unclaimed boot (re)ensures the setup code under the pod-local cache
|
|
129
201
|
// root and logs it ONCE — the retrieval flow is `kubectl logs … | grep
|
|
@@ -140,8 +212,63 @@ export async function openHarness(config, options = {}) {
|
|
|
140
212
|
const code = await ensureSetupCode(identity.cacheDir, env);
|
|
141
213
|
console.log(`sigrun setup code: ${code}`);
|
|
142
214
|
}
|
|
215
|
+
const managedPaths = {
|
|
216
|
+
roots,
|
|
217
|
+
storagePath: config.storagePath,
|
|
218
|
+
coreRoot,
|
|
219
|
+
userAreas: (user) => join(roots.dataDir, "users", user),
|
|
220
|
+
};
|
|
221
|
+
const userServices = new UserServices({
|
|
222
|
+
launch: (policy, command, args, serviceEnv) => {
|
|
223
|
+
if (helper === undefined)
|
|
224
|
+
throw new Error("sandbox helper not found");
|
|
225
|
+
return launchInJail(helper, policy, command, args, serviceEnv);
|
|
226
|
+
},
|
|
227
|
+
policyFor: (user) => {
|
|
228
|
+
const record = identity.users.get(user);
|
|
229
|
+
if (record === undefined)
|
|
230
|
+
return { reason: `unknown user "${user}"` };
|
|
231
|
+
let osAccount;
|
|
232
|
+
if (record.osUser !== undefined) {
|
|
233
|
+
if (!osUsers.available)
|
|
234
|
+
return { reason: "osUser requires CAP_SETUID" };
|
|
235
|
+
try {
|
|
236
|
+
osAccount = lookupOsAccount(record.osUser);
|
|
237
|
+
}
|
|
238
|
+
catch {
|
|
239
|
+
return { reason: `OS account not found: ${record.osUser}` };
|
|
240
|
+
}
|
|
241
|
+
if (osAccount === undefined)
|
|
242
|
+
return { reason: `OS account not found: ${record.osUser}` };
|
|
243
|
+
}
|
|
244
|
+
const built = buildUserPolicy(record, {
|
|
245
|
+
managed: managedPaths,
|
|
246
|
+
nodeBinDir: process.execPath,
|
|
247
|
+
moduleTrees: defaultModuleTrees(coreRoot),
|
|
248
|
+
...(osAccount === undefined ? {} : { osAccount }),
|
|
249
|
+
});
|
|
250
|
+
if (!built.ok)
|
|
251
|
+
return { reason: built.reason };
|
|
252
|
+
if (!isolationState.active()) {
|
|
253
|
+
const serviceEnv = agentEnv(env);
|
|
254
|
+
delete serviceEnv.SSH_AUTH_SOCK;
|
|
255
|
+
serviceEnv.HOME = built.policy.home;
|
|
256
|
+
serviceEnv.TMPDIR = built.policy.tmp;
|
|
257
|
+
serviceEnv.USER = user;
|
|
258
|
+
return { ...built.policy, env: serviceEnv };
|
|
259
|
+
}
|
|
260
|
+
return built.policy;
|
|
261
|
+
},
|
|
262
|
+
isolationActive: () => isolationState.active(),
|
|
263
|
+
now: () => Date.now(),
|
|
264
|
+
managed: managedPaths,
|
|
265
|
+
});
|
|
143
266
|
const storage = await openNodeSqliteStorage(config.storagePath);
|
|
144
267
|
const registry = createRegistry();
|
|
268
|
+
// Late-filled by the loader: factories publish through the same registry
|
|
269
|
+
// instance, and selection rewrites read the declared set LIVE.
|
|
270
|
+
const perUserSlugs = [];
|
|
271
|
+
const userTools = new UserToolRegistry(registry, () => perUserSlugs);
|
|
145
272
|
// The skills convention's package roots (W5 Task 4, spec §9.4): a LATE
|
|
146
273
|
// array — the extension packages that contribute skill dirs load BELOW
|
|
147
274
|
// (after this install), and the section renders per REQUEST, so the
|
|
@@ -203,6 +330,10 @@ export async function openHarness(config, options = {}) {
|
|
|
203
330
|
registry.install(createSkillsExtension(skillsOptions));
|
|
204
331
|
registry.install(codingToolsExtension);
|
|
205
332
|
registry.install(searchToolsExtension);
|
|
333
|
+
// The chat-blocks tools (UI-blocks U5 Task 1, spec §8.1): every agent
|
|
334
|
+
// can show and update interactive block surfaces in chat — the same
|
|
335
|
+
// core-owned slot as the coding/search tools, never quarantined.
|
|
336
|
+
registry.install(blocksToolsExtension);
|
|
206
337
|
// Extension packages load before `Harness.open` reads the registry
|
|
207
338
|
// (spec §3 boot order). Failures quarantine and boot continues. §3
|
|
208
339
|
// factory-result producers are retained alongside the outcomes (Task 9e).
|
|
@@ -210,17 +341,31 @@ export async function openHarness(config, options = {}) {
|
|
|
210
341
|
// loader resolves against FIRST (the settings file's `packages`
|
|
211
342
|
// declarations install here — see src/harness/packages.ts).
|
|
212
343
|
const packagesDir = options.packagesDir ?? packagesDirOf(options.settingsDir ?? defaultSettingsDir());
|
|
213
|
-
// Built-in extensions
|
|
214
|
-
// search ship with core. Resolution needs the settings FILE (the
|
|
344
|
+
// Built-in extensions ship with core. Resolution needs the settings FILE (the
|
|
215
345
|
// `builtinExtensions` toggles live there), so the store is constructed
|
|
216
346
|
// HERE — before the loader — and the one instance threads on to the
|
|
217
347
|
// rest of boot (its `harnessSettings()` call below stays the single
|
|
218
|
-
// construction).
|
|
219
|
-
|
|
348
|
+
// construction). The onWrite observer (U4 §13.2) fires `settingsStore`
|
|
349
|
+
// writes' change events through a boot-owned sink assigned below (the
|
|
350
|
+
// hub does not exist yet here — a late-bound closure keeps the store
|
|
351
|
+
// constructor pure while every committed write still publishes).
|
|
352
|
+
const settingsChangedSinks = [];
|
|
353
|
+
const settingsStore = new SettingsStore({
|
|
354
|
+
dir: options.settingsDir,
|
|
355
|
+
onWrite: (keys) => {
|
|
356
|
+
for (const sink of settingsChangedSinks)
|
|
357
|
+
sink("server", keys);
|
|
358
|
+
},
|
|
359
|
+
});
|
|
220
360
|
// Eager validation (the fail-loud boot posture, matching the
|
|
221
361
|
// `harnessSettings()` contract below): a malformed settings file
|
|
222
362
|
// throws HERE even when the caller seam overrides every field.
|
|
223
363
|
const bootSettings = settingsStore.read();
|
|
364
|
+
// The pending-restart boot snapshot (U4 §13.2): taken ONCE, after the
|
|
365
|
+
// settings load validates and before anything else reads it — the
|
|
366
|
+
// diff base `GET /v1/settings`'s `pendingRestart` reports. Pre-listen
|
|
367
|
+
// by construction: openHarness returns before serve listens.
|
|
368
|
+
const settingsBootSnapshot = snapshotRestartKeys(bootSettings);
|
|
224
369
|
const builtinsEnabled = options.builtins !== false;
|
|
225
370
|
// Skipped entirely (no resolution, no log lines) when the caller
|
|
226
371
|
// turns built-ins off.
|
|
@@ -231,7 +376,7 @@ export async function openHarness(config, options = {}) {
|
|
|
231
376
|
options.extensionSpecifiers ??
|
|
232
377
|
config.extensions).map((specifier) => typeof specifier === "string" ? { entry: specifier } : specifier);
|
|
233
378
|
const builtins = builtinsEnabled
|
|
234
|
-
? resolveBuiltins(bootSettings, configuredInputs, options.extraBuiltins ?? [])
|
|
379
|
+
? resolveBuiltins(bootSettings, configuredInputs, options.extraBuiltins ?? [], options.builtinResolver)
|
|
235
380
|
: { load: [], skipped: [] };
|
|
236
381
|
// The loader input: built-ins FIRST (pi parity — the replacement rule
|
|
237
382
|
// already removed any built-in a configured entry claims), marked
|
|
@@ -274,10 +419,10 @@ export async function openHarness(config, options = {}) {
|
|
|
274
419
|
// the cache dir materializes lazily on first `paths.cache` access).
|
|
275
420
|
// The boot env feeds the admin surface's env-secret listing (the
|
|
276
421
|
// `SIGRUN_EXT_<SLUG>_` fallbacks — never written down, listed only).
|
|
277
|
-
const sharedStores = sharedExtensionStores(roots, env);
|
|
422
|
+
const sharedStores = sharedExtensionStores(roots, env, identity.users);
|
|
278
423
|
const seamHosts = new Map();
|
|
279
424
|
const seamKinds = new Map();
|
|
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, {
|
|
425
|
+
const { outcomes: extensionOutcomes, healthProducers: extensionHealth, runSettledBindings, settingsChangedBindings, lifecycleBindings, workerBindings, userServices: userServiceBindings, providerContributors, loginProviders, webuiBundles: extensionWebuiBundles, skillRoots: extensionSkillRoots, jobBindings, routeBindings, commandBindings, notifierBindings, channelBindings, gatewayEngine, surfaces: surfaceBindings, settingsSections: settingsSectionBindings, perUserSlugs: loadedPerUserSlugs, } = await loadExtensions(loaderInputs, registry, {
|
|
281
426
|
packagesDir,
|
|
282
427
|
hostContextFactory: (extensionName, kinds, priorHost) => {
|
|
283
428
|
// Two-phase, revised for the provisional identity (provider
|
|
@@ -294,7 +439,10 @@ export async function openHarness(config, options = {}) {
|
|
|
294
439
|
createSeamHost({ name: extensionName }, kinds, {
|
|
295
440
|
bindHarness,
|
|
296
441
|
harness: harnessOf,
|
|
297
|
-
}, sharedStores
|
|
442
|
+
}, sharedStores, {
|
|
443
|
+
declared: (slug) => perUserSlugs.includes(slug),
|
|
444
|
+
set: (slug, user, tools) => userTools.set(slug, user, tools),
|
|
445
|
+
}, userServices);
|
|
298
446
|
host.rekey({ name: extensionName }, kinds);
|
|
299
447
|
// File under the final name only, and only once the load
|
|
300
448
|
// resolved: the PRE-import call must NOT file (its identity is
|
|
@@ -307,7 +455,15 @@ export async function openHarness(config, options = {}) {
|
|
|
307
455
|
}
|
|
308
456
|
return host;
|
|
309
457
|
},
|
|
458
|
+
onQuarantine: (slug) => {
|
|
459
|
+
userTools.removeSlug(slug);
|
|
460
|
+
return userServices.quarantine(slug);
|
|
461
|
+
},
|
|
310
462
|
});
|
|
463
|
+
perUserSlugs.push(...loadedPerUserSlugs);
|
|
464
|
+
for (const binding of userServiceBindings) {
|
|
465
|
+
userServices.register(binding.extension, binding.services, binding.packageDir);
|
|
466
|
+
}
|
|
311
467
|
// The late fill (W5 Task 4): the skills extension installed BEFORE the
|
|
312
468
|
// loader ran, holding an empty package-roots array by reference — the
|
|
313
469
|
// collected roots land HERE, and the section (which renders per request)
|
|
@@ -434,6 +590,7 @@ export async function openHarness(config, options = {}) {
|
|
|
434
590
|
// engine loaded (core ships none — `gatewayEngine` is undefined
|
|
435
591
|
// unless an extension declared one).
|
|
436
592
|
gatewayLoaded: gatewayEngine !== undefined,
|
|
593
|
+
rewriteUserSelection: (tx, conversationId, from, to) => rewriteSelection(tx, conversationId, from, to, perUserSlugs),
|
|
437
594
|
};
|
|
438
595
|
registry.install({
|
|
439
596
|
name: "sigrun.jobs",
|
|
@@ -511,17 +668,39 @@ export async function openHarness(config, options = {}) {
|
|
|
511
668
|
// when the resolved interval is greater than zero (0 = the designed
|
|
512
669
|
// disable, no timer); the timer is unref()ed inside, and `stop()` rides
|
|
513
670
|
// on the returned handle for the deploy shutdown path.
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
671
|
+
// The interval refresh scheduler (with the U4 §13.1 re-arm): the
|
|
672
|
+
// cadence is a LIVE setting — a committed write to
|
|
673
|
+
// `server.providers.refreshIntervalMinutes` re-arms the timer at the
|
|
674
|
+
// NEW interval (stop + start; `0` disables, no timer). The re-arm
|
|
675
|
+
// resolves env over the fresh file value exactly like boot does, so
|
|
676
|
+
// an env-pinned cadence (`SIGRUN_PROVIDER_REFRESH_MINUTES`) stays
|
|
677
|
+
// authoritative across re-arms.
|
|
678
|
+
const armProviderRefresh = () => {
|
|
679
|
+
const minutes = resolveRefreshMinutes(env, settingsStore);
|
|
680
|
+
if (!(minutes > 0))
|
|
681
|
+
return () => { };
|
|
682
|
+
return startProviderRefresh(models, {
|
|
683
|
+
intervalMinutes: minutes,
|
|
517
684
|
onReadinessChange: observeReadiness,
|
|
518
685
|
snapshotReadiness,
|
|
519
|
-
}).stop
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
//
|
|
523
|
-
//
|
|
524
|
-
|
|
686
|
+
}).stop;
|
|
687
|
+
};
|
|
688
|
+
let stopProviderRefresh = armProviderRefresh();
|
|
689
|
+
// SAFETY: `stopProviderRefresh` is `let`-bound for the re-arm swap;
|
|
690
|
+
// the shutdown handle the deploy path calls stays this function (it
|
|
691
|
+
// always stops whatever arm is current).
|
|
692
|
+
const stopCurrentProviderRefresh = () => {
|
|
693
|
+
stopProviderRefresh();
|
|
694
|
+
};
|
|
695
|
+
// Every tool/file/upload/job env resolves through this one decision seam.
|
|
696
|
+
const workspaceService = createWorkspaceService({
|
|
697
|
+
isolation: isolationState,
|
|
698
|
+
osUsers,
|
|
699
|
+
pool: sandboxPool,
|
|
700
|
+
directory: identity.users,
|
|
701
|
+
managed: managedPaths,
|
|
702
|
+
ownerOf: async (conversationId, read, ctx) => (await read.snapshot(SessionMetaDoc, conversationId, ctx))?.owner,
|
|
703
|
+
});
|
|
525
704
|
const workspacePrefs = defaultWorkspacePrefs(config.workspace.path);
|
|
526
705
|
const envBuilder = toHarnessEnv(workspaceService, workspacePrefs);
|
|
527
706
|
// The job runtime's env-builder binding resolves here (the placeholder
|
|
@@ -572,7 +751,13 @@ export async function openHarness(config, options = {}) {
|
|
|
572
751
|
get stream() {
|
|
573
752
|
return callerSettings?.stream ?? fileSettings.stream;
|
|
574
753
|
},
|
|
575
|
-
extensions
|
|
754
|
+
get extensions() {
|
|
755
|
+
// Per-user entries are core-computed from SessionMeta.owner,
|
|
756
|
+
// never part of a host default — including an operator-supplied
|
|
757
|
+
// caller list (controller ruling: filter BOTH sources).
|
|
758
|
+
const selected = callerSettings?.extensions ?? registry.snapshot().installed();
|
|
759
|
+
return selected.filter((entry) => !isUserEntryName(entry.name));
|
|
760
|
+
},
|
|
576
761
|
toolExecution: callerSettings?.toolExecution,
|
|
577
762
|
compaction: {
|
|
578
763
|
...config.compaction,
|
|
@@ -580,6 +765,11 @@ export async function openHarness(config, options = {}) {
|
|
|
580
765
|
},
|
|
581
766
|
},
|
|
582
767
|
}, BACKGROUND_CONTEXT);
|
|
768
|
+
// M2 boot sweep: add each conversation owner's declared entries to every
|
|
769
|
+
// default-edited selection. Exact arrays are never widened. Runs before
|
|
770
|
+
// resume and is idempotent; late tool installs still resolve by name.
|
|
771
|
+
const userToolSweep = await sweepUserSelections(harness, storage, perUserSlugs);
|
|
772
|
+
console.log(`user tools: boot sweep scanned ${userToolSweep.scanned} conversation(s), changed ${userToolSweep.changed}`);
|
|
583
773
|
// The seam hosts' lazy binding resolves here: every seam call issued
|
|
584
774
|
// after boot completes finds the harness immediately; one issued from a
|
|
585
775
|
// post-open async factory path waits on this same promise.
|
|
@@ -608,12 +798,35 @@ export async function openHarness(config, options = {}) {
|
|
|
608
798
|
idleThresholdMs: options.eventBus?.idleThresholdMs ?? memorySeam.idleThresholdMs,
|
|
609
799
|
idleSweepMs: options.eventBus?.idleSweepMs ?? IDLE_SWEEP_MS,
|
|
610
800
|
workerDeadlineMs: options.eventBus?.workerDeadlineMs ?? memorySeam.workerDeadlineMs,
|
|
801
|
+
shutdownSettleWaitMs: options.eventBus?.shutdownSettleWaitMs,
|
|
611
802
|
// W3 Task 4: the hub's subscription is installed at open —
|
|
612
803
|
// synchronous, no-blocking-work (the SSE route's filtering side
|
|
613
804
|
// lane does its own async work). Exposed so the bus's open() can
|
|
614
805
|
// wire it before any commit lands.
|
|
615
806
|
onHostEvent: (event) => events.publish(event),
|
|
616
807
|
});
|
|
808
|
+
// The settings-changed publication (U4 §13.2): ONE seam fanning a
|
|
809
|
+
// committed settings write out to (a) the hub — /v1/events delivers
|
|
810
|
+
// `settings.changed` to admins — and (b) every extension's
|
|
811
|
+
// `onSettingsChanged` through the bus's bounded fan-out (rejections
|
|
812
|
+
// logged, never propagated: §13.2's the-write-still-succeeds rule).
|
|
813
|
+
// The scheduler re-arm rides the same seam: a write touching
|
|
814
|
+
// `server.providers.refreshIntervalMinutes` (a LIVE key, §13.1) stops
|
|
815
|
+
// and re-arms the interval timer at the fresh cadence.
|
|
816
|
+
const publishSettingsChanged = (scope, keys, user) => {
|
|
817
|
+
events.publish({ type: "settings.changed", scope, keys: [...keys] });
|
|
818
|
+
if (keys.includes("server.providers.refreshIntervalMinutes") ||
|
|
819
|
+
keys.includes("server.providers") ||
|
|
820
|
+
keys.includes("server")) {
|
|
821
|
+
stopProviderRefresh();
|
|
822
|
+
stopProviderRefresh = armProviderRefresh();
|
|
823
|
+
}
|
|
824
|
+
if (user !== undefined && typeof scope !== "string") {
|
|
825
|
+
userServices.settingsChanged(scope.extension, user);
|
|
826
|
+
}
|
|
827
|
+
void eventBus.emitSettingsChanged(keys, user);
|
|
828
|
+
};
|
|
829
|
+
settingsChangedSinks.push(publishSettingsChanged);
|
|
617
830
|
// Activity stamping (UI refresh A1): every settled run stamps its
|
|
618
831
|
// conversation's `lastActivityAt` (the admission-side stamp lives in the
|
|
619
832
|
// submissions route). Registered directly after the bus opens — host-owned
|
|
@@ -628,6 +841,14 @@ export async function openHarness(config, options = {}) {
|
|
|
628
841
|
runSettled: binding.runSettled,
|
|
629
842
|
});
|
|
630
843
|
}
|
|
844
|
+
// UI-blocks U4 §13.2: the settings-change hook merges into the same
|
|
845
|
+
// per-extension registration (the fan-out below invokes it).
|
|
846
|
+
for (const binding of settingsChangedBindings) {
|
|
847
|
+
handlersByName.set(binding.extension.name, {
|
|
848
|
+
...handlersByName.get(binding.extension.name),
|
|
849
|
+
onSettingsChanged: binding.onSettingsChanged,
|
|
850
|
+
});
|
|
851
|
+
}
|
|
631
852
|
for (const binding of lifecycleBindings) {
|
|
632
853
|
handlersByName.set(binding.extension.name, {
|
|
633
854
|
...handlersByName.get(binding.extension.name),
|
|
@@ -824,8 +1045,16 @@ export async function openHarness(config, options = {}) {
|
|
|
824
1045
|
harness.resume();
|
|
825
1046
|
return {
|
|
826
1047
|
harness,
|
|
1048
|
+
isolation,
|
|
1049
|
+
isolationState,
|
|
1050
|
+
osUsers,
|
|
1051
|
+
sandboxPool,
|
|
1052
|
+
userServices,
|
|
827
1053
|
storage,
|
|
828
1054
|
registry,
|
|
1055
|
+
userTools,
|
|
1056
|
+
rewriteUserSelection: (tx, conversationId, from, to) => rewriteSelection(tx, conversationId, from, to, perUserSlugs),
|
|
1057
|
+
userToolSweep,
|
|
829
1058
|
models,
|
|
830
1059
|
jobs,
|
|
831
1060
|
jobRunTotals: () => jobRunTotals,
|
|
@@ -835,6 +1064,7 @@ export async function openHarness(config, options = {}) {
|
|
|
835
1064
|
webuiBundleCollisions,
|
|
836
1065
|
envBuilder,
|
|
837
1066
|
extensionOutcomes,
|
|
1067
|
+
loginProviders,
|
|
838
1068
|
builtinSkipped: builtinsEnabled ? builtins.skipped : [],
|
|
839
1069
|
providerHealth,
|
|
840
1070
|
providerOwnership,
|
|
@@ -855,6 +1085,7 @@ export async function openHarness(config, options = {}) {
|
|
|
855
1085
|
extensionRoutes: routeBindings,
|
|
856
1086
|
extensionCommands: commandBindings,
|
|
857
1087
|
extensionSurfaces: surfaceBindings,
|
|
1088
|
+
extensionSettingsSections: settingsSectionBindings,
|
|
858
1089
|
notifierBindings,
|
|
859
1090
|
channelBindings,
|
|
860
1091
|
gatewayEngine,
|
|
@@ -867,6 +1098,8 @@ export async function openHarness(config, options = {}) {
|
|
|
867
1098
|
autoNamer,
|
|
868
1099
|
bootRefresh: bootRefreshDone,
|
|
869
1100
|
refreshProvider: refreshOneProvider,
|
|
870
|
-
stopProviderRefresh,
|
|
1101
|
+
stopProviderRefresh: stopCurrentProviderRefresh,
|
|
1102
|
+
settingsBootSnapshot,
|
|
1103
|
+
publishSettingsChanged,
|
|
871
1104
|
};
|
|
872
1105
|
}
|
package/dist/harness/builtins.js
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Built-in extensions
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* Built-in extensions: codemode, MCP, tool search and OAuth ship WITH
|
|
3
|
+
* core — present by default, disableable via settings `builtinExtensions`,
|
|
4
|
+
* replaceable by a same-named configured extension.
|
|
5
5
|
*
|
|
6
|
-
* pi 1.3.0
|
|
7
|
-
*
|
|
8
|
-
* without configuring anything.
|
|
6
|
+
* The first three mirror pi 1.3.0's built-in tool surface; OAuth adds
|
|
7
|
+
* sigrun's standard OIDC/OAuth2 login providers without extra setup.
|
|
9
8
|
*
|
|
10
9
|
* Resolution root (Ruling 2): built-ins resolve through core's OWN
|
|
11
10
|
* node_modules via `createRequire(import.meta.url)` — they are core
|
|
@@ -24,7 +23,7 @@
|
|
|
24
23
|
* (`readPackageName` for path-shaped entries, `parsePackageSpecifier`
|
|
25
24
|
* otherwise), never by string equality on the raw specifier.
|
|
26
25
|
*
|
|
27
|
-
* Disabled (Ruling 6): `builtinExtensions: {codemode?, mcp?, toolSearch?}`
|
|
26
|
+
* Disabled (Ruling 6): `builtinExtensions: {codemode?, mcp?, toolSearch?, oauth?}`
|
|
28
27
|
* (each defaulting to `true`, restart-required). A disabled built-in is
|
|
29
28
|
* absent from `/v1/extensions` entirely; one log line records the skip.
|
|
30
29
|
*
|
|
@@ -39,7 +38,7 @@ import { basename, dirname, isAbsolute, resolve } from "node:path";
|
|
|
39
38
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
40
39
|
import { parsePackageSpecifier, readPackageName } from "./packages.js";
|
|
41
40
|
/**
|
|
42
|
-
* The
|
|
41
|
+
* The four built-ins, in load order (codemode before tool-search: both
|
|
43
42
|
* wrap pi-durable's own extensions and neither depends on the other, but
|
|
44
43
|
* the order pins the tool-collision story should names ever overlap).
|
|
45
44
|
* Specifiers are the published package names (Task 3) — npm keys a file:
|
|
@@ -51,6 +50,8 @@ export const BUILTIN_EXTENSIONS = [
|
|
|
51
50
|
{ key: "codemode", specifier: "@sigrun/ext-codemode", slug: "codemode" },
|
|
52
51
|
{ key: "mcp", specifier: "@sigrun/ext-mcp", slug: "sigrun-mcp" },
|
|
53
52
|
{ key: "toolSearch", specifier: "@sigrun/ext-tool-search", slug: "tool-search" },
|
|
53
|
+
{ key: "gateway", specifier: "@sigrun/ext-gateway", slug: "gateway" },
|
|
54
|
+
{ key: "oauth", specifier: "@sigrun/oauth", slug: "oauth" },
|
|
54
55
|
];
|
|
55
56
|
/** The core-anchored require (built-ins resolve from core's own node_modules). */
|
|
56
57
|
const coreRequire = createRequire(import.meta.url);
|
|
@@ -154,7 +155,10 @@ function configuredPackageName(entry) {
|
|
|
154
155
|
*/
|
|
155
156
|
export function resolveBuiltins(settings, configured,
|
|
156
157
|
/** Test seam: additional built-in specifiers (fixtures), resolved like the real ones. */
|
|
157
|
-
extra = []
|
|
158
|
+
extra = [],
|
|
159
|
+
/** Test seam: replace core resolution to exercise an unresolvable built-in. */
|
|
160
|
+
resolveEntry) {
|
|
161
|
+
const resolve = resolveEntry ?? ((specifier) => builtinEntry(specifier));
|
|
158
162
|
const toggles = settings.builtinExtensions;
|
|
159
163
|
const load = [];
|
|
160
164
|
const skipped = [];
|
|
@@ -179,13 +183,13 @@ extra = []) {
|
|
|
179
183
|
skipped.push({ key: builtin.key, reason: "replaced", by: replacement });
|
|
180
184
|
continue;
|
|
181
185
|
}
|
|
182
|
-
load.push(
|
|
186
|
+
load.push(resolve(builtin.specifier, builtinEntry));
|
|
183
187
|
}
|
|
184
188
|
// The test-seam extra entries (the boot's `extraBuiltins` option):
|
|
185
189
|
// resolved exactly like the real built-ins, appended after them,
|
|
186
190
|
// reported builtin.
|
|
187
191
|
for (const specifier of extra) {
|
|
188
|
-
load.push(
|
|
192
|
+
load.push(resolve(specifier, builtinEntry));
|
|
189
193
|
}
|
|
190
194
|
return { load, skipped };
|
|
191
195
|
}
|