@relayflows/sdk 2.0.32 → 2.0.34

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 (53) hide show
  1. package/dist/cloud-mirror-step.d.ts +12 -0
  2. package/dist/cloud-mirror-step.d.ts.map +1 -1
  3. package/dist/cloud-mirror-step.js +20 -1
  4. package/dist/cloud-mirror-step.js.map +1 -1
  5. package/dist/cloud-transcript-codex.d.ts.map +1 -1
  6. package/dist/cloud-transcript-codex.js +90 -4
  7. package/dist/cloud-transcript-codex.js.map +1 -1
  8. package/dist/cloud-transcript-types.d.ts +19 -1
  9. package/dist/cloud-transcript-types.d.ts.map +1 -1
  10. package/dist/cloud-transcript-types.js.map +1 -1
  11. package/dist/cloud-transcript.d.ts +1 -1
  12. package/dist/cloud-transcript.d.ts.map +1 -1
  13. package/dist/cloud-transcript.js +10 -0
  14. package/dist/cloud-transcript.js.map +1 -1
  15. package/dist/communication/environment.d.ts +2 -0
  16. package/dist/communication/environment.d.ts.map +1 -1
  17. package/dist/communication/environment.js +13 -7
  18. package/dist/communication/environment.js.map +1 -1
  19. package/dist/flow-extension-compat.d.ts.map +1 -1
  20. package/dist/flow-extension-compat.js +3 -1
  21. package/dist/flow-extension-compat.js.map +1 -1
  22. package/dist/helper-reference.d.ts.map +1 -1
  23. package/dist/helper-reference.js +134 -6
  24. package/dist/helper-reference.js.map +1 -1
  25. package/dist/hosted-extension-declarations.d.ts.map +1 -1
  26. package/dist/hosted-extension-declarations.js +4 -0
  27. package/dist/hosted-extension-declarations.js.map +1 -1
  28. package/dist/hosted-extension-isolation.d.ts +19 -2
  29. package/dist/hosted-extension-isolation.d.ts.map +1 -1
  30. package/dist/hosted-extension-isolation.js +29 -3
  31. package/dist/hosted-extension-isolation.js.map +1 -1
  32. package/dist/hosted-extension-runtime.js +2 -2
  33. package/dist/hosted-extension-sandbox.d.ts +2 -0
  34. package/dist/hosted-extension-sandbox.d.ts.map +1 -1
  35. package/dist/hosted-extension-sandbox.js +21 -7
  36. package/dist/hosted-extension-sandbox.js.map +1 -1
  37. package/dist/index.d.ts +1 -1
  38. package/dist/index.d.ts.map +1 -1
  39. package/dist/index.js +1 -1
  40. package/dist/index.js.map +1 -1
  41. package/package.json +2 -2
  42. package/src/cloud-mirror-step.ts +33 -1
  43. package/src/cloud-transcript-codex.ts +96 -4
  44. package/src/cloud-transcript-types.ts +18 -1
  45. package/src/cloud-transcript.ts +9 -1
  46. package/src/communication/environment.ts +15 -8
  47. package/src/flow-extension-compat.ts +3 -1
  48. package/src/helper-reference.ts +131 -6
  49. package/src/hosted-extension-declarations.ts +4 -0
  50. package/src/hosted-extension-isolation.ts +38 -2
  51. package/src/hosted-extension-runtime.ts +2 -2
  52. package/src/hosted-extension-sandbox.ts +19 -4
  53. package/src/index.ts +2 -0
@@ -27,7 +27,8 @@ export function helperNamespacesUsed(body: string, root: string): ReadonlySet<st
27
27
  const program = parseFlowBody(body);
28
28
  if (program === null) return textFallback(body, root);
29
29
  const used = new Set<string>();
30
- walk(program, (node) => {
30
+ const scope = { rootFunctionFound: false };
31
+ walkReferences(program, root, false, scope, (node) => {
31
32
  if (node.type !== 'MemberExpression') return;
32
33
  const object = node.object as AstNode | undefined;
33
34
  if (object?.type !== 'Identifier' || object.name !== root) return;
@@ -111,20 +112,144 @@ type AstNode = {
111
112
  [key: string]: unknown;
112
113
  };
113
114
 
114
- /** Depth-first over every child node, without pulling in a second package. */
115
- function walk(node: AstNode, visit: (node: AstNode) => void): void {
116
- visit(node);
115
+ /**
116
+ * Depth-first over references that still resolve to the outer flow context.
117
+ *
118
+ * A nested callback/method parameter or lexical destructuring can reuse the
119
+ * context's spelling while binding an unrelated value. Counting its members
120
+ * would refuse the flow for a mount it never touches. Acorn gives us binding
121
+ * shapes, so scopes are tracked as syntax instead of guessed from text.
122
+ */
123
+ function walkReferences(
124
+ node: AstNode,
125
+ root: string,
126
+ shadowed: boolean,
127
+ state: { rootFunctionFound: boolean },
128
+ visit: (node: AstNode) => void,
129
+ ): void {
130
+ if (isFunction(node)) {
131
+ const parameters = Array.isArray(node.params) ? node.params.filter(isNode) : [];
132
+ const bindsParameter = parameters.some(parameter => patternBinds(parameter, root));
133
+ let bodyHidden = shadowed;
134
+ const isRootFunction = bindsParameter && !state.rootFunctionFound;
135
+ if (isRootFunction) state.rootFunctionFound = true;
136
+ else if (bindsParameter || (isNode(node.id) && patternBinds(node.id, root)) || functionVarBinds(node, root)) {
137
+ bodyHidden = true;
138
+ }
139
+ // Parameter initializers run before the function body and are outside a
140
+ // body-level `var` scope. Keep them visible unless the parameter list
141
+ // itself binds the root name (in which case all parameter references are
142
+ // local to that parameter environment).
143
+ const parameterHidden = shadowed || (bindsParameter && !isRootFunction);
144
+ for (const parameter of parameters) walkReferences(parameter, root, parameterHidden, state, visit);
145
+ const body = isNode(node.body) ? node.body : undefined;
146
+ if (body !== undefined) walkReferences(body, root, bodyHidden, state, visit);
147
+ return;
148
+ }
149
+ let hidden = shadowed;
150
+ if (node.type === 'BlockStatement' && blockBinds(node, root)) {
151
+ hidden = true;
152
+ } else if (node.type === 'CatchClause' && isNode(node.param) && patternBinds(node.param, root)) {
153
+ hidden = true;
154
+ }
155
+
156
+ if (!hidden) visit(node);
117
157
  for (const key of Object.keys(node)) {
118
158
  if (key === 'type' || key === 'start' || key === 'end' || key === 'loc') continue;
119
159
  const child = node[key];
120
160
  if (Array.isArray(child)) {
121
- for (const entry of child) if (isNode(entry)) walk(entry, visit);
161
+ for (const entry of child) if (isNode(entry)) walkReferences(entry, root, hidden, state, visit);
122
162
  } else if (isNode(child)) {
123
- walk(child, visit);
163
+ walkReferences(child, root, hidden, state, visit);
124
164
  }
125
165
  }
126
166
  }
127
167
 
168
+ function isFunction(node: AstNode): boolean {
169
+ return node.type === 'ArrowFunctionExpression'
170
+ || node.type === 'FunctionExpression'
171
+ || node.type === 'FunctionDeclaration';
172
+ }
173
+
174
+ /** Lexical declarations bind across their complete block, including TDZ. */
175
+ function blockBinds(node: AstNode, root: string): boolean {
176
+ const statements = Array.isArray(node.body) ? node.body.filter(isNode) : [];
177
+ return statements.some((statement) => {
178
+ if (statement.type === 'VariableDeclaration' && statement.kind !== 'var') {
179
+ const declarations = Array.isArray(statement.declarations)
180
+ ? statement.declarations.filter(isNode) : [];
181
+ return declarations.some(declaration => isNode(declaration.id) && patternBinds(declaration.id, root));
182
+ }
183
+ return (statement.type === 'FunctionDeclaration' || statement.type === 'ClassDeclaration')
184
+ && isNode(statement.id) && patternBinds(statement.id, root);
185
+ });
186
+ }
187
+
188
+ /** Identifier occurrences that introduce bindings, including nested patterns. */
189
+ function patternBinds(pattern: AstNode, root: string): boolean {
190
+ switch (pattern.type) {
191
+ case 'Identifier':
192
+ return pattern.name === root;
193
+ case 'AssignmentPattern':
194
+ return isNode(pattern.left) && patternBinds(pattern.left, root);
195
+ case 'RestElement':
196
+ return isNode(pattern.argument) && patternBinds(pattern.argument, root);
197
+ case 'ArrayPattern': {
198
+ const elements = Array.isArray(pattern.elements) ? pattern.elements : [];
199
+ return elements.some(element => isNode(element) && patternBinds(element, root));
200
+ }
201
+ case 'ObjectPattern': {
202
+ const properties = Array.isArray(pattern.properties) ? pattern.properties : [];
203
+ return properties.some(property => {
204
+ if (!isNode(property)) return false;
205
+ if (property.type === 'RestElement') {
206
+ return isNode(property.argument) && patternBinds(property.argument, root);
207
+ }
208
+ // Object keys are labels, not bindings. Only the value side introduces
209
+ // the local (including a default-value AssignmentPattern's left side).
210
+ return property.type === 'Property'
211
+ && isNode(property.value)
212
+ && patternBinds(property.value, root);
213
+ });
214
+ }
215
+ default:
216
+ return false;
217
+ }
218
+ }
219
+
220
+ /** `var` is function-scoped, so a nested function's declaration hides the
221
+ * outer flow context for its entire body, including code before the
222
+ * declaration. Nested functions have their own var scopes and are skipped. */
223
+ function functionVarBinds(node: AstNode, root: string): boolean {
224
+ const body = isNode(node.body) ? node.body : undefined;
225
+ if (body === undefined) return false;
226
+ let found = false;
227
+ const scan = (current: AstNode): void => {
228
+ if (found) return;
229
+ if (current !== body && isFunction(current)) return;
230
+ if (current.type === 'VariableDeclaration' && current.kind === 'var') {
231
+ const declarations = Array.isArray(current.declarations)
232
+ ? current.declarations.filter(isNode) : [];
233
+ if (declarations.some(declaration => isNode(declaration.id) && patternBinds(declaration.id, root))) {
234
+ found = true;
235
+ return;
236
+ }
237
+ }
238
+ for (const key of Object.keys(current)) {
239
+ if (key === 'type' || key === 'start' || key === 'end' || key === 'loc') continue;
240
+ const child = current[key];
241
+ if (Array.isArray(child)) {
242
+ for (const entry of child) if (isNode(entry)) scan(entry);
243
+ } else if (isNode(child)) {
244
+ scan(child);
245
+ }
246
+ if (found) return;
247
+ }
248
+ };
249
+ scan(body);
250
+ return found;
251
+ }
252
+
128
253
  function isNode(value: unknown): value is AstNode {
129
254
  return typeof value === 'object' && value !== null && typeof (value as { type?: unknown }).type === 'string';
130
255
  }
@@ -67,6 +67,9 @@ const STRING_STARTS_WITH = Function.prototype.call.bind(
67
67
  ) as (value: string, search: string) => boolean;
68
68
  const LOCK_HEX64 = /^[0-9a-f]{64}$/;
69
69
  const LOCK_SHA = /^[0-9a-f]{40}$/;
70
+ // `name` reaches pluginStoreDirectory as a path segment, so it stays one
71
+ // lowercase kebab-case component: no separators, no `.`/`..`, no traversal.
72
+ const LOCK_NAME = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
70
73
  const LOCK_OWNER = /^[A-Za-z0-9](?:[A-Za-z0-9-]{0,38})$/;
71
74
  const LOCK_REPO = /^[A-Za-z0-9_.-]{1,100}$/;
72
75
  const HTTPS_GITHUB = /^https:\/\/github\.com\//;
@@ -240,6 +243,7 @@ function parseHostedPluginLock(input: unknown): {
240
243
  ]) ||
241
244
  entry.kind !== "flow-extension" ||
242
245
  typeof entry.name !== "string" ||
246
+ !REGEXP_TEST(LOCK_NAME, entry.name) ||
243
247
  typeof entry.version !== "string" ||
244
248
  typeof entry.digest !== "string" ||
245
249
  !REGEXP_TEST(LOCK_HEX64, entry.digest) ||
@@ -10,6 +10,7 @@ import {
10
10
  import {
11
11
  assertHostedInstallationAuthority,
12
12
  assertHostedRuntimeAuthority,
13
+ loadHostedExtensionRuntime,
13
14
  type HostedExtensionArtifact,
14
15
  type HostedExtensionBase,
15
16
  type HostedExtensionInstallation,
@@ -104,8 +105,42 @@ export interface RunHostedExtensionOptions {
104
105
  readonly prlimitPath?: string;
105
106
  }
106
107
 
108
+ export interface RunHostedSoftwareGardenBabysitterOptions
109
+ extends Omit<RunHostedExtensionOptions, 'installation' | 'base'> {
110
+ /** Exact reviewed Software Garden source inside its lock-backed project. */
111
+ readonly flowPath: string;
112
+ }
113
+
107
114
  export type HostedExtensionResult = HostedExtensionProtocolResult;
108
115
 
116
+ /**
117
+ * Compose the canonical Software Garden base with its installed native
118
+ * Babysitter and execute one host-verified delivery. Base source, extension
119
+ * declarations, lock entries and stored bytes are captured as one opaque
120
+ * generation; the capability runner rechecks that generation immediately
121
+ * before the isolated process starts.
122
+ *
123
+ * Hosted callers use this entrypoint instead of the ordinary authored loader,
124
+ * which imports extension JavaScript in the host and therefore keeps refusing
125
+ * matched hosted handlers.
126
+ */
127
+ export async function runHostedSoftwareGardenBabysitter(
128
+ options: RunHostedSoftwareGardenBabysitterOptions,
129
+ ): Promise<HostedExtensionResult> {
130
+ const runtime = await loadHostedExtensionRuntime(options.flowPath);
131
+ return await runHostedCapabilityExtension({
132
+ installation: runtime.installation,
133
+ base: runtime.base,
134
+ dispatch: options.dispatch,
135
+ input: options.input,
136
+ babysitterTurn: options.babysitterTurn,
137
+ ...(OBJECT_HAS_OWN(options, 'timeoutMs') ? { timeoutMs: options.timeoutMs } : {}),
138
+ ...(OBJECT_HAS_OWN(options, 'bubblewrapPath') ? { bubblewrapPath: options.bubblewrapPath } : {}),
139
+ ...(OBJECT_HAS_OWN(options, 'nodePath') ? { nodePath: options.nodePath } : {}),
140
+ ...(OBJECT_HAS_OWN(options, 'prlimitPath') ? { prlimitPath: options.prlimitPath } : {}),
141
+ });
142
+ }
143
+
109
144
  /**
110
145
  * Execute a capability-only hosted extension in a Linux mount/PID/network/user
111
146
  * namespace. The extension is first imported inside that namespace. It sees
@@ -114,8 +149,9 @@ export type HostedExtensionResult = HostedExtensionProtocolResult;
114
149
  * authenticated capability adapter and passes the original branded dispatch
115
150
  * authority to that adapter out of band.
116
151
  *
117
- * This is deliberately not wired into executeAuthoredFlow yet. #549's refusal
118
- * remains the rollout gate until the Cloud adapter and independent review land.
152
+ * The ordinary authored executor deliberately stays refused: it imports
153
+ * extension JavaScript in the host. `runHostedSoftwareGardenBabysitter` is the
154
+ * canonical composition boundary for a hosted Software Garden delivery.
119
155
  */
120
156
  export async function runHostedCapabilityExtension(
121
157
  options: RunHostedExtensionOptions,
@@ -26,7 +26,7 @@ const REALPATH = realpath;
26
26
  const PATH_DIRNAME = dirname;
27
27
  const PATH_JOIN = join;
28
28
  const PATH_RESOLVE = resolve;
29
- const SOFTWARE_FACTORY_SHA256 = 'b97a3466c2affabb61afa655d4b2f726942d753c0466740b64ba26a78da359c6';
29
+ const SOFTWARE_FACTORY_SHA256 = '8bbcf0e42f47d6bc6491a129935f96b791be03c8a5ed5b73d651c4e77913e2d7';
30
30
  const ARRAY_IS_ARRAY = Array.isArray;
31
31
  const OBJECT_FREEZE = Object.freeze;
32
32
  const WEAK_MAP_GET = Function.prototype.call.bind(WeakMap.prototype.get) as <K extends object, V>(
@@ -243,7 +243,7 @@ async function baseAt(origin: string, generation: RuntimeGeneration): Promise<Ho
243
243
  }
244
244
  const value = frozenHostedPromiseValue({
245
245
  name: 'software-factory',
246
- version: '2.0.22',
246
+ version: '2.0.23',
247
247
  });
248
248
  WEAK_SET_ADD(BASE_AUTHORITY, value);
249
249
  WEAK_MAP_SET(BASE_GENERATION, value, generation);
@@ -53,6 +53,22 @@ const PATH_RESOLVE = resolve;
53
53
  const SPAWN = spawn;
54
54
  const PROCESS_EXEC_PATH = process.execPath;
55
55
  const PROCESS_NODE_VERSION = process.versions.node;
56
+
57
+ /**
58
+ * The host's own `@relayflows/surface` entry, resolved once while this module
59
+ * initializes. `require.resolve` consults `Module._resolveFilename`, which is
60
+ * writable: resolving lazily would run after authored flow code has had a
61
+ * chance to replace it and could hand the sandbox a different package. The
62
+ * lookup therefore happens before any authored module can load, and a failure
63
+ * is reported when a caller actually needs the root.
64
+ */
65
+ const SURFACE_ENTRY: string | undefined = (() => {
66
+ try { return REALPATH_SYNC(CREATE_REQUIRE(import.meta.url).resolve('@relayflows/surface')); }
67
+ catch { return undefined; }
68
+ })();
69
+
70
+ /** @internal Seam for the regression that pins initialization-time capture. */
71
+ export function capturedSurfaceEntry(): string | undefined { return SURFACE_ENTRY; }
56
72
  const PROCESS_PLATFORM = process.platform;
57
73
  const PROMISE = Promise;
58
74
  const EVENT_ON = Function.prototype.call.bind(EventEmitter.prototype.on) as (
@@ -109,7 +125,7 @@ const ADDRESS_SPACE_BYTES = 16 * 1024 * 1024 * 1024;
109
125
  const DATA_BYTES = 3 * 1024 * 1024 * 1024;
110
126
  const SURFACE_RUNTIME_SHA256 = OBJECT_FREEZE({
111
127
  'flow.js': '4aaeacc55de3074f4d121ce7253c3be50a93757e6540ba8159889a9450d1c05c',
112
- 'helpers/providers.js': '7bc62eccaa3a9e786ae0a689bf74160585149e91feef8208e17ef8eca51eed7f',
128
+ 'helpers/providers.js': '4eb06d0d85ca0a3434bb2dbba7407e3d95eef2dfbedaeb0e2de0c0c0ea812457',
113
129
  'provider-trigger.js': 'e2664c65397f93fb486eb6f1e756c7cec3f88b3851d79c23567cad986f80f1ff',
114
130
  'schedule.js': '8fe72f176a75ec0b5f26e12db7a597575c259a2e2cbb59690f9dc20a5e63940b',
115
131
  'triggers.js': '4a3515b571a318f6c7a5661f9310bc9af43e3faf20ea39903a9b363c51258e4c',
@@ -250,9 +266,8 @@ export async function runHostedExtensionSandbox(
250
266
 
251
267
  async function resolveSurfaceRoot(expectedVersion: string, override?: string): Promise<string> {
252
268
  if (override !== undefined) return await checkedSurfaceRoot(override, expectedVersion);
253
- let resolved: string;
254
- try { resolved = REALPATH_SYNC(CREATE_REQUIRE(import.meta.url).resolve('@relayflows/surface')); }
255
- catch { return unsupported('hosted extension cannot resolve @relayflows/surface'); }
269
+ const resolved = SURFACE_ENTRY;
270
+ if (resolved === undefined) return unsupported('hosted extension cannot resolve @relayflows/surface');
256
271
  let directory = PATH_DIRNAME(resolved);
257
272
  const root = PATH_PARSE(directory).root;
258
273
  while (directory !== root) {
package/src/index.ts CHANGED
@@ -61,6 +61,7 @@ export type { StepSpend } from './protocol.js';
61
61
  export { SPEC_SCHEMA_VERSION } from './spec.js';
62
62
 
63
63
  export {
64
+ runHostedSoftwareGardenBabysitter,
64
65
  runHostedCapabilityExtension,
65
66
  loadHostedExtensionRuntime,
66
67
  type HostedExtensionArtifact,
@@ -70,6 +71,7 @@ export {
70
71
  type HostedCapabilityAuthority,
71
72
  type HostedBabysitterCapability,
72
73
  type RunHostedExtensionOptions,
74
+ type RunHostedSoftwareGardenBabysitterOptions,
73
75
  type HostedExtensionResult,
74
76
  } from './hosted-extension-isolation.js';
75
77
  export {