@relayflows/sdk 2.0.28 → 2.0.30

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 (120) hide show
  1. package/dist/authored-flow-executor.d.ts.map +1 -1
  2. package/dist/authored-flow-executor.js +10 -1
  3. package/dist/authored-flow-executor.js.map +1 -1
  4. package/dist/authored-step-index.d.ts +6 -0
  5. package/dist/authored-step-index.d.ts.map +1 -1
  6. package/dist/authored-step-index.js +42 -14
  7. package/dist/authored-step-index.js.map +1 -1
  8. package/dist/bundle-extensions.d.ts.map +1 -1
  9. package/dist/bundle-extensions.js +4 -7
  10. package/dist/bundle-extensions.js.map +1 -1
  11. package/dist/bundle.d.ts.map +1 -1
  12. package/dist/bundle.js +41 -7
  13. package/dist/bundle.js.map +1 -1
  14. package/dist/canonical.d.ts.map +1 -1
  15. package/dist/canonical.js +18 -6
  16. package/dist/canonical.js.map +1 -1
  17. package/dist/cli/status.d.ts +12 -0
  18. package/dist/cli/status.d.ts.map +1 -1
  19. package/dist/cli/status.js +39 -1
  20. package/dist/cli/status.js.map +1 -1
  21. package/dist/flow-extension-compat.d.ts.map +1 -1
  22. package/dist/flow-extension-compat.js +26 -8
  23. package/dist/flow-extension-compat.js.map +1 -1
  24. package/dist/flow-extension-loader.d.ts +7 -0
  25. package/dist/flow-extension-loader.d.ts.map +1 -1
  26. package/dist/flow-extension-loader.js +48 -22
  27. package/dist/flow-extension-loader.js.map +1 -1
  28. package/dist/fs-descriptor.d.ts +17 -0
  29. package/dist/fs-descriptor.d.ts.map +1 -0
  30. package/dist/fs-descriptor.js +96 -0
  31. package/dist/fs-descriptor.js.map +1 -0
  32. package/dist/hosted-base-snapshot.d.ts +25 -0
  33. package/dist/hosted-base-snapshot.d.ts.map +1 -0
  34. package/dist/hosted-base-snapshot.js +278 -0
  35. package/dist/hosted-base-snapshot.js.map +1 -0
  36. package/dist/hosted-extension-declarations.d.ts +10 -0
  37. package/dist/hosted-extension-declarations.d.ts.map +1 -0
  38. package/dist/hosted-extension-declarations.js +272 -0
  39. package/dist/hosted-extension-declarations.js.map +1 -0
  40. package/dist/hosted-extension-isolation.d.ts +82 -0
  41. package/dist/hosted-extension-isolation.d.ts.map +1 -0
  42. package/dist/hosted-extension-isolation.js +322 -0
  43. package/dist/hosted-extension-isolation.js.map +1 -0
  44. package/dist/hosted-extension-manifest.d.ts +9 -0
  45. package/dist/hosted-extension-manifest.d.ts.map +1 -0
  46. package/dist/hosted-extension-manifest.js +235 -0
  47. package/dist/hosted-extension-manifest.js.map +1 -0
  48. package/dist/hosted-extension-protocol.d.ts +16 -0
  49. package/dist/hosted-extension-protocol.d.ts.map +1 -0
  50. package/dist/hosted-extension-protocol.js +224 -0
  51. package/dist/hosted-extension-protocol.js.map +1 -0
  52. package/dist/hosted-extension-runtime.d.ts +30 -0
  53. package/dist/hosted-extension-runtime.d.ts.map +1 -0
  54. package/dist/hosted-extension-runtime.js +167 -0
  55. package/dist/hosted-extension-runtime.js.map +1 -0
  56. package/dist/hosted-extension-sandbox-source.d.ts +9 -0
  57. package/dist/hosted-extension-sandbox-source.d.ts.map +1 -0
  58. package/dist/hosted-extension-sandbox-source.js +95 -0
  59. package/dist/hosted-extension-sandbox-source.js.map +1 -0
  60. package/dist/hosted-extension-sandbox.d.ts +39 -0
  61. package/dist/hosted-extension-sandbox.d.ts.map +1 -0
  62. package/dist/hosted-extension-sandbox.js +418 -0
  63. package/dist/hosted-extension-sandbox.js.map +1 -0
  64. package/dist/hosted-project.d.ts +3 -0
  65. package/dist/hosted-project.d.ts.map +1 -0
  66. package/dist/hosted-project.js +19 -0
  67. package/dist/hosted-project.js.map +1 -0
  68. package/dist/hosted-promise-safety.d.ts +12 -0
  69. package/dist/hosted-promise-safety.d.ts.map +1 -0
  70. package/dist/hosted-promise-safety.js +63 -0
  71. package/dist/hosted-promise-safety.js.map +1 -0
  72. package/dist/index.d.ts +2 -0
  73. package/dist/index.d.ts.map +1 -1
  74. package/dist/index.js +2 -0
  75. package/dist/index.js.map +1 -1
  76. package/dist/intrinsic-array.d.ts +3 -0
  77. package/dist/intrinsic-array.d.ts.map +1 -0
  78. package/dist/intrinsic-array.js +14 -0
  79. package/dist/intrinsic-array.js.map +1 -0
  80. package/dist/json-value.d.ts +6 -1
  81. package/dist/json-value.d.ts.map +1 -1
  82. package/dist/json-value.js +155 -36
  83. package/dist/json-value.js.map +1 -1
  84. package/dist/llm-worker.d.ts +9 -0
  85. package/dist/llm-worker.d.ts.map +1 -1
  86. package/dist/llm-worker.js +15 -1
  87. package/dist/llm-worker.js.map +1 -1
  88. package/dist/plugin-store.d.ts +8 -2
  89. package/dist/plugin-store.d.ts.map +1 -1
  90. package/dist/plugin-store.js +233 -56
  91. package/dist/plugin-store.js.map +1 -1
  92. package/dist/semver-range.d.ts.map +1 -1
  93. package/dist/semver-range.js +27 -15
  94. package/dist/semver-range.js.map +1 -1
  95. package/package.json +2 -2
  96. package/src/authored-flow-executor.ts +10 -1
  97. package/src/authored-step-index.ts +46 -12
  98. package/src/bundle-extensions.ts +6 -5
  99. package/src/bundle.ts +52 -7
  100. package/src/canonical.ts +20 -6
  101. package/src/cli/status.ts +50 -1
  102. package/src/flow-extension-compat.ts +32 -7
  103. package/src/flow-extension-loader.ts +56 -21
  104. package/src/fs-descriptor.ts +127 -0
  105. package/src/hosted-base-snapshot.ts +351 -0
  106. package/src/hosted-extension-declarations.ts +387 -0
  107. package/src/hosted-extension-isolation.ts +457 -0
  108. package/src/hosted-extension-manifest.ts +249 -0
  109. package/src/hosted-extension-protocol.ts +262 -0
  110. package/src/hosted-extension-runtime.ts +266 -0
  111. package/src/hosted-extension-sandbox-source.ts +94 -0
  112. package/src/hosted-extension-sandbox.ts +489 -0
  113. package/src/hosted-project.ts +18 -0
  114. package/src/hosted-promise-safety.ts +68 -0
  115. package/src/index.ts +18 -0
  116. package/src/intrinsic-array.ts +14 -0
  117. package/src/json-value.ts +184 -32
  118. package/src/llm-worker.ts +15 -1
  119. package/src/plugin-store.ts +323 -56
  120. package/src/semver-range.ts +39 -14
@@ -0,0 +1,266 @@
1
+ import { realpath } from 'node:fs/promises';
2
+ import { dirname, join, resolve } from 'node:path';
3
+ import { canonicalize } from './canonical.js';
4
+ import {
5
+ createHostedBaseSnapshot,
6
+ hostedBaseSourceDigest,
7
+ removeHostedBaseSnapshot,
8
+ type HostedBaseSourceRoot,
9
+ } from './hosted-base-snapshot.js';
10
+ import { appendIntrinsicArray } from './intrinsic-array.js';
11
+ import {
12
+ declarationSignature,
13
+ hostedDeclaredExtensions,
14
+ } from './hosted-extension-declarations.js';
15
+ import { findHostedProject } from './hosted-project.js';
16
+ import {
17
+ assertHostedPromiseSafety,
18
+ frozenHostedPromiseValue,
19
+ } from './hosted-promise-safety.js';
20
+ import { PluginError } from './plugin-manifest.js';
21
+ import { pluginStoreDirectory, verifyStoredPlugin } from './plugin-store.js';
22
+
23
+ const INSTALLATION_AUTHORITY = new WeakSet<object>();
24
+ const BASE_AUTHORITY = new WeakSet<object>();
25
+ const REALPATH = realpath;
26
+ const PATH_DIRNAME = dirname;
27
+ const PATH_JOIN = join;
28
+ const PATH_RESOLVE = resolve;
29
+ const SOFTWARE_FACTORY_SHA256 = '49c993220b9c34fab2d4b0e51911656f62b8b657f534d988691960d45bb9d9b6';
30
+ const ARRAY_IS_ARRAY = Array.isArray;
31
+ const OBJECT_FREEZE = Object.freeze;
32
+ const WEAK_MAP_GET = Function.prototype.call.bind(WeakMap.prototype.get) as <K extends object, V>(
33
+ map: WeakMap<K, V>,
34
+ key: K,
35
+ ) => V | undefined;
36
+ const WEAK_MAP_SET = Function.prototype.call.bind(WeakMap.prototype.set) as <K extends object, V>(
37
+ map: WeakMap<K, V>,
38
+ key: K,
39
+ value: V,
40
+ ) => WeakMap<K, V>;
41
+ const WEAK_SET_ADD = Function.prototype.call.bind(WeakSet.prototype.add) as <T extends object>(
42
+ set: WeakSet<T>,
43
+ value: T,
44
+ ) => WeakSet<T>;
45
+ const WEAK_SET_HAS = Function.prototype.call.bind(WeakSet.prototype.has) as <T extends object>(
46
+ set: WeakSet<T>,
47
+ value: T,
48
+ ) => boolean;
49
+
50
+ interface RuntimeGeneration {
51
+ readonly origin: string;
52
+ readonly projectRoot: string | undefined;
53
+ declarations: string;
54
+ sourceRoots: readonly HostedBaseSourceRoot[];
55
+ sourceSha256: string;
56
+ }
57
+ const INSTALLATION_GENERATION = new WeakMap<object, RuntimeGeneration>();
58
+ const BASE_GENERATION = new WeakMap<object, RuntimeGeneration>();
59
+
60
+ export interface HostedExtensionArtifact {
61
+ readonly ref: string;
62
+ readonly name: string;
63
+ readonly version: string;
64
+ readonly directory: string;
65
+ readonly digest: string;
66
+ readonly manifestSha256: string;
67
+ }
68
+
69
+ export interface HostedExtensionInstallation {
70
+ readonly artifacts: readonly HostedExtensionArtifact[];
71
+ }
72
+
73
+ export interface HostedExtensionBase {
74
+ readonly name: string;
75
+ readonly version?: string;
76
+ }
77
+
78
+ export interface HostedExtensionRuntime {
79
+ readonly installation: HostedExtensionInstallation;
80
+ readonly base: HostedExtensionBase;
81
+ }
82
+
83
+ /** Load one base/installation generation that cannot be paired across redeploys. */
84
+ export async function loadHostedExtensionRuntime(flowPath: string): Promise<HostedExtensionRuntime> {
85
+ assertHostedPromiseSafety('plugin_source_invalid');
86
+ const origin = await REALPATH(PATH_RESOLVE(flowPath));
87
+ const generation = newGeneration(origin);
88
+ const loaded = await installationAt(origin, generation);
89
+ generation.declarations = loaded.declarations;
90
+ const base = await baseAt(origin, generation);
91
+ if (loaded.declarations !== declaredExtensions(generation).signature) {
92
+ throw new PluginError(
93
+ 'plugin_source_drift',
94
+ 'Hosted extension declarations changed while their runtime generation was loaded.',
95
+ );
96
+ }
97
+ await assertCurrentGeneration(generation);
98
+ OBJECT_FREEZE(generation);
99
+ return frozenHostedPromiseValue({ installation: loaded.installation, base });
100
+ }
101
+
102
+ /** @internal Metadata-only test seam; public hosted callers use the combined loader. */
103
+ export async function loadHostedExtensionArtifacts(flowPath: string): Promise<HostedExtensionInstallation> {
104
+ assertHostedPromiseSafety('plugin_source_invalid');
105
+ const origin = await REALPATH(PATH_RESOLVE(flowPath));
106
+ const generation = newGeneration(origin);
107
+ const loaded = await installationAt(origin, generation);
108
+ generation.declarations = loaded.declarations;
109
+ return loaded.installation;
110
+ }
111
+
112
+ /** @internal Base-only test seam; public hosted callers use the combined loader. */
113
+ export async function loadHostedExtensionBase(flowPath: string): Promise<HostedExtensionBase> {
114
+ assertHostedPromiseSafety('plugin_source_invalid');
115
+ const origin = await REALPATH(PATH_RESOLVE(flowPath));
116
+ const generation = newGeneration(origin);
117
+ generation.declarations = declaredExtensions(generation).signature;
118
+ return await baseAt(origin, generation);
119
+ }
120
+
121
+ /** Validate both opaque values and refuse a cached generation after redeploy. */
122
+ export async function assertHostedRuntimeAuthority(
123
+ installation: HostedExtensionInstallation,
124
+ base: HostedExtensionBase,
125
+ ): Promise<void> {
126
+ if (typeof base !== 'object' || base === null || !WEAK_SET_HAS(BASE_AUTHORITY, base)) {
127
+ throw new PluginError(
128
+ 'plugin_incompatible',
129
+ 'Hosted extension base authority is malformed; use loadHostedExtensionRuntime.',
130
+ );
131
+ }
132
+ if (
133
+ typeof installation !== 'object' ||
134
+ installation === null ||
135
+ !WEAK_SET_HAS(INSTALLATION_AUTHORITY, installation)
136
+ ) {
137
+ throw new PluginError(
138
+ 'plugin_source_invalid',
139
+ 'Hosted extension installation authority is malformed; use loadHostedExtensionRuntime.',
140
+ );
141
+ }
142
+ const generation = WEAK_MAP_GET(BASE_GENERATION, base);
143
+ if (generation === undefined || generation !== WEAK_MAP_GET(INSTALLATION_GENERATION, installation)) {
144
+ throw new PluginError(
145
+ 'plugin_source_invalid',
146
+ 'Hosted extension base and installation must originate from the same runtime generation.',
147
+ );
148
+ }
149
+ await assertCurrentGeneration(generation);
150
+ }
151
+
152
+ /** @internal Validate a metadata-only installation used by selection tests. */
153
+ export function assertHostedInstallationAuthority(value: unknown): asserts value is HostedExtensionInstallation {
154
+ if (
155
+ typeof value !== 'object' ||
156
+ value === null ||
157
+ !WEAK_SET_HAS(INSTALLATION_AUTHORITY, value) ||
158
+ !ARRAY_IS_ARRAY((value as Partial<HostedExtensionInstallation>).artifacts)
159
+ ) {
160
+ throw new PluginError('plugin_source_invalid', 'Hosted extension installation authority is malformed.');
161
+ }
162
+ }
163
+
164
+ async function installationAt(
165
+ origin: string,
166
+ generation: RuntimeGeneration,
167
+ ): Promise<{
168
+ installation: HostedExtensionInstallation;
169
+ declarations: string;
170
+ }> {
171
+ const root = generation.projectRoot;
172
+ if (root === undefined)
173
+ return frozenHostedPromiseValue({
174
+ installation: installation([], generation),
175
+ declarations: canonicalize([]),
176
+ });
177
+ const artifacts: HostedExtensionArtifact[] = [];
178
+ const declared = hostedDeclaredExtensions(root);
179
+ for (let index = 0; index < declared.length; index += 1) {
180
+ const { ref, entry } = declared[index]!;
181
+ const directory = pluginStoreDirectory(root, entry.name, entry.digest);
182
+ await verifyStoredPlugin(directory, entry.digest);
183
+ appendIntrinsicArray(artifacts, OBJECT_FREEZE({
184
+ ref,
185
+ name: entry.name,
186
+ version: entry.version,
187
+ directory,
188
+ digest: entry.digest,
189
+ manifestSha256: entry.manifestSha256,
190
+ }));
191
+ }
192
+ return frozenHostedPromiseValue({
193
+ installation: installation(artifacts, generation),
194
+ declarations: declarationSignature(declared),
195
+ });
196
+ }
197
+
198
+ function declaredExtensions(generation: RuntimeGeneration): { readonly signature: string } {
199
+ const root = generation.projectRoot;
200
+ return {
201
+ signature: root === undefined ? canonicalize([]) : declarationSignature(hostedDeclaredExtensions(root)),
202
+ };
203
+ }
204
+
205
+ function newGeneration(origin: string): RuntimeGeneration {
206
+ return {
207
+ origin,
208
+ projectRoot: findHostedProject(PATH_DIRNAME(origin)),
209
+ declarations: canonicalize([]),
210
+ sourceRoots: OBJECT_FREEZE([]),
211
+ sourceSha256: '',
212
+ };
213
+ }
214
+
215
+ async function assertCurrentGeneration(generation: RuntimeGeneration): Promise<void> {
216
+ let currentSource: string;
217
+ try {
218
+ currentSource = await hostedBaseSourceDigest(generation.sourceRoots);
219
+ } catch {
220
+ throw new PluginError('plugin_source_invalid', 'Hosted extension runtime generation is no longer readable.');
221
+ }
222
+ if (
223
+ declaredExtensions(generation).signature !== generation.declarations ||
224
+ currentSource !== generation.sourceSha256
225
+ ) {
226
+ throw new PluginError(
227
+ 'plugin_source_invalid',
228
+ 'Hosted extension runtime generation is stale; reload the base and installation together.',
229
+ );
230
+ }
231
+ }
232
+
233
+ async function baseAt(origin: string, generation: RuntimeGeneration): Promise<HostedExtensionBase> {
234
+ const snapshot = await createHostedBaseSnapshot(origin, generation.projectRoot ?? PATH_DIRNAME(origin));
235
+ generation.sourceRoots = snapshot.liveSources;
236
+ generation.sourceSha256 = snapshot.liveDigest;
237
+ try {
238
+ if (snapshot.snapshotFlowSha256 !== SOFTWARE_FACTORY_SHA256) {
239
+ throw new PluginError(
240
+ 'plugin_source_invalid',
241
+ 'Hosted capability isolation accepts only the reviewed Software Factory base source.',
242
+ );
243
+ }
244
+ const value = frozenHostedPromiseValue({
245
+ name: 'software-factory',
246
+ version: '2.0.22',
247
+ });
248
+ WEAK_SET_ADD(BASE_AUTHORITY, value);
249
+ WEAK_MAP_SET(BASE_GENERATION, value, generation);
250
+ return value;
251
+ } finally {
252
+ await removeHostedBaseSnapshot(snapshot);
253
+ }
254
+ }
255
+
256
+ function installation(
257
+ artifacts: readonly HostedExtensionArtifact[],
258
+ generation: RuntimeGeneration,
259
+ ): HostedExtensionInstallation {
260
+ const artifactCopy: HostedExtensionArtifact[] = [];
261
+ for (let index = 0; index < artifacts.length; index += 1) appendIntrinsicArray(artifactCopy, artifacts[index]!);
262
+ const value = frozenHostedPromiseValue({ artifacts: frozenHostedPromiseValue(artifactCopy) });
263
+ WEAK_SET_ADD(INSTALLATION_AUTHORITY, value);
264
+ WEAK_MAP_SET(INSTALLATION_GENERATION, value, generation);
265
+ return value;
266
+ }
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Source for the process that imports one hosted extension inside the Linux
3
+ * namespace assembled by hosted-extension-isolation.ts.
4
+ *
5
+ * Keep this dependency-free. The parent writes it to a private file and
6
+ * mounts that file read-only; the extension is not imported by the host.
7
+ */
8
+ export const HOSTED_EXTENSION_SANDBOX_SOURCE = String.raw`
9
+ import { createReadStream, writeSync } from 'node:fs';
10
+ import { createInterface } from 'node:readline';
11
+ import { pathToFileURL } from 'node:url';
12
+
13
+ const lines = createInterface({ input: createReadStream(null, { fd: 0 }), crlfDelay: Infinity });
14
+ const iterator = lines[Symbol.asyncIterator]();
15
+ const send = value => writeSync(3, JSON.stringify(value) + '\n');
16
+ const receive = async () => {
17
+ const next = await iterator.next();
18
+ if (next.done) throw new Error('hosted capability channel closed');
19
+ return JSON.parse(next.value);
20
+ };
21
+ const subscriptionOf = handler => {
22
+ const trigger = handler?.trigger;
23
+ const filter = trigger?.filter;
24
+ if (trigger?.kind !== 'webhook' || filter === undefined
25
+ || typeof filter.provider !== 'string' || filter.provider !== trigger.name
26
+ || typeof filter.type !== 'string') return undefined;
27
+ const action = typeof filter.payload === 'object' && filter.payload !== null && !Array.isArray(filter.payload)
28
+ ? filter.payload.action : undefined;
29
+ if (action !== undefined && typeof action !== 'string') return undefined;
30
+ return action === undefined
31
+ ? { provider: filter.provider, event: filter.type }
32
+ : { provider: filter.provider, event: filter.type, action };
33
+ };
34
+ const matches = (subscription, identity) => subscription !== undefined
35
+ && subscription.provider === identity.provider
36
+ && subscription.event === identity.event
37
+ && (subscription.action === undefined || subscription.action === identity.action);
38
+
39
+ try {
40
+ const request = await receive();
41
+ if (request?.type !== 'execute' || typeof request.entry !== 'string'
42
+ || typeof request.surfaceRuntime !== 'string' || typeof request.capability !== 'string') {
43
+ throw new Error('malformed hosted extension execution request');
44
+ }
45
+ const runtime = await import(pathToFileURL(request.surfaceRuntime).href);
46
+ const authored = await import(pathToFileURL(request.entry).href);
47
+ const definition = runtime.getFlowDefinition(authored.default);
48
+ const selected = definition.handlers.filter(handler => matches(subscriptionOf(handler), request.identity));
49
+ if (selected.length !== 1) throw new Error('hosted event must match exactly one isolated extension handler');
50
+
51
+ let nextCall = 1;
52
+ let capabilityCalls = 0;
53
+ let completion;
54
+ const invoke = async value => {
55
+ capabilityCalls += 1;
56
+ if (capabilityCalls !== 1) throw new Error('hosted extension may invoke its capability once');
57
+ const id = nextCall++;
58
+ send({ type: 'capability', id, name: request.capability, request: value });
59
+ const response = await receive();
60
+ if (response?.type !== 'capability-result' || response.id !== id || typeof response.ok !== 'boolean') {
61
+ throw new Error('malformed hosted capability response');
62
+ }
63
+ if (!response.ok) throw new Error(typeof response.error === 'string' ? response.error : 'hosted capability refused');
64
+ return response.value;
65
+ };
66
+ const capabilities = Object.freeze({
67
+ cloud: Object.freeze({ babysitterTurn: Object.freeze({ queue: invoke }) }),
68
+ });
69
+ const available = Object.freeze({
70
+ capabilities,
71
+ done(reason) {
72
+ if (completion !== undefined) throw new Error('hosted extension completed more than once');
73
+ completion = reason;
74
+ },
75
+ });
76
+ const context = new Proxy(available, {
77
+ get(target, property, receiver) {
78
+ if (property !== 'capabilities' && property !== 'done') {
79
+ throw new Error('hosted extension context denies ' + String(property));
80
+ }
81
+ return Reflect.get(target, property, receiver);
82
+ },
83
+ has(_target, property) { return property === 'capabilities' || property === 'done'; },
84
+ ownKeys() { return ['capabilities', 'done']; },
85
+ });
86
+ await selected[0].body(context, request.input);
87
+ if (capabilityCalls !== 1) throw new Error('hosted extension did not invoke its capability exactly once');
88
+ if (completion !== 'success') throw new Error('hosted extension did not complete with success');
89
+ send({ type: 'result', completionReason: completion, capabilityCalls });
90
+ } catch (error) {
91
+ send({ type: 'error', message: error instanceof Error ? (error.stack ?? error.message) : String(error) });
92
+ process.exitCode = 1;
93
+ }
94
+ `;