@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,90 @@
1
+ import { z } from "zod";
2
+ import * as S from "effect/Schema";
3
+ import { type ExecutionPlan, type Issue } from "../index.js";
4
+ import type { WorkflowCatalogEntry } from "./ports.js";
5
+ /** A stored publication selected by its admitted publisher App. */
6
+ export declare const workflowPublicationSelectionSchema: z.ZodUnion<readonly [z.ZodObject<{
7
+ workflowId: z.ZodString;
8
+ version: z.ZodNumber;
9
+ digest: z.ZodString & z.ZodType<string & {
10
+ readonly appReleaseKind: "Sha256Digest";
11
+ }, string, z.core.$ZodTypeInternals<string & {
12
+ readonly appReleaseKind: "Sha256Digest";
13
+ }, string>>;
14
+ }, z.core.$strict>, z.ZodObject<{
15
+ workflowId: z.ZodString;
16
+ commit: z.ZodString;
17
+ digest: z.ZodString & z.ZodType<string & {
18
+ readonly appReleaseKind: "Sha256Digest";
19
+ }, string, z.core.$ZodTypeInternals<string & {
20
+ readonly appReleaseKind: "Sha256Digest";
21
+ }, string>>;
22
+ }, z.core.$strict>]>;
23
+ /** Published starts retain the original document and its concrete stored row identity. */
24
+ export declare const graphStartRequestSchema: S.Union<readonly [S.Struct<{
25
+ readonly definition: S.Unknown;
26
+ readonly published: S.optionalKey<S.Never>;
27
+ readonly requestId: S.String;
28
+ readonly bindings: S.declare<Record<string, string | {
29
+ kind: "team-source";
30
+ }>, Record<string, string | {
31
+ kind: "team-source";
32
+ }>>;
33
+ readonly input: S.$Record<S.String, S.Codec<S.Json, S.Json, never, never>>;
34
+ readonly delivery: S.optionalKey<S.Struct<{
35
+ readonly agentId: S.String;
36
+ readonly sessionId: S.String;
37
+ }>>;
38
+ }>, S.Struct<{
39
+ readonly definition: S.Codec<S.Json, S.Json, never, never>;
40
+ readonly published: S.declare<{
41
+ workflowId: string;
42
+ commit: string;
43
+ digest: string & {
44
+ readonly appReleaseKind: "Sha256Digest";
45
+ };
46
+ }, unknown>;
47
+ readonly requestId: S.String;
48
+ readonly bindings: S.declare<Record<string, string | {
49
+ kind: "team-source";
50
+ }>, Record<string, string | {
51
+ kind: "team-source";
52
+ }>>;
53
+ readonly input: S.$Record<S.String, S.Codec<S.Json, S.Json, never, never>>;
54
+ readonly delivery: S.optionalKey<S.Struct<{
55
+ readonly agentId: S.String;
56
+ readonly sessionId: S.String;
57
+ }>>;
58
+ }>]>;
59
+ export type GraphStartRequest = typeof graphStartRequestSchema.Type;
60
+ export type GraphCompileResult = Readonly<{
61
+ kind: "invalid";
62
+ issues: readonly Issue[];
63
+ }> | Readonly<{
64
+ kind: "compiled";
65
+ plan: ExecutionPlan;
66
+ }>;
67
+ /**
68
+ * Compile a definition against the member's live catalog. The App supplies
69
+ * only references to installations; each installation's App id and method
70
+ * digests come from the catalog, so a stale or foreign binding is an issue,
71
+ * never a guess.
72
+ */
73
+ export declare function compileForMember(entries: readonly WorkflowCatalogEntry[], request: Omit<GraphStartRequest, "definition" | "published"> & Readonly<{
74
+ definition: unknown;
75
+ }>): Promise<GraphCompileResult>;
76
+ /**
77
+ * The highest effect any node in the compiled plan uses on each bound
78
+ * installation, subgraphs included (`requiredBindings` already spans every
79
+ * graph). A read-only binding keeps `"read"`; any write or external effect
80
+ * raises it to `"write"`. Pure projection of the plan, never a live grant.
81
+ */
82
+ export declare function planInstallationLevels(plan: Pick<ExecutionPlan, "requiredBindings">): readonly Readonly<{
83
+ installationId: string;
84
+ level: "read" | "write";
85
+ }>[];
86
+ /** One run per (Team, member, App request id); a repeat replays it. */
87
+ export declare function memberRunIdentity(teamId: string, memberId: string, requestId: string): Promise<Readonly<{
88
+ requestId: string;
89
+ runId: string;
90
+ }>>;
@@ -0,0 +1,124 @@
1
+ import { z } from "zod";
2
+ import { bindingsSelectionSchema, workflowBindingSchema, } from "../git-access.js";
3
+ import { gitSha1Schema } from "@fungi.computer/puffball/git-representation";
4
+ import { sha256DigestSchema } from "@fungi.computer/puffball/effect";
5
+ import { workflowIdSchema } from "../workflow-id.js";
6
+ import * as S from "effect/Schema";
7
+ import { compile, contentDigest, workflowDefinitionSchema, } from "../index.js";
8
+ import { workflowRunDeliverySchema } from "./graph-contract.js";
9
+ const id = S.String.pipe(S.check(S.isMinLength(1)), S.check(S.isMaxLength(256)));
10
+ /** A stored publication selected by its admitted publisher App. */
11
+ export const workflowPublicationSelectionSchema = z.union([
12
+ z.strictObject({
13
+ workflowId: workflowIdSchema,
14
+ version: z.number().int().positive(),
15
+ digest: z.string().refine(S.is(sha256DigestSchema)),
16
+ }),
17
+ z.strictObject({
18
+ workflowId: workflowIdSchema,
19
+ commit: gitSha1Schema,
20
+ digest: z.string().refine(S.is(sha256DigestSchema)),
21
+ }),
22
+ ]);
23
+ const startFields = {
24
+ requestId: id,
25
+ /** Canonical installation selections or explicit Team-source namespaces. */
26
+ bindings: S.declare((value) => bindingsSelectionSchema.safeParse(value).success),
27
+ input: S.JsonObject,
28
+ /** Where the run's ending is reported; absent reports nowhere. */
29
+ delivery: S.optionalKey(workflowRunDeliverySchema),
30
+ };
31
+ /** Published starts retain the original document and its concrete stored row identity. */
32
+ export const graphStartRequestSchema = S.Union([
33
+ S.Struct({
34
+ ...startFields,
35
+ definition: S.Unknown,
36
+ published: S.optionalKey(S.Never),
37
+ }),
38
+ S.Struct({
39
+ ...startFields,
40
+ definition: S.Json,
41
+ published: S.declare((value) => workflowPublicationSelectionSchema.options[1].safeParse(value).success),
42
+ }),
43
+ ]);
44
+ /**
45
+ * Compile a definition against the member's live catalog. The App supplies
46
+ * only references to installations; each installation's App id and method
47
+ * digests come from the catalog, so a stale or foreign binding is an issue,
48
+ * never a guess.
49
+ */
50
+ export async function compileForMember(entries, request) {
51
+ const byInstallation = new Map(entries.map((entry) => [entry.installationId, entry]));
52
+ const catalog = {};
53
+ const bindings = {};
54
+ for (const [ref, selection] of Object.entries(request.bindings)) {
55
+ const installation = bindingsSelectionSchema.valueType.options[0].safeParse(selection);
56
+ if (!installation.success) {
57
+ bindings[ref] = workflowBindingSchema.parse(selection);
58
+ continue;
59
+ }
60
+ const installationId = installation.data;
61
+ const entry = byInstallation.get(installationId);
62
+ if (entry === undefined)
63
+ return {
64
+ kind: "invalid",
65
+ issues: [
66
+ {
67
+ code: "binding",
68
+ message: "Installation is not available to you",
69
+ },
70
+ ],
71
+ };
72
+ catalog[entry.appId] =
73
+ entry.events === undefined
74
+ ? { methods: entry.methods }
75
+ : { methods: entry.methods, events: entry.events };
76
+ bindings[ref] = { appId: entry.appId, installationId };
77
+ }
78
+ let definition;
79
+ try {
80
+ definition = S.decodeUnknownSync(workflowDefinitionSchema)(request.definition, { onExcessProperty: "error" });
81
+ }
82
+ catch (cause) {
83
+ if (!S.isSchemaError(cause))
84
+ throw cause;
85
+ return {
86
+ kind: "invalid",
87
+ issues: [{ code: "schema", message: "Definition failed v1 decoding" }],
88
+ };
89
+ }
90
+ const result = await compile(definition, catalog, bindings);
91
+ return result.ok
92
+ ? { kind: "compiled", plan: result.plan }
93
+ : { kind: "invalid", issues: result.issues };
94
+ }
95
+ /**
96
+ * The highest effect any node in the compiled plan uses on each bound
97
+ * installation, subgraphs included (`requiredBindings` already spans every
98
+ * graph). A read-only binding keeps `"read"`; any write or external effect
99
+ * raises it to `"write"`. Pure projection of the plan, never a live grant.
100
+ */
101
+ export function planInstallationLevels(plan) {
102
+ const levels = new Map();
103
+ for (const bound of plan.requiredBindings) {
104
+ if (levels.get(bound.installationId) === "write")
105
+ continue;
106
+ levels.set(bound.installationId, bound.effect === "read" ? "read" : "write");
107
+ }
108
+ return [...levels.entries()]
109
+ .map(([installationId, level]) => ({ installationId, level }))
110
+ .sort((a, b) => a.installationId.localeCompare(b.installationId));
111
+ }
112
+ /** One run per (Team, member, App request id); a repeat replays it. */
113
+ export async function memberRunIdentity(teamId, memberId, requestId) {
114
+ const digest = await contentDigest([
115
+ "fungi.workflow/v1 start",
116
+ teamId,
117
+ memberId,
118
+ requestId,
119
+ ]);
120
+ return {
121
+ requestId: digest,
122
+ runId: "run_" + digest.slice("sha256:".length, 39),
123
+ };
124
+ }
@@ -0,0 +1,20 @@
1
+ export * from "./graph-contract.js";
2
+ export * from "./graph-run-store.js";
3
+ export * from "./graph-dispatch.js";
4
+ export * from "./graph-start.js";
5
+ export * from "./ports.js";
6
+ export * from "./git-pass-contract.js";
7
+ export * from "./git-pass.js";
8
+ export * from "./git-pass-store.js";
9
+ export * from "./bindings.js";
10
+ export * from "./run-authority.js";
11
+ export * from "./run-ceiling.js";
12
+ export * from "./trigger-owner.js";
13
+ export * from "./trigger-plans.js";
14
+ export * from "./trigger-run-reservations.js";
15
+ export * from "./schedules.js";
16
+ export * from "./trigger-router.js";
17
+ export * from "./run-owner.js";
18
+ export * from "./capture.js";
19
+ export * from "./arming-authority.js";
20
+ export * from "./run-report.js";
@@ -0,0 +1,20 @@
1
+ export * from "./graph-contract.js";
2
+ export * from "./graph-run-store.js";
3
+ export * from "./graph-dispatch.js";
4
+ export * from "./graph-start.js";
5
+ export * from "./ports.js";
6
+ export * from "./git-pass-contract.js";
7
+ export * from "./git-pass.js";
8
+ export * from "./git-pass-store.js";
9
+ export * from "./bindings.js";
10
+ export * from "./run-authority.js";
11
+ export * from "./run-ceiling.js";
12
+ export * from "./trigger-owner.js";
13
+ export * from "./trigger-plans.js";
14
+ export * from "./trigger-run-reservations.js";
15
+ export * from "./schedules.js";
16
+ export * from "./trigger-router.js";
17
+ export * from "./run-owner.js";
18
+ export * from "./capture.js";
19
+ export * from "./arming-authority.js";
20
+ export * from "./run-report.js";
@@ -0,0 +1,156 @@
1
+ import type { Pending } from "@fungi.computer/agar/backend-definition";
2
+ import type { ResultRow } from "@fungi.computer/gills";
3
+ import type { RunCeiling } from "./run-ceiling.js";
4
+ import type { Catalog } from "../index.js";
5
+ import type * as Schema from "effect/Schema";
6
+ import type { GraphNodeContext, WorkflowRunContext } from "./graph-contract.js";
7
+ /** One installed App's methods as a member's Workflows may call them. */
8
+ export type WorkflowCatalogEntry = Readonly<{
9
+ installationId: string;
10
+ appId: string;
11
+ displayName: string;
12
+ icon?: string;
13
+ /** Requests declared by the current verified Release, never installation grants. */
14
+ capabilities?: RunCeiling["nodes"][number]["capabilities"];
15
+ /** The host projects its current workflow.publish grant decision, not declaration metadata. */
16
+ workflowPublishGranted?: boolean;
17
+ /** Current workflow.run + approved WORKFLOWS/runGraph host decision; absent refuses. */
18
+ workflowRunGranted?: boolean;
19
+ methods: Catalog[string]["methods"];
20
+ events?: NonNullable<Catalog[string]["events"]>;
21
+ }>;
22
+ /** One graph node's App call, as the run's frozen plan names it. */
23
+ export type WorkflowAppCall = (GraphNodeContext | WorkflowRunContext) & Readonly<{
24
+ installationId: string;
25
+ appId: string;
26
+ method: string;
27
+ methodDigest: string;
28
+ input: Schema.Json;
29
+ /** The run's pinned Computer, if any. */
30
+ computerId: string | null;
31
+ /** Host-captured operations from this stored run, never method input. */
32
+ bindingCeiling: RunCeiling["nodes"][number]["bindings"];
33
+ }>;
34
+ /** The engine's refusal vocabulary (Braid dispatch results). */
35
+ export type WorkflowAppCallResult = Pending | Readonly<{
36
+ kind: "complete";
37
+ output: Schema.Json;
38
+ costMilliunits: number;
39
+ /** How the finished call reads to a person, when the App declares a presentation. */
40
+ row?: ResultRow;
41
+ }> | Readonly<{
42
+ kind: "refused";
43
+ code: "not_granted" | "invalid_input" | "invalid_output" | "revoked" | "unknown_method";
44
+ }> | Readonly<{
45
+ kind: "unavailable";
46
+ }>;
47
+ /** Delivery belongs to the host; saved review state belongs to the runtime. */
48
+ export type ReviewPort = Readonly<{
49
+ notify(review: import("../execution.js").GraphReview): Promise<void>;
50
+ settle(key: Pick<import("../execution.js").GraphReview, "runId" | "nodeId" | "iteration">, state: "answered" | "delivered" | "timed_out" | "closed"): Promise<void>;
51
+ }>;
52
+ /** Host facts are admitted by the host, never decoded from Workflow input. */
53
+ export type NodeInvoker<Facts> = Readonly<{
54
+ invoke(actor: import("@fungi.computer/puffball/app-method-runtime").HostActor, installationId: string, method: string, input: Schema.Json, facts: Facts): Promise<WorkflowAppCallResult>;
55
+ }>;
56
+ /** The host keeps its existing initiator and captured-grant representations. */
57
+ export type RunAuthority<Initiator, Ceiling> = Readonly<{
58
+ resolve(initiator: Initiator): Promise<Readonly<{
59
+ kind: "admitted";
60
+ actor: Extract<import("@fungi.computer/puffball/app-method-runtime").HostActor, {
61
+ kind: "member";
62
+ }>;
63
+ ceiling: Ceiling;
64
+ }> | Readonly<{
65
+ kind: "refused";
66
+ code: "not_granted" | "revoked";
67
+ }>>;
68
+ stillAttached(initiator: Initiator): Promise<boolean>;
69
+ }>;
70
+ /** Shared lifecycle owners use host admission and the host's existing alarm. */
71
+ export type WorkflowLifecyclePorts = Readonly<{
72
+ authorize(memberId: string, installationId: string): Promise<void>;
73
+ wake(due: number | null): Promise<void>;
74
+ commit<A>(body: () => A): Promise<A>;
75
+ }>;
76
+ /** Existing start result; published-selection metadata is supplied by its host. */
77
+ export type GraphStartReply = Readonly<{
78
+ kind: "invalid";
79
+ issues: readonly import("../validate.js").Issue[];
80
+ }> | Readonly<{
81
+ kind: "started" | "replay" | "conflict";
82
+ runId: string;
83
+ planDigest: string;
84
+ }> | Readonly<{
85
+ kind: "refused";
86
+ reason: string;
87
+ }>;
88
+ /** One start port; hosts retain the protected invocation context. */
89
+ export type RunStarter<Context, Request = import("./graph-start.js").GraphStartRequest, Reply = GraphStartReply> = Readonly<{
90
+ start(context: Context, request: Request): Promise<Reply>;
91
+ }>;
92
+ /** An admitted host delivers reports to the originating session, idempotently. */
93
+ export type RunReporter<Initiator> = Readonly<{
94
+ report(initiator: Initiator, runId: string, event: Readonly<{
95
+ kind: "first-review";
96
+ review: import("../execution.js").GraphReview;
97
+ }> | Readonly<{
98
+ kind: "complete";
99
+ output: Schema.Json;
100
+ }> | Readonly<{
101
+ kind: "failed";
102
+ error: Readonly<{
103
+ name: string;
104
+ message: string;
105
+ }>;
106
+ }>): Promise<void>;
107
+ }>;
108
+ /** An immutable published document resolved by its admitted host library. */
109
+ export type PublishedDefinition = Readonly<{
110
+ workflowId: string;
111
+ definition: import("../schema.js").WorkflowDefinition;
112
+ commit: string;
113
+ version: number | null;
114
+ /** Original stored publication digest, validated by the host before compilation. */
115
+ digest: string;
116
+ }>;
117
+ export type WorkflowPublicationSelector = Readonly<{
118
+ workflowId: string;
119
+ }> & (Readonly<{
120
+ version: number;
121
+ }> | Readonly<{
122
+ commit: string;
123
+ }>);
124
+ /** Context holds the host-selected library and access; a saved copy is not admission. */
125
+ export type PublishedDefinitionPort<Context> = Readonly<{
126
+ resolve(context: Context, selector: WorkflowPublicationSelector): Promise<PublishedDefinition | null>;
127
+ }>;
128
+ /** Transient host-issued Git capability; this context is never persisted or guest input. */
129
+ export type WorkflowPublicationContext = Readonly<{
130
+ memberId: string;
131
+ installationId: string;
132
+ computerId: string;
133
+ git: import("@fungi.computer/agar/git").GitBinding;
134
+ }>;
135
+ /** Only the operations the shared trigger owner consumes from the shared run owner. */
136
+ export type WorkflowTriggerRunPorts = Readonly<{
137
+ member(memberId: string): Promise<{
138
+ membershipVersion: number;
139
+ }>;
140
+ memberComputer(memberId: string, computerId: string): Promise<boolean>;
141
+ catalog(memberId: string): Promise<readonly WorkflowCatalogEntry[]>;
142
+ /** Whether this member may have run endings reported to the Agent Session. */
143
+ admitDelivery(memberId: string, delivery: import("./graph-contract.js").WorkflowRunDelivery): Promise<boolean>;
144
+ triggerPlans: ReturnType<typeof import("./trigger-plans.js").createTriggerPlans>;
145
+ startCachedGraph(binding: import("./trigger-owner.js").ArmedWorkflowTrigger, request: Pick<import("./graph-start.js").GraphStartRequest, "requestId" | "input"> & {
146
+ /** The firing event's repository, evaluated from the armed trigger. */
147
+ triggerRepository?: string;
148
+ }): Promise<Exclude<GraphStartReply, {
149
+ kind: "invalid";
150
+ }>>;
151
+ }>;
152
+ /** Hosts retain their existing error vocabulary and denial classification. */
153
+ export type WorkflowErrorPort = Readonly<{
154
+ denied(): Error;
155
+ isDenied(cause: Error): boolean;
156
+ }>;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,60 @@
1
+ import type { NodeAdmissionResult } from "../execution.js";
2
+ import type { ExecutionPlan } from "../index.js";
3
+ import type { HostActor } from "@fungi.computer/puffball/app-method-runtime";
4
+ import type { RunAuthority, WorkflowCatalogEntry } from "./ports.js";
5
+ import type { RunCeiling } from "./run-ceiling.js";
6
+ import type { GraphRun, WorkflowMemberInitiator, WorkflowAgentInitiator, WorkflowKeyInitiator } from "./graph-contract.js";
7
+ export type { WorkflowMemberInitiator, WorkflowAgentInitiator, WorkflowKeyInitiator, };
8
+ /** Host-owned start context after publication resolution and compilation. */
9
+ export type WorkflowRunPreparation = Readonly<{
10
+ plan: ExecutionPlan;
11
+ computerId: string | null;
12
+ }> & (Readonly<{
13
+ initiatedBy: WorkflowMemberInitiator;
14
+ source: "manual" | "schedule" | "event";
15
+ libraryInstallationId?: string;
16
+ /** The firing event's repository; only the trigger owner supplies it. */
17
+ triggerRepository?: string;
18
+ }> | Readonly<{
19
+ initiatedBy: WorkflowAgentInitiator;
20
+ source: "agent";
21
+ libraryInstallationId: string;
22
+ }> | Readonly<{
23
+ initiatedBy: WorkflowKeyInitiator;
24
+ source: "key";
25
+ libraryInstallationId: string;
26
+ }>);
27
+ export type WorkflowRunMember = Readonly<{
28
+ membershipVersion: number;
29
+ }>;
30
+ export type WorkflowRunAuthority = RunAuthority<WorkflowRunPreparation, RunCeiling> & Readonly<{
31
+ recheckRun(preparation: WorkflowRunPreparation): Promise<NodeAdmissionResult>;
32
+ member(memberId: string): Promise<WorkflowRunMember>;
33
+ memberComputer(memberId: string, computerId: string): Promise<boolean>;
34
+ catalog(memberId: string): Promise<readonly WorkflowCatalogEntry[]>;
35
+ }>;
36
+ /** Every check and capture is required; no role, creator or binding default grants access. */
37
+ export type WorkflowRunAuthorityPorts = Readonly<{
38
+ member(memberId: string): Promise<WorkflowRunMember | null>;
39
+ memberComputer(memberId: string, computerId: string): Promise<boolean>;
40
+ catalog(memberId: string): Promise<readonly WorkflowCatalogEntry[]>;
41
+ admitAgent(preparation: Extract<WorkflowRunPreparation, {
42
+ source: "agent";
43
+ }>): Promise<boolean>;
44
+ /** Absent on a host that never admits key-sourced runs; never a default grant. */
45
+ admitKey?(preparation: Extract<WorkflowRunPreparation, {
46
+ source: "key";
47
+ }>): Promise<boolean>;
48
+ capture(preparation: WorkflowRunPreparation, actor: Extract<HostActor, {
49
+ kind: "member";
50
+ }>, member: WorkflowRunMember): Promise<RunCeiling | null>;
51
+ denied(): Error;
52
+ }>;
53
+ /** Shared admission and capture ordering; hosts supply their real human and attachment checks. */
54
+ export declare function createWorkflowRunAuthority(ports: WorkflowRunAuthorityPorts): WorkflowRunAuthority;
55
+ /**
56
+ * Rebuild the preparation a stored run was admitted with, from its recorded
57
+ * `source`/`initiatedBy`, for a later recheck. A run whose source and
58
+ * initiator kind disagree is corrupt, not a recoverable shape.
59
+ */
60
+ export declare function preparationFromRun(run: Pick<GraphRun, "source" | "initiatedBy" | "plan" | "computerId" | "teamId">, libraryInstallationId: string | undefined): WorkflowRunPreparation;
@@ -0,0 +1,145 @@
1
+ import { decodeWorkflowRunMember } from "./run-member.js";
2
+ import { runCeilingSchema } from "./run-ceiling.js";
3
+ import * as S from "effect/Schema";
4
+ import * as Result from "effect/Result";
5
+ /** Shared admission and capture ordering; hosts supply their real human and attachment checks. */
6
+ export function createWorkflowRunAuthority(ports) {
7
+ const readMember = async (id) => decodeWorkflowRunMember(await ports.member(id));
8
+ const member = async (id) => {
9
+ const current = await readMember(id);
10
+ if (current === null)
11
+ throw ports.denied();
12
+ return current;
13
+ };
14
+ const memberComputer = async (id, computerId) => {
15
+ const admitted = await readMember(id);
16
+ if (admitted === null)
17
+ return false;
18
+ const allowed = await ports.memberComputer(id, computerId);
19
+ const current = await readMember(id);
20
+ return allowed && current?.membershipVersion === admitted.membershipVersion;
21
+ };
22
+ const memberId = (preparation) => preparation.initiatedBy.kind === "member"
23
+ ? preparation.initiatedBy.memberId
24
+ : preparation.initiatedBy.grantorMemberId;
25
+ const admitKey = (preparation) => ports.admitKey ? ports.admitKey(preparation) : Promise.resolve(false);
26
+ const stillAttached = async (preparation) => {
27
+ if ((await readMember(memberId(preparation))) === null)
28
+ return false;
29
+ if (preparation.computerId !== null &&
30
+ !(await memberComputer(memberId(preparation), preparation.computerId)))
31
+ return false;
32
+ if (preparation.source === "agent")
33
+ return ports.admitAgent(preparation);
34
+ if (preparation.source === "key")
35
+ return admitKey(preparation);
36
+ return true;
37
+ };
38
+ return {
39
+ async recheckRun(preparation) {
40
+ const id = memberId(preparation);
41
+ const before = await readMember(id);
42
+ if (before === null)
43
+ return { kind: "refused", code: "member_revoked" };
44
+ if (preparation.computerId !== null) {
45
+ const allowed = await ports.memberComputer(id, preparation.computerId);
46
+ const afterComputer = await readMember(id);
47
+ if (afterComputer?.membershipVersion !== before.membershipVersion)
48
+ return { kind: "refused", code: "member_revoked" };
49
+ if (!allowed)
50
+ return { kind: "refused", code: "computer_revoked" };
51
+ }
52
+ if (preparation.source === "agent" &&
53
+ !(await ports.admitAgent(preparation)))
54
+ return { kind: "refused", code: "revoked" };
55
+ if (preparation.source === "key" && !(await admitKey(preparation)))
56
+ return { kind: "refused", code: "revoked" };
57
+ let runGranted = true;
58
+ if (preparation.libraryInstallationId !== undefined) {
59
+ const entries = await ports.catalog(id);
60
+ runGranted = entries.some((entry) => entry.installationId === preparation.libraryInstallationId &&
61
+ entry.workflowRunGranted === true);
62
+ }
63
+ const after = await readMember(id);
64
+ if (after?.membershipVersion !== before.membershipVersion)
65
+ return { kind: "refused", code: "member_revoked" };
66
+ return runGranted
67
+ ? { kind: "admitted" }
68
+ : { kind: "refused", code: "workflow_run_revoked" };
69
+ },
70
+ member,
71
+ memberComputer,
72
+ async catalog(id) {
73
+ const admitted = await member(id);
74
+ const entries = await ports.catalog(id);
75
+ const current = await member(id);
76
+ if (current.membershipVersion !== admitted.membershipVersion)
77
+ throw ports.denied();
78
+ return entries;
79
+ },
80
+ stillAttached,
81
+ async resolve(preparation) {
82
+ const admitted = await readMember(memberId(preparation));
83
+ if (admitted === null)
84
+ return { kind: "refused", code: "not_granted" };
85
+ if (!(await stillAttached(preparation)))
86
+ return { kind: "refused", code: "not_granted" };
87
+ const actor = {
88
+ kind: "member",
89
+ memberId: memberId(preparation),
90
+ };
91
+ const captured = await ports.capture(preparation, actor, admitted);
92
+ const decoded = S.decodeUnknownResult(runCeilingSchema, {
93
+ onExcessProperty: "error",
94
+ })(captured);
95
+ if (Result.isFailure(decoded))
96
+ return { kind: "refused", code: "not_granted" };
97
+ if (!(await stillAttached(preparation)))
98
+ return { kind: "refused", code: "revoked" };
99
+ const current = await readMember(actor.memberId);
100
+ if (current?.membershipVersion !== admitted.membershipVersion)
101
+ return { kind: "refused", code: "revoked" };
102
+ return { kind: "admitted", actor, ceiling: decoded.success };
103
+ },
104
+ };
105
+ }
106
+ /**
107
+ * Rebuild the preparation a stored run was admitted with, from its recorded
108
+ * `source`/`initiatedBy`, for a later recheck. A run whose source and
109
+ * initiator kind disagree is corrupt, not a recoverable shape.
110
+ */
111
+ export function preparationFromRun(run, libraryInstallationId) {
112
+ if (run.source === "agent") {
113
+ if (run.initiatedBy.kind !== "agent")
114
+ throw new Error("Workflow run source does not match its initiator");
115
+ return {
116
+ initiatedBy: run.initiatedBy,
117
+ source: "agent",
118
+ plan: run.plan,
119
+ computerId: run.computerId,
120
+ libraryInstallationId: libraryInstallationId ?? run.teamId,
121
+ };
122
+ }
123
+ if (run.source === "key") {
124
+ if (run.initiatedBy.kind !== "key")
125
+ throw new Error("Workflow run source does not match its initiator");
126
+ return {
127
+ initiatedBy: run.initiatedBy,
128
+ source: "key",
129
+ plan: run.plan,
130
+ computerId: run.computerId,
131
+ libraryInstallationId: libraryInstallationId ?? run.teamId,
132
+ };
133
+ }
134
+ if (run.initiatedBy.kind !== "member")
135
+ throw new Error("Workflow run source does not match its initiator");
136
+ const preparation = {
137
+ initiatedBy: run.initiatedBy,
138
+ source: run.source,
139
+ plan: run.plan,
140
+ computerId: run.computerId,
141
+ };
142
+ return libraryInstallationId === undefined
143
+ ? preparation
144
+ : { ...preparation, libraryInstallationId };
145
+ }