@relayflows/sdk 2.0.16 → 2.0.18
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-flow-error.d.ts +21 -1
- package/dist/authored-flow-error.d.ts.map +1 -1
- package/dist/authored-flow-error.js +14 -0
- package/dist/authored-flow-error.js.map +1 -1
- package/dist/authored-flow-executor.d.ts.map +1 -1
- package/dist/authored-flow-executor.js +133 -3
- package/dist/authored-flow-executor.js.map +1 -1
- package/dist/authored-flow-lifecycle.d.ts +10 -0
- package/dist/authored-flow-lifecycle.d.ts.map +1 -1
- package/dist/authored-flow-lifecycle.js +7 -0
- package/dist/authored-flow-lifecycle.js.map +1 -1
- package/dist/authored-flow-operation.d.ts +11 -0
- package/dist/authored-flow-operation.d.ts.map +1 -1
- package/dist/authored-flow-operation.js +32 -7
- package/dist/authored-flow-operation.js.map +1 -1
- package/dist/authored-human.d.ts +49 -0
- package/dist/authored-human.d.ts.map +1 -0
- package/dist/authored-human.js +97 -0
- package/dist/authored-human.js.map +1 -0
- package/dist/authored-node-entry.js +3 -2
- package/dist/authored-node-entry.js.map +1 -1
- package/dist/authored-node-runner.d.ts.map +1 -1
- package/dist/authored-node-runner.js +52 -18
- package/dist/authored-node-runner.js.map +1 -1
- package/dist/authored-root.d.ts.map +1 -1
- package/dist/authored-root.js +29 -1
- package/dist/authored-root.js.map +1 -1
- package/dist/authored-worker-step.d.ts.map +1 -1
- package/dist/authored-worker-step.js +14 -25
- package/dist/authored-worker-step.js.map +1 -1
- package/dist/cli/answer.d.ts +17 -0
- package/dist/cli/answer.d.ts.map +1 -0
- package/dist/cli/answer.js +89 -0
- package/dist/cli/answer.js.map +1 -0
- package/dist/cli/build.js +1 -1
- package/dist/cli/build.js.map +1 -1
- package/dist/cli/check-triggers.d.ts.map +1 -1
- package/dist/cli/check-triggers.js +12 -0
- package/dist/cli/check-triggers.js.map +1 -1
- package/dist/cli/check.d.ts +25 -0
- package/dist/cli/check.d.ts.map +1 -1
- package/dist/cli/check.js +14 -2
- package/dist/cli/check.js.map +1 -1
- package/dist/cli/cloud-connect-cli.d.ts +54 -0
- package/dist/cli/cloud-connect-cli.d.ts.map +1 -0
- package/dist/cli/cloud-connect-cli.js +109 -0
- package/dist/cli/cloud-connect-cli.js.map +1 -0
- package/dist/cli/cloud-deploy.d.ts +3 -1
- package/dist/cli/cloud-deploy.d.ts.map +1 -1
- package/dist/cli/cloud-deploy.js +44 -6
- package/dist/cli/cloud-deploy.js.map +1 -1
- package/dist/cli/cloud-run.d.ts +2 -1
- package/dist/cli/cloud-run.d.ts.map +1 -1
- package/dist/cli/cloud-run.js +17 -3
- package/dist/cli/cloud-run.js.map +1 -1
- package/dist/cli/cloud-schedule.d.ts +31 -0
- package/dist/cli/cloud-schedule.d.ts.map +1 -0
- package/dist/cli/cloud-schedule.js +156 -0
- package/dist/cli/cloud-schedule.js.map +1 -0
- package/dist/cli/direct-run.d.ts.map +1 -1
- package/dist/cli/direct-run.js +6 -2
- package/dist/cli/direct-run.js.map +1 -1
- package/dist/cli/run.d.ts +22 -2
- package/dist/cli/run.d.ts.map +1 -1
- package/dist/cli/run.js +39 -4
- package/dist/cli/run.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +148 -8
- package/dist/cli.js.map +1 -1
- package/dist/cloud-connect.d.ts +52 -0
- package/dist/cloud-connect.d.ts.map +1 -0
- package/dist/cloud-connect.js +138 -0
- package/dist/cloud-connect.js.map +1 -0
- package/dist/cloud-deploy.d.ts +19 -1
- package/dist/cloud-deploy.d.ts.map +1 -1
- package/dist/cloud-deploy.js +52 -2
- package/dist/cloud-deploy.js.map +1 -1
- package/dist/cloud-http.d.ts +17 -2
- package/dist/cloud-http.d.ts.map +1 -1
- package/dist/cloud-http.js +44 -8
- package/dist/cloud-http.js.map +1 -1
- package/dist/cloud-run.d.ts +30 -0
- package/dist/cloud-run.d.ts.map +1 -1
- package/dist/cloud-run.js +70 -27
- package/dist/cloud-run.js.map +1 -1
- package/dist/cloud-schedule.d.ts +55 -0
- package/dist/cloud-schedule.d.ts.map +1 -0
- package/dist/cloud-schedule.js +146 -0
- package/dist/cloud-schedule.js.map +1 -0
- package/dist/failure-kinds.d.ts +3 -3
- package/dist/failure-kinds.d.ts.map +1 -1
- package/dist/failure-kinds.js +4 -0
- package/dist/failure-kinds.js.map +1 -1
- package/dist/flow-requirements.d.ts +68 -0
- package/dist/flow-requirements.d.ts.map +1 -0
- package/dist/flow-requirements.js +152 -0
- package/dist/flow-requirements.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -1
- package/dist/journal-client.d.ts +11 -0
- package/dist/journal-client.d.ts.map +1 -1
- package/dist/journal-client.js +13 -0
- package/dist/journal-client.js.map +1 -1
- package/dist/named-gate-lowering.js +9 -0
- package/dist/named-gate-lowering.js.map +1 -1
- package/dist/named-gates.d.ts +1 -1
- package/dist/named-gates.d.ts.map +1 -1
- package/dist/named-gates.js +11 -1
- package/dist/named-gates.js.map +1 -1
- package/dist/preflight.d.ts.map +1 -1
- package/dist/preflight.js +155 -9
- package/dist/preflight.js.map +1 -1
- package/dist/progress.d.ts +2 -1
- package/dist/progress.d.ts.map +1 -1
- package/dist/progress.js +9 -3
- package/dist/progress.js.map +1 -1
- package/dist/protocol.d.ts +25 -1
- package/dist/protocol.d.ts.map +1 -1
- package/dist/schedule-trigger.d.ts +35 -0
- package/dist/schedule-trigger.d.ts.map +1 -0
- package/dist/schedule-trigger.js +53 -0
- package/dist/schedule-trigger.js.map +1 -0
- package/dist/spec.d.ts +11 -1
- package/dist/spec.d.ts.map +1 -1
- package/dist/spec.js.map +1 -1
- package/dist/trigger-executor.js +1 -1
- package/dist/trigger-executor.js.map +1 -1
- package/dist/validate.js +1 -1
- package/dist/validate.js.map +1 -1
- package/dist/worker-cli.d.ts +10 -0
- package/dist/worker-cli.d.ts.map +1 -1
- package/dist/worker-cli.js +51 -22
- package/dist/worker-cli.js.map +1 -1
- package/dist/worker.d.ts.map +1 -1
- package/dist/worker.js +9 -0
- 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 +6 -3
- package/dist/wrapper-session.js.map +1 -1
- package/package.json +3 -3
- package/src/authored-flow-error.ts +30 -0
- package/src/authored-flow-executor.ts +141 -2
- package/src/authored-flow-lifecycle.ts +7 -0
- package/src/authored-flow-operation.ts +35 -10
- package/src/authored-human.ts +150 -0
- package/src/authored-node-entry.ts +3 -2
- package/src/authored-node-runner.ts +55 -18
- package/src/authored-root.ts +28 -1
- package/src/authored-worker-step.ts +15 -26
- package/src/cli/answer.ts +98 -0
- package/src/cli/build.ts +1 -1
- package/src/cli/check-triggers.ts +11 -0
- package/src/cli/check.ts +39 -2
- package/src/cli/cloud-connect-cli.ts +130 -0
- package/src/cli/cloud-deploy.ts +40 -6
- package/src/cli/cloud-run.ts +16 -4
- package/src/cli/cloud-schedule.ts +160 -0
- package/src/cli/direct-run.ts +6 -1
- package/src/cli/run.ts +53 -5
- package/src/cli.ts +134 -9
- package/src/cloud-connect.ts +187 -0
- package/src/cloud-deploy.ts +69 -3
- package/src/cloud-http.ts +52 -8
- package/src/cloud-run.ts +99 -26
- package/src/cloud-schedule.ts +175 -0
- package/src/failure-kinds.ts +4 -0
- package/src/flow-requirements.ts +201 -0
- package/src/index.ts +14 -0
- package/src/journal-client.ts +20 -0
- package/src/named-gate-lowering.ts +9 -0
- package/src/named-gates.ts +11 -1
- package/src/preflight.ts +132 -9
- package/src/progress.ts +11 -4
- package/src/protocol.ts +23 -0
- package/src/schedule-trigger.ts +73 -0
- package/src/spec.ts +12 -1
- package/src/trigger-executor.ts +1 -1
- package/src/validate.ts +1 -1
- package/src/worker-cli.ts +41 -0
- package/src/worker.ts +9 -0
- package/src/wrapper-session.ts +5 -1
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { CloudFlowError, cloudFetch, cloudRequest, isCloudRecord, type CloudConnectionOptions } from './cloud-http.js';
|
|
3
|
+
import type { FlowIntegrationRequirement, FlowRequirements } from './flow-requirements.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Integration readiness before a hosted submission — the CLI form of the
|
|
7
|
+
* deploy wizard's "Connect integrations" step, and the same relayfile
|
|
8
|
+
* connect-session flow `agentworkforce deploy` runs for a proactive agent:
|
|
9
|
+
*
|
|
10
|
+
* GET /api/v1/workspaces/<id>/integrations/<provider>/status?scope=workspace
|
|
11
|
+
* POST /api/v1/workspaces/<id>/integrations/connect-session
|
|
12
|
+
* { allowedIntegrations: [<provider>], scope: { kind: "workspace" } }
|
|
13
|
+
* → { connectLink } (older: sessionUrl / url), opened in the browser,
|
|
14
|
+
* then the status is polled until it reports ready.
|
|
15
|
+
*
|
|
16
|
+
* Only `workspace` scope is used: a flow deployment runs as the workspace,
|
|
17
|
+
* which is the scope `checkFlowConnections` reads on the server. Nothing here
|
|
18
|
+
* stores a credential — Cloud's connect page does — and a refusal names the
|
|
19
|
+
* provider and the two ways to connect it (this prompt, or the dashboard).
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
export interface ConnectPrompt {
|
|
23
|
+
/** Asked once per missing provider before a browser opens; `false` refuses the submission. */
|
|
24
|
+
confirm(question: string): Promise<boolean>;
|
|
25
|
+
info(line: string): void;
|
|
26
|
+
/** Defaults to the platform opener (`open`, `xdg-open`, `start`); a failure only means the URL was printed. */
|
|
27
|
+
openUrl?(url: string): Promise<void> | void;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface EnsureConnectionsOptions extends CloudConnectionOptions {
|
|
31
|
+
workspaceId: string;
|
|
32
|
+
/** Omitted (`--no-connect`, or no TTY) means a missing integration is a refusal, never a prompt. */
|
|
33
|
+
prompt?: ConnectPrompt;
|
|
34
|
+
/** Status poll cadence while the browser connect is pending. Default 2 seconds. */
|
|
35
|
+
pollIntervalMs?: number;
|
|
36
|
+
/** How long one connect may stay pending. Default 5 minutes. */
|
|
37
|
+
connectTimeoutMs?: number;
|
|
38
|
+
sleep?: (ms: number, signal?: AbortSignal) => Promise<void>;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface ConnectionsOutcome {
|
|
42
|
+
/** Providers the workspace already had. */
|
|
43
|
+
ready: string[];
|
|
44
|
+
/** Providers connected through this prompt, in order. */
|
|
45
|
+
connected: string[];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const WORKSPACE_ID = /^[A-Za-z0-9_-]{1,128}$/u;
|
|
49
|
+
const PROVIDER_ID = /^[a-z0-9][a-z0-9-]{0,63}$/u;
|
|
50
|
+
|
|
51
|
+
/** Whether the workspace has a ready connection for one provider. */
|
|
52
|
+
export async function integrationConnected(
|
|
53
|
+
workspaceId: string, provider: string, options: CloudConnectionOptions,
|
|
54
|
+
): Promise<boolean> {
|
|
55
|
+
assertIds(workspaceId, provider);
|
|
56
|
+
const base = `/api/v1/workspaces/${encodeURIComponent(workspaceId)}/integrations`;
|
|
57
|
+
try {
|
|
58
|
+
const status = await cloudFetch(`${base}/${encodeURIComponent(provider)}/status?scope=workspace`, options,
|
|
59
|
+
{ method: 'GET', detail: true });
|
|
60
|
+
return isReady(status);
|
|
61
|
+
} catch (error) {
|
|
62
|
+
if (!(error instanceof CloudFlowError) || error.code !== 'http_error') throw error;
|
|
63
|
+
if (error.refusal?.code === 'unknown_provider') {
|
|
64
|
+
throw new CloudFlowError('integration_not_connected',
|
|
65
|
+
`"${provider}" is not an integration this Cloud can connect; the flow declares it, so rename the declaration to a provider Cloud lists.`,
|
|
66
|
+
error.status, error.refusal);
|
|
67
|
+
}
|
|
68
|
+
// An older Cloud without the per-provider status route: the list still says which are connected.
|
|
69
|
+
if (error.status !== 404 && error.status !== 405) throw error;
|
|
70
|
+
const list = await cloudRequest(base, options);
|
|
71
|
+
const rows = Array.isArray(list) ? list : isCloudRecord(list) && Array.isArray(list.integrations) ? list.integrations : [];
|
|
72
|
+
return rows.some(row => isCloudRecord(row) && row.provider === provider && isReady(row));
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Every integration the requirements name is ready when this resolves; each
|
|
78
|
+
* missing one was connected through the prompt, or the whole submission is
|
|
79
|
+
* refused with `integration_not_connected` naming the first missing provider.
|
|
80
|
+
*/
|
|
81
|
+
export async function ensureIntegrationsConnected(
|
|
82
|
+
requirements: Pick<FlowRequirements, 'integrations'>, options: EnsureConnectionsOptions,
|
|
83
|
+
): Promise<ConnectionsOutcome> {
|
|
84
|
+
const outcome: ConnectionsOutcome = { ready: [], connected: [] };
|
|
85
|
+
for (const integration of requirements.integrations) {
|
|
86
|
+
options.signal?.throwIfAborted();
|
|
87
|
+
if (await integrationConnected(options.workspaceId, integration.provider, options)) {
|
|
88
|
+
outcome.ready.push(integration.provider);
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
if (options.prompt === undefined) throw notConnected(integration, options);
|
|
92
|
+
const label = providerLabel(integration.provider);
|
|
93
|
+
options.prompt.info(`This flow needs ${label} (${integration.detail}), which is not connected to this workspace.`);
|
|
94
|
+
if (!await options.prompt.confirm(`Connect ${label} now? (opens browser) [Y/n] `)) {
|
|
95
|
+
throw notConnected(integration, options);
|
|
96
|
+
}
|
|
97
|
+
await connectIntegration(integration.provider, options, options.prompt);
|
|
98
|
+
outcome.connected.push(integration.provider);
|
|
99
|
+
}
|
|
100
|
+
return outcome;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async function connectIntegration(provider: string, options: EnsureConnectionsOptions, prompt: ConnectPrompt): Promise<void> {
|
|
104
|
+
const base = `/api/v1/workspaces/${encodeURIComponent(options.workspaceId)}/integrations`;
|
|
105
|
+
const session = await cloudFetch(`${base}/connect-session`, options, {
|
|
106
|
+
method: 'POST', detail: true,
|
|
107
|
+
body: JSON.stringify({ allowedIntegrations: [provider], scope: { kind: 'workspace' } }),
|
|
108
|
+
});
|
|
109
|
+
const url = isCloudRecord(session)
|
|
110
|
+
? [session.connectLink, session.sessionUrl, session.url].find((value): value is string => typeof value === 'string' && value.length > 0)
|
|
111
|
+
: undefined;
|
|
112
|
+
if (url === undefined || !/^https:\/\//u.test(url)) {
|
|
113
|
+
throw new CloudFlowError('invalid_response', 'Cloud did not return a connect link.');
|
|
114
|
+
}
|
|
115
|
+
prompt.info(`Opening ${url}`);
|
|
116
|
+
prompt.info(`If the browser did not open, visit that link, then return here; waiting for ${providerLabel(provider)} to connect…`);
|
|
117
|
+
try {
|
|
118
|
+
await (prompt.openUrl ?? openInBrowser)(url);
|
|
119
|
+
} catch {
|
|
120
|
+
// The link was printed; the poll below is what decides.
|
|
121
|
+
}
|
|
122
|
+
const interval = options.pollIntervalMs ?? 2_000;
|
|
123
|
+
const deadline = Date.now() + (options.connectTimeoutMs ?? 300_000);
|
|
124
|
+
const sleep = options.sleep ?? defaultSleep;
|
|
125
|
+
while (Date.now() < deadline) {
|
|
126
|
+
await sleep(interval, options.signal);
|
|
127
|
+
options.signal?.throwIfAborted();
|
|
128
|
+
if (await integrationConnected(options.workspaceId, provider, options)) {
|
|
129
|
+
prompt.info(`${providerLabel(provider)} connected.`);
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
throw new CloudFlowError('integration_not_connected',
|
|
134
|
+
`${providerLabel(provider)} was not connected before the wait ran out. Finish at ${url}, then run the command again.`);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function notConnected(integration: FlowIntegrationRequirement, options: EnsureConnectionsOptions): CloudFlowError {
|
|
138
|
+
const label = providerLabel(integration.provider);
|
|
139
|
+
return new CloudFlowError('integration_not_connected',
|
|
140
|
+
`${label} is not connected to this workspace, and this flow needs it (${integration.detail}). `
|
|
141
|
+
+ `Connect it from the dashboard, or run again without --no-connect in a terminal to connect it now `
|
|
142
|
+
+ `(a browser opens a ${label} connect page for workspace ${options.workspaceId}).`);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function isReady(status: unknown): boolean {
|
|
146
|
+
if (!isCloudRecord(status)) return false;
|
|
147
|
+
return status.ready === true || status.state === 'ready' || status.status === 'ready' || status.status === 'connected';
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function assertIds(workspaceId: string, provider: string): void {
|
|
151
|
+
if (!WORKSPACE_ID.test(workspaceId)) throw new CloudFlowError('invalid_response', 'Cloud reported an unusable workspace id.');
|
|
152
|
+
if (!PROVIDER_ID.test(provider)) throw new CloudFlowError('invalid_input', `"${provider}" is not an integration provider id.`);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** `google-mail` → `Google Mail`, `github` → `GitHub`; presentation only. */
|
|
156
|
+
export function providerLabel(provider: string): string {
|
|
157
|
+
const known: Record<string, string> = { github: 'GitHub', gitlab: 'GitLab', hubspot: 'HubSpot', clickup: 'ClickUp' };
|
|
158
|
+
return known[provider] ?? provider.split('-').map(part => part.charAt(0).toUpperCase() + part.slice(1)).join(' ');
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function defaultSleep(ms: number, signal?: AbortSignal): Promise<void> {
|
|
162
|
+
return new Promise((resolve, reject) => {
|
|
163
|
+
if (signal?.aborted) { reject(signal.reason); return; }
|
|
164
|
+
const timer = setTimeout(() => { signal?.removeEventListener('abort', onAbort); resolve(); }, ms);
|
|
165
|
+
function onAbort(): void { clearTimeout(timer); reject(signal!.reason); }
|
|
166
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Detached so a browser that outlives the CLI never holds its stdio; every
|
|
172
|
+
* failure is ignored. `FLOWS_NO_BROWSER=1` (an SSH session, CI) leaves the
|
|
173
|
+
* printed link as the only opener.
|
|
174
|
+
*/
|
|
175
|
+
function openInBrowser(url: string): void {
|
|
176
|
+
if (process.env['FLOWS_NO_BROWSER'] === '1') return;
|
|
177
|
+
const [command, args] = process.platform === 'darwin' ? ['open', [url]]
|
|
178
|
+
: process.platform === 'win32' ? ['cmd', ['/c', 'start', '', url]]
|
|
179
|
+
: ['xdg-open', [url]];
|
|
180
|
+
try {
|
|
181
|
+
const child = spawn(command, args, { detached: true, stdio: 'ignore' });
|
|
182
|
+
child.on('error', () => {});
|
|
183
|
+
child.unref();
|
|
184
|
+
} catch {
|
|
185
|
+
// Printed URL is the fallback.
|
|
186
|
+
}
|
|
187
|
+
}
|
package/src/cloud-deploy.ts
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
import { randomBytes, createHash } from 'node:crypto';
|
|
2
2
|
import { readFile } from 'node:fs/promises';
|
|
3
|
+
import { dirname, resolve } from 'node:path';
|
|
3
4
|
import { loadAuthoredFlow } from './authored-flow-loader.js';
|
|
5
|
+
import { ensureIntegrationsConnected, type ConnectPrompt } from './cloud-connect.js';
|
|
4
6
|
import {
|
|
5
7
|
CloudFlowError, cloudFetch, cloudRequest, isCloudRecord, type CloudConnectionOptions,
|
|
6
8
|
} from './cloud-http.js';
|
|
9
|
+
import { flowRequirements, type FlowRequirements } from './flow-requirements.js';
|
|
10
|
+
import { readProjectConfig } from './cli/check.js';
|
|
7
11
|
|
|
8
12
|
/**
|
|
9
13
|
* Hosted listener deployment: the CLI form of the agentrelay.com onboarding's
|
|
@@ -21,7 +25,9 @@ export type FlowTriggerProvider = (typeof FLOW_TRIGGER_PROVIDERS)[number];
|
|
|
21
25
|
|
|
22
26
|
/** Settings Cloud's launcher prefilter reads per provider (`flow-trigger-sources.ts`). */
|
|
23
27
|
const PROVIDER_SETTINGS: Record<FlowTriggerProvider, readonly string[]> = {
|
|
24
|
-
|
|
28
|
+
// `events`: `issues` (default) or `pull_request` — which GitHub records
|
|
29
|
+
// wake the listener (AgentWorkforce/cloud#3772).
|
|
30
|
+
github: ['repository', 'labels', 'contains', 'events'],
|
|
25
31
|
slack: ['channel', 'contains'],
|
|
26
32
|
linear: ['team', 'contains'],
|
|
27
33
|
jira: ['project', 'contains'],
|
|
@@ -45,10 +51,22 @@ export interface DeployToCloudInput {
|
|
|
45
51
|
approver: string;
|
|
46
52
|
/** Defaults to the flow's declared name. */
|
|
47
53
|
name?: string;
|
|
48
|
-
/**
|
|
54
|
+
/**
|
|
55
|
+
* Coding-agent harnesses the flow uses; Cloud checks their credentials are
|
|
56
|
+
* connected. Defaults to the `cli:` declarations the source carries
|
|
57
|
+
* (`flowRequirements`), else `["claude"]`.
|
|
58
|
+
*/
|
|
49
59
|
agents?: FlowAgentHarness[];
|
|
50
60
|
/** Save without activating: no connection checks, no listener until activated. */
|
|
51
61
|
draft?: boolean;
|
|
62
|
+
/**
|
|
63
|
+
* How a missing integration is connected before activation. Omitted, a
|
|
64
|
+
* missing one refuses with `integration_not_connected` (the `--no-connect`
|
|
65
|
+
* and non-interactive behaviour); given, each is offered a browser connect.
|
|
66
|
+
*/
|
|
67
|
+
connect?: ConnectPrompt;
|
|
68
|
+
/** Skip the pre-submission integration check entirely (Cloud still checks on activation). */
|
|
69
|
+
checkConnections?: boolean;
|
|
52
70
|
}
|
|
53
71
|
|
|
54
72
|
export const FLOW_AGENT_HARNESSES = ['claude', 'codex'] as const;
|
|
@@ -70,6 +88,10 @@ export interface CloudDeployment {
|
|
|
70
88
|
repository: { owner: string; name: string };
|
|
71
89
|
sources: FlowTriggerSource[];
|
|
72
90
|
sourceSha256: string;
|
|
91
|
+
/** What the source declared it needs; the harnesses became `inputs.agents` unless `agents` was given. */
|
|
92
|
+
requirements: FlowRequirements;
|
|
93
|
+
/** Integrations connected through the prompt during this deploy. */
|
|
94
|
+
connected: string[];
|
|
73
95
|
}
|
|
74
96
|
|
|
75
97
|
export function parseRepository(value: string): { owner: string; name: string } {
|
|
@@ -102,6 +124,15 @@ export function parseTriggerSource(value: string): FlowTriggerSource {
|
|
|
102
124
|
if (!setting || setting.length > MAX_SETTING_LENGTH || key in settings) {
|
|
103
125
|
throw new CloudFlowError('invalid_input', `Trigger setting "${key}" must be given once with a non-empty value.`);
|
|
104
126
|
}
|
|
127
|
+
if (key === 'events') {
|
|
128
|
+
// Cloud's enum is lowercase; send it that way whatever the shell typed.
|
|
129
|
+
const events = setting.toLowerCase();
|
|
130
|
+
if (!['issues', 'pull_request'].includes(events)) {
|
|
131
|
+
throw new CloudFlowError('invalid_input', `github events must be "issues" or "pull_request", got "${setting}".`);
|
|
132
|
+
}
|
|
133
|
+
settings[key] = events;
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
105
136
|
settings[key] = setting;
|
|
106
137
|
}
|
|
107
138
|
}
|
|
@@ -143,6 +174,12 @@ export async function deployToCloud(
|
|
|
143
174
|
throw new CloudFlowError('unsupported_source',
|
|
144
175
|
'Cloud deploys one self-contained .flow.ts source without use dependencies.');
|
|
145
176
|
}
|
|
177
|
+
let projectCli: string | undefined;
|
|
178
|
+
try {
|
|
179
|
+
projectCli = readProjectConfig(dirname(resolve(input.path))).cli;
|
|
180
|
+
} catch {
|
|
181
|
+
projectCli = undefined;
|
|
182
|
+
}
|
|
146
183
|
if (input.sources.length === 0 || input.sources.length > 10) {
|
|
147
184
|
throw new CloudFlowError('invalid_input', 'Give between one and ten --on trigger sources.');
|
|
148
185
|
}
|
|
@@ -165,7 +202,30 @@ export async function deployToCloud(
|
|
|
165
202
|
if (workspace === undefined || typeof workspace.id !== 'string' || !workspace.id) {
|
|
166
203
|
throw new CloudFlowError('invalid_response', 'Cloud did not report a current workspace for this credential.');
|
|
167
204
|
}
|
|
168
|
-
|
|
205
|
+
// Every launched run lands in the deployment's repository, so GitHub is
|
|
206
|
+
// required even when no GitHub source wakes it.
|
|
207
|
+
const requirements = flowRequirements(definition, {
|
|
208
|
+
sources, repository: input.repository, ...(projectCli === undefined ? {} : { projectCli }),
|
|
209
|
+
});
|
|
210
|
+
// The declared harnesses become `inputs.agents`; one Cloud cannot run is
|
|
211
|
+
// refused here rather than silently replaced by Claude, which activation
|
|
212
|
+
// would then check while the deployed runs still call the declared CLI.
|
|
213
|
+
const unsupported = requirements.harnesses.filter(harness => !(FLOW_AGENT_HARNESSES as readonly string[]).includes(harness));
|
|
214
|
+
if (input.agents === undefined && unsupported.length > 0) {
|
|
215
|
+
const uses = requirements.harnessUses.filter(use => unsupported.includes(use.harness));
|
|
216
|
+
throw new CloudFlowError('unsupported_source',
|
|
217
|
+
`This flow declares ${uses.map(use => `${use.harness} (${use.detail})`).join(', ')}, which Cloud deployments cannot run yet; `
|
|
218
|
+
+ `Cloud runs ${FLOW_AGENT_HARNESSES.join(' and ')}. Change the declaration, or pass --agents to deploy it anyway.`);
|
|
219
|
+
}
|
|
220
|
+
const declaredAgents = requirements.harnesses.filter((harness): harness is FlowAgentHarness => !unsupported.includes(harness));
|
|
221
|
+
const agents = input.agents ?? (declaredAgents.length > 0 ? declaredAgents : ['claude']);
|
|
222
|
+
// A draft activates nothing, so Cloud checks nothing; match it here.
|
|
223
|
+
const connected = input.draft || input.checkConnections === false
|
|
224
|
+
? []
|
|
225
|
+
: (await ensureIntegrationsConnected(requirements, {
|
|
226
|
+
...options, workspaceId: workspace.id, ...(input.connect === undefined ? {} : { prompt: input.connect }),
|
|
227
|
+
})).connected;
|
|
228
|
+
options.signal?.throwIfAborted();
|
|
169
229
|
const result = await cloudFetch('/api/v1/flows/deploy', options, { method: 'POST', detail: true, body: JSON.stringify({
|
|
170
230
|
workspaceId: workspace.id,
|
|
171
231
|
mode: input.draft ? 'draft' : 'activate',
|
|
@@ -177,6 +237,11 @@ export async function deployToCloud(
|
|
|
177
237
|
inputs: { approver, agents },
|
|
178
238
|
repository: input.repository,
|
|
179
239
|
sources,
|
|
240
|
+
requirements: {
|
|
241
|
+
integrations: requirements.integrations.map(i => i.provider),
|
|
242
|
+
harnesses: requirements.harnesses,
|
|
243
|
+
mcp: requirements.mcp,
|
|
244
|
+
},
|
|
180
245
|
}) });
|
|
181
246
|
if (!isCloudRecord(result) || typeof result.agentId !== 'string' || typeof result.status !== 'string') {
|
|
182
247
|
throw new CloudFlowError('invalid_response', 'Cloud did not return a deployment.');
|
|
@@ -185,6 +250,7 @@ export async function deployToCloud(
|
|
|
185
250
|
agentId: result.agentId, name, status: result.status,
|
|
186
251
|
repository: input.repository, sources,
|
|
187
252
|
sourceSha256: createHash('sha256').update(bytes).digest('hex'),
|
|
253
|
+
requirements, connected,
|
|
188
254
|
};
|
|
189
255
|
}
|
|
190
256
|
|
package/src/cloud-http.ts
CHANGED
|
@@ -12,13 +12,23 @@ export interface CloudConnectionOptions {
|
|
|
12
12
|
requestTimeoutMs?: number;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
+
/** A refusal Cloud answered as JSON: only these fields are ever read. */
|
|
16
|
+
export interface CloudRefusal {
|
|
17
|
+
code: string;
|
|
18
|
+
error: string;
|
|
19
|
+
/** Version-only descriptions, when a route names what it wanted vs. got. */
|
|
20
|
+
expected?: { packageName?: string; version?: string };
|
|
21
|
+
received?: { packageName?: string; version?: string };
|
|
22
|
+
}
|
|
23
|
+
|
|
15
24
|
export class CloudFlowError extends Error {
|
|
16
25
|
constructor(
|
|
17
26
|
readonly code: 'configuration' | 'unsupported_source' | 'invalid_input' | 'invalid_response' | 'http_error'
|
|
18
27
|
| 'transport_error' | 'transient_error' | 'unsupported_storage_backend' | 'sync_too_large' | 'sync_unsupported'
|
|
19
|
-
| 'patch_conflict',
|
|
28
|
+
| 'patch_conflict' | 'integration_not_connected',
|
|
20
29
|
message: string,
|
|
21
30
|
readonly status?: number,
|
|
31
|
+
readonly refusal?: CloudRefusal,
|
|
22
32
|
) {
|
|
23
33
|
super(message);
|
|
24
34
|
this.name = 'CloudFlowError';
|
|
@@ -175,10 +185,20 @@ export async function cloudFetch(
|
|
|
175
185
|
}
|
|
176
186
|
if (!response.ok) {
|
|
177
187
|
// Do not echo server response bodies: they may contain credentials or source.
|
|
178
|
-
|
|
188
|
+
// Reading the body is itself a transport step: a cancellation or timeout
|
|
189
|
+
// there keeps its own classification instead of becoming an http_error.
|
|
190
|
+
let refusal: CloudRefusal | undefined;
|
|
191
|
+
if (init.detail) {
|
|
192
|
+
try {
|
|
193
|
+
refusal = await structuredRefusal(response);
|
|
194
|
+
} catch (error) {
|
|
195
|
+
options.signal?.throwIfAborted();
|
|
196
|
+
throw transportError(deadline.aborted ? deadline.reason : error);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
179
199
|
throw new CloudFlowError('http_error', refusal === undefined
|
|
180
200
|
? `Cloud request failed with HTTP ${response.status}.`
|
|
181
|
-
: `Cloud refused (${refusal.code}): ${refusal.error}`, response.status);
|
|
201
|
+
: `Cloud refused (${refusal.code}): ${refusal.error}`, response.status, refusal);
|
|
182
202
|
}
|
|
183
203
|
try {
|
|
184
204
|
return await response.json();
|
|
@@ -189,12 +209,36 @@ export async function cloudFetch(
|
|
|
189
209
|
}
|
|
190
210
|
}
|
|
191
211
|
|
|
192
|
-
|
|
212
|
+
const REFUSAL_CODE = /^[a-z0-9_]{1,64}$/u;
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Routes answer either `{ code, error }` or a bare `{ error: "<code>" }`;
|
|
216
|
+
* both are read, and an optional `expected`/`received` pair is reduced to
|
|
217
|
+
* package name and version. Nothing else in the body is looked at.
|
|
218
|
+
*/
|
|
219
|
+
async function structuredRefusal(response: Response): Promise<CloudRefusal | undefined> {
|
|
193
220
|
let body: unknown;
|
|
194
|
-
try {
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
221
|
+
try {
|
|
222
|
+
body = await response.json();
|
|
223
|
+
} catch (error) {
|
|
224
|
+
// Only a body that is not JSON is "no structured refusal"; an aborted or
|
|
225
|
+
// timed-out read is a transport failure and propagates.
|
|
226
|
+
if (error instanceof SyntaxError) return undefined;
|
|
227
|
+
throw error;
|
|
228
|
+
}
|
|
229
|
+
if (!isCloudRecord(body) || typeof body.error !== 'string' || body.error.length > 500) return undefined;
|
|
230
|
+
const code = typeof body.code === 'string' ? body.code : body.error;
|
|
231
|
+
if (!REFUSAL_CODE.test(code)) return undefined;
|
|
232
|
+
const versionOnly = (value: unknown): CloudRefusal['expected'] => {
|
|
233
|
+
if (!isCloudRecord(value)) return undefined;
|
|
234
|
+
const out: { packageName?: string; version?: string } = {};
|
|
235
|
+
if (typeof value.packageName === 'string' && value.packageName.length <= 100) out.packageName = value.packageName;
|
|
236
|
+
if (typeof value.version === 'string' && /^[0-9A-Za-z.+-]{1,64}$/u.test(value.version)) out.version = value.version;
|
|
237
|
+
return out;
|
|
238
|
+
};
|
|
239
|
+
const expected = versionOnly(body.expected);
|
|
240
|
+
const received = versionOnly(body.received);
|
|
241
|
+
return { code, error: body.error, ...(expected ? { expected } : {}), ...(received ? { received } : {}) };
|
|
198
242
|
}
|
|
199
243
|
|
|
200
244
|
// Defensive path-segment constraint; accepting a new server ID format needs an SDK change.
|
package/src/cloud-run.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { RUN_COMPLETION_REASONS } from '@relayflows/surface';
|
|
1
|
+
import { RUN_COMPLETION_REASONS, type ScheduleTriggerSource } from '@relayflows/surface';
|
|
2
2
|
import type { RunCompletionReason } from './protocol.js';
|
|
3
3
|
import { readFile } from 'node:fs/promises';
|
|
4
4
|
import { createHash } from 'node:crypto';
|
|
@@ -10,7 +10,7 @@ import type { FlowSpec } from './spec.js';
|
|
|
10
10
|
import { snapshotJsonValue, type JsonValue } from './json-value.js';
|
|
11
11
|
import { loadAuthoredFlow, type SurfaceModuleAuthority } from './authored-flow-loader.js';
|
|
12
12
|
import {
|
|
13
|
-
CloudFlowError, cloudConnection, cloudRequest, cloudRunId, isCloudRecord,
|
|
13
|
+
CloudFlowError, cloudConnection, cloudFetch, cloudRequest, cloudRunId, isCloudRecord,
|
|
14
14
|
type CloudConnectionOptions,
|
|
15
15
|
} from './cloud-http.js';
|
|
16
16
|
import { packWorkingTree, prepareCloudSync, uploadCloudCode } from './cloud-sync.js';
|
|
@@ -53,6 +53,17 @@ export interface CloudAuthoredAuthority {
|
|
|
53
53
|
readonly byteLength: number;
|
|
54
54
|
readonly surface: SurfaceModuleAuthority;
|
|
55
55
|
}
|
|
56
|
+
/** The request-body fields a submission contributes; identical for a run and a schedule. */
|
|
57
|
+
export function cloudSubmissionBody(submission: CloudSubmission): Record<string, unknown> {
|
|
58
|
+
return {
|
|
59
|
+
workflow: submission.workflow,
|
|
60
|
+
fileType: submission.fileType,
|
|
61
|
+
relayflowVersion: 'v2',
|
|
62
|
+
...(submission.authoredAuthority === undefined ? {} : { authoredAuthority: submission.authoredAuthority }),
|
|
63
|
+
...(submission.inputPresent ? { inputs: submission.inputs } : {}),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
56
67
|
export type CloudRunState =
|
|
57
68
|
| { runId: string; status: 'pending' | 'launching' | 'running' }
|
|
58
69
|
| { runId: string; status: 'completed' | 'failed' | 'cancelled'; completionReason: RunCompletionReason };
|
|
@@ -62,13 +73,31 @@ export type CloudRunState =
|
|
|
62
73
|
* happens here; Cloud owns provisioning and the Rust engine owns execution.
|
|
63
74
|
* Returns acceptance, not completion. No local daemon, CLI probe, or node up.
|
|
64
75
|
*/
|
|
65
|
-
|
|
76
|
+
/**
|
|
77
|
+
* What a Cloud submission carries, before any request: the exact source or
|
|
78
|
+
* canonical spec, the pinned authority for authored source, the input, and a
|
|
79
|
+
* local correlation hash. Shared by `runInCloud` and `scheduleInCloud`, so a
|
|
80
|
+
* schedule stores exactly what a run would send.
|
|
81
|
+
*/
|
|
82
|
+
export interface CloudSubmission {
|
|
83
|
+
readonly workflow: string;
|
|
84
|
+
readonly fileType: 'yaml' | 'ts';
|
|
85
|
+
readonly authoredAuthority?: CloudAuthoredAuthority;
|
|
86
|
+
readonly inputs?: JsonValue;
|
|
87
|
+
readonly inputPresent: boolean;
|
|
88
|
+
readonly specHash: string;
|
|
89
|
+
/** The authored flow's declared name, or the spec name. */
|
|
90
|
+
readonly name: string;
|
|
91
|
+
/** Authored `schedule.*` handlers, for `flows schedule` to pick up. */
|
|
92
|
+
readonly schedules: readonly ScheduleTriggerSource[];
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export async function prepareCloudSubmission(
|
|
66
96
|
flow: CloudFlowSource,
|
|
67
|
-
options:
|
|
68
|
-
): Promise<
|
|
69
|
-
const { baseUrl } = cloudConnection(options);
|
|
97
|
+
options: { input?: JsonValue; signal?: AbortSignal } = {},
|
|
98
|
+
): Promise<CloudSubmission> {
|
|
70
99
|
let spec: FlowSpec | undefined;
|
|
71
|
-
let authored: { source: string; authority: CloudAuthoredAuthority } | undefined;
|
|
100
|
+
let authored: { source: string; authority: CloudAuthoredAuthority; name: string; schedules: ScheduleTriggerSource[] } | undefined;
|
|
72
101
|
const inputPresent = Object.prototype.hasOwnProperty.call(options, 'input');
|
|
73
102
|
let authoredInput: JsonValue | undefined;
|
|
74
103
|
try {
|
|
@@ -80,14 +109,27 @@ export async function runInCloud(
|
|
|
80
109
|
if (!bytes.length || Buffer.from(source, 'utf8').compare(bytes) !== 0) {
|
|
81
110
|
throw new CloudFlowError('invalid_input', 'Authored source must be nonempty, lossless UTF-8.');
|
|
82
111
|
}
|
|
83
|
-
|
|
84
|
-
loaded.getDefinition
|
|
112
|
+
let loaded: Awaited<ReturnType<typeof loadAuthoredFlow>>;
|
|
113
|
+
let definition: ReturnType<typeof loaded.getDefinition>;
|
|
114
|
+
try {
|
|
115
|
+
loaded = await loadAuthoredFlow(flow.path);
|
|
116
|
+
definition = loaded.getDefinition(loaded.handle);
|
|
117
|
+
} catch (error) {
|
|
118
|
+
// An authored source that does not load is an authoring problem, and
|
|
119
|
+
// the most common one is `@relayflows/surface` not being resolvable
|
|
120
|
+
// from the flow's directory. Name it; do not call it a spec problem.
|
|
121
|
+
throw new CloudFlowError('unsupported_source',
|
|
122
|
+
`${flow.path} is not a loadable authored flow: ${error instanceof Error ? error.message : String(error)}. `
|
|
123
|
+
+ 'Run `flows check` on it from the same directory.');
|
|
124
|
+
}
|
|
85
125
|
if (loaded.graph.length !== 1) {
|
|
86
126
|
throw new CloudFlowError('unsupported_source',
|
|
87
127
|
'Cloud authored submission currently accepts one self-contained .flow.ts source without use dependencies.');
|
|
88
128
|
}
|
|
89
129
|
authored = {
|
|
90
130
|
source,
|
|
131
|
+
name: definition.name,
|
|
132
|
+
schedules: definition.handlers.flatMap(h => h.trigger.kind === 'schedule' ? [h.trigger] : []),
|
|
91
133
|
authority: Object.freeze({
|
|
92
134
|
schemaVersion: 1,
|
|
93
135
|
sourceSha256: createHash('sha256').update(bytes).digest('hex'),
|
|
@@ -130,12 +172,32 @@ export async function runInCloud(
|
|
|
130
172
|
throw new CloudFlowError('invalid_input', 'Cannot read or compile the declarative flow. Check the file path and YAML/JSON spec.');
|
|
131
173
|
}
|
|
132
174
|
options.signal?.throwIfAborted();
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
}
|
|
175
|
+
if (authored === undefined) {
|
|
176
|
+
const kernel = toKernelSpec(spec!);
|
|
177
|
+
// JSON is a YAML subset. Sending canonical data preserves the exact spec
|
|
178
|
+
// while using the server's existing YAML-to-config admission path.
|
|
179
|
+
return { workflow: canonicalize(spec), fileType: 'yaml', inputPresent: false, specHash: specHash(kernel),
|
|
180
|
+
name: spec!.name ?? "flow", schedules: [] };
|
|
181
|
+
}
|
|
182
|
+
return {
|
|
183
|
+
workflow: authored.source, fileType: 'ts', authoredAuthority: authored.authority,
|
|
184
|
+
inputs: authoredInput, inputPresent: true, name: authored.name, schedules: authored.schedules,
|
|
185
|
+
specHash: createHash('sha256').update(canonicalize({ authority: authored.authority, input: authoredInput })).digest('hex'),
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Submit a declarative flow to Cloud's pinned v2 runtime. Compilation only
|
|
191
|
+
* happens here; Cloud owns provisioning and the Rust engine owns execution.
|
|
192
|
+
* Returns acceptance, not completion. No local daemon, CLI probe, or node up.
|
|
193
|
+
*/
|
|
194
|
+
export async function runInCloud(
|
|
195
|
+
flow: CloudFlowSource,
|
|
196
|
+
options: RunInCloudOptions = {},
|
|
197
|
+
): Promise<CloudRunReceipt> {
|
|
198
|
+
const { baseUrl } = cloudConnection(options);
|
|
199
|
+
const submission = await prepareCloudSubmission(flow, options);
|
|
200
|
+
const hash = submission.specHash;
|
|
139
201
|
// Sync before submission: `prepare` reserves the run ID and the upload lands
|
|
140
202
|
// under it, so the run request below names code Cloud already holds. A
|
|
141
203
|
// refused backend or failed upload therefore never leaves a launched run
|
|
@@ -156,17 +218,28 @@ export async function runInCloud(
|
|
|
156
218
|
}
|
|
157
219
|
options.signal?.throwIfAborted();
|
|
158
220
|
options.onSubmit?.();
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
221
|
+
let result: unknown;
|
|
222
|
+
try {
|
|
223
|
+
result = await cloudFetch('/api/v1/workflows/run', options, { method: 'POST', detail: true, body: JSON.stringify({
|
|
224
|
+
...cloudSubmissionBody(submission),
|
|
225
|
+
...(options.workspaceId === undefined ? {} : { workspaceId: options.workspaceId }),
|
|
226
|
+
...(synced === undefined ? {} : { runId: synced.runId, s3CodeKey: synced.codeKey }),
|
|
227
|
+
}) });
|
|
228
|
+
} catch (error) {
|
|
229
|
+
// Cloud accepts exactly one authored Surface (the one its sandbox runs).
|
|
230
|
+
// A CLI on another release is refused with this code; say which side is
|
|
231
|
+
// which instead of leaving the user with a bare 400 (flows#461).
|
|
232
|
+
if (error instanceof CloudFlowError && submission.authoredAuthority !== undefined
|
|
233
|
+
&& error.refusal?.code === 'relayflow_v2_authored_authority_invalid') {
|
|
234
|
+
const expected = error.refusal.expected?.version;
|
|
235
|
+
throw new CloudFlowError('unsupported_source',
|
|
236
|
+
`Cloud refused this authored flow's Surface: it runs @relayflows/surface ${expected ?? '(version not reported)'} `
|
|
237
|
+
+ `and this CLI authored against ${submission.authoredAuthority.surface.version}. `
|
|
238
|
+
+ 'Install the matching relayflows release, or deploy the flow with `flows deploy`, which uses Cloud\'s own Surface.',
|
|
239
|
+
error.status, error.refusal);
|
|
240
|
+
}
|
|
241
|
+
throw error;
|
|
242
|
+
}
|
|
170
243
|
if (!isCloudRecord(result) || (result.status !== 'pending' && result.status !== 'running')) {
|
|
171
244
|
throw new CloudFlowError('invalid_response', 'Cloud did not return an accepted run.');
|
|
172
245
|
}
|