@relayflows/sdk 2.0.24 → 2.0.25

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 (162) hide show
  1. package/dist/authored-flow-executor.d.ts +3 -0
  2. package/dist/authored-flow-executor.d.ts.map +1 -1
  3. package/dist/authored-flow-executor.js +29 -1
  4. package/dist/authored-flow-executor.js.map +1 -1
  5. package/dist/authored-flow-loader.d.ts +15 -2
  6. package/dist/authored-flow-loader.d.ts.map +1 -1
  7. package/dist/authored-flow-loader.js +33 -4
  8. package/dist/authored-flow-loader.js.map +1 -1
  9. package/dist/authored-hooks.d.ts +29 -0
  10. package/dist/authored-hooks.d.ts.map +1 -0
  11. package/dist/authored-hooks.js +117 -0
  12. package/dist/authored-hooks.js.map +1 -0
  13. package/dist/authored-node-entry.js +1 -0
  14. package/dist/authored-node-entry.js.map +1 -1
  15. package/dist/authored-root.d.ts +7 -0
  16. package/dist/authored-root.d.ts.map +1 -1
  17. package/dist/authored-root.js +14 -1
  18. package/dist/authored-root.js.map +1 -1
  19. package/dist/authored-source-authority.d.ts.map +1 -1
  20. package/dist/authored-source-authority.js +6 -0
  21. package/dist/authored-source-authority.js.map +1 -1
  22. package/dist/bundle-extensions.d.ts +19 -0
  23. package/dist/bundle-extensions.d.ts.map +1 -0
  24. package/dist/bundle-extensions.js +95 -0
  25. package/dist/bundle-extensions.js.map +1 -0
  26. package/dist/bundle-typescript.js +1 -1
  27. package/dist/bundle-typescript.js.map +1 -1
  28. package/dist/bundle.d.ts +11 -0
  29. package/dist/bundle.d.ts.map +1 -1
  30. package/dist/bundle.js +13 -4
  31. package/dist/bundle.js.map +1 -1
  32. package/dist/cli/add-extension.d.ts +28 -0
  33. package/dist/cli/add-extension.d.ts.map +1 -0
  34. package/dist/cli/add-extension.js +135 -0
  35. package/dist/cli/add-extension.js.map +1 -0
  36. package/dist/cli/add.d.ts +3 -0
  37. package/dist/cli/add.d.ts.map +1 -1
  38. package/dist/cli/add.js +8 -0
  39. package/dist/cli/add.js.map +1 -1
  40. package/dist/cli/build.d.ts.map +1 -1
  41. package/dist/cli/build.js +7 -2
  42. package/dist/cli/build.js.map +1 -1
  43. package/dist/cli/check-helper-body.d.ts.map +1 -1
  44. package/dist/cli/check-helper-body.js +4 -0
  45. package/dist/cli/check-helper-body.js.map +1 -1
  46. package/dist/cli/check-triggers.d.ts.map +1 -1
  47. package/dist/cli/check-triggers.js +25 -2
  48. package/dist/cli/check-triggers.js.map +1 -1
  49. package/dist/cli/check-typescript.d.ts.map +1 -1
  50. package/dist/cli/check-typescript.js +5 -1
  51. package/dist/cli/check-typescript.js.map +1 -1
  52. package/dist/cli/check.d.ts +24 -0
  53. package/dist/cli/check.d.ts.map +1 -1
  54. package/dist/cli/check.js.map +1 -1
  55. package/dist/cli/cloud-deploy.d.ts +1 -0
  56. package/dist/cli/cloud-deploy.d.ts.map +1 -1
  57. package/dist/cli/cloud-deploy.js +11 -1
  58. package/dist/cli/cloud-deploy.js.map +1 -1
  59. package/dist/cli/plugin.d.ts +50 -0
  60. package/dist/cli/plugin.d.ts.map +1 -0
  61. package/dist/cli/plugin.js +295 -0
  62. package/dist/cli/plugin.js.map +1 -0
  63. package/dist/cli-commands.d.ts +46 -3
  64. package/dist/cli-commands.d.ts.map +1 -1
  65. package/dist/cli-commands.js +32 -2
  66. package/dist/cli-commands.js.map +1 -1
  67. package/dist/cli.d.ts +2 -1
  68. package/dist/cli.d.ts.map +1 -1
  69. package/dist/cli.js +26 -1
  70. package/dist/cli.js.map +1 -1
  71. package/dist/cloud-deploy.d.ts +5 -0
  72. package/dist/cloud-deploy.d.ts.map +1 -1
  73. package/dist/cloud-deploy.js +7 -6
  74. package/dist/cloud-deploy.js.map +1 -1
  75. package/dist/cloud-run.d.ts +2 -0
  76. package/dist/cloud-run.d.ts.map +1 -1
  77. package/dist/cloud-run.js +14 -4
  78. package/dist/cloud-run.js.map +1 -1
  79. package/dist/failure-kinds.d.ts +2 -2
  80. package/dist/failure-kinds.d.ts.map +1 -1
  81. package/dist/flow-extension-compat.d.ts +18 -0
  82. package/dist/flow-extension-compat.d.ts.map +1 -0
  83. package/dist/flow-extension-compat.js +33 -0
  84. package/dist/flow-extension-compat.js.map +1 -0
  85. package/dist/flow-extension-loader.d.ts +61 -0
  86. package/dist/flow-extension-loader.d.ts.map +1 -0
  87. package/dist/flow-extension-loader.js +173 -0
  88. package/dist/flow-extension-loader.js.map +1 -0
  89. package/dist/flow-extension-manifest.d.ts +61 -0
  90. package/dist/flow-extension-manifest.d.ts.map +1 -0
  91. package/dist/flow-extension-manifest.js +188 -0
  92. package/dist/flow-extension-manifest.js.map +1 -0
  93. package/dist/flow-extension-submit.d.ts +38 -0
  94. package/dist/flow-extension-submit.d.ts.map +1 -0
  95. package/dist/flow-extension-submit.js +94 -0
  96. package/dist/flow-extension-submit.js.map +1 -0
  97. package/dist/flow-requirements.d.ts +13 -3
  98. package/dist/flow-requirements.d.ts.map +1 -1
  99. package/dist/flow-requirements.js +30 -0
  100. package/dist/flow-requirements.js.map +1 -1
  101. package/dist/plugin-github.d.ts +35 -0
  102. package/dist/plugin-github.d.ts.map +1 -0
  103. package/dist/plugin-github.js +92 -0
  104. package/dist/plugin-github.js.map +1 -0
  105. package/dist/plugin-loader.d.ts.map +1 -1
  106. package/dist/plugin-loader.js +5 -1
  107. package/dist/plugin-loader.js.map +1 -1
  108. package/dist/plugin-lock.d.ts +79 -0
  109. package/dist/plugin-lock.d.ts.map +1 -0
  110. package/dist/plugin-lock.js +203 -0
  111. package/dist/plugin-lock.js.map +1 -0
  112. package/dist/plugin-manifest.d.ts +5 -1
  113. package/dist/plugin-manifest.d.ts.map +1 -1
  114. package/dist/plugin-manifest.js +17 -0
  115. package/dist/plugin-manifest.js.map +1 -1
  116. package/dist/plugin-source.d.ts +38 -0
  117. package/dist/plugin-source.d.ts.map +1 -0
  118. package/dist/plugin-source.js +84 -0
  119. package/dist/plugin-source.js.map +1 -0
  120. package/dist/plugin-store.d.ts +30 -0
  121. package/dist/plugin-store.d.ts.map +1 -0
  122. package/dist/plugin-store.js +133 -0
  123. package/dist/plugin-store.js.map +1 -0
  124. package/dist/semver-range.d.ts +10 -0
  125. package/dist/semver-range.d.ts.map +1 -0
  126. package/dist/semver-range.js +88 -0
  127. package/dist/semver-range.js.map +1 -0
  128. package/package.json +2 -2
  129. package/src/authored-flow-executor.ts +36 -1
  130. package/src/authored-flow-loader.ts +49 -6
  131. package/src/authored-hooks.ts +136 -0
  132. package/src/authored-node-entry.ts +1 -0
  133. package/src/authored-root.ts +22 -1
  134. package/src/authored-source-authority.ts +6 -0
  135. package/src/bundle-extensions.ts +85 -0
  136. package/src/bundle-typescript.ts +1 -1
  137. package/src/bundle.ts +14 -4
  138. package/src/cli/add-extension.ts +134 -0
  139. package/src/cli/add.ts +9 -0
  140. package/src/cli/build.ts +7 -2
  141. package/src/cli/check-helper-body.ts +4 -0
  142. package/src/cli/check-triggers.ts +27 -2
  143. package/src/cli/check-typescript.ts +5 -1
  144. package/src/cli/check.ts +23 -0
  145. package/src/cli/cloud-deploy.ts +11 -1
  146. package/src/cli/plugin.ts +286 -0
  147. package/src/cli-commands.ts +32 -2
  148. package/src/cli.ts +23 -1
  149. package/src/cloud-deploy.ts +18 -8
  150. package/src/cloud-run.ts +16 -6
  151. package/src/flow-extension-compat.ts +35 -0
  152. package/src/flow-extension-loader.ts +217 -0
  153. package/src/flow-extension-manifest.ts +190 -0
  154. package/src/flow-extension-submit.ts +120 -0
  155. package/src/flow-requirements.ts +44 -3
  156. package/src/plugin-github.ts +98 -0
  157. package/src/plugin-loader.ts +5 -1
  158. package/src/plugin-lock.ts +194 -0
  159. package/src/plugin-manifest.ts +17 -0
  160. package/src/plugin-source.ts +91 -0
  161. package/src/plugin-store.ts +105 -0
  162. package/src/semver-range.ts +74 -0
@@ -0,0 +1,10 @@
1
+ interface Parsed {
2
+ readonly triple: readonly [number, number, number];
3
+ readonly pre?: string;
4
+ }
5
+ export declare function parseVersion(value: string): Parsed | undefined;
6
+ export declare function isVersionRange(value: string): boolean;
7
+ /** Whether `version` satisfies `range`; false for malformed input rather than a throw. */
8
+ export declare function satisfiesRange(version: string, range: string): boolean;
9
+ export {};
10
+ //# sourceMappingURL=semver-range.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"semver-range.d.ts","sourceRoot":"","sources":["../src/semver-range.ts"],"names":[],"mappings":"AAYA,UAAU,MAAM;IAAG,QAAQ,CAAC,MAAM,EAAE,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC;IAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE;AAE9F,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAM9D;AAED,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAErD;AAgCD,0FAA0F;AAC1F,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAgBtE"}
@@ -0,0 +1,88 @@
1
+ /**
2
+ * The small semver subset plugin `compat` ranges may use. Deliberately not a
3
+ * dependency on `semver`: a plugin's compatibility claim has to be checkable by
4
+ * every reader — Cloud, the CLI, a catalog — from one short, obvious rule.
5
+ *
6
+ * Accepted: `*`, `x.y.z`, `^x.y.z`, `~x.y.z`, `>=x.y.z`, `>=x.y.z <a.b.c`.
7
+ * Prerelease tags are ordered lexically after the numeric triple and only
8
+ * compare against a range whose bounds name the same triple.
9
+ */
10
+ const VERSION = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?$/;
11
+ const RANGE = /^(\*|(?:[\^~]|>=)?\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?: <\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?)?)$/;
12
+ export function parseVersion(value) {
13
+ const m = VERSION.exec(value);
14
+ if (!m)
15
+ return undefined;
16
+ const triple = [Number(m[1]), Number(m[2]), Number(m[3])];
17
+ if (triple.some(n => !Number.isSafeInteger(n)))
18
+ return undefined;
19
+ return m[4] === undefined ? { triple } : { triple, pre: m[4] };
20
+ }
21
+ export function isVersionRange(value) {
22
+ return RANGE.test(value);
23
+ }
24
+ function compare(a, b) {
25
+ for (let i = 0; i < 3; i++)
26
+ if (a.triple[i] !== b.triple[i])
27
+ return a.triple[i] - b.triple[i];
28
+ if (a.pre === b.pre)
29
+ return 0;
30
+ if (a.pre === undefined)
31
+ return 1;
32
+ if (b.pre === undefined)
33
+ return -1;
34
+ return comparePrerelease(a.pre, b.pre);
35
+ }
36
+ function comparePrerelease(a, b) {
37
+ const as = a.split('.');
38
+ const bs = b.split('.');
39
+ const n = Math.max(as.length, bs.length);
40
+ for (let i = 0; i < n; i++) {
41
+ const left = as[i];
42
+ const right = bs[i];
43
+ if (left === undefined)
44
+ return -1;
45
+ if (right === undefined)
46
+ return 1;
47
+ const leftNum = /^\d+$/.test(left) ? Number(left) : undefined;
48
+ const rightNum = /^\d+$/.test(right) ? Number(right) : undefined;
49
+ if (leftNum !== undefined && rightNum !== undefined) {
50
+ if (leftNum !== rightNum)
51
+ return leftNum - rightNum;
52
+ continue;
53
+ }
54
+ if (leftNum !== undefined)
55
+ return -1;
56
+ if (rightNum !== undefined)
57
+ return 1;
58
+ if (left !== right)
59
+ return left < right ? -1 : 1;
60
+ }
61
+ return 0;
62
+ }
63
+ /** Whether `version` satisfies `range`; false for malformed input rather than a throw. */
64
+ export function satisfiesRange(version, range) {
65
+ const v = parseVersion(version);
66
+ if (v === undefined || !isVersionRange(range))
67
+ return false;
68
+ if (range === '*')
69
+ return true;
70
+ const [lowerText, upperText] = range.split(' ');
71
+ const operator = /^[\^~]|^>=/.exec(lowerText)?.[0] ?? '';
72
+ const lower = parseVersion(lowerText.slice(operator.length));
73
+ if (compare(v, lower) < 0)
74
+ return false;
75
+ if (operator === '')
76
+ return compare(v, lower) === 0;
77
+ let upper;
78
+ if (upperText !== undefined)
79
+ upper = parseVersion(upperText.slice(1));
80
+ else if (operator === '^') {
81
+ const [major, minor] = lower.triple;
82
+ upper = major > 0 ? { triple: [major + 1, 0, 0] } : minor > 0 ? { triple: [0, minor + 1, 0] } : { triple: [0, 0, lower.triple[2] + 1] };
83
+ }
84
+ else if (operator === '~')
85
+ upper = { triple: [lower.triple[0], lower.triple[1] + 1, 0] };
86
+ return upper === undefined || compare(v, upper) < 0;
87
+ }
88
+ //# sourceMappingURL=semver-range.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"semver-range.js","sourceRoot":"","sources":["../src/semver-range.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@relayflows/sdk",
3
- "version": "2.0.24",
3
+ "version": "2.0.25",
4
4
  "description": "TypeScript-first authoring SDK for Relayflows. Compiles specs; speaks the journal protocol.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -49,7 +49,7 @@
49
49
  "@modelcontextprotocol/sdk": "^1.30.0",
50
50
  "@relayfile/adapter-core": "0.5.26",
51
51
  "@relayfile/relay-helpers": "0.4.11",
52
- "@relayflows/surface": "2.0.24",
52
+ "@relayflows/surface": "2.0.25",
53
53
  "@types/js-yaml": "^4.0.9",
54
54
  "ai-hist": "0.4.1",
55
55
  "ajv": "^8.17.1",
@@ -44,6 +44,8 @@ import {
44
44
  } from './authored-flow-operation.js';
45
45
  import { AuthoredFlowLifecycle } from './authored-flow-lifecycle.js';
46
46
  import { JournalClient } from './journal-client.js';
47
+ import { createHookEvaluator } from './authored-hooks.js';
48
+ import { probeFlowExtension, type LoadedFlowExtension } from './flow-extension-loader.js';
47
49
  import type {
48
50
  CompletionReason as ProtocolCompletionReason,
49
51
  RunCompletionReason as ProtocolRunCompletionReason,
@@ -148,6 +150,8 @@ export interface ExecuteAuthoredFlowOptions {
148
150
  readonly localAgentStream?: string;
149
151
  /** Durable kernel root that owns this body's child admission identities. */
150
152
  readonly rootRunId?: string;
153
+ /** Installed flow-extension plugins, in lock order, so `f.hook` can AND-compose them. */
154
+ readonly extensions?: readonly LoadedFlowExtension[];
151
155
  }
152
156
 
153
157
  export async function executeAuthoredFlow<Input = undefined>(
@@ -171,7 +175,7 @@ export async function executeAuthoredFlow<Input = undefined>(
171
175
  ...(options.onWait !== undefined ? { onWait: options.onWait } : {}),
172
176
  };
173
177
  const definition = getDefinition<Input>(handle);
174
- const headerFields = Object.keys(definition.header).filter(key => key !== 'tools' && key !== 'budget' && key !== 'memory');
178
+ const headerFields = Object.keys(definition.header).filter(key => key !== 'tools' && key !== 'budget' && key !== 'memory' && key !== 'version' && key !== 'hooks');
175
179
  if (definition.header.tools && Object.keys(definition.header.tools).some(key => !['mcp', ...helperProviders.map(p => p.namespace)].includes(key))) headerFields.push('tools');
176
180
  if (definition.header.tools?.relayfile !== undefined) headerFields.push('tools.relayfile');
177
181
  const helperPreflight = checkSlackHelpers(definition);
@@ -188,6 +192,9 @@ export async function executeAuthoredFlow<Input = undefined>(
188
192
 
189
193
  const checkedMcp = await checkMcpHeader(definition, flowPath);
190
194
  if (!checkedMcp.report.ok) throw new McpPreflightError(checkedMcp.report);
195
+ for (const extension of options.extensions ?? []) {
196
+ if (extension.manifest !== undefined) await probeFlowExtension(extension.manifest);
197
+ }
191
198
 
192
199
  const budget = new AuthoredBudget(definition.header.budget);
193
200
  if (definition.header.memory?.agent === true) {
@@ -349,6 +356,17 @@ export async function executeAuthoredFlow<Input = undefined>(
349
356
  ));
350
357
  }
351
358
 
359
+ const evaluateHook = createHookEvaluator({
360
+ journal,
361
+ ...(options.rootRunId === undefined ? {} : { rootRunId: options.rootRunId }),
362
+ flowName: definition.name,
363
+ declared: definition.header?.hooks ?? [],
364
+ extensions: options.extensions ?? [],
365
+ peekStep: () => nextStep,
366
+ restoreStep: (step) => { nextStep = step; },
367
+ ...(options.signal === undefined ? {} : { signal: options.signal }),
368
+ });
369
+
352
370
  const context: Ctx = {
353
371
  ...createHelpers(<T>(call: HelperCall): Step<T> => {
354
372
  const verb = `${call.provider}.${call.verb}`;
@@ -477,6 +495,23 @@ export async function executeAuthoredFlow<Input = undefined>(
477
495
  assertOperationAllowed('dispatch', definition.name, requestedCompletion);
478
496
  throw unsupportedVerb('dispatch');
479
497
  },
498
+ hook(name, input) {
499
+ assertOperationAllowed('hook', definition.name, requestedCompletion);
500
+ const id = `hook-${nextStep++}`;
501
+ const snapshot = snapshotJsonValue(input, 'f.hook input');
502
+ return trackStep(authoredSteps, new AuthoredFlowOperation<boolean>(
503
+ id, 'hook',
504
+ () => assertOperationAllowed('hook', definition.name, requestedCompletion),
505
+ async () => {
506
+ const verdict = await evaluateHook(id, name, snapshot, context);
507
+ const record = { hook: name, step: id, verdict: verdict ? 'pass' : 'fail' };
508
+ const literal = `'${JSON.stringify(record).replaceAll("'", "'\\''")}'`;
509
+ await observeStep(id, 'deterministic', () => lowerDeterministic(id, `printf '%s' ${literal}`, false), options.onProgress);
510
+ return verdict;
511
+ },
512
+ lifecycle,
513
+ ));
514
+ },
480
515
  done(reason) {
481
516
  if (!isSurfaceFlowCompletionReason(reason)) {
482
517
  throw new AuthoredFlowExecutionError(
@@ -9,6 +9,9 @@ import {
9
9
  type AuthoredFlowDefinition,
10
10
  type FlowHandle,
11
11
  } from './authored-flow.js';
12
+ import { canonicalize } from './canonical.js';
13
+ import { composeDefinition, loadFlowExtensions, parseHooksExport, type ImportedFlow, type LoadedFlowExtension } from './flow-extension-loader.js';
14
+ import type { RuntimeVersions } from './flow-extension-compat.js';
12
15
 
13
16
  export class AuthoredFlowLoadError extends Error {
14
17
  constructor(message: string, readonly kind: 'invalid_spec' | 'use_not_found' | 'use_invalid' | 'use_cycle' = 'invalid_spec') {
@@ -40,8 +43,20 @@ export interface LoadedAuthoredFlow {
40
43
  readonly getDefinition: GetFlowDefinition;
41
44
  /** Exact Surface package/runtime that owns the handle's WeakMap identity. */
42
45
  readonly surfaceAuthority: SurfaceModuleAuthority;
43
- /** Dependency-first load order, each canonical absolute path appearing once. */
46
+ /**
47
+ * Dependency-first load order, each canonical absolute path appearing once.
48
+ * Flow-extension entries follow the root, so a graph of one node still
49
+ * means "one self-contained source" to the hosted paths that require it.
50
+ */
44
51
  readonly graph: readonly LoadedAuthoredFlowNode[];
52
+ /** Schema-2 flow extensions composed onto the root, in lock order; empty when the project declares none. */
53
+ readonly extensions: readonly LoadedFlowExtension[];
54
+ }
55
+
56
+ export interface LoadAuthoredFlowOptions {
57
+ /** `compose` (default) verifies and appends the project's flow extensions; `none` loads the root alone. */
58
+ readonly extensions?: 'compose' | 'none';
59
+ readonly versions?: RuntimeVersions;
45
60
  }
46
61
 
47
62
  export interface LoadedAuthoredFlowNode {
@@ -53,7 +68,7 @@ export interface LoadedAuthoredFlowNode {
53
68
  }
54
69
 
55
70
  /** Import and validate a direct-run module without executing its authored body. */
56
- export async function loadAuthoredFlow(path: string): Promise<LoadedAuthoredFlow> {
71
+ export async function loadAuthoredFlow(path: string, options: LoadAuthoredFlowOptions = {}): Promise<LoadedAuthoredFlow> {
57
72
  const loaded = new Map<string, LoadedAuthoredFlowNode>();
58
73
  const visiting = new Set<string>();
59
74
  async function visit(sourcePath: string, isRoot = false): Promise<LoadedAuthoredFlowNode> {
@@ -98,11 +113,39 @@ export async function loadAuthoredFlow(path: string): Promise<LoadedAuthoredFlow
98
113
  }
99
114
  }
100
115
  const root = await visit(resolve(path), true);
101
- return Object.freeze({ sourcePath: root.path, handle: root.handle, getDefinition: root.getDefinition,
102
- surfaceAuthority: root.surfaceAuthority, graph: Object.freeze([...loaded.values()]) });
116
+ const graph = [...loaded.values()];
117
+ if (options.extensions === 'none') {
118
+ return Object.freeze({ sourcePath: root.path, handle: root.handle, getDefinition: root.getDefinition,
119
+ surfaceAuthority: root.surfaceAuthority, graph: Object.freeze(graph), extensions: Object.freeze([]) });
120
+ }
121
+ // Extensions are verified (lock, digest, manifest hash, compat) before their
122
+ // entry is imported, and are composed after the root in lock order. The
123
+ // composed definition is served for the root handle only; every other
124
+ // handle keeps the surface copy's own answer.
125
+ const baseDefinition = root.getDefinition(root.handle);
126
+ const extensions = await loadFlowExtensions(root.path, { definition: baseDefinition, surfaceAuthority: root.surfaceAuthority }, {
127
+ importFlow: importAuthoredFlow,
128
+ sameAuthority: (a, b) => canonicalize(a) === canonicalize(b),
129
+ ...(options.versions === undefined ? {} : { versions: options.versions }),
130
+ });
131
+ if (extensions.length === 0) {
132
+ return Object.freeze({ sourcePath: root.path, handle: root.handle, getDefinition: root.getDefinition,
133
+ surfaceAuthority: root.surfaceAuthority, graph: Object.freeze(graph), extensions });
134
+ }
135
+ const composed = composeDefinition(baseDefinition, extensions);
136
+ const getDefinition: GetFlowDefinition = <Input = unknown>(handle: FlowHandle) =>
137
+ (handle === root.handle ? composed : root.getDefinition(handle)) as AuthoredFlowDefinition<Input>;
138
+ for (const extension of extensions) {
139
+ // The node carries the accessor the entry's own surface copy handed back:
140
+ // the root's accessor answers for the root's WeakMap only.
141
+ graph.push(Object.freeze({ path: extension.entryPath, handle: extension.handle, getDefinition: extension.getDefinition,
142
+ surfaceAuthority: root.surfaceAuthority, use: Object.freeze([]) }));
143
+ }
144
+ return Object.freeze({ sourcePath: root.path, handle: root.handle, getDefinition,
145
+ surfaceAuthority: root.surfaceAuthority, graph: Object.freeze(graph), extensions });
103
146
  }
104
147
 
105
- async function importAuthoredFlow(path: string): Promise<Pick<LoadedAuthoredFlow, 'handle' | 'getDefinition' | 'surfaceAuthority'>> {
148
+ async function importAuthoredFlow(path: string): Promise<ImportedFlow<SurfaceModuleAuthority>> {
106
149
  const absolutePath = resolve(path);
107
150
  try {
108
151
  accessSync(absolutePath, constants.R_OK);
@@ -128,7 +171,7 @@ async function importAuthoredFlow(path: string): Promise<Pick<LoadedAuthoredFlow
128
171
  `Flow "${path}" must default-export flow(...): ${errorMessage(error)}`,
129
172
  );
130
173
  }
131
- return { handle, getDefinition, surfaceAuthority };
174
+ return { handle, getDefinition, surfaceAuthority, hooks: parseHooksExport(authoredModule, path) };
132
175
  }
133
176
 
134
177
  /**
@@ -0,0 +1,136 @@
1
+ import type { Ctx } from '@relayflows/surface';
2
+ import { AuthoredFlowExecutionError } from './authored-flow-error.js';
3
+ import type { LoadedFlowExtension } from './flow-extension-loader.js';
4
+ import type { JournalClient } from './journal-client.js';
5
+
6
+ const HOOK_STREAM = 'hooks';
7
+
8
+ export interface HookRecord {
9
+ hook: string;
10
+ step: string;
11
+ plugin: string | null;
12
+ verdict: 'pass' | 'fail' | 'noop';
13
+ because?: string;
14
+ /** Parent `nextStep` after this verdict, so resume does not reuse inner step ids. */
15
+ afterStep?: number;
16
+ }
17
+
18
+ function recordKey(record: HookRecord): string {
19
+ return `${record.step}:${record.plugin ?? 'noop'}`;
20
+ }
21
+
22
+ /**
23
+ * AND-compose installed hook implementations in lock order. Verdicts are
24
+ * appended to the root run's `hooks` stream before the boolean is returned,
25
+ * so a resume reads the recorded verdict and never re-runs the closure.
26
+ * No implementations is a journaled no-op returning true. A false short-circuits.
27
+ */
28
+ export function createHookEvaluator(options: {
29
+ readonly journal: JournalClient;
30
+ readonly rootRunId?: string;
31
+ readonly flowName: string;
32
+ readonly declared: readonly string[];
33
+ readonly extensions: readonly LoadedFlowExtension[];
34
+ readonly peekStep?: () => number;
35
+ readonly restoreStep?: (step: number) => void;
36
+ readonly signal?: AbortSignal;
37
+ }): (id: string, name: string, input: unknown, context: Ctx) => Promise<boolean> {
38
+ let recorded: Promise<Map<string, HookRecord>> | undefined;
39
+
40
+ async function load(): Promise<Map<string, HookRecord>> {
41
+ const verdicts = new Map<string, HookRecord>();
42
+ if (options.rootRunId === undefined) return verdicts;
43
+ let offset = 0;
44
+ for (;;) {
45
+ const page = await options.journal.streamRead(options.rootRunId, HOOK_STREAM, offset, 1000);
46
+ for (const message of page.messages) {
47
+ const raw = (message as { message?: unknown }).message ?? message;
48
+ if (typeof raw !== 'object' || raw === null) continue;
49
+ const record = raw as HookRecord;
50
+ if (typeof record.hook !== 'string' || typeof record.step !== 'string') continue;
51
+ if (record.verdict !== 'pass' && record.verdict !== 'fail' && record.verdict !== 'noop') continue;
52
+ if (record.plugin !== null && typeof record.plugin !== 'string') continue;
53
+ verdicts.set(recordKey(record), record);
54
+ }
55
+ if (page.messages.length === 0 || page.next_offset <= offset) break;
56
+ offset = page.next_offset;
57
+ }
58
+ return verdicts;
59
+ }
60
+
61
+ async function lookup(key: string): Promise<HookRecord | undefined> {
62
+ if (options.rootRunId === undefined) return undefined;
63
+ recorded ??= load();
64
+ return (await recorded).get(key);
65
+ }
66
+
67
+ async function append(record: HookRecord): Promise<void> {
68
+ if (options.rootRunId === undefined) return;
69
+ recorded ??= load();
70
+ await options.journal.streamAppend(options.rootRunId, HOOK_STREAM, record);
71
+ (await recorded).set(recordKey(record), record);
72
+ }
73
+
74
+ return async (id, name, input, context) => {
75
+ if (!options.declared.includes(name)) {
76
+ throw new AuthoredFlowExecutionError(
77
+ 'unsupported_verb',
78
+ `hook "${name}" is not declared by flow "${options.flowName}"`,
79
+ );
80
+ }
81
+ const impls = options.extensions.filter(extension => extension.hooks[name] !== undefined);
82
+ if (impls.length === 0) {
83
+ const existing = await lookup(`${id}:noop`);
84
+ const record = existing ?? { hook: name, step: id, plugin: null, verdict: 'noop' as const };
85
+ if (existing === undefined) await append(record);
86
+ else if (existing.afterStep !== undefined) options.restoreStep?.(existing.afterStep);
87
+ return true;
88
+ }
89
+ for (const extension of impls) {
90
+ const key = `${id}:${extension.name}`;
91
+ let record = await lookup(key);
92
+ if (record === undefined) {
93
+ let verdict = false;
94
+ let because: string | undefined;
95
+ try {
96
+ verdict = await boundHook(extension.hooks[name]!(context, input), options.signal) === true;
97
+ } catch (error) {
98
+ // Cancellation is control-plane state, not a plugin verdict. The
99
+ // shared execution signal also stops tracked f.run/f.agent work;
100
+ // rethrow so a later resume may run the hook again instead of
101
+ // replaying a permanent decline caused by an interrupted attempt.
102
+ if (options.signal?.aborted) throw error;
103
+ verdict = false;
104
+ because = error instanceof Error ? error.message : String(error);
105
+ }
106
+ record = {
107
+ hook: name, step: id, plugin: extension.name,
108
+ verdict: verdict ? 'pass' : 'fail',
109
+ ...(because === undefined ? {} : { because }),
110
+ ...(options.peekStep === undefined ? {} : { afterStep: options.peekStep() }),
111
+ };
112
+ await append(record);
113
+ } else if (record.afterStep !== undefined) {
114
+ options.restoreStep?.(record.afterStep);
115
+ }
116
+ if (record.verdict === 'fail') return false;
117
+ }
118
+ return true;
119
+ };
120
+ }
121
+
122
+ async function boundHook(run: Promise<boolean>, signal?: AbortSignal): Promise<boolean> {
123
+ // The parent run's wallclock budget owns the deadline. A second timer here
124
+ // would not be the signal captured by the hook's Ctx operations, allowing
125
+ // those operations to continue after this wrapper returned.
126
+ if (signal === undefined) return await run;
127
+ if (signal.aborted) throw signal.reason ?? new Error('hook cancelled');
128
+ return await new Promise<boolean>((resolve, reject) => {
129
+ const onAbort = () => reject(signal.reason ?? new Error('hook cancelled'));
130
+ signal.addEventListener('abort', onAbort, { once: true });
131
+ run.then(
132
+ value => { signal.removeEventListener('abort', onAbort); resolve(value); },
133
+ error => { signal.removeEventListener('abort', onAbort); reject(error); },
134
+ );
135
+ });
136
+ }
@@ -72,6 +72,7 @@ try {
72
72
  request.metadata.inputPresent ? request.metadata.input : undefined, {
73
73
  getDefinition: loaded.getDefinition, dataDir: request.dataDir,
74
74
  flowPath: request.metadata.flowPath, rootRunId: request.rootRunId,
75
+ extensions: loaded.extensions,
75
76
  localAgentStream: request.localAgentStream, signal: controller.signal,
76
77
  onProgress: event => send({ type: 'progress', event }),
77
78
  onWait: event => send({ type: 'wait', event }),
@@ -21,6 +21,12 @@ import { isSurfaceCompletionReason } from './authored-step-output.js';
21
21
 
22
22
  const ROOT_KIND = 'relayflows.authored-root.v1';
23
23
 
24
+ export interface AuthoredRootExtension {
25
+ readonly name: string;
26
+ readonly digest: string;
27
+ readonly ref: string;
28
+ }
29
+
24
30
  export interface AuthoredRootMetadata {
25
31
  readonly kind: typeof ROOT_KIND;
26
32
  readonly flowName: string;
@@ -28,6 +34,8 @@ export interface AuthoredRootMetadata {
28
34
  readonly sourceSha256: string;
29
35
  readonly surface: SurfaceModuleAuthority;
30
36
  readonly sources: readonly AuthoredRootSourceAuthority[];
37
+ /** Plugin-level provenance in lock order; empty when the project declares none. */
38
+ readonly extensions: readonly AuthoredRootExtension[];
31
39
  readonly localAgentStream?: string;
32
40
  readonly inputPresent: boolean;
33
41
  readonly input?: unknown;
@@ -68,6 +76,9 @@ export async function executeDurableAuthoredFlow(
68
76
  sourceSha256: sha256(source),
69
77
  surface: loaded.surfaceAuthority,
70
78
  sources: Object.freeze(sources),
79
+ extensions: Object.freeze((loaded.extensions ?? []).map(extension => Object.freeze({
80
+ name: extension.name, digest: extension.digest, ref: extension.ref,
81
+ }))),
71
82
  ...(options.localAgentStream === undefined ? {} : { localAgentStream: options.localAgentStream }),
72
83
  inputPresent: input !== undefined,
73
84
  ...(input === undefined ? {} : { input: jsonSnapshot(input, 'authored root input') }),
@@ -172,7 +183,7 @@ export async function readAuthoredRootMetadata(
172
183
  if (!isRootMetadata(value)) {
173
184
  throw new Error('authored root journal has malformed authority metadata');
174
185
  }
175
- return value;
186
+ return Object.freeze({ ...value, extensions: value.extensions ?? [] });
176
187
  }
177
188
 
178
189
  async function driveRoot(
@@ -203,6 +214,7 @@ async function driveRoot(
203
214
  flowPath: metadata.flowPath,
204
215
  localAgentStream: options.localAgentStream,
205
216
  rootRunId: dispatch.run_id,
217
+ extensions: loaded.extensions,
206
218
  ...options.lifecycle,
207
219
  signal: callerSignal === undefined
208
220
  ? rootSignal
@@ -409,10 +421,19 @@ function isRootMetadata(value: unknown): value is AuthoredRootMetadata {
409
421
  && typeof surface.version === 'string' && /^[a-f0-9]{64}$/.test(surface.packageSha256 ?? '')
410
422
  && /^[a-f0-9]{64}$/.test(surface.runtimeSha256 ?? '')
411
423
  && Array.isArray(sources) && sources.length > 0 && sources.every(isRootSourceAuthority)
424
+ && (root.extensions === undefined || (Array.isArray(root.extensions) && root.extensions.every(isRootExtension)))
412
425
  && (root.localAgentStream === undefined
413
426
  || /^local-agent-[a-f0-9-]+$/.test(root.localAgentStream));
414
427
  }
415
428
 
429
+ function isRootExtension(value: unknown): value is AuthoredRootExtension {
430
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) return false;
431
+ const extension = value as Partial<AuthoredRootExtension>;
432
+ return typeof extension.name === 'string' && extension.name.length > 0
433
+ && /^[a-f0-9]{64}$/.test(extension.digest ?? '')
434
+ && typeof extension.ref === 'string' && extension.ref.startsWith('github:');
435
+ }
436
+
416
437
  function isRootSourceAuthority(value: unknown): value is AuthoredRootSourceAuthority {
417
438
  if (typeof value !== 'object' || value === null || Array.isArray(value)) return false;
418
439
  const source = value as Partial<AuthoredRootSourceAuthority>;
@@ -35,6 +35,12 @@ export async function loadPinnedAuthoredSource(metadata: AuthoredRootMetadata, i
35
35
  if (canonicalize(loadedSources) !== canonicalize(metadata.sources)) {
36
36
  throw new Error('authored root declared source graph authority mismatch');
37
37
  }
38
+ const loadedExtensions = (loaded.extensions ?? []).map(extension => ({
39
+ name: extension.name, digest: extension.digest, ref: extension.ref,
40
+ }));
41
+ if (canonicalize(loadedExtensions) !== canonicalize(metadata.extensions ?? [])) {
42
+ throw new Error('authored root declared extension authority mismatch');
43
+ }
38
44
  return loaded;
39
45
  }
40
46
 
@@ -0,0 +1,85 @@
1
+ import { readFile, readdir } from 'node:fs/promises';
2
+ import { dirname, join } from 'node:path';
3
+ import type { BundleFile } from './bundle.js';
4
+ import { safePath, sha256 } from './bundle.js';
5
+ import { findPluginProject } from './plugin-loader.js';
6
+ import { parsePluginLock, readPluginLock, reconcileDeclaredExtensions, type PluginLock } from './plugin-lock.js';
7
+ import { PluginError } from './plugin-manifest.js';
8
+ import { pluginStoreDirectory, readStoredPluginFiles, verifyStoredPlugin } from './plugin-store.js';
9
+
10
+ const EMPTY_LOCK: PluginLock = Object.freeze({ version: 2, plugins: Object.freeze([]) });
11
+
12
+ /**
13
+ * Plugin provenance and bytes to put in a sealed bundle: `lockfile.json` is
14
+ * the same v2 lock the project records, and each extension's materialized
15
+ * files land under `plugins/<name>/…`. No project / no github: plugins →
16
+ * `{ version: 2, plugins: [] }` and no files. Fail closed on lock/store drift.
17
+ */
18
+ export async function collectBundleExtensions(flowPath: string): Promise<{ lock: PluginLock; files: BundleFile[] }> {
19
+ const root = findPluginProject(dirname(flowPath));
20
+ if (root === undefined) return { lock: EMPTY_LOCK, files: [] };
21
+ const locked = reconcileDeclaredExtensions(root);
22
+ const files: BundleFile[] = [];
23
+ for (const { entry } of locked) {
24
+ const stored = await readStoredPluginFiles(pluginStoreDirectory(root, entry.name, entry.digest), entry.digest);
25
+ for (const file of stored) files.push({ path: `plugins/${entry.name}/${file.path}`, data: file.data });
26
+ }
27
+ return { lock: readPluginLock(root), files };
28
+ }
29
+
30
+ /**
31
+ * After `verifyBundle` has hashed every payload file, check that `lockfile.json`
32
+ * is a v2 plugin lock (or a legacy v1 `{version:1, adapters}` with no plugins)
33
+ * and that each locked extension's store digest matches `plugins/<name>/manifest.json`.
34
+ */
35
+ export async function verifyBundlePluginLock(bundle: string): Promise<void> {
36
+ let parsed: unknown;
37
+ try { parsed = JSON.parse(await readFile(join(bundle, 'lockfile.json'), 'utf8')); }
38
+ catch { throw new Error('lockfile.json: not valid JSON'); }
39
+ if (isLegacyV1Lock(parsed) || isLegacyNpmLock(parsed)) {
40
+ await assertNoLegacyPluginPayload(bundle);
41
+ return;
42
+ }
43
+ let lock: PluginLock;
44
+ try { lock = parsePluginLock(parsed); }
45
+ catch (error) {
46
+ throw new Error(error instanceof PluginError ? error.message : 'lockfile.json: not a plugin lock');
47
+ }
48
+ for (const entry of lock.plugins) {
49
+ if (!safePath(entry.name) || entry.name.includes('/')) {
50
+ throw new Error(`lockfile.json: plugin name ${entry.name} is not a safe path component`);
51
+ }
52
+ const directory = join(bundle, 'plugins', entry.name);
53
+ await verifyStoredPlugin(directory, entry.digest);
54
+ let pluginManifest: Buffer;
55
+ try { pluginManifest = await readFile(join(directory, 'flows-plugin.json')); }
56
+ catch { throw new Error(`lockfile.json: plugin ${entry.name} is missing plugins/${entry.name}/flows-plugin.json`); }
57
+ if (sha256(pluginManifest) !== entry.manifestSha256) {
58
+ throw new Error(`lockfile.json: plugin ${entry.name} flows-plugin.json does not match the lockfile manifest hash`);
59
+ }
60
+ }
61
+ }
62
+
63
+ async function assertNoLegacyPluginPayload(bundle: string): Promise<void> {
64
+ let entries: string[];
65
+ try { entries = await readdir(join(bundle, 'plugins')); }
66
+ catch (error) {
67
+ if ((error as NodeJS.ErrnoException).code === 'ENOENT') return;
68
+ throw error;
69
+ }
70
+ if (entries.length > 0) {
71
+ throw new Error('lockfile.json: legacy locks cannot authenticate plugins/ payloads');
72
+ }
73
+ }
74
+
75
+ function isLegacyV1Lock(value: unknown): boolean {
76
+ return typeof value === 'object' && value !== null && !Array.isArray(value)
77
+ && (value as { version?: unknown }).version === 1
78
+ && Array.isArray((value as { adapters?: unknown }).adapters);
79
+ }
80
+
81
+ function isLegacyNpmLock(value: unknown): boolean {
82
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) return false;
83
+ const lock = value as { lockfileVersion?: unknown; packages?: unknown };
84
+ return (lock.lockfileVersion === 2 || lock.lockfileVersion === 3) && typeof lock.packages === 'object' && lock.packages !== null;
85
+ }
@@ -60,7 +60,7 @@ process.stdout.write(JSON.stringify({ spec, authored }));
60
60
  '--no-compile-autoload-bunfig', '--outfile', executable, input], directory);
61
61
  const files: BundleFile[] = [
62
62
  { path: 'flow', data: await readFile(executable) },
63
- { path: 'lockfile.json', data: canonicalize(lock) },
63
+ { path: 'package-lock.json', data: canonicalize(lock) },
64
64
  ];
65
65
  return { spec, authored: inspected.authored === true, compiler, files };
66
66
  } finally { await rm(staging, { recursive: true, force: true }); }
package/src/bundle.ts CHANGED
@@ -18,12 +18,24 @@ export function sha256(data: Uint8Array | string): string {
18
18
  return createHash('sha256').update(data).digest('hex');
19
19
  }
20
20
 
21
- function safePath(path: string): boolean {
21
+ /** A bundle-relative path: no empty, `.`, or `..` component, no backslash, NUL, or drive colon. */
22
+ export function safePath(path: string): boolean {
22
23
  return path.length > 0 && !path.includes('\\') && !path.includes('\0')
23
24
  && path.split('/').every(part => part !== '' && part !== '.' && part !== '..')
24
25
  && !path.includes(':');
25
26
  }
26
27
 
28
+ /**
29
+ * The canonical entry list whose sha256 is a payload's content digest. Shared
30
+ * by sealed flow bundles and materialized plugins so `@sha256:` means one
31
+ * thing everywhere: the digest of `[{bytes,path,sha256}]`, sorted by path.
32
+ */
33
+ export function payloadManifest(files: readonly { path: string; data: Uint8Array }[]): string {
34
+ return canonicalize([...files]
35
+ .sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0)
36
+ .map(file => ({ path: file.path, sha256: sha256(file.data), bytes: file.data.length })));
37
+ }
38
+
27
39
  /** Manifest and identity are envelopes, excluded to avoid circular hashing. */
28
40
  export async function sealBundle(options: BundleOptions): Promise<string> {
29
41
  if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(options.name)) {
@@ -41,9 +53,7 @@ export async function sealBundle(options: BundleOptions): Promise<string> {
41
53
  for (const required of ['spec.canonical.json', 'preflight.json', 'lockfile.json']) {
42
54
  if (!paths.has(required)) throw new Error(`${required}: missing bundle file`);
43
55
  }
44
- const manifest = canonicalize(files.map(file => ({
45
- path: file.path, sha256: sha256(file.data), bytes: file.data.length,
46
- })));
56
+ const manifest = payloadManifest(files);
47
57
  const digest = sha256(manifest);
48
58
  const out = resolve(options.out);
49
59
  const target = join(out, `${options.name}@sha256:${digest}`);