@sigrun/sigrun 0.4.2 → 0.5.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 +37 -1
- package/README.md +46 -23
- 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 +282 -0
- package/dist/harness/boot.js +209 -9
- package/dist/harness/builtins.js +12 -9
- package/dist/harness/event-bus.js +64 -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 +2 -0
- package/dist/harness/seam-host.js +139 -26
- package/dist/harness/settings.js +2 -2
- package/dist/harness/tools.js +70 -0
- package/dist/jobs/fire.js +33 -11
- package/dist/jobs/skills-refs.js +40 -0
- package/dist/sandbox/policy.js +14 -0
- package/dist/server/blocks/answers.js +9 -3
- package/dist/server/conversation-ops.js +21 -3
- package/dist/server/helpers.js +1 -1
- package/dist/server/host-bridge.js +6 -1
- package/dist/server/notifications.js +6 -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 +9 -0
- package/dist/settings/registry.js +6 -0
- package/dist/skills/index.js +88 -19
- package/node_modules/@sigrun/shared/CHANGELOG.md +3 -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-DWmsAQl2.js → index-BtczbUXo.js} +1 -1
- package/webui/dist/index.html +1 -1
package/dist/harness/boot.js
CHANGED
|
@@ -31,15 +31,39 @@ 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
33
|
import { modelGuidanceExtension } from "./sections/model-guidance.js";
|
|
34
|
-
import { createSkillsExtension } from "../skills/index.js";
|
|
34
|
+
import { createSkillsExtension, scanFsRoot } from "../skills/index.js";
|
|
35
35
|
import { SLASH_COMMANDS } from "../server/commands.js";
|
|
36
36
|
import { packagePromptRootsOf } from "../commands/templates.js";
|
|
37
37
|
import { blocksToolsExtension } from "./blocks/tools.js";
|
|
38
|
-
import {
|
|
38
|
+
import { createCodingToolsExtension, searchToolsExtension } from "./tools.js";
|
|
39
39
|
import { collectProviders, createProviderMergeSink, } from "./providers/index.js";
|
|
40
40
|
import { EventHub } from "../server/events/hub.js";
|
|
41
|
-
import { providerAuthStatus } from "../server/helpers.js";
|
|
41
|
+
import { providerAuthStatus, storeAgentChange } from "../server/helpers.js";
|
|
42
42
|
import { bootRefresh, DEFAULT_REFRESH_TIMEOUT_MS, refreshProvider, } from "./providers/refresh.js";
|
|
43
|
+
/**
|
|
44
|
+
* The default per-request stream deadline (curator-eval hardening): the
|
|
45
|
+
* OpenAI SDK waits up to 10 minutes for response HEADERS by default, and
|
|
46
|
+
* a request that never reaches the provider never gets headers — the
|
|
47
|
+
* generation task wedges in phase=request. Five minutes covers long
|
|
48
|
+
* first tokens on slow providers (observed 40-60s on the eval models;
|
|
49
|
+
* reasoning models can run far longer once streaming) because the SDK's
|
|
50
|
+
* timer clears at headers arrival, not stream end.
|
|
51
|
+
*/
|
|
52
|
+
export const DEFAULT_STREAM_TIMEOUT_MS = 300_000;
|
|
53
|
+
/**
|
|
54
|
+
* The boot `stream` getter's merge, factored out for the harness-open
|
|
55
|
+
* test: caller seam over file over the default `timeoutMs`, field-wise.
|
|
56
|
+
* Returns `undefined` when nothing is set (the honest empty shape).
|
|
57
|
+
*/
|
|
58
|
+
export function mergeStreamSettings(caller, file) {
|
|
59
|
+
const merged = {
|
|
60
|
+
...(caller ?? file),
|
|
61
|
+
...(caller === undefined && file?.timeoutMs === undefined
|
|
62
|
+
? { timeoutMs: DEFAULT_STREAM_TIMEOUT_MS }
|
|
63
|
+
: {}),
|
|
64
|
+
};
|
|
65
|
+
return Object.keys(merged).length > 0 ? merged : undefined;
|
|
66
|
+
}
|
|
43
67
|
import { startProviderRefresh } from "./providers/scheduler.js";
|
|
44
68
|
import { fileModelsStore } from "./providers/models-store.js";
|
|
45
69
|
import { packagesDirOf, selfPackageRoot } from "./packages.js";
|
|
@@ -55,6 +79,14 @@ import { probeOsUsers } from "../sandbox/availability.js";
|
|
|
55
79
|
import { createAdmissionGuard } from "./identity/users.js";
|
|
56
80
|
import { sweepAccountLinks } from "./identity/link-sweep.js";
|
|
57
81
|
import { SessionMetaDoc } from "../server/session-meta.js";
|
|
82
|
+
import { learnedSkillsDir } from "../curation/paths.js";
|
|
83
|
+
import { SkillStore } from "../curation/store.js";
|
|
84
|
+
import { OverlayStore } from "../curation/overlays.js";
|
|
85
|
+
import { UsageCounters } from "../curation/usage.js";
|
|
86
|
+
import { buildCurationRegistry } from "../curation/targets.js";
|
|
87
|
+
import { createCurationHost } from "../curation/host.js";
|
|
88
|
+
import { createCurationToolFeed, wrapExtensionCurationTools, } from "../curation/wrap.js";
|
|
89
|
+
import { createCurationGateExtension, CURATION_GATE_EXTENSION } from "../curation/gate.js";
|
|
58
90
|
import { probeIsolation, runHelperProbe, selfTestHelper, } from "../sandbox/availability.js";
|
|
59
91
|
import { resolveBuiltins, } from "./builtins.js";
|
|
60
92
|
import { createRegistry } from "./registry.js";
|
|
@@ -346,9 +378,106 @@ export async function openHarness(config, options = {}) {
|
|
|
346
378
|
// ones, which `listSkills` includes by design).
|
|
347
379
|
const skillsOptions = {
|
|
348
380
|
userDir: join(roots.configDir, "skills"),
|
|
381
|
+
// Curator A9 (P6): the canonical workspace ROOT for overlay keys —
|
|
382
|
+
// the configured default workspace (a conversation with a
|
|
383
|
+
// sub-cwd still matches the root-keyed overlay; the accessor
|
|
384
|
+
// stays live for the config's sake).
|
|
385
|
+
workspaceRoot: () => config.workspace.path,
|
|
386
|
+
// Curator A3 (P2): the conversation owner's learned root —
|
|
387
|
+
// `<dataDir>/users/<owner>/skills`, undefined for ownerless
|
|
388
|
+
// conversations. Discovery resolves the owner per render.
|
|
389
|
+
learnedRoot: (owner) => owner === undefined
|
|
390
|
+
? undefined
|
|
391
|
+
: learnedSkillsDir(managedPaths.userAreas(owner)),
|
|
349
392
|
packageRoots: () => collectedSkillRoots,
|
|
350
393
|
sectionMaxBytes: () => skillsSettings?.read().skills?.sectionMaxBytes ?? 4096,
|
|
351
394
|
};
|
|
395
|
+
// Curator A6 (P3/P4): the per-user curation wiring — one skill store,
|
|
396
|
+
// one overlay store and one usage counter set per EXISTING user,
|
|
397
|
+
// created lazily on first use and cached. `userExists` is the LIVE
|
|
398
|
+
// directory read (a removed user's factories stop resolving), and
|
|
399
|
+
// `nameTakenElsewhere` scans the host-side roots the harness owns
|
|
400
|
+
// (the user dir and the collected package roots; a conversation's
|
|
401
|
+
// workspace can only be checked through its env, which the store
|
|
402
|
+
// cannot see — the create-refusal there is the A4 store's own).
|
|
403
|
+
const skillStores = new Map();
|
|
404
|
+
const overlayStores = new Map();
|
|
405
|
+
const usageCounters = new Map();
|
|
406
|
+
const curationOf = (user) => {
|
|
407
|
+
if (user === undefined)
|
|
408
|
+
return undefined;
|
|
409
|
+
// Curator A10: cache-FIRST resolution — a store set captured before
|
|
410
|
+
// a user's deletion stays resolvable, so its own `userExists` gate
|
|
411
|
+
// fails every later op with "user removed" (the A4/A5 fail-closed
|
|
412
|
+
// property: an in-flight run's late write never recreates the
|
|
413
|
+
// roots). A never-seen user still needs the live directory read.
|
|
414
|
+
if (skillStores.get(user) === undefined &&
|
|
415
|
+
identity.users.get(user) === undefined) {
|
|
416
|
+
return undefined;
|
|
417
|
+
}
|
|
418
|
+
const userArea = managedPaths.userAreas(user);
|
|
419
|
+
let store = skillStores.get(user);
|
|
420
|
+
if (store === undefined) {
|
|
421
|
+
store = new SkillStore({
|
|
422
|
+
userArea,
|
|
423
|
+
user,
|
|
424
|
+
now: () => clockNow(),
|
|
425
|
+
nameTakenElsewhere: async (name) => {
|
|
426
|
+
for (const skill of scanFsRoot(join(roots.configDir, "skills"), "user dir")) {
|
|
427
|
+
if (skill.name === name)
|
|
428
|
+
return "user dir";
|
|
429
|
+
}
|
|
430
|
+
for (const root of collectedSkillRoots) {
|
|
431
|
+
if (scanFsRoot(root.dir, "package").some((skill) => skill.name === name))
|
|
432
|
+
return root.extension;
|
|
433
|
+
}
|
|
434
|
+
return undefined;
|
|
435
|
+
},
|
|
436
|
+
userExists: () => identity.users.get(user) !== undefined,
|
|
437
|
+
});
|
|
438
|
+
skillStores.set(user, store);
|
|
439
|
+
}
|
|
440
|
+
let overlays = overlayStores.get(user);
|
|
441
|
+
if (overlays === undefined) {
|
|
442
|
+
overlays = new OverlayStore({
|
|
443
|
+
userArea,
|
|
444
|
+
user,
|
|
445
|
+
userExists: () => identity.users.get(user) !== undefined,
|
|
446
|
+
feed: store,
|
|
447
|
+
});
|
|
448
|
+
overlayStores.set(user, overlays);
|
|
449
|
+
}
|
|
450
|
+
let usage = usageCounters.get(user);
|
|
451
|
+
if (usage === undefined) {
|
|
452
|
+
usage = new UsageCounters({
|
|
453
|
+
userArea,
|
|
454
|
+
user,
|
|
455
|
+
now: () => clockNow(),
|
|
456
|
+
// The A4 gate, but silent: the cached counters outlive the user
|
|
457
|
+
// (cache-first resolution), and telemetry is void-and-forget —
|
|
458
|
+
// a removed user's flush drops instead of recreating the
|
|
459
|
+
// deleted curation directory (deletion's fail-closed claim).
|
|
460
|
+
userExists: () => identity.users.get(user) !== undefined,
|
|
461
|
+
});
|
|
462
|
+
usageCounters.set(user, usage);
|
|
463
|
+
}
|
|
464
|
+
return { store, overlays, usage };
|
|
465
|
+
};
|
|
466
|
+
const curationSkillsOptions = {
|
|
467
|
+
...skillsOptions,
|
|
468
|
+
skillStore: (user) => curationOf(user)?.store,
|
|
469
|
+
overlays: (owner) => curationOf(owner)?.overlays,
|
|
470
|
+
usage: (owner) => curationOf(owner)?.usage,
|
|
471
|
+
};
|
|
472
|
+
// Curator A6 (P4): the read tool counts learned-root reads through the
|
|
473
|
+
// same per-user wiring (load_skill and /skill: count through the
|
|
474
|
+
// skills options above).
|
|
475
|
+
const codingWithUsage = createCodingToolsExtension({
|
|
476
|
+
countersOf: (owner) => curationOf(owner)?.usage,
|
|
477
|
+
learnedRootOf: (owner) => owner === undefined
|
|
478
|
+
? undefined
|
|
479
|
+
: learnedSkillsDir(managedPaths.userAreas(owner)),
|
|
480
|
+
});
|
|
352
481
|
const templateOptions = {
|
|
353
482
|
userDir: join(roots.configDir, "prompts"),
|
|
354
483
|
packageRoots: () => collectedTemplateRoots,
|
|
@@ -371,8 +500,22 @@ export async function openHarness(config, options = {}) {
|
|
|
371
500
|
},
|
|
372
501
|
};
|
|
373
502
|
registry.install(contextFilesExtension);
|
|
374
|
-
|
|
375
|
-
|
|
503
|
+
// Curator A8 (P9): the wrap feed — the mutable set of curation tool
|
|
504
|
+
// names, seeded with the core set and grown by the loader as each
|
|
505
|
+
// extension's curationTargets validate. The skills extension's
|
|
506
|
+
// install below wraps through it (its OWN tools: `skill_manage`);
|
|
507
|
+
// the loader wraps every extension-declared target tool at its
|
|
508
|
+
// install. `isCurationTool` on the built registry (A7) stays the
|
|
509
|
+
// read-side API — the feed is only the write-side wrap oracle.
|
|
510
|
+
const curationFeed = createCurationToolFeed();
|
|
511
|
+
// Curator A6: the skills extension carries the curation wiring
|
|
512
|
+
// (skill_manage + usage counting), and the coding tools count
|
|
513
|
+
// learned-root reads — the same extension NAMES as before, so
|
|
514
|
+
// nothing downstream observes the swap. A8: the install WRAPS the
|
|
515
|
+
// curation tools (skill_manage's registration carries the hint
|
|
516
|
+
// interception; load_skill and the section are untouched).
|
|
517
|
+
registry.install(wrapExtensionCurationTools(createSkillsExtension(curationSkillsOptions), curationFeed));
|
|
518
|
+
registry.install(codingWithUsage);
|
|
376
519
|
registry.install(searchToolsExtension);
|
|
377
520
|
// The chat-blocks tools (UI-blocks U5 Task 1, spec §8.1): every agent
|
|
378
521
|
// can show and update interactive block surfaces in chat — the same
|
|
@@ -466,8 +609,11 @@ export async function openHarness(config, options = {}) {
|
|
|
466
609
|
const sharedStores = sharedExtensionStores(roots, env, identity.users);
|
|
467
610
|
const seamHosts = new Map();
|
|
468
611
|
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, {
|
|
612
|
+
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
613
|
packagesDir,
|
|
614
|
+
// Curator A8 (P9): the wrap feed — the loader grows it with each
|
|
615
|
+
// validated target tool and wraps those tools at their install.
|
|
616
|
+
curationFeed,
|
|
471
617
|
hostContextFactory: (extensionName, kinds, priorHost) => {
|
|
472
618
|
// Two-phase, revised for the provisional identity (provider
|
|
473
619
|
// plugins Task 9 P1): the loader's PRE-import call passes the
|
|
@@ -505,6 +651,11 @@ export async function openHarness(config, options = {}) {
|
|
|
505
651
|
},
|
|
506
652
|
});
|
|
507
653
|
perUserSlugs.push(...loadedPerUserSlugs);
|
|
654
|
+
// The curation-target registry (curator P7): the loader's validated,
|
|
655
|
+
// slug-attributed bindings folded into the ONE registry — the A8
|
|
656
|
+
// wrapping feed (isCurationTool) and the A9 host.curation surface
|
|
657
|
+
// read it. An empty fold is the no-declarer deploy.
|
|
658
|
+
const curation = buildCurationRegistry(curationBindings);
|
|
508
659
|
for (const binding of userServiceBindings) {
|
|
509
660
|
userServices.register(binding.extension, binding.services, binding.packageDir);
|
|
510
661
|
}
|
|
@@ -640,6 +791,11 @@ export async function openHarness(config, options = {}) {
|
|
|
640
791
|
name: "sigrun.jobs",
|
|
641
792
|
tasks: [createScheduleTask(jobRuntimeDeps)],
|
|
642
793
|
});
|
|
794
|
+
// Curator A8 (P6 gate): the hooks-only curator-run gate — installed
|
|
795
|
+
// in the registry but NEVER in the host default selection (the
|
|
796
|
+
// settings getter below filters it like a per-user entry): only A9's
|
|
797
|
+
// producer selects it, in the internal run conversations it creates.
|
|
798
|
+
registry.install(createCurationGateExtension());
|
|
643
799
|
// The host event hub (W3 Task 4, spec §7.4): ONE per opened harness.
|
|
644
800
|
// The event bus's commit-derived events subscribe below (after the bus
|
|
645
801
|
// opens); the provider refresh paths observe readiness here — each
|
|
@@ -828,14 +984,26 @@ export async function openHarness(config, options = {}) {
|
|
|
828
984
|
return callerSettings?.retry ?? fileSettings.retry;
|
|
829
985
|
},
|
|
830
986
|
get stream() {
|
|
831
|
-
|
|
987
|
+
// The stream-request default (curator-eval hardening): a
|
|
988
|
+
// model request with no explicit timeout can wedge forever —
|
|
989
|
+
// the OpenAI SDK's `timeout` is a headers-arrival deadline and
|
|
990
|
+
// defaults to 10 minutes, but a request that never reaches the
|
|
991
|
+
// provider (undici socket-pool class) never gets headers to
|
|
992
|
+
// time out against, so pi-durable's generation task sits in
|
|
993
|
+
// phase=request indefinitely. `mergeStreamSettings` bounds
|
|
994
|
+
// every request with a default `timeoutMs`; the caller seam
|
|
995
|
+
// and the file each override field-wise.
|
|
996
|
+
return mergeStreamSettings(callerSettings?.stream, fileSettings.stream);
|
|
832
997
|
},
|
|
833
998
|
get extensions() {
|
|
834
999
|
// Per-user entries are core-computed from SessionMeta.owner,
|
|
835
1000
|
// never part of a host default — including an operator-supplied
|
|
836
|
-
// caller list (controller ruling: filter BOTH sources).
|
|
1001
|
+
// caller list (controller ruling: filter BOTH sources). The
|
|
1002
|
+
// curator-run gate (A8) is filtered the same way: it is selected
|
|
1003
|
+
// ONLY in the internal run conversations A9's producer creates.
|
|
837
1004
|
const selected = callerSettings?.extensions ?? registry.snapshot().installed();
|
|
838
|
-
return selected.filter((entry) => !isUserEntryName(entry.name)
|
|
1005
|
+
return selected.filter((entry) => !isUserEntryName(entry.name) &&
|
|
1006
|
+
entry.name !== CURATION_GATE_EXTENSION);
|
|
839
1007
|
},
|
|
840
1008
|
toolExecution: callerSettings?.toolExecution,
|
|
841
1009
|
compaction: {
|
|
@@ -920,6 +1088,36 @@ export async function openHarness(config, options = {}) {
|
|
|
920
1088
|
// order). The bus's close stops delivery; the unsubscribe is unused.
|
|
921
1089
|
const now = clockNow;
|
|
922
1090
|
wireActivity(eventBus, harness, now);
|
|
1091
|
+
// Curator A9 (P6): the ONE curation host — the `host.curation`
|
|
1092
|
+
// surface over the per-user stores (curationOf), the curation
|
|
1093
|
+
// registry, the jobs records and the run driver. The server's host
|
|
1094
|
+
// bridge and every seam host expose the SAME instance (the bridge
|
|
1095
|
+
// carries it; the seam hosts delegate). The digest producer's
|
|
1096
|
+
// default agent is the create op's own store-default change (what
|
|
1097
|
+
// every server-created conversation starts from): the structural
|
|
1098
|
+
// `{settings, registry, providerEnv}` subset `storeAgentChange`
|
|
1099
|
+
// reads, closed over the LIVE references so a settings PATCH
|
|
1100
|
+
// reaches later digests without a restart.
|
|
1101
|
+
// SAFETY: the cast — `storeAgentChange` reads exactly these three
|
|
1102
|
+
// members (settings.read, registry.snapshot, providerEnv) and
|
|
1103
|
+
// nothing else of OpenedHarness; the live closure satisfies them.
|
|
1104
|
+
const curationHost = createCurationHost({
|
|
1105
|
+
harness,
|
|
1106
|
+
registry,
|
|
1107
|
+
storage,
|
|
1108
|
+
curation,
|
|
1109
|
+
defaultModel,
|
|
1110
|
+
stores: curationOf,
|
|
1111
|
+
skillsOptions: {
|
|
1112
|
+
userDir: join(roots.configDir, "skills"),
|
|
1113
|
+
packageRoots: () => collectedSkillRoots,
|
|
1114
|
+
},
|
|
1115
|
+
defaultAgent: () => storeAgentChange({
|
|
1116
|
+
settings: { read: () => settingsStore.read() },
|
|
1117
|
+
registry,
|
|
1118
|
+
providerEnv: effectiveEnv,
|
|
1119
|
+
}, defaultModel),
|
|
1120
|
+
});
|
|
923
1121
|
const handlersByName = new Map();
|
|
924
1122
|
for (const binding of runSettledBindings) {
|
|
925
1123
|
handlersByName.set(binding.extension.name, {
|
|
@@ -1176,6 +1374,8 @@ export async function openHarness(config, options = {}) {
|
|
|
1176
1374
|
channelBindings,
|
|
1177
1375
|
gatewayEngine,
|
|
1178
1376
|
skillRoots: extensionSkillRoots,
|
|
1377
|
+
curation,
|
|
1378
|
+
curationHost,
|
|
1179
1379
|
commandExpansion,
|
|
1180
1380
|
eventBus,
|
|
1181
1381
|
events,
|
package/dist/harness/builtins.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Built-in extensions: codemode, MCP, tool search and
|
|
3
|
-
* core — present by default, disableable via settings
|
|
4
|
-
* replaceable by a same-named configured extension.
|
|
2
|
+
* Built-in extensions: codemode, MCP, tool search, OAuth and the curator
|
|
3
|
+
* ship WITH core — present by default, disableable via settings
|
|
4
|
+
* `builtinExtensions`, replaceable by a same-named configured extension.
|
|
5
5
|
*
|
|
6
6
|
* The first three mirror pi 1.3.0's built-in tool surface; OAuth adds
|
|
7
7
|
* sigrun's standard OIDC/OAuth2 login providers without extra setup.
|
|
@@ -38,13 +38,15 @@ import { basename, dirname, isAbsolute, resolve } from "node:path";
|
|
|
38
38
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
39
39
|
import { parsePackageSpecifier, readPackageName } from "./packages.js";
|
|
40
40
|
/**
|
|
41
|
-
* The
|
|
41
|
+
* The six built-ins, in load order (codemode before tool-search: both
|
|
42
42
|
* 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
|
-
*
|
|
43
|
+
* the order pins the tool-collision story should names ever overlap;
|
|
44
|
+
* curator loads LAST — its loops observe every earlier built-in's
|
|
45
|
+
* contributions). Specifiers are the published package names (Task 3)
|
|
46
|
+
* — npm keys a file: dependency by the target's own manifest name, so
|
|
47
|
+
* the key must be the name the packages publish under (the dev shape
|
|
48
|
+
* carries `file:` values under these same keys until Task 8 publishes
|
|
49
|
+
* exact `0.1.0`s).
|
|
48
50
|
*/
|
|
49
51
|
export const BUILTIN_EXTENSIONS = [
|
|
50
52
|
{ key: "codemode", specifier: "@sigrun/ext-codemode", slug: "codemode" },
|
|
@@ -52,6 +54,7 @@ export const BUILTIN_EXTENSIONS = [
|
|
|
52
54
|
{ key: "toolSearch", specifier: "@sigrun/ext-tool-search", slug: "tool-search" },
|
|
53
55
|
{ key: "gateway", specifier: "@sigrun/ext-gateway", slug: "gateway" },
|
|
54
56
|
{ key: "oauth", specifier: "@sigrun/oauth", slug: "oauth" },
|
|
57
|
+
{ key: "curator", specifier: "@sigrun/ext-curator", slug: "curator" },
|
|
55
58
|
];
|
|
56
59
|
/** The core-anchored require (built-ins resolve from core's own node_modules). */
|
|
57
60
|
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
|
package/dist/harness/loader.js
CHANGED
|
@@ -93,6 +93,7 @@ import { satisfies, validRange } from "semver";
|
|
|
93
93
|
import { HOST_RUNTIME_MODULES, hostModuleVersion, readPackageAssetsDir, readPackageName, readPackageVersion, resolveExtensionEntry, } from "./packages.js";
|
|
94
94
|
import { RESERVED_CORE_SECTION_KEYS, RESERVED_EXTENSION_SLUGS, } from "./reserved-sections.js";
|
|
95
95
|
import { slugifyName } from "./stores.js";
|
|
96
|
+
import { wrapExtensionCurationTools } from "../curation/wrap.js";
|
|
96
97
|
/** Every lifecycle field name, in validation order (mirrors the contract doc). */
|
|
97
98
|
const LIFECYCLE_FIELDS = [
|
|
98
99
|
"conversationCreated",
|
|
@@ -217,6 +218,52 @@ function validateWorkers(value) {
|
|
|
217
218
|
* (the §8.2 grammar), and duplicate names within one extension are
|
|
218
219
|
* rejected (they would collide on the `ext:<slug>:<name>` id).
|
|
219
220
|
*/
|
|
221
|
+
/**
|
|
222
|
+
* Validate a factory result's `curationTargets` declaration (curator
|
|
223
|
+
* P7). Absent → undefined (no targets). Present but malformed →
|
|
224
|
+
* throw; the loader quarantines the extension with the thrown message
|
|
225
|
+
* (fail-loud: an extension that THINKS it declared a curation target
|
|
226
|
+
* must not silently lose it). Per target, validation order: name,
|
|
227
|
+
* duplicate qualified name (checked at collection — the slug is
|
|
228
|
+
* unknown here), tools list, guidance, undo (REQUIRED — no undo, no
|
|
229
|
+
* magic), maintain.
|
|
230
|
+
*
|
|
231
|
+
* The REGISTERED-TOOL check (a target listing a tool its extension
|
|
232
|
+
* does not register) and the cross-extension duplicate qualified id
|
|
233
|
+
* live in the load loop, where the registry and the slug are known.
|
|
234
|
+
*/
|
|
235
|
+
function validateCurationTargets(value) {
|
|
236
|
+
if (value === undefined)
|
|
237
|
+
return undefined;
|
|
238
|
+
if (!Array.isArray(value)) {
|
|
239
|
+
throw new Error("extension factory returned a curationTargets field that is not an array");
|
|
240
|
+
}
|
|
241
|
+
const targets = [];
|
|
242
|
+
for (const entry of value) {
|
|
243
|
+
const target = entry;
|
|
244
|
+
const name = target?.name;
|
|
245
|
+
if (typeof name !== "string" || name.length === 0) {
|
|
246
|
+
throw new Error("extension factory returned a curation target without a name");
|
|
247
|
+
}
|
|
248
|
+
const tools = target?.tools;
|
|
249
|
+
if (!Array.isArray(tools) ||
|
|
250
|
+
tools.some((tool) => typeof tool !== "string" || tool.length === 0)) {
|
|
251
|
+
throw new Error(`extension factory returned a curation target "${name}" whose tools is not an array of non-empty strings`);
|
|
252
|
+
}
|
|
253
|
+
if (typeof target?.guidance !== "function") {
|
|
254
|
+
throw new Error(`extension factory returned a curation target "${name}" whose guidance is not a function`);
|
|
255
|
+
}
|
|
256
|
+
if (typeof target?.undo !== "function") {
|
|
257
|
+
throw new Error(`curation target "${name}" has no undo`);
|
|
258
|
+
}
|
|
259
|
+
if (target?.maintain !== undefined &&
|
|
260
|
+
typeof target.maintain !== "function") {
|
|
261
|
+
throw new Error(`extension factory returned a curation target "${name}" whose maintain is not a function`);
|
|
262
|
+
}
|
|
263
|
+
targets.push(entry);
|
|
264
|
+
}
|
|
265
|
+
return targets;
|
|
266
|
+
}
|
|
220
267
|
function validateUserServices(value) {
|
|
221
268
|
if (value === undefined)
|
|
222
269
|
return undefined;
|
|
@@ -1204,6 +1251,10 @@ async function resolvedFromFactory(factory, hostContext) {
|
|
|
1204
1251
|
}
|
|
1205
1252
|
// `workers` is a contract key — the candidate read needs no cast.
|
|
1206
1253
|
const workers = validateWorkers(candidate.workers);
|
|
1254
|
+
// Curation targets (curator P7): validated without the slug (the
|
|
1255
|
+
// qualified-id duplicate check runs in the load loop where the slug
|
|
1256
|
+
// and the already-claimed ids are known).
|
|
1257
|
+
const curationTargets = validateCurationTargets(candidate.curationTargets);
|
|
1207
1258
|
return {
|
|
1208
1259
|
...(candidate.perUserTools === undefined
|
|
1209
1260
|
? {}
|
|
@@ -1244,6 +1295,7 @@ async function resolvedFromFactory(factory, hostContext) {
|
|
|
1244
1295
|
? {}
|
|
1245
1296
|
: { ambientKinds: candidate.ambientKinds }),
|
|
1246
1297
|
...(workers === undefined ? {} : { workers }),
|
|
1298
|
+
...(curationTargets === undefined ? {} : { curationTargets }),
|
|
1247
1299
|
...(providers === undefined ? {} : { providers }),
|
|
1248
1300
|
...(loginProviders === undefined ? {} : { loginProviders }),
|
|
1249
1301
|
...(webuiBundles === undefined ? {} : { webuiBundles }),
|
|
@@ -1438,6 +1490,10 @@ export async function loadExtensions(specifiers, registry, options = {}) {
|
|
|
1438
1490
|
const settingsChangedBindings = [];
|
|
1439
1491
|
const lifecycleBindings = [];
|
|
1440
1492
|
const workerBindings = [];
|
|
1493
|
+
// Curation targets (curator P7): slug-attributed declarations, one
|
|
1494
|
+
// entry per loaded extension whose result carried them — boot folds
|
|
1495
|
+
// these into the ONE CurationRegistry (src/curation/targets.ts).
|
|
1496
|
+
const curationBindings = [];
|
|
1441
1497
|
const userServiceBindings = [];
|
|
1442
1498
|
// Jobs Task 3: declared jobs, attributed to the extension slug (the
|
|
1443
1499
|
// `ext:<slug>:<name>` id's namespace).
|
|
@@ -1475,6 +1531,11 @@ export async function loadExtensions(specifiers, registry, options = {}) {
|
|
|
1475
1531
|
// Slug → declared name, for collision rejection at load (this boot's
|
|
1476
1532
|
// loaded set only — quarantined extensions never claim a slug).
|
|
1477
1533
|
const slugs = new Map();
|
|
1534
|
+
// Qualified curation-target ids ("<slug>:<name>", curator P7): the same
|
|
1535
|
+
// claimed-only-by-LOADED pattern as `slugs` — a target id names ONE
|
|
1536
|
+
// target, so a duplicate quarantines the later declarer instead of
|
|
1537
|
+
// shadowing (never last-wins).
|
|
1538
|
+
const curationIds = new Set();
|
|
1478
1539
|
// Channel id → slug (gateway C1): the same claimed-only-by-LOADED pattern
|
|
1479
1540
|
// as `slugs`. Chat bindings are keyed by channel id, so a shadowing
|
|
1480
1541
|
// adapter would inherit another adapter's chats — the collision
|
|
@@ -1695,6 +1756,32 @@ export async function loadExtensions(specifiers, registry, options = {}) {
|
|
|
1695
1756
|
// install, exactly like the channel-id collision — a second engine
|
|
1696
1757
|
// leaves no registry entry, no slug claim, and no binding. First
|
|
1697
1758
|
// loaded wins.
|
|
1759
|
+
// Curation targets (curator P7): the two slug-dependent rules
|
|
1760
|
+
// check HERE — after the slug is final, BEFORE the registry
|
|
1761
|
+
// install (a violation leaves no entry, no slug claim, no
|
|
1762
|
+
// binding — the same point as the one-engine rule below).
|
|
1763
|
+
// A target's tools list must name tools THIS extension
|
|
1764
|
+
// registers (A8's wrapping rewrites registrations — an
|
|
1765
|
+
// unregistered name would wrap nothing); a qualified id
|
|
1766
|
+
// ("<slug>:<name>") names ONE target.
|
|
1767
|
+
if (resolved.curationTargets !== undefined) {
|
|
1768
|
+
const registered = new Set((extension?.tools ?? []).map((tool) => tool.name));
|
|
1769
|
+
// Claim each id AS it validates: an intra-list duplicate
|
|
1770
|
+
// (the same name twice in one result) hits the check on
|
|
1771
|
+
// its SECOND arrival, quarantining before any install.
|
|
1772
|
+
const claimed = new Set();
|
|
1773
|
+
for (const target of resolved.curationTargets) {
|
|
1774
|
+
const id = `${slug}:${target.name}`;
|
|
1775
|
+
if (curationIds.has(id) || claimed.has(id)) {
|
|
1776
|
+
throw new Error(`duplicate curation target id "${id}" — a qualified id names one curation target`);
|
|
1777
|
+
}
|
|
1778
|
+
claimed.add(id);
|
|
1779
|
+
const unregistered = target.tools.find((tool) => !registered.has(tool));
|
|
1780
|
+
if (unregistered !== undefined) {
|
|
1781
|
+
throw new Error(`extension "${slug}" declares curation target "${target.name}" listing tool "${unregistered}" it does not register`);
|
|
1782
|
+
}
|
|
1783
|
+
}
|
|
1784
|
+
}
|
|
1698
1785
|
if (resolved.gateway !== undefined) {
|
|
1699
1786
|
if (gatewayEngine !== undefined) {
|
|
1700
1787
|
throw new Error(`gateway engine already declared by "${gatewayEngine.extension}"`);
|
|
@@ -1702,7 +1789,23 @@ export async function loadExtensions(specifiers, registry, options = {}) {
|
|
|
1702
1789
|
gatewayEngine = { extension: slug, engine: resolved.gateway };
|
|
1703
1790
|
}
|
|
1704
1791
|
if (extension !== undefined) {
|
|
1705
|
-
|
|
1792
|
+
// Curator A8 (P9): grow the wrap feed with this extension's
|
|
1793
|
+
// just-validated target tools, then WRAP every curation-tool
|
|
1794
|
+
// registration at the install — its own target tools, plus any
|
|
1795
|
+
// feed name it registers (a non-core `skill_manage`). Growth
|
|
1796
|
+
// lives HERE, after every quarantining check, so the feed
|
|
1797
|
+
// never learns a tool whose extension failed the load. The
|
|
1798
|
+
// bindings below keep pairing with the UNWRAPPED object: they
|
|
1799
|
+
// carry identity, never tools, and the wrap changes only
|
|
1800
|
+
// `execute`.
|
|
1801
|
+
if (options.curationFeed !== undefined &&
|
|
1802
|
+
resolved.curationTargets !== undefined) {
|
|
1803
|
+
for (const target of resolved.curationTargets) {
|
|
1804
|
+
for (const tool of target.tools)
|
|
1805
|
+
options.curationFeed.add(tool);
|
|
1806
|
+
}
|
|
1807
|
+
}
|
|
1808
|
+
registry.install(wrapExtensionCurationTools(extension, options.curationFeed));
|
|
1706
1809
|
}
|
|
1707
1810
|
if (resolved.perUserTools === true)
|
|
1708
1811
|
perUserSlugs.push(slug);
|
|
@@ -1773,6 +1876,15 @@ export async function loadExtensions(specifiers, registry, options = {}) {
|
|
|
1773
1876
|
workers: resolved.workers,
|
|
1774
1877
|
});
|
|
1775
1878
|
}
|
|
1879
|
+
if (resolved.curationTargets !== undefined) {
|
|
1880
|
+
curationBindings.push({
|
|
1881
|
+
extension: slug,
|
|
1882
|
+
targets: resolved.curationTargets,
|
|
1883
|
+
});
|
|
1884
|
+
for (const target of resolved.curationTargets) {
|
|
1885
|
+
curationIds.add(`${slug}:${target.name}`);
|
|
1886
|
+
}
|
|
1887
|
+
}
|
|
1776
1888
|
if (resolved.jobs !== undefined) {
|
|
1777
1889
|
jobBindings.push({ extension: slug, jobs: resolved.jobs });
|
|
1778
1890
|
}
|
|
@@ -1890,6 +2002,7 @@ export async function loadExtensions(specifiers, registry, options = {}) {
|
|
|
1890
2002
|
settingsChangedBindings,
|
|
1891
2003
|
lifecycleBindings,
|
|
1892
2004
|
workerBindings,
|
|
2005
|
+
curationBindings,
|
|
1893
2006
|
jobBindings,
|
|
1894
2007
|
routeBindings,
|
|
1895
2008
|
commandBindings,
|
package/dist/harness/naming.js
CHANGED
|
@@ -104,6 +104,12 @@ export function createAutoNamer(deps) {
|
|
|
104
104
|
// skip sits next to the isTaskOwned check per the plan.
|
|
105
105
|
if (meta?.source === "cron")
|
|
106
106
|
return;
|
|
107
|
+
// Internal conversations (curator P5) are never named: they are
|
|
108
|
+
// invisible on every owner-keyed surface, and a name would exist
|
|
109
|
+
// only to be hidden. The bus already withholds the settle event;
|
|
110
|
+
// this is the belt-and-braces read-side gate.
|
|
111
|
+
if (meta?.internal === true)
|
|
112
|
+
return;
|
|
107
113
|
// An absent doc (never written — reads serve nothing, defaults are
|
|
108
114
|
// free) is an unnamed conversation with zero attempts: name it.
|
|
109
115
|
if (meta?.name !== undefined || meta?.autoName !== undefined)
|