@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/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,39 @@ 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
|
+
// The caller-supplied host wins (serving spec §1: `serve()` resolves
|
|
173
|
+
// the ONE effective host — options.host → SIGRUN_LISTEN_HOST →
|
|
174
|
+
// DEFAULT_LISTEN_HOST — and passes it here). The env read below is
|
|
175
|
+
// NOT a second source on the serve path (the option is always set
|
|
176
|
+
// there); it remains for direct `openHarness` callers that never
|
|
177
|
+
// bind a listener (tests) and must derive the same auth posture.
|
|
178
|
+
const listenHost = options.listenHost ?? env.SIGRUN_LISTEN_HOST ?? "0.0.0.0";
|
|
179
|
+
const methods = effectiveMethods(config.auth, listenHost);
|
|
180
|
+
const isolationState = new IsolationState({
|
|
181
|
+
mode: config.isolation.mode,
|
|
182
|
+
availability: isolation,
|
|
183
|
+
userCount: () => identity.users.list().length,
|
|
184
|
+
log: (line) => console.warn(line),
|
|
185
|
+
});
|
|
186
|
+
// These refusals happen during openHarness, before createServer/listen.
|
|
187
|
+
if (identity.users.list().length >= 2 && methods.length === 0) {
|
|
188
|
+
throw new Error("multi-user requires authentication: configure SIGRUN_AUTH=token or SIGRUN_AUTH=proxy, or remove users until only one remains");
|
|
189
|
+
}
|
|
190
|
+
if ((isolationState.active() || config.isolation.mode === "always") &&
|
|
191
|
+
methods.includes("proxy") &&
|
|
192
|
+
config.auth.proxy.secretHeader === undefined) {
|
|
193
|
+
throw new Error("isolation with proxy authentication requires server.auth.proxy.secretHeader; configure a shared-secret header or disable isolation");
|
|
194
|
+
}
|
|
195
|
+
identity.users.configureAdmission({
|
|
196
|
+
firstWorkspace: config.workspace.path,
|
|
197
|
+
admit: createAdmissionGuard({
|
|
198
|
+
isolation,
|
|
199
|
+
mode: config.isolation.mode,
|
|
200
|
+
methods,
|
|
201
|
+
proxySecretHeader: config.auth.proxy.secretHeader,
|
|
202
|
+
}),
|
|
203
|
+
});
|
|
204
|
+
isolationState.active();
|
|
127
205
|
// The unclaimed onboarding posture (W1 Task 4, spec §5.8): every
|
|
128
206
|
// unclaimed boot (re)ensures the setup code under the pod-local cache
|
|
129
207
|
// root and logs it ONCE — the retrieval flow is `kubectl logs … | grep
|
|
@@ -140,8 +218,63 @@ export async function openHarness(config, options = {}) {
|
|
|
140
218
|
const code = await ensureSetupCode(identity.cacheDir, env);
|
|
141
219
|
console.log(`sigrun setup code: ${code}`);
|
|
142
220
|
}
|
|
221
|
+
const managedPaths = {
|
|
222
|
+
roots,
|
|
223
|
+
storagePath: config.storagePath,
|
|
224
|
+
coreRoot,
|
|
225
|
+
userAreas: (user) => join(roots.dataDir, "users", user),
|
|
226
|
+
};
|
|
227
|
+
const userServices = new UserServices({
|
|
228
|
+
launch: (policy, command, args, serviceEnv) => {
|
|
229
|
+
if (helper === undefined)
|
|
230
|
+
throw new Error("sandbox helper not found");
|
|
231
|
+
return launchInJail(helper, policy, command, args, serviceEnv);
|
|
232
|
+
},
|
|
233
|
+
policyFor: (user) => {
|
|
234
|
+
const record = identity.users.get(user);
|
|
235
|
+
if (record === undefined)
|
|
236
|
+
return { reason: `unknown user "${user}"` };
|
|
237
|
+
let osAccount;
|
|
238
|
+
if (record.osUser !== undefined) {
|
|
239
|
+
if (!osUsers.available)
|
|
240
|
+
return { reason: "osUser requires CAP_SETUID" };
|
|
241
|
+
try {
|
|
242
|
+
osAccount = lookupOsAccount(record.osUser);
|
|
243
|
+
}
|
|
244
|
+
catch {
|
|
245
|
+
return { reason: `OS account not found: ${record.osUser}` };
|
|
246
|
+
}
|
|
247
|
+
if (osAccount === undefined)
|
|
248
|
+
return { reason: `OS account not found: ${record.osUser}` };
|
|
249
|
+
}
|
|
250
|
+
const built = buildUserPolicy(record, {
|
|
251
|
+
managed: managedPaths,
|
|
252
|
+
nodeBinDir: process.execPath,
|
|
253
|
+
moduleTrees: defaultModuleTrees(coreRoot),
|
|
254
|
+
...(osAccount === undefined ? {} : { osAccount }),
|
|
255
|
+
});
|
|
256
|
+
if (!built.ok)
|
|
257
|
+
return { reason: built.reason };
|
|
258
|
+
if (!isolationState.active()) {
|
|
259
|
+
const serviceEnv = agentEnv(env);
|
|
260
|
+
delete serviceEnv.SSH_AUTH_SOCK;
|
|
261
|
+
serviceEnv.HOME = built.policy.home;
|
|
262
|
+
serviceEnv.TMPDIR = built.policy.tmp;
|
|
263
|
+
serviceEnv.USER = user;
|
|
264
|
+
return { ...built.policy, env: serviceEnv };
|
|
265
|
+
}
|
|
266
|
+
return built.policy;
|
|
267
|
+
},
|
|
268
|
+
isolationActive: () => isolationState.active(),
|
|
269
|
+
now: () => Date.now(),
|
|
270
|
+
managed: managedPaths,
|
|
271
|
+
});
|
|
143
272
|
const storage = await openNodeSqliteStorage(config.storagePath);
|
|
144
273
|
const registry = createRegistry();
|
|
274
|
+
// Late-filled by the loader: factories publish through the same registry
|
|
275
|
+
// instance, and selection rewrites read the declared set LIVE.
|
|
276
|
+
const perUserSlugs = [];
|
|
277
|
+
const userTools = new UserToolRegistry(registry, () => perUserSlugs);
|
|
145
278
|
// The skills convention's package roots (W5 Task 4, spec §9.4): a LATE
|
|
146
279
|
// array — the extension packages that contribute skill dirs load BELOW
|
|
147
280
|
// (after this install), and the section renders per REQUEST, so the
|
|
@@ -203,6 +336,10 @@ export async function openHarness(config, options = {}) {
|
|
|
203
336
|
registry.install(createSkillsExtension(skillsOptions));
|
|
204
337
|
registry.install(codingToolsExtension);
|
|
205
338
|
registry.install(searchToolsExtension);
|
|
339
|
+
// The chat-blocks tools (UI-blocks U5 Task 1, spec §8.1): every agent
|
|
340
|
+
// can show and update interactive block surfaces in chat — the same
|
|
341
|
+
// core-owned slot as the coding/search tools, never quarantined.
|
|
342
|
+
registry.install(blocksToolsExtension);
|
|
206
343
|
// Extension packages load before `Harness.open` reads the registry
|
|
207
344
|
// (spec §3 boot order). Failures quarantine and boot continues. §3
|
|
208
345
|
// factory-result producers are retained alongside the outcomes (Task 9e).
|
|
@@ -210,17 +347,31 @@ export async function openHarness(config, options = {}) {
|
|
|
210
347
|
// loader resolves against FIRST (the settings file's `packages`
|
|
211
348
|
// declarations install here — see src/harness/packages.ts).
|
|
212
349
|
const packagesDir = options.packagesDir ?? packagesDirOf(options.settingsDir ?? defaultSettingsDir());
|
|
213
|
-
// Built-in extensions
|
|
214
|
-
// search ship with core. Resolution needs the settings FILE (the
|
|
350
|
+
// Built-in extensions ship with core. Resolution needs the settings FILE (the
|
|
215
351
|
// `builtinExtensions` toggles live there), so the store is constructed
|
|
216
352
|
// HERE — before the loader — and the one instance threads on to the
|
|
217
353
|
// rest of boot (its `harnessSettings()` call below stays the single
|
|
218
|
-
// construction).
|
|
219
|
-
|
|
354
|
+
// construction). The onWrite observer (U4 §13.2) fires `settingsStore`
|
|
355
|
+
// writes' change events through a boot-owned sink assigned below (the
|
|
356
|
+
// hub does not exist yet here — a late-bound closure keeps the store
|
|
357
|
+
// constructor pure while every committed write still publishes).
|
|
358
|
+
const settingsChangedSinks = [];
|
|
359
|
+
const settingsStore = new SettingsStore({
|
|
360
|
+
dir: options.settingsDir,
|
|
361
|
+
onWrite: (keys) => {
|
|
362
|
+
for (const sink of settingsChangedSinks)
|
|
363
|
+
sink("server", keys);
|
|
364
|
+
},
|
|
365
|
+
});
|
|
220
366
|
// Eager validation (the fail-loud boot posture, matching the
|
|
221
367
|
// `harnessSettings()` contract below): a malformed settings file
|
|
222
368
|
// throws HERE even when the caller seam overrides every field.
|
|
223
369
|
const bootSettings = settingsStore.read();
|
|
370
|
+
// The pending-restart boot snapshot (U4 §13.2): taken ONCE, after the
|
|
371
|
+
// settings load validates and before anything else reads it — the
|
|
372
|
+
// diff base `GET /v1/settings`'s `pendingRestart` reports. Pre-listen
|
|
373
|
+
// by construction: openHarness returns before serve listens.
|
|
374
|
+
const settingsBootSnapshot = snapshotRestartKeys(bootSettings);
|
|
224
375
|
const builtinsEnabled = options.builtins !== false;
|
|
225
376
|
// Skipped entirely (no resolution, no log lines) when the caller
|
|
226
377
|
// turns built-ins off.
|
|
@@ -231,7 +382,7 @@ export async function openHarness(config, options = {}) {
|
|
|
231
382
|
options.extensionSpecifiers ??
|
|
232
383
|
config.extensions).map((specifier) => typeof specifier === "string" ? { entry: specifier } : specifier);
|
|
233
384
|
const builtins = builtinsEnabled
|
|
234
|
-
? resolveBuiltins(bootSettings, configuredInputs, options.extraBuiltins ?? [])
|
|
385
|
+
? resolveBuiltins(bootSettings, configuredInputs, options.extraBuiltins ?? [], options.builtinResolver)
|
|
235
386
|
: { load: [], skipped: [] };
|
|
236
387
|
// The loader input: built-ins FIRST (pi parity — the replacement rule
|
|
237
388
|
// already removed any built-in a configured entry claims), marked
|
|
@@ -274,10 +425,10 @@ export async function openHarness(config, options = {}) {
|
|
|
274
425
|
// the cache dir materializes lazily on first `paths.cache` access).
|
|
275
426
|
// The boot env feeds the admin surface's env-secret listing (the
|
|
276
427
|
// `SIGRUN_EXT_<SLUG>_` fallbacks — never written down, listed only).
|
|
277
|
-
const sharedStores = sharedExtensionStores(roots, env);
|
|
428
|
+
const sharedStores = sharedExtensionStores(roots, env, identity.users);
|
|
278
429
|
const seamHosts = new Map();
|
|
279
430
|
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, {
|
|
431
|
+
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
432
|
packagesDir,
|
|
282
433
|
hostContextFactory: (extensionName, kinds, priorHost) => {
|
|
283
434
|
// Two-phase, revised for the provisional identity (provider
|
|
@@ -294,7 +445,10 @@ export async function openHarness(config, options = {}) {
|
|
|
294
445
|
createSeamHost({ name: extensionName }, kinds, {
|
|
295
446
|
bindHarness,
|
|
296
447
|
harness: harnessOf,
|
|
297
|
-
}, sharedStores
|
|
448
|
+
}, sharedStores, {
|
|
449
|
+
declared: (slug) => perUserSlugs.includes(slug),
|
|
450
|
+
set: (slug, user, tools) => userTools.set(slug, user, tools),
|
|
451
|
+
}, userServices);
|
|
298
452
|
host.rekey({ name: extensionName }, kinds);
|
|
299
453
|
// File under the final name only, and only once the load
|
|
300
454
|
// resolved: the PRE-import call must NOT file (its identity is
|
|
@@ -307,7 +461,15 @@ export async function openHarness(config, options = {}) {
|
|
|
307
461
|
}
|
|
308
462
|
return host;
|
|
309
463
|
},
|
|
464
|
+
onQuarantine: (slug) => {
|
|
465
|
+
userTools.removeSlug(slug);
|
|
466
|
+
return userServices.quarantine(slug);
|
|
467
|
+
},
|
|
310
468
|
});
|
|
469
|
+
perUserSlugs.push(...loadedPerUserSlugs);
|
|
470
|
+
for (const binding of userServiceBindings) {
|
|
471
|
+
userServices.register(binding.extension, binding.services, binding.packageDir);
|
|
472
|
+
}
|
|
311
473
|
// The late fill (W5 Task 4): the skills extension installed BEFORE the
|
|
312
474
|
// loader ran, holding an empty package-roots array by reference — the
|
|
313
475
|
// collected roots land HERE, and the section (which renders per request)
|
|
@@ -434,6 +596,7 @@ export async function openHarness(config, options = {}) {
|
|
|
434
596
|
// engine loaded (core ships none — `gatewayEngine` is undefined
|
|
435
597
|
// unless an extension declared one).
|
|
436
598
|
gatewayLoaded: gatewayEngine !== undefined,
|
|
599
|
+
rewriteUserSelection: (tx, conversationId, from, to) => rewriteSelection(tx, conversationId, from, to, perUserSlugs),
|
|
437
600
|
};
|
|
438
601
|
registry.install({
|
|
439
602
|
name: "sigrun.jobs",
|
|
@@ -511,17 +674,39 @@ export async function openHarness(config, options = {}) {
|
|
|
511
674
|
// when the resolved interval is greater than zero (0 = the designed
|
|
512
675
|
// disable, no timer); the timer is unref()ed inside, and `stop()` rides
|
|
513
676
|
// on the returned handle for the deploy shutdown path.
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
677
|
+
// The interval refresh scheduler (with the U4 §13.1 re-arm): the
|
|
678
|
+
// cadence is a LIVE setting — a committed write to
|
|
679
|
+
// `server.providers.refreshIntervalMinutes` re-arms the timer at the
|
|
680
|
+
// NEW interval (stop + start; `0` disables, no timer). The re-arm
|
|
681
|
+
// resolves env over the fresh file value exactly like boot does, so
|
|
682
|
+
// an env-pinned cadence (`SIGRUN_PROVIDER_REFRESH_MINUTES`) stays
|
|
683
|
+
// authoritative across re-arms.
|
|
684
|
+
const armProviderRefresh = () => {
|
|
685
|
+
const minutes = resolveRefreshMinutes(env, settingsStore);
|
|
686
|
+
if (!(minutes > 0))
|
|
687
|
+
return () => { };
|
|
688
|
+
return startProviderRefresh(models, {
|
|
689
|
+
intervalMinutes: minutes,
|
|
517
690
|
onReadinessChange: observeReadiness,
|
|
518
691
|
snapshotReadiness,
|
|
519
|
-
}).stop
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
//
|
|
523
|
-
//
|
|
524
|
-
|
|
692
|
+
}).stop;
|
|
693
|
+
};
|
|
694
|
+
let stopProviderRefresh = armProviderRefresh();
|
|
695
|
+
// SAFETY: `stopProviderRefresh` is `let`-bound for the re-arm swap;
|
|
696
|
+
// the shutdown handle the deploy path calls stays this function (it
|
|
697
|
+
// always stops whatever arm is current).
|
|
698
|
+
const stopCurrentProviderRefresh = () => {
|
|
699
|
+
stopProviderRefresh();
|
|
700
|
+
};
|
|
701
|
+
// Every tool/file/upload/job env resolves through this one decision seam.
|
|
702
|
+
const workspaceService = createWorkspaceService({
|
|
703
|
+
isolation: isolationState,
|
|
704
|
+
osUsers,
|
|
705
|
+
pool: sandboxPool,
|
|
706
|
+
directory: identity.users,
|
|
707
|
+
managed: managedPaths,
|
|
708
|
+
ownerOf: async (conversationId, read, ctx) => (await read.snapshot(SessionMetaDoc, conversationId, ctx))?.owner,
|
|
709
|
+
});
|
|
525
710
|
const workspacePrefs = defaultWorkspacePrefs(config.workspace.path);
|
|
526
711
|
const envBuilder = toHarnessEnv(workspaceService, workspacePrefs);
|
|
527
712
|
// The job runtime's env-builder binding resolves here (the placeholder
|
|
@@ -572,7 +757,13 @@ export async function openHarness(config, options = {}) {
|
|
|
572
757
|
get stream() {
|
|
573
758
|
return callerSettings?.stream ?? fileSettings.stream;
|
|
574
759
|
},
|
|
575
|
-
extensions
|
|
760
|
+
get extensions() {
|
|
761
|
+
// Per-user entries are core-computed from SessionMeta.owner,
|
|
762
|
+
// never part of a host default — including an operator-supplied
|
|
763
|
+
// caller list (controller ruling: filter BOTH sources).
|
|
764
|
+
const selected = callerSettings?.extensions ?? registry.snapshot().installed();
|
|
765
|
+
return selected.filter((entry) => !isUserEntryName(entry.name));
|
|
766
|
+
},
|
|
576
767
|
toolExecution: callerSettings?.toolExecution,
|
|
577
768
|
compaction: {
|
|
578
769
|
...config.compaction,
|
|
@@ -580,6 +771,11 @@ export async function openHarness(config, options = {}) {
|
|
|
580
771
|
},
|
|
581
772
|
},
|
|
582
773
|
}, BACKGROUND_CONTEXT);
|
|
774
|
+
// M2 boot sweep: add each conversation owner's declared entries to every
|
|
775
|
+
// default-edited selection. Exact arrays are never widened. Runs before
|
|
776
|
+
// resume and is idempotent; late tool installs still resolve by name.
|
|
777
|
+
const userToolSweep = await sweepUserSelections(harness, storage, perUserSlugs);
|
|
778
|
+
console.log(`user tools: boot sweep scanned ${userToolSweep.scanned} conversation(s), changed ${userToolSweep.changed}`);
|
|
583
779
|
// The seam hosts' lazy binding resolves here: every seam call issued
|
|
584
780
|
// after boot completes finds the harness immediately; one issued from a
|
|
585
781
|
// post-open async factory path waits on this same promise.
|
|
@@ -608,12 +804,35 @@ export async function openHarness(config, options = {}) {
|
|
|
608
804
|
idleThresholdMs: options.eventBus?.idleThresholdMs ?? memorySeam.idleThresholdMs,
|
|
609
805
|
idleSweepMs: options.eventBus?.idleSweepMs ?? IDLE_SWEEP_MS,
|
|
610
806
|
workerDeadlineMs: options.eventBus?.workerDeadlineMs ?? memorySeam.workerDeadlineMs,
|
|
807
|
+
shutdownSettleWaitMs: options.eventBus?.shutdownSettleWaitMs,
|
|
611
808
|
// W3 Task 4: the hub's subscription is installed at open —
|
|
612
809
|
// synchronous, no-blocking-work (the SSE route's filtering side
|
|
613
810
|
// lane does its own async work). Exposed so the bus's open() can
|
|
614
811
|
// wire it before any commit lands.
|
|
615
812
|
onHostEvent: (event) => events.publish(event),
|
|
616
813
|
});
|
|
814
|
+
// The settings-changed publication (U4 §13.2): ONE seam fanning a
|
|
815
|
+
// committed settings write out to (a) the hub — /v1/events delivers
|
|
816
|
+
// `settings.changed` to admins — and (b) every extension's
|
|
817
|
+
// `onSettingsChanged` through the bus's bounded fan-out (rejections
|
|
818
|
+
// logged, never propagated: §13.2's the-write-still-succeeds rule).
|
|
819
|
+
// The scheduler re-arm rides the same seam: a write touching
|
|
820
|
+
// `server.providers.refreshIntervalMinutes` (a LIVE key, §13.1) stops
|
|
821
|
+
// and re-arms the interval timer at the fresh cadence.
|
|
822
|
+
const publishSettingsChanged = (scope, keys, user) => {
|
|
823
|
+
events.publish({ type: "settings.changed", scope, keys: [...keys] });
|
|
824
|
+
if (keys.includes("server.providers.refreshIntervalMinutes") ||
|
|
825
|
+
keys.includes("server.providers") ||
|
|
826
|
+
keys.includes("server")) {
|
|
827
|
+
stopProviderRefresh();
|
|
828
|
+
stopProviderRefresh = armProviderRefresh();
|
|
829
|
+
}
|
|
830
|
+
if (user !== undefined && typeof scope !== "string") {
|
|
831
|
+
userServices.settingsChanged(scope.extension, user);
|
|
832
|
+
}
|
|
833
|
+
void eventBus.emitSettingsChanged(keys, user);
|
|
834
|
+
};
|
|
835
|
+
settingsChangedSinks.push(publishSettingsChanged);
|
|
617
836
|
// Activity stamping (UI refresh A1): every settled run stamps its
|
|
618
837
|
// conversation's `lastActivityAt` (the admission-side stamp lives in the
|
|
619
838
|
// submissions route). Registered directly after the bus opens — host-owned
|
|
@@ -628,6 +847,14 @@ export async function openHarness(config, options = {}) {
|
|
|
628
847
|
runSettled: binding.runSettled,
|
|
629
848
|
});
|
|
630
849
|
}
|
|
850
|
+
// UI-blocks U4 §13.2: the settings-change hook merges into the same
|
|
851
|
+
// per-extension registration (the fan-out below invokes it).
|
|
852
|
+
for (const binding of settingsChangedBindings) {
|
|
853
|
+
handlersByName.set(binding.extension.name, {
|
|
854
|
+
...handlersByName.get(binding.extension.name),
|
|
855
|
+
onSettingsChanged: binding.onSettingsChanged,
|
|
856
|
+
});
|
|
857
|
+
}
|
|
631
858
|
for (const binding of lifecycleBindings) {
|
|
632
859
|
handlersByName.set(binding.extension.name, {
|
|
633
860
|
...handlersByName.get(binding.extension.name),
|
|
@@ -824,8 +1051,16 @@ export async function openHarness(config, options = {}) {
|
|
|
824
1051
|
harness.resume();
|
|
825
1052
|
return {
|
|
826
1053
|
harness,
|
|
1054
|
+
isolation,
|
|
1055
|
+
isolationState,
|
|
1056
|
+
osUsers,
|
|
1057
|
+
sandboxPool,
|
|
1058
|
+
userServices,
|
|
827
1059
|
storage,
|
|
828
1060
|
registry,
|
|
1061
|
+
userTools,
|
|
1062
|
+
rewriteUserSelection: (tx, conversationId, from, to) => rewriteSelection(tx, conversationId, from, to, perUserSlugs),
|
|
1063
|
+
userToolSweep,
|
|
829
1064
|
models,
|
|
830
1065
|
jobs,
|
|
831
1066
|
jobRunTotals: () => jobRunTotals,
|
|
@@ -835,6 +1070,7 @@ export async function openHarness(config, options = {}) {
|
|
|
835
1070
|
webuiBundleCollisions,
|
|
836
1071
|
envBuilder,
|
|
837
1072
|
extensionOutcomes,
|
|
1073
|
+
loginProviders,
|
|
838
1074
|
builtinSkipped: builtinsEnabled ? builtins.skipped : [],
|
|
839
1075
|
providerHealth,
|
|
840
1076
|
providerOwnership,
|
|
@@ -855,6 +1091,7 @@ export async function openHarness(config, options = {}) {
|
|
|
855
1091
|
extensionRoutes: routeBindings,
|
|
856
1092
|
extensionCommands: commandBindings,
|
|
857
1093
|
extensionSurfaces: surfaceBindings,
|
|
1094
|
+
extensionSettingsSections: settingsSectionBindings,
|
|
858
1095
|
notifierBindings,
|
|
859
1096
|
channelBindings,
|
|
860
1097
|
gatewayEngine,
|
|
@@ -867,6 +1104,8 @@ export async function openHarness(config, options = {}) {
|
|
|
867
1104
|
autoNamer,
|
|
868
1105
|
bootRefresh: bootRefreshDone,
|
|
869
1106
|
refreshProvider: refreshOneProvider,
|
|
870
|
-
stopProviderRefresh,
|
|
1107
|
+
stopProviderRefresh: stopCurrentProviderRefresh,
|
|
1108
|
+
settingsBootSnapshot,
|
|
1109
|
+
publishSettingsChanged,
|
|
871
1110
|
};
|
|
872
1111
|
}
|
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 five 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
|
}
|