@sigrun/sigrun 0.1.6 → 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 +192 -0
- package/README.md +445 -42
- 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/schema.js +9 -2
- package/dist/blocks/validate.d.ts +73 -0
- package/dist/blocks/validate.js +57 -8
- package/dist/cli.js +26 -2
- package/dist/config.js +67 -1
- package/dist/extension.d.ts +230 -22
- package/dist/harness/ambient.js +39 -185
- package/dist/harness/blocks/entries.js +47 -0
- package/dist/harness/blocks/state.js +58 -0
- package/dist/harness/blocks/tools.js +274 -0
- package/dist/harness/boot.js +259 -20
- package/dist/harness/builtins.js +15 -11
- package/dist/harness/event-bus.js +105 -10
- package/dist/harness/identity/accounts.js +186 -0
- package/dist/harness/identity/groups.js +14 -0
- package/dist/harness/identity/index.js +13 -6
- package/dist/harness/identity/json-store.js +15 -4
- package/dist/harness/identity/setup-code-check.js +9 -0
- package/dist/harness/identity/tokens.js +28 -5
- package/dist/harness/identity/users.js +111 -12
- package/dist/harness/interactions.js +292 -136
- package/dist/harness/loader.js +271 -17
- package/dist/harness/packages.js +1 -1
- package/dist/harness/reserved-sections.js +21 -0
- package/dist/harness/seam-host.js +92 -6
- package/dist/harness/sections/context-files.js +1 -1
- package/dist/harness/settings.js +63 -2
- package/dist/harness/stores.js +241 -4
- package/dist/harness/user-services.js +391 -0
- package/dist/harness/user-tools.js +187 -0
- package/dist/harness/workspace.js +61 -57
- package/dist/jobs/fire.js +10 -16
- package/dist/sandbox/availability.js +211 -0
- package/dist/sandbox/command-observer.js +59 -0
- package/dist/sandbox/grants.js +159 -0
- package/dist/sandbox/helper.js +29 -0
- package/dist/sandbox/isolation.js +29 -0
- package/dist/sandbox/launch.js +31 -0
- package/dist/sandbox/os-account.js +49 -0
- package/dist/sandbox/policy.js +274 -0
- package/dist/sandbox/pool.js +548 -0
- package/dist/sandbox/protocol.js +167 -0
- package/dist/sandbox/sandboxed-env.js +245 -0
- package/dist/sandbox/unavailable-env.js +49 -0
- package/dist/sandbox/worker.js +261 -0
- package/dist/serve.js +213 -25
- package/dist/server/auth/access-principal.js +16 -0
- package/dist/server/auth/pipeline.js +99 -36
- package/dist/server/blocks/answers.js +94 -0
- package/dist/server/blocks/chat-surfaces.js +393 -0
- package/dist/server/blocks/notes.js +216 -0
- package/dist/server/blocks/summaries.js +328 -0
- package/dist/server/commands.js +5 -0
- package/dist/server/content-parts-lazy.js +13 -0
- package/dist/server/conversation-handler.js +2 -5
- package/dist/server/conversation-ops.js +138 -37
- package/dist/server/helpers.js +18 -6
- package/dist/server/host-bridge.js +40 -20
- package/dist/server/images/cache.js +213 -0
- package/dist/server/images/convert.js +13 -0
- package/dist/server/images/fetch.js +302 -0
- package/dist/server/images/ranges.js +42 -0
- package/dist/server/images/rewrite.js +94 -0
- package/dist/server/images/sign.js +84 -0
- package/dist/server/images/sniff.js +62 -0
- package/dist/server/index.js +152 -4
- package/dist/server/login/flows.js +126 -0
- package/dist/server/login/http.js +31 -0
- package/dist/server/login/resolve.js +133 -0
- package/dist/server/login/setup-claim.js +55 -0
- package/dist/server/login/usernames.js +15 -0
- package/dist/server/notifications.js +21 -4
- package/dist/server/ownership.js +13 -0
- package/dist/server/routes/admin.js +58 -0
- package/dist/server/routes/auth.js +88 -2
- package/dist/server/routes/conversations.js +3 -6
- package/dist/server/routes/events.js +28 -3
- package/dist/server/routes/extensions.js +37 -5
- package/dist/server/routes/images.js +274 -0
- package/dist/server/routes/interactions.js +57 -68
- package/dist/server/routes/jobs.js +2 -8
- package/dist/server/routes/login.js +223 -0
- package/dist/server/routes/me.js +90 -0
- package/dist/server/routes/meta.js +19 -0
- package/dist/server/routes/onboarding.js +94 -84
- package/dist/server/routes/settings-sections.js +190 -0
- package/dist/server/routes/settings.js +16 -4
- package/dist/server/routes/submissions.js +1 -1
- package/dist/server/routes/surfaces.js +175 -23
- package/dist/server/routes/users.js +116 -20
- package/dist/server/schema-fixture.js +213 -0
- package/dist/server/session-meta.js +3 -0
- package/dist/server/shutdown.js +11 -4
- package/dist/server/static.js +18 -0
- package/dist/server/surfaces/dedup.js +9 -2
- package/dist/server/surfaces/service.js +181 -13
- package/dist/server/surfaces/settings-surfaces.js +597 -0
- package/dist/server/turns/submit-turn.js +26 -0
- package/dist/server/views.js +16 -0
- package/dist/settings/core-sections.js +532 -0
- package/dist/settings/pending.js +181 -0
- package/dist/settings/registry.js +453 -0
- package/dist/skills/index.js +6 -0
- package/node_modules/@sigrun/shared/CHANGELOG.md +11 -0
- package/node_modules/@sigrun/shared/dist/access.d.ts +4 -2
- package/node_modules/@sigrun/shared/dist/access.js +11 -4
- package/node_modules/@sigrun/shared/dist/ambient.d.ts +17 -74
- package/node_modules/@sigrun/shared/dist/ambient.js +8 -6
- package/node_modules/@sigrun/shared/dist/blocks/text.js +16 -2
- package/node_modules/@sigrun/shared/dist/blocks/transport.d.ts +4 -0
- package/node_modules/@sigrun/shared/dist/blocks/types.d.ts +21 -4
- package/node_modules/@sigrun/shared/dist/interactions.d.ts +22 -48
- package/node_modules/@sigrun/shared/dist/interactions.js +9 -7
- package/node_modules/@sigrun/shared/dist/settings.d.ts +27 -2
- package/node_modules/@sigrun/shared/dist/themes.d.ts +2 -3
- package/node_modules/@sigrun/shared/dist/themes.js +2 -3
- package/node_modules/@sigrun/shared/dist/turns.d.ts +14 -0
- package/node_modules/@sigrun/shared/dist/turns.js +53 -3
- package/node_modules/@sigrun/shared/dist/views.d.ts +202 -12
- package/node_modules/@sigrun/shared/dist/views.js +16 -0
- package/node_modules/@sigrun/shared/package.json +1 -1
- package/package.json +13 -9
- package/webui/dist/assets/index-SO2W4jjk.js +55 -0
- package/webui/dist/assets/index-yqkL6BSS.css +1 -0
- package/webui/dist/index.html +2 -2
- package/CONTEXT.md +0 -163
- package/webui/dist/assets/index-CiYInlLx.css +0 -1
- package/webui/dist/assets/index-JDfOQV9z.js +0 -52
|
@@ -34,14 +34,22 @@
|
|
|
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
|
*/
|
|
42
48
|
import { timingSafeEqual } from "node:crypto";
|
|
43
49
|
import { ipAllowed } from "./ip.js";
|
|
44
50
|
import { isLoopbackHost } from "./loopback.js";
|
|
51
|
+
import { AdmissionRefusedError } from "../../harness/identity/users.js";
|
|
52
|
+
import { effectiveAdmin } from "../../harness/identity/groups.js";
|
|
45
53
|
/** One header read: Fastify headers may be string | string[]; first wins. */
|
|
46
54
|
function headerOf(request, name) {
|
|
47
55
|
const value = request.headers[name.toLowerCase()];
|
|
@@ -57,7 +65,7 @@ function timingSafeStringEqual(a, b) {
|
|
|
57
65
|
return timingSafeEqual(left, right);
|
|
58
66
|
}
|
|
59
67
|
/** The request's Bearer credential, if the Authorization header carries one. */
|
|
60
|
-
function bearerOf(request) {
|
|
68
|
+
export function bearerOf(request) {
|
|
61
69
|
const value = headerOf(request, "authorization");
|
|
62
70
|
if (value === undefined)
|
|
63
71
|
return undefined;
|
|
@@ -65,7 +73,7 @@ function bearerOf(request) {
|
|
|
65
73
|
return match?.[1]?.trim() || undefined;
|
|
66
74
|
}
|
|
67
75
|
/** The `sigrun_session` cookie value (naive parse; the cookie is ours). */
|
|
68
|
-
function sessionCookieOf(request) {
|
|
76
|
+
export function sessionCookieOf(request) {
|
|
69
77
|
const raw = headerOf(request, "cookie");
|
|
70
78
|
if (raw === undefined)
|
|
71
79
|
return undefined;
|
|
@@ -160,6 +168,15 @@ function isPublicPath(request, ctx) {
|
|
|
160
168
|
return true;
|
|
161
169
|
if (pattern === "/v1/auth/logout" && request.method === "POST")
|
|
162
170
|
return true;
|
|
171
|
+
if (pattern === "/v1/auth/providers" && request.method === "GET")
|
|
172
|
+
return true;
|
|
173
|
+
if (pattern === "/v1/auth/login/:provider" && request.method === "GET")
|
|
174
|
+
return true;
|
|
175
|
+
if (pattern === "/v1/auth/login/code" && request.method === "POST")
|
|
176
|
+
return true;
|
|
177
|
+
if (pattern === "/v1/auth/callback/:provider" &&
|
|
178
|
+
(request.method === "GET" || request.method === "POST"))
|
|
179
|
+
return true;
|
|
163
180
|
if (pattern === "/v1/onboarding" || pattern?.startsWith("/v1/onboarding/")) {
|
|
164
181
|
return true;
|
|
165
182
|
}
|
|
@@ -173,6 +190,22 @@ function isPublicPath(request, ctx) {
|
|
|
173
190
|
}
|
|
174
191
|
return false;
|
|
175
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
|
+
}
|
|
176
209
|
/**
|
|
177
210
|
* Stage 5a (proxy): resolve the forward-auth headers, or reply with the
|
|
178
211
|
* rejection. The secret header is DELETED from the request before any
|
|
@@ -180,7 +213,24 @@ function isPublicPath(request, ctx) {
|
|
|
180
213
|
*/
|
|
181
214
|
async function resolveProxy(request, ctx) {
|
|
182
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");
|
|
183
230
|
if (!ipAllowed(request.socket.remoteAddress, proxy.trustedProxies)) {
|
|
231
|
+
if (!attemptingToken) {
|
|
232
|
+
warnProxyRejected(request, "untrusted proxy");
|
|
233
|
+
}
|
|
184
234
|
return { status: 401 };
|
|
185
235
|
}
|
|
186
236
|
if (proxy.secret !== undefined && proxy.secretHeader !== undefined) {
|
|
@@ -188,12 +238,19 @@ async function resolveProxy(request, ctx) {
|
|
|
188
238
|
const presented = headerOf(request, name);
|
|
189
239
|
delete request.headers[name];
|
|
190
240
|
if (presented === undefined || !timingSafeStringEqual(presented, proxy.secret)) {
|
|
241
|
+
if (!attemptingToken) {
|
|
242
|
+
warnProxyRejected(request, `missing or wrong shared secret (${proxy.secretHeader} header)`);
|
|
243
|
+
}
|
|
191
244
|
return { status: 401 };
|
|
192
245
|
}
|
|
193
246
|
}
|
|
194
247
|
const user = headerOf(request, proxy.userHeader);
|
|
195
|
-
if (user === undefined)
|
|
248
|
+
if (user === undefined) {
|
|
249
|
+
if (!attemptingToken) {
|
|
250
|
+
warnProxyRejected(request, `missing identity header (${proxy.userHeader})`);
|
|
251
|
+
}
|
|
196
252
|
return { status: 401 };
|
|
253
|
+
}
|
|
197
254
|
const groups = headerOf(request, proxy.groupsHeader)
|
|
198
255
|
?.split(",")
|
|
199
256
|
.map((group) => group.trim())
|
|
@@ -201,53 +258,54 @@ async function resolveProxy(request, ctx) {
|
|
|
201
258
|
const { users } = ctx.opened.identity;
|
|
202
259
|
// The invalid-username case (a proxy header that can't be a directory
|
|
203
260
|
// user) is a loud 403, not a 500: the proxy vouched for an identity
|
|
204
|
-
// 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).
|
|
205
263
|
if (!/^[a-z0-9][a-z0-9._-]*$/.test(user)) {
|
|
264
|
+
warnProxyRejected(request, "unusable identity claim");
|
|
206
265
|
return { status: 403, error: "invalid_username" };
|
|
207
266
|
}
|
|
208
267
|
const existing = users.get(user);
|
|
209
268
|
if (existing === undefined && !proxy.autoProvision) {
|
|
269
|
+
warnProxyRejected(request, "untrusted identity source");
|
|
210
270
|
return { status: 403, error: "user_not_provisioned" };
|
|
211
271
|
}
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
// untouched (a group-exit clears it at this user's next request under
|
|
233
|
-
// a non-empty list, never under an empty one). Absent counts as false;
|
|
234
|
-
// the auto-provisioned record was added with the flag already set, so
|
|
235
|
-
// the comparison is against the RESOLVED record, never `existing`.
|
|
236
|
-
if (adminGroups.length > 0 &&
|
|
237
|
-
(record.groupAdmin ?? false) !== groupAdmin) {
|
|
238
|
-
await users.update(user, { groupAdmin });
|
|
272
|
+
let record;
|
|
273
|
+
try {
|
|
274
|
+
record = existing ??
|
|
275
|
+
(await users.add({
|
|
276
|
+
user,
|
|
277
|
+
...(headerOf(request, proxy.nameHeader) === undefined
|
|
278
|
+
? {}
|
|
279
|
+
: { name: headerOf(request, proxy.nameHeader) }),
|
|
280
|
+
...(headerOf(request, proxy.emailHeader) === undefined
|
|
281
|
+
? {}
|
|
282
|
+
: { email: headerOf(request, proxy.emailHeader) }),
|
|
283
|
+
admin: false,
|
|
284
|
+
source: "proxy",
|
|
285
|
+
}));
|
|
286
|
+
}
|
|
287
|
+
catch (error) {
|
|
288
|
+
if (error instanceof AdmissionRefusedError) {
|
|
289
|
+
return { status: 403, error: error.code };
|
|
290
|
+
}
|
|
291
|
+
throw error;
|
|
239
292
|
}
|
|
293
|
+
// Persist the authenticated source's groups on every proxy request;
|
|
294
|
+
// setGroups avoids the write when the values are unchanged. Effective
|
|
295
|
+
// admin is computed over the UPDATED record, so a group exit revokes
|
|
296
|
+
// the request's grant immediately and host-side checks agree.
|
|
297
|
+
record = await users.setGroups(user, "proxy", groups);
|
|
240
298
|
return {
|
|
241
299
|
user: record.user,
|
|
242
300
|
...(record.name === undefined ? {} : { name: record.name }),
|
|
243
301
|
...(record.email === undefined ? {} : { email: record.email }),
|
|
244
|
-
admin: record
|
|
302
|
+
admin: effectiveAdmin(record, adminGroups),
|
|
245
303
|
service: false,
|
|
246
304
|
method: "proxy",
|
|
247
305
|
};
|
|
248
306
|
}
|
|
249
307
|
/** Stage 5b (token): verify a presented credential against the token store. */
|
|
250
|
-
function resolveToken(request, ctx) {
|
|
308
|
+
export function resolveToken(request, ctx) {
|
|
251
309
|
const presented = bearerOf(request) ?? sessionCookieOf(request);
|
|
252
310
|
if (presented === undefined)
|
|
253
311
|
return { status: 401 };
|
|
@@ -262,9 +320,14 @@ function resolveToken(request, ctx) {
|
|
|
262
320
|
user: user.user,
|
|
263
321
|
...(user.name === undefined ? {} : { name: user.name }),
|
|
264
322
|
...(user.email === undefined ? {} : { email: user.email }),
|
|
265
|
-
|
|
323
|
+
// Token/session credentials are not group sources: derive from the
|
|
324
|
+
// stored record without writing groups on this request path.
|
|
325
|
+
admin: effectiveAdmin(user, ctx.config.auth.adminGroups),
|
|
266
326
|
service: false,
|
|
267
|
-
method: "token",
|
|
327
|
+
method: record.kind === "session" ? "session" : "token",
|
|
328
|
+
...(record.kind === "session" && record.provider !== undefined
|
|
329
|
+
? { provider: record.provider }
|
|
330
|
+
: {}),
|
|
268
331
|
};
|
|
269
332
|
}
|
|
270
333
|
/** The ONE request hook. Stages 1→5 in brief order; a stage replies or falls through. */
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The block answer (UI-blocks U5 Task 3, spec §8.4): a turn action
|
|
3
|
+
* (`submit`, default `turn: true`; any action with `turn: true`) makes
|
|
4
|
+
* core submit a user message for the acting user through the normal
|
|
5
|
+
* submission path (`submitOp`):
|
|
6
|
+
*
|
|
7
|
+
* { type: "input",
|
|
8
|
+
* content: textProjection, // "Submitted 'Ranking': 1. …"
|
|
9
|
+
* entry: { kind: "sigrun.blocks.answer",
|
|
10
|
+
* data: { entryId, action, payload, state } },
|
|
11
|
+
* requestId: "blocks:" + actionId }
|
|
12
|
+
*
|
|
13
|
+
* The surface's unnoted change records fold into `content` first and
|
|
14
|
+
* `notedThrough` advances BEFORE the §8.3 note step runs — so the turn
|
|
15
|
+
* sends no separate note for this surface, while changes on OTHER
|
|
16
|
+
* surfaces still note. `requestId` makes a double click one turn.
|
|
17
|
+
*/
|
|
18
|
+
import { BACKGROUND_CONTEXT } from "@earendil-works/chord/context";
|
|
19
|
+
import { toText } from "@sigrun/shared/blocks";
|
|
20
|
+
import { submitOp } from "../conversation-ops.js";
|
|
21
|
+
import { readChatBlock } from "./chat-surfaces.js";
|
|
22
|
+
import { NOTE_HEADER } from "./notes.js";
|
|
23
|
+
import { PROJECTION_LOCALE, PROJECTION_WIDTH, } from "../../harness/blocks/tools.js";
|
|
24
|
+
import { BlocksAnswerEntry } from "../../harness/blocks/entries.js";
|
|
25
|
+
import { BlocksStateDoc } from "../../harness/blocks/state.js";
|
|
26
|
+
/**
|
|
27
|
+
* Submit one block answer: fold the surface's unnoted changes and its
|
|
28
|
+
* current projection into the user-visible content, advance the
|
|
29
|
+
* surface's `notedThrough`, then admit the input through `submitOp` with
|
|
30
|
+
* `skipNoteFor` (this surface notes no further this turn) and the
|
|
31
|
+
* answer entry. The route owns access gating and actionId dedup; this
|
|
32
|
+
* caller runs only for a located, write-accessible block whose handler
|
|
33
|
+
* starts a turn.
|
|
34
|
+
*
|
|
35
|
+
* The fold and the `notedThrough` advance run in ONE commit BEFORE the
|
|
36
|
+
* note step (§8.4: "The surface's unnoted change records are folded into
|
|
37
|
+
* `content` first, and `notedThrough` advances, before the §8.3 note
|
|
38
|
+
* step runs"), so the answer turn never double-reports its surface.
|
|
39
|
+
*/
|
|
40
|
+
export async function answerBlock(ctx, conversationId, entryId, action, payload, actionId) {
|
|
41
|
+
const { opened } = ctx;
|
|
42
|
+
const block = await readChatBlock(opened.harness, conversationId, entryId);
|
|
43
|
+
if (block === undefined) {
|
|
44
|
+
throw new Error(`answerBlock: block ${entryId} not found`);
|
|
45
|
+
}
|
|
46
|
+
const conversation = await opened.harness.conversation(conversationId, BACKGROUND_CONTEXT);
|
|
47
|
+
if (conversation === undefined) {
|
|
48
|
+
throw new Error(`answerBlock: conversation ${conversationId} not found`);
|
|
49
|
+
}
|
|
50
|
+
// The fold (one commit): read the CURRENT state, build the content
|
|
51
|
+
// (changes + projection), advance notedThrough past every record the
|
|
52
|
+
// fold covers.
|
|
53
|
+
const folded = await conversation.commit(async (tx) => {
|
|
54
|
+
const draft = await tx.doc(BlocksStateDoc, conversationId, entryId, entryId);
|
|
55
|
+
const pending = draft.changes.filter((record) => record.seq > draft.notedThrough);
|
|
56
|
+
const lines = [];
|
|
57
|
+
if (pending.length > 0) {
|
|
58
|
+
const byAuthor = new Map();
|
|
59
|
+
for (const record of pending) {
|
|
60
|
+
const list = byAuthor.get(record.by) ?? [];
|
|
61
|
+
list.push(record.summary);
|
|
62
|
+
byAuthor.set(record.by, list);
|
|
63
|
+
}
|
|
64
|
+
lines.push(NOTE_HEADER);
|
|
65
|
+
for (const [author, summaries] of byAuthor) {
|
|
66
|
+
lines.push(`• ${block.title} (${author}): ${summaries.join("; ")}`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
lines.push(`Submitted '${block.title}': ${toText(block.tree, draft.data, {
|
|
70
|
+
width: PROJECTION_WIDTH,
|
|
71
|
+
locale: PROJECTION_LOCALE,
|
|
72
|
+
})}`);
|
|
73
|
+
if (pending.length > 0) {
|
|
74
|
+
draft.notedThrough = pending[pending.length - 1].seq;
|
|
75
|
+
}
|
|
76
|
+
return {
|
|
77
|
+
content: lines.join("\n"),
|
|
78
|
+
state: JSON.parse(JSON.stringify(draft.data)),
|
|
79
|
+
};
|
|
80
|
+
}, BACKGROUND_CONTEXT);
|
|
81
|
+
await submitOp(ctx, conversation, folded.content, {
|
|
82
|
+
...(actionId === undefined ? {} : { requestId: `blocks:${actionId}` }),
|
|
83
|
+
skipNoteFor: entryId,
|
|
84
|
+
entry: {
|
|
85
|
+
kind: BlocksAnswerEntry.kind,
|
|
86
|
+
data: {
|
|
87
|
+
entryId,
|
|
88
|
+
action,
|
|
89
|
+
payload,
|
|
90
|
+
state: folded.state,
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
}
|