@sigrun/sigrun 0.2.0 → 0.2.1
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 +95 -7
- package/README.md +99 -12
- package/dist/blocks/build-stamp.json +13 -0
- package/dist/blocks/builders.d.ts +57 -0
- package/dist/blocks/images.d.ts +26 -0
- package/dist/blocks/index.d.ts +20 -0
- package/dist/blocks/limits.d.ts +25 -0
- package/dist/blocks/placements.d.ts +26 -0
- package/dist/blocks/schema.d.ts +38 -0
- package/dist/blocks/validate.d.ts +73 -0
- package/dist/extension.d.ts +41 -7
- package/dist/harness/boot.js +6 -0
- package/dist/harness/builtins.js +1 -1
- package/dist/harness/seam-host.js +12 -0
- package/dist/harness/sections/context-files.js +1 -1
- package/dist/sandbox/availability.js +69 -20
- package/dist/sandbox/launch.js +11 -0
- package/dist/sandbox/policy.js +24 -3
- package/dist/sandbox/worker.js +4 -24
- package/dist/serve.js +139 -26
- package/dist/server/auth/pipeline.js +52 -3
- package/dist/server/images/cache.js +13 -5
- package/dist/server/index.js +10 -2
- package/dist/server/notifications.js +21 -4
- package/dist/server/routes/images.js +16 -9
- package/dist/server/routes/me.js +1 -1
- package/dist/server/routes/meta.js +4 -0
- package/node_modules/@sigrun/shared/CHANGELOG.md +5 -0
- package/node_modules/@sigrun/shared/dist/views.d.ts +27 -0
- package/node_modules/@sigrun/shared/package.json +1 -1
- package/package.json +8 -9
- package/webui/dist/assets/{index-CxB969qp.js → index-SO2W4jjk.js} +1 -1
- package/webui/dist/index.html +1 -1
- package/CONTEXT.md +0 -163
|
@@ -34,8 +34,14 @@
|
|
|
34
34
|
* request before handlers run) + a non-empty user header; a
|
|
35
35
|
* directory miss auto-provisions (`source:"proxy"`, admin:false)
|
|
36
36
|
* unless `autoProvision:false` → 403 `user_not_provisioned`.
|
|
37
|
+
* Every proxy rejection (401/403) emits ONE sanitized `warn` naming
|
|
38
|
+
* the socket peer + a short reason (release-readiness Task 1, spec
|
|
39
|
+
* §3 — never tokens, cookies, identity-header VALUES, query
|
|
40
|
+
* strings, or the full URL; a request presenting a Bearer/cookie
|
|
41
|
+
* credential is a token attempt and stays silent).
|
|
37
42
|
* `token`: Bearer or the `sigrun_session` cookie → `tokens.verify`;
|
|
38
|
-
* the token's user must exist in the directory, else 401
|
|
43
|
+
* the token's user must exist in the directory, else 401 (no log —
|
|
44
|
+
* token failures are not proxy diagnostics).
|
|
39
45
|
* 6. **Empty list** (the naive workstation path): the trimmed
|
|
40
46
|
* `X-Sigrun-User` header verbatim, `admin: true`, method `"none"`.
|
|
41
47
|
*/
|
|
@@ -184,6 +190,22 @@ function isPublicPath(request, ctx) {
|
|
|
184
190
|
}
|
|
185
191
|
return false;
|
|
186
192
|
}
|
|
193
|
+
/**
|
|
194
|
+
* The proxy-auth rejection diagnostic (release-readiness Task 1, spec §3):
|
|
195
|
+
* ONE `warn` per rejected request, naming the socket peer, the request
|
|
196
|
+
* method, a short reason, and — only when Fastify already resolved it —
|
|
197
|
+
* the ROUTE TEMPLATE (`routeOptions.url`, never the raw URL). The line
|
|
198
|
+
* must never carry API tokens, cookies, identity-header VALUES, query
|
|
199
|
+
* strings, or the full request URL; a header NAME appears only where the
|
|
200
|
+
* reason itself is "that configured header was absent/wrong" (its value
|
|
201
|
+
* never does). This is diagnostic logging, not evidence that the
|
|
202
|
+
* configured allowlist is correct.
|
|
203
|
+
*/
|
|
204
|
+
function warnProxyRejected(request, reason) {
|
|
205
|
+
const peer = request.socket.remoteAddress ?? "unknown";
|
|
206
|
+
const route = request.routeOptions.url;
|
|
207
|
+
console.warn(`sigrun: proxy auth rejected ${request.method} request from ${peer} (${reason})${route === undefined ? "" : ` on ${route}`}`);
|
|
208
|
+
}
|
|
187
209
|
/**
|
|
188
210
|
* Stage 5a (proxy): resolve the forward-auth headers, or reply with the
|
|
189
211
|
* rejection. The secret header is DELETED from the request before any
|
|
@@ -191,7 +213,24 @@ function isPublicPath(request, ctx) {
|
|
|
191
213
|
*/
|
|
192
214
|
async function resolveProxy(request, ctx) {
|
|
193
215
|
const { proxy, adminGroups } = ctx.config.auth;
|
|
216
|
+
// A request presenting a Bearer/cookie CREDENTIAL is attempting token
|
|
217
|
+
// auth (or riding the stage-3 service token), not claiming a proxy
|
|
218
|
+
// identity: when the token method is ALSO configured, suppress the
|
|
219
|
+
// rejection diagnostic — a denied credential is the token method's
|
|
220
|
+
// failure, which never logs (spec §3). The pipeline visits the proxy
|
|
221
|
+
// method FIRST for such a request (a trusted peer presenting a session
|
|
222
|
+
// without proxy headers is a legitimate client shape behind
|
|
223
|
+
// forward-auth), and a "missing secret" warn would fire even when the
|
|
224
|
+
// token then authenticates. In a PROXY-ONLY deployment the suppression
|
|
225
|
+
// does not apply: a rejected credential-bearing request there IS a
|
|
226
|
+
// proxy-method 401 and must log (spec §3 exempts token-auth failures,
|
|
227
|
+
// and no token method exists).
|
|
228
|
+
const attemptingToken = (bearerOf(request) !== undefined || sessionCookieOf(request) !== undefined) &&
|
|
229
|
+
ctx.methods.includes("token");
|
|
194
230
|
if (!ipAllowed(request.socket.remoteAddress, proxy.trustedProxies)) {
|
|
231
|
+
if (!attemptingToken) {
|
|
232
|
+
warnProxyRejected(request, "untrusted proxy");
|
|
233
|
+
}
|
|
195
234
|
return { status: 401 };
|
|
196
235
|
}
|
|
197
236
|
if (proxy.secret !== undefined && proxy.secretHeader !== undefined) {
|
|
@@ -199,12 +238,19 @@ async function resolveProxy(request, ctx) {
|
|
|
199
238
|
const presented = headerOf(request, name);
|
|
200
239
|
delete request.headers[name];
|
|
201
240
|
if (presented === undefined || !timingSafeStringEqual(presented, proxy.secret)) {
|
|
241
|
+
if (!attemptingToken) {
|
|
242
|
+
warnProxyRejected(request, `missing or wrong shared secret (${proxy.secretHeader} header)`);
|
|
243
|
+
}
|
|
202
244
|
return { status: 401 };
|
|
203
245
|
}
|
|
204
246
|
}
|
|
205
247
|
const user = headerOf(request, proxy.userHeader);
|
|
206
|
-
if (user === undefined)
|
|
248
|
+
if (user === undefined) {
|
|
249
|
+
if (!attemptingToken) {
|
|
250
|
+
warnProxyRejected(request, `missing identity header (${proxy.userHeader})`);
|
|
251
|
+
}
|
|
207
252
|
return { status: 401 };
|
|
253
|
+
}
|
|
208
254
|
const groups = headerOf(request, proxy.groupsHeader)
|
|
209
255
|
?.split(",")
|
|
210
256
|
.map((group) => group.trim())
|
|
@@ -212,12 +258,15 @@ async function resolveProxy(request, ctx) {
|
|
|
212
258
|
const { users } = ctx.opened.identity;
|
|
213
259
|
// The invalid-username case (a proxy header that can't be a directory
|
|
214
260
|
// user) is a loud 403, not a 500: the proxy vouched for an identity
|
|
215
|
-
// this server can never represent.
|
|
261
|
+
// this server can never represent. The warn's reason never carries the
|
|
262
|
+
// claimed value (spec §3: no identity-header values).
|
|
216
263
|
if (!/^[a-z0-9][a-z0-9._-]*$/.test(user)) {
|
|
264
|
+
warnProxyRejected(request, "unusable identity claim");
|
|
217
265
|
return { status: 403, error: "invalid_username" };
|
|
218
266
|
}
|
|
219
267
|
const existing = users.get(user);
|
|
220
268
|
if (existing === undefined && !proxy.autoProvision) {
|
|
269
|
+
warnProxyRejected(request, "untrusted identity source");
|
|
221
270
|
return { status: 403, error: "user_not_provisioned" };
|
|
222
271
|
}
|
|
223
272
|
let record;
|
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
* only).
|
|
9
9
|
*
|
|
10
10
|
* Layout: each entry is one file `<sha256(u)>.<variant>` beside ONE
|
|
11
|
-
* `index.json` (the metadata table: type, size, added-at,
|
|
12
|
-
* pin). The index is written atomically on every mutation, so a re-open
|
|
11
|
+
* `index.json` (the metadata table: type, size, added-at, clamped
|
|
12
|
+
* max-age, last-hit, pin). The index is written atomically on every mutation, so a re-open
|
|
13
13
|
* (the same cache dir, a new process) serves the prior entries.
|
|
14
14
|
*
|
|
15
15
|
* Eviction never deletes a file currently being STREAMED or WRITTEN
|
|
@@ -74,7 +74,8 @@ export class ImageCache {
|
|
|
74
74
|
return;
|
|
75
75
|
}
|
|
76
76
|
for (const [key, entry] of Object.entries(parsed.entries)) {
|
|
77
|
-
|
|
77
|
+
// Pre-max-age indexes used the original default: one hour.
|
|
78
|
+
this.entries.set(key, { ...entry, maxAge: entry.maxAge ?? 3600, pins: 0 });
|
|
78
79
|
}
|
|
79
80
|
}
|
|
80
81
|
/** One hit, or `undefined` on a miss. `now` stamps the LRU order. */
|
|
@@ -99,12 +100,17 @@ export class ImageCache {
|
|
|
99
100
|
entry.pins += 1;
|
|
100
101
|
await this.persist();
|
|
101
102
|
const stream = createReadStream(file);
|
|
103
|
+
let unpinned = false;
|
|
102
104
|
const unpin = () => {
|
|
105
|
+
// Node emits close after error: one hit releases one pin.
|
|
106
|
+
if (unpinned)
|
|
107
|
+
return;
|
|
108
|
+
unpinned = true;
|
|
103
109
|
entry.pins -= 1;
|
|
104
110
|
};
|
|
105
111
|
stream.once("close", unpin);
|
|
106
112
|
stream.once("error", unpin);
|
|
107
|
-
return { type: entry.type, stream };
|
|
113
|
+
return { type: entry.type, addedAt: entry.addedAt, maxAge: entry.maxAge, stream };
|
|
108
114
|
}
|
|
109
115
|
/**
|
|
110
116
|
* Store one fetched image. The write is staged and renamed (atomic:
|
|
@@ -112,7 +118,7 @@ export class ImageCache {
|
|
|
112
118
|
* is pinned for the write's duration — the eviction pass a same-key
|
|
113
119
|
* overwrite triggers can never remove the file mid-rename.
|
|
114
120
|
*/
|
|
115
|
-
async put(u, variant, type, body, now) {
|
|
121
|
+
async put(u, variant, type, body, now, maxAge = 3600) {
|
|
116
122
|
await this.open();
|
|
117
123
|
if (this.closed)
|
|
118
124
|
return;
|
|
@@ -122,6 +128,7 @@ export class ImageCache {
|
|
|
122
128
|
type,
|
|
123
129
|
size: body.byteLength,
|
|
124
130
|
addedAt: at,
|
|
131
|
+
maxAge,
|
|
125
132
|
lastHit: at,
|
|
126
133
|
pins: 1,
|
|
127
134
|
};
|
|
@@ -180,6 +187,7 @@ export class ImageCache {
|
|
|
180
187
|
type: entry.type,
|
|
181
188
|
size: entry.size,
|
|
182
189
|
addedAt: entry.addedAt,
|
|
190
|
+
maxAge: entry.maxAge,
|
|
183
191
|
lastHit: entry.lastHit,
|
|
184
192
|
},
|
|
185
193
|
])),
|
package/dist/server/index.js
CHANGED
|
@@ -93,8 +93,9 @@ import { resolveWebuiBundle, serveWebuiBundle, } from "./static.js";
|
|
|
93
93
|
* then the configured methods — the resolved principal stamps
|
|
94
94
|
* `SessionMeta.owner` at create/fork/import.
|
|
95
95
|
* @param options.listenHost the host the server will bind (fallback
|
|
96
|
-
* `"0.0.0.0"` when omitted; `serve()` passes
|
|
97
|
-
*
|
|
96
|
+
* `"0.0.0.0"` when omitted; `serve()` passes its ONE resolved host —
|
|
97
|
+
* `options.host` → `SIGRUN_LISTEN_HOST` → the dual-stack `::`, serving
|
|
98
|
+
* spec §1); the pipeline's Host check and the default method list
|
|
98
99
|
* (`effectiveMethods`) both derive from it.
|
|
99
100
|
* @param contributedBundles the extension-contributed webui-bundle registry
|
|
100
101
|
* (build-and-webui Task 6 threads it here from boot; the serve-time
|
|
@@ -299,6 +300,13 @@ export async function createServer(opened, config, contributedBundles = [], opti
|
|
|
299
300
|
commandCatalog,
|
|
300
301
|
listenHost,
|
|
301
302
|
methods,
|
|
303
|
+
// The listener record (release-readiness Task 1, serving spec §2):
|
|
304
|
+
// host + port are the server construction's inputs; `families`
|
|
305
|
+
// starts EMPTY (never listened) and `serve()` fills it in place
|
|
306
|
+
// with the measured families after `app.listen` resolves — the
|
|
307
|
+
// meta route reads whichever state exists at request time, never
|
|
308
|
+
// probing itself. A hand-built test server serves intent only.
|
|
309
|
+
listener: { host: listenHost, port: config.port, families: [] },
|
|
302
310
|
env,
|
|
303
311
|
// U4 §13.2: the harness's settings-changed fan-out (hub publish +
|
|
304
312
|
// extension onSettingsChanged + the scheduler re-arm).
|
|
@@ -78,6 +78,10 @@ export function startNotifications(ctx) {
|
|
|
78
78
|
title: base.title,
|
|
79
79
|
body: base.body,
|
|
80
80
|
...(url === undefined ? {} : { url }),
|
|
81
|
+
// The settled outcome rides ONLY run.settled events — the
|
|
82
|
+
// other variants never gain the field (the type is the union;
|
|
83
|
+
// the conditional spread keeps the wire shape exact).
|
|
84
|
+
...(base.outcome === undefined ? {} : { outcome: base.outcome }),
|
|
81
85
|
};
|
|
82
86
|
for (const target of targets) {
|
|
83
87
|
// Fire-and-forget per transport call: one notifier's failure or
|
|
@@ -123,7 +127,7 @@ export function startNotifications(ctx) {
|
|
|
123
127
|
}, recipients);
|
|
124
128
|
};
|
|
125
129
|
/** run.settled → the owner, gated by the live `notifications.runSettled` mode. */
|
|
126
|
-
const handleRunSettled = async (conversationId) => {
|
|
130
|
+
const handleRunSettled = async (conversationId, outcome, reason) => {
|
|
127
131
|
const mode = opened.settings.read().notifications?.runSettled ?? "unwatched";
|
|
128
132
|
if (mode === "never")
|
|
129
133
|
return;
|
|
@@ -134,13 +138,26 @@ export function startNotifications(ctx) {
|
|
|
134
138
|
(watchers.get(conversationId) ?? 0) > 0) {
|
|
135
139
|
return;
|
|
136
140
|
}
|
|
141
|
+
// Translate the EXISTING wire outcome/reason straight across (no
|
|
142
|
+
// second answer read, no inferred status). An unanswered run never
|
|
143
|
+
// titles itself "Reply ready": the reason is the run's own, with
|
|
144
|
+
// the bare status as the fallback for a reason-less settle.
|
|
145
|
+
const settledOutcome = outcome === "answered"
|
|
146
|
+
? { status: "answered" }
|
|
147
|
+
: { status: "unanswered", reason: reason ?? "unanswered" };
|
|
148
|
+
const settledReason = reason ?? "unanswered";
|
|
137
149
|
deliver({
|
|
138
150
|
type: "run.settled",
|
|
139
151
|
conversationId,
|
|
140
152
|
jobId: undefined,
|
|
141
153
|
callId: undefined,
|
|
142
|
-
title:
|
|
143
|
-
|
|
154
|
+
title: outcome === "answered"
|
|
155
|
+
? "Reply ready"
|
|
156
|
+
: "Run ended without a reply",
|
|
157
|
+
body: outcome === "answered"
|
|
158
|
+
? displayName(meta, conversationId)
|
|
159
|
+
: `${displayName(meta, conversationId)} — ${settledReason}`,
|
|
160
|
+
outcome: settledOutcome,
|
|
144
161
|
}, [meta?.owner]);
|
|
145
162
|
};
|
|
146
163
|
/**
|
|
@@ -177,7 +194,7 @@ export function startNotifications(ctx) {
|
|
|
177
194
|
void bus.runBounded("notifications interaction.pending", () => handleInteraction(event.conversationId, event.callId));
|
|
178
195
|
}
|
|
179
196
|
else if (event.type === "run.settled") {
|
|
180
|
-
void bus.runBounded("notifications run.settled", () => handleRunSettled(event.conversationId));
|
|
197
|
+
void bus.runBounded("notifications run.settled", () => handleRunSettled(event.conversationId, event.outcome, event.reason));
|
|
181
198
|
}
|
|
182
199
|
else if (event.type === "job.changed") {
|
|
183
200
|
void bus.runBounded("notifications job.changed", () => {
|
|
@@ -17,9 +17,10 @@
|
|
|
17
17
|
* POSTed as the raw text body; the route's `bodyLimit` is the §11.1
|
|
18
18
|
* 192 KiB rule (an over-limit body is Fastify's 413 `too_large`).
|
|
19
19
|
*
|
|
20
|
-
* Fetches go through the on-disk LRU cache
|
|
21
|
-
*
|
|
22
|
-
*
|
|
20
|
+
* Fetches go through the on-disk LRU cache: a hit fresher than its
|
|
21
|
+
* clamped max-age re-serves without an upstream call; an older hit
|
|
22
|
+
* revalidates, and a refusal is never replaced by stale bytes.
|
|
23
|
+
* Concurrent requests for one URL share ONE in-flight upstream fetch
|
|
23
24
|
* (Review Focus 4). Failures are never cached. Responses carry the
|
|
24
25
|
* sniffed Content-Type, `X-Content-Type-Options: nosniff`, and
|
|
25
26
|
* `Cache-Control: private, max-age=<upstream hint clamped to 1 h–7 d>`.
|
|
@@ -114,13 +115,17 @@ export function registerImageRoutes(app, ctx) {
|
|
|
114
115
|
const inflight = new Map();
|
|
115
116
|
/** Fetch one URL through the cache, sharing in-flight work. */
|
|
116
117
|
async function fetchThroughCache(u, variant) {
|
|
117
|
-
const
|
|
118
|
-
|
|
118
|
+
const now = deps.now();
|
|
119
|
+
const hit = await cache.get(u, variant, now);
|
|
120
|
+
if (hit !== undefined && now - hit.addedAt < hit.maxAge * 1000) {
|
|
119
121
|
const chunks = [];
|
|
120
122
|
for await (const chunk of hit.stream)
|
|
121
123
|
chunks.push(chunk);
|
|
122
|
-
return { bytes: Buffer.concat(chunks), type: hit.type };
|
|
124
|
+
return { bytes: Buffer.concat(chunks), type: hit.type, maxAge: hit.maxAge };
|
|
123
125
|
}
|
|
126
|
+
// A stale hit is never served: close its pin and revalidate.
|
|
127
|
+
if (hit !== undefined)
|
|
128
|
+
hit.stream.destroy();
|
|
124
129
|
const inflightKey = `${variant}${u}`;
|
|
125
130
|
const existing = inflight.get(inflightKey);
|
|
126
131
|
if (existing !== undefined)
|
|
@@ -128,11 +133,13 @@ export function registerImageRoutes(app, ctx) {
|
|
|
128
133
|
const work = (async () => {
|
|
129
134
|
const fetched = await fetchImage(u, deps);
|
|
130
135
|
// Cache the original variant first — `as=png` requests cache
|
|
131
|
-
// BOTH variants (the conversion reads the fetched bytes).
|
|
132
|
-
|
|
136
|
+
// BOTH variants (the conversion reads the fetched bytes). The
|
|
137
|
+
// clamped hint is BOTH the response header and the cache TTL.
|
|
138
|
+
const maxAge = clampedMaxAge(fetched.maxAge);
|
|
139
|
+
await cache.put(u, "orig", fetched.type, fetched.bytes, deps.now(), maxAge);
|
|
133
140
|
if (variant === "png") {
|
|
134
141
|
const converted = await toPng(fetched.bytes);
|
|
135
|
-
await cache.put(u, "png", "png", converted);
|
|
142
|
+
await cache.put(u, "png", "png", converted, deps.now(), maxAge);
|
|
136
143
|
return {
|
|
137
144
|
bytes: converted,
|
|
138
145
|
type: "png",
|
package/dist/server/routes/me.js
CHANGED
|
@@ -52,7 +52,7 @@ export function registerMeRoutes(app, ctx) {
|
|
|
52
52
|
const settings = body;
|
|
53
53
|
await stores.settings.set(settings);
|
|
54
54
|
const { slug } = request.params;
|
|
55
|
-
|
|
55
|
+
ctx.publishSettingsChanged({ extension: slug }, Object.keys(settings), request.principal.user);
|
|
56
56
|
const response = { settings, pendingRestart: [] };
|
|
57
57
|
return response;
|
|
58
58
|
});
|
|
@@ -39,6 +39,10 @@ export function registerMetaRoutes(app, ctx) {
|
|
|
39
39
|
version: SIGRUN_VERSION,
|
|
40
40
|
harnessVersion: HARNESS_VERSION,
|
|
41
41
|
schemaVersion: SCHEMA_VERSION,
|
|
42
|
+
// The retained listener facts (release-readiness Task 1, serving
|
|
43
|
+
// spec §2): measured once at listen, projected verbatim — this
|
|
44
|
+
// route never probes (a pure synchronous projection).
|
|
45
|
+
listener: ctx.listener,
|
|
42
46
|
extensions: opened.extensionOutcomes.map((outcome) => {
|
|
43
47
|
const workers = workerStatuses.get(outcome.name);
|
|
44
48
|
const surfaces = opened.extensionSurfaces.find((binding) => binding.extension === outcome.name);
|
|
@@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.2.1] - 2026-10-28
|
|
11
|
+
### Added
|
|
12
|
+
- Additive listener metadata on the meta response: host, port, and
|
|
13
|
+
measured address families.
|
|
14
|
+
|
|
10
15
|
## [0.2.0] - 2026-10-27
|
|
11
16
|
### Added
|
|
12
17
|
- Surface and block wire types extended for the U5/U6/U7 workstreams:
|
|
@@ -403,11 +403,38 @@ export type WorkerStatus = {
|
|
|
403
403
|
readonly lastError?: string;
|
|
404
404
|
};
|
|
405
405
|
/** The `GET /v1/meta` payload (spec §5; the health block is Task 9e). */
|
|
406
|
+
/**
|
|
407
|
+
* The measured listener facts (release-readiness Task 1, serving spec §2):
|
|
408
|
+
* captured ONCE when the listener goes live (never probed per request) and
|
|
409
|
+
* retained on the server context; `GET /v1/meta` projects them verbatim.
|
|
410
|
+
*/
|
|
411
|
+
export interface ListenerInfo {
|
|
412
|
+
/** The effective host string supplied to `listen` — never a resolved peer. */
|
|
413
|
+
readonly host: string;
|
|
414
|
+
/** The configured/selected port the listener bound. */
|
|
415
|
+
readonly port: number;
|
|
416
|
+
/**
|
|
417
|
+
* The address families the listener accepts, in stable `ipv4`, `ipv6`
|
|
418
|
+
* order. Literal binds report the bound socket's family; the `::`
|
|
419
|
+
* wildcard reports exactly the loopback families a startup reachability
|
|
420
|
+
* check connected on (measured, never inferred from the wildcard
|
|
421
|
+
* spelling). Empty on a server that never listened (a hand-built test
|
|
422
|
+
* server projecting intent only).
|
|
423
|
+
*/
|
|
424
|
+
readonly families: readonly ("ipv4" | "ipv6")[];
|
|
425
|
+
}
|
|
406
426
|
export interface MetaResponse {
|
|
407
427
|
/** The sigrun server's own version (the package manifest). */
|
|
408
428
|
readonly version: string;
|
|
409
429
|
readonly harnessVersion: string;
|
|
410
430
|
readonly schemaVersion: string;
|
|
431
|
+
/**
|
|
432
|
+
* The listener facts (release-readiness Task 1, serving spec §2). A
|
|
433
|
+
* REQUIRED field on this type; OLDER SERVERS (≤0.2.0) omit it on the
|
|
434
|
+
* wire, so clients consuming it must tolerate its absence during
|
|
435
|
+
* mixed-version upgrades.
|
|
436
|
+
*/
|
|
437
|
+
readonly listener: ListenerInfo;
|
|
411
438
|
readonly extensions: readonly {
|
|
412
439
|
readonly name: string;
|
|
413
440
|
readonly status: string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sigrun/sigrun",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "Durable personal assistant on pi-durable",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"test": "vitest run",
|
|
31
31
|
"typecheck": "tsc --noEmit",
|
|
32
32
|
"build:shared": "npm --workspace @sigrun/shared run build",
|
|
33
|
-
"build": "npm run build:shared && tsc -p tsconfig.build.json && tsc -p tsconfig.extension.json && node -e \"require('node:fs').rmSync('dist/extension.js',{force:true})\"",
|
|
33
|
+
"build": "npm run build:shared && tsc -p tsconfig.build.json && tsc -p tsconfig.extension.json && node scripts/stamp-block-build.mjs && node -e \"require('node:fs').rmSync('dist/extension.js',{force:true})\"",
|
|
34
34
|
"build:webui": "npm --workspace @sigrun/webui run build",
|
|
35
35
|
"typecheck:webui": "npm --workspace @sigrun/webui run typecheck",
|
|
36
36
|
"test:e2e": "npm run build:webui && npm --workspace @sigrun/webui run test:e2e",
|
|
@@ -45,11 +45,11 @@
|
|
|
45
45
|
"@earendil-works/pi-tui": "^1.3.0",
|
|
46
46
|
"@fastify/static": "^10.1.7",
|
|
47
47
|
"@sigrun/ext-codemode": "0.1.1",
|
|
48
|
-
"@sigrun/ext-gateway": "0.1.
|
|
48
|
+
"@sigrun/ext-gateway": "0.1.2",
|
|
49
49
|
"@sigrun/ext-mcp": "0.2.1",
|
|
50
50
|
"@sigrun/ext-tool-search": "0.1.1",
|
|
51
51
|
"@sigrun/oauth": "0.1.1",
|
|
52
|
-
"@sigrun/shared": "0.2.
|
|
52
|
+
"@sigrun/shared": "0.2.1",
|
|
53
53
|
"croner": "^10.0.1",
|
|
54
54
|
"fastify": "^5.12.7",
|
|
55
55
|
"sharp": "^0.35.5",
|
|
@@ -59,9 +59,9 @@
|
|
|
59
59
|
"@sigrun/shared"
|
|
60
60
|
],
|
|
61
61
|
"optionalDependencies": {
|
|
62
|
-
"@sigrun/sandbox-linux-arm64": "0.2.
|
|
63
|
-
"@sigrun/sandbox-linux-x64": "0.2.
|
|
64
|
-
"@sigrun/tui": "0.2.
|
|
62
|
+
"@sigrun/sandbox-linux-arm64": "0.2.1",
|
|
63
|
+
"@sigrun/sandbox-linux-x64": "0.2.1",
|
|
64
|
+
"@sigrun/tui": "0.2.1"
|
|
65
65
|
},
|
|
66
66
|
"devDependencies": {
|
|
67
67
|
"@types/node": "^26.6.4",
|
|
@@ -90,8 +90,7 @@
|
|
|
90
90
|
"THIRD_PARTY_NOTICES.md",
|
|
91
91
|
"README.md",
|
|
92
92
|
"LICENSE",
|
|
93
|
-
"CHANGELOG.md"
|
|
94
|
-
"CONTEXT.md"
|
|
93
|
+
"CHANGELOG.md"
|
|
95
94
|
],
|
|
96
95
|
"allowScripts": {
|
|
97
96
|
"@google/genai@2.21.0": true,
|