@sigrun/sigrun 0.5.0 → 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 +55 -0
- package/README.md +18 -11
- package/dist/extension.d.ts +54 -1
- package/dist/harness/boot.js +27 -4
- package/dist/harness/builtins.js +9 -5
- package/dist/harness/identity/primary-admin.js +19 -0
- package/dist/harness/reserved-sections.js +2 -0
- package/dist/harness/seam-host.js +15 -0
- package/dist/harness/sections/context-files.js +6 -3
- package/dist/harness/sections/job-state.js +24 -0
- package/dist/harness/workspace.js +30 -3
- package/dist/jobs/fire.js +4 -3
- package/dist/jobs/index.js +34 -7
- package/dist/jobs/reconcile.js +9 -0
- package/dist/jobs/state-dir.js +16 -0
- package/dist/jobs/transfer.js +14 -0
- package/dist/sandbox/policy.js +13 -4
- package/dist/serve.js +5 -3
- package/dist/server/conversation-ops.js +13 -0
- package/dist/server/host-bridge.js +25 -0
- package/dist/server/notifications.js +102 -16
- package/dist/server/routes/jobs.js +31 -0
- package/dist/settings/core-sections.js +9 -0
- package/dist/settings/registry.js +6 -0
- package/node_modules/@sigrun/shared/CHANGELOG.md +3 -0
- package/node_modules/@sigrun/shared/package.json +1 -1
- package/package.json +6 -6
- package/webui/dist/assets/index-BR0di0OG.css +1 -0
- package/webui/dist/assets/{index-BtczbUXo.js → index-BzrA1T4g.js} +5 -5
- package/webui/dist/index.html +2 -2
- package/webui/dist/assets/index-h7zDY9vD.css +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,47 @@ 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
|
+
## [0.6.0] - 2026-11-02
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- **Escalations core.** Extensions send notices through `host.notify`
|
|
12
|
+
(validated targets, delivered on the bounded side lane), look jobs up
|
|
13
|
+
with `host.jobs.get`, create job-scoped conversations, and read a
|
|
14
|
+
conversation's history through the trusted `host.conversations.entries`
|
|
15
|
+
/ `entry` (callers enforce access themselves).
|
|
16
|
+
- **Per-job state.** Each job gets a state folder under its owner's jobs
|
|
17
|
+
root (jail-granted to the run); the run's prompt names it. State follows
|
|
18
|
+
the job through delete, transfer and retire, and resets on request
|
|
19
|
+
(`POST /v1/jobs/{id}/state/reset`, and a Reset button in the jobs tab).
|
|
20
|
+
- Extension views and panels render through the block renderer (they were
|
|
21
|
+
a JSON placeholder in 0.5.0 — the curator view showed raw data).
|
|
22
|
+
- The settings page gains the `builtinExtensions.gateway` toggle.
|
|
23
|
+
|
|
24
|
+
### Changed
|
|
25
|
+
- **Ownerless jobs belong to the primary admin** (the earliest-created
|
|
26
|
+
directory admin): run conversations, access checks, `host.jobs.get` and
|
|
27
|
+
job state all use that effective owner. A single-operator install with
|
|
28
|
+
an empty directory stays ownerless.
|
|
29
|
+
- The identity prompt section is multi-user and no longer names the agent
|
|
30
|
+
(naming is persona territory, `SOUL.md`).
|
|
31
|
+
- Built-in documentation says six built-ins everywhere (codemode, MCP,
|
|
32
|
+
tool search, gateway, OAuth, curator); everything else is optional.
|
|
33
|
+
|
|
34
|
+
### Fixed
|
|
35
|
+
- The web conversation list heals after the events stream drops: it
|
|
36
|
+
reloads on every reconnect and when the tab becomes visible again, and
|
|
37
|
+
reopens an EventSource the browser gave up on (5-60 s backoff).
|
|
38
|
+
Conversations created on another device no longer stay invisible until
|
|
39
|
+
a reload.
|
|
40
|
+
- Leaving an extension view: the Chats rail entry works and picking a
|
|
41
|
+
conversation in the sidebar returns to the chat.
|
|
42
|
+
- Phone layout: a long URL in a user message no longer makes the chat
|
|
43
|
+
slide sideways; tables keep readable columns and scroll inside their
|
|
44
|
+
card; the composer's controls stay on one row.
|
|
45
|
+
- Extension surfaces: params-carrying action/data URLs, zero-width surface
|
|
46
|
+
pages, and cadence refreshes.
|
|
47
|
+
- `SIGRUN_EXTENSIONS` without a model env no longer crashes boot.
|
|
48
|
+
|
|
8
49
|
## [0.5.0] - 2026-11-02
|
|
9
50
|
|
|
10
51
|
### Added
|
|
@@ -32,7 +73,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
32
73
|
step read `.list` off a promise). A lazy store proxy now preserves the
|
|
33
74
|
sync-store contract through the pre-bind window.
|
|
34
75
|
|
|
76
|
+
### Added
|
|
77
|
+
- `host.notify` lets extensions send validated explicit notices through
|
|
78
|
+
every declared notifier, with optional conversation or declared-view
|
|
79
|
+
deep links. Delivery is enqueued on the bounded side lane and is not
|
|
80
|
+
gated by `notifications.runSettled`.
|
|
81
|
+
- Extension host job hooks: `host.jobs.get(id)` returns a read-only live-job
|
|
82
|
+
projection with its effective owner, and `host.conversations.create({job})`
|
|
83
|
+
stamps a validated job association in the session-meta creation commit.
|
|
84
|
+
Unknown, retired, and owner-mismatched jobs are rejected before storage
|
|
85
|
+
writes; creating the conversation does not create a job run or `lastRun`.
|
|
86
|
+
|
|
35
87
|
### Changed
|
|
88
|
+
- Ownerless and orphaned-owner jobs now run as the primary admin; their
|
|
89
|
+
runs are no longer visible to every user. The effective owner is
|
|
90
|
+
resolved live without rewriting the declared record.
|
|
36
91
|
- Reworded the `pi` manifest handling so it no longer implies pi
|
|
37
92
|
extension compatibility. Sigrun reads `pi.extensions`/`pi.skills` only
|
|
38
93
|
to LOCATE files and shares the SKILL.md format; it does not implement
|
package/README.md
CHANGED
|
@@ -531,8 +531,8 @@ form when the effective auth methods include `token`; `/v1/auth/providers`
|
|
|
531
531
|
returns an empty list on proxy-only deployments because their completed
|
|
532
532
|
sign-ins could not authenticate the resulting session. The TUI continues to
|
|
533
533
|
use tokens. Login providers come from extensions.
|
|
534
|
-
The reference implementation is **`@sigrun/oauth`, the
|
|
535
|
-
|
|
534
|
+
The reference implementation is **`@sigrun/oauth`, one of the six built-in
|
|
535
|
+
extensions**. It is bundled and loads by default, so no `extensions` entry is
|
|
536
536
|
needed. Set `"builtinExtensions": {"oauth": false}` to disable it; like the
|
|
537
537
|
other built-in toggles, this is restart-required. A configured extension
|
|
538
538
|
whose package name is also `@sigrun/oauth` replaces the built-in. If the
|
|
@@ -1082,10 +1082,13 @@ a dashboard) are in
|
|
|
1082
1082
|
|
|
1083
1083
|
## Built-in extensions
|
|
1084
1084
|
|
|
1085
|
-
|
|
1086
|
-
search**, **gateway**,
|
|
1087
|
-
what pi 1.3.0 ships built-in; the gateway is sigrun's own
|
|
1088
|
-
engine; OAuth provides OIDC, GitHub, and Discord login
|
|
1085
|
+
Six extensions are built into core — **codemode**, **MCP**, **tool
|
|
1086
|
+
search**, **gateway**, **OAuth login**, and the **curator**. The first
|
|
1087
|
+
three mirror what pi 1.3.0 ships built-in; the gateway is sigrun's own
|
|
1088
|
+
channel-adapter engine; OAuth provides OIDC, GitHub, and Discord login
|
|
1089
|
+
providers; the curator learns per-user skills (see
|
|
1090
|
+
[Skills and prompt templates](#skills-and-prompt-templates)). Every other extension is optional
|
|
1091
|
+
and loads only when `extensions` names it. The built-ins
|
|
1089
1092
|
load by default on every boot, with no settings entry, and resolve from
|
|
1090
1093
|
core's own `node_modules` (they are core dependencies, never installed
|
|
1091
1094
|
into the packages dir — `extensions` has nothing to install for them,
|
|
@@ -1093,7 +1096,8 @@ unless a same-named package is deliberately installed to replace one;
|
|
|
1093
1096
|
see below).
|
|
1094
1097
|
A default install therefore has pi's tool surface without configuring
|
|
1095
1098
|
anything. On `GET /v1/extensions` they report under the names
|
|
1096
|
-
`codemode`, `sigrun-mcp`, `tool-search`, `gateway`,
|
|
1099
|
+
`codemode`, `sigrun-mcp`, `tool-search`, `gateway`, `oauth`, and
|
|
1100
|
+
`curator`.
|
|
1097
1101
|
Configure OAuth providers in `settings/oauth.json`; see the
|
|
1098
1102
|
`@sigrun/oauth` package README for provider settings and secrets.
|
|
1099
1103
|
|
|
@@ -1109,7 +1113,8 @@ default. They join the offered set when an enabled MCP server's
|
|
|
1109
1113
|
automatic decision (auto-enable only narrows the default set, never a
|
|
1110
1114
|
named list).
|
|
1111
1115
|
- **Turning one off** — `"builtinExtensions": {"codemode": false,
|
|
1112
|
-
"mcp": false, "toolSearch": false, "gateway": false, "oauth": false
|
|
1116
|
+
"mcp": false, "toolSearch": false, "gateway": false, "oauth": false,
|
|
1117
|
+
"curator": false}`.
|
|
1113
1118
|
Each defaults to `true`; each is restart-required. A disabled built-in
|
|
1114
1119
|
disappears from `GET /v1/extensions` entirely — no row, not even a
|
|
1115
1120
|
skipped one — and its config is then ignored.
|
|
@@ -1136,12 +1141,13 @@ docs/extending.md).
|
|
|
1136
1141
|
"extensions": [ // configured extensions install + load as above
|
|
1137
1142
|
"@acme/sigrun-ext-example@^0.1.0"
|
|
1138
1143
|
],
|
|
1139
|
-
"builtinExtensions": { // all
|
|
1144
|
+
"builtinExtensions": { // all six default to true; restart required
|
|
1140
1145
|
"codemode": true,
|
|
1141
1146
|
"mcp": true,
|
|
1142
1147
|
"toolSearch": false,
|
|
1143
1148
|
"gateway": true,
|
|
1144
|
-
"oauth": true
|
|
1149
|
+
"oauth": true,
|
|
1150
|
+
"curator": true
|
|
1145
1151
|
}
|
|
1146
1152
|
}
|
|
1147
1153
|
```
|
|
@@ -1202,7 +1208,8 @@ model receives the expanded text.
|
|
|
1202
1208
|
|
|
1203
1209
|
**The curator and learned skills.** Beyond the three roots above, every
|
|
1204
1210
|
user has a private fourth root — learned skills the curator records for
|
|
1205
|
-
them under `<data-dir>/users/<user>/skills/`.
|
|
1211
|
+
them under `<data-dir>/users/<user>/skills/`. For skills it ranks second:
|
|
1212
|
+
the workspace wins over it, and it wins over the user dir and packages. The curator reviews the
|
|
1206
1213
|
user's conversations (owner-authored tool calls are captured as
|
|
1207
1214
|
"Noted" hints, never executed on the spot) and writes through the
|
|
1208
1215
|
versioned `skill_manage` surface, so every learned skill keeps its last
|
package/dist/extension.d.ts
CHANGED
|
@@ -38,7 +38,7 @@ import type { Access, Sharing } from "@sigrun/shared/access";
|
|
|
38
38
|
import type { AmbientPlace, AmbientRecord } from "@sigrun/shared/ambient";
|
|
39
39
|
import type { Action, DataOps, IconName, Tree } from "@sigrun/shared/blocks";
|
|
40
40
|
import type { InteractionAnswer, InteractionRecord } from "@sigrun/shared/interactions";
|
|
41
|
-
import type { JobSpec } from "@sigrun/shared/jobs";
|
|
41
|
+
import type { JobSpec, JobWire } from "@sigrun/shared/jobs";
|
|
42
42
|
import type { AccountLinkWire, ConversationSummary, ConversationViewWire, HealthStatus, ModelEntry, SessionMeta, SlashCommand, SubmissionRecordWire, ThinkingLevelWire, UserWire as DirectoryUserWire } from "@sigrun/shared/views";
|
|
43
43
|
export type { Access, AccountLinkWire, AmbientRecord, DirectoryUserWire, InteractionRecord, JobSpec, ModelEntry, Sharing, SlashCommand, };
|
|
44
44
|
type JsonParams = JsonObject;
|
|
@@ -630,6 +630,23 @@ export type RunSettledOutcome = {
|
|
|
630
630
|
readonly status: "unanswered";
|
|
631
631
|
readonly reason: string;
|
|
632
632
|
};
|
|
633
|
+
/** One explicit notification an extension sends through `host.notify`. */
|
|
634
|
+
export interface ExtensionNoticeInput {
|
|
635
|
+
/** A directory user; `undefined` only when the directory is empty. */
|
|
636
|
+
readonly user: string | undefined;
|
|
637
|
+
/** 1..200 characters. */
|
|
638
|
+
readonly title: string;
|
|
639
|
+
/** 0..2000 characters. */
|
|
640
|
+
readonly body: string;
|
|
641
|
+
/** 1..64 characters, each `[a-z0-9.-]`. */
|
|
642
|
+
readonly kind: string;
|
|
643
|
+
readonly target?: {
|
|
644
|
+
readonly view: string;
|
|
645
|
+
readonly params?: JsonObject;
|
|
646
|
+
} | {
|
|
647
|
+
readonly conversation: number;
|
|
648
|
+
};
|
|
649
|
+
}
|
|
633
650
|
/** Fields shared by every notification variant. */
|
|
634
651
|
interface NotificationEventBase {
|
|
635
652
|
readonly conversationId?: number;
|
|
@@ -650,6 +667,12 @@ export type NotificationEvent = NotificationEventBase & ({
|
|
|
650
667
|
readonly callId?: string;
|
|
651
668
|
} | {
|
|
652
669
|
readonly type: "job.failed";
|
|
670
|
+
} | {
|
|
671
|
+
readonly type: "extension";
|
|
672
|
+
/** The calling extension's slug. */
|
|
673
|
+
readonly extension: string;
|
|
674
|
+
/** The extension-defined notice kind. */
|
|
675
|
+
readonly kind: string;
|
|
653
676
|
} | {
|
|
654
677
|
readonly type: "run.settled";
|
|
655
678
|
/** Whether the run answered or not (model error, abort, …), with
|
|
@@ -1018,6 +1041,15 @@ export type SubmissionInputPart = {
|
|
|
1018
1041
|
};
|
|
1019
1042
|
/** How a submit behaves when a run is already live (pi-durable's `whenBusy`). */
|
|
1020
1043
|
export type WhenBusyMode = NonNullable<InputSubmissionDraft["whenBusy"]>;
|
|
1044
|
+
/** The extension-facing, read-only projection of one live job. */
|
|
1045
|
+
export interface HostJobInfo {
|
|
1046
|
+
readonly id: string;
|
|
1047
|
+
readonly name: string;
|
|
1048
|
+
/** The effective owner (ownerless jobs resolve to the primary admin). */
|
|
1049
|
+
readonly owner?: string;
|
|
1050
|
+
readonly enabled: boolean;
|
|
1051
|
+
readonly state: JobWire["state"];
|
|
1052
|
+
}
|
|
1021
1053
|
/**
|
|
1022
1054
|
* The host context sigrun hands an extension factory: the scoped
|
|
1023
1055
|
* extension stores, the interaction/ambient seam surfaces, and the
|
|
@@ -1146,6 +1178,14 @@ export interface SigrunHost {
|
|
|
1146
1178
|
readonly sharing?: Sharing;
|
|
1147
1179
|
readonly name?: string;
|
|
1148
1180
|
readonly source?: string;
|
|
1181
|
+
/**
|
|
1182
|
+
* Associate this new conversation with one live job. The requested
|
|
1183
|
+
* owner must equal the job's effective owner (including both absent).
|
|
1184
|
+
* Unknown, retired, or mismatched jobs throw `ConversationOpError`
|
|
1185
|
+
* with `{error: "invalid_field", field: "job"}` before any write.
|
|
1186
|
+
* Success stamps `jobId`; it does not create a job run or `lastRun`.
|
|
1187
|
+
*/
|
|
1188
|
+
readonly job?: string;
|
|
1149
1189
|
/**
|
|
1150
1190
|
* Pin the conversation's extension/tool selection (gateway C4):
|
|
1151
1191
|
* names resolved against the registry — an unknown name throws a
|
|
@@ -1167,6 +1207,9 @@ export interface SigrunHost {
|
|
|
1167
1207
|
readonly archived?: boolean;
|
|
1168
1208
|
}): Promise<readonly ConversationSummary[]>;
|
|
1169
1209
|
meta(id: ConversationId): Promise<SessionMeta>;
|
|
1210
|
+
/** The fork-aware transcript read (trusted; enforce access yourself with `host.access.accessOf`). */
|
|
1211
|
+
entries(id: ConversationId, query: Omit<EntryQuery, "conversationId">, limit: number, cursor?: Cursor): Promise<Page<EntryRecord, Cursor>>;
|
|
1212
|
+
entry(id: ConversationId, entryId: EntryId): Promise<EntryRecord | undefined>;
|
|
1170
1213
|
/**
|
|
1171
1214
|
* Withdraw/settle the conversation's active submission (gateway
|
|
1172
1215
|
* C4, the abort route's op). On an idle conversation it RESOLVES
|
|
@@ -1233,6 +1276,16 @@ export interface SigrunHost {
|
|
|
1233
1276
|
readonly mime?: string;
|
|
1234
1277
|
}>;
|
|
1235
1278
|
};
|
|
1279
|
+
/** Read-only job lookup; retired and unknown ids resolve to `undefined`. */
|
|
1280
|
+
readonly jobs: {
|
|
1281
|
+
get(id: string): Promise<HostJobInfo | undefined>;
|
|
1282
|
+
};
|
|
1283
|
+
/**
|
|
1284
|
+
* Send one explicit notice through every registered notifier. Validation
|
|
1285
|
+
* failures throw; once accepted, delivery is enqueued on the bounded side
|
|
1286
|
+
* lane and this resolves without awaiting notifier completion.
|
|
1287
|
+
*/
|
|
1288
|
+
notify(input: ExtensionNoticeInput): Promise<void>;
|
|
1236
1289
|
/** The user directory (admin wire form; emails are admin material). */
|
|
1237
1290
|
readonly users: {
|
|
1238
1291
|
list(): Promise<readonly DirectoryUserWire[]>;
|
package/dist/harness/boot.js
CHANGED
|
@@ -30,6 +30,7 @@ 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
35
|
import { createSkillsExtension, scanFsRoot } from "../skills/index.js";
|
|
35
36
|
import { SLASH_COMMANDS } from "../server/commands.js";
|
|
@@ -78,6 +79,7 @@ import { IsolationState } from "../sandbox/isolation.js";
|
|
|
78
79
|
import { probeOsUsers } from "../sandbox/availability.js";
|
|
79
80
|
import { createAdmissionGuard } from "./identity/users.js";
|
|
80
81
|
import { sweepAccountLinks } from "./identity/link-sweep.js";
|
|
82
|
+
import { effectiveOwner as resolveEffectiveOwner } from "./identity/primary-admin.js";
|
|
81
83
|
import { SessionMetaDoc } from "../server/session-meta.js";
|
|
82
84
|
import { learnedSkillsDir } from "../curation/paths.js";
|
|
83
85
|
import { SkillStore } from "../curation/store.js";
|
|
@@ -120,6 +122,7 @@ export const RESERVED_CORE_SECTION_OWNERS = {
|
|
|
120
122
|
skills: "sigrun.skills",
|
|
121
123
|
"tool-guidance": "sigrun.model-guidance",
|
|
122
124
|
"execution-guidance": "sigrun.model-guidance",
|
|
125
|
+
"job-state": "sigrun.job-state",
|
|
123
126
|
};
|
|
124
127
|
/**
|
|
125
128
|
* Open (or reopen) the durable harness at `config.storagePath`.
|
|
@@ -361,6 +364,10 @@ export async function openHarness(config, options = {}) {
|
|
|
361
364
|
// reads it at RENDER time, long after the assignment — the skills cap
|
|
362
365
|
// is a live setting (`skills.sectionMaxBytes`, default 4KB).
|
|
363
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;
|
|
364
371
|
// Context-file sections (SOUL.md persona, AGENTS.md instructions), the
|
|
365
372
|
// skills convention, and the core tool surface (plan Task 1: coding
|
|
366
373
|
// tools read/write/edit/bash + search tools find/grep/ls) are all
|
|
@@ -500,6 +507,13 @@ export async function openHarness(config, options = {}) {
|
|
|
500
507
|
},
|
|
501
508
|
};
|
|
502
509
|
registry.install(contextFilesExtension);
|
|
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
|
+
}));
|
|
503
517
|
// Curator A8 (P9): the wrap feed — the mutable set of curation tool
|
|
504
518
|
// names, seeded with the core set and grown by the loader as each
|
|
505
519
|
// extension's curationTargets validate. The skills extension's
|
|
@@ -765,6 +779,9 @@ export async function openHarness(config, options = {}) {
|
|
|
765
779
|
// deps object is shared with `fireJob`'s run-now and the JobsService
|
|
766
780
|
// below (one clock, one tz, one models store).
|
|
767
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),
|
|
768
785
|
// SAFETY: harness + storage are assigned right after `Harness.open`
|
|
769
786
|
// returns below (the open needs the task registered, so the deps
|
|
770
787
|
// object is created first with placeholders). Every read — task
|
|
@@ -912,7 +929,7 @@ export async function openHarness(config, options = {}) {
|
|
|
912
929
|
stopProviderRefresh();
|
|
913
930
|
};
|
|
914
931
|
// Every tool/file/upload/job env resolves through this one decision seam.
|
|
915
|
-
|
|
932
|
+
workspaceService = createWorkspaceService({
|
|
916
933
|
isolation: isolationState,
|
|
917
934
|
osUsers,
|
|
918
935
|
pool: sandboxPool,
|
|
@@ -1205,6 +1222,7 @@ export async function openHarness(config, options = {}) {
|
|
|
1205
1222
|
// (an extension's declaration is its boot-time declaration).
|
|
1206
1223
|
const fileJobs = () => settingsStore.read().jobs ?? [];
|
|
1207
1224
|
const jobsSources = () => ({ file: fileJobs(), extensions: jobBindings });
|
|
1225
|
+
const jobEffectiveOwner = (owner) => resolveEffectiveOwner(identity.users.list(), owner);
|
|
1208
1226
|
// The root conversation — the schedule tasks' owner (spec §8.1).
|
|
1209
1227
|
// `Harness.root` is idempotent (creates with `agent` on first call,
|
|
1210
1228
|
// returns the reserved root thereafter), so the server's own
|
|
@@ -1233,7 +1251,7 @@ export async function openHarness(config, options = {}) {
|
|
|
1233
1251
|
// SSE stream computes `accessOf` per principal and adds it. A
|
|
1234
1252
|
// future in-process hub subscriber must never read a fabricated
|
|
1235
1253
|
// access off the hub's copy.
|
|
1236
|
-
const { access: _dropped, ...row } = jobWireOf(jobId, record);
|
|
1254
|
+
const { access: _dropped, ...row } = jobWireOf(jobId, record, jobEffectiveOwner(record.owner));
|
|
1237
1255
|
events.publish({
|
|
1238
1256
|
type: "job.changed",
|
|
1239
1257
|
job: row,
|
|
@@ -1249,7 +1267,9 @@ export async function openHarness(config, options = {}) {
|
|
|
1249
1267
|
id: jobId,
|
|
1250
1268
|
name: newest.spec.name,
|
|
1251
1269
|
source: newest.source,
|
|
1252
|
-
...(newest.owner === undefined
|
|
1270
|
+
...(jobEffectiveOwner(newest.owner) === undefined
|
|
1271
|
+
? {}
|
|
1272
|
+
: { owner: jobEffectiveOwner(newest.owner) }),
|
|
1253
1273
|
...(newest.sharing === undefined ? {} : { sharing: newest.sharing }),
|
|
1254
1274
|
schedule: newest.spec.schedule,
|
|
1255
1275
|
action: newest.spec.action,
|
|
@@ -1285,12 +1305,14 @@ export async function openHarness(config, options = {}) {
|
|
|
1285
1305
|
jobRunTotals.set(outcome, (jobRunTotals.get(outcome) ?? 0) + 1);
|
|
1286
1306
|
}
|
|
1287
1307
|
});
|
|
1288
|
-
|
|
1308
|
+
jobs = createJobsService({
|
|
1289
1309
|
harness,
|
|
1290
1310
|
storage,
|
|
1291
1311
|
runtime: jobRuntimeDeps,
|
|
1312
|
+
effectiveOwner: jobEffectiveOwner,
|
|
1292
1313
|
rootId: rootConversationId,
|
|
1293
1314
|
sources: jobsSources,
|
|
1315
|
+
jobsRoot: (owner) => workspaceService?.jobsRoot(owner),
|
|
1294
1316
|
settings: () => settingsStore.read(),
|
|
1295
1317
|
onChange: (jobId) => {
|
|
1296
1318
|
void publishJobChanged(jobId);
|
|
@@ -1311,6 +1333,7 @@ export async function openHarness(config, options = {}) {
|
|
|
1311
1333
|
runtime: jobRuntimeDeps,
|
|
1312
1334
|
rootId: rootConversationId,
|
|
1313
1335
|
sources: jobsSources,
|
|
1336
|
+
jobsRoot: (owner) => workspaceService?.jobsRoot(owner),
|
|
1314
1337
|
}, BACKGROUND_CONTEXT);
|
|
1315
1338
|
// The deleted-owner warning (§8.3): a declared job whose `owner` names
|
|
1316
1339
|
// a user no longer in the directory runs OWNERLESS until its source is
|
package/dist/harness/builtins.js
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Built-in extensions: codemode, MCP, tool search, OAuth and
|
|
3
|
-
* ship WITH core — present by default, disableable via settings
|
|
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
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
|
*
|
|
@@ -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
|
+
}
|
|
@@ -21,6 +21,7 @@ export const RESERVED_CORE_SECTION_KEYS = [
|
|
|
21
21
|
"skills",
|
|
22
22
|
"tool-guidance",
|
|
23
23
|
"execution-guidance",
|
|
24
|
+
"job-state",
|
|
24
25
|
];
|
|
25
26
|
/**
|
|
26
27
|
* Core-owned directory names directly under a store root (multi-user M1).
|
|
@@ -31,6 +32,7 @@ export const RESERVED_CORE_SECTION_KEYS = [
|
|
|
31
32
|
*/
|
|
32
33
|
export const RESERVED_EXTENSION_SLUGS = [
|
|
33
34
|
"users",
|
|
35
|
+
"jobs",
|
|
34
36
|
"workspace",
|
|
35
37
|
"models",
|
|
36
38
|
"sandbox",
|
|
@@ -321,6 +321,7 @@ export function createSeamHost(identity, kinds, bind, sharedStores, perUserTools
|
|
|
321
321
|
...(opts.owner === undefined ? {} : { owner: opts.owner }),
|
|
322
322
|
...(opts.sharing === undefined ? {} : { sharing: opts.sharing }),
|
|
323
323
|
...(opts.name === undefined ? {} : { name: opts.name }),
|
|
324
|
+
...(opts.job === undefined ? {} : { job: opts.job }),
|
|
324
325
|
...(opts.agent === undefined ? {} : { agent: opts.agent }),
|
|
325
326
|
// The source default: the CALLING extension's slug (spec §9.5).
|
|
326
327
|
source: opts.source ?? identity.name,
|
|
@@ -342,6 +343,12 @@ export function createSeamHost(identity, kinds, bind, sharedStores, perUserTools
|
|
|
342
343
|
const bridge = await hostBridge("conversations.meta");
|
|
343
344
|
return bridge.conversations.meta(id);
|
|
344
345
|
},
|
|
346
|
+
async entries(id, query, limit, cursor) {
|
|
347
|
+
return (await hostBridge("conversations.entries")).conversations.entries(id, query, limit, cursor);
|
|
348
|
+
},
|
|
349
|
+
async entry(id, entryId) {
|
|
350
|
+
return (await hostBridge("conversations.entry")).conversations.entry(id, entryId);
|
|
351
|
+
},
|
|
345
352
|
// The C4a ops: straight delegation (the bridge resolves the id and
|
|
346
353
|
// runs the op its HTTP twin runs).
|
|
347
354
|
async abort(id) {
|
|
@@ -367,6 +374,11 @@ export function createSeamHost(identity, kinds, bind, sharedStores, perUserTools
|
|
|
367
374
|
return (await hostBridge("conversations.readFile")).conversations.readFile(id, path);
|
|
368
375
|
},
|
|
369
376
|
},
|
|
377
|
+
jobs: {
|
|
378
|
+
async get(id) {
|
|
379
|
+
return (await hostBridge("jobs.get")).jobs.get(id);
|
|
380
|
+
},
|
|
381
|
+
},
|
|
370
382
|
users: {
|
|
371
383
|
async list() {
|
|
372
384
|
return (await hostBridge("users.list")).users.list();
|
|
@@ -483,6 +495,9 @@ export function createSeamHost(identity, kinds, bind, sharedStores, perUserTools
|
|
|
483
495
|
return (await hostBridge("commands.run")).commands.run(conversationId, name, args, opts);
|
|
484
496
|
},
|
|
485
497
|
},
|
|
498
|
+
async notify(input) {
|
|
499
|
+
return (await hostBridge("notify")).notify(identity.name, input);
|
|
500
|
+
},
|
|
486
501
|
// The surfaces seam (UI-blocks U3 Task 4): `changed` awaits the
|
|
487
502
|
// bridge (so a factory-body call waits, never resolves early) and
|
|
488
503
|
// delegates to the server-side SurfaceService — the rev bump + the
|
|
@@ -45,10 +45,13 @@ async function readContextFile(input, fileName, context) {
|
|
|
45
45
|
/**
|
|
46
46
|
* The static `identity` section body: runtime facts only. No tool claims
|
|
47
47
|
* (the system prompt already declares the actually-offered tools per
|
|
48
|
-
* request)
|
|
49
|
-
* mid-conversation model swap, defeating the minimal-patch design)
|
|
48
|
+
* request), no model name (it would re-emit this section on every
|
|
49
|
+
* mid-conversation model swap, defeating the minimal-patch design), and
|
|
50
|
+
* no NAME: what the agent is called is `SOUL.md` territory, since a user
|
|
51
|
+
* may choose a different name for their own agent. Durability is runtime
|
|
52
|
+
* truth, not identity copy — it stays out of this section.
|
|
50
53
|
*/
|
|
51
|
-
export const IDENTITY_TEXT = "You are
|
|
54
|
+
export const IDENTITY_TEXT = "You are a small but extensible multi-user AI assistant built on the pi-durable harness. Conversations can span channels (web UI, terminal UI, HTTP API), and multiple independent conversations may be active at once.";
|
|
52
55
|
/**
|
|
53
56
|
* The persona sigrun ships when no `SOUL.md` exists. A present `SOUL.md`
|
|
54
57
|
* replaces it wholesale; the default is never merged in.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/** The persistent state folder shared by every run of one job. */
|
|
2
|
+
import { defineExtension, section } from "@earendil-works/pi-durable";
|
|
3
|
+
import { jobStateDir } from "../../jobs/state-dir.js";
|
|
4
|
+
import { SessionMetaDoc } from "../../server/session-meta.js";
|
|
5
|
+
/** Name a new-conversation job run's persistent, per-owner state folder. */
|
|
6
|
+
export function createJobStateExtension(deps) {
|
|
7
|
+
return defineExtension({
|
|
8
|
+
name: "sigrun.job-state",
|
|
9
|
+
sections: [
|
|
10
|
+
section("job-state", async (input, context) => {
|
|
11
|
+
const meta = await input.read.snapshot(SessionMetaDoc, input.conversationId, context);
|
|
12
|
+
if (meta?.jobId === undefined)
|
|
13
|
+
return undefined;
|
|
14
|
+
const job = await deps.job(meta.jobId);
|
|
15
|
+
if (job === undefined)
|
|
16
|
+
return undefined;
|
|
17
|
+
const root = deps.jobsRoot(meta.owner);
|
|
18
|
+
if (root === undefined)
|
|
19
|
+
return undefined;
|
|
20
|
+
return `Job state: this conversation is a run of job "${job.name}" (${meta.jobId}). Its state folder is ${jobStateDir(root, meta.jobId)} — create it if missing. Use it to keep notes between runs (for example, when an issue was first seen). Other runs of this job use the same folder; write files atomically.`;
|
|
21
|
+
}),
|
|
22
|
+
],
|
|
23
|
+
});
|
|
24
|
+
}
|
|
@@ -1,15 +1,43 @@
|
|
|
1
1
|
/** Resolve each conversation's ExecutionEnv: single-user, jailed, or fail-closed. */
|
|
2
|
-
import { isAbsolute, resolve } from "node:path";
|
|
2
|
+
import { isAbsolute, join, resolve } from "node:path";
|
|
3
3
|
import { BACKGROUND_CONTEXT } from "@earendil-works/chord/context";
|
|
4
4
|
import { buildUserPolicy } from "../sandbox/policy.js";
|
|
5
5
|
import { SandboxedExecutionEnv } from "../sandbox/sandboxed-env.js";
|
|
6
6
|
import { UnavailableExecutionEnv } from "../sandbox/unavailable-env.js";
|
|
7
7
|
import { selfPackageRoot } from "./packages.js";
|
|
8
8
|
import { defaultModuleTrees } from "../sandbox/policy.js";
|
|
9
|
+
import { primaryAdmin } from "./identity/primary-admin.js";
|
|
9
10
|
import { AgentExecutionEnv } from "./agent-env.js";
|
|
10
11
|
import { lookupOsAccount } from "../sandbox/os-account.js";
|
|
12
|
+
/** The live owner's record, falling back to the primary admin. */
|
|
13
|
+
export function selectRecord(records, owner) {
|
|
14
|
+
return records.find((record) => record.user === owner) ?? primaryAdmin(records);
|
|
15
|
+
}
|
|
11
16
|
export function createWorkspaceService(deps) {
|
|
12
17
|
return {
|
|
18
|
+
jobsRoot(owner) {
|
|
19
|
+
if (deps === undefined)
|
|
20
|
+
return undefined;
|
|
21
|
+
const { directory, managed } = deps;
|
|
22
|
+
const records = directory.list();
|
|
23
|
+
const record = selectRecord(records, owner);
|
|
24
|
+
if (record === undefined)
|
|
25
|
+
return join(managed.roots.dataDir, "jobs");
|
|
26
|
+
if (record.osUser !== undefined) {
|
|
27
|
+
if (deps.osUsers?.available !== true)
|
|
28
|
+
return undefined;
|
|
29
|
+
try {
|
|
30
|
+
const account = (deps.lookupAccount ?? lookupOsAccount)(record.osUser);
|
|
31
|
+
return account === undefined
|
|
32
|
+
? undefined
|
|
33
|
+
: join(account.home, ".local", "state", "sigrun", "jobs");
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return join(managed.userAreas(record.user), "jobs");
|
|
40
|
+
},
|
|
13
41
|
async forConversation(conversation, prefs, read, ctx) {
|
|
14
42
|
const workspace = prefs.defaultWorkspace;
|
|
15
43
|
if (workspace.type !== "local") {
|
|
@@ -22,8 +50,7 @@ export function createWorkspaceService(deps) {
|
|
|
22
50
|
const owner = read === undefined
|
|
23
51
|
? undefined
|
|
24
52
|
: await deps.ownerOf(conversation.id, read, ctx ?? BACKGROUND_CONTEXT);
|
|
25
|
-
const record = records
|
|
26
|
-
?? records.find((candidate) => candidate.admin);
|
|
53
|
+
const record = selectRecord(records, owner);
|
|
27
54
|
let osAccount;
|
|
28
55
|
if (record?.osUser !== undefined) {
|
|
29
56
|
if (deps.osUsers?.available !== true) {
|
package/dist/jobs/fire.js
CHANGED
|
@@ -216,6 +216,7 @@ async function firePrompt(deps, id, record, fireKey, baseRun, prior, context) {
|
|
|
216
216
|
: { thinkingLevel: action.thinking }),
|
|
217
217
|
},
|
|
218
218
|
init: async (tx, newId) => {
|
|
219
|
+
const owner = deps.effectiveOwner(record.owner);
|
|
219
220
|
const run = {
|
|
220
221
|
...baseRun,
|
|
221
222
|
conversationId: newId,
|
|
@@ -229,9 +230,9 @@ async function firePrompt(deps, id, record, fireKey, baseRun, prior, context) {
|
|
|
229
230
|
meta.jobId = id;
|
|
230
231
|
if (action.deliver !== undefined)
|
|
231
232
|
meta.deliver = [...action.deliver];
|
|
232
|
-
if (
|
|
233
|
-
meta.owner =
|
|
234
|
-
await deps.rewriteUserSelection?.(tx, newId, undefined,
|
|
233
|
+
if (owner !== undefined)
|
|
234
|
+
meta.owner = owner;
|
|
235
|
+
await deps.rewriteUserSelection?.(tx, newId, undefined, owner);
|
|
235
236
|
if (action.runSharing !== undefined)
|
|
236
237
|
meta.sharing = action.runSharing;
|
|
237
238
|
meta.name = runName(record.spec.name, deps.now(), record, deps);
|