@sigrun/sigrun 0.6.0 → 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 +102 -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/packages.js +7 -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 +13 -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
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Task 11: every-root discovery with safe reads.
|
|
3
|
+
*
|
|
4
|
+
* `listAllRoots` is the Skills-tab (core/skills view) counterpart to
|
|
5
|
+
* `discoverSkills`: where `merge()` keeps only the winning skill per
|
|
6
|
+
* name, this lists EVERY root's skills — losers included — in precedence
|
|
7
|
+
* order (workspace > learned > user dir > packages in install order),
|
|
8
|
+
* with shadowing info (`shadowedBy` names the root kind that won), the
|
|
9
|
+
* hidden flag, and `{error}` rows for skills that fail the safety
|
|
10
|
+
* checks. The frontmatter parsing is `scanFsRoot`'s own.
|
|
11
|
+
*
|
|
12
|
+
* `readSkillFile` / `listSupportFiles` are the ONLY read paths the view
|
|
13
|
+
* uses: core reads these roots with HOST privileges, so every read
|
|
14
|
+
* refuses symlinks on EVERY path segment, verifies real-path containment
|
|
15
|
+
* within the discovery root, allows only SKILL.md or
|
|
16
|
+
* `references|templates|scripts|assets/<rel>`, and caps reads at
|
|
17
|
+
* 256 KiB — the same checks the curator store applies
|
|
18
|
+
* (`assertNoSymlinks` + support-file shape). Without them the view is a
|
|
19
|
+
* confused deputy: a symlinked SKILL.md would show the user a file the
|
|
20
|
+
* agent's sandbox could never read. A skill that fails the checks
|
|
21
|
+
* becomes `{error}` and is never read further.
|
|
22
|
+
*/
|
|
23
|
+
import { lstat, readdir, realpath, readFile, stat } from "node:fs/promises";
|
|
24
|
+
import { join, sep } from "node:path";
|
|
25
|
+
import { validSkillName } from "../curation/names.js";
|
|
26
|
+
import { parseSkillFile } from "./index.js";
|
|
27
|
+
/** The largest file the view will read (SKILL.md or a support file). */
|
|
28
|
+
export const SKILL_READ_CAP_BYTES = 256 * 1024;
|
|
29
|
+
const SKILL_FILE = "SKILL.md";
|
|
30
|
+
const SUPPORT_ROOTS = ["references", "templates", "scripts", "assets"];
|
|
31
|
+
const SYMLINK_REFUSED = "symlinks are not allowed in skill files";
|
|
32
|
+
/**
|
|
33
|
+
* The file argument's allowed shapes: exactly `SKILL.md`, or a relative
|
|
34
|
+
* path under `references/`, `templates/`, `scripts/`, or `assets/` with
|
|
35
|
+
* no empty/`.`/`..` segments, no backslashes, no absolute prefix, no NUL.
|
|
36
|
+
* Returns the forward-slash segments.
|
|
37
|
+
*/
|
|
38
|
+
function fileSegments(file) {
|
|
39
|
+
if (file === SKILL_FILE)
|
|
40
|
+
return [file];
|
|
41
|
+
if (file.includes("\\") || file.startsWith("/") || file.includes("\0")) {
|
|
42
|
+
throw new Error("skill file must be SKILL.md or a relative support-file path");
|
|
43
|
+
}
|
|
44
|
+
const parts = file.split("/");
|
|
45
|
+
if (parts.length < 2 ||
|
|
46
|
+
!SUPPORT_ROOTS.includes(parts[0]) ||
|
|
47
|
+
parts.some((part) => part === "" || part === "." || part === "..")) {
|
|
48
|
+
throw new Error("skill file must be SKILL.md or under references/, templates/, scripts/ or assets/");
|
|
49
|
+
}
|
|
50
|
+
return parts;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Refuse symlinks on EVERY segment of `<root>/<segments>` (the curator
|
|
54
|
+
* store's `assertNoSymlinks` check): a symlink anywhere on the path means
|
|
55
|
+
* the read target is not what the root contains, so the read is refused
|
|
56
|
+
* outright. Returns once a segment does not exist — the read itself will
|
|
57
|
+
* report the absence.
|
|
58
|
+
*/
|
|
59
|
+
async function assertNoSymlinks(root, segments) {
|
|
60
|
+
let path = root;
|
|
61
|
+
for (const segment of segments) {
|
|
62
|
+
path = join(path, segment);
|
|
63
|
+
try {
|
|
64
|
+
if ((await lstat(path)).isSymbolicLink())
|
|
65
|
+
throw new Error(SYMLINK_REFUSED);
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
if (error.code === "ENOENT")
|
|
69
|
+
return;
|
|
70
|
+
throw error;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Real-path containment: after resolving every link, `target` must sit
|
|
76
|
+
* inside `root`. The belt to `assertNoSymlinks`'s braces — a hardlink-free
|
|
77
|
+
* lstat walk still cannot see a rename race, and containment catches any
|
|
78
|
+
* resolution that escaped the anchor regardless of how.
|
|
79
|
+
*/
|
|
80
|
+
async function assertContained(root, target) {
|
|
81
|
+
const [realRoot, realTarget] = await Promise.all([
|
|
82
|
+
realpath(root),
|
|
83
|
+
realpath(target),
|
|
84
|
+
]);
|
|
85
|
+
if (realTarget !== realRoot && !realTarget.startsWith(realRoot + sep)) {
|
|
86
|
+
throw new Error("skill file escapes its discovery root");
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* The ONLY read path the core/skills view uses. `rootDir` is the
|
|
91
|
+
* discovery root (the containment anchor), `skill` the skill directory
|
|
92
|
+
* name inside it, `file` either `SKILL.md` or a support-file path under
|
|
93
|
+
* `references|templates|scripts|assets`. Symlinks on any segment and any
|
|
94
|
+
* escape from `rootDir` are refused; reads over 256 KiB are refused.
|
|
95
|
+
*/
|
|
96
|
+
export async function readSkillFile(rootDir, skill, file) {
|
|
97
|
+
if (!validSkillName(skill))
|
|
98
|
+
throw new Error(`invalid skill name: ${skill}`);
|
|
99
|
+
const segments = [skill, ...fileSegments(file)];
|
|
100
|
+
await assertNoSymlinks(rootDir, segments);
|
|
101
|
+
const target = join(rootDir, ...segments);
|
|
102
|
+
await assertContained(rootDir, target);
|
|
103
|
+
const info = await stat(target);
|
|
104
|
+
if (info.size > SKILL_READ_CAP_BYTES) {
|
|
105
|
+
throw new Error(`skill file exceeds the 256 KiB read cap: ${file}`);
|
|
106
|
+
}
|
|
107
|
+
const buffer = await readFile(target);
|
|
108
|
+
if (buffer.byteLength > SKILL_READ_CAP_BYTES) {
|
|
109
|
+
throw new Error(`skill file exceeds the 256 KiB read cap: ${file}`);
|
|
110
|
+
}
|
|
111
|
+
return buffer.toString("utf8");
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Recursively collect relative file paths under `dir`, never following
|
|
115
|
+
* symlinks: a symlinked entry (file or directory) is skipped, not
|
|
116
|
+
* traversed — the listing mirrors what `readSkillFile` would serve.
|
|
117
|
+
*/
|
|
118
|
+
async function collectFiles(dir, prefix, out) {
|
|
119
|
+
let entries;
|
|
120
|
+
try {
|
|
121
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
122
|
+
}
|
|
123
|
+
catch (error) {
|
|
124
|
+
if (error.code === "ENOENT")
|
|
125
|
+
return;
|
|
126
|
+
throw error;
|
|
127
|
+
}
|
|
128
|
+
entries.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
|
|
129
|
+
for (const entry of entries) {
|
|
130
|
+
if (entry.isSymbolicLink())
|
|
131
|
+
continue;
|
|
132
|
+
const relative = `${prefix}/${entry.name}`;
|
|
133
|
+
if (entry.isDirectory()) {
|
|
134
|
+
await collectFiles(join(dir, entry.name), relative, out);
|
|
135
|
+
}
|
|
136
|
+
else if (entry.isFile()) {
|
|
137
|
+
out.push(relative);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* The support-file listing under one skill, with the same safety posture
|
|
143
|
+
* as `readSkillFile`: the skill directory must be symlink-free and
|
|
144
|
+
* contained in `rootDir`, and symlinked entries inside the support roots
|
|
145
|
+
* are excluded rather than followed. A skill without support files (or
|
|
146
|
+
* without a directory at all) lists nothing.
|
|
147
|
+
*/
|
|
148
|
+
export async function listSupportFiles(rootDir, skill) {
|
|
149
|
+
if (!validSkillName(skill))
|
|
150
|
+
throw new Error(`invalid skill name: ${skill}`);
|
|
151
|
+
await assertNoSymlinks(rootDir, [skill]);
|
|
152
|
+
const skillDir = join(rootDir, skill);
|
|
153
|
+
try {
|
|
154
|
+
if (!(await lstat(skillDir)).isDirectory())
|
|
155
|
+
return [];
|
|
156
|
+
}
|
|
157
|
+
catch (error) {
|
|
158
|
+
if (error.code === "ENOENT")
|
|
159
|
+
return [];
|
|
160
|
+
throw error;
|
|
161
|
+
}
|
|
162
|
+
await assertContained(rootDir, skillDir);
|
|
163
|
+
const out = [];
|
|
164
|
+
for (const root of SUPPORT_ROOTS) {
|
|
165
|
+
await collectFiles(join(skillDir, root), root, out);
|
|
166
|
+
}
|
|
167
|
+
return out.sort();
|
|
168
|
+
}
|
|
169
|
+
/** The safety check a skill must pass before its SKILL.md is ever read. */
|
|
170
|
+
async function skillSafetyError(rootDir, dirName) {
|
|
171
|
+
try {
|
|
172
|
+
await assertNoSymlinks(rootDir, [dirName]);
|
|
173
|
+
const skillDir = join(rootDir, dirName);
|
|
174
|
+
if (!(await lstat(skillDir)).isDirectory())
|
|
175
|
+
return undefined; // not a skill
|
|
176
|
+
await assertContained(rootDir, skillDir);
|
|
177
|
+
// SKILL.md itself: present, symlink-free, and inside the root.
|
|
178
|
+
await assertNoSymlinks(rootDir, [dirName, SKILL_FILE]);
|
|
179
|
+
await assertContained(rootDir, join(skillDir, SKILL_FILE));
|
|
180
|
+
return undefined;
|
|
181
|
+
}
|
|
182
|
+
catch (error) {
|
|
183
|
+
return error instanceof Error ? error.message : String(error);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* List ONE root's skill directories, applying the safety checks before
|
|
188
|
+
* any read. Entries that are not directories, and directories without a
|
|
189
|
+
* SKILL.md, are not skills (the v1 rule, unchanged). A symlinked skill
|
|
190
|
+
* dir or SKILL.md is an error row — named by its directory, never read.
|
|
191
|
+
* Readable skills parse through `parseSkillFile` (scanFsRoot's own
|
|
192
|
+
* frontmatter parsing); within one root, duplicate names resolve
|
|
193
|
+
* first-wins in sorted directory order with a log, exactly like
|
|
194
|
+
* scanFsRoot.
|
|
195
|
+
*/
|
|
196
|
+
async function scanRootSafely(root) {
|
|
197
|
+
let entries;
|
|
198
|
+
try {
|
|
199
|
+
entries = await readdir(root.rootDir, { withFileTypes: true });
|
|
200
|
+
}
|
|
201
|
+
catch (error) {
|
|
202
|
+
if (error.code === "ENOENT")
|
|
203
|
+
return [];
|
|
204
|
+
throw error;
|
|
205
|
+
}
|
|
206
|
+
const names = [...new Set(entries.map((entry) => entry.name))].sort();
|
|
207
|
+
const skills = [];
|
|
208
|
+
const byName = new Map();
|
|
209
|
+
for (const dirName of names) {
|
|
210
|
+
if (!validSkillName(dirName))
|
|
211
|
+
continue;
|
|
212
|
+
const dir = join(root.rootDir, dirName);
|
|
213
|
+
const base = {
|
|
214
|
+
root: root.kind,
|
|
215
|
+
rootDir: root.rootDir,
|
|
216
|
+
dir,
|
|
217
|
+
hidden: false,
|
|
218
|
+
...(root.packageName === undefined ? {} : { packageName: root.packageName }),
|
|
219
|
+
};
|
|
220
|
+
// A symlinked entry (skill dir or SKILL.md inside) is an error row —
|
|
221
|
+
// never read further, so the target's content leaks nowhere.
|
|
222
|
+
const safetyError = await skillSafetyError(root.rootDir, dirName);
|
|
223
|
+
if (safetyError !== undefined) {
|
|
224
|
+
skills.push({ ...base, name: dirName, description: "", error: safetyError });
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
let text;
|
|
228
|
+
try {
|
|
229
|
+
text = await readSkillFile(root.rootDir, dirName, SKILL_FILE);
|
|
230
|
+
}
|
|
231
|
+
catch (error) {
|
|
232
|
+
if (error.code === "ENOENT")
|
|
233
|
+
continue; // no SKILL.md — not a skill
|
|
234
|
+
skills.push({
|
|
235
|
+
...base,
|
|
236
|
+
name: dirName,
|
|
237
|
+
description: "",
|
|
238
|
+
error: error instanceof Error ? error.message : String(error),
|
|
239
|
+
});
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
const parsed = parseSkillFile(text);
|
|
243
|
+
if (parsed === undefined || parsed.name === undefined || parsed.description === undefined) {
|
|
244
|
+
// The v1 skip rule (a malformed SKILL.md is not a skill) holds for
|
|
245
|
+
// the view too — the row simply does not appear.
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
const skill = {
|
|
249
|
+
...base,
|
|
250
|
+
name: parsed.name,
|
|
251
|
+
description: parsed.description,
|
|
252
|
+
hidden: parsed.disableModelInvocation,
|
|
253
|
+
};
|
|
254
|
+
if (!byName.has(skill.name)) {
|
|
255
|
+
byName.set(skill.name, skill);
|
|
256
|
+
skills.push(skill);
|
|
257
|
+
}
|
|
258
|
+
else {
|
|
259
|
+
console.warn(`skills: duplicate name "${skill.name}" (${dir} ignored; ${byName.get(skill.name)?.dir} wins)`);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
return skills.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* Every root's skills, losers included, in precedence order: workspace,
|
|
266
|
+
* learned, user dir, then packages in install order. A name listed by
|
|
267
|
+
* several roots wins in the FIRST (highest-precedence) root; every
|
|
268
|
+
* lower-precedence row carries `shadowedBy` naming the winning root
|
|
269
|
+
* kind. Absent roots are the designed empty case.
|
|
270
|
+
*/
|
|
271
|
+
export async function listAllRoots(opts) {
|
|
272
|
+
const roots = [];
|
|
273
|
+
if (opts.workspace !== undefined) {
|
|
274
|
+
roots.push({ kind: "workspace", rootDir: opts.workspace });
|
|
275
|
+
}
|
|
276
|
+
if (opts.learned !== undefined) {
|
|
277
|
+
roots.push({ kind: "learned", rootDir: opts.learned });
|
|
278
|
+
}
|
|
279
|
+
roots.push({ kind: "user dir", rootDir: opts.userDir });
|
|
280
|
+
for (const pkg of opts.packageRoots) {
|
|
281
|
+
roots.push({ kind: "package", rootDir: pkg.dir, packageName: pkg.extension });
|
|
282
|
+
}
|
|
283
|
+
const out = [];
|
|
284
|
+
const winners = new Map();
|
|
285
|
+
for (const root of roots) {
|
|
286
|
+
for (const skill of await scanRootSafely(root)) {
|
|
287
|
+
if (skill.error === undefined) {
|
|
288
|
+
const winner = winners.get(skill.name);
|
|
289
|
+
if (winner === undefined) {
|
|
290
|
+
winners.set(skill.name, skill.root);
|
|
291
|
+
}
|
|
292
|
+
else {
|
|
293
|
+
out.push({ ...skill, shadowedBy: winner });
|
|
294
|
+
continue;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
out.push(skill);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
return out;
|
|
301
|
+
}
|
|
@@ -5,6 +5,19 @@ 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
|
+
|
|
9
|
+
## [0.7.0] - 2026-11-04
|
|
10
|
+
|
|
11
|
+
- New `./model-label` subpath: `modelBreadcrumb` collapses every non-leaf path segment of
|
|
12
|
+
a model id to its first character and prepends the provider (`bifrost` +
|
|
13
|
+
`chain/acme-flash` → `b/c/acme-flash`).
|
|
14
|
+
- New `./auth` subpath: the one `isProxyOnly` predicate (server, webui, CLI), treating
|
|
15
|
+
absent/empty method lists as proxy-only (fail safe).
|
|
16
|
+
|
|
17
|
+
## [0.6.1] - 2026-11-03
|
|
18
|
+
|
|
19
|
+
- Version pin to the 0.6.1 core family (no shared-source changes beyond the version).
|
|
20
|
+
|
|
8
21
|
## [0.6.0] - 2026-11-02
|
|
9
22
|
|
|
10
23
|
Version-companion release for sigrun 0.6.0 (no shared changes).
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The auth posture shared by the server, the webui, and the CLI
|
|
3
|
+
* (polish task 12; proxy-trust spec §5.2/§5.8).
|
|
4
|
+
*
|
|
5
|
+
* `isProxyOnly` had THREE private copies (src/server/auth/proxy-only.ts,
|
|
6
|
+
* webui/src/auth/AuthGate.tsx, src/cli.ts) that could drift: a proxy-only
|
|
7
|
+
* server mints no redeemable setup code, so every surface must agree on
|
|
8
|
+
* what "proxy-only" means. One definition lives here now; the server's
|
|
9
|
+
* own module re-exports it for its existing callers.
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* Whether the configured auth methods are exactly proxy-only: a `proxy`
|
|
13
|
+
* method with no `token` method. A proxy-only server has no token
|
|
14
|
+
* callers, so an unvouched setup-code prompt can never be redeemed —
|
|
15
|
+
* onboarding happens through the proxy in a browser.
|
|
16
|
+
*
|
|
17
|
+
* UNKNOWN posture fails safe: an absent/empty method list (the 403
|
|
18
|
+
* setup_required path carries no methods; a pre-methods server speaks
|
|
19
|
+
* an older shape) counts as proxy-only — a stranger-facing server whose
|
|
20
|
+
* auth methods cannot be seen must not invite an unredeemable claim.
|
|
21
|
+
*/
|
|
22
|
+
export declare function isProxyOnly(methods: readonly string[] | undefined): boolean;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The auth posture shared by the server, the webui, and the CLI
|
|
3
|
+
* (polish task 12; proxy-trust spec §5.2/§5.8).
|
|
4
|
+
*
|
|
5
|
+
* `isProxyOnly` had THREE private copies (src/server/auth/proxy-only.ts,
|
|
6
|
+
* webui/src/auth/AuthGate.tsx, src/cli.ts) that could drift: a proxy-only
|
|
7
|
+
* server mints no redeemable setup code, so every surface must agree on
|
|
8
|
+
* what "proxy-only" means. One definition lives here now; the server's
|
|
9
|
+
* own module re-exports it for its existing callers.
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* Whether the configured auth methods are exactly proxy-only: a `proxy`
|
|
13
|
+
* method with no `token` method. A proxy-only server has no token
|
|
14
|
+
* callers, so an unvouched setup-code prompt can never be redeemed —
|
|
15
|
+
* onboarding happens through the proxy in a browser.
|
|
16
|
+
*
|
|
17
|
+
* UNKNOWN posture fails safe: an absent/empty method list (the 403
|
|
18
|
+
* setup_required path carries no methods; a pre-methods server speaks
|
|
19
|
+
* an older shape) counts as proxy-only — a stranger-facing server whose
|
|
20
|
+
* auth methods cannot be seen must not invite an unredeemable claim.
|
|
21
|
+
*/
|
|
22
|
+
export function isProxyOnly(methods) {
|
|
23
|
+
if (methods === undefined || methods.length === 0)
|
|
24
|
+
return true;
|
|
25
|
+
return methods.includes("proxy") && !methods.includes("token");
|
|
26
|
+
}
|
|
@@ -18,7 +18,7 @@ export { lttb } from "./lttb.js";
|
|
|
18
18
|
export { formatValue } from "./format.js";
|
|
19
19
|
export { toText } from "./text.js";
|
|
20
20
|
export { canonicalJson, eventKeyWeb, instanceKey } from "./instance.js";
|
|
21
|
-
export type { Action, ActionButton, Bindable, Binding, Block, BlockBase, BulkAction, CalloutBlock, Card, CardBadge, CardMetric, CardMeta, CardStatus, ChartAxisX, ChartAxisY, ChartBlock, ChartSeries, ChartThreshold, ChoiceField, ChoiceOption, CollectionBlock, CollectionColumn, CollectionGroup, CollectionSort, CollectionTime, ColumnChild, ColumnsBlock, Confirm, CoreTarget, DataOp, DataOps, DataUpdate, DateField, DatePreset, DaterangeField, DatetimeField, DetailsBlock, DiffBlock, Field, FieldBase, FieldsBlock, FieldsRow, FileField, Format, FormBlock, GridBlock, GridChild, ImageBlock, ListField, LogBlock, LogLine, LongtextField, MarkdownBlock, MeterBlock, MeterThreshold, NumberField, PageBlock, PageEmpty, Placement, SectionBlock, SecretField, Size, StackBlock, StatBlock, TextField, ToggleField, Tone, Tree, TreeBlock, TreeNode, WidthCondition, } from "./types.js";
|
|
21
|
+
export type { Action, ActionButton, Bindable, Binding, Block, BlockBase, BulkAction, CalloutBlock, Card, CardBadge, CardMetric, CardMeta, CardStatus, ChartAxisX, ChartAxisY, ChartBlock, ChartSeries, ChartThreshold, ChoiceField, ChoiceOption, CollectionBlock, CollectionColumn, CollectionGroup, CollectionSort, CollectionTime, ColumnChild, ColumnsBlock, Confirm, CoreTarget, DataOp, DataOps, DataUpdate, DateField, DatePreset, DaterangeField, DatetimeField, DetailsBlock, DiffBlock, Field, FieldBase, FieldsBlock, FieldsRow, FileField, Format, FormBlock, GridBlock, GridChild, ImageBlock, ListField, LogBlock, LogLine, LongtextField, MarkdownBlock, MeterBlock, MeterThreshold, NumberField, PageBlock, PageEmpty, PageSelection, Placement, SectionBlock, SecretField, Size, StackBlock, StatBlock, TextField, ToggleField, Tone, Tree, TreeBlock, TreeNode, WidthCondition, } from "./types.js";
|
|
22
22
|
export type { IconName } from "./icons.js";
|
|
23
23
|
export type { TextOptions } from "./text.js";
|
|
24
24
|
export type { SurfaceAccess, SurfaceActErr, SurfaceActOk, SurfaceActResult, SurfaceChangedEvent, SurfaceFetch, SurfaceLoad, SurfaceTransport, SurfaceUploadBody, } from "./transport.js";
|
|
@@ -108,6 +108,19 @@ export interface PageEmpty {
|
|
|
108
108
|
title: string;
|
|
109
109
|
hint?: string;
|
|
110
110
|
}
|
|
111
|
+
/**
|
|
112
|
+
* Server-declared list+detail drill-in (spec §12.3), presentation-only:
|
|
113
|
+
* on compact widths `selected: false` shows the sidebar list full width
|
|
114
|
+
* (main hidden); `selected: true` shows main with a back arrow that
|
|
115
|
+
* navigates with `back` as the params. Wide widths are unchanged —
|
|
116
|
+
* sidebar and main sit side by side regardless of `selected`. `toText`
|
|
117
|
+
* ignores it (sidebar then main, as today).
|
|
118
|
+
*/
|
|
119
|
+
export interface PageSelection {
|
|
120
|
+
readonly selected: boolean;
|
|
121
|
+
/** The params the compact back arrow navigates to. */
|
|
122
|
+
readonly back: JsonObject;
|
|
123
|
+
}
|
|
111
124
|
export interface PageBlock extends BlockBase {
|
|
112
125
|
readonly type: "page";
|
|
113
126
|
readonly title: Bindable<string>;
|
|
@@ -121,6 +134,8 @@ export interface PageBlock extends BlockBase {
|
|
|
121
134
|
/** Blocks for a side pane, shown when non-empty. */
|
|
122
135
|
readonly detail?: readonly Block[];
|
|
123
136
|
readonly empty?: PageEmpty;
|
|
137
|
+
/** Compact-width list+detail drill-in (see {@link PageSelection}). */
|
|
138
|
+
readonly selection?: PageSelection;
|
|
124
139
|
}
|
|
125
140
|
export interface StackBlock extends BlockBase {
|
|
126
141
|
readonly type: "stack";
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The model breadcrumb (polish pass Task 5, spec §5.1): how a client
|
|
3
|
+
* displays "which model answered this message".
|
|
4
|
+
*
|
|
5
|
+
* User ruling: the provider is ALWAYS a breadcrumb segment. Every path
|
|
6
|
+
* segment except the leaf collapses to its first character, the leaf
|
|
7
|
+
* stays whole, and the provider joins as its own one-character segment —
|
|
8
|
+
* `openai` + `gpt-4o-mini` reads `o/gpt-4o-mini`, an old bifrost chain
|
|
9
|
+
* message (bare `acme-flash`) `b/acme-flash`, a new one (Task 4 restored
|
|
10
|
+
* the display id, so `chain/acme-flash`) `b/c/acme-flash`. The untruncated
|
|
11
|
+
* id rides along as `full` for a hover title, so the transcript stays
|
|
12
|
+
* compact without losing what it abbreviated.
|
|
13
|
+
*
|
|
14
|
+
* A message with NO provider shows its plain model name: the archive has
|
|
15
|
+
* rows recorded before a provider was stamped, and inventing a segment
|
|
16
|
+
* for them would name a provider nobody recorded.
|
|
17
|
+
*
|
|
18
|
+
* Pure module (no imports at all): both clients render this from one
|
|
19
|
+
* source, so it must never drag in a dependency one of them can't resolve.
|
|
20
|
+
*/
|
|
21
|
+
/**
|
|
22
|
+
* The two strings a client shows for one message's model: `short` is the
|
|
23
|
+
* collapsed breadcrumb (the transcript label), `full` the untruncated
|
|
24
|
+
* `provider/model` id (a hover title).
|
|
25
|
+
*/
|
|
26
|
+
export declare function modelBreadcrumb(provider: string | undefined, model: string): {
|
|
27
|
+
readonly short: string;
|
|
28
|
+
readonly full: string;
|
|
29
|
+
};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The model breadcrumb (polish pass Task 5, spec §5.1): how a client
|
|
3
|
+
* displays "which model answered this message".
|
|
4
|
+
*
|
|
5
|
+
* User ruling: the provider is ALWAYS a breadcrumb segment. Every path
|
|
6
|
+
* segment except the leaf collapses to its first character, the leaf
|
|
7
|
+
* stays whole, and the provider joins as its own one-character segment —
|
|
8
|
+
* `openai` + `gpt-4o-mini` reads `o/gpt-4o-mini`, an old bifrost chain
|
|
9
|
+
* message (bare `acme-flash`) `b/acme-flash`, a new one (Task 4 restored
|
|
10
|
+
* the display id, so `chain/acme-flash`) `b/c/acme-flash`. The untruncated
|
|
11
|
+
* id rides along as `full` for a hover title, so the transcript stays
|
|
12
|
+
* compact without losing what it abbreviated.
|
|
13
|
+
*
|
|
14
|
+
* A message with NO provider shows its plain model name: the archive has
|
|
15
|
+
* rows recorded before a provider was stamped, and inventing a segment
|
|
16
|
+
* for them would name a provider nobody recorded.
|
|
17
|
+
*
|
|
18
|
+
* Pure module (no imports at all): both clients render this from one
|
|
19
|
+
* source, so it must never drag in a dependency one of them can't resolve.
|
|
20
|
+
*/
|
|
21
|
+
/** The id's path segments, empties dropped (`a//b/` → `["a","b"]`). */
|
|
22
|
+
function pathSegments(id) {
|
|
23
|
+
return id.split("/").filter((segment) => segment !== "");
|
|
24
|
+
}
|
|
25
|
+
/** The collapsed breadcrumb path: every non-leaf segment's first
|
|
26
|
+
* character + the whole leaf. Never called with an empty path. */
|
|
27
|
+
function collapse(path) {
|
|
28
|
+
const leaf = path[path.length - 1] ?? "";
|
|
29
|
+
const head = path.slice(0, -1).map((segment) => segment.slice(0, 1));
|
|
30
|
+
return [...head, leaf].join("/");
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* The two strings a client shows for one message's model: `short` is the
|
|
34
|
+
* collapsed breadcrumb (the transcript label), `full` the untruncated
|
|
35
|
+
* `provider/model` id (a hover title).
|
|
36
|
+
*/
|
|
37
|
+
export function modelBreadcrumb(provider, model) {
|
|
38
|
+
if (provider === undefined || provider === "") {
|
|
39
|
+
return { short: model, full: model };
|
|
40
|
+
}
|
|
41
|
+
// The id may already carry its provider (`bifrost/chain/x`): strip the
|
|
42
|
+
// prefix rather than folding it into a second segment, so the provider
|
|
43
|
+
// is never named twice.
|
|
44
|
+
const prefix = `${provider}/`;
|
|
45
|
+
const rest = model.startsWith(prefix) ? model.slice(prefix.length) : model;
|
|
46
|
+
const path = pathSegments(rest);
|
|
47
|
+
if (path.length === 0) {
|
|
48
|
+
// Nothing but the provider — show it whole, never a trailing slash.
|
|
49
|
+
return { short: provider.slice(0, 1), full: provider };
|
|
50
|
+
}
|
|
51
|
+
return {
|
|
52
|
+
short: `${provider.slice(0, 1)}/${collapse(path)}`,
|
|
53
|
+
full: `${provider}/${path.join("/")}`,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
@@ -558,6 +558,13 @@ export interface MetaResponse {
|
|
|
558
558
|
readonly keys: readonly string[];
|
|
559
559
|
}[];
|
|
560
560
|
};
|
|
561
|
+
/**
|
|
562
|
+
* The core-owned view surfaces (tabs+skills Task 4) — the same list
|
|
563
|
+
* `/v1/extensions` serves under `coreViews`; the meta copy exists so
|
|
564
|
+
* the tab shell's rail never fetches `/v1/extensions` separately.
|
|
565
|
+
* Absent when no core views attach.
|
|
566
|
+
*/
|
|
567
|
+
readonly coreViews?: readonly CoreViewInfo[];
|
|
561
568
|
/**
|
|
562
569
|
* Boot-merge collisions (build-and-webui Task 6): name collisions among
|
|
563
570
|
* extension-contributed webui bundles, the provider-catalog merge sink,
|
|
@@ -733,6 +740,27 @@ export interface ExtensionSurfaceInfo {
|
|
|
733
740
|
/** The `GET /v1/extensions` payload (spec §7.7). */
|
|
734
741
|
export interface ExtensionsResponse {
|
|
735
742
|
readonly extensions: readonly ExtensionInfo[];
|
|
743
|
+
/**
|
|
744
|
+
* The core-owned view surfaces (tabs+skills Task 4): one row per
|
|
745
|
+
* `SurfaceDef<"view">` attached under the reserved `core` slug via
|
|
746
|
+
* `SurfaceService.attachCoreViews` (the Jobs/Skills views). Core views
|
|
747
|
+
* are NOT `extensions[]` rows — the health dot stays unaffected — so
|
|
748
|
+
* channels list them from here, ahead of the extension views.
|
|
749
|
+
*/
|
|
750
|
+
readonly coreViews?: readonly CoreViewInfo[];
|
|
751
|
+
}
|
|
752
|
+
/**
|
|
753
|
+
* One core-owned view surface on the wire (tabs+skills Task 4): the
|
|
754
|
+
* id/title/icon the channels' rail/tab registration reads, plus the
|
|
755
|
+
* declared refresh cadence. Mirrors the view half of
|
|
756
|
+
* `ExtensionSurfaceInfo`, minus `placement` (core views are always
|
|
757
|
+
* `view`) and `sharedData` (render-time concern, never list-time).
|
|
758
|
+
*/
|
|
759
|
+
export interface CoreViewInfo {
|
|
760
|
+
readonly id: string;
|
|
761
|
+
readonly title: string;
|
|
762
|
+
readonly icon: string;
|
|
763
|
+
readonly refreshMs?: number;
|
|
736
764
|
}
|
|
737
765
|
/**
|
|
738
766
|
* The `GET /v1/settings/sections` payload (UI-blocks U4 Task 4, spec
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sigrun/shared",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "sigrun shared leaf package \u2014 theme tokens, turn/conversation shaping, and the channel wire types (pure leaf, zero dependencies)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -20,6 +20,10 @@
|
|
|
20
20
|
"types": "./dist/turns.d.ts",
|
|
21
21
|
"default": "./dist/turns.js"
|
|
22
22
|
},
|
|
23
|
+
"./model-label": {
|
|
24
|
+
"types": "./dist/model-label.d.ts",
|
|
25
|
+
"default": "./dist/model-label.js"
|
|
26
|
+
},
|
|
23
27
|
"./session-meta": {
|
|
24
28
|
"types": "./dist/session-meta.d.ts",
|
|
25
29
|
"default": "./dist/session-meta.js"
|
|
@@ -40,6 +44,10 @@
|
|
|
40
44
|
"types": "./dist/access.d.ts",
|
|
41
45
|
"default": "./dist/access.js"
|
|
42
46
|
},
|
|
47
|
+
"./auth": {
|
|
48
|
+
"types": "./dist/auth.d.ts",
|
|
49
|
+
"default": "./dist/auth.js"
|
|
50
|
+
},
|
|
43
51
|
"./settings": {
|
|
44
52
|
"types": "./dist/settings.d.ts",
|
|
45
53
|
"default": "./dist/settings.js"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sigrun/sigrun",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Self-hosted, multi-user personal-assistant server",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -45,12 +45,12 @@
|
|
|
45
45
|
"@earendil-works/pi-tui": "^1.3.0",
|
|
46
46
|
"@fastify/static": "^10.1.7",
|
|
47
47
|
"@sigrun/ext-codemode": "0.1.2",
|
|
48
|
-
"@sigrun/ext-curator": "0.
|
|
49
|
-
"@sigrun/ext-gateway": "0.1.
|
|
48
|
+
"@sigrun/ext-curator": "0.2.0",
|
|
49
|
+
"@sigrun/ext-gateway": "0.1.14",
|
|
50
50
|
"@sigrun/ext-mcp": "0.2.2",
|
|
51
51
|
"@sigrun/ext-tool-search": "0.1.2",
|
|
52
52
|
"@sigrun/oauth": "0.1.1",
|
|
53
|
-
"@sigrun/shared": "0.
|
|
53
|
+
"@sigrun/shared": "0.7.0",
|
|
54
54
|
"croner": "^10.0.1",
|
|
55
55
|
"fastify": "^5.12.7",
|
|
56
56
|
"semver": "^7.8.5",
|
|
@@ -61,9 +61,9 @@
|
|
|
61
61
|
"@sigrun/shared"
|
|
62
62
|
],
|
|
63
63
|
"optionalDependencies": {
|
|
64
|
-
"@sigrun/sandbox-linux-arm64": "0.
|
|
65
|
-
"@sigrun/sandbox-linux-x64": "0.
|
|
66
|
-
"@sigrun/tui": "0.
|
|
64
|
+
"@sigrun/sandbox-linux-arm64": "0.7.0",
|
|
65
|
+
"@sigrun/sandbox-linux-x64": "0.7.0",
|
|
66
|
+
"@sigrun/tui": "0.7.0"
|
|
67
67
|
},
|
|
68
68
|
"devDependencies": {
|
|
69
69
|
"@sigrun/memory-markdown": "file:../sigrun-memory-markdown",
|