@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.
- package/CHANGELOG.md +45 -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 +9 -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 +3 -2
- 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/CHANGELOG.md
CHANGED
|
@@ -5,7 +5,50 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
-
## [
|
|
8
|
+
## [0.5.0] - 2026-11-02
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- **The curator** (sixth built-in, `@sigrun/ext-curator`): per-user
|
|
12
|
+
curation over learned skills and curation targets. Message attribution
|
|
13
|
+
records the submitting user per input; fork reviews (loop 1) run after
|
|
14
|
+
conversation activity past a threshold; the idle maintenance pass
|
|
15
|
+
(loop 2, the `curator-tick` job) digests mixed-authorship
|
|
16
|
+
conversations — per-entry attribution keeps other users' turns out of
|
|
17
|
+
the digest input; the versioned per-user skill store (feed, pins,
|
|
18
|
+
content scan, undo); usage telemetry; internal runs hidden from every
|
|
19
|
+
owner-keyed path; the curation-target contract (`@sigrun/memory-markdown`
|
|
20
|
+
0.2.0 is the first target: per-user USER.md/MEMORY.md with hard limits
|
|
21
|
+
and versions); notices + the undo view; hint-by-wrapping interception
|
|
22
|
+
with provable authorship.
|
|
23
|
+
- A default `stream.timeoutMs` (5 minutes) via the boot settings getter:
|
|
24
|
+
a model request with no explicit timeout could wedge forever (the
|
|
25
|
+
OpenAI SDK's timeout is a headers-arrival deadline; a request that
|
|
26
|
+
never reaches the provider never gets headers). Caller seam and
|
|
27
|
+
settings file override field-wise.
|
|
28
|
+
|
|
29
|
+
### Fixed
|
|
30
|
+
- The seam host's pre-bind `curation.skills(user)`/`overlays(user)`
|
|
31
|
+
returned a promise of the store instead of a store (loop 2's archive
|
|
32
|
+
step read `.list` off a promise). A lazy store proxy now preserves the
|
|
33
|
+
sync-store contract through the pre-bind window.
|
|
34
|
+
|
|
35
|
+
### Changed
|
|
36
|
+
- Reworded the `pi` manifest handling so it no longer implies pi
|
|
37
|
+
extension compatibility. Sigrun reads `pi.extensions`/`pi.skills` only
|
|
38
|
+
to LOCATE files and shares the SKILL.md format; it does not implement
|
|
39
|
+
pi's `ExtensionAPI` (a pi extension's `registerTool`/`registerCommand`
|
|
40
|
+
have no counterpart here). `docs/extending.md` gains a "Not pi
|
|
41
|
+
extensions" section stating the two ABIs are unrelated and that only
|
|
42
|
+
the skills layer transfers. The `.ts` skip and empty-entry warnings now
|
|
43
|
+
say `pi manifest:` instead of `pi-compat:`.
|
|
44
|
+
|
|
45
|
+
## [0.4.2] - 2026-10-31
|
|
46
|
+
|
|
47
|
+
### Fixed
|
|
48
|
+
- `@sigrun/shared` and `@sigrun/tui` tarballs ship their `dist/`
|
|
49
|
+
again (a missing `prepublishOnly` published four-file, dist-less
|
|
50
|
+
packages since 0.2.0 — project installs quarantined `ext-gateway` on
|
|
51
|
+
`shared/dist/live.js`).
|
|
9
52
|
|
|
10
53
|
## [0.4.1] - 2026-10-31
|
|
11
54
|
|
|
@@ -642,3 +685,4 @@ recreate).*
|
|
|
642
685
|
surface, settings panel, command surface).
|
|
643
686
|
- pi-durable upgraded 1.0.2 → 1.2.0: ToolExecutionResult `output` rename across
|
|
644
687
|
core, registry-driven settings roster, `modelTools` two-layer filtering.
|
|
688
|
+
|
package/README.md
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
# Sigrun
|
|
2
2
|
|
|
3
|
-
Sigrun is a **
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
**Why use it?**
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
longer rationale.
|
|
3
|
+
Sigrun is a **self-hosted, multi-user personal-assistant server**. Each
|
|
4
|
+
user signs in to their own conversations, settings, credentials, and
|
|
5
|
+
tools, and their agent runs in its own execution jail. One agent loop
|
|
6
|
+
serves every surface — a web UI, a terminal UI, an HTTP API, scheduled
|
|
7
|
+
jobs, and (via the gateway extension) chat-channel adapters. The core
|
|
8
|
+
stays small: memory, model providers, tools, MCP support, and login are
|
|
9
|
+
extensions installed at boot.
|
|
10
|
+
|
|
11
|
+
**Why use it?** Several people can share one install without sharing an
|
|
12
|
+
agent: real authentication, per-user extensions and secrets, and a
|
|
13
|
+
per-user execution jail that fails closed. Memory and integrations are
|
|
14
|
+
extension-owned and individually replaceable. Conversations survive
|
|
15
|
+
restarts, and it deploys as an npm package, a Docker compose reference,
|
|
16
|
+
or a Helm chart. [`docs/why.md`](docs/why.md) is the longer rationale.
|
|
17
17
|
|
|
18
18
|
**Status:** 0.x — useful and exercised, but extension and settings
|
|
19
19
|
contracts can still evolve between minor releases. See
|
|
@@ -24,12 +24,12 @@ posture, and [`CONTRIBUTING.md`](CONTRIBUTING.md) to build and test.
|
|
|
24
24
|
[Install](#install) and [Quickstart](#quickstart) for the model/credential
|
|
25
25
|
setup every install needs.
|
|
26
26
|
|
|
27
|
-
- **
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
27
|
+
- **Multi-user** — token, proxy, and OIDC/GitHub/Discord sign-in, with
|
|
28
|
+
invites after the first admin claims the install; per-user extension
|
|
29
|
+
settings, secrets, and services; and a Landlock execution jail per user.
|
|
30
|
+
A second user is refused when isolation is unavailable. See
|
|
31
|
+
[Authentication](#authentication) and
|
|
32
|
+
[Multi-user execution isolation](#multi-user-execution-isolation).
|
|
33
33
|
- **Extensions** — separate npm packages loaded from `SIGRUN_EXTENSIONS` (or the settings file's
|
|
34
34
|
top-level `extensions` list); a failing extension is quarantined and
|
|
35
35
|
reported, never a boot abort.
|
|
@@ -42,6 +42,12 @@ setup every install needs.
|
|
|
42
42
|
API the web UI consumes (feature parity by design): conversation
|
|
43
43
|
sidebar, streaming conversation pane, fork, the interaction and
|
|
44
44
|
ambient seams, slash commands. See [Launch modes](#launch-modes).
|
|
45
|
+
- **Restart-safe** — conversations, submissions, and schedules live in one
|
|
46
|
+
SQLite database (`SIGRUN_STORAGE_PATH`), and a restart resumes where it
|
|
47
|
+
stopped. The harness primitives (conversations, inbox steering/follow-up
|
|
48
|
+
modes, retries, streaming, compaction) come from
|
|
49
|
+
[pi-durable](https://www.npmjs.com/package/@earendil-works/pi-durable);
|
|
50
|
+
sigrun is the deployable shell around them.
|
|
45
51
|
|
|
46
52
|
## Install
|
|
47
53
|
|
|
@@ -716,7 +722,8 @@ links, stops their worker and services, and removes their per-user tool
|
|
|
716
722
|
entries. Extensions then receive `userDeleted` while the old record and
|
|
717
723
|
secrets remain readable, so they can revoke external provider
|
|
718
724
|
credentials. Finally core removes the directory record, per-user secrets,
|
|
719
|
-
user-settings directory,
|
|
725
|
+
user-settings directory, per-user cache directories, and that user's
|
|
726
|
+
learned skills and curation state (never transferred).
|
|
720
727
|
|
|
721
728
|
**Credentials belong to a person and never transfer.** Data and state
|
|
722
729
|
stay for the extension to retain, move, or delete according to its own
|
|
@@ -1008,11 +1015,11 @@ Entry grammar:
|
|
|
1008
1015
|
checkout `FETCH_HEAD`; present without a ref → fetch only (the
|
|
1009
1016
|
checkout stays where you left it). Entry discovery: the object form's
|
|
1010
1017
|
`entry` → a `sigrun` manifest (`{"sigrun": {"extension": "./index.js"}}`)
|
|
1011
|
-
→ a pi
|
|
1018
|
+
→ a `pi` manifest (the first non-`.ts` `pi.extensions` entry; each
|
|
1012
1019
|
`.ts` entry is skipped with one warning — it cannot load under plain
|
|
1013
1020
|
node) → `dist/index.js`.
|
|
1014
1021
|
- **Skills from packages** — a package may ship skills instead of (or
|
|
1015
|
-
beside) extension code: manifest `sigrun.skills` or pi
|
|
1022
|
+
beside) extension code: manifest `sigrun.skills` or the `pi`-shaped
|
|
1016
1023
|
`pi.skills` (`["./skills"]`, the same SKILL.md format), or a
|
|
1017
1024
|
factory-result `skills` field (absolute dirs). A package that declares
|
|
1018
1025
|
skills and has no loadable entry is a valid **skills-only** package
|
|
@@ -1193,6 +1200,22 @@ typed — the submission's user entry carries
|
|
|
1193
1200
|
`{kind: "sigrun.user.expanded", data: {typed, command}}` while the
|
|
1194
1201
|
model receives the expanded text.
|
|
1195
1202
|
|
|
1203
|
+
**The curator and learned skills.** Beyond the three roots above, every
|
|
1204
|
+
user has a private fourth root — learned skills the curator records for
|
|
1205
|
+
them under `<data-dir>/users/<user>/skills/`. The curator reviews the
|
|
1206
|
+
user's conversations (owner-authored tool calls are captured as
|
|
1207
|
+
"Noted" hints, never executed on the spot) and writes through the
|
|
1208
|
+
versioned `skill_manage` surface, so every learned skill keeps its last
|
|
1209
|
+
ten versions, a change feed, and one-call undo. Learned skills never
|
|
1210
|
+
leave their user: they are not shared, not transferred on ownership
|
|
1211
|
+
moves, and removed with the user on `DELETE /v1/users/{user}`. Each
|
|
1212
|
+
user can pause or disable the curator in their own per-user settings —
|
|
1213
|
+
`PUT /v1/me/extensions/curator/settings` with `{"paused": true}` stops
|
|
1214
|
+
new reviews until flipped back, `{"enabled": false}` turns the curator
|
|
1215
|
+
off entirely (both are per-user and take effect on the next run) — and
|
|
1216
|
+
an operator can disable it server-wide with
|
|
1217
|
+
`"builtinExtensions": {"curator": false}` (restart required).
|
|
1218
|
+
|
|
1196
1219
|
## Jobs
|
|
1197
1220
|
|
|
1198
1221
|
A job is a schedule plus an action, durable by construction — one
|
package/dist/commands/expand.js
CHANGED
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
* kind must not start with `pi.`).
|
|
32
32
|
*/
|
|
33
33
|
import { listSkills, } from "../skills/index.js";
|
|
34
|
+
import { OVERLAY_HEADING, overlayForSkill } from "../curation/overlays.js";
|
|
34
35
|
import { discoverTemplates, } from "./templates.js";
|
|
35
36
|
/** The user entry kind an expanded input is placed as. */
|
|
36
37
|
export const EXPANDED_ENTRY_KIND = "sigrun.user.expanded";
|
|
@@ -149,8 +150,13 @@ export function expandTemplate(text, templates) {
|
|
|
149
150
|
* shape): the `<skill …>` block, plus a blank line and the trimmed
|
|
150
151
|
* arguments ONLY when arguments were given. An unknown skill — or any
|
|
151
152
|
* other text — passes through UNCHANGED.
|
|
153
|
+
*
|
|
154
|
+
* `overlays` (curator A5, P2): the acting owner's overlay text per
|
|
155
|
+
* skill name — when present, the block's body carries the upstream
|
|
156
|
+
* body followed by `OVERLAY_HEADING` and the overlay text, the same
|
|
157
|
+
* shape `load_skill` serves. Absent = the plain pi shape.
|
|
152
158
|
*/
|
|
153
|
-
export function expandSkillCommand(text, skills) {
|
|
159
|
+
export function expandSkillCommand(text, skills, overlays) {
|
|
154
160
|
if (!text.startsWith("/skill:"))
|
|
155
161
|
return text;
|
|
156
162
|
const spaceIndex = text.indexOf(" ");
|
|
@@ -160,9 +166,12 @@ export function expandSkillCommand(text, skills) {
|
|
|
160
166
|
if (skill === undefined)
|
|
161
167
|
return text;
|
|
162
168
|
const location = `${skill.dir}/SKILL.md`;
|
|
169
|
+
const overlay = overlays?.get(skill.name);
|
|
163
170
|
// Pi parity: the body is the frontmatter-stripped SKILL.md, trimmed —
|
|
164
171
|
// `</skill>` sits on its own line with no stray blank lines.
|
|
165
|
-
const body =
|
|
172
|
+
const body = (overlay === undefined
|
|
173
|
+
? skill.body
|
|
174
|
+
: `${skill.body}\n${OVERLAY_HEADING}\n\n${overlay}`).trim();
|
|
166
175
|
const block = `<skill name="${skill.name}" location="${location}">\n` +
|
|
167
176
|
`References are relative to ${skill.dir}.\n` +
|
|
168
177
|
`\n${body}\n` +
|
|
@@ -205,7 +214,7 @@ export function expandableTemplates(templates, commandNames) {
|
|
|
205
214
|
* error `parseCommandArgs` raises) — the submit path maps it to a
|
|
206
215
|
* client error.
|
|
207
216
|
*/
|
|
208
|
-
export async function expandInput(parts, expansion, env, context) {
|
|
217
|
+
export async function expandInput(parts, expansion, env, context, owner) {
|
|
209
218
|
// Only a LEADING text part starting with `/` is a command: the
|
|
210
219
|
// command is what the user typed first (attachments and later text
|
|
211
220
|
// parts pass through untouched, in place).
|
|
@@ -223,10 +232,33 @@ export async function expandInput(parts, expansion, env, context) {
|
|
|
223
232
|
if (reserved.has(commandName))
|
|
224
233
|
return { parts };
|
|
225
234
|
if (commandName.startsWith("skill:")) {
|
|
226
|
-
const
|
|
227
|
-
const
|
|
235
|
+
const skillName = commandName.slice("skill:".length);
|
|
236
|
+
const skills = await listSkills(env, context, expansion.skills, { owner });
|
|
237
|
+
// Curator A5 (P2): the owner's overlay rides on the upstream body,
|
|
238
|
+
// the same shape `load_skill` serves — discovered skills without
|
|
239
|
+
// one (or an owner without a store) map nothing, and the block
|
|
240
|
+
// keeps pi's plain shape.
|
|
241
|
+
const overlays = new Map();
|
|
242
|
+
for (const skill of skills) {
|
|
243
|
+
const overlay = await overlayForSkill(expansion.skills, owner, skill, env);
|
|
244
|
+
if (overlay !== undefined)
|
|
245
|
+
overlays.set(skill.name, overlay);
|
|
246
|
+
}
|
|
247
|
+
const expanded = expandSkillCommand(text, skills, overlays);
|
|
228
248
|
if (expanded === text)
|
|
229
249
|
return { parts };
|
|
250
|
+
// Curator A6 (P4): a `/skill:name` that actually expanded counts as
|
|
251
|
+
// usage for the owner (the discovered skill was a learned one).
|
|
252
|
+
if (owner !== undefined) {
|
|
253
|
+
const skill = skills.find((candidate) => candidate.name === skillName);
|
|
254
|
+
if (skill?.root === "learned") {
|
|
255
|
+
const usage = expansion.skills.usage?.(owner);
|
|
256
|
+
if (usage !== undefined) {
|
|
257
|
+
usage.bump(skill.name, "expand");
|
|
258
|
+
void usage.flush();
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
230
262
|
return {
|
|
231
263
|
parts: parts.map((part) => (part === textPart ? { type: "text", text: expanded } : part)),
|
|
232
264
|
typed: text,
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Message attribution (curator P1, spec §6.1): every user input records
|
|
3
|
+
* the submitting user through every admission path — HTTP submit (the
|
|
4
|
+
* resolved principal), the extension host bridge (`opts.user`, recorded
|
|
5
|
+
* verbatim; extensions own their user mapping), a block answer (the
|
|
6
|
+
* acting user), and a job fire (the job's owner).
|
|
7
|
+
*
|
|
8
|
+
* The carrier is a SIDE record, the `sigrun.authors` conversation doc:
|
|
9
|
+
* `pi.user` entries are `Entry<never>` (they cannot carry data), and a
|
|
10
|
+
* new input entry kind would have to be taught to every `UserEntry.is`
|
|
11
|
+
* consumer. The doc keys the author two ways:
|
|
12
|
+
*
|
|
13
|
+
* - `byRequest` — written BEFORE `conversation.submit` (the requestId
|
|
14
|
+
* exists pre-admission, so this never races). An orphan record from a
|
|
15
|
+
* failed submit is harmless.
|
|
16
|
+
* - `bySubmission` — written right after admission returns: the key the
|
|
17
|
+
* live run's input ids resolve against, readable from a tool through
|
|
18
|
+
* any `DocumentReader`. A tool call that lands before this write reads
|
|
19
|
+
* "unattributed" and fails closed.
|
|
20
|
+
*
|
|
21
|
+
* An `undefined` user (an unidentified caller) records nothing: its
|
|
22
|
+
* entries read unattributed, exactly like every entry admitted before
|
|
23
|
+
* this doc existed — unattributed entries are never curation input.
|
|
24
|
+
*/
|
|
25
|
+
import { randomUUID } from "node:crypto";
|
|
26
|
+
import { defineDoc, UserEntry } from "@earendil-works/pi-durable";
|
|
27
|
+
import { EXPANDED_ENTRY_KIND } from "../commands/expand.js";
|
|
28
|
+
import { BlocksAnswerEntry } from "../harness/blocks/entries.js";
|
|
29
|
+
/** The `sigrun.authors` doc (latest-only singleton per conversation). */
|
|
30
|
+
export const AuthorsDoc = defineDoc({
|
|
31
|
+
kind: "sigrun.authors",
|
|
32
|
+
version: 1,
|
|
33
|
+
scope: "conversation",
|
|
34
|
+
checkpointWhen: () => false,
|
|
35
|
+
history: "latest",
|
|
36
|
+
fork: "current",
|
|
37
|
+
initial: () => ({ byRequest: {}, bySubmission: {} }),
|
|
38
|
+
});
|
|
39
|
+
/** The entry kinds an input submission places (the author carriers). */
|
|
40
|
+
const INPUT_KINDS = new Set([
|
|
41
|
+
UserEntry.kind,
|
|
42
|
+
EXPANDED_ENTRY_KIND,
|
|
43
|
+
BlocksAnswerEntry.kind,
|
|
44
|
+
]);
|
|
45
|
+
/** Whether one entry is a placed user input (any of {@link INPUT_KINDS}). */
|
|
46
|
+
function isInputEntry(entry) {
|
|
47
|
+
return INPUT_KINDS.has(entry.kind);
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* The requestId an input is admitted under: the caller's own id, or a
|
|
51
|
+
* generated `sigrun:`-prefixed UUID when the caller passed none — every
|
|
52
|
+
* input submission carries one, so `byRequest` can key every admission.
|
|
53
|
+
*/
|
|
54
|
+
export function ensureRequestId(requestId) {
|
|
55
|
+
return requestId ?? `sigrun:${randomUUID()}`;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Record `user` as the author of one input, keyed by its requestId
|
|
59
|
+
* (pre-admission) or its submission id (post-admission). A no-op for an
|
|
60
|
+
* `undefined` user: an unidentified caller leaves no record and its
|
|
61
|
+
* entries read unattributed.
|
|
62
|
+
*/
|
|
63
|
+
export async function recordAuthor(conversation, key, user, ctx) {
|
|
64
|
+
if (user === undefined)
|
|
65
|
+
return;
|
|
66
|
+
await conversation.commit(async (tx) => {
|
|
67
|
+
const doc = await tx.doc(AuthorsDoc, conversation.id);
|
|
68
|
+
if ("requestId" in key)
|
|
69
|
+
doc.byRequest[key.requestId] = user;
|
|
70
|
+
else
|
|
71
|
+
doc.bySubmission[String(key.submissionId)] = user;
|
|
72
|
+
}, ctx);
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* The author of one submission, from any `DocumentReader` — including the
|
|
76
|
+
* `ToolExecutionApi` a tool runs with. Reads `bySubmission` only, so a
|
|
77
|
+
* tool call that lands before the post-admission write resolves
|
|
78
|
+
* `undefined` (unattributed — fail closed, never guess).
|
|
79
|
+
*/
|
|
80
|
+
export async function authorOfSubmission(read, conversationId, submissionId, ctx) {
|
|
81
|
+
const doc = await read.snapshot(AuthorsDoc, conversationId, ctx);
|
|
82
|
+
return doc?.bySubmission[String(submissionId)];
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Every user-input entry in the conversation's ACTIVE context mapped to
|
|
86
|
+
* its author — host-side (full `Session` access; the harness resolves
|
|
87
|
+
* both record keys through the submission's `entry` link and its
|
|
88
|
+
* `requestId`). Entries with no attribution (everything admitted before
|
|
89
|
+
* this doc, or by an unidentified caller) are present with `undefined`:
|
|
90
|
+
* the map's presence set is the input entries, the values are authors.
|
|
91
|
+
*/
|
|
92
|
+
export async function authorsOfEntries(harness, conversationId, ctx) {
|
|
93
|
+
const result = new Map();
|
|
94
|
+
const conversation = await harness.conversation(conversationId, ctx);
|
|
95
|
+
if (conversation === undefined)
|
|
96
|
+
return result;
|
|
97
|
+
const view = await conversation.context(ctx);
|
|
98
|
+
for (const entry of view.entries) {
|
|
99
|
+
if (isInputEntry(entry))
|
|
100
|
+
result.set(entry.id, undefined);
|
|
101
|
+
}
|
|
102
|
+
const doc = await harness.snapshot(AuthorsDoc, conversationId, ctx);
|
|
103
|
+
if (doc === undefined)
|
|
104
|
+
return result;
|
|
105
|
+
// The byRequest join: the requestId resolves to the submission (and
|
|
106
|
+
// its placed entry) through the transaction's conversation-scoped
|
|
107
|
+
// lookup — the one typed path from a requestId to the record.
|
|
108
|
+
const byRequest = await harness.commit(async (tx) => {
|
|
109
|
+
const resolved = [];
|
|
110
|
+
for (const [requestId, user] of Object.entries(doc.byRequest)) {
|
|
111
|
+
const submission = await tx.submissionByRequest(conversationId, requestId);
|
|
112
|
+
if (submission?.entry !== undefined) {
|
|
113
|
+
resolved.push({ entry: submission.entry, user });
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return resolved;
|
|
117
|
+
}, ctx);
|
|
118
|
+
for (const { entry, user } of byRequest) {
|
|
119
|
+
if (result.has(entry))
|
|
120
|
+
result.set(entry, user);
|
|
121
|
+
}
|
|
122
|
+
// The bySubmission join (authoritative: keyed by the exact id). The
|
|
123
|
+
// submission handle re-reads the live record, so a still-queued input
|
|
124
|
+
// joins once its entry places.
|
|
125
|
+
for (const [id, user] of Object.entries(doc.bySubmission)) {
|
|
126
|
+
const submission = await harness.submission(Number(id), ctx);
|
|
127
|
+
if (submission === undefined)
|
|
128
|
+
continue;
|
|
129
|
+
const record = await submission.status(ctx);
|
|
130
|
+
if (record.entry !== undefined && result.has(record.entry)) {
|
|
131
|
+
result.set(record.entry, user);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return result;
|
|
135
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Curation state removal on user deletion (curator A10, spec §6.4):
|
|
3
|
+
* a departing user's learned skills and curation state are REMOVED with
|
|
4
|
+
* them, never transferred — a skill distilled from their conversations
|
|
5
|
+
* can carry their paths, hosts and preferences, and must not load for
|
|
6
|
+
* the recipient (memory's shape; the spec's call).
|
|
7
|
+
*
|
|
8
|
+
* Core owns exactly the two roots under the managed user area:
|
|
9
|
+
* `<userArea>/skills` (the learned root) and `<userArea>/curation`
|
|
10
|
+
* (versions, overlays, the feed, usage counters). The user area itself,
|
|
11
|
+
* workspaces and every extension-owned per-user dir stay for the §10.6
|
|
12
|
+
* recipient/retention rules; targets clean their own data through their
|
|
13
|
+
* `userDeleted` handler.
|
|
14
|
+
*
|
|
15
|
+
* Fail-closed counterpart (Review Focus 1): the per-user stores gate
|
|
16
|
+
* EVERY operation on the live directory record (A4/A5's `userExists`),
|
|
17
|
+
* so a curator run in flight when its user is deleted cannot write —
|
|
18
|
+
* its late calls fail `user removed` and create nothing. This function
|
|
19
|
+
* only removes what exists; it never recreates a root.
|
|
20
|
+
*/
|
|
21
|
+
import { rm } from "node:fs/promises";
|
|
22
|
+
import { curationDir, learnedSkillsDir } from "./paths.js";
|
|
23
|
+
/**
|
|
24
|
+
* Remove one user's learned skills root and curation state directory.
|
|
25
|
+
* Idempotent (`force`): absent roots are a no-op, and nothing is ever
|
|
26
|
+
* created. Symlink-safe by construction — `rm` removes a symlinked
|
|
27
|
+
* root itself, never its target.
|
|
28
|
+
*/
|
|
29
|
+
export async function removeUserCuration(userArea) {
|
|
30
|
+
await rm(learnedSkillsDir(userArea), { recursive: true, force: true });
|
|
31
|
+
await rm(curationDir(userArea), { recursive: true, force: true });
|
|
32
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The curator-run gate (curator P6): `sigrun.curation-gate` — a
|
|
3
|
+
* hooks-only core extension that confines an internal curator run to
|
|
4
|
+
* its allow list.
|
|
5
|
+
*
|
|
6
|
+
* Selected ONLY in the run conversations A9's producer creates (this
|
|
7
|
+
* task installs it; boot keeps it OUT of the host default extension
|
|
8
|
+
* selection, the same never-defaulted treatment as per-user entries).
|
|
9
|
+
* Its `beforeTool` blocks any tool not in `CurationRunDoc.allow` with
|
|
10
|
+
* `"<tool> is not available in this review; use one of: <allow,
|
|
11
|
+
* comma-separated>"` — a decision only; it writes nothing (a hook
|
|
12
|
+
* cannot commit, and the gate has no write to make). A conversation
|
|
13
|
+
* with no run doc (never, for a correctly-created run) allows
|
|
14
|
+
* everything: the gate is a confinement, not a filter on ordinary
|
|
15
|
+
* conversations.
|
|
16
|
+
*/
|
|
17
|
+
import { defineExtension, hook, ToolTask } from "@earendil-works/pi-durable";
|
|
18
|
+
import { curationRun } from "./run-doc.js";
|
|
19
|
+
/** The gate extension's registered name. */
|
|
20
|
+
export const CURATION_GATE_EXTENSION = "sigrun.curation-gate";
|
|
21
|
+
/**
|
|
22
|
+
* Build the gate extension: one `pi.tool` hook, no tools, no sections,
|
|
23
|
+
* no prompt bytes. Boot installs it into the registry; A9's producer
|
|
24
|
+
* selects it in the internal run conversations it creates.
|
|
25
|
+
*/
|
|
26
|
+
export function createCurationGateExtension() {
|
|
27
|
+
return defineExtension({
|
|
28
|
+
name: CURATION_GATE_EXTENSION,
|
|
29
|
+
hooks: [
|
|
30
|
+
hook(ToolTask, {
|
|
31
|
+
/**
|
|
32
|
+
* Block any tool outside the run's allow list, naming the
|
|
33
|
+
* allowed list so the model can recover without another
|
|
34
|
+
* round of guessing. `undefined` (no run doc, or the tool
|
|
35
|
+
* is allowed) lets the call proceed.
|
|
36
|
+
*/
|
|
37
|
+
async beforeTool(call, api, context) {
|
|
38
|
+
const run = await curationRun(api, api.conversationId, context);
|
|
39
|
+
if (run === undefined)
|
|
40
|
+
return undefined;
|
|
41
|
+
if (run.allow.includes(call.name))
|
|
42
|
+
return undefined;
|
|
43
|
+
return {
|
|
44
|
+
block: `${call.name} is not available in this review; use one of: ${run.allow.join(", ")}`,
|
|
45
|
+
};
|
|
46
|
+
},
|
|
47
|
+
}),
|
|
48
|
+
],
|
|
49
|
+
});
|
|
50
|
+
}
|