@sigrun/sigrun 0.0.0-stage → 0.1.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.
Files changed (130) hide show
  1. package/CHANGELOG.md +204 -0
  2. package/CONTEXT.md +163 -0
  3. package/LICENSE +21 -0
  4. package/README.md +691 -2
  5. package/THIRD_PARTY_NOTICES.md +28 -0
  6. package/dist/backup/marker.js +80 -0
  7. package/dist/backup/run-backup.js +339 -0
  8. package/dist/cli.js +952 -0
  9. package/dist/commands/expand.js +245 -0
  10. package/dist/commands/templates.js +272 -0
  11. package/dist/config.js +1146 -0
  12. package/dist/extension.d.ts +431 -0
  13. package/dist/harness/activity.js +42 -0
  14. package/dist/harness/ambient.js +261 -0
  15. package/dist/harness/auth.js +130 -0
  16. package/dist/harness/boot.js +865 -0
  17. package/dist/harness/builtins.js +191 -0
  18. package/dist/harness/event-bus.js +1203 -0
  19. package/dist/harness/health.js +30 -0
  20. package/dist/harness/identity/index.js +37 -0
  21. package/dist/harness/identity/invites.js +94 -0
  22. package/dist/harness/identity/json-store.js +108 -0
  23. package/dist/harness/identity/setup-code.js +81 -0
  24. package/dist/harness/identity/tokens.js +101 -0
  25. package/dist/harness/identity/users.js +93 -0
  26. package/dist/harness/interactions.js +283 -0
  27. package/dist/harness/loader.js +1236 -0
  28. package/dist/harness/memory-default.js +35 -0
  29. package/dist/harness/naming.js +193 -0
  30. package/dist/harness/packages.js +1002 -0
  31. package/dist/harness/providers/auth-bridge.js +60 -0
  32. package/dist/harness/providers/index.js +277 -0
  33. package/dist/harness/providers/models-store.js +105 -0
  34. package/dist/harness/providers/refresh.js +161 -0
  35. package/dist/harness/providers/scheduler.js +89 -0
  36. package/dist/harness/registry.js +9 -0
  37. package/dist/harness/reserved-sections.js +24 -0
  38. package/dist/harness/seam-host.js +239 -0
  39. package/dist/harness/sections/context-files.js +78 -0
  40. package/dist/harness/sections/model-guidance.js +77 -0
  41. package/dist/harness/settings.js +1053 -0
  42. package/dist/harness/stores.js +450 -0
  43. package/dist/harness/tools.js +500 -0
  44. package/dist/harness/workspace.js +73 -0
  45. package/dist/index.js +39 -0
  46. package/dist/jobs/docs.js +257 -0
  47. package/dist/jobs/fire.js +342 -0
  48. package/dist/jobs/index.js +228 -0
  49. package/dist/jobs/observe.js +165 -0
  50. package/dist/jobs/reconcile.js +245 -0
  51. package/dist/jobs/schedule.js +77 -0
  52. package/dist/jobs/spec.js +248 -0
  53. package/dist/jobs/task.js +190 -0
  54. package/dist/jobs/transfer.js +53 -0
  55. package/dist/serve.js +164 -0
  56. package/dist/server/auth/claim-lock.js +28 -0
  57. package/dist/server/auth/ip.js +51 -0
  58. package/dist/server/auth/login-limiter.js +52 -0
  59. package/dist/server/auth/loopback.js +47 -0
  60. package/dist/server/auth/pipeline.js +330 -0
  61. package/dist/server/auth/principal.js +26 -0
  62. package/dist/server/commands.js +198 -0
  63. package/dist/server/content-parts.js +142 -0
  64. package/dist/server/context.js +13 -0
  65. package/dist/server/conversation-handler.js +61 -0
  66. package/dist/server/conversation-ops.js +310 -0
  67. package/dist/server/events/hub.js +39 -0
  68. package/dist/server/events/types.js +8 -0
  69. package/dist/server/helpers.js +351 -0
  70. package/dist/server/host-bridge.js +131 -0
  71. package/dist/server/index.js +232 -0
  72. package/dist/server/notifications.js +185 -0
  73. package/dist/server/ownership.js +67 -0
  74. package/dist/server/readiness.js +36 -0
  75. package/dist/server/routes/auth.js +172 -0
  76. package/dist/server/routes/conversations.js +341 -0
  77. package/dist/server/routes/events.js +161 -0
  78. package/dist/server/routes/extension-routes.js +127 -0
  79. package/dist/server/routes/extensions.js +168 -0
  80. package/dist/server/routes/files.js +308 -0
  81. package/dist/server/routes/interactions.js +260 -0
  82. package/dist/server/routes/jobs.js +271 -0
  83. package/dist/server/routes/meta.js +146 -0
  84. package/dist/server/routes/metrics.js +219 -0
  85. package/dist/server/routes/onboarding.js +268 -0
  86. package/dist/server/routes/probes.js +53 -0
  87. package/dist/server/routes/providers.js +198 -0
  88. package/dist/server/routes/settings.js +48 -0
  89. package/dist/server/routes/stream.js +131 -0
  90. package/dist/server/routes/submissions.js +317 -0
  91. package/dist/server/routes/users.js +169 -0
  92. package/dist/server/schema-fixture.js +950 -0
  93. package/dist/server/session-meta.js +247 -0
  94. package/dist/server/static.js +169 -0
  95. package/dist/server/views.js +71 -0
  96. package/dist/skills/index.js +418 -0
  97. package/node_modules/@sigrun/shared/CHANGELOG.md +16 -0
  98. package/node_modules/@sigrun/shared/LICENSE +21 -0
  99. package/node_modules/@sigrun/shared/README.md +10 -0
  100. package/node_modules/@sigrun/shared/dist/access.d.ts +55 -0
  101. package/node_modules/@sigrun/shared/dist/access.js +72 -0
  102. package/node_modules/@sigrun/shared/dist/ambient.d.ts +111 -0
  103. package/node_modules/@sigrun/shared/dist/ambient.js +23 -0
  104. package/node_modules/@sigrun/shared/dist/commands.d.ts +75 -0
  105. package/node_modules/@sigrun/shared/dist/commands.js +25 -0
  106. package/node_modules/@sigrun/shared/dist/conversations.d.ts +31 -0
  107. package/node_modules/@sigrun/shared/dist/conversations.js +94 -0
  108. package/node_modules/@sigrun/shared/dist/interactions.d.ts +91 -0
  109. package/node_modules/@sigrun/shared/dist/interactions.js +21 -0
  110. package/node_modules/@sigrun/shared/dist/jobs.d.ts +100 -0
  111. package/node_modules/@sigrun/shared/dist/jobs.js +19 -0
  112. package/node_modules/@sigrun/shared/dist/json.d.ts +20 -0
  113. package/node_modules/@sigrun/shared/dist/json.js +13 -0
  114. package/node_modules/@sigrun/shared/dist/session-meta.d.ts +103 -0
  115. package/node_modules/@sigrun/shared/dist/session-meta.js +13 -0
  116. package/node_modules/@sigrun/shared/dist/settings.d.ts +401 -0
  117. package/node_modules/@sigrun/shared/dist/settings.js +19 -0
  118. package/node_modules/@sigrun/shared/dist/themes.d.ts +50 -0
  119. package/node_modules/@sigrun/shared/dist/themes.js +156 -0
  120. package/node_modules/@sigrun/shared/dist/turns.d.ts +102 -0
  121. package/node_modules/@sigrun/shared/dist/turns.js +166 -0
  122. package/node_modules/@sigrun/shared/dist/view-delta.d.ts +54 -0
  123. package/node_modules/@sigrun/shared/dist/view-delta.js +135 -0
  124. package/node_modules/@sigrun/shared/dist/views.d.ts +742 -0
  125. package/node_modules/@sigrun/shared/dist/views.js +38 -0
  126. package/node_modules/@sigrun/shared/package.json +83 -0
  127. package/package.json +90 -5
  128. package/webui/dist/assets/index-Rl0KhCLa.css +1 -0
  129. package/webui/dist/assets/index-rOiARZLF.js +50 -0
  130. package/webui/dist/index.html +37 -0
@@ -0,0 +1,283 @@
1
+ /**
2
+ * The interaction seam (spec: docs/approval-ux-seams.md): core-owned,
3
+ * channel-neutral records for "this conversation is waiting on a human
4
+ * decision", written by extensions through the host context and answered
5
+ * by channels through one generic route.
6
+ *
7
+ * SPLIT (build-and-webui Task 1): the canonical TYPE declarations live in
8
+ * the `@sigrun/shared` package (`shared/src/interactions.ts` — the records
9
+ * ride the §5 wire as-is, so the shapes ARE the channel contract);
10
+ * this module owns the docs, validation, and index RUNTIME, importing the
11
+ * types (type-only — no runtime dependency on the built package).
12
+ *
13
+ * Layering (strict envelope / closed primitives / opaque details):
14
+ *
15
+ * - envelope: kind, callId, status, timestamps, answer — load-bearing for
16
+ * joining tool slots and lifecycle arbitration. Strictly validated.
17
+ * - body: composed ONLY from the closed primitives (text, options,
18
+ * fields, diff). Structurally validated; unknown body keys quarantine.
19
+ * - details: opaque extension-owned JSON. Never interpreted.
20
+ *
21
+ * Answer transport: the generic route commits `answer` into the record.
22
+ * First-answer-wins is enforced INSIDE the commit (the mutation line
23
+ * serializes commits), mirroring the gated-doc-family pattern.
24
+ * Resolution semantics (what allow/deny mean) stay extension-owned: the
25
+ * extension reads the record through `api.snapshot` in its own poll loop.
26
+ *
27
+ * Enumerability: pi-durable's conversation view mounts only its five
28
+ * built-in docs (dist/harness/view.js `MOUNTED`), and family docs are
29
+ * excluded even there, so pending interactions need an index. The
30
+ * `InteractionsIndexDoc` (session singleton) lists pending callIds per
31
+ * conversation; every seed/answer/expire commit maintains it. Channels
32
+ * read the list route, which snapshots the index and then the records.
33
+ */
34
+ import { defineDoc, defineDocFamily } from "@earendil-works/pi-durable";
35
+ /** The interaction record doc family: one record per callId, per conversation. */
36
+ export const InteractionDoc = defineDocFamily({
37
+ kind: "sigrun.interaction",
38
+ version: 1,
39
+ scope: "conversation",
40
+ history: "latest",
41
+ fork: "current",
42
+ family: true,
43
+ initial: (callId) => ({
44
+ kind: "",
45
+ callId,
46
+ status: "pending",
47
+ createdAt: 0,
48
+ }),
49
+ });
50
+ export const InteractionsIndexDoc = defineDoc({
51
+ kind: "sigrun.interactions.index",
52
+ version: 1,
53
+ scope: "session",
54
+ initial: () => ({ pending: {} }),
55
+ });
56
+ /** Terminal statuses: answering one of these is a no-op (first wins).
57
+ * Exported for the event bus (W3 Task 4): the `interaction.pending` /
58
+ * `interaction.settled` host-event split derives from the SAME set — one
59
+ * definition, never two. */
60
+ export const TERMINAL = new Set([
61
+ "answered",
62
+ "skipped",
63
+ "expired",
64
+ "cancelled",
65
+ ]);
66
+ const TONES = new Set([
67
+ "danger",
68
+ "warn",
69
+ "ok",
70
+ "neutral",
71
+ ]);
72
+ const FIELD_TYPES = new Set([
73
+ "string",
74
+ "number",
75
+ "boolean",
76
+ "secret",
77
+ ]);
78
+ /** Structural validation of one interaction record. Returns the error text or null. */
79
+ export function validateInteraction(value) {
80
+ if (typeof value !== "object" || value === null) {
81
+ return "interaction record must be an object";
82
+ }
83
+ const r = value;
84
+ if (typeof r.kind !== "string" || r.kind.length === 0) {
85
+ return "kind must be a non-empty string";
86
+ }
87
+ if (typeof r.callId !== "string" || r.callId.length === 0) {
88
+ return "callId must be a non-empty string";
89
+ }
90
+ if (typeof r.status !== "string" || !TERMINAL.has(r.status)) {
91
+ if (r.status !== "pending")
92
+ return `unknown status ${String(r.status)}`;
93
+ }
94
+ if (typeof r.createdAt !== "number") {
95
+ return "createdAt must be a number";
96
+ }
97
+ if (r.expiresAt !== undefined && typeof r.expiresAt !== "number") {
98
+ return "expiresAt must be a number";
99
+ }
100
+ if (r.settledAt !== undefined && typeof r.settledAt !== "number") {
101
+ return "settledAt must be a number";
102
+ }
103
+ if (r.answer !== undefined) {
104
+ const problem = validateAnswer(r.answer);
105
+ if (problem !== null)
106
+ return problem;
107
+ }
108
+ if (r.answeredBy !== undefined && r.answeredBy !== "human" && r.answeredBy !== "extension") {
109
+ return "answeredBy must be 'human' or 'extension'";
110
+ }
111
+ const body = r.body;
112
+ if (body !== undefined) {
113
+ if (typeof body !== "object" || body === null) {
114
+ return "body must be an object";
115
+ }
116
+ const b = body;
117
+ const known = new Set(["text", "options", "fields", "diff"]);
118
+ const unknownKey = Object.keys(b).find((k) => !known.has(k));
119
+ if (unknownKey !== undefined) {
120
+ return `body.${unknownKey} is outside the closed primitives (move it to details)`;
121
+ }
122
+ if (b.text !== undefined && typeof b.text !== "string") {
123
+ return "body.text must be a string";
124
+ }
125
+ if (b.options !== undefined) {
126
+ if (!Array.isArray(b.options))
127
+ return "body.options must be an array";
128
+ for (const o of b.options) {
129
+ const problem = validateOption(o);
130
+ if (problem !== null)
131
+ return problem;
132
+ }
133
+ }
134
+ if (b.fields !== undefined) {
135
+ if (!Array.isArray(b.fields))
136
+ return "body.fields must be an array";
137
+ for (const f of b.fields) {
138
+ const problem = validateField(f);
139
+ if (problem !== null)
140
+ return problem;
141
+ }
142
+ }
143
+ if (b.diff !== undefined) {
144
+ const problem = validateDiff(b.diff);
145
+ if (problem !== null)
146
+ return problem;
147
+ }
148
+ }
149
+ return null;
150
+ }
151
+ function validateAnswer(value) {
152
+ if (typeof value !== "object" || value === null) {
153
+ return "answer must be an object";
154
+ }
155
+ const a = value;
156
+ const known = new Set(["ids", "values", "text"]);
157
+ const unknownKey = Object.keys(a).find((k) => !known.has(k));
158
+ if (unknownKey !== undefined)
159
+ return `answer.${unknownKey} is not a valid field`;
160
+ if (a.ids !== undefined && !Array.isArray(a.ids)) {
161
+ return "answer.ids must be an array of strings";
162
+ }
163
+ if (Array.isArray(a.ids) && a.ids.some((id) => typeof id !== "string")) {
164
+ return "answer.ids must be an array of strings";
165
+ }
166
+ if (a.values !== undefined) {
167
+ if (typeof a.values !== "object" || a.values === null) {
168
+ return "answer.values must be an object";
169
+ }
170
+ const bad = Object.entries(a.values).find(([, v]) => typeof v !== "string" && typeof v !== "number" && typeof v !== "boolean");
171
+ if (bad !== undefined) {
172
+ return `answer.values.${bad[0]} must be string, number, or boolean`;
173
+ }
174
+ }
175
+ if (a.text !== undefined && typeof a.text !== "string") {
176
+ return "answer.text must be a string";
177
+ }
178
+ return null;
179
+ }
180
+ function validateOption(value) {
181
+ if (typeof value !== "object" || value === null)
182
+ return "option must be an object";
183
+ const o = value;
184
+ if (typeof o.id !== "string" || o.id.length === 0)
185
+ return "option.id must be a non-empty string";
186
+ if (typeof o.label !== "string" || o.label.length === 0) {
187
+ return "option.label must be a non-empty string";
188
+ }
189
+ if (o.description !== undefined && typeof o.description !== "string") {
190
+ return "option.description must be a string";
191
+ }
192
+ if (o.tone !== undefined && !TONES.has(String(o.tone))) {
193
+ return `option.tone must be one of danger|warn|ok|neutral (got ${String(o.tone)})`;
194
+ }
195
+ if (o.recommended !== undefined && typeof o.recommended !== "boolean") {
196
+ return "option.recommended must be a boolean";
197
+ }
198
+ return null;
199
+ }
200
+ function validateField(value) {
201
+ if (typeof value !== "object" || value === null)
202
+ return "field must be an object";
203
+ const f = value;
204
+ if (typeof f.name !== "string" || f.name.length === 0) {
205
+ return "field.name must be a non-empty string";
206
+ }
207
+ if (typeof f.type !== "string" || !FIELD_TYPES.has(f.type)) {
208
+ return `field.type must be one of string|number|boolean|secret (got ${String(f.type)})`;
209
+ }
210
+ if (f.label !== undefined && typeof f.label !== "string") {
211
+ return "field.label must be a string";
212
+ }
213
+ if (f.required !== undefined && typeof f.required !== "boolean") {
214
+ return "field.required must be a boolean";
215
+ }
216
+ return null;
217
+ }
218
+ function validateDiff(value) {
219
+ if (typeof value !== "object" || value === null)
220
+ return "diff must be an object";
221
+ const d = value;
222
+ if (typeof d.before !== "string")
223
+ return "diff.before must be a string";
224
+ if (typeof d.after !== "string")
225
+ return "diff.after must be a string";
226
+ if (d.path !== undefined && typeof d.path !== "string")
227
+ return "diff.path must be a string";
228
+ if (d.lang !== undefined && typeof d.lang !== "string")
229
+ return "diff.lang must be a string";
230
+ return null;
231
+ }
232
+ /**
233
+ * Add one conversation's pending callIds to the index (inside the caller's
234
+ * commit; the draft is tracked, so mutation is the write).
235
+ */
236
+ export function indexPending(draft, conversationId, callId) {
237
+ const key = String(conversationId);
238
+ const current = draft.pending[key] ?? [];
239
+ if (!current.includes(callId)) {
240
+ draft.pending[key] = [...current, callId];
241
+ }
242
+ }
243
+ /** Remove one conversation's callId from the index (idempotent). */
244
+ export function indexSettled(draft, conversationId, callId) {
245
+ const key = String(conversationId);
246
+ const current = draft.pending[key];
247
+ if (current === undefined)
248
+ return;
249
+ const next = current.filter((id) => id !== callId);
250
+ if (next.length === 0) {
251
+ const { [key]: _drop, ...rest } = draft.pending;
252
+ draft.pending = rest;
253
+ }
254
+ else if (next.length !== current.length) {
255
+ draft.pending[key] = next;
256
+ }
257
+ }
258
+ /**
259
+ * The committed answer for one record, or `undefined` when the record was
260
+ * already terminal (first-answer-wins: the caller must NOT overwrite).
261
+ * The draft is mutated in place — this runs inside the caller's commit.
262
+ */
263
+ export function applyAnswer(draft, answer, answeredBy, now, answeredByUser) {
264
+ if (draft.status !== "pending")
265
+ return false;
266
+ const clean = {};
267
+ if (answer.ids !== undefined)
268
+ clean.ids = [...answer.ids];
269
+ if (answer.values !== undefined)
270
+ clean.values = { ...answer.values };
271
+ if (answer.text !== undefined)
272
+ clean.text = answer.text;
273
+ const mutable = draft;
274
+ mutable.answer = clean;
275
+ mutable.answeredBy = answeredBy;
276
+ // The answering USER rides alongside the actor class (spec §5.9): who
277
+ // answered matters to every participant a conversation is shared with.
278
+ if (answeredByUser !== undefined)
279
+ mutable.answeredByUser = answeredByUser;
280
+ mutable.status = "answered";
281
+ mutable.settledAt = now;
282
+ return true;
283
+ }