@sigrun/sigrun 0.4.2 → 0.6.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 +92 -1
- package/README.md +63 -33
- package/dist/commands/expand.js +37 -5
- package/dist/curation/attribution.js +135 -0
- package/dist/curation/deletion.js +32 -0
- package/dist/curation/gate.js +50 -0
- package/dist/curation/host.js +342 -0
- package/dist/curation/internal.js +54 -0
- package/dist/curation/names.js +5 -0
- package/dist/curation/overlays.js +325 -0
- package/dist/curation/paths.js +17 -0
- package/dist/curation/run-doc.js +64 -0
- package/dist/curation/runs.js +579 -0
- package/dist/curation/scan.js +30 -0
- package/dist/curation/skill-tool.js +308 -0
- package/dist/curation/store.js +444 -0
- package/dist/curation/targets.js +77 -0
- package/dist/curation/usage.js +124 -0
- package/dist/curation/wrap.js +212 -0
- package/dist/extension.d.ts +336 -1
- package/dist/harness/boot.js +236 -13
- package/dist/harness/builtins.js +19 -12
- package/dist/harness/event-bus.js +64 -0
- package/dist/harness/identity/primary-admin.js +19 -0
- package/dist/harness/loader.js +114 -1
- package/dist/harness/naming.js +6 -0
- package/dist/harness/packages.js +14 -9
- package/dist/harness/reserved-sections.js +4 -0
- package/dist/harness/seam-host.js +154 -26
- package/dist/harness/sections/context-files.js +6 -3
- package/dist/harness/sections/job-state.js +24 -0
- package/dist/harness/settings.js +2 -2
- package/dist/harness/tools.js +70 -0
- package/dist/harness/workspace.js +30 -3
- package/dist/jobs/fire.js +37 -14
- package/dist/jobs/index.js +34 -7
- package/dist/jobs/reconcile.js +9 -0
- package/dist/jobs/skills-refs.js +40 -0
- package/dist/jobs/state-dir.js +16 -0
- package/dist/jobs/transfer.js +14 -0
- package/dist/sandbox/policy.js +27 -4
- package/dist/serve.js +5 -3
- package/dist/server/blocks/answers.js +9 -3
- package/dist/server/conversation-ops.js +34 -3
- package/dist/server/helpers.js +1 -1
- package/dist/server/host-bridge.js +31 -1
- package/dist/server/notifications.js +108 -16
- package/dist/server/routes/jobs.js +31 -0
- package/dist/server/routes/meta.js +8 -3
- package/dist/server/routes/submissions.js +5 -0
- package/dist/server/routes/surfaces.js +3 -1
- package/dist/server/routes/users.js +7 -0
- package/dist/server/schema-fixture.js +168 -0
- package/dist/server/session-meta.js +12 -10
- package/dist/settings/core-sections.js +18 -0
- package/dist/settings/registry.js +12 -0
- package/dist/skills/index.js +88 -19
- package/node_modules/@sigrun/shared/CHANGELOG.md +6 -1
- package/node_modules/@sigrun/shared/dist/access.d.ts +3 -0
- package/node_modules/@sigrun/shared/dist/access.js +8 -0
- package/node_modules/@sigrun/shared/dist/session-meta.d.ts +15 -4
- package/node_modules/@sigrun/shared/dist/settings.d.ts +7 -5
- package/node_modules/@sigrun/shared/package.json +1 -1
- package/package.json +9 -7
- package/webui/dist/assets/index-BR0di0OG.css +1 -0
- package/webui/dist/assets/{index-DWmsAQl2.js → index-BzrA1T4g.js} +5 -5
- package/webui/dist/index.html +2 -2
- package/webui/dist/assets/index-h7zDY9vD.css +0 -1
package/dist/harness/boot.js
CHANGED
|
@@ -30,16 +30,41 @@ import { openIdentityStores, ensureSetupCode, } from "./identity/index.js";
|
|
|
30
30
|
import { MEMORY_SECTION_KEY, noopMemoryExtension } from "./memory-default.js";
|
|
31
31
|
import { createSeamHost, } from "./seam-host.js";
|
|
32
32
|
import { contextFilesExtension } from "./sections/context-files.js";
|
|
33
|
+
import { createJobStateExtension } from "./sections/job-state.js";
|
|
33
34
|
import { modelGuidanceExtension } from "./sections/model-guidance.js";
|
|
34
|
-
import { createSkillsExtension } from "../skills/index.js";
|
|
35
|
+
import { createSkillsExtension, scanFsRoot } from "../skills/index.js";
|
|
35
36
|
import { SLASH_COMMANDS } from "../server/commands.js";
|
|
36
37
|
import { packagePromptRootsOf } from "../commands/templates.js";
|
|
37
38
|
import { blocksToolsExtension } from "./blocks/tools.js";
|
|
38
|
-
import {
|
|
39
|
+
import { createCodingToolsExtension, searchToolsExtension } from "./tools.js";
|
|
39
40
|
import { collectProviders, createProviderMergeSink, } from "./providers/index.js";
|
|
40
41
|
import { EventHub } from "../server/events/hub.js";
|
|
41
|
-
import { providerAuthStatus } from "../server/helpers.js";
|
|
42
|
+
import { providerAuthStatus, storeAgentChange } from "../server/helpers.js";
|
|
42
43
|
import { bootRefresh, DEFAULT_REFRESH_TIMEOUT_MS, refreshProvider, } from "./providers/refresh.js";
|
|
44
|
+
/**
|
|
45
|
+
* The default per-request stream deadline (curator-eval hardening): the
|
|
46
|
+
* OpenAI SDK waits up to 10 minutes for response HEADERS by default, and
|
|
47
|
+
* a request that never reaches the provider never gets headers — the
|
|
48
|
+
* generation task wedges in phase=request. Five minutes covers long
|
|
49
|
+
* first tokens on slow providers (observed 40-60s on the eval models;
|
|
50
|
+
* reasoning models can run far longer once streaming) because the SDK's
|
|
51
|
+
* timer clears at headers arrival, not stream end.
|
|
52
|
+
*/
|
|
53
|
+
export const DEFAULT_STREAM_TIMEOUT_MS = 300_000;
|
|
54
|
+
/**
|
|
55
|
+
* The boot `stream` getter's merge, factored out for the harness-open
|
|
56
|
+
* test: caller seam over file over the default `timeoutMs`, field-wise.
|
|
57
|
+
* Returns `undefined` when nothing is set (the honest empty shape).
|
|
58
|
+
*/
|
|
59
|
+
export function mergeStreamSettings(caller, file) {
|
|
60
|
+
const merged = {
|
|
61
|
+
...(caller ?? file),
|
|
62
|
+
...(caller === undefined && file?.timeoutMs === undefined
|
|
63
|
+
? { timeoutMs: DEFAULT_STREAM_TIMEOUT_MS }
|
|
64
|
+
: {}),
|
|
65
|
+
};
|
|
66
|
+
return Object.keys(merged).length > 0 ? merged : undefined;
|
|
67
|
+
}
|
|
43
68
|
import { startProviderRefresh } from "./providers/scheduler.js";
|
|
44
69
|
import { fileModelsStore } from "./providers/models-store.js";
|
|
45
70
|
import { packagesDirOf, selfPackageRoot } from "./packages.js";
|
|
@@ -54,7 +79,16 @@ import { IsolationState } from "../sandbox/isolation.js";
|
|
|
54
79
|
import { probeOsUsers } from "../sandbox/availability.js";
|
|
55
80
|
import { createAdmissionGuard } from "./identity/users.js";
|
|
56
81
|
import { sweepAccountLinks } from "./identity/link-sweep.js";
|
|
82
|
+
import { effectiveOwner as resolveEffectiveOwner } from "./identity/primary-admin.js";
|
|
57
83
|
import { SessionMetaDoc } from "../server/session-meta.js";
|
|
84
|
+
import { learnedSkillsDir } from "../curation/paths.js";
|
|
85
|
+
import { SkillStore } from "../curation/store.js";
|
|
86
|
+
import { OverlayStore } from "../curation/overlays.js";
|
|
87
|
+
import { UsageCounters } from "../curation/usage.js";
|
|
88
|
+
import { buildCurationRegistry } from "../curation/targets.js";
|
|
89
|
+
import { createCurationHost } from "../curation/host.js";
|
|
90
|
+
import { createCurationToolFeed, wrapExtensionCurationTools, } from "../curation/wrap.js";
|
|
91
|
+
import { createCurationGateExtension, CURATION_GATE_EXTENSION } from "../curation/gate.js";
|
|
58
92
|
import { probeIsolation, runHelperProbe, selfTestHelper, } from "../sandbox/availability.js";
|
|
59
93
|
import { resolveBuiltins, } from "./builtins.js";
|
|
60
94
|
import { createRegistry } from "./registry.js";
|
|
@@ -88,6 +122,7 @@ export const RESERVED_CORE_SECTION_OWNERS = {
|
|
|
88
122
|
skills: "sigrun.skills",
|
|
89
123
|
"tool-guidance": "sigrun.model-guidance",
|
|
90
124
|
"execution-guidance": "sigrun.model-guidance",
|
|
125
|
+
"job-state": "sigrun.job-state",
|
|
91
126
|
};
|
|
92
127
|
/**
|
|
93
128
|
* Open (or reopen) the durable harness at `config.storagePath`.
|
|
@@ -329,6 +364,10 @@ export async function openHarness(config, options = {}) {
|
|
|
329
364
|
// reads it at RENDER time, long after the assignment — the skills cap
|
|
330
365
|
// is a live setting (`skills.sectionMaxBytes`, default 4KB).
|
|
331
366
|
let skillsSettings;
|
|
367
|
+
// The job-state section resolves its services only when a job run renders;
|
|
368
|
+
// both are constructed after the core registry installs.
|
|
369
|
+
let workspaceService;
|
|
370
|
+
let jobs;
|
|
332
371
|
// Context-file sections (SOUL.md persona, AGENTS.md instructions), the
|
|
333
372
|
// skills convention, and the core tool surface (plan Task 1: coding
|
|
334
373
|
// tools read/write/edit/bash + search tools find/grep/ls) are all
|
|
@@ -346,9 +385,106 @@ export async function openHarness(config, options = {}) {
|
|
|
346
385
|
// ones, which `listSkills` includes by design).
|
|
347
386
|
const skillsOptions = {
|
|
348
387
|
userDir: join(roots.configDir, "skills"),
|
|
388
|
+
// Curator A9 (P6): the canonical workspace ROOT for overlay keys —
|
|
389
|
+
// the configured default workspace (a conversation with a
|
|
390
|
+
// sub-cwd still matches the root-keyed overlay; the accessor
|
|
391
|
+
// stays live for the config's sake).
|
|
392
|
+
workspaceRoot: () => config.workspace.path,
|
|
393
|
+
// Curator A3 (P2): the conversation owner's learned root —
|
|
394
|
+
// `<dataDir>/users/<owner>/skills`, undefined for ownerless
|
|
395
|
+
// conversations. Discovery resolves the owner per render.
|
|
396
|
+
learnedRoot: (owner) => owner === undefined
|
|
397
|
+
? undefined
|
|
398
|
+
: learnedSkillsDir(managedPaths.userAreas(owner)),
|
|
349
399
|
packageRoots: () => collectedSkillRoots,
|
|
350
400
|
sectionMaxBytes: () => skillsSettings?.read().skills?.sectionMaxBytes ?? 4096,
|
|
351
401
|
};
|
|
402
|
+
// Curator A6 (P3/P4): the per-user curation wiring — one skill store,
|
|
403
|
+
// one overlay store and one usage counter set per EXISTING user,
|
|
404
|
+
// created lazily on first use and cached. `userExists` is the LIVE
|
|
405
|
+
// directory read (a removed user's factories stop resolving), and
|
|
406
|
+
// `nameTakenElsewhere` scans the host-side roots the harness owns
|
|
407
|
+
// (the user dir and the collected package roots; a conversation's
|
|
408
|
+
// workspace can only be checked through its env, which the store
|
|
409
|
+
// cannot see — the create-refusal there is the A4 store's own).
|
|
410
|
+
const skillStores = new Map();
|
|
411
|
+
const overlayStores = new Map();
|
|
412
|
+
const usageCounters = new Map();
|
|
413
|
+
const curationOf = (user) => {
|
|
414
|
+
if (user === undefined)
|
|
415
|
+
return undefined;
|
|
416
|
+
// Curator A10: cache-FIRST resolution — a store set captured before
|
|
417
|
+
// a user's deletion stays resolvable, so its own `userExists` gate
|
|
418
|
+
// fails every later op with "user removed" (the A4/A5 fail-closed
|
|
419
|
+
// property: an in-flight run's late write never recreates the
|
|
420
|
+
// roots). A never-seen user still needs the live directory read.
|
|
421
|
+
if (skillStores.get(user) === undefined &&
|
|
422
|
+
identity.users.get(user) === undefined) {
|
|
423
|
+
return undefined;
|
|
424
|
+
}
|
|
425
|
+
const userArea = managedPaths.userAreas(user);
|
|
426
|
+
let store = skillStores.get(user);
|
|
427
|
+
if (store === undefined) {
|
|
428
|
+
store = new SkillStore({
|
|
429
|
+
userArea,
|
|
430
|
+
user,
|
|
431
|
+
now: () => clockNow(),
|
|
432
|
+
nameTakenElsewhere: async (name) => {
|
|
433
|
+
for (const skill of scanFsRoot(join(roots.configDir, "skills"), "user dir")) {
|
|
434
|
+
if (skill.name === name)
|
|
435
|
+
return "user dir";
|
|
436
|
+
}
|
|
437
|
+
for (const root of collectedSkillRoots) {
|
|
438
|
+
if (scanFsRoot(root.dir, "package").some((skill) => skill.name === name))
|
|
439
|
+
return root.extension;
|
|
440
|
+
}
|
|
441
|
+
return undefined;
|
|
442
|
+
},
|
|
443
|
+
userExists: () => identity.users.get(user) !== undefined,
|
|
444
|
+
});
|
|
445
|
+
skillStores.set(user, store);
|
|
446
|
+
}
|
|
447
|
+
let overlays = overlayStores.get(user);
|
|
448
|
+
if (overlays === undefined) {
|
|
449
|
+
overlays = new OverlayStore({
|
|
450
|
+
userArea,
|
|
451
|
+
user,
|
|
452
|
+
userExists: () => identity.users.get(user) !== undefined,
|
|
453
|
+
feed: store,
|
|
454
|
+
});
|
|
455
|
+
overlayStores.set(user, overlays);
|
|
456
|
+
}
|
|
457
|
+
let usage = usageCounters.get(user);
|
|
458
|
+
if (usage === undefined) {
|
|
459
|
+
usage = new UsageCounters({
|
|
460
|
+
userArea,
|
|
461
|
+
user,
|
|
462
|
+
now: () => clockNow(),
|
|
463
|
+
// The A4 gate, but silent: the cached counters outlive the user
|
|
464
|
+
// (cache-first resolution), and telemetry is void-and-forget —
|
|
465
|
+
// a removed user's flush drops instead of recreating the
|
|
466
|
+
// deleted curation directory (deletion's fail-closed claim).
|
|
467
|
+
userExists: () => identity.users.get(user) !== undefined,
|
|
468
|
+
});
|
|
469
|
+
usageCounters.set(user, usage);
|
|
470
|
+
}
|
|
471
|
+
return { store, overlays, usage };
|
|
472
|
+
};
|
|
473
|
+
const curationSkillsOptions = {
|
|
474
|
+
...skillsOptions,
|
|
475
|
+
skillStore: (user) => curationOf(user)?.store,
|
|
476
|
+
overlays: (owner) => curationOf(owner)?.overlays,
|
|
477
|
+
usage: (owner) => curationOf(owner)?.usage,
|
|
478
|
+
};
|
|
479
|
+
// Curator A6 (P4): the read tool counts learned-root reads through the
|
|
480
|
+
// same per-user wiring (load_skill and /skill: count through the
|
|
481
|
+
// skills options above).
|
|
482
|
+
const codingWithUsage = createCodingToolsExtension({
|
|
483
|
+
countersOf: (owner) => curationOf(owner)?.usage,
|
|
484
|
+
learnedRootOf: (owner) => owner === undefined
|
|
485
|
+
? undefined
|
|
486
|
+
: learnedSkillsDir(managedPaths.userAreas(owner)),
|
|
487
|
+
});
|
|
352
488
|
const templateOptions = {
|
|
353
489
|
userDir: join(roots.configDir, "prompts"),
|
|
354
490
|
packageRoots: () => collectedTemplateRoots,
|
|
@@ -371,8 +507,29 @@ export async function openHarness(config, options = {}) {
|
|
|
371
507
|
},
|
|
372
508
|
};
|
|
373
509
|
registry.install(contextFilesExtension);
|
|
374
|
-
|
|
375
|
-
|
|
510
|
+
// Escalations A2: the core-owned job-state prompt section names the run's
|
|
511
|
+
// state folder (WorkspaceService.jobsRoot, A1); deps late-bind like
|
|
512
|
+
// skillsSettings — services are created further down.
|
|
513
|
+
registry.install(createJobStateExtension({
|
|
514
|
+
jobsRoot: (owner) => workspaceService?.jobsRoot(owner),
|
|
515
|
+
job: async (id) => await jobs?.get(id),
|
|
516
|
+
}));
|
|
517
|
+
// Curator A8 (P9): the wrap feed — the mutable set of curation tool
|
|
518
|
+
// names, seeded with the core set and grown by the loader as each
|
|
519
|
+
// extension's curationTargets validate. The skills extension's
|
|
520
|
+
// install below wraps through it (its OWN tools: `skill_manage`);
|
|
521
|
+
// the loader wraps every extension-declared target tool at its
|
|
522
|
+
// install. `isCurationTool` on the built registry (A7) stays the
|
|
523
|
+
// read-side API — the feed is only the write-side wrap oracle.
|
|
524
|
+
const curationFeed = createCurationToolFeed();
|
|
525
|
+
// Curator A6: the skills extension carries the curation wiring
|
|
526
|
+
// (skill_manage + usage counting), and the coding tools count
|
|
527
|
+
// learned-root reads — the same extension NAMES as before, so
|
|
528
|
+
// nothing downstream observes the swap. A8: the install WRAPS the
|
|
529
|
+
// curation tools (skill_manage's registration carries the hint
|
|
530
|
+
// interception; load_skill and the section are untouched).
|
|
531
|
+
registry.install(wrapExtensionCurationTools(createSkillsExtension(curationSkillsOptions), curationFeed));
|
|
532
|
+
registry.install(codingWithUsage);
|
|
376
533
|
registry.install(searchToolsExtension);
|
|
377
534
|
// The chat-blocks tools (UI-blocks U5 Task 1, spec §8.1): every agent
|
|
378
535
|
// can show and update interactive block surfaces in chat — the same
|
|
@@ -466,8 +623,11 @@ export async function openHarness(config, options = {}) {
|
|
|
466
623
|
const sharedStores = sharedExtensionStores(roots, env, identity.users);
|
|
467
624
|
const seamHosts = new Map();
|
|
468
625
|
const seamKinds = new Map();
|
|
469
|
-
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, {
|
|
626
|
+
const { outcomes: extensionOutcomes, healthProducers: extensionHealth, runSettledBindings, settingsChangedBindings, lifecycleBindings, workerBindings, curationBindings, userServices: userServiceBindings, providerContributors, loginProviders, webuiBundles: extensionWebuiBundles, skillRoots: extensionSkillRoots, jobBindings, routeBindings, commandBindings, notifierBindings, channelBindings, gatewayEngine, surfaces: surfaceBindings, settingsSections: settingsSectionBindings, perUserSlugs: loadedPerUserSlugs, } = await loadExtensions(loaderInputs, registry, {
|
|
470
627
|
packagesDir,
|
|
628
|
+
// Curator A8 (P9): the wrap feed — the loader grows it with each
|
|
629
|
+
// validated target tool and wraps those tools at their install.
|
|
630
|
+
curationFeed,
|
|
471
631
|
hostContextFactory: (extensionName, kinds, priorHost) => {
|
|
472
632
|
// Two-phase, revised for the provisional identity (provider
|
|
473
633
|
// plugins Task 9 P1): the loader's PRE-import call passes the
|
|
@@ -505,6 +665,11 @@ export async function openHarness(config, options = {}) {
|
|
|
505
665
|
},
|
|
506
666
|
});
|
|
507
667
|
perUserSlugs.push(...loadedPerUserSlugs);
|
|
668
|
+
// The curation-target registry (curator P7): the loader's validated,
|
|
669
|
+
// slug-attributed bindings folded into the ONE registry — the A8
|
|
670
|
+
// wrapping feed (isCurationTool) and the A9 host.curation surface
|
|
671
|
+
// read it. An empty fold is the no-declarer deploy.
|
|
672
|
+
const curation = buildCurationRegistry(curationBindings);
|
|
508
673
|
for (const binding of userServiceBindings) {
|
|
509
674
|
userServices.register(binding.extension, binding.services, binding.packageDir);
|
|
510
675
|
}
|
|
@@ -614,6 +779,9 @@ export async function openHarness(config, options = {}) {
|
|
|
614
779
|
// deps object is shared with `fireJob`'s run-now and the JobsService
|
|
615
780
|
// below (one clock, one tz, one models store).
|
|
616
781
|
const jobRuntimeDeps = {
|
|
782
|
+
// Resolve declared-job owners against the live directory at each use
|
|
783
|
+
// (§6.5); no cache and no rewrite of the source's stored record.
|
|
784
|
+
effectiveOwner: (owner) => resolveEffectiveOwner(identity.users.list(), owner),
|
|
617
785
|
// SAFETY: harness + storage are assigned right after `Harness.open`
|
|
618
786
|
// returns below (the open needs the task registered, so the deps
|
|
619
787
|
// object is created first with placeholders). Every read — task
|
|
@@ -640,6 +808,11 @@ export async function openHarness(config, options = {}) {
|
|
|
640
808
|
name: "sigrun.jobs",
|
|
641
809
|
tasks: [createScheduleTask(jobRuntimeDeps)],
|
|
642
810
|
});
|
|
811
|
+
// Curator A8 (P6 gate): the hooks-only curator-run gate — installed
|
|
812
|
+
// in the registry but NEVER in the host default selection (the
|
|
813
|
+
// settings getter below filters it like a per-user entry): only A9's
|
|
814
|
+
// producer selects it, in the internal run conversations it creates.
|
|
815
|
+
registry.install(createCurationGateExtension());
|
|
643
816
|
// The host event hub (W3 Task 4, spec §7.4): ONE per opened harness.
|
|
644
817
|
// The event bus's commit-derived events subscribe below (after the bus
|
|
645
818
|
// opens); the provider refresh paths observe readiness here — each
|
|
@@ -756,7 +929,7 @@ export async function openHarness(config, options = {}) {
|
|
|
756
929
|
stopProviderRefresh();
|
|
757
930
|
};
|
|
758
931
|
// Every tool/file/upload/job env resolves through this one decision seam.
|
|
759
|
-
|
|
932
|
+
workspaceService = createWorkspaceService({
|
|
760
933
|
isolation: isolationState,
|
|
761
934
|
osUsers,
|
|
762
935
|
pool: sandboxPool,
|
|
@@ -828,14 +1001,26 @@ export async function openHarness(config, options = {}) {
|
|
|
828
1001
|
return callerSettings?.retry ?? fileSettings.retry;
|
|
829
1002
|
},
|
|
830
1003
|
get stream() {
|
|
831
|
-
|
|
1004
|
+
// The stream-request default (curator-eval hardening): a
|
|
1005
|
+
// model request with no explicit timeout can wedge forever —
|
|
1006
|
+
// the OpenAI SDK's `timeout` is a headers-arrival deadline and
|
|
1007
|
+
// defaults to 10 minutes, but a request that never reaches the
|
|
1008
|
+
// provider (undici socket-pool class) never gets headers to
|
|
1009
|
+
// time out against, so pi-durable's generation task sits in
|
|
1010
|
+
// phase=request indefinitely. `mergeStreamSettings` bounds
|
|
1011
|
+
// every request with a default `timeoutMs`; the caller seam
|
|
1012
|
+
// and the file each override field-wise.
|
|
1013
|
+
return mergeStreamSettings(callerSettings?.stream, fileSettings.stream);
|
|
832
1014
|
},
|
|
833
1015
|
get extensions() {
|
|
834
1016
|
// Per-user entries are core-computed from SessionMeta.owner,
|
|
835
1017
|
// never part of a host default — including an operator-supplied
|
|
836
|
-
// caller list (controller ruling: filter BOTH sources).
|
|
1018
|
+
// caller list (controller ruling: filter BOTH sources). The
|
|
1019
|
+
// curator-run gate (A8) is filtered the same way: it is selected
|
|
1020
|
+
// ONLY in the internal run conversations A9's producer creates.
|
|
837
1021
|
const selected = callerSettings?.extensions ?? registry.snapshot().installed();
|
|
838
|
-
return selected.filter((entry) => !isUserEntryName(entry.name)
|
|
1022
|
+
return selected.filter((entry) => !isUserEntryName(entry.name) &&
|
|
1023
|
+
entry.name !== CURATION_GATE_EXTENSION);
|
|
839
1024
|
},
|
|
840
1025
|
toolExecution: callerSettings?.toolExecution,
|
|
841
1026
|
compaction: {
|
|
@@ -920,6 +1105,36 @@ export async function openHarness(config, options = {}) {
|
|
|
920
1105
|
// order). The bus's close stops delivery; the unsubscribe is unused.
|
|
921
1106
|
const now = clockNow;
|
|
922
1107
|
wireActivity(eventBus, harness, now);
|
|
1108
|
+
// Curator A9 (P6): the ONE curation host — the `host.curation`
|
|
1109
|
+
// surface over the per-user stores (curationOf), the curation
|
|
1110
|
+
// registry, the jobs records and the run driver. The server's host
|
|
1111
|
+
// bridge and every seam host expose the SAME instance (the bridge
|
|
1112
|
+
// carries it; the seam hosts delegate). The digest producer's
|
|
1113
|
+
// default agent is the create op's own store-default change (what
|
|
1114
|
+
// every server-created conversation starts from): the structural
|
|
1115
|
+
// `{settings, registry, providerEnv}` subset `storeAgentChange`
|
|
1116
|
+
// reads, closed over the LIVE references so a settings PATCH
|
|
1117
|
+
// reaches later digests without a restart.
|
|
1118
|
+
// SAFETY: the cast — `storeAgentChange` reads exactly these three
|
|
1119
|
+
// members (settings.read, registry.snapshot, providerEnv) and
|
|
1120
|
+
// nothing else of OpenedHarness; the live closure satisfies them.
|
|
1121
|
+
const curationHost = createCurationHost({
|
|
1122
|
+
harness,
|
|
1123
|
+
registry,
|
|
1124
|
+
storage,
|
|
1125
|
+
curation,
|
|
1126
|
+
defaultModel,
|
|
1127
|
+
stores: curationOf,
|
|
1128
|
+
skillsOptions: {
|
|
1129
|
+
userDir: join(roots.configDir, "skills"),
|
|
1130
|
+
packageRoots: () => collectedSkillRoots,
|
|
1131
|
+
},
|
|
1132
|
+
defaultAgent: () => storeAgentChange({
|
|
1133
|
+
settings: { read: () => settingsStore.read() },
|
|
1134
|
+
registry,
|
|
1135
|
+
providerEnv: effectiveEnv,
|
|
1136
|
+
}, defaultModel),
|
|
1137
|
+
});
|
|
923
1138
|
const handlersByName = new Map();
|
|
924
1139
|
for (const binding of runSettledBindings) {
|
|
925
1140
|
handlersByName.set(binding.extension.name, {
|
|
@@ -1007,6 +1222,7 @@ export async function openHarness(config, options = {}) {
|
|
|
1007
1222
|
// (an extension's declaration is its boot-time declaration).
|
|
1008
1223
|
const fileJobs = () => settingsStore.read().jobs ?? [];
|
|
1009
1224
|
const jobsSources = () => ({ file: fileJobs(), extensions: jobBindings });
|
|
1225
|
+
const jobEffectiveOwner = (owner) => resolveEffectiveOwner(identity.users.list(), owner);
|
|
1010
1226
|
// The root conversation — the schedule tasks' owner (spec §8.1).
|
|
1011
1227
|
// `Harness.root` is idempotent (creates with `agent` on first call,
|
|
1012
1228
|
// returns the reserved root thereafter), so the server's own
|
|
@@ -1035,7 +1251,7 @@ export async function openHarness(config, options = {}) {
|
|
|
1035
1251
|
// SSE stream computes `accessOf` per principal and adds it. A
|
|
1036
1252
|
// future in-process hub subscriber must never read a fabricated
|
|
1037
1253
|
// access off the hub's copy.
|
|
1038
|
-
const { access: _dropped, ...row } = jobWireOf(jobId, record);
|
|
1254
|
+
const { access: _dropped, ...row } = jobWireOf(jobId, record, jobEffectiveOwner(record.owner));
|
|
1039
1255
|
events.publish({
|
|
1040
1256
|
type: "job.changed",
|
|
1041
1257
|
job: row,
|
|
@@ -1051,7 +1267,9 @@ export async function openHarness(config, options = {}) {
|
|
|
1051
1267
|
id: jobId,
|
|
1052
1268
|
name: newest.spec.name,
|
|
1053
1269
|
source: newest.source,
|
|
1054
|
-
...(newest.owner === undefined
|
|
1270
|
+
...(jobEffectiveOwner(newest.owner) === undefined
|
|
1271
|
+
? {}
|
|
1272
|
+
: { owner: jobEffectiveOwner(newest.owner) }),
|
|
1055
1273
|
...(newest.sharing === undefined ? {} : { sharing: newest.sharing }),
|
|
1056
1274
|
schedule: newest.spec.schedule,
|
|
1057
1275
|
action: newest.spec.action,
|
|
@@ -1087,12 +1305,14 @@ export async function openHarness(config, options = {}) {
|
|
|
1087
1305
|
jobRunTotals.set(outcome, (jobRunTotals.get(outcome) ?? 0) + 1);
|
|
1088
1306
|
}
|
|
1089
1307
|
});
|
|
1090
|
-
|
|
1308
|
+
jobs = createJobsService({
|
|
1091
1309
|
harness,
|
|
1092
1310
|
storage,
|
|
1093
1311
|
runtime: jobRuntimeDeps,
|
|
1312
|
+
effectiveOwner: jobEffectiveOwner,
|
|
1094
1313
|
rootId: rootConversationId,
|
|
1095
1314
|
sources: jobsSources,
|
|
1315
|
+
jobsRoot: (owner) => workspaceService?.jobsRoot(owner),
|
|
1096
1316
|
settings: () => settingsStore.read(),
|
|
1097
1317
|
onChange: (jobId) => {
|
|
1098
1318
|
void publishJobChanged(jobId);
|
|
@@ -1113,6 +1333,7 @@ export async function openHarness(config, options = {}) {
|
|
|
1113
1333
|
runtime: jobRuntimeDeps,
|
|
1114
1334
|
rootId: rootConversationId,
|
|
1115
1335
|
sources: jobsSources,
|
|
1336
|
+
jobsRoot: (owner) => workspaceService?.jobsRoot(owner),
|
|
1116
1337
|
}, BACKGROUND_CONTEXT);
|
|
1117
1338
|
// The deleted-owner warning (§8.3): a declared job whose `owner` names
|
|
1118
1339
|
// a user no longer in the directory runs OWNERLESS until its source is
|
|
@@ -1176,6 +1397,8 @@ export async function openHarness(config, options = {}) {
|
|
|
1176
1397
|
channelBindings,
|
|
1177
1398
|
gatewayEngine,
|
|
1178
1399
|
skillRoots: extensionSkillRoots,
|
|
1400
|
+
curation,
|
|
1401
|
+
curationHost,
|
|
1179
1402
|
commandExpansion,
|
|
1180
1403
|
eventBus,
|
|
1181
1404
|
events,
|
package/dist/harness/builtins.js
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Built-in extensions: codemode, MCP, tool search
|
|
3
|
-
* core — present by default, disableable via settings
|
|
4
|
-
* replaceable by a same-named configured extension.
|
|
2
|
+
* Built-in extensions: codemode, MCP, tool search, the gateway, OAuth and
|
|
3
|
+
* the curator ship WITH core — present by default, disableable via settings
|
|
4
|
+
* `builtinExtensions`, replaceable by a same-named configured extension.
|
|
5
|
+
* Every other extension is optional and loads only when configured.
|
|
5
6
|
*
|
|
6
|
-
* The first three mirror pi 1.3.0's built-in tool surface;
|
|
7
|
-
*
|
|
7
|
+
* The first three mirror pi 1.3.0's built-in tool surface; the gateway is
|
|
8
|
+
* the channel-adapter engine (hermes/openclaw parity: channels work out of
|
|
9
|
+
* the box); OAuth adds sigrun's standard OIDC/OAuth2 login providers
|
|
10
|
+
* without extra setup; the curator runs the per-user learning loops.
|
|
8
11
|
*
|
|
9
12
|
* Resolution root (Ruling 2): built-ins resolve through core's OWN
|
|
10
13
|
* node_modules via `createRequire(import.meta.url)` — they are core
|
|
@@ -23,7 +26,8 @@
|
|
|
23
26
|
* (`readPackageName` for path-shaped entries, `parsePackageSpecifier`
|
|
24
27
|
* otherwise), never by string equality on the raw specifier.
|
|
25
28
|
*
|
|
26
|
-
* Disabled (Ruling 6): `builtinExtensions: {codemode?, mcp?, toolSearch?,
|
|
29
|
+
* Disabled (Ruling 6): `builtinExtensions: {codemode?, mcp?, toolSearch?,
|
|
30
|
+
* gateway?, oauth?, curator?}`
|
|
27
31
|
* (each defaulting to `true`, restart-required). A disabled built-in is
|
|
28
32
|
* absent from `/v1/extensions` entirely; one log line records the skip.
|
|
29
33
|
*
|
|
@@ -38,13 +42,15 @@ import { basename, dirname, isAbsolute, resolve } from "node:path";
|
|
|
38
42
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
39
43
|
import { parsePackageSpecifier, readPackageName } from "./packages.js";
|
|
40
44
|
/**
|
|
41
|
-
* The
|
|
45
|
+
* The six built-ins, in load order (codemode before tool-search: both
|
|
42
46
|
* wrap pi-durable's own extensions and neither depends on the other, but
|
|
43
|
-
* the order pins the tool-collision story should names ever overlap
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
47
|
+
* the order pins the tool-collision story should names ever overlap;
|
|
48
|
+
* curator loads LAST — its loops observe every earlier built-in's
|
|
49
|
+
* contributions). Specifiers are the published package names (Task 3)
|
|
50
|
+
* — npm keys a file: dependency by the target's own manifest name, so
|
|
51
|
+
* the key must be the name the packages publish under (the dev shape
|
|
52
|
+
* carries `file:` values under these same keys until Task 8 publishes
|
|
53
|
+
* exact `0.1.0`s).
|
|
48
54
|
*/
|
|
49
55
|
export const BUILTIN_EXTENSIONS = [
|
|
50
56
|
{ key: "codemode", specifier: "@sigrun/ext-codemode", slug: "codemode" },
|
|
@@ -52,6 +58,7 @@ export const BUILTIN_EXTENSIONS = [
|
|
|
52
58
|
{ key: "toolSearch", specifier: "@sigrun/ext-tool-search", slug: "tool-search" },
|
|
53
59
|
{ key: "gateway", specifier: "@sigrun/ext-gateway", slug: "gateway" },
|
|
54
60
|
{ key: "oauth", specifier: "@sigrun/oauth", slug: "oauth" },
|
|
61
|
+
{ key: "curator", specifier: "@sigrun/ext-curator", slug: "curator" },
|
|
55
62
|
];
|
|
56
63
|
/** The core-anchored require (built-ins resolve from core's own node_modules). */
|
|
57
64
|
const coreRequire = createRequire(import.meta.url);
|
|
@@ -103,6 +103,22 @@ export class EventBus {
|
|
|
103
103
|
}
|
|
104
104
|
#unsubscribe;
|
|
105
105
|
#harness;
|
|
106
|
+
/**
|
|
107
|
+
* Internal-run detection (curator P5): the SessionMeta `internal` flag,
|
|
108
|
+
* resolved SYNCHRONOUSLY from the commit stream — every SessionMeta
|
|
109
|
+
* document change warms the cache (see the pre-pass in `observe`), so
|
|
110
|
+
* the arms there always find the flag of the conversation the
|
|
111
|
+
* publication belongs to. The marker is written in the conversation's
|
|
112
|
+
* CREATING commit and never changes (`markInternal`), so a cached
|
|
113
|
+
* value never goes stale; `false` is cached too, keeping ordinary
|
|
114
|
+
* traffic at zero extra reads (the bus's no-blocking-work contract
|
|
115
|
+
* forbids a snapshot here).
|
|
116
|
+
*/
|
|
117
|
+
#internal = new Map();
|
|
118
|
+
/** Whether `conversationId` is internal per the commit-warmed cache. */
|
|
119
|
+
#isInternal(conversationId) {
|
|
120
|
+
return this.#internal.get(conversationId) === true;
|
|
121
|
+
}
|
|
106
122
|
#queueMax;
|
|
107
123
|
#hookDeadlineMs;
|
|
108
124
|
#idleThresholdMs;
|
|
@@ -663,10 +679,37 @@ export class EventBus {
|
|
|
663
679
|
try {
|
|
664
680
|
const lifecycle = [];
|
|
665
681
|
const forkParents = new Map();
|
|
682
|
+
// Curator P5, PRE-PASS: warm the internal-flag cache off every
|
|
683
|
+
// SessionMeta document change BEFORE any arm below can fire —
|
|
684
|
+
// change order within a publication is unspecified, so the
|
|
685
|
+
// conversation/submission arms must never depend on the SessionMeta
|
|
686
|
+
// change having been seen first (an internal conversation created in
|
|
687
|
+
// THIS commit emits nothing, its `conversation.created` host event
|
|
688
|
+
// included). The plain `document` arm alone: a fork's
|
|
689
|
+
// `document.copy` carries no value to read (and `internal` is never
|
|
690
|
+
// inherited — the child's own first SessionMeta commit re-warms it).
|
|
691
|
+
// The marker is written in the creating commit and never changes
|
|
692
|
+
// (`markInternal`), so a cached value never goes stale; `false` is
|
|
693
|
+
// cached too, keeping ordinary traffic at zero extra reads (the
|
|
694
|
+
// bus's no-blocking-work contract forbids a snapshot here).
|
|
695
|
+
for (const change of publication.changes) {
|
|
696
|
+
if (change.type !== "document")
|
|
697
|
+
continue;
|
|
698
|
+
if (change.record.kind !== SessionMetaDoc.definition.kind)
|
|
699
|
+
continue;
|
|
700
|
+
if (change.conversationId === undefined)
|
|
701
|
+
continue;
|
|
702
|
+
this.#internal.set(change.conversationId, change.value?.internal === true);
|
|
703
|
+
}
|
|
666
704
|
for (const change of publication.changes) {
|
|
667
705
|
if (change.type !== "conversation")
|
|
668
706
|
continue;
|
|
669
707
|
const record = change.value;
|
|
708
|
+
// Curator P5: an internal conversation emits nothing — not the
|
|
709
|
+
// lifecycle events, not the host event (every owner-keyed path
|
|
710
|
+
// skips internal; the SSE stream's filtering is belt-and-braces).
|
|
711
|
+
if (this.#isInternal(record.id))
|
|
712
|
+
continue;
|
|
670
713
|
lifecycle.push({
|
|
671
714
|
type: "conversationCreated",
|
|
672
715
|
conversationId: record.id,
|
|
@@ -690,6 +733,17 @@ export class EventBus {
|
|
|
690
733
|
if (change.type !== "document" && change.type !== "document.copy") {
|
|
691
734
|
continue;
|
|
692
735
|
}
|
|
736
|
+
// Curator P5: an internal conversation emits nothing on ANY
|
|
737
|
+
// document arm — not the SessionMeta summary diffs, and not the
|
|
738
|
+
// interaction/ambient seam events either (every conversation.*
|
|
739
|
+
// host event and every derived lifecycle is withheld; the flag
|
|
740
|
+
// was warmed by the pre-pass above).
|
|
741
|
+
if (change.conversationId !== undefined &&
|
|
742
|
+
this.#isInternal(change.conversationId)) {
|
|
743
|
+
this.#archivedBasis.delete(change.conversationId);
|
|
744
|
+
this.#metaBasis.delete(change.conversationId);
|
|
745
|
+
continue;
|
|
746
|
+
}
|
|
693
747
|
const kind = change.record.kind;
|
|
694
748
|
if (kind !== SessionMetaDoc.definition.kind) {
|
|
695
749
|
// W3 Task 4: the interaction + ambient seam doc families feed
|
|
@@ -958,6 +1012,16 @@ export class EventBus {
|
|
|
958
1012
|
const value = change.value;
|
|
959
1013
|
if (value.type !== "input")
|
|
960
1014
|
continue;
|
|
1015
|
+
// Curator P5: an internal conversation's submissions are invisible
|
|
1016
|
+
// on every surface the bus feeds — no idle bookkeeping (the run
|
|
1017
|
+
// never arms the idle clock), no run.admitted/run.settled host
|
|
1018
|
+
// events (the SSE stream and the notifications surface see
|
|
1019
|
+
// nothing), and no settle-group enqueueing (no extension
|
|
1020
|
+
// runSettled delivery — the brief's three bus gates in one skip).
|
|
1021
|
+
// The flag was warmed by the pre-pass; `markInternal` runs in the
|
|
1022
|
+
// creating commit, which always precedes any submission.
|
|
1023
|
+
if (this.#isInternal(value.conversationId))
|
|
1024
|
+
continue;
|
|
961
1025
|
// Idle bookkeeping (Task 4): a PLACED input both stamps the
|
|
962
1026
|
// admission clock and marks the run live; a terminal input marks
|
|
963
1027
|
// the run end. Write-type submissions never touch either — a
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The primary admin is the first stored admin in directory insertion
|
|
3
|
+
* order. Callers pass the live directory at each use so demotions and
|
|
4
|
+
* deletions take effect without a cache or a record rewrite.
|
|
5
|
+
*/
|
|
6
|
+
export function primaryAdmin(records) {
|
|
7
|
+
return records.find((record) => record.admin === true);
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Resolve the effective owner at use time: a live declared owner wins;
|
|
11
|
+
* an absent or orphaned owner falls back to the primary admin. An empty
|
|
12
|
+
* directory (single-operator mode) stays ownerless.
|
|
13
|
+
*/
|
|
14
|
+
export function effectiveOwner(records, owner) {
|
|
15
|
+
if (owner !== undefined && records.some((record) => record.user === owner)) {
|
|
16
|
+
return owner;
|
|
17
|
+
}
|
|
18
|
+
return primaryAdmin(records)?.user;
|
|
19
|
+
}
|