@fungi.computer/braid 0.0.0-stage → 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +260 -2
- package/dist/cloudflare/graph-engine.d.ts +25 -0
- package/dist/cloudflare/graph-engine.js +46 -0
- package/dist/cloudflare/graph-program.d.ts +6 -0
- package/dist/cloudflare/graph-program.js +528 -0
- package/dist/cloudflare/index.d.ts +1 -0
- package/dist/cloudflare/index.js +1 -0
- package/dist/code-execution.d.ts +16 -0
- package/dist/code-execution.js +50 -0
- package/dist/code-result.d.ts +11 -0
- package/dist/code-result.js +19 -0
- package/dist/code-sandbox.d.ts +9 -0
- package/dist/code-sandbox.js +13 -0
- package/dist/compile.d.ts +41 -0
- package/dist/compile.js +240 -0
- package/dist/execution.d.ts +113 -0
- package/dist/execution.js +108 -0
- package/dist/expressions.d.ts +16 -0
- package/dist/expressions.js +322 -0
- package/dist/git-access.d.ts +59 -0
- package/dist/git-access.js +106 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +8 -0
- package/dist/ports.d.ts +58 -0
- package/dist/ports.js +256 -0
- package/dist/runtime/arming-authority.d.ts +83 -0
- package/dist/runtime/arming-authority.js +90 -0
- package/dist/runtime/bindings.d.ts +25 -0
- package/dist/runtime/bindings.js +41 -0
- package/dist/runtime/capture.d.ts +18 -0
- package/dist/runtime/capture.js +99 -0
- package/dist/runtime/git-pass-contract.d.ts +63 -0
- package/dist/runtime/git-pass-contract.js +36 -0
- package/dist/runtime/git-pass-store.d.ts +32 -0
- package/dist/runtime/git-pass-store.js +82 -0
- package/dist/runtime/git-pass.d.ts +41 -0
- package/dist/runtime/git-pass.js +72 -0
- package/dist/runtime/graph-contract.d.ts +784 -0
- package/dist/runtime/graph-contract.js +122 -0
- package/dist/runtime/graph-dispatch.d.ts +31 -0
- package/dist/runtime/graph-dispatch.js +111 -0
- package/dist/runtime/graph-run-store.d.ts +73 -0
- package/dist/runtime/graph-run-store.js +267 -0
- package/dist/runtime/graph-start.d.ts +90 -0
- package/dist/runtime/graph-start.js +124 -0
- package/dist/runtime/index.d.ts +20 -0
- package/dist/runtime/index.js +20 -0
- package/dist/runtime/ports.d.ts +153 -0
- package/dist/runtime/ports.js +1 -0
- package/dist/runtime/run-authority.d.ts +60 -0
- package/dist/runtime/run-authority.js +145 -0
- package/dist/runtime/run-ceiling.d.ts +106 -0
- package/dist/runtime/run-ceiling.js +23 -0
- package/dist/runtime/run-member.d.ts +3 -0
- package/dist/runtime/run-member.js +12 -0
- package/dist/runtime/run-owner.d.ts +1628 -0
- package/dist/runtime/run-owner.js +513 -0
- package/dist/runtime/run-report.d.ts +14 -0
- package/dist/runtime/run-report.js +23 -0
- package/dist/runtime/schedules.d.ts +39 -0
- package/dist/runtime/schedules.js +149 -0
- package/dist/runtime/stored-trigger-rows.d.ts +12 -0
- package/dist/runtime/stored-trigger-rows.js +17 -0
- package/dist/runtime/trigger-owner.d.ts +590 -0
- package/dist/runtime/trigger-owner.js +280 -0
- package/dist/runtime/trigger-plans.d.ts +766 -0
- package/dist/runtime/trigger-plans.js +62 -0
- package/dist/runtime/trigger-router.d.ts +34 -0
- package/dist/runtime/trigger-router.js +143 -0
- package/dist/runtime/trigger-run-reservations.d.ts +28 -0
- package/dist/runtime/trigger-run-reservations.js +114 -0
- package/dist/schema.d.ts +1111 -0
- package/dist/schema.js +227 -0
- package/dist/triggers.d.ts +46 -0
- package/dist/triggers.js +75 -0
- package/dist/validate.d.ts +25 -0
- package/dist/validate.js +679 -0
- package/dist/workflow-id.d.ts +3 -0
- package/dist/workflow-id.js +3 -0
- package/package.json +95 -3
package/dist/ports.js
ADDED
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import * as S from "effect/Schema";
|
|
2
|
+
import { Validator, escapePointer, format } from "@cfworker/json-schema";
|
|
3
|
+
const portSchema = S.Struct({
|
|
4
|
+
type: S.optionalKey(S.Literals([
|
|
5
|
+
"string",
|
|
6
|
+
"number",
|
|
7
|
+
"integer",
|
|
8
|
+
"boolean",
|
|
9
|
+
"array",
|
|
10
|
+
"object",
|
|
11
|
+
"null",
|
|
12
|
+
])),
|
|
13
|
+
format: S.optionalKey(S.String.pipe(S.check(S.makeFilter((value) => Object.hasOwn(format, value) ||
|
|
14
|
+
value === "fungi:image" ||
|
|
15
|
+
value === "fungi:video" ||
|
|
16
|
+
value === "fungi:file")))),
|
|
17
|
+
title: S.optionalKey(S.String),
|
|
18
|
+
description: S.optionalKey(S.String),
|
|
19
|
+
const: S.optionalKey(S.Json),
|
|
20
|
+
default: S.optionalKey(S.Json),
|
|
21
|
+
pattern: S.optionalKey(S.String),
|
|
22
|
+
anyOf: S.optionalKey(S.mutable(S.Array(S.suspend(() => portSchema))).pipe(S.check(S.isMinLength(1)))),
|
|
23
|
+
oneOf: S.optionalKey(S.mutable(S.Array(S.suspend(() => portSchema))).pipe(S.check(S.isMinLength(1)))),
|
|
24
|
+
enum: S.optionalKey(S.mutable(S.Array(S.Json)).pipe(S.check(S.isMinLength(1)), S.check(S.makeFilter((values) => new Set(values.map((value) => JSON.stringify(value))).size ===
|
|
25
|
+
values.length)))),
|
|
26
|
+
properties: S.optionalKey(S.Record(S.String, S.suspend(() => portSchema))),
|
|
27
|
+
required: S.optionalKey(S.mutable(S.Array(S.String)).pipe(S.check(S.makeFilter((values) => new Set(values).size === values.length)))),
|
|
28
|
+
additionalProperties: S.optionalKey(S.Boolean),
|
|
29
|
+
items: S.optionalKey(S.suspend(() => portSchema)),
|
|
30
|
+
minItems: S.optionalKey(S.Number.pipe(S.check(S.isInt()), S.check(S.isGreaterThanOrEqualTo(0)))),
|
|
31
|
+
maxItems: S.optionalKey(S.Number.pipe(S.check(S.isInt()), S.check(S.isGreaterThanOrEqualTo(0)))),
|
|
32
|
+
minLength: S.optionalKey(S.Number.pipe(S.check(S.isInt()), S.check(S.isGreaterThanOrEqualTo(0)))),
|
|
33
|
+
maxLength: S.optionalKey(S.Number.pipe(S.check(S.isInt()), S.check(S.isGreaterThanOrEqualTo(0)))),
|
|
34
|
+
minimum: S.optionalKey(S.Number),
|
|
35
|
+
maximum: S.optionalKey(S.Number),
|
|
36
|
+
exclusiveMinimum: S.optionalKey(S.Number),
|
|
37
|
+
exclusiveMaximum: S.optionalKey(S.Number),
|
|
38
|
+
});
|
|
39
|
+
/** Effect owns the supported schema grammar; the Workers-safe validator owns instance validation. */
|
|
40
|
+
export function parseProjection(value) {
|
|
41
|
+
const parsed = S.decodeUnknownSync(portSchema)(value, {
|
|
42
|
+
onExcessProperty: "error",
|
|
43
|
+
});
|
|
44
|
+
return parsed;
|
|
45
|
+
}
|
|
46
|
+
export function accepts(schema, value) {
|
|
47
|
+
return new Validator(schema, "7").validate(value).valid;
|
|
48
|
+
}
|
|
49
|
+
/** Defaults fill absent properties; explicit values still have to validate. */
|
|
50
|
+
function defaultRunValue(schema, value) {
|
|
51
|
+
const items = schema.items;
|
|
52
|
+
if (S.is(S.Array(S.Json))(value) && items)
|
|
53
|
+
return value.map((item) => defaultRunValue(items, item));
|
|
54
|
+
const properties = schema.properties;
|
|
55
|
+
if (!S.is(S.JsonObject)(value) || !properties)
|
|
56
|
+
return value;
|
|
57
|
+
const entries = new Map();
|
|
58
|
+
for (const [key, item] of Object.entries(value))
|
|
59
|
+
entries.set(key, defaultRunValue(properties[key] ?? {}, item));
|
|
60
|
+
for (const [key, property] of Object.entries(properties))
|
|
61
|
+
if (!Object.hasOwn(value, key) && property.default !== undefined)
|
|
62
|
+
entries.set(key, defaultRunValue(property, property.default));
|
|
63
|
+
return S.decodeUnknownSync(S.JsonObject)(Object.fromEntries(entries));
|
|
64
|
+
}
|
|
65
|
+
/** Every run source uses the same supported projection and instance validator. */
|
|
66
|
+
export function admitRunInput(definition, raw) {
|
|
67
|
+
const schema = parseProjection(definition.input);
|
|
68
|
+
const input = defaultRunValue(schema, raw);
|
|
69
|
+
if (!S.is(S.JsonObject)(input))
|
|
70
|
+
return {
|
|
71
|
+
kind: "invalid",
|
|
72
|
+
issues: [
|
|
73
|
+
{
|
|
74
|
+
code: "invalid_input",
|
|
75
|
+
path: "",
|
|
76
|
+
message: "Run input must be an object",
|
|
77
|
+
},
|
|
78
|
+
],
|
|
79
|
+
};
|
|
80
|
+
const result = new Validator(schema, "7").validate(input);
|
|
81
|
+
return result.valid
|
|
82
|
+
? { kind: "accepted", input }
|
|
83
|
+
: {
|
|
84
|
+
kind: "invalid",
|
|
85
|
+
issues: result.errors.map((error) => ({
|
|
86
|
+
code: "invalid_input",
|
|
87
|
+
path: error.instanceLocation,
|
|
88
|
+
message: error.error,
|
|
89
|
+
})),
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
export function ports(schema) {
|
|
93
|
+
return schema.properties ?? {};
|
|
94
|
+
}
|
|
95
|
+
/** Only guaranteed object paths have a static type. The library owns Pointer spelling. */
|
|
96
|
+
export function portProjection(schema, path = "") {
|
|
97
|
+
if (schema === undefined)
|
|
98
|
+
return undefined;
|
|
99
|
+
if (path === "")
|
|
100
|
+
return schema;
|
|
101
|
+
const branches = schema.anyOf ?? schema.oneOf;
|
|
102
|
+
if (branches) {
|
|
103
|
+
const projections = branches.map((branch) => portProjection(branch, path));
|
|
104
|
+
return projections.every((part) => part !== undefined)
|
|
105
|
+
? { anyOf: projections }
|
|
106
|
+
: undefined;
|
|
107
|
+
}
|
|
108
|
+
const found = new Map();
|
|
109
|
+
const index = (part, at) => {
|
|
110
|
+
found.set(at, part);
|
|
111
|
+
for (const key of part.required ?? []) {
|
|
112
|
+
const child = part.properties && Object.hasOwn(part.properties, key)
|
|
113
|
+
? part.properties[key]
|
|
114
|
+
: undefined;
|
|
115
|
+
if (child)
|
|
116
|
+
index(child, `${at}/${escapePointer(key)}`);
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
index(schema, "");
|
|
120
|
+
return found.get(path);
|
|
121
|
+
}
|
|
122
|
+
/** A method receives its declared value; only the graph-facing view wraps scalars and unions. */
|
|
123
|
+
export function methodInputPorts(schema) {
|
|
124
|
+
return schema.type === "object" && !schema.anyOf && !schema.oneOf
|
|
125
|
+
? schema
|
|
126
|
+
: {
|
|
127
|
+
type: "object",
|
|
128
|
+
properties: { value: schema },
|
|
129
|
+
required: ["value"],
|
|
130
|
+
additionalProperties: false,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
function completionBranches(schema) {
|
|
134
|
+
const union = schema.anyOf ?? schema.oneOf;
|
|
135
|
+
// Flatten only a union without sibling assertions; keep every declared constraint.
|
|
136
|
+
return union &&
|
|
137
|
+
Object.keys(schema).every((key) => ["anyOf", "oneOf", "title", "description"].includes(key))
|
|
138
|
+
? union.flatMap(completionBranches)
|
|
139
|
+
: [schema];
|
|
140
|
+
}
|
|
141
|
+
function completionOutlets(schema) {
|
|
142
|
+
if (!schema.anyOf && !schema.oneOf)
|
|
143
|
+
return undefined;
|
|
144
|
+
const branches = completionBranches(schema);
|
|
145
|
+
const refused = branches.filter((part) => part.type === "object" && part.required?.includes("error"));
|
|
146
|
+
const ok = branches.filter((part) => !refused.includes(part));
|
|
147
|
+
if (!ok.length || !refused.length)
|
|
148
|
+
return undefined;
|
|
149
|
+
const union = (parts) => parts.length === 1 ? (parts[0] ?? {}) : { anyOf: [...parts] };
|
|
150
|
+
return { ok: union(ok), refused: union(refused) };
|
|
151
|
+
}
|
|
152
|
+
/** Declared error-object unions expose whole completion records on mutually exclusive outlets. */
|
|
153
|
+
export function methodOutputPorts(schema) {
|
|
154
|
+
return completionOutlets(schema) ?? ports(methodInputPorts(schema));
|
|
155
|
+
}
|
|
156
|
+
export function methodInputValue(schema, values) {
|
|
157
|
+
return methodInputPorts(schema) === schema ? values : values.value;
|
|
158
|
+
}
|
|
159
|
+
export function methodOutputValues(schema, value) {
|
|
160
|
+
if (!accepts(schema, value))
|
|
161
|
+
return undefined;
|
|
162
|
+
const outlets = completionOutlets(schema);
|
|
163
|
+
if (outlets)
|
|
164
|
+
return accepts(outlets.refused, value) ? { refused: value } : { ok: value };
|
|
165
|
+
if (methodInputPorts(schema) !== schema)
|
|
166
|
+
return { value };
|
|
167
|
+
if (!S.is(S.JsonObject)(value))
|
|
168
|
+
return undefined;
|
|
169
|
+
return Object.fromEntries(Object.entries(value).filter(([key]) => Object.hasOwn(ports(schema), key)));
|
|
170
|
+
}
|
|
171
|
+
export function portKind(schema) {
|
|
172
|
+
if (schema.format === "fungi:image" ||
|
|
173
|
+
schema.format === "fungi:video" ||
|
|
174
|
+
schema.format === "fungi:file")
|
|
175
|
+
return schema.format;
|
|
176
|
+
switch (schema.type) {
|
|
177
|
+
case "string":
|
|
178
|
+
return "text";
|
|
179
|
+
case "integer":
|
|
180
|
+
case "number":
|
|
181
|
+
return "number";
|
|
182
|
+
case "boolean":
|
|
183
|
+
return "bool";
|
|
184
|
+
case "array":
|
|
185
|
+
return { list: portKind(schema.items ?? {}) };
|
|
186
|
+
case "object":
|
|
187
|
+
return "object";
|
|
188
|
+
case "null":
|
|
189
|
+
return "null";
|
|
190
|
+
default:
|
|
191
|
+
return "any";
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
/** Sound subset check, not a claim to decide arbitrary JSON Schema implication. */
|
|
195
|
+
export function assignable(source, target) {
|
|
196
|
+
if (source.anyOf || source.oneOf)
|
|
197
|
+
// Ignoring source siblings widens the proof obligation, never the accepted values.
|
|
198
|
+
return (source.anyOf ?? source.oneOf ?? []).every((part) => assignable(part, target));
|
|
199
|
+
if (target.format && source.format !== target.format)
|
|
200
|
+
return false;
|
|
201
|
+
if (target.type === "string" &&
|
|
202
|
+
(source.format === "fungi:image" ||
|
|
203
|
+
source.format === "fungi:video" ||
|
|
204
|
+
source.format === "fungi:file") &&
|
|
205
|
+
source.format !== target.format)
|
|
206
|
+
return false;
|
|
207
|
+
// Literal values are decided by the maintained instance validator, including ranges and patterns.
|
|
208
|
+
if (source.const !== undefined)
|
|
209
|
+
return accepts(target, source.const);
|
|
210
|
+
if (source.enum)
|
|
211
|
+
return source.enum.every((value) => accepts(target, value));
|
|
212
|
+
if (target.oneOf)
|
|
213
|
+
return JSON.stringify(source) === JSON.stringify(target);
|
|
214
|
+
if (target.anyOf) {
|
|
215
|
+
const { anyOf, ...constraints } = target;
|
|
216
|
+
return (assignable(source, constraints) &&
|
|
217
|
+
anyOf.some((part) => assignable(source, part)));
|
|
218
|
+
}
|
|
219
|
+
if (target.const !== undefined || target.enum)
|
|
220
|
+
return false;
|
|
221
|
+
if (target.pattern && source.pattern !== target.pattern)
|
|
222
|
+
return false;
|
|
223
|
+
for (const key of ["exclusiveMinimum", "exclusiveMaximum"])
|
|
224
|
+
if (target[key] !== undefined && source[key] !== target[key])
|
|
225
|
+
return false;
|
|
226
|
+
if (target.type &&
|
|
227
|
+
source.type !== target.type &&
|
|
228
|
+
!(source.type === "integer" && target.type === "number"))
|
|
229
|
+
return false;
|
|
230
|
+
for (const key of ["minimum", "minLength", "minItems"])
|
|
231
|
+
if (target[key] !== undefined &&
|
|
232
|
+
(source[key] === undefined || source[key] < target[key]))
|
|
233
|
+
return false;
|
|
234
|
+
for (const key of ["maximum", "maxLength", "maxItems"])
|
|
235
|
+
if (target[key] !== undefined &&
|
|
236
|
+
(source[key] === undefined || source[key] > target[key]))
|
|
237
|
+
return false;
|
|
238
|
+
if (target.items && !assignable(source.items ?? {}, target.items))
|
|
239
|
+
return false;
|
|
240
|
+
for (const key of target.required ?? [])
|
|
241
|
+
if (!source.required?.includes(key))
|
|
242
|
+
return false;
|
|
243
|
+
const src = ports(source);
|
|
244
|
+
for (const [key, wanted] of Object.entries(ports(target))) {
|
|
245
|
+
const supplied = src[key];
|
|
246
|
+
if (supplied
|
|
247
|
+
? !assignable(supplied, wanted)
|
|
248
|
+
: source.additionalProperties !== false)
|
|
249
|
+
return false;
|
|
250
|
+
}
|
|
251
|
+
if (target.additionalProperties === false &&
|
|
252
|
+
(source.additionalProperties !== false ||
|
|
253
|
+
Object.keys(src).some((key) => !Object.hasOwn(ports(target), key))))
|
|
254
|
+
return false;
|
|
255
|
+
return true;
|
|
256
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type { WorkflowRunMember } from "./run-authority.js";
|
|
3
|
+
/** Host-captured provenance from the actual connector or approved-policy owner. */
|
|
4
|
+
export declare const workflowArmedViaSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
5
|
+
kind: z.ZodLiteral<"connector">;
|
|
6
|
+
agentId: z.ZodString;
|
|
7
|
+
attachmentId: z.ZodString;
|
|
8
|
+
attachmentVersion: z.ZodNumber;
|
|
9
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
10
|
+
kind: z.ZodLiteral<"policy">;
|
|
11
|
+
agentId: z.ZodString;
|
|
12
|
+
policyRevision: z.ZodString;
|
|
13
|
+
grantorMemberId: z.ZodString;
|
|
14
|
+
}, z.core.$strict>], "kind">;
|
|
15
|
+
/** Provenance from the actual connector or approved-policy owner. */
|
|
16
|
+
export type WorkflowArmedVia = z.output<typeof workflowArmedViaSchema>;
|
|
17
|
+
/** The grantor stays the armer; decoding this shape alone grants no authority. */
|
|
18
|
+
export declare const workflowArmingReceiptSchema: z.ZodObject<{
|
|
19
|
+
armedBy: z.ZodString;
|
|
20
|
+
armedVia: z.ZodNullable<z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
21
|
+
kind: z.ZodLiteral<"connector">;
|
|
22
|
+
agentId: z.ZodString;
|
|
23
|
+
attachmentId: z.ZodString;
|
|
24
|
+
attachmentVersion: z.ZodNumber;
|
|
25
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
26
|
+
kind: z.ZodLiteral<"policy">;
|
|
27
|
+
agentId: z.ZodString;
|
|
28
|
+
policyRevision: z.ZodString;
|
|
29
|
+
grantorMemberId: z.ZodString;
|
|
30
|
+
}, z.core.$strict>], "kind">>;
|
|
31
|
+
grantorMembershipVersion: z.ZodNumber;
|
|
32
|
+
}, z.core.$strict>;
|
|
33
|
+
/** Captured by a host after current frame, installation and member admission. */
|
|
34
|
+
export type WorkflowArmingReceipt = z.output<typeof workflowArmingReceiptSchema>;
|
|
35
|
+
/** Current host-owner readback, after live grantor and approval checks. */
|
|
36
|
+
export declare const workflowArmingAccessSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
37
|
+
kind: z.ZodLiteral<"connector">;
|
|
38
|
+
agentId: z.ZodString;
|
|
39
|
+
attachmentId: z.ZodString;
|
|
40
|
+
attachmentVersion: z.ZodNumber;
|
|
41
|
+
grantorMemberId: z.ZodString;
|
|
42
|
+
level: z.ZodEnum<{
|
|
43
|
+
write: "write";
|
|
44
|
+
read: "read";
|
|
45
|
+
}>;
|
|
46
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
47
|
+
kind: z.ZodLiteral<"policy">;
|
|
48
|
+
agentId: z.ZodString;
|
|
49
|
+
policyRevision: z.ZodString;
|
|
50
|
+
grantorMemberId: z.ZodString;
|
|
51
|
+
level: z.ZodEnum<{
|
|
52
|
+
write: "write";
|
|
53
|
+
read: "read";
|
|
54
|
+
}>;
|
|
55
|
+
}, z.core.$strict>], "kind">;
|
|
56
|
+
/** The actual source identity plus its currently approved access level. */
|
|
57
|
+
export type WorkflowArmingAccess = z.output<typeof workflowArmingAccessSchema>;
|
|
58
|
+
/** Lookup keys exclude captured versions, revisions and grantors. */
|
|
59
|
+
export type WorkflowArmingLookup = Readonly<{
|
|
60
|
+
kind: "connector";
|
|
61
|
+
agentId: string;
|
|
62
|
+
attachmentId: string;
|
|
63
|
+
}> | Readonly<{
|
|
64
|
+
kind: "policy";
|
|
65
|
+
agentId: string;
|
|
66
|
+
}>;
|
|
67
|
+
/** The host reader obtains approval, revision and grantor from its current owner row. */
|
|
68
|
+
export type WorkflowArmingAuthorityPorts = Readonly<{
|
|
69
|
+
member(memberId: string): Promise<WorkflowRunMember | null>;
|
|
70
|
+
readAgentAccess(lookup: WorkflowArmingLookup): Promise<WorkflowArmingAccess | null>;
|
|
71
|
+
}>;
|
|
72
|
+
/** Trusted result produced by the authority functions, never decoded from caller JSON. */
|
|
73
|
+
export type WorkflowArmingAdmission = Readonly<{
|
|
74
|
+
kind: "admitted";
|
|
75
|
+
receipt: WorkflowArmingReceipt;
|
|
76
|
+
}> | Readonly<{
|
|
77
|
+
kind: "refused";
|
|
78
|
+
code: "not_granted" | "revoked";
|
|
79
|
+
}>;
|
|
80
|
+
/** Recheck Agent authority only; the host also admits member, Computer and installation grants. */
|
|
81
|
+
export declare function recheckWorkflowArming(receipt: WorkflowArmingReceipt, ports: WorkflowArmingAuthorityPorts): Promise<WorkflowArmingAdmission>;
|
|
82
|
+
/** Capture the version from the host's actual member owner, never a frame or tool argument. */
|
|
83
|
+
export declare function captureWorkflowArming(source: Pick<WorkflowArmingReceipt, "armedBy" | "armedVia">, ports: WorkflowArmingAuthorityPorts): Promise<WorkflowArmingAdmission>;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { decodeWorkflowRunMember } from "./run-member.js";
|
|
3
|
+
const identity = z.string().min(1).max(256);
|
|
4
|
+
const membershipVersion = z.number().int().nonnegative();
|
|
5
|
+
const connector = z.strictObject({
|
|
6
|
+
kind: z.literal("connector"),
|
|
7
|
+
agentId: identity,
|
|
8
|
+
attachmentId: identity,
|
|
9
|
+
attachmentVersion: z.number().int().positive(),
|
|
10
|
+
});
|
|
11
|
+
const policy = z.strictObject({
|
|
12
|
+
kind: z.literal("policy"),
|
|
13
|
+
agentId: identity,
|
|
14
|
+
// The approved policy revision is opaque, nonempty and bounded.
|
|
15
|
+
policyRevision: identity,
|
|
16
|
+
grantorMemberId: identity,
|
|
17
|
+
});
|
|
18
|
+
/** Host-captured provenance from the actual connector or approved-policy owner. */
|
|
19
|
+
export const workflowArmedViaSchema = z.discriminatedUnion("kind", [
|
|
20
|
+
connector,
|
|
21
|
+
policy,
|
|
22
|
+
]);
|
|
23
|
+
/** The grantor stays the armer; decoding this shape alone grants no authority. */
|
|
24
|
+
export const workflowArmingReceiptSchema = z
|
|
25
|
+
.strictObject({
|
|
26
|
+
armedBy: identity,
|
|
27
|
+
/** A human arm has an explicit null; Agent arms retain their actual live source. */
|
|
28
|
+
armedVia: workflowArmedViaSchema.nullable(),
|
|
29
|
+
grantorMembershipVersion: membershipVersion,
|
|
30
|
+
})
|
|
31
|
+
.superRefine((receipt, context) => {
|
|
32
|
+
if (receipt.armedVia?.kind === "policy" &&
|
|
33
|
+
receipt.armedVia.grantorMemberId !== receipt.armedBy)
|
|
34
|
+
context.addIssue({
|
|
35
|
+
code: "custom",
|
|
36
|
+
path: ["armedVia", "grantorMemberId"],
|
|
37
|
+
message: "The approved policy grantor must be the armer",
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
/** Current host-owner readback, after live grantor and approval checks. */
|
|
41
|
+
export const workflowArmingAccessSchema = z.discriminatedUnion("kind", [
|
|
42
|
+
connector.extend({
|
|
43
|
+
grantorMemberId: identity,
|
|
44
|
+
level: z.enum(["read", "write"]),
|
|
45
|
+
}),
|
|
46
|
+
policy.extend({ level: z.enum(["read", "write"]) }),
|
|
47
|
+
]);
|
|
48
|
+
/** Recheck Agent authority only; the host also admits member, Computer and installation grants. */
|
|
49
|
+
export async function recheckWorkflowArming(receipt, ports) {
|
|
50
|
+
const captured = workflowArmingReceiptSchema.safeParse(receipt);
|
|
51
|
+
if (!captured.success)
|
|
52
|
+
return { kind: "refused", code: "not_granted" };
|
|
53
|
+
const before = decodeWorkflowRunMember(await ports.member(captured.data.armedBy));
|
|
54
|
+
if (before?.membershipVersion !== captured.data.grantorMembershipVersion)
|
|
55
|
+
return { kind: "refused", code: "revoked" };
|
|
56
|
+
const via = captured.data.armedVia;
|
|
57
|
+
if (via === null)
|
|
58
|
+
return { kind: "admitted", receipt: captured.data };
|
|
59
|
+
const lookup = via.kind === "connector"
|
|
60
|
+
? {
|
|
61
|
+
kind: "connector",
|
|
62
|
+
agentId: via.agentId,
|
|
63
|
+
attachmentId: via.attachmentId,
|
|
64
|
+
}
|
|
65
|
+
: { kind: "policy", agentId: via.agentId };
|
|
66
|
+
const current = workflowArmingAccessSchema.safeParse(await ports.readAgentAccess(lookup));
|
|
67
|
+
if (!current.success ||
|
|
68
|
+
current.data.level !== "write" ||
|
|
69
|
+
current.data.agentId !== via.agentId ||
|
|
70
|
+
current.data.grantorMemberId !== captured.data.armedBy)
|
|
71
|
+
return { kind: "refused", code: "revoked" };
|
|
72
|
+
const unchanged = via.kind === "connector"
|
|
73
|
+
? current.data.kind === "connector" &&
|
|
74
|
+
current.data.attachmentId === via.attachmentId &&
|
|
75
|
+
current.data.attachmentVersion === via.attachmentVersion
|
|
76
|
+
: current.data.kind === "policy" &&
|
|
77
|
+
current.data.policyRevision === via.policyRevision;
|
|
78
|
+
const after = decodeWorkflowRunMember(await ports.member(captured.data.armedBy));
|
|
79
|
+
return unchanged &&
|
|
80
|
+
after?.membershipVersion === captured.data.grantorMembershipVersion
|
|
81
|
+
? { kind: "admitted", receipt: captured.data }
|
|
82
|
+
: { kind: "refused", code: "revoked" };
|
|
83
|
+
}
|
|
84
|
+
/** Capture the version from the host's actual member owner, never a frame or tool argument. */
|
|
85
|
+
export async function captureWorkflowArming(source, ports) {
|
|
86
|
+
const current = decodeWorkflowRunMember(await ports.member(source.armedBy));
|
|
87
|
+
if (current === null)
|
|
88
|
+
return { kind: "refused", code: "not_granted" };
|
|
89
|
+
return recheckWorkflowArming({ ...source, grantorMembershipVersion: current.membershipVersion }, ports);
|
|
90
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { Issue, WorkflowDefinition } from "../index.js";
|
|
2
|
+
import type { WorkflowCatalogEntry } from "./ports.js";
|
|
3
|
+
/** Declared App references that require a current human-visible installation. */
|
|
4
|
+
export type WorkflowBindingRequest = Readonly<{
|
|
5
|
+
name: string;
|
|
6
|
+
appId: string;
|
|
7
|
+
reason: "missing" | "ambiguous";
|
|
8
|
+
}>;
|
|
9
|
+
export type WorkflowBindingsResolution = Readonly<{
|
|
10
|
+
kind: "bound";
|
|
11
|
+
bindings: Readonly<Record<string, string>>;
|
|
12
|
+
}> | Readonly<{
|
|
13
|
+
kind: "needs-binding";
|
|
14
|
+
bindings: readonly WorkflowBindingRequest[];
|
|
15
|
+
}> | Readonly<{
|
|
16
|
+
kind: "invalid";
|
|
17
|
+
issues: readonly Issue[];
|
|
18
|
+
}>;
|
|
19
|
+
export type CatalogInstallation = Pick<WorkflowCatalogEntry, "installationId" | "appId">;
|
|
20
|
+
/**
|
|
21
|
+
* Resolve parsed Workflow references against the starting human's live catalog.
|
|
22
|
+
* An explicit choice must still be visible and match the declared App. Omitted
|
|
23
|
+
* choices use only a unique current match; no saved installation defaults apply.
|
|
24
|
+
*/
|
|
25
|
+
export declare function resolveWorkflowBindings(definition: Pick<WorkflowDefinition, "apps">, requested: Readonly<Record<string, string>> | undefined, catalog: readonly CatalogInstallation[]): WorkflowBindingsResolution;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
function resolveInstallation(appId, explicit, catalog) {
|
|
2
|
+
const matches = catalog.filter((entry) => entry.appId === appId);
|
|
3
|
+
if (explicit !== undefined)
|
|
4
|
+
return matches.some((entry) => entry.installationId === explicit)
|
|
5
|
+
? { installationId: explicit }
|
|
6
|
+
: { reason: "missing" };
|
|
7
|
+
const unique = matches.length === 1 ? matches[0] : undefined;
|
|
8
|
+
return unique !== undefined
|
|
9
|
+
? { installationId: unique.installationId }
|
|
10
|
+
: { reason: matches.length === 0 ? "missing" : "ambiguous" };
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Resolve parsed Workflow references against the starting human's live catalog.
|
|
14
|
+
* An explicit choice must still be visible and match the declared App. Omitted
|
|
15
|
+
* choices use only a unique current match; no saved installation defaults apply.
|
|
16
|
+
*/
|
|
17
|
+
export function resolveWorkflowBindings(definition, requested, catalog) {
|
|
18
|
+
for (const name of Object.keys(requested ?? {})) {
|
|
19
|
+
if (!Object.hasOwn(definition.apps, name))
|
|
20
|
+
return {
|
|
21
|
+
kind: "invalid",
|
|
22
|
+
issues: [{ code: "binding", message: `Unknown App binding: ${name}` }],
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
const selected = [];
|
|
26
|
+
const needed = [];
|
|
27
|
+
for (const [name, spec] of Object.entries(definition.apps)) {
|
|
28
|
+
const resolved = resolveInstallation(spec.appId, requested?.[name], catalog);
|
|
29
|
+
if ("installationId" in resolved)
|
|
30
|
+
selected.push([name, resolved.installationId]);
|
|
31
|
+
else
|
|
32
|
+
needed.push({
|
|
33
|
+
name,
|
|
34
|
+
appId: spec.appId,
|
|
35
|
+
reason: resolved.reason,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
return needed.length === 0
|
|
39
|
+
? { kind: "bound", bindings: Object.fromEntries(selected) }
|
|
40
|
+
: { kind: "needs-binding", bindings: needed };
|
|
41
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { RepositoryRef } from "@fungi.computer/agar/git";
|
|
2
|
+
import type { HostActor } from "@fungi.computer/puffball/app-method-runtime";
|
|
3
|
+
import { type WorkflowNodeTarget, type WorkflowNodeCeiling } from "./graph-dispatch.js";
|
|
4
|
+
import { type RunCeiling } from "./run-ceiling.js";
|
|
5
|
+
import type { WorkflowRunAuthorityPorts, WorkflowRunMember } from "./run-authority.js";
|
|
6
|
+
/** Same Computer-bound requirements as live Workflow App admission. */
|
|
7
|
+
export declare function workflowMethodRequiresComputer(requires: RunCeiling["nodes"][number]["capabilities"]): boolean;
|
|
8
|
+
/** Hosts provide their existing live human admission, never the compiled catalog as a grant. */
|
|
9
|
+
export type WorkflowCeilingCapturePorts = Readonly<{
|
|
10
|
+
node(actor: Extract<HostActor, {
|
|
11
|
+
kind: "member";
|
|
12
|
+
}>, member: WorkflowRunMember, target: WorkflowNodeTarget): Promise<WorkflowNodeCeiling | null>;
|
|
13
|
+
git(actor: Extract<HostActor, {
|
|
14
|
+
kind: "member";
|
|
15
|
+
}>, member: WorkflowRunMember, repository: RepositoryRef, scope: "read" | "write"): Promise<boolean>;
|
|
16
|
+
}>;
|
|
17
|
+
/** Assemble the start ceiling from real host admissions; minting and invocation stay outside capture. */
|
|
18
|
+
export declare function createWorkflowCeilingCapture(ports: WorkflowCeilingCapturePorts): WorkflowRunAuthorityPorts["capture"];
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import * as S from "effect/Schema";
|
|
2
|
+
import * as Option from "effect/Option";
|
|
3
|
+
import { appBackendBindingOperation, scopeOperationToMethod, appBackendBindingsSchema, appCapabilityIdSchema, } from "@fungi.computer/puffball/app";
|
|
4
|
+
import { gitAccessScope, resolveGitAccess } from "../git-access.js";
|
|
5
|
+
import { resolveWorkflowNodeTarget, workflowNodeWithinCeiling, } from "./graph-dispatch.js";
|
|
6
|
+
import { runCeilingSchema } from "./run-ceiling.js";
|
|
7
|
+
/** Same Computer-bound requirements as live Workflow App admission. */
|
|
8
|
+
export function workflowMethodRequiresComputer(requires) {
|
|
9
|
+
return requires.some((capability) => capability === "computer.list" ||
|
|
10
|
+
capability === "computer.readFile" ||
|
|
11
|
+
capability === "computer.writeFile" ||
|
|
12
|
+
capability === "computer.execute" ||
|
|
13
|
+
capability === "media.generation");
|
|
14
|
+
}
|
|
15
|
+
/** Assemble the start ceiling from real host admissions; minting and invocation stay outside capture. */
|
|
16
|
+
export function createWorkflowCeilingCapture(ports) {
|
|
17
|
+
return async (preparation, actor, member) => {
|
|
18
|
+
const initiator = preparation.initiatedBy;
|
|
19
|
+
const human = initiator.kind === "member"
|
|
20
|
+
? initiator.memberId
|
|
21
|
+
: initiator.grantorMemberId;
|
|
22
|
+
if (actor.memberId !== human)
|
|
23
|
+
return null;
|
|
24
|
+
const nodes = [];
|
|
25
|
+
const nodeGitAccess = [];
|
|
26
|
+
const captureGraph = async (graphId, graphNodes) => {
|
|
27
|
+
for (const node of graphNodes) {
|
|
28
|
+
if (node.kind !== "app")
|
|
29
|
+
continue;
|
|
30
|
+
const binding = preparation.plan.bindings[node.app];
|
|
31
|
+
if (binding === undefined || !("installationId" in binding))
|
|
32
|
+
return false;
|
|
33
|
+
const coordinates = graphId === undefined
|
|
34
|
+
? { nodeId: node.id }
|
|
35
|
+
: { nodeId: node.id, graphId };
|
|
36
|
+
const resolved = resolveWorkflowNodeTarget(preparation, {
|
|
37
|
+
...coordinates,
|
|
38
|
+
installationId: binding.installationId,
|
|
39
|
+
method: node.method,
|
|
40
|
+
});
|
|
41
|
+
if (resolved === null)
|
|
42
|
+
return false;
|
|
43
|
+
const requires = S.decodeUnknownOption(S.Array(appCapabilityIdSchema))(resolved.method.requires);
|
|
44
|
+
if (Option.isNone(requires))
|
|
45
|
+
return false;
|
|
46
|
+
if (preparation.computerId === null &&
|
|
47
|
+
(workflowMethodRequiresComputer(requires.value) ||
|
|
48
|
+
node.gitAccess !== undefined))
|
|
49
|
+
return false;
|
|
50
|
+
const scopedMethod = {
|
|
51
|
+
effect: resolved.method.effect,
|
|
52
|
+
requires: requires.value,
|
|
53
|
+
};
|
|
54
|
+
const admitted = await ports.node(actor, member, resolved.target);
|
|
55
|
+
if (admitted === null ||
|
|
56
|
+
!workflowNodeWithinCeiling(resolved.method, admitted))
|
|
57
|
+
return false;
|
|
58
|
+
const level = resolved.method.effect === "read" ? "read" : admitted.level;
|
|
59
|
+
const bindings = S.decodeUnknownSync(appBackendBindingsSchema)(admitted.bindings
|
|
60
|
+
.map((binding) => ({
|
|
61
|
+
name: binding.name,
|
|
62
|
+
operations: binding.operations.filter((name) => {
|
|
63
|
+
const operation = appBackendBindingOperation(binding.name, name);
|
|
64
|
+
return (operation !== undefined &&
|
|
65
|
+
scopeOperationToMethod(operation, scopedMethod, level));
|
|
66
|
+
}),
|
|
67
|
+
}))
|
|
68
|
+
.filter((binding) => binding.operations.length > 0));
|
|
69
|
+
nodes.push({
|
|
70
|
+
...coordinates,
|
|
71
|
+
level,
|
|
72
|
+
capabilities: admitted.capabilities.filter((capability) => resolved.method.requires.includes(capability)),
|
|
73
|
+
bindings,
|
|
74
|
+
});
|
|
75
|
+
if (node.gitAccess !== undefined) {
|
|
76
|
+
const gitAccess = resolveGitAccess(preparation.plan.bindings, node.gitAccess, "triggerRepository" in preparation
|
|
77
|
+
? preparation.triggerRepository
|
|
78
|
+
: undefined);
|
|
79
|
+
// Read access never needs more than read, whatever the node's level.
|
|
80
|
+
const scope = gitAccess === null || gitAccessScope(gitAccess) === "read"
|
|
81
|
+
? "read"
|
|
82
|
+
: level;
|
|
83
|
+
if (gitAccess === null ||
|
|
84
|
+
!(await ports.git(actor, member, gitAccess.repository, scope)))
|
|
85
|
+
return false;
|
|
86
|
+
nodeGitAccess.push({ ...coordinates, gitAccess });
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return true;
|
|
90
|
+
};
|
|
91
|
+
if (!(await captureGraph(undefined, preparation.plan.definition.nodes)))
|
|
92
|
+
return null;
|
|
93
|
+
for (const [graphId, graph] of Object.entries(preparation.plan.definition.subgraphs ?? {}))
|
|
94
|
+
if (!(await captureGraph(graphId, graph.nodes)))
|
|
95
|
+
return null;
|
|
96
|
+
const decoded = S.decodeUnknownOption(runCeilingSchema)({ nodes, nodeGitAccess }, { onExcessProperty: "error" });
|
|
97
|
+
return Option.isSome(decoded) ? decoded.value : null;
|
|
98
|
+
};
|
|
99
|
+
}
|