@fungi.computer/braid 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 (83) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +260 -2
  3. package/dist/cloudflare/graph-engine.d.ts +25 -0
  4. package/dist/cloudflare/graph-engine.js +46 -0
  5. package/dist/cloudflare/graph-program.d.ts +6 -0
  6. package/dist/cloudflare/graph-program.js +550 -0
  7. package/dist/cloudflare/index.d.ts +1 -0
  8. package/dist/cloudflare/index.js +1 -0
  9. package/dist/code-execution.d.ts +16 -0
  10. package/dist/code-execution.js +50 -0
  11. package/dist/code-result.d.ts +11 -0
  12. package/dist/code-result.js +19 -0
  13. package/dist/code-sandbox.d.ts +9 -0
  14. package/dist/code-sandbox.js +13 -0
  15. package/dist/compile.d.ts +43 -0
  16. package/dist/compile.js +248 -0
  17. package/dist/execution.d.ts +194 -0
  18. package/dist/execution.js +130 -0
  19. package/dist/expressions.d.ts +16 -0
  20. package/dist/expressions.js +322 -0
  21. package/dist/git-access.d.ts +59 -0
  22. package/dist/git-access.js +106 -0
  23. package/dist/index.d.ts +9 -0
  24. package/dist/index.js +9 -0
  25. package/dist/ports.d.ts +58 -0
  26. package/dist/ports.js +256 -0
  27. package/dist/review-text.d.ts +15 -0
  28. package/dist/review-text.js +40 -0
  29. package/dist/runtime/arming-authority.d.ts +83 -0
  30. package/dist/runtime/arming-authority.js +90 -0
  31. package/dist/runtime/bindings.d.ts +25 -0
  32. package/dist/runtime/bindings.js +41 -0
  33. package/dist/runtime/capture.d.ts +18 -0
  34. package/dist/runtime/capture.js +99 -0
  35. package/dist/runtime/git-pass-contract.d.ts +63 -0
  36. package/dist/runtime/git-pass-contract.js +36 -0
  37. package/dist/runtime/git-pass-store.d.ts +32 -0
  38. package/dist/runtime/git-pass-store.js +82 -0
  39. package/dist/runtime/git-pass.d.ts +41 -0
  40. package/dist/runtime/git-pass.js +72 -0
  41. package/dist/runtime/graph-contract.d.ts +788 -0
  42. package/dist/runtime/graph-contract.js +122 -0
  43. package/dist/runtime/graph-dispatch.d.ts +31 -0
  44. package/dist/runtime/graph-dispatch.js +111 -0
  45. package/dist/runtime/graph-run-store.d.ts +79 -0
  46. package/dist/runtime/graph-run-store.js +267 -0
  47. package/dist/runtime/graph-start.d.ts +90 -0
  48. package/dist/runtime/graph-start.js +124 -0
  49. package/dist/runtime/index.d.ts +20 -0
  50. package/dist/runtime/index.js +20 -0
  51. package/dist/runtime/ports.d.ts +156 -0
  52. package/dist/runtime/ports.js +1 -0
  53. package/dist/runtime/run-authority.d.ts +60 -0
  54. package/dist/runtime/run-authority.js +145 -0
  55. package/dist/runtime/run-ceiling.d.ts +106 -0
  56. package/dist/runtime/run-ceiling.js +23 -0
  57. package/dist/runtime/run-member.d.ts +3 -0
  58. package/dist/runtime/run-member.js +12 -0
  59. package/dist/runtime/run-owner.d.ts +1650 -0
  60. package/dist/runtime/run-owner.js +513 -0
  61. package/dist/runtime/run-report.d.ts +14 -0
  62. package/dist/runtime/run-report.js +23 -0
  63. package/dist/runtime/schedules.d.ts +39 -0
  64. package/dist/runtime/schedules.js +149 -0
  65. package/dist/runtime/stored-trigger-rows.d.ts +12 -0
  66. package/dist/runtime/stored-trigger-rows.js +17 -0
  67. package/dist/runtime/trigger-owner.d.ts +594 -0
  68. package/dist/runtime/trigger-owner.js +280 -0
  69. package/dist/runtime/trigger-plans.d.ts +774 -0
  70. package/dist/runtime/trigger-plans.js +62 -0
  71. package/dist/runtime/trigger-router.d.ts +34 -0
  72. package/dist/runtime/trigger-router.js +143 -0
  73. package/dist/runtime/trigger-run-reservations.d.ts +28 -0
  74. package/dist/runtime/trigger-run-reservations.js +114 -0
  75. package/dist/schema.d.ts +1183 -0
  76. package/dist/schema.js +239 -0
  77. package/dist/triggers.d.ts +46 -0
  78. package/dist/triggers.js +75 -0
  79. package/dist/validate.d.ts +25 -0
  80. package/dist/validate.js +686 -0
  81. package/dist/workflow-id.d.ts +3 -0
  82. package/dist/workflow-id.js +3 -0
  83. package/package.json +96 -3
@@ -0,0 +1,513 @@
1
+ import * as S from "effect/Schema";
2
+ import * as Struct from "effect/Struct";
3
+ import { nodeEventSchema, } from "../execution.js";
4
+ import { z } from "zod";
5
+ import { bindingsSelectionSchema, workflowBindingSchema, } from "../git-access.js";
6
+ import { canonicalJson, contentDigest } from "../index.js";
7
+ import { createTriggerPlans } from "./trigger-plans.js";
8
+ import { preparationFromRun, } from "./run-authority.js";
9
+ import { createTriggerRunReservations } from "./trigger-run-reservations.js";
10
+ import { createGraphWorkflowRuns } from "./graph-run-store.js";
11
+ import { graphRunSchema, graphTriggerProvenanceSchema, workflowRunContextSchema, } from "./graph-contract.js";
12
+ import { resolveWorkflowNode, } from "./graph-dispatch.js";
13
+ import { compileForMember, graphStartRequestSchema, memberRunIdentity, } from "./graph-start.js";
14
+ /** The member can supply a plan, never stored authority or a captured ceiling. */
15
+ export const memberCompiledGraphStartSchema = graphRunSchema.mapFields(Struct.omit([
16
+ "teamId",
17
+ "actorId",
18
+ "trigger",
19
+ "publication",
20
+ "ceiling",
21
+ "libraryInstallationId",
22
+ "source",
23
+ "initiatedBy",
24
+ ]));
25
+ const publisherGranted = (entries, installationId) => entries.some((entry) => entry.installationId === installationId &&
26
+ entry.workflowPublishGranted === true &&
27
+ entry.capabilities?.includes("workflow.publish") === true);
28
+ const workflowNodeTargetSchema = z.lazy(() => z.strictObject({
29
+ installationId: z.string().min(1).max(256),
30
+ method: z.string().min(1).max(256),
31
+ }));
32
+ /**
33
+ * The one Workflow run owner: graph runs, their captured authority, trigger
34
+ * reservations and revocation. Every run, Storyboard included, is a graph run.
35
+ */
36
+ export function createTeamWorkflowRuns(options) {
37
+ const { sql } = options;
38
+ const graph = createGraphWorkflowRuns(options);
39
+ const triggerReservations = createTriggerRunReservations(options);
40
+ sql.exec(`CREATE TABLE IF NOT EXISTS hub_workflow_run_cleanups (
41
+ run_id TEXT PRIMARY KEY REFERENCES hub_graph_runs(run_id), computer_id TEXT NOT NULL,
42
+ acknowledged INTEGER NOT NULL DEFAULT 0 CHECK(acknowledged IN (0,1))
43
+ ) STRICT`);
44
+ const triggerPlans = createTriggerPlans(sql);
45
+ const admitGraphMember = async (memberId, computerId, delivery) => {
46
+ if (options.admission() !== "active")
47
+ throw options.errors.suspended();
48
+ await options.member(memberId);
49
+ if (computerId !== null &&
50
+ !(await options.memberComputer(memberId, computerId)))
51
+ throw options.errors.app("denied");
52
+ if (delivery !== undefined &&
53
+ !(await options.admitDelivery(memberId, delivery)))
54
+ throw options.errors.app("denied");
55
+ };
56
+ /** A report names its Workflows library, so only a library run may declare one. */
57
+ const requireDeliveryLibrary = (delivery, libraryInstallationId) => {
58
+ if (delivery !== undefined && libraryInstallationId === undefined)
59
+ throw options.errors.app("invalid");
60
+ };
61
+ const captureGraph = async (preparation) => {
62
+ if (options.admission() !== "active")
63
+ throw options.errors.suspended();
64
+ const admitted = await options.authority.resolve(preparation);
65
+ const memberId = preparation.initiatedBy.kind === "member"
66
+ ? preparation.initiatedBy.memberId
67
+ : preparation.initiatedBy.grantorMemberId;
68
+ if (admitted.kind !== "admitted" || admitted.actor.memberId !== memberId)
69
+ throw options.errors.app("denied");
70
+ if (options.admission() !== "active")
71
+ throw options.errors.suspended();
72
+ return admitted.ceiling;
73
+ };
74
+ const startCompiled = (preparation, request, identity, ceiling, trigger, publication, libraryInstallationId) => {
75
+ const memberId = preparation.initiatedBy.kind === "member"
76
+ ? preparation.initiatedBy.memberId
77
+ : preparation.initiatedBy.grantorMemberId;
78
+ let run = {
79
+ teamId: options.teamId(),
80
+ actorId: memberId,
81
+ runId: identity.runId,
82
+ requestId: identity.requestId,
83
+ computerId: preparation.computerId,
84
+ plan: preparation.plan,
85
+ input: request.input,
86
+ ceiling,
87
+ source: preparation.source,
88
+ initiatedBy: preparation.initiatedBy,
89
+ };
90
+ if (trigger !== undefined) {
91
+ const provenance = S.decodeUnknownSync(graphTriggerProvenanceSchema)(trigger, { onExcessProperty: "error" });
92
+ if (provenance.armedBy !== memberId)
93
+ throw options.errors.app("denied");
94
+ run = { ...run, trigger: provenance };
95
+ }
96
+ if (publication !== undefined)
97
+ run = { ...run, publication };
98
+ if (libraryInstallationId !== undefined)
99
+ run = { ...run, libraryInstallationId };
100
+ if (request.delivery !== undefined)
101
+ run = { ...run, delivery: request.delivery };
102
+ const result = graph.startCompiled(run);
103
+ return {
104
+ kind: result,
105
+ runId: identity.runId,
106
+ planDigest: preparation.plan.planDigest,
107
+ };
108
+ };
109
+ const terminalTriggerRun = (rawRunId, outcome = "terminal") => {
110
+ const runId = z.string().min(1).max(256).parse(rawRunId);
111
+ options.transactionSync(() => {
112
+ const run = graph.read(runId);
113
+ if (run === null)
114
+ return;
115
+ graph.markTerminal(runId, outcome);
116
+ triggerReservations.terminalTriggerRun(runId);
117
+ if (run.computerId !== null)
118
+ sql.exec("INSERT OR IGNORE INTO hub_workflow_run_cleanups(run_id,computer_id) VALUES(?,?)", runId, run.computerId);
119
+ });
120
+ };
121
+ /** Every Installation a stored run acts through: plan bindings and its library. */
122
+ const runInstallations = (run) => new Set([
123
+ ...Object.values(run.plan.bindings).map((binding) => "installationId" in binding ? binding.installationId : undefined),
124
+ run.libraryInstallationId,
125
+ run.publication?.installationId,
126
+ run.trigger?.libraryInstallationId,
127
+ ].filter((id) => id !== undefined));
128
+ /**
129
+ * Cancel matching active runs in one transaction: reviews close, trigger
130
+ * reservations settle and Computer cleanup is queued. Every later node
131
+ * admission, review answer and Git pass for the run then refuses.
132
+ */
133
+ const revoke = (matches) => {
134
+ const revoked = options.transactionSync(() => {
135
+ const runs = graph.active().filter(matches);
136
+ for (const run of runs)
137
+ terminalTriggerRun(run.runId, "cancelled");
138
+ return runs.length;
139
+ });
140
+ if (revoked > 0)
141
+ options.wake();
142
+ };
143
+ const workflowRunOutcome = (rawRunId) => graph.outcome(z.string().min(1).max(256).parse(rawRunId));
144
+ const admitWorkflowNode = (rawContext, rawTarget) => {
145
+ const context = S.decodeUnknownSync(workflowRunContextSchema)(rawContext, {
146
+ onExcessProperty: "error",
147
+ });
148
+ const target = workflowNodeTargetSchema.parse(rawTarget);
149
+ const run = graph.read(context.runId);
150
+ if (options.admission() !== "active" ||
151
+ run?.teamId !== options.teamId() ||
152
+ workflowRunOutcome(context.runId) !== "active")
153
+ return null;
154
+ const resolved = resolveWorkflowNode(run, { ...context, ...target });
155
+ if (resolved === null || !("workflowId" in resolved))
156
+ return null;
157
+ const { installationId, appId, method, methodDigest, computerId } = resolved;
158
+ // Host-only call metadata, including the captured ceiling, is not invocation context.
159
+ const storedContext = S.decodeUnknownSync(workflowRunContextSchema)(resolved);
160
+ if (canonicalJson(context) !== canonicalJson(storedContext))
161
+ return null;
162
+ const execution = graph
163
+ .events(run.runId)
164
+ .slice()
165
+ .reverse()
166
+ .find((event) => event.graphId === context.graphId &&
167
+ event.nodeId === context.nodeId &&
168
+ event.iteration === context.iteration);
169
+ if (execution?.state !== "running" || execution.poll !== context.poll)
170
+ return null;
171
+ return {
172
+ teamId: run.teamId,
173
+ memberId: run.actorId,
174
+ computerId,
175
+ installationId,
176
+ appId,
177
+ method,
178
+ methodDigest,
179
+ context: storedContext,
180
+ };
181
+ };
182
+ const cancellations = new Map();
183
+ return {
184
+ graph,
185
+ /** Every node kind rechecks the stored run; completion cleanup stays with the root engine. */
186
+ async recheckGraphNode(raw) {
187
+ const event = S.decodeUnknownSync(nodeEventSchema)(raw, {
188
+ onExcessProperty: "error",
189
+ });
190
+ const run = graph.read(event.runId);
191
+ if (run?.teamId !== options.teamId() ||
192
+ options.admission() !== "active" ||
193
+ graph.outcome(event.runId) !== "active")
194
+ return { kind: "refused", code: "revoked" };
195
+ const definition = event.graphId === undefined
196
+ ? run.plan.definition
197
+ : run.plan.definition.subgraphs?.[event.graphId];
198
+ if (event.state !== "running" ||
199
+ !definition?.nodes.some((node) => node.id === event.nodeId))
200
+ return { kind: "refused", code: "unknown_method" };
201
+ const libraryInstallationId = run.publication?.installationId ??
202
+ run.libraryInstallationId ??
203
+ run.trigger?.libraryInstallationId;
204
+ const preparation = preparationFromRun(run, libraryInstallationId);
205
+ const decision = await options.authority.recheckRun(preparation);
206
+ if (decision.kind === "refused")
207
+ graph.recordEvent({
208
+ ...event,
209
+ state: "failed",
210
+ refusal: decision.code,
211
+ errorMessage: decision.code.replaceAll("_", " "),
212
+ });
213
+ return decision;
214
+ },
215
+ /** Recompile the member plan against live catalog, then capture before storage. */
216
+ async startMemberGraph(memberId, raw, libraryInstallationId) {
217
+ const request = S.decodeUnknownSync(memberCompiledGraphStartSchema)(raw, {
218
+ onExcessProperty: "error",
219
+ });
220
+ requireDeliveryLibrary(request.delivery, libraryInstallationId);
221
+ await admitGraphMember(memberId, request.computerId, request.delivery);
222
+ const entries = await options.catalog(memberId);
223
+ const bindings = Object.fromEntries(Object.entries(request.plan.bindings).map(([name, binding]) => [
224
+ name,
225
+ "installationId" in binding ? binding.installationId : binding,
226
+ ]));
227
+ const compiled = await compileForMember(entries, {
228
+ requestId: request.requestId,
229
+ definition: request.plan.definition,
230
+ bindings,
231
+ input: request.input,
232
+ });
233
+ if (compiled.kind !== "compiled" ||
234
+ canonicalJson(compiled.plan) !== canonicalJson(request.plan))
235
+ throw options.errors.app("denied");
236
+ let preparation = {
237
+ initiatedBy: { kind: "member", memberId },
238
+ source: "manual",
239
+ plan: compiled.plan,
240
+ computerId: request.computerId,
241
+ };
242
+ if (libraryInstallationId !== undefined)
243
+ preparation = { ...preparation, libraryInstallationId };
244
+ const ceiling = await captureGraph(preparation);
245
+ let run = {
246
+ ...request,
247
+ plan: compiled.plan,
248
+ teamId: options.teamId(),
249
+ actorId: memberId,
250
+ ceiling,
251
+ source: preparation.source,
252
+ initiatedBy: preparation.initiatedBy,
253
+ };
254
+ if (libraryInstallationId !== undefined)
255
+ run = { ...run, libraryInstallationId };
256
+ return graph.startCompiled(run);
257
+ },
258
+ ...triggerReservations,
259
+ workflowRunOutcome,
260
+ admitWorkflowNode,
261
+ /** Up to 50 runs of one installation, newest first; `actorId` narrows to one viewer. */
262
+ listRuns(installationId, actorId, workflowId) {
263
+ let filter = { installationId };
264
+ if (actorId !== undefined)
265
+ filter = { ...filter, actorId };
266
+ if (workflowId !== undefined)
267
+ filter = { ...filter, workflowId };
268
+ return graph.list(filter);
269
+ },
270
+ bindTriggerRun(reservationId, rawRunId) {
271
+ const runId = z.string().min(1).max(256).parse(rawRunId);
272
+ if (graph.read(runId) === null)
273
+ return "conflict";
274
+ return options.transactionSync(() => {
275
+ const result = triggerReservations.bindTriggerRun(reservationId, runId);
276
+ if (result !== "conflict" && graph.outcome(runId) !== "active")
277
+ triggerReservations.terminalTriggerRun(runId);
278
+ return result;
279
+ });
280
+ },
281
+ terminalTriggerRun,
282
+ async cancel(memberId, rawRunId) {
283
+ const runId = z.string().min(1).max(256).parse(rawRunId);
284
+ await options.member(memberId);
285
+ const run = graph.read(runId);
286
+ if (run === null ||
287
+ (run.actorId !== memberId &&
288
+ !(await options.reviewer(memberId)).isOwner))
289
+ return { kind: "refused", reason: "denied" };
290
+ const prior = graph.outcome(runId);
291
+ if (prior === "terminal" || prior === "cancelled")
292
+ return { kind: prior };
293
+ const pending = cancellations.get(runId);
294
+ if (pending !== undefined)
295
+ return pending;
296
+ const cancellation = (async () => {
297
+ const outcome = await options.terminateGraphRun(runId);
298
+ terminalTriggerRun(runId, outcome);
299
+ return {
300
+ kind: graph.outcome(runId) === "cancelled"
301
+ ? "cancelled"
302
+ : "terminal",
303
+ };
304
+ })();
305
+ cancellations.set(runId, cancellation);
306
+ try {
307
+ return await cancellation;
308
+ }
309
+ finally {
310
+ cancellations.delete(runId);
311
+ }
312
+ },
313
+ pendingWorkflowRunCleanups() {
314
+ return sql
315
+ .exec("SELECT run_id,computer_id FROM hub_workflow_run_cleanups WHERE acknowledged=0 ORDER BY run_id LIMIT 100")
316
+ .toArray()
317
+ .map((raw) => {
318
+ const row = z
319
+ .object({ run_id: z.string(), computer_id: z.string() })
320
+ .parse(raw);
321
+ return {
322
+ teamId: options.teamId(),
323
+ runId: row.run_id,
324
+ computerId: row.computer_id,
325
+ };
326
+ });
327
+ },
328
+ /** Delivery consumer calls this only after durable Computer revocation resolves. */
329
+ acknowledgeWorkflowRunCleanup(rawRunId) {
330
+ const runId = z.string().min(1).max(256).parse(rawRunId);
331
+ options.transactionSync(() => {
332
+ sql.exec("UPDATE hub_workflow_run_cleanups SET acknowledged=1 WHERE run_id=?", runId);
333
+ });
334
+ },
335
+ triggerPlans,
336
+ /** Member-started generic graphs only; trigger starts use startCachedGraph instead. */
337
+ async startGraph(memberId, raw, computerId, libraryInstallationId) {
338
+ const request = S.decodeUnknownSync(graphStartRequestSchema)(raw, {
339
+ onExcessProperty: "error",
340
+ });
341
+ requireDeliveryLibrary(request.delivery, libraryInstallationId);
342
+ await admitGraphMember(memberId, computerId, request.delivery);
343
+ const entries = await options.catalog(memberId);
344
+ let publication;
345
+ if (request.published !== undefined) {
346
+ if (libraryInstallationId === undefined ||
347
+ !publisherGranted(entries, libraryInstallationId))
348
+ throw options.errors.app("denied");
349
+ // The admitted publisher supplies its original immutable row, not a compiled plan.
350
+ publication = {
351
+ installationId: libraryInstallationId,
352
+ workflowId: request.published.workflowId,
353
+ commit: request.published.commit,
354
+ digest: request.published.digest,
355
+ armedBy: { kind: "member", memberId },
356
+ };
357
+ if (publication.digest !==
358
+ (await contentDigest(S.decodeUnknownSync(S.Json)(request.definition))))
359
+ return {
360
+ kind: "invalid",
361
+ issues: [
362
+ {
363
+ code: "schema",
364
+ message: "Published definition digest does not match",
365
+ },
366
+ ],
367
+ };
368
+ }
369
+ const compiled = await compileForMember(entries, request);
370
+ if (compiled.kind === "invalid")
371
+ return compiled;
372
+ const identity = await memberRunIdentity(options.teamId(), memberId, request.requestId);
373
+ let preparation = {
374
+ initiatedBy: { kind: "member", memberId },
375
+ source: "manual",
376
+ computerId,
377
+ plan: compiled.plan,
378
+ };
379
+ if (libraryInstallationId !== undefined)
380
+ preparation = { ...preparation, libraryInstallationId };
381
+ const ceiling = await captureGraph(preparation);
382
+ if (publication !== undefined &&
383
+ !publisherGranted(await options.catalog(memberId), publication.installationId))
384
+ throw options.errors.app("denied");
385
+ return startCompiled(preparation, request, identity, ceiling, undefined, publication, libraryInstallationId);
386
+ },
387
+ async startCachedGraph(binding, started) {
388
+ const { triggerRepository, ...request } = started;
389
+ await admitGraphMember(binding.armedBy, binding.computerId);
390
+ const stored = triggerPlans.read(binding);
391
+ if (stored?.refusal !== null)
392
+ return {
393
+ kind: "refused",
394
+ reason: stored?.refusal ?? "Compiled Workflow unavailable; re-arm required",
395
+ };
396
+ const entries = await options.catalog(binding.armedBy);
397
+ if (!publisherGranted(entries, binding.installationId))
398
+ return {
399
+ kind: "refused",
400
+ reason: "Workflow publisher is no longer available; re-arm required",
401
+ };
402
+ if (Object.values(binding.bindings).some((selection) => {
403
+ const installation = bindingsSelectionSchema.valueType.options[0].safeParse(selection);
404
+ return (installation.success &&
405
+ !entries.some((entry) => entry.installationId === installation.data));
406
+ }))
407
+ return {
408
+ kind: "refused",
409
+ reason: "Workflow is no longer valid for this member",
410
+ };
411
+ for (const selectedBinding of Object.values(stored.plan.bindings)) {
412
+ // The closed namespace variant is admitted by live Git capture below.
413
+ if (workflowBindingSchema.options[1].safeParse(selectedBinding).success)
414
+ continue;
415
+ if (!("installationId" in selectedBinding))
416
+ return {
417
+ kind: "refused",
418
+ reason: "Workflow is no longer valid for this member",
419
+ };
420
+ const { appId, installationId } = selectedBinding;
421
+ const current = entries.find((entry) => entry.installationId === installationId && entry.appId === appId);
422
+ const pinned = stored.plan.catalog[appId];
423
+ if (current === undefined ||
424
+ pinned === undefined ||
425
+ Object.entries(pinned.methods).some(([name, method]) => current.methods[name]?.digest !== method.digest) ||
426
+ Object.entries(pinned.events ?? {}).some(([name, event]) => current.events?.[name]?.digest !== event.digest))
427
+ return {
428
+ kind: "refused",
429
+ reason: "Workflow is no longer valid for this member",
430
+ };
431
+ }
432
+ const identity = await memberRunIdentity(options.teamId(), binding.armedBy, request.requestId);
433
+ let preparation = {
434
+ initiatedBy: { kind: "member", memberId: binding.armedBy },
435
+ source: stored.plan.definition.triggers?.find((trigger) => trigger.id === binding.triggerId)?.on === "schedule"
436
+ ? "schedule"
437
+ : "event",
438
+ libraryInstallationId: binding.installationId,
439
+ computerId: binding.computerId,
440
+ plan: stored.plan,
441
+ };
442
+ if (triggerRepository !== undefined)
443
+ preparation = { ...preparation, triggerRepository };
444
+ const ceiling = await captureGraph(preparation);
445
+ if (!publisherGranted(await options.catalog(binding.armedBy), binding.installationId))
446
+ return {
447
+ kind: "refused",
448
+ reason: "Workflow publisher is no longer available; re-arm required",
449
+ };
450
+ // Re-read after live admission/capture I/O: disarm or push invalidation wins.
451
+ const current = triggerPlans.read(binding);
452
+ if (current?.refusal !== null ||
453
+ canonicalJson(current.plan) !== canonicalJson(stored.plan))
454
+ return {
455
+ kind: "refused",
456
+ reason: current?.refusal ??
457
+ "Compiled Workflow unavailable; re-arm required",
458
+ };
459
+ let trigger = {
460
+ libraryInstallationId: binding.installationId,
461
+ workflowId: binding.workflowId,
462
+ commit: binding.commit,
463
+ triggerId: binding.triggerId,
464
+ armedBy: binding.armedBy,
465
+ };
466
+ if (binding.nodeGitAccess !== undefined)
467
+ trigger = { ...trigger, nodeGitAccess: binding.nodeGitAccess };
468
+ if (triggerRepository !== undefined)
469
+ trigger = { ...trigger, repository: triggerRepository };
470
+ return startCompiled(preparation, binding.delivery === undefined
471
+ ? request
472
+ : { ...request, delivery: binding.delivery }, identity, ceiling, trigger, {
473
+ installationId: binding.installationId,
474
+ workflowId: binding.workflowId,
475
+ commit: binding.commit,
476
+ digest: binding.digest,
477
+ armedBy: { kind: "member", memberId: binding.armedBy },
478
+ }, binding.installationId);
479
+ },
480
+ callWorkflowApp: options.callWorkflowApp,
481
+ admitDelivery: options.admitDelivery,
482
+ memberComputer: options.memberComputer,
483
+ catalog: options.catalog,
484
+ reviewer: options.reviewer,
485
+ member: options.member,
486
+ /** A removed member's runs end now; node admission would refuse them anyway. */
487
+ revokeMember(rawMemberId) {
488
+ const memberId = z.string().min(1).max(256).parse(rawMemberId);
489
+ revoke((run) => run.actorId === memberId);
490
+ },
491
+ /** Apps calls this with its permission change inside the shared transaction. */
492
+ revokeInstallationInMutation(rawInstallationId) {
493
+ const installationId = z
494
+ .string()
495
+ .min(1)
496
+ .max(256)
497
+ .parse(rawInstallationId);
498
+ revoke((run) => runInstallations(run).has(installationId));
499
+ },
500
+ /** A revoked Release ends every run whose frozen plan binds an affected App. */
501
+ revokeReleaseInMutation(appIds) {
502
+ const affected = new Set(z.array(z.string().min(1).max(256)).parse(appIds));
503
+ if (affected.size === 0)
504
+ return;
505
+ revoke((run) => Object.values(run.plan.bindings).some((binding) => "appId" in binding && affected.has(binding.appId)));
506
+ },
507
+ /** The kernel's lifecycle caller owns the transaction. Suspension ends every run. */
508
+ onLifecycle(status) {
509
+ if (status === "suspended")
510
+ revoke(() => true);
511
+ },
512
+ };
513
+ }
@@ -0,0 +1,14 @@
1
+ import * as S from "effect/Schema";
2
+ /** How a run ended, as its host reports it; decoded where it crosses a door. */
3
+ export declare const workflowRunEndingSchema: S.Union<readonly [S.Struct<{
4
+ readonly kind: S.Literal<"complete">;
5
+ readonly output: S.Codec<S.Json, S.Json, never, never>;
6
+ }>, S.Struct<{
7
+ readonly kind: S.Literal<"failed">;
8
+ readonly error: S.String;
9
+ }>, S.Struct<{
10
+ readonly kind: S.Literal<"cancelled">;
11
+ }>]>;
12
+ export type WorkflowRunEnding = typeof workflowRunEndingSchema.Type;
13
+ /** The follow-up the run's declared delivery Session receives, on every host. */
14
+ export declare function workflowRunReport(title: string, runId: string, ending: WorkflowRunEnding): string;
@@ -0,0 +1,23 @@
1
+ import * as S from "effect/Schema";
2
+ /** How a run ended, as its host reports it; decoded where it crosses a door. */
3
+ export const workflowRunEndingSchema = S.Union([
4
+ S.Struct({ kind: S.Literal("complete"), output: S.Json }),
5
+ S.Struct({ kind: S.Literal("failed"), error: S.String }),
6
+ S.Struct({ kind: S.Literal("cancelled") }),
7
+ ]);
8
+ /** A Session message stays readable; the full output remains in runStatus. */
9
+ const OUTPUT_PREVIEW_CHARS = 2_000;
10
+ const ERROR_CHARS = 500;
11
+ const clip = (text, limit) => text.length <= limit ? text : `${text.slice(0, limit)}…`;
12
+ /** The follow-up the run's declared delivery Session receives, on every host. */
13
+ export function workflowRunReport(title, runId, ending) {
14
+ const name = `Workflow "${title}" (run ${runId})`;
15
+ switch (ending.kind) {
16
+ case "complete":
17
+ return `${name} finished.\nResult:\n${clip(JSON.stringify(ending.output, null, 2), OUTPUT_PREVIEW_CHARS)}`;
18
+ case "failed":
19
+ return `${name} failed: ${clip(ending.error, ERROR_CHARS)}`;
20
+ case "cancelled":
21
+ return `${name} was cancelled.`;
22
+ }
23
+ }
@@ -0,0 +1,39 @@
1
+ import type { WatchdogSqliteOwner } from "@fungi.computer/watchdog";
2
+ import { type WorkflowArmer, type WorkflowInstallationAuthority, type WorkflowTriggerPrincipal, type WorkflowTriggerTarget, type createTeamWorkflowTriggers } from "./trigger-owner.js";
3
+ /** Team-owned schedule records; Almanac alone decides occurrences and transitions. */
4
+ export declare function createWorkflowSchedules(options: {
5
+ sql: WatchdogSqliteOwner["sql"];
6
+ triggers: ReturnType<typeof createTeamWorkflowTriggers>;
7
+ teamId: () => string;
8
+ now: () => number;
9
+ commit: <A>(body: () => A) => Promise<A>;
10
+ authorize: (memberId: string, installationId: string) => Promise<void>;
11
+ wake: (due: number | null) => Promise<void>;
12
+ reportFailure: (failure: Readonly<{
13
+ source: "workflow_schedule_fire";
14
+ cause: unknown;
15
+ }>) => void;
16
+ }): {
17
+ nextWake: () => number | null;
18
+ arm(armer: WorkflowArmer, target: WorkflowTriggerTarget, computerId: string, installationAuthority: WorkflowInstallationAuthority): Promise<{
19
+ kind: "refused";
20
+ reason: string;
21
+ } | {
22
+ kind: "armed";
23
+ scheduleId: string;
24
+ }>;
25
+ list(principal: WorkflowTriggerPrincipal, installationId: string): Promise<{
26
+ scheduleId: string;
27
+ workflowId: string;
28
+ triggerId: string;
29
+ nextDueAt: number;
30
+ state: "active" | "finished" | "paused" | "cancelled";
31
+ lastError: string | null;
32
+ }[]>;
33
+ disarm(principal: WorkflowTriggerPrincipal, installationId: string, scheduleId: string): Promise<{
34
+ kind: "not_found";
35
+ } | {
36
+ kind: "disarmed";
37
+ }>;
38
+ fireDue(): Promise<void>;
39
+ };