@sigrun/sigrun 0.6.1 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +92 -0
- package/README.md +105 -18
- package/dist/blocks/build-stamp.json +2 -2
- package/dist/blocks/schema.js +5 -0
- package/dist/cli.js +85 -22
- package/dist/curation/host.js +46 -21
- package/dist/curation/migrate.js +96 -0
- package/dist/curation/overlays.js +52 -16
- package/dist/curation/paths.js +22 -0
- package/dist/curation/runs.js +34 -17
- package/dist/curation/skill-tool.js +34 -9
- package/dist/curation/store.js +382 -254
- package/dist/curation/targets.js +21 -0
- package/dist/curation/wrap.js +1 -6
- package/dist/extension.d.ts +12 -3
- package/dist/harness/boot.js +143 -6
- package/dist/harness/event-bus.js +19 -0
- package/dist/harness/loader.js +22 -0
- package/dist/harness/reserved-sections.js +2 -0
- package/dist/harness/seam-host.js +71 -16
- package/dist/harness/workspace.js +34 -1
- package/dist/history/git.js +707 -0
- package/dist/history/trailers.js +145 -0
- package/dist/history/workspace-history.js +47 -0
- package/dist/jobs/index.js +5 -3
- package/dist/jobs/reconcile.js +2 -3
- package/dist/jobs/state-dir.js +9 -0
- package/dist/jobs/transfer.js +6 -6
- package/dist/sandbox/grants.js +11 -0
- package/dist/sandbox/launch.js +19 -9
- package/dist/sandbox/policy.js +6 -1
- package/dist/serve.js +3 -0
- package/dist/server/auth/proxy-only.js +5 -4
- package/dist/server/core-views/index.js +26 -0
- package/dist/server/core-views/jobs.js +661 -0
- package/dist/server/core-views/skills-tree.js +628 -0
- package/dist/server/core-views/skills.js +1090 -0
- package/dist/server/index.js +88 -0
- package/dist/server/jobs-access.js +62 -0
- package/dist/server/notifications.js +24 -12
- package/dist/server/routes/extensions.js +9 -1
- package/dist/server/routes/jobs.js +19 -52
- package/dist/server/routes/meta.js +9 -0
- package/dist/server/routes/onboarding.js +13 -1
- package/dist/server/surfaces/service.js +38 -0
- package/dist/skills/index.js +1 -1
- package/dist/skills/roots.js +301 -0
- package/node_modules/@sigrun/shared/CHANGELOG.md +8 -0
- package/node_modules/@sigrun/shared/dist/auth.d.ts +22 -0
- package/node_modules/@sigrun/shared/dist/auth.js +26 -0
- package/node_modules/@sigrun/shared/dist/blocks/index.d.ts +1 -1
- package/node_modules/@sigrun/shared/dist/blocks/types.d.ts +15 -0
- package/node_modules/@sigrun/shared/dist/model-label.d.ts +29 -0
- package/node_modules/@sigrun/shared/dist/model-label.js +55 -0
- package/node_modules/@sigrun/shared/dist/views.d.ts +28 -0
- package/node_modules/@sigrun/shared/package.json +9 -1
- package/package.json +7 -7
- package/webui/dist/assets/index-BI5k-Oci.js +55 -0
- package/webui/dist/assets/index-EGluyRXk.css +1 -0
- package/webui/dist/favicon.svg +7 -5
- package/webui/dist/index.html +3 -3
- package/webui/dist/assets/index-BR0di0OG.css +0 -1
- package/webui/dist/assets/index-BzrA1T4g.js +0 -55
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,39 @@ 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.7.0] - 2026-11-04
|
|
9
|
+
|
|
10
|
+
The polish pass: three waves of behaviour fixes, the full deferred-minors sweep, and a
|
|
11
|
+
fresh-eyes clean-room install verification.
|
|
12
|
+
|
|
13
|
+
### Core
|
|
14
|
+
|
|
15
|
+
- **Host-bridge fail-fast** (breaking for embedders): bridged host members now reject
|
|
16
|
+
immediately with `HostBridgeUnavailableError` when no server will ever bind the bridge
|
|
17
|
+
(`openHarness` gains `expectHostBridge`; `createServer` marks it expected on entry).
|
|
18
|
+
Handlers log once per extension+member instead of stalling the hook deadline.
|
|
19
|
+
- Date-range presets (`PnD`/`PnW`) count calendar days; the `T…` part stays elapsed time.
|
|
20
|
+
- `modelBreadcrumb` in `@sigrun/shared/model-label`; the webui shows the provider as a
|
|
21
|
+
breadcrumb (`b/c/acme-flash`) with the full id on hover.
|
|
22
|
+
- `isProxyOnly` lives once in `@sigrun/shared/auth` (empty/absent methods fail safe);
|
|
23
|
+
the CLI onboarding gate keys on proxy identity (its claim can never carry the loopback
|
|
24
|
+
vouch; `sigrun run`'s own loopback companion is exempt).
|
|
25
|
+
- Curator: empty-text skill writes refused; `.tmp` sweep; pruned undos report-and-continue
|
|
26
|
+
(siblings still revert); two-targets-one-tool validated at load; scan caps gained
|
|
27
|
+
cursors; the 10k guard reports truncation.
|
|
28
|
+
- serializePolicy has one writer; smoke scripts fail loudly (the registry smoke's
|
|
29
|
+
false-green fixed); `-h/--help` accepted at every CLI level.
|
|
30
|
+
- Onboarding claim errors list the accepted body keys; token-create against fresh roots
|
|
31
|
+
blames the roots.
|
|
32
|
+
- e2e suite: zero fixed sleeps (condition waits + boot barriers); a stale-closure in the
|
|
33
|
+
composer's answered-refetch was found and fixed by the strictness.
|
|
34
|
+
|
|
35
|
+
### Fresh-eyes fixes (clean-room verified)
|
|
36
|
+
|
|
37
|
+
- The compose route boots (tmpfs `/cache:uid=1000`); the container documents its token
|
|
38
|
+
path and the image puts `sigrun` on PATH (next build); the README documents the
|
|
39
|
+
submission and claim body shapes and where the setup code lands per deploy shape.
|
|
40
|
+
|
|
8
41
|
## [0.6.1] - 2026-11-03
|
|
9
42
|
|
|
10
43
|
### Fixed
|
|
@@ -15,6 +48,65 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
15
48
|
|
|
16
49
|
- Release-pin bump to 0.6.1 (root, shared, tui, sandbox platform packages, chart appVersion, compose defaults).
|
|
17
50
|
|
|
51
|
+
## [Unreleased]
|
|
52
|
+
|
|
53
|
+
### Added
|
|
54
|
+
- **Views are tabs (webui).** The webui shell registers views as rail
|
|
55
|
+
tabs with remembered routes; the chat view keeps the conversation
|
|
56
|
+
list and right panel to itself. Below the tablet breakpoint a phone
|
|
57
|
+
bottom tab bar (with a More sheet) replaces the rail.
|
|
58
|
+
- **`page.selection`** — a server-declared, presentation-only
|
|
59
|
+
list-first drill-in for compact widths (`{selected, back}` on a
|
|
60
|
+
`page`; webui and TUI render it, `toText` ignores it).
|
|
61
|
+
- **Core views.** Core attaches its own view surfaces under the
|
|
62
|
+
reserved `core` slug and lists them as `coreViews` (one
|
|
63
|
+
`CoreViewInfo {id, title, icon, refreshMs?}` per view) on
|
|
64
|
+
`/v1/extensions` and `/v1/meta`; they are never `extensions[]` rows,
|
|
65
|
+
so the health dot is unaffected. `job.changed` now bumps every
|
|
66
|
+
rendered core/jobs instance.
|
|
67
|
+
- **The Jobs tab** (`core/jobs`): the jobs surface as a core blocks
|
|
68
|
+
view — sidebar of the caller-visible jobs, drill-in with summary,
|
|
69
|
+
paged runs and revisions (a selected revision diffs its spec JSON
|
|
70
|
+
against the previous one), Run now / Reset state for write access,
|
|
71
|
+
and per-row navigation into a run's conversation. The Jobs panel is
|
|
72
|
+
gone.
|
|
73
|
+
- **The Skills tab** (`core/skills`): every skill root the deployment
|
|
74
|
+
serves — Workspace / Learned / Config dir (the deployment-wide
|
|
75
|
+
`<config-dir>/skills/`, read-only) / one group per installed package
|
|
76
|
+
(`· <pkg>`, with a per-row package badge) / Deleted — with shadowing visible, owned-skill CRUD, per-skill git
|
|
77
|
+
history (25 entries per page, per-file diffs, restore), deleted-skill
|
|
78
|
+
restore, and per-user overlays as the only write to the read-only
|
|
79
|
+
shared roots. Some surface actions return `navigate`; the webui
|
|
80
|
+
applies that navigation, the TUI does not yet (a known channel gap).
|
|
81
|
+
- **Every-root skill discovery** (`src/skills/roots.ts`): workspace,
|
|
82
|
+
learned, user dir, and every installed package's skills listed in one
|
|
83
|
+
pass, with precedence/shadowing resolution and symlink-safe reads.
|
|
84
|
+
- **GitHistory** (`src/history/git.ts`) — isolated local git for
|
|
85
|
+
sigrun-owned history stores: execFile with explicit `--git-dir` /
|
|
86
|
+
`--work-tree`, a hermetic git env, per-repo locking, and fail-soft
|
|
87
|
+
writes (a broken repo is quarantined aside and re-initialized;
|
|
88
|
+
mutations never fail because history did).
|
|
89
|
+
- **Curation stores on git** (Task 9): each user's learned skills,
|
|
90
|
+
overlays, and curator state live in a per-user git history repo; the
|
|
91
|
+
change feed's `actor` widens to `"outside"` and its `id`/`before`/
|
|
92
|
+
`after` are commit SHAs. A once-per-process migration imports the
|
|
93
|
+
pre-git store (the legacy files are left read-only, outside the
|
|
94
|
+
repo's pathspec).
|
|
95
|
+
- **Workspace skills history** (Task 10): the workspace `skills/`
|
|
96
|
+
tree gets its own per-user repo (git dir outside the work tree),
|
|
97
|
+
caught up on settled runs and tab opens; ownerless conversations
|
|
98
|
+
skip it.
|
|
99
|
+
|
|
100
|
+
### Changed
|
|
101
|
+
- **The curation seal**: workspaces and grants that sit inside or
|
|
102
|
+
contain a user's `curation/` tree are rejected; boot revalidates
|
|
103
|
+
stored records and names each failing one loudly. A user whose
|
|
104
|
+
workspace was the bare user area loses agent tools until an admin
|
|
105
|
+
re-points it: `PATCH /v1/users/<u> {"workspace": "<userArea>/workspace"}`.
|
|
106
|
+
History tamper-resistance holds under active sandbox isolation; a
|
|
107
|
+
single-user install without isolation runs the agent unsandboxed
|
|
108
|
+
(the existing trust model).
|
|
109
|
+
|
|
18
110
|
## [0.6.0] - 2026-11-02
|
|
19
111
|
|
|
20
112
|
### Added
|
package/README.md
CHANGED
|
@@ -20,7 +20,10 @@ contracts can still evolve between minor releases. See
|
|
|
20
20
|
[`SECURITY.md`](SECURITY.md) for the support, trust, and verification
|
|
21
21
|
posture, and [`CONTRIBUTING.md`](CONTRIBUTING.md) to build and test.
|
|
22
22
|
|
|
23
|
-
**Try it:** `npm i -g @sigrun/sigrun && sigrun run`
|
|
23
|
+
**Try it:** `npm i -g @sigrun/sigrun && sigrun run` (on a workstation,
|
|
24
|
+
with a terminal — `sigrun run` is the interactive mode: it claims the
|
|
25
|
+
install via prompts and launches the TUI; without a TTY or on a server,
|
|
26
|
+
use `sigrun serve` below) — or jump to
|
|
24
27
|
[Install](#install) and [Quickstart](#quickstart) for the model/credential
|
|
25
28
|
setup every install needs.
|
|
26
29
|
|
|
@@ -114,6 +117,9 @@ Open `http://localhost:3000/` (web UI) or drive the API:
|
|
|
114
117
|
```bash
|
|
115
118
|
curl localhost:3000/v1/meta
|
|
116
119
|
curl -X POST localhost:3000/v1/conversations -H 'content-type: application/json' -d '{}'
|
|
120
|
+
# A submission carries its text in the `input` field:
|
|
121
|
+
curl -X POST localhost:3000/v1/conversations/1/submissions \
|
|
122
|
+
-H 'content-type: application/json' -d '{"input": "hello"}'
|
|
117
123
|
```
|
|
118
124
|
|
|
119
125
|
From a source checkout the dev shape is `npm install && npm start`
|
|
@@ -121,6 +127,9 @@ From a source checkout the dev shape is `npm install && npm start`
|
|
|
121
127
|
|
|
122
128
|
## Launch modes
|
|
123
129
|
|
|
130
|
+
`sigrun --help` (or `-h`) prints usage and exits 0; the same flag works
|
|
131
|
+
after a subcommand (`sigrun serve --help`).
|
|
132
|
+
|
|
124
133
|
The `sigrun` binary (`package.json` `bin`, run via tsx) has three
|
|
125
134
|
entrypoints:
|
|
126
135
|
|
|
@@ -387,7 +396,7 @@ or a `/32` / `/128` entry) does vouch, as an explicit operator
|
|
|
387
396
|
decision, and boot logs a warning for each such entry when no shared
|
|
388
397
|
secret is configured.
|
|
389
398
|
|
|
390
|
-
**Rejection diagnostics.**
|
|
399
|
+
**Rejection diagnostics.** A rejection logs one sanitized line
|
|
391
400
|
(subject to the throttle below) naming the socket peer, the method, a
|
|
392
401
|
short reason, and the route, when known — never tokens, cookies, or
|
|
393
402
|
identity-header values. The throttle emits one line per reason per 60 s,
|
|
@@ -664,17 +673,26 @@ re-pointed to a different user. The holder or an administrator must remove
|
|
|
664
673
|
a conflicting old link first.
|
|
665
674
|
|
|
666
675
|
**Onboarding (the first claim).** An unclaimed server serves
|
|
667
|
-
`GET /v1/onboarding` + `POST /v1/onboarding/claim`.
|
|
668
|
-
|
|
669
|
-
the
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
+
`GET /v1/onboarding` + `POST /v1/onboarding/claim`. The claim body is
|
|
677
|
+
`{"code": "…", "user": "<username>"}` (plus optional `name`, `email`,
|
|
678
|
+
`issueToken`, `tokenName`); the identity field is `user`, not
|
|
679
|
+
`username`. On a loopback bind or through a trusted proxy identity the
|
|
680
|
+
claim needs no code; otherwise the one-shot setup code is required —
|
|
681
|
+
printed once in the log at boot and written to `<cacheDir>/setup-code`
|
|
682
|
+
(in the Helm chart: the pod log and `/cache/setup-code`;
|
|
683
|
+
`SIGRUN_SETUP_CODE` pins it instead). The claimant becomes the first
|
|
684
|
+
admin and owns the reserved root conversation. Later users join by
|
|
685
|
+
invite code (`POST /v1/users/invites`, admin) or by trusted-proxy
|
|
686
|
+
auto-provision; admins can transfer or remove users
|
|
687
|
+
(`PATCH`/`DELETE /v1/users/{user}` — deletion transfers their
|
|
676
688
|
conversations).
|
|
677
689
|
|
|
690
|
+
**Setup code location.** `<cacheDir>` resolves from `SIGRUN_CACHE_DIR`
|
|
691
|
+
(compose/chart set it to `/cache`); without it the code lands under the
|
|
692
|
+
`SIGRUN_HOME`-derived XDG cache (e.g. `/data/home/.cache/sigrun` on the
|
|
693
|
+
published image). The boot **log line** is the always-true source
|
|
694
|
+
regardless of deploy shape — grep it: `grep 'setup code' <the log>`.
|
|
695
|
+
|
|
678
696
|
**Proxy-only onboarding.** On a proxy-only server bound beyond
|
|
679
697
|
loopback, onboarding happens entirely **through the proxy**: the first
|
|
680
698
|
claim and every later sign-in arrive as trusted proxy identities, no
|
|
@@ -1206,14 +1224,42 @@ typed — the submission's user entry carries
|
|
|
1206
1224
|
`{kind: "sigrun.user.expanded", data: {typed, command}}` while the
|
|
1207
1225
|
model receives the expanded text.
|
|
1208
1226
|
|
|
1227
|
+
**The Skills tab.** The core Skills view (`#/ext/core/skills`) is the
|
|
1228
|
+
management surface for all of this: every skill root the deployment
|
|
1229
|
+
serves, grouped — **Workspace**, **Learned**, **Config dir** (the
|
|
1230
|
+
deployment-wide `<config-dir>/skills/`, read-only), one group **per
|
|
1231
|
+
installed package** (`· <pkg>` — package skills, each row carrying the
|
|
1232
|
+
package badge), and **Deleted** — with
|
|
1233
|
+
shadowing made visible (a lower-precedence skill that never wins says
|
|
1234
|
+
so). Skills you own are editable in place; the deployment's shared
|
|
1235
|
+
roots (the config dir and packages) are read-only here, and a per-user
|
|
1236
|
+
**overlay** is their only write (your overlay, your precedence — other
|
|
1237
|
+
users are unaffected). Each history-backed skill keeps its full **git
|
|
1238
|
+
history**: page through it 25 entries at a time, read the per-file
|
|
1239
|
+
diffs of any change, and restore an old state.
|
|
1240
|
+
|
|
1241
|
+
The tab is per-user — a caller sees their own Workspace and Learned
|
|
1242
|
+
roots. On the naive single-operator loopback posture (the userless
|
|
1243
|
+
superuser — no `X-Sigrun-User` header), the Skills tab resolves that
|
|
1244
|
+
caller to the **primary admin** (the earliest-created directory user
|
|
1245
|
+
with the admin flag — the same fallback the execution jail uses), so
|
|
1246
|
+
her workspace and learned skills are the ones on screen. With an empty
|
|
1247
|
+
directory (no claimed user yet) there is no primary admin, so the
|
|
1248
|
+
tab shows only the deployment's shared roots (Config dir / Packages)
|
|
1249
|
+
and its own empty Workspace/Learned scope.
|
|
1250
|
+
|
|
1251
|
+
Some surface actions
|
|
1252
|
+
hand the channel a navigation to apply (a surface action returning
|
|
1253
|
+
`navigate`): the webui applies it; the TUI does not yet apply
|
|
1254
|
+
`ActionResult.navigate` — a known channel gap.
|
|
1255
|
+
|
|
1209
1256
|
**The curator and learned skills.** Beyond the three roots above, every
|
|
1210
1257
|
user has a private fourth root — learned skills the curator records for
|
|
1211
1258
|
them under `<data-dir>/users/<user>/skills/`. For skills it ranks second:
|
|
1212
1259
|
the workspace wins over it, and it wins over the user dir and packages. The curator reviews the
|
|
1213
1260
|
user's conversations (owner-authored tool calls are captured as
|
|
1214
1261
|
"Noted" hints, never executed on the spot) and writes through the
|
|
1215
|
-
versioned `skill_manage` surface
|
|
1216
|
-
ten versions, a change feed, and one-call undo. Learned skills never
|
|
1262
|
+
versioned `skill_manage` surface. Learned skills never
|
|
1217
1263
|
leave their user: they are not shared, not transferred on ownership
|
|
1218
1264
|
moves, and removed with the user on `DELETE /v1/users/{user}`. Each
|
|
1219
1265
|
user can pause or disable the curator in their own per-user settings —
|
|
@@ -1223,6 +1269,36 @@ off entirely (both are per-user and take effect on the next run) — and
|
|
|
1223
1269
|
an operator can disable it server-wide with
|
|
1224
1270
|
`"builtinExtensions": {"curator": false}` (restart required).
|
|
1225
1271
|
|
|
1272
|
+
**Skill history is local git, per user.** Both writable roots keep
|
|
1273
|
+
their history in a private per-user git repository under the user's
|
|
1274
|
+
curation history dir — the learned skills, overlays, and curator state
|
|
1275
|
+
in one repo, the workspace's `skills/` tree in a second repo whose git
|
|
1276
|
+
dir lives outside the work tree. There is no remote and no upstream:
|
|
1277
|
+
the history is a local audit trail, not a bounded version count. Edits
|
|
1278
|
+
made outside the store are folded in as catch-up commits: when the tab
|
|
1279
|
+
opens they record actor `outside`; when a run settles, the workspace
|
|
1280
|
+
repo catches up with actor `agent` carrying the conversation id (only
|
|
1281
|
+
the workspace repo has a settle hook). History writes for the
|
|
1282
|
+
workspace root are keyed to the conversation's owner — ownerless
|
|
1283
|
+
conversations skip it. The history's tamper-resistance holds under
|
|
1284
|
+
active sandbox isolation (the agent's jail never grants the curation
|
|
1285
|
+
dir); a single-user install without isolation runs the agent
|
|
1286
|
+
unsandboxed — the existing trust model, nothing extra promised.
|
|
1287
|
+
|
|
1288
|
+
**Upgrade note (curation seal).** Workspaces and grants are now
|
|
1289
|
+
rejected when they sit inside or contain a user's `curation/` tree — a
|
|
1290
|
+
record pointing at the bare user area would hand the agent the whole
|
|
1291
|
+
curation state, history included. A pre-existing user whose workspace
|
|
1292
|
+
or grant was exactly that loses its agent's tools until an admin
|
|
1293
|
+
re-points it; boot logs each such user and path loudly. The repair is
|
|
1294
|
+
one call:
|
|
1295
|
+
|
|
1296
|
+
```sh
|
|
1297
|
+
curl -X PATCH localhost:3000/v1/users/<u> \
|
|
1298
|
+
-H 'content-type: application/json' \
|
|
1299
|
+
-d '{"workspace": "<userArea>/workspace"}'
|
|
1300
|
+
```
|
|
1301
|
+
|
|
1226
1302
|
## Jobs
|
|
1227
1303
|
|
|
1228
1304
|
A job is a schedule plus an action, durable by construction — one
|
|
@@ -1281,9 +1357,17 @@ always maps to the exact definition it ran.
|
|
|
1281
1357
|
|
|
1282
1358
|
**Run now:** `POST /v1/jobs/{id}/run` (write access) fires immediately
|
|
1283
1359
|
— the same hook external triggers (CI, Alertmanager) use with a token.
|
|
1284
|
-
In the web UI the Jobs
|
|
1285
|
-
with a “Run now” button; in the TUI `/jobs` prints the table (name,
|
|
1286
|
-
next run, last outcome).
|
|
1360
|
+
In the web UI the Jobs tab (the core `core/jobs` view, `#/ext/core/jobs`)
|
|
1361
|
+
lists jobs with a “Run now” button; in the TUI `/jobs` prints the table (name,
|
|
1362
|
+
next run, last outcome). The tab is the block-rendered twin of the
|
|
1363
|
+
REST surface: the sidebar lists the caller-visible jobs, selecting one
|
|
1364
|
+
drills into its summary, runs, and revisions (a revision view diffs its
|
|
1365
|
+
spec against the previous one), and a run with a conversation links
|
|
1366
|
+
straight to it — that action-result navigation is applied by the webui;
|
|
1367
|
+
the TUI does not yet apply `ActionResult.navigate` (a known channel
|
|
1368
|
+
gap). Access is exactly the `/v1/jobs` rule — including the naive
|
|
1369
|
+
single-operator posture: the userless superuser (no `X-Sigrun-User`
|
|
1370
|
+
header) sees and manages every job, matching the REST routes' grant.
|
|
1287
1371
|
|
|
1288
1372
|
## Restarting
|
|
1289
1373
|
|
|
@@ -1353,11 +1437,14 @@ curl -X POST -H "Authorization: Bearer $TOKEN" \
|
|
|
1353
1437
|
the history (newest first).
|
|
1354
1438
|
- `POST /v1/conversations`, `POST /v1/conversations/{id}/submissions`,
|
|
1355
1439
|
`GET /v1/conversations/{id}` — the durable conversation surface
|
|
1356
|
-
(SSE streaming views included).
|
|
1440
|
+
(SSE streaming views included). A submission body is
|
|
1441
|
+
`{"input": "..."}` (or a `{"input": [parts]}` array of
|
|
1442
|
+
text/image/file content parts).
|
|
1357
1443
|
- `GET /v1/providers` — per-provider readiness, statuses only (never
|
|
1358
1444
|
credentials); `GET /v1/auth/check` is the deprecated alias.
|
|
1359
1445
|
- `GET /v1/onboarding`, `POST /v1/onboarding/claim` — the unclaimed
|
|
1360
|
-
state and the first-user claim
|
|
1446
|
+
state and the first-user claim (body `{"code": "…", "user": "…"}` —
|
|
1447
|
+
the identity field is `user`).
|
|
1361
1448
|
- `POST /v1/auth/login`, `POST /v1/auth/logout`, `GET /v1/auth/whoami` —
|
|
1362
1449
|
token sign-in, logout, and the resolved principal.
|
|
1363
1450
|
- `GET /v1/auth/providers`, `GET /v1/auth/login/{provider}`,
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"algorithm": "sha256",
|
|
3
3
|
"sources": {
|
|
4
|
-
"shared/src/blocks/index.ts": "
|
|
4
|
+
"shared/src/blocks/index.ts": "f1a1a5c59d83ee1b062b6ee118597cf20094a40df13554d7aa572c41cb2d64a0",
|
|
5
5
|
"src/blocks/builders.ts": "a0c9835aaeab871bc2548d4b8d4426849c60cdb0cb1b20142d3bbe11e8377005",
|
|
6
6
|
"src/blocks/images.ts": "2e1bd54546c9c1c5bf0cfe9b488aaffd7c37979ef84bae9789168275e63e202f",
|
|
7
7
|
"src/blocks/index.ts": "8878a2e2269572ebdecca2cf3b95fa67f144232d8a8fe4258b120948064d0520",
|
|
8
8
|
"src/blocks/limits.ts": "1e913f29167114e26bed6f8c1fd04c64b119ee1db3f4e9e2ae161ddf70578683",
|
|
9
9
|
"src/blocks/placements.ts": "fccda42669713d91dc78d025268ee127f926891cf383727c1e235a39b9f15c6f",
|
|
10
|
-
"src/blocks/schema.ts": "
|
|
10
|
+
"src/blocks/schema.ts": "13f2a225b09e41019f448b88a47b6f98ef508561955b8d847314dc88351f930a",
|
|
11
11
|
"src/blocks/validate.ts": "31b9fc78f569cceff229bd1f28bfc62e2f8da437684100e051104f059a1ca19f"
|
|
12
12
|
}
|
|
13
13
|
}
|
package/dist/blocks/schema.js
CHANGED
|
@@ -185,6 +185,11 @@ const PageBlock = Type.Object({
|
|
|
185
185
|
title: Type.String(),
|
|
186
186
|
hint: Type.Optional(Type.String()),
|
|
187
187
|
})),
|
|
188
|
+
/** Server-declared compact drill-in (presentation-only). */
|
|
189
|
+
selection: Type.Optional(Type.Object({
|
|
190
|
+
selected: Type.Boolean(),
|
|
191
|
+
back: Type.Object({}, { additionalProperties: true }),
|
|
192
|
+
})),
|
|
188
193
|
});
|
|
189
194
|
const StackBlock = Type.Object({
|
|
190
195
|
...BlockBase,
|
package/dist/cli.js
CHANGED
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
* cluster's monolith).
|
|
41
41
|
*/
|
|
42
42
|
import { spawn as nodeSpawn } from "node:child_process";
|
|
43
|
-
import { readFileSync, realpathSync } from "node:fs";
|
|
43
|
+
import { existsSync, readFileSync, realpathSync } from "node:fs";
|
|
44
44
|
import { mkdir, readFile } from "node:fs/promises";
|
|
45
45
|
import { createRequire } from "node:module";
|
|
46
46
|
import { dirname, join } from "node:path";
|
|
@@ -57,9 +57,11 @@ import { SettingsStore } from "./harness/settings.js";
|
|
|
57
57
|
import { resolveHelper } from "./sandbox/helper.js";
|
|
58
58
|
import { selfPackageRoot } from "./harness/packages.js";
|
|
59
59
|
import { createAdmissionGuard } from "./harness/identity/users.js";
|
|
60
|
+
import { isProxyOnly } from "@sigrun/shared/auth";
|
|
60
61
|
import { serve } from "./serve.js";
|
|
61
62
|
import { ACCENT_NAMES, PALETTE_NAMES, isAccentName, isPaletteName, } from "@sigrun/shared/themes";
|
|
62
|
-
export const USAGE_TEXT = `usage: sigrun [--
|
|
63
|
+
export const USAGE_TEXT = `usage: sigrun [-h | --help]
|
|
64
|
+
sigrun [--user <name>]
|
|
63
65
|
sigrun serve
|
|
64
66
|
sigrun attach [--server <url>] [--user <name>]
|
|
65
67
|
sigrun token create <name> --user <user>
|
|
@@ -92,6 +94,8 @@ function usageError(message) {
|
|
|
92
94
|
* is a usage error.
|
|
93
95
|
*/
|
|
94
96
|
function parseTokenArgs(argv) {
|
|
97
|
+
if (argv.includes("-h") || argv.includes("--help"))
|
|
98
|
+
return { command: "help" };
|
|
95
99
|
const sub = argv[0];
|
|
96
100
|
if (sub === "list") {
|
|
97
101
|
if (argv.length !== 1) {
|
|
@@ -143,6 +147,8 @@ function parseTokenArgs(argv) {
|
|
|
143
147
|
* deriving it from the external username; ordinary add still requires it.
|
|
144
148
|
*/
|
|
145
149
|
function parseUserArgs(argv) {
|
|
150
|
+
if (argv.includes("-h") || argv.includes("--help"))
|
|
151
|
+
return { command: "help" };
|
|
146
152
|
const sub = argv[0];
|
|
147
153
|
if (sub === "list") {
|
|
148
154
|
if (argv.length !== 1) {
|
|
@@ -215,6 +221,8 @@ function parseUserArgs(argv) {
|
|
|
215
221
|
* note says so).
|
|
216
222
|
*/
|
|
217
223
|
function parseProviderArgs(argv) {
|
|
224
|
+
if (argv.includes("-h") || argv.includes("--help"))
|
|
225
|
+
return { command: "help" };
|
|
218
226
|
const sub = argv[0];
|
|
219
227
|
if (sub !== "list" && sub !== "key" && sub !== "clear") {
|
|
220
228
|
throw usageError(`unknown provider subcommand ${JSON.stringify(sub ?? "")} (list, key, clear)`);
|
|
@@ -277,6 +285,12 @@ export function parseArgs(argv) {
|
|
|
277
285
|
return parseUserArgs(argv.slice(1));
|
|
278
286
|
if (argv[0] === "provider")
|
|
279
287
|
return parseProviderArgs(argv.slice(1));
|
|
288
|
+
// `-h`/`--help` ANYWHERE wins over every other argument: the first
|
|
289
|
+
// thing a stranger types must print usage with exit 0, never a usage
|
|
290
|
+
// error (fresh-eyes A6).
|
|
291
|
+
if (argv.includes("-h") || argv.includes("--help")) {
|
|
292
|
+
return { command: "help" };
|
|
293
|
+
}
|
|
280
294
|
let command;
|
|
281
295
|
let server;
|
|
282
296
|
let user;
|
|
@@ -526,10 +540,21 @@ export async function ttyPrompt(question, opts) {
|
|
|
526
540
|
* source is operator-typed (unchecked): derive the canonical key before
|
|
527
541
|
* it reaches the wire (the server derives on receipt too). Unusable
|
|
528
542
|
* input fails loud, never a silent empty identity.
|
|
543
|
+
*
|
|
544
|
+
* The flag is OPERATOR-EXPLICIT (polish task 12): an empty `--user ""`
|
|
545
|
+
* errors like any other unusable flag value — never a silent fallthrough
|
|
546
|
+
* to SIGRUN_USER/tui.json (the operator asked for a user; resolving a
|
|
547
|
+
* different identity would be a surprise). Ambient sources (env, file)
|
|
548
|
+
* keep the lenient "empty means unset" rule.
|
|
529
549
|
*/
|
|
530
|
-
function resolveUser(parsed, env, file) {
|
|
531
|
-
|
|
532
|
-
|
|
550
|
+
export function resolveUser(parsed, env, file) {
|
|
551
|
+
// The ambient chain: an empty/blank value means UNSET and the next
|
|
552
|
+
// source takes over (the pre-existing env→file semantics — only the
|
|
553
|
+
// explicit flag errors on empty).
|
|
554
|
+
const envUser = env.SIGRUN_USER?.trim() === "" ? undefined : env.SIGRUN_USER;
|
|
555
|
+
const fileUser = file.user?.trim() === "" ? undefined : file.user;
|
|
556
|
+
const raw = parsed.user ?? envUser ?? fileUser;
|
|
557
|
+
if (raw === undefined)
|
|
533
558
|
return undefined;
|
|
534
559
|
const derived = deriveUsername(raw);
|
|
535
560
|
if (derived === undefined) {
|
|
@@ -677,7 +702,8 @@ async function runProviderCommand(parsed, env, io, fetchImpl, readSecret) {
|
|
|
677
702
|
* - `token revoke` fails loud on an unknown id.
|
|
678
703
|
*/
|
|
679
704
|
async function runIdentityCommand(parsed, env, io, isolationProbe) {
|
|
680
|
-
const
|
|
705
|
+
const roots = resolveStoreRoots(env);
|
|
706
|
+
const stores = openIdentityStores(roots);
|
|
681
707
|
if (parsed.command === "token") {
|
|
682
708
|
switch (parsed.sub) {
|
|
683
709
|
case "create": {
|
|
@@ -690,6 +716,17 @@ async function runIdentityCommand(parsed, env, io, isolationProbe) {
|
|
|
690
716
|
}
|
|
691
717
|
const user = derived;
|
|
692
718
|
if (stores.users.get(user) === undefined) {
|
|
719
|
+
// Fresh-eyes B2b: an ABSENT users.json means this CLI is
|
|
720
|
+
// pointed at roots that have never been claimed — almost
|
|
721
|
+
// always the host-vs-container trap (the claim happened in
|
|
722
|
+
// the container's /data; the host CLI reads XDG roots), so
|
|
723
|
+
// "add the user" is the wrong remedy. A PRESENT directory
|
|
724
|
+
// without the user is a genuine unknown user.
|
|
725
|
+
if (!existsSync(join(roots.configDir, "users.json"))) {
|
|
726
|
+
throw new Error(`unknown user "${user}" — no user directory at ${roots.configDir}: this CLI's state roots hold no claimed install. ` +
|
|
727
|
+
`If the server runs in a container, mint the token there (docker exec <ctr> sigrun token create …), ` +
|
|
728
|
+
`or point this CLI at the same state roots (SIGRUN_CONFIG_DIR/SIGRUN_DATA_DIR) as the server.`);
|
|
729
|
+
}
|
|
693
730
|
throw new Error(`unknown user "${user}" — add it first: sigrun user add ${user}`);
|
|
694
731
|
}
|
|
695
732
|
const { token } = await stores.tokens.create(parsed.tokenName, user);
|
|
@@ -810,15 +847,6 @@ async function runIdentityCommand(parsed, env, io, isolationProbe) {
|
|
|
810
847
|
// parseArgs guarantees one of the cases above.
|
|
811
848
|
throw new Error(`unhandled ${parsed.command} subcommand`);
|
|
812
849
|
}
|
|
813
|
-
/**
|
|
814
|
-
* The CLI-local mirror of the server's proxy-only rule (proxy-trust
|
|
815
|
-
* Task 6): no token callers exist, so a setup-code prompt can never
|
|
816
|
-
* be redeemed. Kept here rather than imported from src/server — the
|
|
817
|
-
* CLI does not import server auth modules.
|
|
818
|
-
*/
|
|
819
|
-
function isProxyOnly(methods) {
|
|
820
|
-
return methods.includes("proxy") && !methods.includes("token");
|
|
821
|
-
}
|
|
822
850
|
/**
|
|
823
851
|
* The pre-launch onboarding flow (Task 7, spec §5.8): `attach`/`run`
|
|
824
852
|
* ask the server its state; an UNCLAIMED server gets the interactive
|
|
@@ -830,7 +858,14 @@ function isProxyOnly(methods) {
|
|
|
830
858
|
* old path and surfaces whatever the server says. A failed CLAIM is
|
|
831
859
|
* loud (the server's error body, exit 1) — the operator re-runs.
|
|
832
860
|
*/
|
|
833
|
-
async function onboardIfUnclaimed(server, prompt, fetchImpl
|
|
861
|
+
export async function onboardIfUnclaimed(server, prompt, fetchImpl,
|
|
862
|
+
/**
|
|
863
|
+
* `loopbackVouch`: set by the `run` command — its companion boot is
|
|
864
|
+
* loopback-bound, so the claim redeems on the loopback vouch even
|
|
865
|
+
* though the (unauthenticated) methods list is the proxy-only
|
|
866
|
+
* fail-safe shape. Unset (attach) keeps the strict guard.
|
|
867
|
+
*/
|
|
868
|
+
opts) {
|
|
834
869
|
let state;
|
|
835
870
|
try {
|
|
836
871
|
const response = await fetchImpl(`${server}/v1/onboarding`, {
|
|
@@ -846,6 +881,10 @@ async function onboardIfUnclaimed(server, prompt, fetchImpl) {
|
|
|
846
881
|
const body = (await response.json().catch(() => undefined));
|
|
847
882
|
if (body?.error !== "setup_required")
|
|
848
883
|
return undefined;
|
|
884
|
+
// The 403 carries no methods: the posture is unknown, so the
|
|
885
|
+
// dead-end guard stays armed (`[]` is proxy-only by definition —
|
|
886
|
+
// a stranger-facing server that hides its methods must not
|
|
887
|
+
// prompt for a setup code it may not accept).
|
|
849
888
|
state = { state: "unclaimed", methods: [], codeRequired: true };
|
|
850
889
|
}
|
|
851
890
|
else {
|
|
@@ -858,10 +897,21 @@ async function onboardIfUnclaimed(server, prompt, fetchImpl) {
|
|
|
858
897
|
}
|
|
859
898
|
if (state.state !== "unclaimed")
|
|
860
899
|
return undefined;
|
|
861
|
-
// Proxy-trust Task 6: a proxy-only
|
|
862
|
-
//
|
|
863
|
-
//
|
|
864
|
-
|
|
900
|
+
// Proxy-trust Task 6 (+ polish task 12's parity ruling): a proxy-only
|
|
901
|
+
// server mints no redeemable setup code, so ANY CLI claim against it
|
|
902
|
+
// dead-ends — even a non-loopback deployment with NO proxy secret
|
|
903
|
+
// (the loopback peer vouches in that shape, so codeRequired is false,
|
|
904
|
+
// yet the claim 403s on own-host protection). The webui gates on
|
|
905
|
+
// codeRequired && no proxyIdentity alone; the CLI used to ALSO exempt
|
|
906
|
+
// that loopback case and prompt for a code the server then refuses
|
|
907
|
+
// with 403 proxy_identity_required. The CLI now gates exactly like
|
|
908
|
+
// the webui: a proxy-only server means onboarding happens in a
|
|
909
|
+
// browser, no exceptions. (The run-mode companion boot IS exempted:
|
|
910
|
+
// it binds loopback, which both drops codeRequired AND vouches the
|
|
911
|
+
// identity. Its methods are [] — proxy-only by the fail-safe — and
|
|
912
|
+
// the claim then succeeds on the loopback vouch.)
|
|
913
|
+
const methods = state.methods; // the ?? [] version-skew guard lives in isProxyOnly itself (fail-safe on unknown posture)
|
|
914
|
+
if (isProxyOnly(methods) && state.proxyIdentity === undefined && opts?.loopbackVouch !== true) {
|
|
865
915
|
throw new Error("sigrun: this sigrun onboards through its sign-in proxy; open it in a browser");
|
|
866
916
|
}
|
|
867
917
|
let code;
|
|
@@ -965,6 +1015,12 @@ export async function cliMain(argv, env = process.env, deps, io = {
|
|
|
965
1015
|
}
|
|
966
1016
|
try {
|
|
967
1017
|
switch (parsed.command) {
|
|
1018
|
+
case "help": {
|
|
1019
|
+
// Help goes to the DATA sink (stdout): the operator asked for
|
|
1020
|
+
// it, so exit 0 — only a usage ERROR prints to stderr + exit 1.
|
|
1021
|
+
io.out?.(USAGE_TEXT);
|
|
1022
|
+
return 0;
|
|
1023
|
+
}
|
|
968
1024
|
case "serve": {
|
|
969
1025
|
await realDeps.serve(env);
|
|
970
1026
|
return 0;
|
|
@@ -1028,8 +1084,15 @@ export async function cliMain(argv, env = process.env, deps, io = {
|
|
|
1028
1084
|
const server = `http://localhost:${booted.port}`;
|
|
1029
1085
|
// Loopback onboarding (Task 7): no code (loopback is the trust);
|
|
1030
1086
|
// tui.json gains ONLY the user — workstation run stays
|
|
1031
|
-
// unauthenticated, so there is no token to keep.
|
|
1032
|
-
|
|
1087
|
+
// unauthenticated, so there is no token to keep. The
|
|
1088
|
+
// loopbackVouch exemption (fresh-eyes A5): this boot's methods
|
|
1089
|
+
// are [] (unauthenticated loopback), which the dead-end guard
|
|
1090
|
+
// reads as proxy-only — but the claim HERE redeems on the
|
|
1091
|
+
// loopback vouch, so run mode must skip the guard or a fresh
|
|
1092
|
+
// `sigrun run` dead-ends on the proxy message.
|
|
1093
|
+
const onboarded = await onboardIfUnclaimed(server, prompt, fetchImpl, {
|
|
1094
|
+
loopbackVouch: true,
|
|
1095
|
+
});
|
|
1033
1096
|
if (onboarded !== undefined) {
|
|
1034
1097
|
await writeTuiFile({ user: onboarded.user }, env);
|
|
1035
1098
|
}
|
package/dist/curation/host.js
CHANGED
|
@@ -119,6 +119,7 @@ export function createCurationHost(deps) {
|
|
|
119
119
|
let count = 0;
|
|
120
120
|
let scanned = 0;
|
|
121
121
|
let cursor;
|
|
122
|
+
let truncated = false;
|
|
122
123
|
for (;;) {
|
|
123
124
|
const page = await conversation.entries(query, MODEL_CALLS_PAGE, cursor, BACKGROUND_CONTEXT);
|
|
124
125
|
for (const entry of page.items) {
|
|
@@ -129,13 +130,20 @@ export function createCurationHost(deps) {
|
|
|
129
130
|
continue;
|
|
130
131
|
count += 1;
|
|
131
132
|
}
|
|
132
|
-
if (page.next === undefined ||
|
|
133
|
-
|
|
134
|
-
|
|
133
|
+
if (page.next === undefined || page.items.length === 0)
|
|
134
|
+
break;
|
|
135
|
+
if (scanned >= MODEL_CALLS_SCAN_LIMIT) {
|
|
136
|
+
truncated = true;
|
|
135
137
|
break;
|
|
136
138
|
}
|
|
137
139
|
cursor = page.next;
|
|
138
140
|
}
|
|
141
|
+
if (truncated) {
|
|
142
|
+
// Report, never truncate silently (the A9 ruling): the count
|
|
143
|
+
// is a LOWER bound — a review gated on it may fire early,
|
|
144
|
+
// and the operator must know the scan guard tripped.
|
|
145
|
+
console.warn(`curation: modelCallsSince(${String(id)}) hit the ${MODEL_CALLS_SCAN_LIMIT}-entry scan guard — the reported count ${count} is a lower bound (the conversation is longer)`);
|
|
146
|
+
}
|
|
139
147
|
return count;
|
|
140
148
|
},
|
|
141
149
|
async takeHints(id) {
|
|
@@ -202,7 +210,9 @@ export function createCurationHost(deps) {
|
|
|
202
210
|
list: () => store.list(),
|
|
203
211
|
view: (name, file) => store.view(name, file),
|
|
204
212
|
feed: (limit, before) => store.feed(limit, before),
|
|
205
|
-
pin: (name, pinned) =>
|
|
213
|
+
pin: async (name, pinned) => {
|
|
214
|
+
await store.pin(name, pinned);
|
|
215
|
+
},
|
|
206
216
|
undo: (entryIds) => store.undo(entryIds),
|
|
207
217
|
apply: (name, change, meta) => store.apply(name, change, {
|
|
208
218
|
actor: "curator",
|
|
@@ -301,25 +311,40 @@ export function createCurationHost(deps) {
|
|
|
301
311
|
const conversation = await harness.conversation(runId, BACKGROUND_CONTEXT);
|
|
302
312
|
if (conversation === undefined)
|
|
303
313
|
return;
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
const details = message.details;
|
|
315
|
-
const change = details?.curation;
|
|
316
|
-
if (change === undefined)
|
|
317
|
-
continue;
|
|
318
|
-
const target = curation.byTool(message.toolName);
|
|
319
|
-
if (target === undefined)
|
|
314
|
+
// Cursor-complete walk (the A9 ruling): a run with more tool
|
|
315
|
+
// results than one page loses NOTHING — pages until exhaustion,
|
|
316
|
+
// bounded so a cursor bug cannot loop forever.
|
|
317
|
+
let cursor;
|
|
318
|
+
let scanned = 0;
|
|
319
|
+
for (;;) {
|
|
320
|
+
const page = await conversation.entries({ order: "ascending" }, 500, cursor, BACKGROUND_CONTEXT);
|
|
321
|
+
for (const entry of page.items) {
|
|
322
|
+
scanned += 1;
|
|
323
|
+
if (entry.kind !== "pi.tool-result")
|
|
320
324
|
continue;
|
|
321
|
-
|
|
325
|
+
for (const message of entry.model ?? []) {
|
|
326
|
+
if (message.role !== "toolResult")
|
|
327
|
+
continue;
|
|
328
|
+
// SAFETY: the tool-result details carry the A6/A8
|
|
329
|
+
// `details.curation` shape ({changeId, summary}); the
|
|
330
|
+
// structural read below only touches those two fields.
|
|
331
|
+
const details = message.details;
|
|
332
|
+
const change = details?.curation;
|
|
333
|
+
if (change === undefined)
|
|
334
|
+
continue;
|
|
335
|
+
const target = curation.byTool(message.toolName);
|
|
336
|
+
if (target === undefined)
|
|
337
|
+
continue;
|
|
338
|
+
await target.undo(user, change.changeId, { abortSignal: undefined });
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
if (page.next === undefined || page.items.length === 0)
|
|
342
|
+
return;
|
|
343
|
+
if (scanned >= MODEL_CALLS_SCAN_LIMIT) {
|
|
344
|
+
console.warn(`curation: undoTargetChanges hit the ${MODEL_CALLS_SCAN_LIMIT}-entry scan guard on review ${reviewId} — target undos beyond the guard were NOT run`);
|
|
345
|
+
return;
|
|
322
346
|
}
|
|
347
|
+
cursor = page.next;
|
|
323
348
|
}
|
|
324
349
|
}
|
|
325
350
|
/** The run conversation carrying one reviewId (or undefined). */
|