@sigrun/sigrun 0.1.5 → 0.2.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 +136 -0
- package/README.md +352 -36
- package/THIRD_PARTY_NOTICES.md +23 -0
- package/dist/blocks/schema.js +9 -2
- package/dist/blocks/validate.js +57 -8
- package/dist/cli.js +26 -2
- package/dist/config.js +67 -1
- package/dist/extension.d.ts +189 -15
- package/dist/harness/agent-env.js +62 -0
- 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 +253 -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 +80 -6
- 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 -55
- package/dist/jobs/fire.js +10 -16
- package/dist/sandbox/availability.js +162 -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 +20 -0
- package/dist/sandbox/os-account.js +49 -0
- package/dist/sandbox/policy.js +253 -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 +281 -0
- package/dist/serve.js +82 -7
- package/dist/server/auth/access-principal.js +16 -0
- package/dist/server/auth/pipeline.js +47 -33
- 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 +205 -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 +144 -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/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 +267 -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 +15 -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 +15 -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 +175 -12
- package/node_modules/@sigrun/shared/dist/views.js +16 -0
- package/node_modules/@sigrun/shared/package.json +1 -1
- package/package.json +13 -8
- package/webui/dist/assets/index-CxB969qp.js +55 -0
- package/webui/dist/assets/index-yqkL6BSS.css +1 -0
- package/webui/dist/index.html +2 -2
- package/webui/dist/assets/index-B4-GYqEb.js +0 -50
- package/webui/dist/assets/index-BiSMljer.css +0 -1
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The agent shell environment (multi-user spec §8, the env-leak fix).
|
|
3
|
+
*
|
|
4
|
+
* pi-durable's `NodeExecutionEnv` runs commands with `{...process.env,
|
|
5
|
+
* ...shellEnv, ...options.env}` unless a call passes `inheritEnv: false`,
|
|
6
|
+
* and its bash tool always passes `inheritEnv: true`. So without this,
|
|
7
|
+
* an agent's `env` prints sigrun's own environment: `SIGRUN_API_TOKEN`,
|
|
8
|
+
* `SIGRUN_PROXY_SECRET`, and provider keys. `shellEnv` cannot fix it
|
|
9
|
+
* (it merges over `process.env`, it never subtracts), so
|
|
10
|
+
* {@link AgentExecutionEnv} forces every `exec` onto an allowlist built
|
|
11
|
+
* from sigrun's environment, plus whatever the caller set explicitly.
|
|
12
|
+
*
|
|
13
|
+
* The allowlist is the spec's list plus two variables existing installs
|
|
14
|
+
* depend on:
|
|
15
|
+
* - `NPM_CONFIG_CACHE`: the image runs with a read-only root filesystem
|
|
16
|
+
* and points npm's cache at `/cache/npm`; without it `npm install` in
|
|
17
|
+
* an agent shell fails writing `~/.npm`.
|
|
18
|
+
* - `SSH_AUTH_SOCK`: a socket path, not a secret; on a single-user
|
|
19
|
+
* workstation it is how the agent's `git push` reaches the operator's
|
|
20
|
+
* ssh-agent. The multi-user jail (spec §9) must not pass it to other
|
|
21
|
+
* users' workers.
|
|
22
|
+
*/
|
|
23
|
+
import { NodeExecutionEnv } from "@earendil-works/pi-durable/env/node";
|
|
24
|
+
/** Variables copied from sigrun's environment into every agent shell. */
|
|
25
|
+
export const AGENT_ENV_KEYS = [
|
|
26
|
+
"PATH",
|
|
27
|
+
"HOME",
|
|
28
|
+
"TMPDIR",
|
|
29
|
+
"LANG",
|
|
30
|
+
"TZ",
|
|
31
|
+
"TERM",
|
|
32
|
+
"USER",
|
|
33
|
+
"LOGNAME",
|
|
34
|
+
"SHELL",
|
|
35
|
+
"NPM_CONFIG_CACHE",
|
|
36
|
+
"SSH_AUTH_SOCK",
|
|
37
|
+
];
|
|
38
|
+
/** Locale variables (`LC_ALL`, `LC_CTYPE`, …) pass through by prefix. */
|
|
39
|
+
const AGENT_ENV_PREFIXES = ["LC_"];
|
|
40
|
+
/** The allowlisted subset of `source` (defined values only). */
|
|
41
|
+
export function agentEnv(source = process.env) {
|
|
42
|
+
const env = {};
|
|
43
|
+
for (const [key, value] of Object.entries(source)) {
|
|
44
|
+
if (value === undefined)
|
|
45
|
+
continue;
|
|
46
|
+
if (AGENT_ENV_KEYS.includes(key) ||
|
|
47
|
+
AGENT_ENV_PREFIXES.some((prefix) => key.startsWith(prefix))) {
|
|
48
|
+
env[key] = value;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return env;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Upstream's local env with every command confined to {@link agentEnv}
|
|
55
|
+
* plus the call's own `env`. `inheritEnv` is overridden whatever the
|
|
56
|
+
* caller asked for: that flag is how the leak happens.
|
|
57
|
+
*/
|
|
58
|
+
export class AgentExecutionEnv extends NodeExecutionEnv {
|
|
59
|
+
exec(command, options, context) {
|
|
60
|
+
return super.exec(command, { ...options, inheritEnv: false, env: { ...agentEnv(), ...options?.env } }, context);
|
|
61
|
+
}
|
|
62
|
+
}
|
package/dist/harness/ambient.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The ambient seam (spec: docs/approval-ux-seams.md, "Ambient surfaces"
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
2
|
+
* The ambient seam (spec: docs/approval-ux-seams.md, "Ambient surfaces";
|
|
3
|
+
* the block mapping: UI-blocks design §9.2): core-owned,
|
|
4
|
+
* channel-neutral records for persistent read-mostly state — statusline
|
|
5
|
+
* slots and panel widgets — written by extensions through the host
|
|
6
|
+
* context and rendered by channels generically.
|
|
6
7
|
*
|
|
7
8
|
* SPLIT (build-and-webui Task 1): the canonical TYPE declarations live in
|
|
8
9
|
* the `@sigrun/shared` package (`shared/src/ambient.ts` — the records ride
|
|
@@ -16,10 +17,22 @@
|
|
|
16
17
|
* not silent composition — the pi-zentui chrome-skin lesson).
|
|
17
18
|
*
|
|
18
19
|
* Layering mirrors the interaction seam: strict envelope (key, place,
|
|
19
|
-
* budget, refresh, expires) around a body
|
|
20
|
-
* ambient
|
|
20
|
+
* budget, refresh, expires) around a block-tree body (placement
|
|
21
|
+
* `ambient`; `status`/`footer` allow one line), with opaque `details`.
|
|
22
|
+
* Legacy-shaped bodies FAIL with paths (ruling 16).
|
|
21
23
|
*/
|
|
22
24
|
import { defineDoc, defineDocFamily } from "@earendil-works/pi-durable";
|
|
25
|
+
import { validateTree } from "../blocks/validate.js";
|
|
26
|
+
/**
|
|
27
|
+
* Narrow a stored record to the public wire record. SAFETY: the two
|
|
28
|
+
* shapes are identical at runtime — a stored body passed `validateTree`
|
|
29
|
+
* (placement `ambient`) at write time, and a validated tree IS the JSON
|
|
30
|
+
* the mirror describes; the cast only restores the type-level naming
|
|
31
|
+
* `JsonObject`-erasure removed.
|
|
32
|
+
*/
|
|
33
|
+
export function asAmbientRecord(value) {
|
|
34
|
+
return value;
|
|
35
|
+
}
|
|
23
36
|
/**
|
|
24
37
|
* The ambient record family: one record per surface key, per conversation.
|
|
25
38
|
* (Session-scoped statuslines that outlive one conversation are a v2
|
|
@@ -48,9 +61,12 @@ export const AmbientOwnersDoc = defineDoc({
|
|
|
48
61
|
});
|
|
49
62
|
const PLACES = new Set(["status", "footer", "panel"]);
|
|
50
63
|
const OVERFLOWS = new Set(["drop", "wrap", "summarize"]);
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
64
|
+
/**
|
|
65
|
+
* Structural validation of one ambient record: the envelope's own checks,
|
|
66
|
+
* then the body tree through `validateTree({placement: "ambient"})` with
|
|
67
|
+
* the one-line rule for `status`/`footer` (§9.2). Returns the error text
|
|
68
|
+
* (with the tree paths) or null.
|
|
69
|
+
*/
|
|
54
70
|
export function validateAmbient(value) {
|
|
55
71
|
if (typeof value !== "object" || value === null) {
|
|
56
72
|
return "ambient record must be an object";
|
|
@@ -80,182 +96,20 @@ export function validateAmbient(value) {
|
|
|
80
96
|
const body = r.body;
|
|
81
97
|
if (body === undefined)
|
|
82
98
|
return null; // an empty write clears the surface
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
}
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
if (b.rows !== undefined) {
|
|
98
|
-
if (!Array.isArray(b.rows))
|
|
99
|
-
return "body.rows must be an array";
|
|
100
|
-
for (const row of b.rows) {
|
|
101
|
-
const problem = validateRow(row);
|
|
102
|
-
if (problem !== null)
|
|
103
|
-
return problem;
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
if (b.gauge !== undefined) {
|
|
107
|
-
const problem = validateGauge(b.gauge);
|
|
108
|
-
if (problem !== null)
|
|
109
|
-
return problem;
|
|
110
|
-
}
|
|
111
|
-
if (b.ledger !== undefined) {
|
|
112
|
-
if (!Array.isArray(b.ledger))
|
|
113
|
-
return "body.ledger must be an array";
|
|
114
|
-
for (const e of b.ledger) {
|
|
115
|
-
const problem = validateLedgerEntry(e);
|
|
116
|
-
if (problem !== null)
|
|
117
|
-
return problem;
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
return null;
|
|
121
|
-
}
|
|
122
|
-
function validateLine(value) {
|
|
123
|
-
if (typeof value !== "object" || value === null)
|
|
124
|
-
return "line must be an object";
|
|
125
|
-
const l = value;
|
|
126
|
-
if (l.text === undefined && l.segments === undefined) {
|
|
127
|
-
return "line needs text or segments";
|
|
128
|
-
}
|
|
129
|
-
if (l.text !== undefined && typeof l.text !== "string") {
|
|
130
|
-
return "line.text must be a string";
|
|
131
|
-
}
|
|
132
|
-
if (l.segments !== undefined) {
|
|
133
|
-
if (!Array.isArray(l.segments))
|
|
134
|
-
return "line.segments must be an array";
|
|
135
|
-
for (const s of l.segments) {
|
|
136
|
-
if (typeof s !== "object" || s === null)
|
|
137
|
-
return "segment must be an object";
|
|
138
|
-
const seg = s;
|
|
139
|
-
if (typeof seg.id !== "string" || seg.id.length === 0) {
|
|
140
|
-
return "segment.id must be a non-empty string";
|
|
141
|
-
}
|
|
142
|
-
if (typeof seg.text !== "string")
|
|
143
|
-
return "segment.text must be a string";
|
|
144
|
-
if (seg.place !== undefined && !ZONES.has(String(seg.place))) {
|
|
145
|
-
return `segment.place must be one of left|middle|right (got ${String(seg.place)})`;
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
return null;
|
|
150
|
-
}
|
|
151
|
-
function validateRow(value) {
|
|
152
|
-
if (typeof value !== "object" || value === null)
|
|
153
|
-
return "row must be an object";
|
|
154
|
-
const r = value;
|
|
155
|
-
if (typeof r.id !== "string" || r.id.length === 0) {
|
|
156
|
-
return "row.id must be a non-empty string";
|
|
157
|
-
}
|
|
158
|
-
if (typeof r.label !== "string")
|
|
159
|
-
return "row.label must be a string";
|
|
160
|
-
if (r.status !== undefined && typeof r.status !== "string") {
|
|
161
|
-
return "row.status must be a string";
|
|
162
|
-
}
|
|
163
|
-
if (r.severity !== undefined &&
|
|
164
|
-
r.severity !== 0 &&
|
|
165
|
-
r.severity !== 1 &&
|
|
166
|
-
r.severity !== 2) {
|
|
167
|
-
return "row.severity must be 0, 1, or 2";
|
|
168
|
-
}
|
|
169
|
-
if (r.quiet !== undefined && typeof r.quiet !== "boolean") {
|
|
170
|
-
return "row.quiet must be a boolean";
|
|
171
|
-
}
|
|
172
|
-
if (r.detail !== undefined && typeof r.detail !== "string") {
|
|
173
|
-
return "row.detail must be a string";
|
|
174
|
-
}
|
|
175
|
-
if (r.depth !== undefined && typeof r.depth !== "number") {
|
|
176
|
-
return "row.depth must be a number";
|
|
177
|
-
}
|
|
178
|
-
if (r.deps !== undefined && !Array.isArray(r.deps)) {
|
|
179
|
-
return "row.deps must be an array of ids";
|
|
180
|
-
}
|
|
181
|
-
if (Array.isArray(r.deps) && r.deps.some((d) => typeof d !== "string")) {
|
|
182
|
-
return "row.deps must be an array of ids";
|
|
183
|
-
}
|
|
184
|
-
if (r.spinnerLabel !== undefined && typeof r.spinnerLabel !== "string") {
|
|
185
|
-
return "row.spinnerLabel must be a string";
|
|
186
|
-
}
|
|
187
|
-
if (r.metrics !== undefined) {
|
|
188
|
-
const problem = validateMetrics(r.metrics);
|
|
189
|
-
if (problem !== null)
|
|
190
|
-
return problem;
|
|
191
|
-
}
|
|
192
|
-
return null;
|
|
193
|
-
}
|
|
194
|
-
function validateMetrics(value) {
|
|
195
|
-
if (typeof value !== "object" || value === null)
|
|
196
|
-
return "metrics must be an object";
|
|
197
|
-
const m = value;
|
|
198
|
-
for (const k of ["tokens", "cost", "current", "total", "completed"]) {
|
|
199
|
-
if (m[k] !== undefined && typeof m[k] !== "number") {
|
|
200
|
-
return `metrics.${k} must be a number`;
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
if (m.format !== undefined && !FORMATS.has(String(m.format))) {
|
|
204
|
-
return `metrics.format must be one of compact|usd|percent (got ${String(m.format)})`;
|
|
205
|
-
}
|
|
206
|
-
return null;
|
|
207
|
-
}
|
|
208
|
-
function validateGauge(value) {
|
|
209
|
-
if (typeof value !== "object" || value === null)
|
|
210
|
-
return "gauge must be an object";
|
|
211
|
-
const g = value;
|
|
212
|
-
if (typeof g.label !== "string")
|
|
213
|
-
return "gauge.label must be a string";
|
|
214
|
-
if (g.percent !== undefined && typeof g.percent !== "number") {
|
|
215
|
-
return "gauge.percent must be a number";
|
|
216
|
-
}
|
|
217
|
-
if (g.phase !== undefined && typeof g.phase !== "string") {
|
|
218
|
-
return "gauge.phase must be a string";
|
|
219
|
-
}
|
|
220
|
-
if (g.elapsedMs !== undefined && typeof g.elapsedMs !== "number") {
|
|
221
|
-
return "gauge.elapsedMs must be a number";
|
|
222
|
-
}
|
|
223
|
-
if (g.recentOutput !== undefined && !Array.isArray(g.recentOutput)) {
|
|
224
|
-
return "gauge.recentOutput must be an array of strings";
|
|
225
|
-
}
|
|
226
|
-
if (Array.isArray(g.recentOutput) && g.recentOutput.some((l) => typeof l !== "string")) {
|
|
227
|
-
return "gauge.recentOutput must be an array of strings";
|
|
228
|
-
}
|
|
229
|
-
if (g.checks !== undefined && !Array.isArray(g.checks)) {
|
|
230
|
-
return "gauge.checks must be row-shaped";
|
|
231
|
-
}
|
|
232
|
-
if (Array.isArray(g.checks)) {
|
|
233
|
-
for (const row of g.checks) {
|
|
234
|
-
const problem = validateRow(row);
|
|
235
|
-
if (problem !== null)
|
|
236
|
-
return `gauge.checks: ${problem}`;
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
return null;
|
|
240
|
-
}
|
|
241
|
-
function validateLedgerEntry(value) {
|
|
242
|
-
if (typeof value !== "object" || value === null)
|
|
243
|
-
return "ledger entry must be an object";
|
|
244
|
-
const e = value;
|
|
245
|
-
if (typeof e.id !== "string" || e.id.length === 0) {
|
|
246
|
-
return "ledger.id must be a non-empty string";
|
|
247
|
-
}
|
|
248
|
-
if (typeof e.at !== "number")
|
|
249
|
-
return "ledger.at must be a number";
|
|
250
|
-
if (typeof e.kind !== "string")
|
|
251
|
-
return "ledger.kind must be a string";
|
|
252
|
-
if (typeof e.summary !== "string")
|
|
253
|
-
return "ledger.summary must be a string";
|
|
254
|
-
if (e.detail !== undefined && !Array.isArray(e.detail)) {
|
|
255
|
-
return "ledger.detail must be an array of strings";
|
|
256
|
-
}
|
|
257
|
-
if (Array.isArray(e.detail) && e.detail.some((d) => typeof d !== "string")) {
|
|
258
|
-
return "ledger.detail must be an array of strings";
|
|
99
|
+
// Ruling 16 (no compatibility layer): legacy-shaped bodies (objects
|
|
100
|
+
// keyed line/rows/gauge/ledger rather than {v, root}) fail HERE, with
|
|
101
|
+
// the path — a tree is {v, root}.
|
|
102
|
+
const result = validateTree(body, {
|
|
103
|
+
placement: "ambient",
|
|
104
|
+
ambientPlace: r.place,
|
|
105
|
+
});
|
|
106
|
+
if (!result.ok) {
|
|
107
|
+
const first = result.errors[0];
|
|
108
|
+
if (first === undefined)
|
|
109
|
+
return "body must be a block tree";
|
|
110
|
+
// The tree-relative path joins the record's field ("body.root…")
|
|
111
|
+
// so the error names where the record is wrong, not just the tree.
|
|
112
|
+
return `body is not a valid ambient tree: body.${first.path} ${first.message}`;
|
|
259
113
|
}
|
|
260
114
|
return null;
|
|
261
115
|
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The chat-blocks entry kinds (UI-blocks U5 Task 1, spec §8.2 storage):
|
|
3
|
+
* typed pi-durable entries the transcript carries.
|
|
4
|
+
*
|
|
5
|
+
* - `sigrun.blocks` (`BlocksEntry`): one agent-authored block surface,
|
|
6
|
+
* `{title, tree, data}` EXACTLY as `show_blocks` created it. It never
|
|
7
|
+
* changes — the live state lives in the `sigrun.blocks.state` family
|
|
8
|
+
* (state.ts); the entry is the immutable definition the channels
|
|
9
|
+
* re-render from.
|
|
10
|
+
* - `sigrun.blocks.note` (`BlocksNoteEntry`): the §8.3 change note — a
|
|
11
|
+
* `type: "write"` submission whose entry carries the note as a `model`
|
|
12
|
+
* user message. Declared here so the kind token exists in one place;
|
|
13
|
+
* the note WRITER is Task 3 (src/server/blocks/notes.ts).
|
|
14
|
+
* - `sigrun.blocks.answer` (`BlocksAnswerEntry`, Task 3): a block answer
|
|
15
|
+
* as an input entry kind. Declared alongside for the same
|
|
16
|
+
* one-place-ness; Task 3 owns its writer.
|
|
17
|
+
*
|
|
18
|
+
* Payload typing: `Tree`'s block interfaces carry no implicit index
|
|
19
|
+
* signatures, so the payload cannot satisfy pi-durable's
|
|
20
|
+
* `D extends JsonValue` on its own — `tree` is typed `Tree & JsonValue`
|
|
21
|
+
* (verified: a `Tree` literal satisfies the intersection), and the
|
|
22
|
+
* runtime object pi-durable stores is exactly the validated tree.
|
|
23
|
+
*/
|
|
24
|
+
import { defineEntry } from "@earendil-works/pi-durable";
|
|
25
|
+
/** One agent-authored block surface in chat (spec §8.2): never changes. */
|
|
26
|
+
export const BlocksEntry = defineEntry("sigrun.blocks");
|
|
27
|
+
/**
|
|
28
|
+
* The §8.3 change note entry: `model` is `[UserMessage]` carrying the
|
|
29
|
+
* note text (Task 3 writes it before every turn admission). `data`
|
|
30
|
+
* carries the note's COVERAGE ({@link BlocksNoteData}): the durable
|
|
31
|
+
* artifact of which changes the admitted note names, so a deduped
|
|
32
|
+
* retry (pi-durable returns the existing submission, discarding the
|
|
33
|
+
* retried text) advances `notedThrough` only as far as the ADMITTED
|
|
34
|
+
* note covered — never past changes a later compute added but the
|
|
35
|
+
* admitted text never described.
|
|
36
|
+
*
|
|
37
|
+
* NOTE: `advance`'s element array is mutable-typed because pi-durable's
|
|
38
|
+
* `D extends JsonValue` constraint rejects `readonly` arrays; the
|
|
39
|
+
* stored value is still treated as immutable by the writer.
|
|
40
|
+
*/
|
|
41
|
+
export const BlocksNoteEntry = defineEntry("sigrun.blocks.note");
|
|
42
|
+
/**
|
|
43
|
+
* A block answer as a user-turn entry (Task 3): `model` is `[UserMessage]`
|
|
44
|
+
* with the server-built answer projection; no payload — the answer's
|
|
45
|
+
* `{entryId, action, payload, state}` ride the submission record.
|
|
46
|
+
*/
|
|
47
|
+
export const BlocksAnswerEntry = defineEntry("sigrun.blocks.answer");
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The chat-block state family (UI-blocks U5 Task 1, spec §8.2): the live
|
|
3
|
+
* state of one `sigrun.blocks` entry, a conversation-scoped document
|
|
4
|
+
* family keyed by entry id.
|
|
5
|
+
*
|
|
6
|
+
* Shape `{rev, data, changes, notedThrough}`:
|
|
7
|
+
* - `rev` — the monotonic data revision (both agent `update_blocks` and
|
|
8
|
+
* user edits advance it; every advance publishes `surface.changed`);
|
|
9
|
+
* - `data` — the CURRENT data (the entry's `data` at `show_blocks` time,
|
|
10
|
+
* then whatever ops applied since);
|
|
11
|
+
* - `changes` — the user's `ChangeRecord`s (agent updates record NONE:
|
|
12
|
+
* the agent made the change and already knows);
|
|
13
|
+
* - `notedThrough` — the highest `seq` already sent to the agent in a
|
|
14
|
+
* §8.3 note. Records at or below it are trimmed once more than
|
|
15
|
+
* {@link MAX_CHANGES} have accumulated (the trim runs on append).
|
|
16
|
+
*
|
|
17
|
+
* Fork semantics (ruling 19): `history: "latest", fork: "current"` —
|
|
18
|
+
* pi-durable copies the current document at fork creation, so the child
|
|
19
|
+
* gets the block's current `data`, `rev`, records and `notedThrough`,
|
|
20
|
+
* then diverges. Blocks created after the fork point copy too, but are
|
|
21
|
+
* never addressable in the child (their entry is not in its history).
|
|
22
|
+
*/
|
|
23
|
+
import { defineDocFamily } from "@earendil-works/pi-durable";
|
|
24
|
+
/** The trim threshold (spec §8.2): trim at-or-below `notedThrough` once more than 50 accumulate. */
|
|
25
|
+
export const MAX_CHANGES = 50;
|
|
26
|
+
/** The `sigrun.blocks.state` family: one member per (conversation, entry id). */
|
|
27
|
+
export const BlocksStateDoc = defineDocFamily({
|
|
28
|
+
kind: "sigrun.blocks.state",
|
|
29
|
+
version: 1,
|
|
30
|
+
scope: "conversation",
|
|
31
|
+
history: "latest",
|
|
32
|
+
fork: "current",
|
|
33
|
+
family: true,
|
|
34
|
+
// The seed only shapes a first touch; every real member is created by
|
|
35
|
+
// show_blocks, which writes the full state in its append commit.
|
|
36
|
+
initial: () => ({ rev: 0, data: {}, changes: [], notedThrough: 0 }),
|
|
37
|
+
});
|
|
38
|
+
/**
|
|
39
|
+
* Read one block's state (committed). `undefined` when the conversation
|
|
40
|
+
* has no such member — the entry id is unknown here, or the block was
|
|
41
|
+
* never created in this conversation.
|
|
42
|
+
*/
|
|
43
|
+
export async function readBlocksState(harness, conversationId, entryId, ctx) {
|
|
44
|
+
return await harness.snapshot(BlocksStateDoc, conversationId, entryId, ctx);
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Trim change records at or below `notedThrough` once more than
|
|
48
|
+
* {@link MAX_CHANGES} have accumulated (spec §8.2) — in the caller's
|
|
49
|
+
* commit, on the draft the append already holds.
|
|
50
|
+
*/
|
|
51
|
+
export function trimChanges(state) {
|
|
52
|
+
if (state.changes.length <= MAX_CHANGES)
|
|
53
|
+
return;
|
|
54
|
+
const kept = state.changes.filter((record) => record.seq > state.notedThrough);
|
|
55
|
+
// Keep the newest MAX_CHANGES of what remains (the trim is bounded, not
|
|
56
|
+
// a wholesale wipe): a surge of edits above notedThrough keeps its tail.
|
|
57
|
+
state.changes = kept.slice(Math.max(0, kept.length - MAX_CHANGES));
|
|
58
|
+
}
|