@sigrun/sigrun 0.4.1 → 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.
Files changed (56) hide show
  1. package/CHANGELOG.md +45 -1
  2. package/README.md +46 -23
  3. package/dist/commands/expand.js +37 -5
  4. package/dist/curation/attribution.js +135 -0
  5. package/dist/curation/deletion.js +32 -0
  6. package/dist/curation/gate.js +50 -0
  7. package/dist/curation/host.js +342 -0
  8. package/dist/curation/internal.js +54 -0
  9. package/dist/curation/names.js +5 -0
  10. package/dist/curation/overlays.js +325 -0
  11. package/dist/curation/paths.js +17 -0
  12. package/dist/curation/run-doc.js +64 -0
  13. package/dist/curation/runs.js +579 -0
  14. package/dist/curation/scan.js +30 -0
  15. package/dist/curation/skill-tool.js +308 -0
  16. package/dist/curation/store.js +444 -0
  17. package/dist/curation/targets.js +77 -0
  18. package/dist/curation/usage.js +124 -0
  19. package/dist/curation/wrap.js +212 -0
  20. package/dist/extension.d.ts +282 -0
  21. package/dist/harness/boot.js +209 -9
  22. package/dist/harness/builtins.js +12 -9
  23. package/dist/harness/event-bus.js +64 -0
  24. package/dist/harness/loader.js +114 -1
  25. package/dist/harness/naming.js +6 -0
  26. package/dist/harness/packages.js +14 -9
  27. package/dist/harness/reserved-sections.js +2 -0
  28. package/dist/harness/seam-host.js +139 -26
  29. package/dist/harness/settings.js +2 -2
  30. package/dist/harness/tools.js +70 -0
  31. package/dist/jobs/fire.js +33 -11
  32. package/dist/jobs/skills-refs.js +40 -0
  33. package/dist/sandbox/policy.js +14 -0
  34. package/dist/server/blocks/answers.js +9 -3
  35. package/dist/server/conversation-ops.js +21 -3
  36. package/dist/server/helpers.js +1 -1
  37. package/dist/server/host-bridge.js +6 -1
  38. package/dist/server/notifications.js +6 -0
  39. package/dist/server/routes/meta.js +8 -3
  40. package/dist/server/routes/submissions.js +5 -0
  41. package/dist/server/routes/surfaces.js +3 -1
  42. package/dist/server/routes/users.js +7 -0
  43. package/dist/server/schema-fixture.js +168 -0
  44. package/dist/server/session-meta.js +12 -10
  45. package/dist/settings/core-sections.js +9 -0
  46. package/dist/settings/registry.js +6 -0
  47. package/dist/skills/index.js +88 -19
  48. package/node_modules/@sigrun/shared/CHANGELOG.md +9 -1
  49. package/node_modules/@sigrun/shared/dist/access.d.ts +3 -0
  50. package/node_modules/@sigrun/shared/dist/access.js +8 -0
  51. package/node_modules/@sigrun/shared/dist/session-meta.d.ts +15 -4
  52. package/node_modules/@sigrun/shared/dist/settings.d.ts +7 -5
  53. package/node_modules/@sigrun/shared/package.json +3 -2
  54. package/package.json +9 -7
  55. package/webui/dist/assets/{index-DWmsAQl2.js → index-BtczbUXo.js} +1 -1
  56. package/webui/dist/index.html +1 -1
@@ -20,6 +20,7 @@ import { AgentDoc, configure } from "@earendil-works/pi-durable";
20
20
  import { accessOf, isSharing, } from "@sigrun/shared/access";
21
21
  import { extname } from "node:path";
22
22
  import { touchActivity } from "../harness/activity.js";
23
+ import { ensureRequestId, recordAuthor, } from "../curation/attribution.js";
23
24
  import { resolveServedModelRefreshing } from "../harness/providers/refresh.js";
24
25
  import { isSuperuser } from "./auth/principal.js";
25
26
  import { isConfinedPath, parseSubmissionInput } from "./content-parts.js";
@@ -247,6 +248,9 @@ export async function submitOp(ctx, conversation, input, opts = {}) {
247
248
  if (!parsed.ok) {
248
249
  throw new ConversationOpError({ error: "invalid_input" });
249
250
  }
251
+ // Every input carries a requestId (curator P1): the caller's own id, or
252
+ // a generated `sigrun:` UUID — the pre-admission attribution key.
253
+ const requestId = ensureRequestId(opts.requestId);
250
254
  // Spec §2/§3 binding contract: a conversation pinned to an unserved
251
255
  // model fails explicitly AT SUBMIT — 4xx with the available ids —
252
256
  // never a silent fallback, and never a settled no_model run surfaced
@@ -316,7 +320,11 @@ export async function submitOp(ctx, conversation, input, opts = {}) {
316
320
  else {
317
321
  try {
318
322
  const env = await envThunk();
319
- expansion = await expandInput(parsed.parts, opened.commandExpansion, env, BACKGROUND_CONTEXT);
323
+ // Curator A3: the learned root resolves through the conversation
324
+ // OWNER (SessionMetaDoc), not the submitting user — a shared-write
325
+ // submitter expands against the owner's learned skills.
326
+ const meta = await opened.harness.snapshot(SessionMetaDoc, conversation.id, BACKGROUND_CONTEXT);
327
+ expansion = await expandInput(parsed.parts, opened.commandExpansion, env, BACKGROUND_CONTEXT, meta?.owner);
320
328
  }
321
329
  catch (error) {
322
330
  if (error instanceof CommandUsageError) {
@@ -356,13 +364,20 @@ export async function submitOp(ctx, conversation, input, opts = {}) {
356
364
  const draft = {
357
365
  type: "input",
358
366
  content,
359
- ...(opts.requestId === undefined ? {} : { requestId: opts.requestId }),
367
+ requestId,
360
368
  ...(whenBusy === undefined ? {} : { whenBusy: whenBusy }),
361
369
  ...(entry === undefined ? {} : { entry }),
362
370
  };
371
+ // Attribution (curator P1), two writes per input: `byRequest` BEFORE the
372
+ // admission (the requestId exists pre-admission, so this never races; an
373
+ // orphan record from a failed submit is harmless) and `bySubmission`
374
+ // right after it returns — the key the live run's input ids resolve
375
+ // against. An `undefined` user records nothing (unattributed).
376
+ await recordAuthor(conversation, { requestId }, opts.user, BACKGROUND_CONTEXT);
363
377
  // The submit pass-through is retired (§6.7): the memory seam's
364
378
  // runSettled owns capture, so submissions admit directly.
365
379
  const submission = await conversation.submit(draft, BACKGROUND_CONTEXT);
380
+ await recordAuthor(conversation, { submissionId: submission.id }, opts.user, BACKGROUND_CONTEXT);
366
381
  // Admission stamps activity: the sidebar's activity sort must move the
367
382
  // moment a prompt lands — before the run settles.
368
383
  await touchActivity(opened.harness, conversation.id, opened.now, BACKGROUND_CONTEXT);
@@ -394,7 +409,10 @@ export async function listConversationsOp(ctx, caller, filters = {}) {
394
409
  // flags, and the create source (spec §9.5).
395
410
  const meta = await opened.harness.snapshot(SessionMetaDoc, record.id, BACKGROUND_CONTEXT);
396
411
  const access = accessOf(caller, meta ?? {});
397
- // Hidden is absent from the list (spec §5.9).
412
+ // Hidden is absent from the list (spec §5.9) — and internal means
413
+ // hidden for EVERY caller (curator P5, accessOf rule 0): the
414
+ // superuser and the bridge's trusted read are covered by the same
415
+ // line, so no caller-specific skip can drift.
398
416
  if (access === "none")
399
417
  continue;
400
418
  // The archived filter (absent = all).
@@ -86,7 +86,7 @@ function autoEnableRemovals(settings, env) {
86
86
  const server = entry;
87
87
  if (server.enabled === false || server.disabled === true)
88
88
  continue;
89
- // @sigrun/ext-mcp's server default is "codemode"; its pi-compat
89
+ // @sigrun/ext-mcp's server default is "codemode"; its deprecated
90
90
  // alias `codemode-deferred` normalizes to "codemode".
91
91
  const exposure = server.exposure === "codemode-deferred"
92
92
  ? "codemode"
@@ -59,11 +59,13 @@ export function createHostBridge(ctx) {
59
59
  const conversation = await conversationOf(id);
60
60
  // `user` is accepted and NOT verified: extensions own their
61
61
  // user mapping (spec §9.5) — a channel's unmapped user submits
62
- // fine, and owner attribution happened at create.
62
+ // fine, and owner attribution happened at create. P1 records
63
+ // it verbatim as the input's author.
63
64
  return await submitOp(ctx, conversation, input, {
64
65
  ...(opts?.requestId === undefined
65
66
  ? {}
66
67
  : { requestId: opts.requestId }),
68
+ ...(opts?.user === undefined ? {} : { user: opts.user }),
67
69
  ...(opts?.whenBusy === undefined
68
70
  ? {}
69
71
  : { whenBusy: opts.whenBusy }),
@@ -284,5 +286,8 @@ export function createHostBridge(ctx) {
284
286
  ctx.surfaces.bumpRev(slug, id, key, update);
285
287
  },
286
288
  },
289
+ // Curator P6 (A9): the boot-built curation host, verbatim — the
290
+ // ONE instance every seam host reaches through this bridge.
291
+ curation: ctx.opened.curationHost,
287
292
  };
288
293
  }
@@ -132,6 +132,12 @@ export function startNotifications(ctx) {
132
132
  if (mode === "never")
133
133
  return;
134
134
  const meta = await metaOf(conversationId);
135
+ // Curator P5: an internal run never notifies (no "Reply ready" for
136
+ // curator work). The bus already withholds the host event; this is
137
+ // the belt-and-braces read-side gate (a hand-published or future
138
+ // emit path cannot leak the run).
139
+ if (meta?.internal === true)
140
+ return;
135
141
  if (mode === "cron" && meta?.source !== "cron")
136
142
  return;
137
143
  if (mode === "unwatched" &&
@@ -178,8 +178,8 @@ export function registerMetaRoutes(app, ctx) {
178
178
  // templates appear the moment their files land, no reload command. The
179
179
  // core + extension halves remain boot constants; see `commands.ts` for
180
180
  // the scope rulings.
181
- app.get("/v1/commands", async () => {
182
- // The workspace root (the first of the three) reads through a
181
+ app.get("/v1/commands", async (request) => {
182
+ // The workspace root (the first of the four) reads through a
183
183
  // workspace env, exactly like skill discovery at generation time;
184
184
  // the catalog scan uses the root conversation's env — the static
185
185
  // workspace every conversation resolves to today.
@@ -188,7 +188,12 @@ export function registerMetaRoutes(app, ctx) {
188
188
  if (env === undefined) {
189
189
  throw new Error("envBuilder returned no ExecutionEnv");
190
190
  }
191
- const skills = await listSkills(env, BACKGROUND_CONTEXT, expansion.skills);
191
+ // Curator A3: learned skills are the CALLER's — a user's `/skill:`
192
+ // menu shows their own learned skills and nobody else's (an
193
+ // unidentified caller sees none).
194
+ const skills = await listSkills(env, BACKGROUND_CONTEXT, expansion.skills, {
195
+ owner: request.principal.user,
196
+ });
192
197
  const templates = expandableTemplates(await discoverTemplates(env, BACKGROUND_CONTEXT, expansion.templates), expansion.commandNames());
193
198
  return {
194
199
  commands: [
@@ -46,6 +46,11 @@ export function registerSubmissionRoutes(app, ctx) {
46
46
  ...(body.requestId === undefined
47
47
  ? {}
48
48
  : { requestId: String(body.requestId) }),
49
+ // Attribution (curator P1): the resolved principal's user is
50
+ // the input's author; an unidentified caller records nothing.
51
+ ...(request.principal.user === undefined
52
+ ? {}
53
+ : { user: request.principal.user }),
49
54
  ...(body.whenBusy === undefined ? {} : { whenBusy: body.whenBusy }),
50
55
  ...(request.query?.waitMs ===
51
56
  undefined
@@ -508,7 +508,9 @@ export function registerSurfaceRoutes(app, ctx) {
508
508
  if (chatActionStartsTurn(block.tree, body.action)) {
509
509
  const answerPayload = (body.payload ?? null);
510
510
  const wrapped = { ok: true, rev: block.state.rev };
511
- await answerBlock(ctx, id, entryId, body.action, answerPayload, actionId);
511
+ await answerBlock(ctx, id, entryId, body.action, answerPayload, actionId,
512
+ // The acting user authors the answer turn (curator P1).
513
+ request.principal.user);
512
514
  if (actionId !== undefined) {
513
515
  ctx.surfaces.chatDedupRecord(actionId, address, fingerprint, wrapped);
514
516
  }
@@ -16,6 +16,7 @@ import { withClaimLock } from "../auth/claim-lock.js";
16
16
  import { effectiveAdmin, storedGroups } from "../../harness/identity/groups.js";
17
17
  import { transferOwnership } from "../ownership.js";
18
18
  import { validateGrantPath } from "../../sandbox/grants.js";
19
+ import { removeUserCuration } from "../../curation/deletion.js";
19
20
  /** The wire form of one directory user. `admin` stays the STORED flag;
20
21
  * `groupAdmin` is DERIVED from stored groups and retained for one release
21
22
  * so the users UI keeps rendering. */
@@ -265,6 +266,12 @@ export function registerUserRoutes(app, ctx) {
265
266
  // §10.6 step 5: data/state stay for the recipient; core owns only
266
267
  // the departing user's credentials, settings, and every cache slug.
267
268
  await ctx.opened.extensionStores.removeUserData(user);
269
+ // Curator A10 (P2): the departing user's learned skills and curation
270
+ // state are REMOVED, never transferred — a skill distilled from
271
+ // their conversations can carry their paths and preferences, so it
272
+ // must not load for the recipient. In-flight curator runs fail
273
+ // closed through the stores' user-exists gate (nothing recreated).
274
+ await removeUserCuration(ctx.managedPaths.userAreas(user));
268
275
  return await reply.code(204).send();
269
276
  });
270
277
  }
@@ -336,6 +336,174 @@ export const VIEW_SCHEMA = {
336
336
  }
337
337
  }
338
338
  },
339
+ {
340
+ "kind": "object",
341
+ "fields": {
342
+ "name": {
343
+ "kind": "string"
344
+ },
345
+ "description": {
346
+ "kind": "string"
347
+ },
348
+ "parameters": {
349
+ "kind": "object",
350
+ "fields": {
351
+ "type": {
352
+ "kind": "string"
353
+ },
354
+ "required": {
355
+ "kind": "array",
356
+ "variants": [
357
+ {
358
+ "kind": "string"
359
+ }
360
+ ]
361
+ },
362
+ "properties": {
363
+ "kind": "object",
364
+ "fields": {
365
+ "action": {
366
+ "kind": "object",
367
+ "fields": {
368
+ "anyOf": {
369
+ "kind": "array",
370
+ "variants": [
371
+ {
372
+ "kind": "object",
373
+ "fields": {
374
+ "type": {
375
+ "kind": "string"
376
+ },
377
+ "const": {
378
+ "kind": "string"
379
+ }
380
+ }
381
+ }
382
+ ]
383
+ },
384
+ "description": {
385
+ "kind": "string"
386
+ }
387
+ }
388
+ },
389
+ "name": {
390
+ "kind": "object",
391
+ "fields": {
392
+ "type": {
393
+ "kind": "string"
394
+ },
395
+ "description": {
396
+ "kind": "string"
397
+ }
398
+ }
399
+ },
400
+ "file_path": {
401
+ "kind": "object",
402
+ "fields": {
403
+ "type": {
404
+ "kind": "string"
405
+ },
406
+ "description": {
407
+ "kind": "string"
408
+ }
409
+ }
410
+ },
411
+ "content": {
412
+ "kind": "object",
413
+ "fields": {
414
+ "type": {
415
+ "kind": "string"
416
+ },
417
+ "description": {
418
+ "kind": "string"
419
+ }
420
+ }
421
+ },
422
+ "old_text": {
423
+ "kind": "object",
424
+ "fields": {
425
+ "type": {
426
+ "kind": "string"
427
+ },
428
+ "description": {
429
+ "kind": "string"
430
+ }
431
+ }
432
+ },
433
+ "new_text": {
434
+ "kind": "object",
435
+ "fields": {
436
+ "type": {
437
+ "kind": "string"
438
+ },
439
+ "description": {
440
+ "kind": "string"
441
+ }
442
+ }
443
+ },
444
+ "absorbed_into": {
445
+ "kind": "object",
446
+ "fields": {
447
+ "type": {
448
+ "kind": "string"
449
+ },
450
+ "description": {
451
+ "kind": "string"
452
+ }
453
+ }
454
+ },
455
+ "reason": {
456
+ "kind": "object",
457
+ "fields": {
458
+ "type": {
459
+ "kind": "string"
460
+ },
461
+ "description": {
462
+ "kind": "string"
463
+ }
464
+ }
465
+ },
466
+ "overlay_root": {
467
+ "kind": "object",
468
+ "fields": {
469
+ "anyOf": {
470
+ "kind": "array",
471
+ "variants": [
472
+ {
473
+ "kind": "object",
474
+ "fields": {
475
+ "type": {
476
+ "kind": "string"
477
+ },
478
+ "const": {
479
+ "kind": "string"
480
+ }
481
+ }
482
+ }
483
+ ]
484
+ },
485
+ "description": {
486
+ "kind": "string"
487
+ }
488
+ }
489
+ },
490
+ "remove": {
491
+ "kind": "object",
492
+ "fields": {
493
+ "type": {
494
+ "kind": "string"
495
+ },
496
+ "description": {
497
+ "kind": "string"
498
+ }
499
+ }
500
+ }
501
+ }
502
+ }
503
+ }
504
+ }
505
+ }
506
+ },
339
507
  {
340
508
  "kind": "object",
341
509
  "fields": {
@@ -29,10 +29,10 @@
29
29
  * other's fields (the mutation line serializes commits).
30
30
  *
31
31
  * Server-owned fields (`autoName`, `autoNameAttempts`,
32
- * `lastActivityAt`, `jobId`, `deliver`) are written by host code only
33
- * (activity.ts, the A2 auto-namer, fire.ts's job stamp); they are NOT
34
- * patch surface — the strict validator keeps rejecting them as
35
- * `invalid_field`.
32
+ * `lastActivityAt`, `jobId`, `deliver`, `internal`) are written by host
33
+ * code only (activity.ts, the A2 auto-namer, fire.ts's job stamp, the
34
+ * curator's creating commit); they are NOT patch surface — the strict
35
+ * validator keeps rejecting them as `invalid_field`.
36
36
  */
37
37
  import { defineDoc } from "@earendil-works/pi-durable";
38
38
  import { isSharing } from "@sigrun/shared/access";
@@ -158,12 +158,14 @@ export function validateSessionMetaPatch(body) {
158
158
  case "source":
159
159
  case "owner":
160
160
  case "jobId":
161
- case "deliver": {
162
- // Server-only (spec §5.9 + gateway C7): the KEY itself is the
163
- // client error, whatever the value — even a `null` clear. The
164
- // job stamp fields move with the fire (fire.ts's creating
165
- // commit) and the delivery targets ride the job spec, never a
166
- // meta patch.
161
+ case "deliver":
162
+ case "internal": {
163
+ // Server-only (spec §5.9 + gateway C7 + curator P5): the KEY
164
+ // itself is the client error, whatever the value — even a `null`
165
+ // clear. The job stamp fields move with the fire (fire.ts's
166
+ // creating commit) and the delivery targets ride the job spec,
167
+ // never a meta patch; the internal-run marker moves with the
168
+ // curator's creating commit (markInternal) alone.
167
169
  return { error: "invalid_field", field: key };
168
170
  }
169
171
  case "sharing": {
@@ -504,6 +504,15 @@ export const CORE_SETTINGS_SECTIONS = [
504
504
  help: "The built-in OIDC/OAuth2 login extension — applies at restart",
505
505
  apply: applyOf("builtinExtensions.oauth"),
506
506
  },
507
+ {
508
+ type: "toggle",
509
+ // Absent means ON (the built-in set defaults enabled).
510
+ default: true,
511
+ name: "builtinExtensions.curator",
512
+ label: "Curator",
513
+ help: "The built-in curator extension — applies at restart",
514
+ apply: applyOf("builtinExtensions.curator"),
515
+ },
507
516
  ],
508
517
  },
509
518
  ],
@@ -175,6 +175,12 @@ export const CORE_SETTING_KEYS = [
175
175
  section: "extensions",
176
176
  reason: "the built-in set resolves at boot (a disabled built-in never loads)",
177
177
  },
178
+ {
179
+ key: "builtinExtensions.curator",
180
+ apply: "restart",
181
+ section: "extensions",
182
+ reason: "the built-in set resolves at boot (a disabled built-in never loads)",
183
+ },
178
184
  {
179
185
  key: "autoEnableCodemode",
180
186
  apply: "live",