@relayflows/sdk 2.0.33 → 2.0.35
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/dist/authored-dispatch.d.ts +24 -0
- package/dist/authored-dispatch.d.ts.map +1 -0
- package/dist/authored-dispatch.js +81 -0
- package/dist/authored-dispatch.js.map +1 -0
- package/dist/authored-flow-error.d.ts +1 -1
- package/dist/authored-flow-error.d.ts.map +1 -1
- package/dist/authored-flow-error.js.map +1 -1
- package/dist/authored-flow-executor.d.ts +17 -0
- package/dist/authored-flow-executor.d.ts.map +1 -1
- package/dist/authored-flow-executor.js +92 -16
- package/dist/authored-flow-executor.js.map +1 -1
- package/dist/authored-flow-loader.d.ts.map +1 -1
- package/dist/authored-flow-loader.js +9 -0
- package/dist/authored-flow-loader.js.map +1 -1
- package/dist/authored-node-entry.js +1 -0
- package/dist/authored-node-entry.js.map +1 -1
- package/dist/authored-root.js +1 -0
- package/dist/authored-root.js.map +1 -1
- package/dist/authored-worker-step.d.ts +3 -1
- package/dist/authored-worker-step.d.ts.map +1 -1
- package/dist/authored-worker-step.js +18 -7
- package/dist/authored-worker-step.js.map +1 -1
- package/dist/cli/run.d.ts +5 -0
- package/dist/cli/run.d.ts.map +1 -1
- package/dist/cli/run.js +8 -0
- package/dist/cli/run.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +3 -0
- package/dist/cli.js.map +1 -1
- package/dist/cloud-mirror-step.d.ts +2 -0
- package/dist/cloud-mirror-step.d.ts.map +1 -1
- package/dist/cloud-mirror-step.js +9 -1
- package/dist/cloud-mirror-step.js.map +1 -1
- package/dist/cloud-mirror.d.ts.map +1 -1
- package/dist/cloud-mirror.js +17 -2
- package/dist/cloud-mirror.js.map +1 -1
- package/dist/communication/environment.d.ts +2 -0
- package/dist/communication/environment.d.ts.map +1 -1
- package/dist/communication/environment.js +13 -7
- package/dist/communication/environment.js.map +1 -1
- package/dist/flow-extension-compat.d.ts.map +1 -1
- package/dist/flow-extension-compat.js +3 -1
- package/dist/flow-extension-compat.js.map +1 -1
- package/dist/hosted-extension-declarations.d.ts.map +1 -1
- package/dist/hosted-extension-declarations.js +4 -0
- package/dist/hosted-extension-declarations.js.map +1 -1
- package/dist/hosted-extension-isolation.d.ts +19 -2
- package/dist/hosted-extension-isolation.d.ts.map +1 -1
- package/dist/hosted-extension-isolation.js +29 -3
- package/dist/hosted-extension-isolation.js.map +1 -1
- package/dist/hosted-extension-runtime.js +2 -2
- package/dist/hosted-extension-sandbox.d.ts +2 -0
- package/dist/hosted-extension-sandbox.d.ts.map +1 -1
- package/dist/hosted-extension-sandbox.js +20 -6
- package/dist/hosted-extension-sandbox.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/worker-slots.d.ts +15 -0
- package/dist/worker-slots.d.ts.map +1 -1
- package/dist/worker-slots.js +40 -3
- package/dist/worker-slots.js.map +1 -1
- package/package.json +2 -2
- package/src/authored-dispatch.ts +113 -0
- package/src/authored-flow-error.ts +3 -0
- package/src/authored-flow-executor.ts +126 -15
- package/src/authored-flow-loader.ts +12 -0
- package/src/authored-node-entry.ts +1 -0
- package/src/authored-root.ts +1 -0
- package/src/authored-worker-step.ts +17 -6
- package/src/cli/run.ts +10 -0
- package/src/cli.ts +3 -0
- package/src/cloud-mirror-step.ts +16 -2
- package/src/cloud-mirror.ts +15 -2
- package/src/communication/environment.ts +15 -8
- package/src/flow-extension-compat.ts +3 -1
- package/src/hosted-extension-declarations.ts +4 -0
- package/src/hosted-extension-isolation.ts +38 -2
- package/src/hosted-extension-runtime.ts +2 -2
- package/src/hosted-extension-sandbox.ts +18 -3
- package/src/index.ts +2 -0
- package/src/worker-slots.ts +63 -4
package/src/cloud-mirror-step.ts
CHANGED
|
@@ -152,6 +152,8 @@ export interface MirroredJournal {
|
|
|
152
152
|
children: string[];
|
|
153
153
|
/** Authored graph hints by step id, from an authored root's step index. */
|
|
154
154
|
hints: Map<string, { label?: string; after?: string[] }>;
|
|
155
|
+
/** Authored completion receipts are durable evidence, not graph nodes. */
|
|
156
|
+
hidden: string[];
|
|
155
157
|
}
|
|
156
158
|
|
|
157
159
|
type Payload = Record<string, unknown>;
|
|
@@ -507,6 +509,7 @@ export function mirrorJournal(
|
|
|
507
509
|
transcripts,
|
|
508
510
|
children: index.children,
|
|
509
511
|
hints: index.hints,
|
|
512
|
+
hidden: index.hidden,
|
|
510
513
|
};
|
|
511
514
|
}
|
|
512
515
|
|
|
@@ -522,7 +525,11 @@ export function mirrorJournal(
|
|
|
522
525
|
function authoredIndex(
|
|
523
526
|
events: readonly JournalEvent[],
|
|
524
527
|
env: NodeJS.ProcessEnv,
|
|
525
|
-
): {
|
|
528
|
+
): {
|
|
529
|
+
children: string[];
|
|
530
|
+
hints: Map<string, { label?: string; after?: string[] }>;
|
|
531
|
+
hidden: string[];
|
|
532
|
+
} {
|
|
526
533
|
const messages: unknown[] = [];
|
|
527
534
|
for (const event of events) {
|
|
528
535
|
if (event.entry_type !== 'stream.appended') continue;
|
|
@@ -531,8 +538,15 @@ function authoredIndex(
|
|
|
531
538
|
}
|
|
532
539
|
const children: string[] = [];
|
|
533
540
|
const hints = new Map<string, { label?: string; after?: string[] }>();
|
|
541
|
+
const hidden: string[] = [];
|
|
534
542
|
for (const entry of foldAuthoredStepRecords(messages).values()) {
|
|
535
543
|
if (/^[A-Za-z0-9][A-Za-z0-9_-]*$/u.test(entry.runId)) children.push(entry.runId);
|
|
544
|
+
// This is the executor's durable verdict receipt, not an authored unit of
|
|
545
|
+
// work. Only an entry in the authored root index can be hidden here, so a
|
|
546
|
+
// declarative step coincidentally named `complete-N` remains visible.
|
|
547
|
+
if (/(?:^|--)complete-[1-9][0-9]*$/.test(entry.step)) {
|
|
548
|
+
hidden.push(`${entry.runId}/${entry.step}`);
|
|
549
|
+
}
|
|
536
550
|
const name = label(entry.label, env);
|
|
537
551
|
const after = entry.after === undefined
|
|
538
552
|
? undefined
|
|
@@ -546,7 +560,7 @@ function authoredIndex(
|
|
|
546
560
|
...(after === undefined ? {} : { after }),
|
|
547
561
|
});
|
|
548
562
|
}
|
|
549
|
-
return { children, hints };
|
|
563
|
+
return { children, hints, hidden };
|
|
550
564
|
}
|
|
551
565
|
|
|
552
566
|
/**
|
package/src/cloud-mirror.ts
CHANGED
|
@@ -287,6 +287,7 @@ export function createRunMirror(options: RunMirrorOptions): RunMirror {
|
|
|
287
287
|
const finals = new Map<string, { journalRunId: string; row: FinalStep }>();
|
|
288
288
|
const transcripts = new Map<string, { stepName: string; attempts: Array<{ attempt: number; path: string }> }>();
|
|
289
289
|
const hints = new Map<string, { label?: string; after?: string[] }>();
|
|
290
|
+
const hidden = new Set<string>();
|
|
290
291
|
/** Live steps the cache cap dropped, by identity, so a capped view owns up to it. */
|
|
291
292
|
const evicted = new Set<string>();
|
|
292
293
|
/** Finished steps past the report cap, by identity. Counted, never sent. */
|
|
@@ -318,6 +319,7 @@ export function createRunMirror(options: RunMirrorOptions): RunMirror {
|
|
|
318
319
|
|
|
319
320
|
const remember = (journalRunId: string, step: SnapshotStep, order: number): void => {
|
|
320
321
|
const key = `${journalRunId}/${step.stepName}`;
|
|
322
|
+
if (hidden.has(key)) return;
|
|
321
323
|
const existing = steps.get(key);
|
|
322
324
|
const changed = existing === undefined || fingerprint(existing.step) !== fingerprint(step);
|
|
323
325
|
steps.set(key, { step, order, changedAt: changed ? now() : existing!.changedAt });
|
|
@@ -365,19 +367,30 @@ export function createRunMirror(options: RunMirrorOptions): RunMirror {
|
|
|
365
367
|
continue;
|
|
366
368
|
}
|
|
367
369
|
for (const child of folded.children) admit(child);
|
|
370
|
+
for (const key of folded.hidden) {
|
|
371
|
+
hidden.add(key);
|
|
372
|
+
steps.delete(key);
|
|
373
|
+
finals.delete(key);
|
|
374
|
+
transcripts.delete(key);
|
|
375
|
+
}
|
|
368
376
|
for (const [key, hint] of folded.hints) {
|
|
369
377
|
// Merge, never replace: a completion record can omit a label or an
|
|
370
378
|
// edge the admission carried, and the view must not lose a step's
|
|
371
379
|
// name between them.
|
|
372
380
|
const existing = hints.get(key);
|
|
373
381
|
hints.set(key, {
|
|
374
|
-
|
|
375
|
-
|
|
382
|
+
// The root's authored index is scanned before the child journal and
|
|
383
|
+
// carries causal edges across child runs. A child spec only knows
|
|
384
|
+
// its own single-step graph (`depends_on: []`), so it may fill a
|
|
385
|
+
// missing field but must never erase the root's richer answer.
|
|
386
|
+
...(existing?.label ?? hint.label ? { label: existing?.label ?? hint.label } : {}),
|
|
387
|
+
...(existing?.after ?? hint.after ? { after: existing?.after ?? hint.after } : {}),
|
|
376
388
|
});
|
|
377
389
|
}
|
|
378
390
|
folded.steps.forEach((step, order) => remember(runId, step, order));
|
|
379
391
|
for (const row of folded.finals) {
|
|
380
392
|
const key = `${runId}/${row.stepName}`;
|
|
393
|
+
if (hidden.has(key)) continue;
|
|
381
394
|
// The report cap bounds what this process holds, not just what it
|
|
382
395
|
// sends: a run with ten thousand steps must not grow its own mirror.
|
|
383
396
|
// Steps past it are counted by identity, so a step read twice is one
|
|
@@ -4,19 +4,26 @@ import { wrapperEnvironment } from '../wrapper-runtime.js';
|
|
|
4
4
|
const terminalNames = ['TERM', 'COLORTERM', 'XDG_CONFIG_HOME', 'XDG_DATA_HOME', 'XDG_CACHE_HOME'];
|
|
5
5
|
const relayNames = ['RELAY_API_KEY', 'RELAY_WORKSPACE_KEY', 'AGENT_RELAY_WORKSPACE_KEY',
|
|
6
6
|
'RELAY_BROKER_API_KEY', 'RELAY_AGENT_TOKEN', 'RELAY_NODE_TOKEN', 'RELAY_WORKSPACES_JSON'];
|
|
7
|
-
|
|
7
|
+
// Multi-provider CLIs select their provider in their own configuration.
|
|
8
|
+
const multiProvider = ['OPENAI_API_KEY', 'OPENAI_BASE_URL', 'ANTHROPIC_API_KEY', 'ANTHROPIC_BASE_URL',
|
|
9
|
+
'GEMINI_API_KEY', 'GOOGLE_API_KEY', 'OPENROUTER_API_KEY'] as const;
|
|
10
|
+
const providerNames: Readonly<Record<string, readonly string[]>> = {
|
|
8
11
|
claude: ['ANTHROPIC_API_KEY', 'ANTHROPIC_AUTH_TOKEN', 'ANTHROPIC_BASE_URL', 'CLAUDE_CODE_OAUTH_TOKEN', 'CLAUDE_CONFIG_DIR'],
|
|
9
12
|
codex: ['OPENAI_API_KEY', 'OPENAI_BASE_URL', 'CODEX_HOME'],
|
|
10
13
|
gemini: ['GEMINI_API_KEY', 'GOOGLE_API_KEY', 'GOOGLE_APPLICATION_CREDENTIALS', 'GOOGLE_CLOUD_PROJECT', 'GOOGLE_CLOUD_LOCATION'],
|
|
11
|
-
|
|
14
|
+
// AgentWorkforce/cloud#4026 passes both endpoint names unchanged as aliases
|
|
15
|
+
// for one run-scoped House proxy base. Dropping either can let a compatible
|
|
16
|
+
// Cursor version fall back to its provider default and bypass the House route.
|
|
17
|
+
'cursor-agent': ['CURSOR_API_KEY', 'CURSOR_API_ENDPOINT', 'CURSOR_API_BASE_URL'],
|
|
12
18
|
droid: ['FACTORY_API_KEY'],
|
|
13
|
-
|
|
19
|
+
// AgentWorkforce/cloud#4026's house-provider run contract gives Grok an
|
|
20
|
+
// isolated home plus an xAI-compatible base URL. These remain Grok-scoped.
|
|
21
|
+
grok: ['XAI_API_KEY', 'GROK_XAI_API_BASE_URL', 'GROK_HOME'],
|
|
22
|
+
...Object.fromEntries(['opencode', 'aider', 'goose', 'pi', 'deepagents'].map(cli => [cli, multiProvider])),
|
|
14
23
|
};
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
for (const cli of ['opencode', 'aider', 'goose', 'pi', 'deepagents']) providerNames[cli] = multiProvider;
|
|
19
|
-
function selected(source: NodeJS.ProcessEnv, names: string[]) {
|
|
24
|
+
/** @internal Names-only test seam; credential lists remain private. */
|
|
25
|
+
export function providerEnvironmentCliNames(): readonly string[] { return Object.keys(providerNames); }
|
|
26
|
+
function selected(source: NodeJS.ProcessEnv, names: readonly string[]) {
|
|
20
27
|
return Object.fromEntries(names.filter(name => source[name] !== undefined).map(name => [name, source[name]]));
|
|
21
28
|
}
|
|
22
29
|
export function brokerEnvironment(source: NodeJS.ProcessEnv): NodeJS.ProcessEnv {
|
|
@@ -42,7 +42,9 @@ function assertRuntimeRange(
|
|
|
42
42
|
*/
|
|
43
43
|
export function assertBaseCompatible(manifest: FlowExtensionManifest, base: { readonly name: string; readonly version?: string }): void {
|
|
44
44
|
let entry: FlowExtensionManifest['compat']['base'][number] | undefined;
|
|
45
|
-
|
|
45
|
+
// First match, as `.find()` did before the intrinsic-capture rewrite: a
|
|
46
|
+
// later duplicate name must not widen or narrow the range that decides.
|
|
47
|
+
for (let index = 0; index < manifest.compat.base.length && entry === undefined; index += 1) {
|
|
46
48
|
if (manifest.compat.base[index]!.name === base.name) entry = manifest.compat.base[index];
|
|
47
49
|
}
|
|
48
50
|
if (entry === undefined) {
|
|
@@ -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
|
-
*
|
|
118
|
-
*
|
|
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 = '
|
|
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.
|
|
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 (
|
|
@@ -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
|
-
|
|
254
|
-
|
|
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 {
|
package/src/worker-slots.ts
CHANGED
|
@@ -7,6 +7,16 @@ export function isAgentCapacity(value: unknown): value is number {
|
|
|
7
7
|
return Number.isInteger(value) && (value as number) >= 1 && (value as number) <= MAX_LOCAL_AGENT_CAPACITY;
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
+
export interface WorkerSlotScope {
|
|
11
|
+
run<T>(work: () => Promise<T>): Promise<T>;
|
|
12
|
+
/** Refuse this flow's queued/future work without closing the shared pool. */
|
|
13
|
+
close(reason: unknown): void;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface ScopeState {
|
|
17
|
+
closed?: { readonly reason: unknown };
|
|
18
|
+
}
|
|
19
|
+
|
|
10
20
|
/**
|
|
11
21
|
* First-come admission to a worker that holds `capacity` dispatches at once.
|
|
12
22
|
*
|
|
@@ -19,7 +29,11 @@ export function isAgentCapacity(value: unknown): value is number {
|
|
|
19
29
|
*/
|
|
20
30
|
export class WorkerSlots {
|
|
21
31
|
private held = 0;
|
|
22
|
-
private readonly waiting: Array<{
|
|
32
|
+
private readonly waiting: Array<{
|
|
33
|
+
resolve: () => void;
|
|
34
|
+
reject: (reason: unknown) => void;
|
|
35
|
+
scope?: ScopeState;
|
|
36
|
+
}> = [];
|
|
23
37
|
private closed: { readonly reason: unknown } | undefined;
|
|
24
38
|
|
|
25
39
|
constructor(readonly capacity: number) {
|
|
@@ -27,18 +41,37 @@ export class WorkerSlots {
|
|
|
27
41
|
}
|
|
28
42
|
|
|
29
43
|
async run<T>(work: () => Promise<T>): Promise<T> {
|
|
44
|
+
return this.runInScope(work);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** One independently closable admission scope over this shared capacity. */
|
|
48
|
+
scope(): WorkerSlotScope {
|
|
49
|
+
const scope: ScopeState = {};
|
|
50
|
+
return Object.freeze({
|
|
51
|
+
run: <T>(work: () => Promise<T>) => this.runInScope(work, scope),
|
|
52
|
+
close: (reason: unknown) => this.closeScope(scope, reason),
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
private async runInScope<T>(work: () => Promise<T>, scope?: ScopeState): Promise<T> {
|
|
30
57
|
if (this.closed !== undefined) throw this.closed.reason;
|
|
58
|
+
if (scope?.closed !== undefined) throw scope.closed.reason;
|
|
31
59
|
if (this.held < this.capacity) this.held++;
|
|
32
60
|
// A released slot is handed straight to the next waiter, so `held` never
|
|
33
61
|
// dips below capacity while anyone is queued and no later caller can jump it.
|
|
34
62
|
else {
|
|
35
|
-
await new Promise<void>((resolve, reject) => this.waiting.push({ resolve, reject }));
|
|
63
|
+
await new Promise<void>((resolve, reject) => this.waiting.push({ resolve, reject, scope }));
|
|
36
64
|
// Woken with the slot, but the body may have failed in between (see release).
|
|
37
65
|
const closed = this.closedReason();
|
|
38
66
|
if (closed !== undefined) {
|
|
39
67
|
this.release();
|
|
40
68
|
throw closed.reason;
|
|
41
69
|
}
|
|
70
|
+
const scopeClosed = scopeClosedReason(scope);
|
|
71
|
+
if (scopeClosed !== undefined) {
|
|
72
|
+
this.release();
|
|
73
|
+
throw scopeClosed.reason;
|
|
74
|
+
}
|
|
42
75
|
}
|
|
43
76
|
try {
|
|
44
77
|
return await work();
|
|
@@ -66,17 +99,28 @@ export class WorkerSlots {
|
|
|
66
99
|
return;
|
|
67
100
|
}
|
|
68
101
|
setImmediate(() => {
|
|
69
|
-
if (this.closed === undefined) {
|
|
102
|
+
if (this.closed === undefined && next.scope?.closed === undefined) {
|
|
70
103
|
next.resolve();
|
|
71
104
|
return;
|
|
72
105
|
}
|
|
73
106
|
// Closed meanwhile. `close` could not see this waiter (already taken off
|
|
74
107
|
// the queue), so refuse it here, and pass the slot on instead of leaking it.
|
|
75
|
-
next.reject(this.closed.reason);
|
|
108
|
+
next.reject(this.closed?.reason ?? next.scope!.closed!.reason);
|
|
76
109
|
this.release();
|
|
77
110
|
});
|
|
78
111
|
}
|
|
79
112
|
|
|
113
|
+
private closeScope(scope: ScopeState, reason: unknown): void {
|
|
114
|
+
if (scope.closed !== undefined) return;
|
|
115
|
+
scope.closed = { reason };
|
|
116
|
+
for (let index = this.waiting.length - 1; index >= 0; index--) {
|
|
117
|
+
const waiter = this.waiting[index]!;
|
|
118
|
+
if (waiter.scope !== scope) continue;
|
|
119
|
+
this.waiting.splice(index, 1);
|
|
120
|
+
waiter.reject(reason);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
80
124
|
/**
|
|
81
125
|
* Refuse every queued and future admission with `reason`; work already
|
|
82
126
|
* holding a slot is not interrupted. The authored body calls this when it
|
|
@@ -90,3 +134,18 @@ export class WorkerSlots {
|
|
|
90
134
|
for (const waiter of this.waiting.splice(0)) waiter.reject(reason);
|
|
91
135
|
}
|
|
92
136
|
}
|
|
137
|
+
|
|
138
|
+
/** Read through a call so a check after an `await` is not narrowed away. */
|
|
139
|
+
function scopeClosedReason(scope?: ScopeState): { readonly reason: unknown } | undefined {
|
|
140
|
+
return scope?.closed;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** One run-tree-wide capacity pool, shared by parent and child flows. */
|
|
144
|
+
export interface AuthoredWorkerSlots {
|
|
145
|
+
readonly agent: WorkerSlots;
|
|
146
|
+
readonly llm: WorkerSlots;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function authoredWorkerSlots(capacity: number): AuthoredWorkerSlots {
|
|
150
|
+
return Object.freeze({ agent: new WorkerSlots(capacity), llm: new WorkerSlots(capacity) });
|
|
151
|
+
}
|