@relayflows/sdk 2.0.37 → 2.0.39
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/agent-reaper.d.ts +1 -1
- package/dist/agent-reaper.d.ts.map +1 -1
- package/dist/agent-reaper.js +6 -4
- package/dist/agent-reaper.js.map +1 -1
- package/dist/authored-budget.d.ts +26 -1
- package/dist/authored-budget.d.ts.map +1 -1
- package/dist/authored-budget.js +72 -2
- package/dist/authored-budget.js.map +1 -1
- package/dist/authored-flow-error.d.ts +7 -1
- package/dist/authored-flow-error.d.ts.map +1 -1
- package/dist/authored-flow-error.js.map +1 -1
- package/dist/authored-node-runner.d.ts.map +1 -1
- package/dist/authored-node-runner.js +3 -2
- package/dist/authored-node-runner.js.map +1 -1
- package/dist/authored-root.d.ts.map +1 -1
- package/dist/authored-root.js +141 -6
- package/dist/authored-root.js.map +1 -1
- package/dist/authored-worker-step.js +1 -0
- package/dist/authored-worker-step.js.map +1 -1
- package/dist/child-stop.d.ts +3 -1
- package/dist/child-stop.d.ts.map +1 -1
- package/dist/child-stop.js +42 -2
- package/dist/child-stop.js.map +1 -1
- package/dist/cli/add-extension.js +1 -1
- package/dist/cli/add-extension.js.map +1 -1
- package/dist/cli/check.d.ts.map +1 -1
- package/dist/cli/check.js +25 -7
- package/dist/cli/check.js.map +1 -1
- package/dist/cli/cli-probe.d.ts +1 -0
- package/dist/cli/cli-probe.d.ts.map +1 -1
- package/dist/cli/cli-probe.js +72 -36
- package/dist/cli/cli-probe.js.map +1 -1
- package/dist/cli/cloud-deploy.d.ts +9 -1
- package/dist/cli/cloud-deploy.d.ts.map +1 -1
- package/dist/cli/cloud-deploy.js +24 -5
- package/dist/cli/cloud-deploy.js.map +1 -1
- package/dist/cli/cloud-versions.d.ts +22 -0
- package/dist/cli/cloud-versions.d.ts.map +1 -0
- package/dist/cli/cloud-versions.js +96 -0
- package/dist/cli/cloud-versions.js.map +1 -0
- package/dist/cli/direct-run.d.ts.map +1 -1
- package/dist/cli/direct-run.js +4 -1
- package/dist/cli/direct-run.js.map +1 -1
- package/dist/cli/pinned-cli-alias.d.ts +13 -0
- package/dist/cli/pinned-cli-alias.d.ts.map +1 -0
- package/dist/cli/pinned-cli-alias.js +58 -0
- package/dist/cli/pinned-cli-alias.js.map +1 -0
- package/dist/cli/run.d.ts +8 -0
- package/dist/cli/run.d.ts.map +1 -1
- package/dist/cli/run.js +91 -8
- package/dist/cli/run.js.map +1 -1
- package/dist/cli/step-failure.d.ts.map +1 -1
- package/dist/cli/step-failure.js +18 -0
- package/dist/cli/step-failure.js.map +1 -1
- package/dist/cli-commands.d.ts +27 -0
- package/dist/cli-commands.d.ts.map +1 -1
- package/dist/cli-commands.js +18 -0
- package/dist/cli-commands.js.map +1 -1
- package/dist/cli.d.ts +9 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +17 -0
- package/dist/cli.js.map +1 -1
- package/dist/cloud-deploy.d.ts +17 -0
- package/dist/cloud-deploy.d.ts.map +1 -1
- package/dist/cloud-deploy.js +8 -1
- package/dist/cloud-deploy.js.map +1 -1
- package/dist/cloud-run.d.ts.map +1 -1
- package/dist/cloud-run.js +9 -2
- package/dist/cloud-run.js.map +1 -1
- package/dist/cloud-versions-wire.d.ts +25 -0
- package/dist/cloud-versions-wire.d.ts.map +1 -0
- package/dist/cloud-versions-wire.js +39 -0
- package/dist/cloud-versions-wire.js.map +1 -0
- package/dist/cloud-versions.d.ts +47 -0
- package/dist/cloud-versions.d.ts.map +1 -0
- package/dist/cloud-versions.js +131 -0
- package/dist/cloud-versions.js.map +1 -0
- package/dist/communication/worker.d.ts.map +1 -1
- package/dist/communication/worker.js +24 -6
- package/dist/communication/worker.js.map +1 -1
- package/dist/compile.d.ts.map +1 -1
- package/dist/compile.js +16 -7
- package/dist/compile.js.map +1 -1
- package/dist/failure-kinds.d.ts +9 -1
- package/dist/failure-kinds.d.ts.map +1 -1
- package/dist/failure-kinds.js +6 -0
- package/dist/failure-kinds.js.map +1 -1
- package/dist/flow-extension-loader.d.ts +1 -1
- package/dist/flow-extension-loader.d.ts.map +1 -1
- package/dist/flow-extension-loader.js +143 -14
- package/dist/flow-extension-loader.js.map +1 -1
- package/dist/flow-extension-manifest.d.ts +1 -0
- package/dist/flow-extension-manifest.d.ts.map +1 -1
- package/dist/flow-extension-manifest.js +12 -3
- package/dist/flow-extension-manifest.js.map +1 -1
- package/dist/hosted-extension-manifest.d.ts.map +1 -1
- package/dist/hosted-extension-manifest.js +25 -4
- package/dist/hosted-extension-manifest.js.map +1 -1
- package/dist/hosted-extension-runtime.js +1 -1
- package/dist/hosted-extension-sandbox.js +2 -2
- package/dist/llm-worker.d.ts.map +1 -1
- package/dist/llm-worker.js +4 -2
- package/dist/llm-worker.js.map +1 -1
- package/dist/mcp-stdio.d.ts +2 -1
- package/dist/mcp-stdio.d.ts.map +1 -1
- package/dist/mcp-stdio.js +38 -21
- package/dist/mcp-stdio.js.map +1 -1
- package/dist/preflight.d.ts +2 -0
- package/dist/preflight.d.ts.map +1 -1
- package/dist/preflight.js.map +1 -1
- package/dist/protocol.d.ts +9 -0
- package/dist/protocol.d.ts.map +1 -1
- package/dist/resolved-cli-identity.d.ts +14 -0
- package/dist/resolved-cli-identity.d.ts.map +1 -0
- package/dist/resolved-cli-identity.js +19 -0
- package/dist/resolved-cli-identity.js.map +1 -0
- package/dist/worker-cli.d.ts +3 -1
- package/dist/worker-cli.d.ts.map +1 -1
- package/dist/worker-cli.js +116 -40
- package/dist/worker-cli.js.map +1 -1
- package/dist/worker-lease.d.ts +3 -1
- package/dist/worker-lease.d.ts.map +1 -1
- package/dist/worker-lease.js +47 -9
- package/dist/worker-lease.js.map +1 -1
- package/dist/worker.d.ts.map +1 -1
- package/dist/worker.js +4 -2
- package/dist/worker.js.map +1 -1
- package/dist/wrapper-session.d.ts +3 -1
- package/dist/wrapper-session.d.ts.map +1 -1
- package/dist/wrapper-session.js +93 -53
- package/dist/wrapper-session.js.map +1 -1
- package/package.json +2 -2
- package/src/agent-reaper.ts +7 -4
- package/src/authored-budget.ts +73 -1
- package/src/authored-flow-error.ts +6 -0
- package/src/authored-node-runner.ts +3 -2
- package/src/authored-root.ts +150 -7
- package/src/authored-worker-step.ts +1 -0
- package/src/child-stop.ts +40 -1
- package/src/cli/add-extension.ts +1 -1
- package/src/cli/check.ts +25 -4
- package/src/cli/cli-probe.ts +73 -37
- package/src/cli/cloud-deploy.ts +21 -7
- package/src/cli/cloud-versions.ts +103 -0
- package/src/cli/direct-run.ts +4 -1
- package/src/cli/pinned-cli-alias.ts +74 -0
- package/src/cli/run.ts +103 -7
- package/src/cli/step-failure.ts +18 -0
- package/src/cli-commands.ts +18 -0
- package/src/cli.ts +15 -0
- package/src/cloud-deploy.ts +24 -3
- package/src/cloud-run.ts +9 -2
- package/src/cloud-versions-wire.ts +58 -0
- package/src/cloud-versions.ts +179 -0
- package/src/communication/worker.ts +24 -9
- package/src/compile.ts +28 -13
- package/src/failure-kinds.ts +14 -0
- package/src/flow-extension-loader.ts +154 -14
- package/src/flow-extension-manifest.ts +6 -3
- package/src/hosted-extension-manifest.ts +28 -5
- package/src/hosted-extension-runtime.ts +1 -1
- package/src/hosted-extension-sandbox.ts +2 -2
- package/src/llm-worker.ts +6 -4
- package/src/mcp-stdio.ts +34 -20
- package/src/preflight.ts +2 -0
- package/src/protocol.ts +9 -0
- package/src/resolved-cli-identity.ts +42 -0
- package/src/worker-cli.ts +123 -37
- package/src/worker-lease.ts +50 -9
- package/src/worker.ts +6 -4
- package/src/wrapper-session.ts +89 -56
package/src/cloud-deploy.ts
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
} from './flow-requirements.js';
|
|
12
12
|
import { readProjectConfig } from './cli/check.js';
|
|
13
13
|
import { assertNoUseDependencies, collectExtensionSubmissions } from './flow-extension-submit.js';
|
|
14
|
+
import { parseVersionChange, type CloudFlowVersionChange } from './cloud-versions-wire.js';
|
|
14
15
|
|
|
15
16
|
/**
|
|
16
17
|
* Hosted listener deployment: the CLI form of the agentrelay.com onboarding's
|
|
@@ -104,6 +105,8 @@ export interface CloudDeployment {
|
|
|
104
105
|
requirements: FlowRequirements;
|
|
105
106
|
/** Integrations connected through the prompt during this deploy. */
|
|
106
107
|
connected: string[];
|
|
108
|
+
/** The listener's active version after this deploy; absent from a Cloud without versions. */
|
|
109
|
+
version?: CloudFlowVersionChange;
|
|
107
110
|
}
|
|
108
111
|
|
|
109
112
|
export function parseRepository(value: string): { owner: string; name: string } {
|
|
@@ -165,9 +168,18 @@ export function parseTriggerSource(value: string): FlowTriggerSource {
|
|
|
165
168
|
return { provider: provider as FlowTriggerProvider, settings };
|
|
166
169
|
}
|
|
167
170
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
+
/** One authored source, read and loaded the way every hosted deploy sends it. */
|
|
172
|
+
export interface DeploySource {
|
|
173
|
+
bytes: Buffer;
|
|
174
|
+
source: string;
|
|
175
|
+
definition: ReturnType<Awaited<ReturnType<typeof loadAuthoredFlow>>['getDefinition']>;
|
|
176
|
+
extensions: Awaited<ReturnType<typeof collectExtensionSubmissions>>;
|
|
177
|
+
projectCli: string | undefined;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export async function loadDeploySource(
|
|
181
|
+
input: { path: string; plugins?: readonly string[] },
|
|
182
|
+
): Promise<DeploySource> {
|
|
171
183
|
if (!/\.flow\.ts$/iu.test(input.path)) {
|
|
172
184
|
throw new CloudFlowError('unsupported_source', 'flows deploy takes one authored .flow.ts source.');
|
|
173
185
|
}
|
|
@@ -204,6 +216,13 @@ export async function deployToCloud(
|
|
|
204
216
|
} catch {
|
|
205
217
|
projectCli = undefined;
|
|
206
218
|
}
|
|
219
|
+
return { bytes, source, definition, extensions, projectCli };
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export async function deployToCloud(
|
|
223
|
+
input: DeployToCloudInput, options: CloudConnectionOptions = {},
|
|
224
|
+
): Promise<CloudDeployment> {
|
|
225
|
+
const { bytes, source, definition, extensions, projectCli } = await loadDeploySource(input);
|
|
207
226
|
if (input.sources.length === 0 || input.sources.length > 10) {
|
|
208
227
|
throw new CloudFlowError('invalid_input', 'Give between one and ten --on trigger sources.');
|
|
209
228
|
}
|
|
@@ -274,11 +293,13 @@ export async function deployToCloud(
|
|
|
274
293
|
if (!isCloudRecord(result) || typeof result.agentId !== 'string' || typeof result.status !== 'string') {
|
|
275
294
|
throw new CloudFlowError('invalid_response', 'Cloud did not return a deployment.');
|
|
276
295
|
}
|
|
296
|
+
const version = parseVersionChange(result.version);
|
|
277
297
|
return {
|
|
278
298
|
agentId: result.agentId, name, status: result.status,
|
|
279
299
|
repository: input.repository, sources,
|
|
280
300
|
sourceSha256: createHash('sha256').update(bytes).digest('hex'),
|
|
281
301
|
requirements, connected,
|
|
302
|
+
...(version === undefined ? {} : { version }),
|
|
282
303
|
};
|
|
283
304
|
}
|
|
284
305
|
|
package/src/cloud-run.ts
CHANGED
|
@@ -98,6 +98,7 @@ export async function prepareCloudSubmission(
|
|
|
98
98
|
options: { input?: JsonValue; signal?: AbortSignal } = {},
|
|
99
99
|
): Promise<CloudSubmission> {
|
|
100
100
|
let spec: FlowSpec | undefined;
|
|
101
|
+
let compiledKernelInput = false;
|
|
101
102
|
let authored: { source: string; authority: CloudAuthoredAuthority; name: string; schedules: ScheduleTriggerSource[]; extensions: readonly FlowExtensionSubmission[] } | undefined;
|
|
102
103
|
const inputPresent = Object.prototype.hasOwnProperty.call(options, 'input');
|
|
103
104
|
let authoredInput: JsonValue | undefined;
|
|
@@ -150,7 +151,11 @@ export async function prepareCloudSubmission(
|
|
|
150
151
|
// compatibility. If both fail, retain the original authoring diagnostic.
|
|
151
152
|
try {
|
|
152
153
|
spec = compileSpec(kernelToAuthoring(parsed));
|
|
153
|
-
|
|
154
|
+
compiledKernelInput = true;
|
|
155
|
+
} catch (kernelError) {
|
|
156
|
+
if (kernelError instanceof CompileError && kernelError.kind === 'untrusted_cli_identity') {
|
|
157
|
+
throw new CloudFlowError('invalid_input', kernelError.message);
|
|
158
|
+
}
|
|
154
159
|
throw error;
|
|
155
160
|
}
|
|
156
161
|
}
|
|
@@ -176,7 +181,9 @@ export async function prepareCloudSubmission(
|
|
|
176
181
|
const kernel = toKernelSpec(spec!);
|
|
177
182
|
// JSON is a YAML subset. Sending canonical data preserves the exact spec
|
|
178
183
|
// while using the server's existing YAML-to-config admission path.
|
|
179
|
-
|
|
184
|
+
// A compiled input stays in the kernel dialect. kernelToAuthoring refuses
|
|
185
|
+
// serialized cli_identity: only local preflight may mint that proof.
|
|
186
|
+
return { workflow: canonicalize(compiledKernelInput ? kernel : spec), fileType: 'yaml', inputPresent: false, specHash: specHash(kernel),
|
|
180
187
|
name: spec!.name ?? "flow", schedules: [] };
|
|
181
188
|
}
|
|
182
189
|
return {
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { isCloudRecord } from './cloud-http.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* What a Cloud write did to a listener's active version (cloud#4115).
|
|
5
|
+
* `reactivated`: the bytes match an earlier version, so the pointer moved to
|
|
6
|
+
* it without a new row, and `version` can be LOWER than `previousVersion`.
|
|
7
|
+
*/
|
|
8
|
+
export interface CloudFlowVersionChange {
|
|
9
|
+
version: number;
|
|
10
|
+
previousVersion: number | null;
|
|
11
|
+
change: 'created' | 'reactivated' | 'unchanged';
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface CloudFlowVersion {
|
|
15
|
+
version: number;
|
|
16
|
+
sourceSha256: string;
|
|
17
|
+
origin: string;
|
|
18
|
+
createdAt: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const CHANGES = ['created', 'reactivated', 'unchanged'] as const;
|
|
22
|
+
|
|
23
|
+
function isVersionNumber(value: unknown): value is number {
|
|
24
|
+
return typeof value === 'number' && Number.isSafeInteger(value) && value >= 1;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Undefined for a Cloud that predates versions or a malformed field. */
|
|
28
|
+
export function parseVersionChange(value: unknown): CloudFlowVersionChange | undefined {
|
|
29
|
+
if (!isCloudRecord(value) || !isVersionNumber(value.version)
|
|
30
|
+
|| !(value.previousVersion === null || isVersionNumber(value.previousVersion))
|
|
31
|
+
|| !(CHANGES as readonly unknown[]).includes(value.change)) return undefined;
|
|
32
|
+
return {
|
|
33
|
+
version: value.version,
|
|
34
|
+
previousVersion: value.previousVersion as number | null,
|
|
35
|
+
change: value.change as CloudFlowVersionChange['change'],
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function parseVersion(value: unknown): CloudFlowVersion | undefined {
|
|
40
|
+
if (!isCloudRecord(value) || !isVersionNumber(value.version) || typeof value.sourceSha256 !== 'string'
|
|
41
|
+
|| typeof value.origin !== 'string' || typeof value.createdAt !== 'string') return undefined;
|
|
42
|
+
return { version: value.version, sourceSha256: value.sourceSha256, origin: value.origin, createdAt: value.createdAt };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* `version 4 (was 3)`; a rollback by content says the number went down, so
|
|
47
|
+
* nobody reads v2-after-v3 as a failed deploy.
|
|
48
|
+
*/
|
|
49
|
+
export function describeVersionChange(change: CloudFlowVersionChange): string {
|
|
50
|
+
if (change.change === 'unchanged') return `version ${change.version} (unchanged)`;
|
|
51
|
+
const was = change.previousVersion === null ? '' : ` (was ${change.previousVersion}`;
|
|
52
|
+
if (change.change === 'reactivated') {
|
|
53
|
+
const down = change.previousVersion !== null && change.version < change.previousVersion
|
|
54
|
+
? '; active version went down' : '';
|
|
55
|
+
return `re-activated version ${change.version}${was}${down}${was ? ')' : ''}`;
|
|
56
|
+
}
|
|
57
|
+
return `version ${change.version}${was}${was ? ')' : ''}`;
|
|
58
|
+
}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { ensureIntegrationsConnected, type ConnectPrompt } from './cloud-connect.js';
|
|
3
|
+
import {
|
|
4
|
+
CloudFlowError, cloudFetch, cloudRequest, isCloudRecord, type CloudConnectionOptions,
|
|
5
|
+
} from './cloud-http.js';
|
|
6
|
+
import {
|
|
7
|
+
FLOW_AGENT_HARNESSES, listCloudDeployments, loadDeploySource, type FlowTriggerSource,
|
|
8
|
+
} from './cloud-deploy.js';
|
|
9
|
+
import {
|
|
10
|
+
parseVersion, parseVersionChange, type CloudFlowVersion, type CloudFlowVersionChange,
|
|
11
|
+
} from './cloud-versions-wire.js';
|
|
12
|
+
import { flowRequirements, mergeFlowExtensionRequirements, type FlowRequirements } from './flow-requirements.js';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Versions of a hosted listener (AgentWorkforce/cloud#4115). A deployed
|
|
16
|
+
* flow's source is immutable: `flows deploy <file> --flow <name-or-id>` makes
|
|
17
|
+
* the next version and moves the listener's pointer to it, leaving the
|
|
18
|
+
* listener's repository, triggers, approver, agents and run budget untouched;
|
|
19
|
+
* `flows rollback` moves the pointer to any recorded version.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
const LISTENER_UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/iu;
|
|
23
|
+
|
|
24
|
+
export interface CloudListener {
|
|
25
|
+
agentId: string;
|
|
26
|
+
name: string;
|
|
27
|
+
status: string;
|
|
28
|
+
repository: { owner: string; name: string; host?: 'gitlab' };
|
|
29
|
+
sources: FlowTriggerSource[];
|
|
30
|
+
activeVersion: CloudFlowVersion | null;
|
|
31
|
+
versions: CloudFlowVersion[];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** A listener id as given, else the one deployment in the workspace with that name. */
|
|
35
|
+
export async function resolveCloudFlow(flow: string, options: CloudConnectionOptions = {}): Promise<string> {
|
|
36
|
+
const wanted = flow.trim();
|
|
37
|
+
if (!wanted) throw new CloudFlowError('invalid_input', '--flow needs a flow name or listener id.');
|
|
38
|
+
if (LISTENER_UUID.test(wanted)) return wanted.toLowerCase();
|
|
39
|
+
const deployments = await listCloudDeployments(options);
|
|
40
|
+
// Any listener id the workspace lists, before names: ids are not all uuids.
|
|
41
|
+
const byId = deployments.find(deployment => deployment.agentId === wanted);
|
|
42
|
+
if (byId) return byId.agentId;
|
|
43
|
+
const matches = deployments.filter(deployment => deployment.name.toLowerCase() === wanted.toLowerCase());
|
|
44
|
+
if (matches.length === 1) return matches[0]!.agentId;
|
|
45
|
+
if (matches.length === 0) {
|
|
46
|
+
throw new CloudFlowError('invalid_input', `No flow named "${wanted}" in this workspace; list them with: flows deployments`);
|
|
47
|
+
}
|
|
48
|
+
throw new CloudFlowError('invalid_input',
|
|
49
|
+
`More than one flow is named "${wanted}" (${matches.map(match => match.agentId).join(', ')}); pass the listener id.`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export async function getCloudListener(agentId: string, options: CloudConnectionOptions = {}): Promise<CloudListener> {
|
|
53
|
+
const payload = await cloudRequest(`/api/v1/flows/listeners/${encodeURIComponent(agentId)}`, options);
|
|
54
|
+
const listener = isCloudRecord(payload) && isCloudRecord(payload.listener) ? payload.listener : undefined;
|
|
55
|
+
if (!listener || typeof listener.name !== 'string' || typeof listener.status !== 'string'
|
|
56
|
+
|| !isCloudRecord(listener.repository) || typeof listener.repository.owner !== 'string'
|
|
57
|
+
|| typeof listener.repository.name !== 'string' || !Array.isArray(listener.sources)) {
|
|
58
|
+
throw new CloudFlowError('invalid_response', 'Cloud did not return the flow listener.');
|
|
59
|
+
}
|
|
60
|
+
const versions = isCloudRecord(payload) && Array.isArray(payload.versions)
|
|
61
|
+
? payload.versions.flatMap(entry => parseVersion(entry) ?? []) : [];
|
|
62
|
+
return {
|
|
63
|
+
agentId,
|
|
64
|
+
name: listener.name,
|
|
65
|
+
status: listener.status,
|
|
66
|
+
repository: {
|
|
67
|
+
owner: listener.repository.owner, name: listener.repository.name,
|
|
68
|
+
...(listener.repository.host === 'gitlab' ? { host: 'gitlab' as const } : {}),
|
|
69
|
+
},
|
|
70
|
+
sources: listener.sources.flatMap((entry): FlowTriggerSource[] => isCloudRecord(entry) && typeof entry.provider === 'string'
|
|
71
|
+
? [{ provider: entry.provider as FlowTriggerSource['provider'],
|
|
72
|
+
settings: Object.fromEntries(Object.entries(isCloudRecord(entry.settings) ? entry.settings : {})
|
|
73
|
+
.flatMap(([key, value]) => typeof value === 'string' ? [[key, value]] : typeof value === 'boolean' ? [[key, String(value)]] : [])) }]
|
|
74
|
+
: []),
|
|
75
|
+
activeVersion: isCloudRecord(payload) ? parseVersion(payload.activeVersion) ?? null : null,
|
|
76
|
+
versions,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export interface DeployVersionInput {
|
|
81
|
+
path: string;
|
|
82
|
+
/** The flow's deployed name or its listener id. */
|
|
83
|
+
flow: string;
|
|
84
|
+
plugins?: readonly string[];
|
|
85
|
+
connect?: ConnectPrompt;
|
|
86
|
+
checkConnections?: boolean;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export interface CloudVersionDeployment {
|
|
90
|
+
agentId: string;
|
|
91
|
+
name: string;
|
|
92
|
+
status: string;
|
|
93
|
+
sourceSha256: string;
|
|
94
|
+
requirements: FlowRequirements;
|
|
95
|
+
connected: string[];
|
|
96
|
+
version: CloudFlowVersionChange;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** `POST /api/v1/flows/listeners/<id>/versions`: the source alone becomes the next version. */
|
|
100
|
+
export async function deployVersionToCloud(
|
|
101
|
+
input: DeployVersionInput, options: CloudConnectionOptions = {},
|
|
102
|
+
): Promise<CloudVersionDeployment> {
|
|
103
|
+
const { bytes, source, definition, extensions, projectCli } = await loadDeploySource(input);
|
|
104
|
+
const agentId = await resolveCloudFlow(input.flow, options);
|
|
105
|
+
const listener = await getCloudListener(agentId, options);
|
|
106
|
+
options.signal?.throwIfAborted();
|
|
107
|
+
// Requirements derive from the source, read against the listener's own
|
|
108
|
+
// repository and triggers, exactly as the create form reads its flags.
|
|
109
|
+
const requirements = mergeFlowExtensionRequirements(
|
|
110
|
+
flowRequirements(definition, {
|
|
111
|
+
sources: listener.sources,
|
|
112
|
+
// A GitHub repository means every run needs GitHub; a GitLab project does not.
|
|
113
|
+
...(listener.repository.host === 'gitlab' ? {} : { repository: listener.repository }),
|
|
114
|
+
...(projectCli === undefined ? {} : { projectCli }),
|
|
115
|
+
}),
|
|
116
|
+
extensions.map(extension => ({ name: extension.name, permissions: extension.manifest.permissions })),
|
|
117
|
+
);
|
|
118
|
+
// As on create: a harness Cloud cannot run is refused here, not deployed.
|
|
119
|
+
// There is no --agents override beside --flow; the listener's agents stay.
|
|
120
|
+
const unsupported = requirements.harnessUses
|
|
121
|
+
.filter(use => !(FLOW_AGENT_HARNESSES as readonly string[]).includes(use.harness));
|
|
122
|
+
if (unsupported.length > 0) {
|
|
123
|
+
throw new CloudFlowError('unsupported_source',
|
|
124
|
+
`This flow declares ${unsupported.map(use => `${use.harness} (${use.detail})`).join(', ')}, which Cloud deployments cannot run yet; `
|
|
125
|
+
+ `Cloud runs ${FLOW_AGENT_HARNESSES.join(' and ')}. Change the declaration before deploying a new version.`);
|
|
126
|
+
}
|
|
127
|
+
const whoami = await cloudRequest('/api/v1/auth/whoami', options);
|
|
128
|
+
const workspace = isCloudRecord(whoami) && isCloudRecord(whoami.currentWorkspace) ? whoami.currentWorkspace : undefined;
|
|
129
|
+
if (workspace === undefined || typeof workspace.id !== 'string' || !workspace.id) {
|
|
130
|
+
throw new CloudFlowError('invalid_response', 'Cloud did not report a current workspace for this credential.');
|
|
131
|
+
}
|
|
132
|
+
// A draft activates nothing, so Cloud checks nothing; match it here.
|
|
133
|
+
const connected = listener.status !== 'listening' || input.checkConnections === false
|
|
134
|
+
? []
|
|
135
|
+
: (await ensureIntegrationsConnected(requirements, {
|
|
136
|
+
...options, workspaceId: workspace.id, ...(input.connect === undefined ? {} : { prompt: input.connect }),
|
|
137
|
+
})).connected;
|
|
138
|
+
options.signal?.throwIfAborted();
|
|
139
|
+
const result = await cloudFetch(`/api/v1/flows/listeners/${encodeURIComponent(agentId)}/versions`, options, {
|
|
140
|
+
method: 'POST', detail: true, body: JSON.stringify({
|
|
141
|
+
workspaceId: workspace.id,
|
|
142
|
+
source,
|
|
143
|
+
requirements: {
|
|
144
|
+
integrations: requirements.integrations.map(integration => integration.provider),
|
|
145
|
+
harnesses: requirements.harnesses,
|
|
146
|
+
mcp: requirements.mcp,
|
|
147
|
+
},
|
|
148
|
+
...(extensions.length === 0 ? {} : { extensions }),
|
|
149
|
+
}),
|
|
150
|
+
});
|
|
151
|
+
const version = isCloudRecord(result) ? parseVersionChange(result.version) : undefined;
|
|
152
|
+
if (!isCloudRecord(result) || typeof result.status !== 'string' || version === undefined) {
|
|
153
|
+
throw new CloudFlowError('invalid_response', 'Cloud did not return the new flow version.');
|
|
154
|
+
}
|
|
155
|
+
return {
|
|
156
|
+
agentId, name: listener.name, status: result.status,
|
|
157
|
+
sourceSha256: createHash('sha256').update(bytes).digest('hex'),
|
|
158
|
+
requirements, connected, version,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** `POST /api/v1/flows/listeners/<id>/versions/<n>/activate`: history is kept; only the pointer moves. */
|
|
163
|
+
export async function activateCloudFlowVersion(
|
|
164
|
+
flow: string, version: number, options: CloudConnectionOptions = {},
|
|
165
|
+
): Promise<{ agentId: string; status: string; version: CloudFlowVersionChange }> {
|
|
166
|
+
if (!Number.isSafeInteger(version) || version < 1) {
|
|
167
|
+
throw new CloudFlowError('invalid_input', `A version is a positive whole number, got "${version}".`);
|
|
168
|
+
}
|
|
169
|
+
const agentId = await resolveCloudFlow(flow, options);
|
|
170
|
+
const result = await cloudFetch(
|
|
171
|
+
`/api/v1/flows/listeners/${encodeURIComponent(agentId)}/versions/${version}/activate`,
|
|
172
|
+
options, { method: 'POST', detail: true },
|
|
173
|
+
);
|
|
174
|
+
const change = isCloudRecord(result) ? parseVersionChange(result.version) : undefined;
|
|
175
|
+
if (!isCloudRecord(result) || typeof result.status !== 'string' || change === undefined) {
|
|
176
|
+
throw new CloudFlowError('invalid_response', 'Cloud did not confirm the active version.');
|
|
177
|
+
}
|
|
178
|
+
return { agentId, status: result.status, version: change };
|
|
179
|
+
}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { communicationHistory } from './history.js';
|
|
2
|
-
import {
|
|
2
|
+
import { mkdir, mkdtemp, rm, symlink } from 'node:fs/promises';
|
|
3
|
+
import { basename, join } from 'node:path';
|
|
3
4
|
import { setTimeout as delay } from 'node:timers/promises';
|
|
4
5
|
import type { JournalClient } from '../journal-client.js';
|
|
5
6
|
import type { StepDispatchEvent } from '../protocol.js';
|
|
6
|
-
import type {
|
|
7
|
+
import type { ResolvedKernelAgentStep } from '../resolved-cli-identity.js';
|
|
7
8
|
import { withWorkerLease } from '../worker-lease.js';
|
|
8
9
|
import { workerInstruction } from '../worker-input.js';
|
|
9
10
|
import { resolveCliModel } from '../cli-adapter.js';
|
|
@@ -20,7 +21,7 @@ export function requireCommunicationCli(cli: string | undefined): void {
|
|
|
20
21
|
export async function completeCommunicationDispatch(client: JournalClient, dispatch: StepDispatchEvent,
|
|
21
22
|
instruction: CommunicationInstruction, dataDir: string, runRoot?: string,
|
|
22
23
|
environment?: NodeJS.ProcessEnv): Promise<void> {
|
|
23
|
-
const spec = dispatch.spec as
|
|
24
|
+
const spec = dispatch.spec as ResolvedKernelAgentStep;
|
|
24
25
|
requireCommunicationCli(spec.cli);
|
|
25
26
|
let output: unknown;
|
|
26
27
|
let completionReason: 'success' | 'worker_error' = 'success';
|
|
@@ -36,7 +37,7 @@ export async function completeCommunicationDispatch(client: JournalClient, dispa
|
|
|
36
37
|
started_pins: dispatch.pins, end_pins: dispatch.pins });
|
|
37
38
|
}
|
|
38
39
|
async function run(client: JournalClient, dispatch: StepDispatchEvent, instruction: CommunicationInstruction,
|
|
39
|
-
spec:
|
|
40
|
+
spec: ResolvedKernelAgentStep, dataDir: string, lease: AbortSignal, runRoot?: string,
|
|
40
41
|
environment?: NodeJS.ProcessEnv): Promise<unknown> {
|
|
41
42
|
// Same contract as the CLI worker: a declared directory is resolved and held
|
|
42
43
|
// inside the same run root the CLI worker measures against, before anything
|
|
@@ -53,6 +54,7 @@ async function run(client: JournalClient, dispatch: StepDispatchEvent, instructi
|
|
|
53
54
|
let pumping: Promise<void> | undefined;
|
|
54
55
|
let receipts = Promise.resolve();
|
|
55
56
|
let unsubscribe: (() => void) | undefined;
|
|
57
|
+
let cliLinkDirectory: string | undefined;
|
|
56
58
|
try {
|
|
57
59
|
let resolve!: (value: unknown) => void;
|
|
58
60
|
let reject!: (reason: unknown) => void;
|
|
@@ -80,10 +82,20 @@ async function run(client: JournalClient, dispatch: StepDispatchEvent, instructi
|
|
|
80
82
|
void receipts.catch(reject);
|
|
81
83
|
});
|
|
82
84
|
// Relay supplies each CLI's launch flags and injection behavior.
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
85
|
+
const cliIdentity = spec.cli_identity ?? spec.cli!;
|
|
86
|
+
const argv0 = basename(cliIdentity);
|
|
87
|
+
let command = spec.cli!;
|
|
88
|
+
if (basename(command) !== argv0) {
|
|
89
|
+
const linkRoot = join(dataDir, 'communication');
|
|
90
|
+
await mkdir(linkRoot, { recursive: true, mode: 0o700 });
|
|
91
|
+
cliLinkDirectory = await mkdtemp(join(linkRoot, 'cli-'));
|
|
92
|
+
command = join(cliLinkDirectory, argv0);
|
|
93
|
+
await symlink(spec.cli!, command, 'file');
|
|
94
|
+
}
|
|
95
|
+
handle = await relay.broker.spawnPty({ name, cli: basename(cliIdentity).replace(/\.exe$/i, ''), task: prompt, channels: [], skipRelayPrompt: true,
|
|
96
|
+
model: resolveCliModel(cliIdentity, spec.model), cwd: directory,
|
|
97
|
+
harnessConfig: { runtime: 'pty', command: quote(command), args: [],
|
|
98
|
+
cwd: directory, env: { ...agentEnvironment(cliIdentity, environment ?? process.env),
|
|
87
99
|
RELAYFLOW_COMMUNICATION_SOCKET: tools.path, RELAYFLOW_COMMUNICATION_TOKEN: tools.token },
|
|
88
100
|
delivery: { mode: 'pty-injection', format: 'relay-block' } } });
|
|
89
101
|
const ready = await handle.waitForReady(Math.min(instruction.timeoutMs, 90_000));
|
|
@@ -100,7 +112,10 @@ async function run(client: JournalClient, dispatch: StepDispatchEvent, instructi
|
|
|
100
112
|
finally {
|
|
101
113
|
try { await receipts; }
|
|
102
114
|
finally { try { await handle?.release('Flow communication attempt ended', { deleteIdentity: true }); }
|
|
103
|
-
finally { try { await tools?.close(); } finally {
|
|
115
|
+
finally { try { await tools?.close(); } finally {
|
|
116
|
+
try { await relay.close(); }
|
|
117
|
+
finally { if (cliLinkDirectory) await rm(cliLinkDirectory, { recursive: true, force: true }); }
|
|
118
|
+
} } }
|
|
104
119
|
}
|
|
105
120
|
}
|
|
106
121
|
}
|
package/src/compile.ts
CHANGED
|
@@ -24,10 +24,7 @@ import type {
|
|
|
24
24
|
AgentStepSpec,
|
|
25
25
|
DeterministicStepSpec,
|
|
26
26
|
FlowSpec,
|
|
27
|
-
KernelAgentStep,
|
|
28
27
|
KernelRunSpec,
|
|
29
|
-
KernelStepCommon,
|
|
30
|
-
KernelStepSpec,
|
|
31
28
|
KernelTriggerSpec,
|
|
32
29
|
KernelVerificationSpec,
|
|
33
30
|
LlmStepSpec,
|
|
@@ -45,6 +42,13 @@ import { validateSpec, type ValidationResult } from './validate.js';
|
|
|
45
42
|
import { snapshotJsonValue } from './json-value.js';
|
|
46
43
|
import { expandYamlHelpers } from './yaml-helpers.js';
|
|
47
44
|
import { expandCommunication, validateCommunicationTopology } from './communication/spec.js';
|
|
45
|
+
import {
|
|
46
|
+
inheritResolvedCliIdentities,
|
|
47
|
+
resolvedCliIdentities,
|
|
48
|
+
type ResolvedKernelAgentStep,
|
|
49
|
+
type ResolvedKernelStepCommon,
|
|
50
|
+
type ResolvedKernelStepSpec,
|
|
51
|
+
} from './resolved-cli-identity.js';
|
|
48
52
|
|
|
49
53
|
export class CompileError extends Error {
|
|
50
54
|
readonly errors: string[];
|
|
@@ -173,7 +177,7 @@ export function compileSpec(spec: unknown): CompiledFlowSpec {
|
|
|
173
177
|
...(input.workspace !== undefined ? { workspace: input.workspace } : {}),
|
|
174
178
|
...(input.tools !== undefined ? { tools: input.tools } : {}),
|
|
175
179
|
};
|
|
176
|
-
return flow;
|
|
180
|
+
return inheritResolvedCliIdentities(spec, flow);
|
|
177
181
|
}
|
|
178
182
|
|
|
179
183
|
function compileStep(step: StepSpec): StepSpec {
|
|
@@ -325,6 +329,7 @@ export function toKernelSpec(flow: FlowSpec): KernelRunSpec {
|
|
|
325
329
|
// This public boundary is callable without compileSpec. Compile again so
|
|
326
330
|
// runtime casts are validated and all returned schema data is snapshotted.
|
|
327
331
|
const compiled = compileSpec(flow);
|
|
332
|
+
const cliIdentities = resolvedCliIdentities(compiled);
|
|
328
333
|
|
|
329
334
|
return {
|
|
330
335
|
version: compiled.version,
|
|
@@ -347,7 +352,10 @@ export function toKernelSpec(flow: FlowSpec): KernelRunSpec {
|
|
|
347
352
|
// reverse order would hand the green gate a source whose barrier it does
|
|
348
353
|
// not wait for.
|
|
349
354
|
steps: lowerNamedGates(lowerStepsGreenGates(compiled.steps))
|
|
350
|
-
.map((step) => toKernelStep(
|
|
355
|
+
.map((step) => toKernelStep(
|
|
356
|
+
resolveNamedAgent(step, compiled.agents),
|
|
357
|
+
cliIdentities?.get(step.id),
|
|
358
|
+
)),
|
|
351
359
|
...(compiled.budget !== undefined ? { budget: toKernelBudget(compiled.budget) } : {}),
|
|
352
360
|
};
|
|
353
361
|
}
|
|
@@ -374,7 +382,7 @@ export function kernelToAuthoring(value: unknown): unknown {
|
|
|
374
382
|
const steps = requireKernelArray(root['steps'], 'spec.steps')
|
|
375
383
|
.map((step, index) => kernelStepToAuthoring(step, `spec.steps[${index}]`));
|
|
376
384
|
const triggers = root['triggers'];
|
|
377
|
-
|
|
385
|
+
const authoring: FlowSpec = {
|
|
378
386
|
...copyDefined(root, ['version', 'name', 'description', 'cli']),
|
|
379
387
|
...(triggers !== undefined
|
|
380
388
|
? {
|
|
@@ -386,7 +394,8 @@ export function kernelToAuthoring(value: unknown): unknown {
|
|
|
386
394
|
...(root['budget'] !== undefined
|
|
387
395
|
? { budget: kernelBudgetToAuthoring(root['budget'], 'spec.budget') }
|
|
388
396
|
: {}),
|
|
389
|
-
};
|
|
397
|
+
} as FlowSpec;
|
|
398
|
+
return authoring;
|
|
390
399
|
}
|
|
391
400
|
|
|
392
401
|
/**
|
|
@@ -443,17 +452,22 @@ function kernelStepToAuthoring(value: unknown, at: string): unknown {
|
|
|
443
452
|
const unionKeys = [
|
|
444
453
|
'id', 'type', 'depends_on', 'max_iterations', 'retry', 'verification', 'memory', 'requirements', 'input',
|
|
445
454
|
'command', 'timeout_ms', 'lease_ms', 'on_non_zero', 'prompt', 'model', 'cli', 'instruction',
|
|
446
|
-
'cwd', 'recovery_mode', 'surfaces', 'permissions',
|
|
455
|
+
'cwd', 'recovery_mode', 'surfaces', 'permissions', 'cli_identity',
|
|
447
456
|
] as const;
|
|
448
457
|
const step = requireKernelObject(value, unionKeys, at);
|
|
458
|
+
if (step['cli_identity'] !== undefined) {
|
|
459
|
+
throw new CompileError([
|
|
460
|
+
`${at}.cli_identity: host-proved adapter identity is not accepted from serialized input`,
|
|
461
|
+
], 'untrusted_cli_identity');
|
|
462
|
+
}
|
|
449
463
|
const type = step['type'];
|
|
450
464
|
const commonKeys = ['id', 'type', 'depends_on', 'max_iterations', 'retry', 'verification', 'memory', 'requirements', 'input'] as const;
|
|
451
465
|
const typeKeys = type === 'deterministic'
|
|
452
466
|
? ['command', 'timeout_ms', 'lease_ms', 'on_non_zero'] as const
|
|
453
467
|
: type === 'llm'
|
|
454
|
-
? ['prompt', 'model', 'cli'] as const
|
|
468
|
+
? ['prompt', 'model', 'cli', 'cli_identity'] as const
|
|
455
469
|
: type === 'agent'
|
|
456
|
-
? ['instruction', 'cli', 'model', 'cwd', 'recovery_mode', 'surfaces', 'permissions'] as const
|
|
470
|
+
? ['instruction', 'cli', 'model', 'cwd', 'recovery_mode', 'surfaces', 'permissions', 'cli_identity'] as const
|
|
457
471
|
: [];
|
|
458
472
|
assertKernelKeys(step, [...commonKeys, ...typeKeys], at);
|
|
459
473
|
if (step['retry'] !== undefined) validateAuthoringRetryDefaults(step['retry'], `${at}.retry`);
|
|
@@ -596,8 +610,8 @@ function isObject(value: unknown): value is Record<string, unknown> {
|
|
|
596
610
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
597
611
|
}
|
|
598
612
|
|
|
599
|
-
function toKernelStep(step: StepSpec):
|
|
600
|
-
const common:
|
|
613
|
+
function toKernelStep(step: StepSpec, cliIdentity?: string): ResolvedKernelStepSpec {
|
|
614
|
+
const common: ResolvedKernelStepCommon = {
|
|
601
615
|
id: step.id,
|
|
602
616
|
depends_on: step.input === undefined ? step.dependsOn ?? []
|
|
603
617
|
: [...new Set([...(step.dependsOn ?? []), ...bindingDependencies(step.input)])],
|
|
@@ -605,6 +619,7 @@ function toKernelStep(step: StepSpec): KernelStepSpec {
|
|
|
605
619
|
max_iterations: step.maxIterations ?? 1,
|
|
606
620
|
retry: { ...KERNEL_RETRY_DEFAULTS },
|
|
607
621
|
verification: toKernelVerification(step),
|
|
622
|
+
...(cliIdentity !== undefined ? { cli_identity: cliIdentity } : {}),
|
|
608
623
|
...(step.requirements !== undefined ? { requirements: {
|
|
609
624
|
...Object.fromEntries(Object.entries(step.requirements).filter(([key]) => key !== 'expectedDurationMs')),
|
|
610
625
|
...(step.requirements.expectedDurationMs !== undefined ? { expected_duration_ms: step.requirements.expectedDurationMs } : {}),
|
|
@@ -643,7 +658,7 @@ function toKernelStep(step: StepSpec): KernelStepSpec {
|
|
|
643
658
|
};
|
|
644
659
|
}
|
|
645
660
|
case 'agent': {
|
|
646
|
-
const out:
|
|
661
|
+
const out: ResolvedKernelAgentStep = {
|
|
647
662
|
...common,
|
|
648
663
|
type: 'agent',
|
|
649
664
|
instruction: step.instruction,
|
package/src/failure-kinds.ts
CHANGED
|
@@ -144,6 +144,12 @@ export const RUN_FAILURE_KINDS = [
|
|
|
144
144
|
* attaches and before the resume touches the journal.
|
|
145
145
|
*/
|
|
146
146
|
'local_agent_unavailable',
|
|
147
|
+
/**
|
|
148
|
+
* The authored root's worker lease was lost and the kernel's retry did not
|
|
149
|
+
* reach this process. The run is NOT failed (`status: running`): its
|
|
150
|
+
* completed steps are journaled and `flows resume <rootRunId>` continues it.
|
|
151
|
+
*/
|
|
152
|
+
'root_lease_lost',
|
|
147
153
|
] as const;
|
|
148
154
|
|
|
149
155
|
/**
|
|
@@ -271,6 +277,14 @@ export interface StepFailedDetails {
|
|
|
271
277
|
* `retries_exhausted`.
|
|
272
278
|
*/
|
|
273
279
|
maxIterations?: number;
|
|
280
|
+
/**
|
|
281
|
+
* Attempts before this one that died without a result -- the kernel
|
|
282
|
+
* journaled them `crashed` or `lease_expired` itself -- and so, by design,
|
|
283
|
+
* consumed none of `maxIterations` (relayflowd-core recovery.rs). Present
|
|
284
|
+
* only when non-zero. Without it `attempt` can exceed `maxIterations`
|
|
285
|
+
* (`attempt=2/1`) and read as the kernel overrunning its own budget.
|
|
286
|
+
*/
|
|
287
|
+
unchargedAttempts?: number;
|
|
274
288
|
exitCode?: number;
|
|
275
289
|
/**
|
|
276
290
|
* Terminal-safe UTF-8 excerpt of the captured stdout, at most
|