@relayflows/sdk 2.0.15 → 2.0.17
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-artifacts.d.ts +20 -0
- package/dist/agent-artifacts.d.ts.map +1 -0
- package/dist/agent-artifacts.js +81 -0
- package/dist/agent-artifacts.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 +3 -3
- package/dist/authored-flow-executor.d.ts.map +1 -1
- package/dist/authored-flow-executor.js +93 -7
- 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-node-runner.d.ts.map +1 -1
- package/dist/authored-node-runner.js +37 -14
- package/dist/authored-node-runner.js.map +1 -1
- package/dist/authored-step-output.d.ts +2 -0
- package/dist/authored-step-output.d.ts.map +1 -1
- package/dist/authored-step-output.js +5 -1
- package/dist/authored-step-output.js.map +1 -1
- package/dist/authored-worker-step.d.ts.map +1 -1
- package/dist/authored-worker-step.js +15 -8
- package/dist/authored-worker-step.js.map +1 -1
- package/dist/cli/check-triggers.d.ts.map +1 -1
- package/dist/cli/check-triggers.js +10 -0
- package/dist/cli/check-triggers.js.map +1 -1
- package/dist/cli/check.d.ts +14 -0
- package/dist/cli/check.d.ts.map +1 -1
- package/dist/cli/check.js +6 -1
- package/dist/cli/check.js.map +1 -1
- package/dist/cli/cloud-deploy.d.ts +30 -0
- package/dist/cli/cloud-deploy.d.ts.map +1 -0
- package/dist/cli/cloud-deploy.js +162 -0
- package/dist/cli/cloud-deploy.js.map +1 -0
- package/dist/cli/cloud-run.d.ts +3 -1
- package/dist/cli/cloud-run.d.ts.map +1 -1
- package/dist/cli/cloud-run.js +37 -5
- package/dist/cli/cloud-run.js.map +1 -1
- package/dist/cli/cloud-schedule.d.ts +29 -0
- package/dist/cli/cloud-schedule.d.ts.map +1 -0
- package/dist/cli/cloud-schedule.js +131 -0
- package/dist/cli/cloud-schedule.js.map +1 -0
- package/dist/cli/cloud-sync.d.ts +13 -0
- package/dist/cli/cloud-sync.d.ts.map +1 -0
- package/dist/cli/cloud-sync.js +39 -0
- package/dist/cli/cloud-sync.js.map +1 -0
- package/dist/cli/direct-run.js +1 -1
- package/dist/cli/direct-run.js.map +1 -1
- package/dist/cli/run.d.ts +1 -1
- package/dist/cli/run.d.ts.map +1 -1
- package/dist/cli/run.js +17 -4
- package/dist/cli/run.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +117 -12
- package/dist/cli.js.map +1 -1
- package/dist/cloud-deploy.d.ts +70 -0
- package/dist/cloud-deploy.d.ts.map +1 -0
- package/dist/cloud-deploy.js +192 -0
- package/dist/cloud-deploy.js.map +1 -0
- package/dist/cloud-http.d.ts +52 -2
- package/dist/cloud-http.d.ts.map +1 -1
- package/dist/cloud-http.js +135 -9
- package/dist/cloud-http.js.map +1 -1
- package/dist/cloud-run.d.ts +53 -0
- package/dist/cloud-run.d.ts.map +1 -1
- package/dist/cloud-run.js +97 -25
- package/dist/cloud-run.js.map +1 -1
- package/dist/cloud-schedule.d.ts +48 -0
- package/dist/cloud-schedule.d.ts.map +1 -0
- package/dist/cloud-schedule.js +125 -0
- package/dist/cloud-schedule.js.map +1 -0
- package/dist/cloud-sync.d.ts +66 -0
- package/dist/cloud-sync.d.ts.map +1 -0
- package/dist/cloud-sync.js +287 -0
- package/dist/cloud-sync.js.map +1 -0
- package/dist/failure-kinds.d.ts +10 -3
- package/dist/failure-kinds.d.ts.map +1 -1
- package/dist/failure-kinds.js +10 -0
- package/dist/failure-kinds.js.map +1 -1
- 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/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/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/agent-artifacts.ts +76 -0
- package/src/authored-flow-error.ts +1 -0
- package/src/authored-flow-executor.ts +95 -8
- package/src/authored-flow-lifecycle.ts +7 -0
- package/src/authored-flow-operation.ts +35 -10
- package/src/authored-node-runner.ts +35 -13
- package/src/authored-step-output.ts +6 -1
- package/src/authored-worker-step.ts +16 -9
- package/src/cli/check-triggers.ts +9 -0
- package/src/cli/check.ts +21 -1
- package/src/cli/cloud-deploy.ts +157 -0
- package/src/cli/cloud-run.ts +37 -6
- package/src/cli/cloud-schedule.ts +135 -0
- package/src/cli/cloud-sync.ts +37 -0
- package/src/cli/direct-run.ts +1 -1
- package/src/cli/run.ts +18 -5
- package/src/cli.ts +107 -12
- package/src/cloud-deploy.ts +247 -0
- package/src/cloud-http.ts +173 -9
- package/src/cloud-run.ts +143 -24
- package/src/cloud-schedule.ts +158 -0
- package/src/cloud-sync.ts +315 -0
- package/src/failure-kinds.ts +10 -0
- package/src/index.ts +14 -0
- package/src/named-gate-lowering.ts +9 -0
- package/src/named-gates.ts +11 -1
- package/src/preflight.ts +132 -9
- 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
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
|
|
9
9
|
/** Slice-P kinds the surface `.gate(config)` accepts and the SDK lowers. */
|
|
10
10
|
const NAMED_GATE_KINDS = new Set([
|
|
11
|
-
'references_input', 'subprocess_gate', 'word_count_bounds', 'regex_match',
|
|
11
|
+
'references_input', 'subprocess_gate', 'word_count_bounds', 'regex_match', 'artifact_exists',
|
|
12
12
|
]);
|
|
13
13
|
|
|
14
14
|
function isNamedGateConfig(candidate: unknown): candidate is NamedGate {
|
|
@@ -31,6 +31,14 @@ export class AuthoredFlowOperation<T> {
|
|
|
31
31
|
* still throws `unsupported_gate` and never sets this field.
|
|
32
32
|
*/
|
|
33
33
|
namedGate: NamedGate | undefined = undefined;
|
|
34
|
+
/**
|
|
35
|
+
* Predicate gate attached via `.gate(fn, because?)`. Author code: it runs
|
|
36
|
+
* in this process after the step completes, and its verdict is journaled as
|
|
37
|
+
* a lowered `<id>.gate` deterministic step (docs/SURFACE.md §6), so replay
|
|
38
|
+
* and resume see the recorded verdict and never re-run the closure. The
|
|
39
|
+
* function itself is never serialized. `flows check` cannot prove it.
|
|
40
|
+
*/
|
|
41
|
+
predicateGate: { predicate: (value: T) => boolean; because?: string } | undefined = undefined;
|
|
34
42
|
private state: OperationState = 'created';
|
|
35
43
|
private thenInvoked = false;
|
|
36
44
|
private rootFailureRecorded = false;
|
|
@@ -61,20 +69,29 @@ export class AuthoredFlowOperation<T> {
|
|
|
61
69
|
const operation = this;
|
|
62
70
|
const step: Step<T> = {
|
|
63
71
|
gate(configOrPredicate: NamedGate | ((value: T) => boolean), _because?: string): Step<T> {
|
|
72
|
+
if (operation.namedGate !== undefined || operation.predicateGate !== undefined) {
|
|
73
|
+
throw new AuthoredFlowExecutionError('unsupported_gate', 'a step takes one .gate().');
|
|
74
|
+
}
|
|
64
75
|
if (isNamedGateConfig(configOrPredicate)) {
|
|
65
76
|
// Config-object gate: lowers into the compiled StepSpec's
|
|
66
77
|
// `verification:` field via slice-P named-gate lowering.
|
|
67
78
|
operation.namedGate = configOrPredicate;
|
|
68
79
|
return step;
|
|
69
80
|
}
|
|
70
|
-
// Predicate gate:
|
|
71
|
-
//
|
|
72
|
-
//
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
81
|
+
// Predicate gate: runtime control flow. The closure cannot be
|
|
82
|
+
// journaled, but its VERDICT can — the executor runs it once after
|
|
83
|
+
// the step completes and records pass/fail as a `<id>.gate` step.
|
|
84
|
+
if (typeof configOrPredicate !== 'function') {
|
|
85
|
+
throw new AuthoredFlowExecutionError(
|
|
86
|
+
'unsupported_gate',
|
|
87
|
+
'.gate() takes a named gate config ({type: "…", …}) or a predicate function.',
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
if (_because !== undefined && typeof _because !== 'string') {
|
|
91
|
+
throw new AuthoredFlowExecutionError('unsupported_gate', '.gate(predicate, because) takes a string reason.');
|
|
92
|
+
}
|
|
93
|
+
operation.predicateGate = { predicate: configOrPredicate, ...(_because === undefined ? {} : { because: _because }) };
|
|
94
|
+
return step;
|
|
78
95
|
},
|
|
79
96
|
then<TResult1 = T, TResult2 = never>(
|
|
80
97
|
onfulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | null,
|
|
@@ -144,7 +161,15 @@ export class AuthoredFlowOperation<T> {
|
|
|
144
161
|
try {
|
|
145
162
|
this.assertCanStart();
|
|
146
163
|
this.state = 'running';
|
|
147
|
-
const
|
|
164
|
+
const started = await this.start();
|
|
165
|
+
// The predicate gate, when present, is applied here for every kind of
|
|
166
|
+
// operation, so a helper or plugin step cannot carry a gate that never
|
|
167
|
+
// runs. The executor installs the applier; without one, a predicate
|
|
168
|
+
// gate is refused rather than skipped.
|
|
169
|
+
const value = this.predicateGate === undefined ? started
|
|
170
|
+
: this.scope.applyPredicateGate === undefined
|
|
171
|
+
? (() => { throw new AuthoredFlowExecutionError('unsupported_gate', 'this runtime cannot apply predicate gates'); })()
|
|
172
|
+
: await this.scope.applyPredicateGate(this as unknown as { id: string; predicateGate: unknown }, started);
|
|
148
173
|
this.state = 'fulfilled';
|
|
149
174
|
this.resolve(value);
|
|
150
175
|
} catch (error) {
|
|
@@ -161,30 +161,44 @@ export async function verifyAuthoredNodeResult(
|
|
|
161
161
|
result: AuthoredFlowExecutionResult, metadata: AuthoredRootMetadata,
|
|
162
162
|
rootRunId: string, socketPath: string,
|
|
163
163
|
): Promise<void> {
|
|
164
|
-
const invalid = (): never => { throw new Error(
|
|
164
|
+
const invalid = (why = ''): never => { throw new Error(`authored runtime result has no matching durable completion${process.env['FLOWS_VERIFIER_DEBUG'] && why ? ` (${why})` : ''}`); };
|
|
165
165
|
if (result.rootRunId !== rootRunId || result.name !== metadata.flowName
|
|
166
166
|
|| !isLoweredCompletion(result.completionReason)
|
|
167
|
-
|| !Array.isArray(result.journalSteps) || result.journalSteps.length === 0) invalid();
|
|
167
|
+
|| !Array.isArray(result.journalSteps) || result.journalSteps.length === 0) invalid('frame');
|
|
168
168
|
const terminal = result.journalSteps.at(-1)!;
|
|
169
|
-
if (!terminal || !/^complete-[1-9][0-9]*$/.test(terminal.id)) invalid();
|
|
169
|
+
if (!terminal || !/^complete-[1-9][0-9]*$/.test(terminal.id)) invalid('terminal');
|
|
170
170
|
const count = Number(terminal.id.slice('complete-'.length));
|
|
171
|
-
|
|
171
|
+
// A predicate gate is journaled as `<step>.gate`: a child run subordinate
|
|
172
|
+
// to the authored step it judges, in the same `<step>.gate` shape a named
|
|
173
|
+
// gate lowers to inside its step's own spec. Neither consumes an ordinal —
|
|
174
|
+
// `complete-N` counts the operations the author wrote (SURFACE.md §6) —
|
|
175
|
+
// so gates are set aside from the count and the contiguity check, and
|
|
176
|
+
// verified separately: every gate must name a claimed parent, and is then
|
|
177
|
+
// held to the same durable-completion evidence as any other child run.
|
|
178
|
+
const isGate = (id: string): boolean => /\.gate$/.test(id);
|
|
179
|
+
const authored = result.journalSteps.filter(step => typeof step?.id === 'string' && !isGate(step.id));
|
|
180
|
+
const gates = result.journalSteps.filter(step => typeof step?.id === 'string' && isGate(step.id));
|
|
181
|
+
if (!Number.isSafeInteger(count) || authored.length !== count || isGate(terminal.id)) invalid(`count ${authored.length}!=${count}`);
|
|
172
182
|
const ordinal = (id: string): number => Number(/-([1-9][0-9]*)$/.exec(id)?.[1]);
|
|
173
183
|
// Parallel awaits may finish in either order; validate a copy in declaration order.
|
|
174
|
-
const ordered = [...
|
|
184
|
+
const ordered = [...authored].sort((a,b)=>ordinal(a.id)-ordinal(b.id));
|
|
185
|
+
const authoredIds = new Set(ordered.map(step => step.id));
|
|
186
|
+
for (const gate of gates) {
|
|
187
|
+
if (!authoredIds.has(gate.id.slice(0, -'.gate'.length))) invalid(`orphan ${gate.id}`);
|
|
188
|
+
}
|
|
175
189
|
const runs = new Set<string>();
|
|
176
190
|
const journal = new JournalClient(socketPath);
|
|
177
191
|
await journal.connect();
|
|
178
192
|
try {
|
|
179
193
|
await journal.hello('flows-authored-result-verifier');
|
|
180
|
-
for (const [index, claimed] of ordered.entries()) {
|
|
194
|
+
for (const [index, claimed] of [...ordered, ...gates].entries()) {
|
|
181
195
|
if (!claimed || typeof claimed.id !== 'string' || typeof claimed.runId !== 'string'
|
|
182
|
-
|| ordinal(claimed.id) !== index+1 || claimed.completionReason !== 'success'
|
|
183
|
-
|| runs.has(claimed.runId)) invalid();
|
|
196
|
+
|| (index < ordered.length && ordinal(claimed.id) !== index+1) || claimed.completionReason !== 'success'
|
|
197
|
+
|| runs.has(claimed.runId)) invalid(`claim ${claimed?.id}`);
|
|
184
198
|
runs.add(claimed.runId);
|
|
185
199
|
const state = await journal.runGet(claimed.runId);
|
|
186
200
|
if (state.run_id !== claimed.runId || state.status !== 'completed'
|
|
187
|
-
|| state.steps[claimed.id]?.state !== 'done') invalid();
|
|
201
|
+
|| state.steps[claimed.id]?.state !== 'done') invalid(`state ${claimed.id} ${state.status} ${state.steps[claimed.id]?.state}`);
|
|
188
202
|
const entries: Array<{
|
|
189
203
|
seq: number; entry_type: string; step_id?: string; payload?: {
|
|
190
204
|
completionReason?: string; spec?: { name?: string; steps?: Array<{id?:string;type?:string;command?:string}> };
|
|
@@ -196,7 +210,7 @@ export async function verifyAuthoredNodeResult(
|
|
|
196
210
|
if (page.length === 0) break;
|
|
197
211
|
for (const raw of page) {
|
|
198
212
|
const entry = raw as typeof entries[number];
|
|
199
|
-
if (!Number.isSafeInteger(entry?.seq) || entry.seq < fromSeq) invalid();
|
|
213
|
+
if (!Number.isSafeInteger(entry?.seq) || entry.seq < fromSeq) invalid('seq');
|
|
200
214
|
fromSeq = entry.seq + 1;
|
|
201
215
|
// Keep only completion evidence; streaming logs can span many pages.
|
|
202
216
|
if (['run.spawned', 'step.completed', 'run.completed'].includes(entry.entry_type)) entries.push(entry);
|
|
@@ -204,12 +218,20 @@ export async function verifyAuthoredNodeResult(
|
|
|
204
218
|
}
|
|
205
219
|
const spec = entries.find(entry => entry.entry_type === 'run.spawned')?.payload?.spec;
|
|
206
220
|
const step = spec?.steps?.[0];
|
|
221
|
+
// A named gate lowers INTO the step's own spec as a second, dependent
|
|
222
|
+
// `<id>.gate` step (named-gate-lowering.ts); that is the only other
|
|
223
|
+
// step a child spec may carry, and it must have completed too.
|
|
224
|
+
const lowered = spec?.steps ?? [];
|
|
225
|
+
const specShape = lowered.length === 1 || (lowered.length === 2 && lowered[1]?.id === `${claimed.id}.gate`);
|
|
207
226
|
const completed = entries.filter(entry => entry.entry_type === 'step.completed' && entry.step_id === claimed.id);
|
|
227
|
+
const gateCompleted = lowered.length === 2
|
|
228
|
+
? entries.filter(entry => entry.entry_type === 'step.completed' && entry.step_id === `${claimed.id}.gate`) : [];
|
|
208
229
|
const terminalFacts = entries.filter(entry => entry.entry_type === 'run.completed');
|
|
209
|
-
if (spec?.name !== `${metadata.flowName}/${claimed.id}` ||
|
|
230
|
+
if (spec?.name !== `${metadata.flowName}/${claimed.id}` || !specShape
|
|
210
231
|
|| step?.id !== claimed.id || completed.length !== 1
|
|
211
232
|
|| completed[0]?.payload?.completionReason !== 'success'
|
|
212
|
-
||
|
|
233
|
+
|| (lowered.length === 2 && (gateCompleted.length !== 1 || gateCompleted[0]?.payload?.completionReason !== 'success'))
|
|
234
|
+
|| terminalFacts.length !== 1 || terminalFacts[0]?.payload?.completionReason !== 'success') invalid(`evidence ${claimed.id} spec=${spec?.name} step=${step?.id} completed=${completed.length}`);
|
|
213
235
|
if (claimed === terminal) {
|
|
214
236
|
// The claimed verdict must match the marker the journal actually
|
|
215
237
|
// recorded, so an IPC frame cannot claim `success` over a run whose
|
|
@@ -219,7 +241,7 @@ export async function verifyAuthoredNodeResult(
|
|
|
219
241
|
// the runtime validation of untrusted IPC, and it is why nothing has
|
|
220
242
|
// to be re-asserted here just to satisfy the type.
|
|
221
243
|
if (step?.type !== 'deterministic'
|
|
222
|
-
|| step.command !== completionMarker(result.completionReason)) invalid();
|
|
244
|
+
|| step.command !== completionMarker(result.completionReason)) invalid('marker');
|
|
223
245
|
}
|
|
224
246
|
}
|
|
225
247
|
} finally { journal.close(); }
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { COMPLETION_REASONS, RUN_COMPLETION_REASONS } from '@relayflows/surface';
|
|
1
|
+
import { COMPLETION_REASONS, RUN_COMPLETION_REASONS, FLOW_COMPLETION_REASONS, type FlowCompletionReason } from '@relayflows/surface';
|
|
2
2
|
import { AuthoredFlowExecutionError } from './authored-flow-error.js';
|
|
3
3
|
import type { JournalClient } from './journal-client.js';
|
|
4
4
|
import type { CompletionReason as ProtocolCompletionReason, RunCompletionReason as ProtocolRunCompletionReason, RunOutcome } from './protocol.js';
|
|
@@ -93,6 +93,11 @@ export function isSurfaceRunCompletionReason(value: unknown): value is ProtocolR
|
|
|
93
93
|
&& (RUN_COMPLETION_REASONS as readonly string[]).includes(value);
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
+
export function isSurfaceFlowCompletionReason(value: unknown): value is FlowCompletionReason {
|
|
97
|
+
return typeof value === 'string'
|
|
98
|
+
&& (FLOW_COMPLETION_REASONS as readonly string[]).includes(value);
|
|
99
|
+
}
|
|
100
|
+
|
|
96
101
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
97
102
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
98
103
|
}
|
|
@@ -7,7 +7,7 @@ import { classifyOutcome, type RunLifecycleOptions } from './cli/run.js';
|
|
|
7
7
|
import type { PreflightDiagnostic } from './preflight.js';
|
|
8
8
|
import { AuthoredFlowExecutionError } from './authored-flow-error.js';
|
|
9
9
|
import type { JournalClient } from './journal-client.js';
|
|
10
|
-
import { SPEC_SCHEMA_VERSION, type FlowSpec, type StepSpec } from './spec.js';
|
|
10
|
+
import { SPEC_SCHEMA_VERSION, type FlowSpec, type PermissionsSpec, type StepSpec } from './spec.js';
|
|
11
11
|
import { isSurfaceCompletionReason, readCompletedStepOutput, readSuccessfulOutput } from './authored-step-output.js';
|
|
12
12
|
import type { AuthoredFlowJournalStep } from './authored-flow-executor.js';
|
|
13
13
|
import { snapshotJsonValue } from './json-value.js';
|
|
@@ -96,13 +96,10 @@ export function authoredWorkerRunner(
|
|
|
96
96
|
if (options.workspace !== undefined && WORKSPACE_PERMISSION_ANNOTATION.test(options.workspace)) {
|
|
97
97
|
throw new AuthoredFlowExecutionError(
|
|
98
98
|
'unsupported_workspace_permission',
|
|
99
|
-
`flow "${definition.name}" step "${id}": workspace "${options.workspace}"
|
|
100
|
-
+
|
|
101
|
-
+
|
|
102
|
-
+ '
|
|
103
|
-
+ 'Silently accepting and ignoring it would let a flow believe a restriction is in effect '
|
|
104
|
-
+ "when it is not. Declare a bare surface name (no trailing \": readonly\"/\": readwrite\") "
|
|
105
|
-
+ 'if you do not need enforcement, or use the declarative spec\'s `permissions` field, which is real.',
|
|
99
|
+
`flow "${definition.name}" step "${id}": workspace "${options.workspace}": `
|
|
100
|
+
+ "Workspace permission suffixes are unsupported. Use a bare workspace name and f.agent's "
|
|
101
|
+
+ "permissions option, for example permissions: { fileGlobs: ['src/**'], accessPreset: 'readonly' }. "
|
|
102
|
+
+ 'This declaration is validated and recorded with the step spec; it is not currently enforced (gate 8 / #442).',
|
|
106
103
|
);
|
|
107
104
|
}
|
|
108
105
|
if (options.cli !== undefined && typeof options.cli !== 'string') {
|
|
@@ -129,8 +126,12 @@ export function authoredWorkerRunner(
|
|
|
129
126
|
`f.agent options.transport must be 'direct' or 'relay' (got ${JSON.stringify(options.transport)}).`,
|
|
130
127
|
);
|
|
131
128
|
}
|
|
129
|
+
const permissions = options.permissions;
|
|
130
|
+
const permissionsSnapshot = permissions === undefined ? undefined
|
|
131
|
+
: snapshotJsonValue(permissions, 'f.agent options.permissions') as unknown as PermissionsSpec;
|
|
132
132
|
const output = await run({
|
|
133
133
|
id, type: 'agent', instruction: options.task,
|
|
134
|
+
...(permissionsSnapshot === undefined ? {} : { permissions: permissionsSnapshot }),
|
|
134
135
|
...(localAgentStream === undefined ? {} : { surfaces: { streams: [{ stream: localAgentStream }] } }),
|
|
135
136
|
...(options.workspace === undefined ? {} : { surfaces: { workspace: [{ surface: options.workspace }] } }),
|
|
136
137
|
...(options.cli === undefined ? {} : { cli: options.cli }),
|
|
@@ -143,7 +144,13 @@ export function authoredWorkerRunner(
|
|
|
143
144
|
throw new AuthoredFlowExecutionError('journal_protocol_violation', `step "${id}" produced a non-object output`);
|
|
144
145
|
}
|
|
145
146
|
const stdout = 'stdout_tail' in output ? output.stdout_tail : undefined;
|
|
146
|
-
|
|
147
|
+
// The worker that ran the CLI measured the artifacts and journaled them
|
|
148
|
+
// in the step's output; read that fact back rather than re-scanning a
|
|
149
|
+
// directory this process may not even share with the agent.
|
|
150
|
+
const journaled = 'artifacts' in output ? output.artifacts : undefined;
|
|
151
|
+
const artifacts = Array.isArray(journaled) && journaled.every(entry => typeof entry === 'string')
|
|
152
|
+
? [...journaled] : [];
|
|
153
|
+
return { summary: typeof stdout === 'string' ? stdout : JSON.stringify(output), artifacts };
|
|
147
154
|
},
|
|
148
155
|
async llm(id: string, prompt: string, options?: LlmOptions, verification?: NamedGate): Promise<unknown> {
|
|
149
156
|
if (options !== undefined && (typeof options !== 'object' || options === null || options.output === undefined)) {
|
|
@@ -2,6 +2,7 @@ import { dirname, resolve } from 'node:path';
|
|
|
2
2
|
import { loadAuthoredFlow, type LoadedAuthoredFlow } from '../authored-flow-loader.js';
|
|
3
3
|
import { preflightWebhookTriggers } from '../preflight.js';
|
|
4
4
|
import { preflightProviderTriggers } from '../provider-trigger-contract.js';
|
|
5
|
+
import { scheduleLowering } from '../schedule-trigger.js';
|
|
5
6
|
import { checkSlackHelpers } from '../slack-preflight.js';
|
|
6
7
|
import { inputFailureReport, readProjectConfig, type CheckReport } from './check.js';
|
|
7
8
|
|
|
@@ -31,12 +32,20 @@ export async function checkAuthoredTriggers(path: string): Promise<{
|
|
|
31
32
|
const diagnostics = [
|
|
32
33
|
...triggerDiagnostics, ...providerDiagnostics, ...helperReport.diagnostics,
|
|
33
34
|
];
|
|
35
|
+
// A schedule is inspectable data: print what it lowers to, and say plainly
|
|
36
|
+
// when the local runner cannot drive it. Neither is a refusal — Cloud can.
|
|
37
|
+
const schedules = triggers.flatMap((trigger, handler) => {
|
|
38
|
+
if (trigger.kind !== 'schedule') return [];
|
|
39
|
+
const lowering = scheduleLowering(definition.name, trigger);
|
|
40
|
+
return [{ handler, ...lowering }];
|
|
41
|
+
});
|
|
34
42
|
return {
|
|
35
43
|
loaded,
|
|
36
44
|
report: {
|
|
37
45
|
// Severity, not emptiness: a warning must never refuse a flow.
|
|
38
46
|
ok: !diagnostics.some(diagnostic => diagnostic.severity === 'refusal'),
|
|
39
47
|
path, gates: [], resolutions: [], diagnostics,
|
|
48
|
+
...(schedules.length === 0 ? {} : { schedules }),
|
|
40
49
|
...(config.path === undefined ? {} : { projectConfigPath: config.path }),
|
|
41
50
|
},
|
|
42
51
|
};
|
package/src/cli/check.ts
CHANGED
|
@@ -45,9 +45,24 @@ export interface CheckReport {
|
|
|
45
45
|
projectConfigPath?: string;
|
|
46
46
|
gates: StepGateInspection[];
|
|
47
47
|
resolutions: CliResolution[];
|
|
48
|
+
/** Authored `schedule.*` handlers and the `flows.tick` subscription each lowers to. */
|
|
49
|
+
schedules?: ScheduleInspection[];
|
|
48
50
|
diagnostics: Array<PreflightDiagnostic | CheckInputDiagnostic | CheckWarningDiagnostic>;
|
|
49
51
|
}
|
|
50
52
|
|
|
53
|
+
export interface ScheduleInspection {
|
|
54
|
+
/** Position among the flow's handlers, so two identical declarations stay distinct. */
|
|
55
|
+
handler: number;
|
|
56
|
+
cron?: string;
|
|
57
|
+
tz?: string;
|
|
58
|
+
intervalMs?: number;
|
|
59
|
+
epochMs?: number;
|
|
60
|
+
staleAfterMs?: number;
|
|
61
|
+
scheduleId: string;
|
|
62
|
+
/** Present when the local tick runner cannot drive it (only a cron-aware runner can). */
|
|
63
|
+
localUnsupported?: string;
|
|
64
|
+
}
|
|
65
|
+
|
|
51
66
|
export interface CheckWarningDiagnostic {
|
|
52
67
|
severity: 'warning';
|
|
53
68
|
kind: CheckWarningKind;
|
|
@@ -340,7 +355,12 @@ function systemProbes(flowDirectory: string, config: ProjectConfig): PreflightPr
|
|
|
340
355
|
helper: helperReady,
|
|
341
356
|
cli: (cli, source, model) => probeCli(cli, source === 'project' ? config.directory : flowDirectory, model),
|
|
342
357
|
executor: (trigger) => config.executors.includes(trigger.executor),
|
|
343
|
-
|
|
358
|
+
// A deterministic step runs in the daemon's working directory — the
|
|
359
|
+
// directory `flows run` was invoked from, or Cloud's code mount — not in
|
|
360
|
+
// the flow file's. Probing `./x` against the flow's directory answered a
|
|
361
|
+
// question the kernel never asks, and refused a Cloud run whose synced
|
|
362
|
+
// tree held the script while its source sat in the state directory.
|
|
363
|
+
command: (binary) => executableExists(binary, process.cwd()),
|
|
344
364
|
};
|
|
345
365
|
}
|
|
346
366
|
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { CloudFlowError } from '../cloud-http.js';
|
|
2
|
+
import {
|
|
3
|
+
deployToCloud, listCloudDeployments, parseAgentHarnesses, parseRepository, parseTriggerSource, undeployFromCloud,
|
|
4
|
+
type FlowTriggerSource,
|
|
5
|
+
} from '../cloud-deploy.js';
|
|
6
|
+
import type { CliIo } from '../cli.js';
|
|
7
|
+
|
|
8
|
+
export interface CloudDeployArgs {
|
|
9
|
+
command: 'cloud-deploy';
|
|
10
|
+
value: string;
|
|
11
|
+
repo: string;
|
|
12
|
+
on: string[];
|
|
13
|
+
approver: string | undefined;
|
|
14
|
+
name: string | undefined;
|
|
15
|
+
agents: string | undefined;
|
|
16
|
+
draft: boolean;
|
|
17
|
+
json: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* `flows deploy <flow.ts> --repo <owner/name> --on <provider>[:k=v,…] [--on …]
|
|
22
|
+
* --approver <handle> [--name <n>] [--json]`
|
|
23
|
+
*
|
|
24
|
+
* Parsed here rather than in `parseDeployArgs` because the two `deploy` forms
|
|
25
|
+
* share nothing but the word: the digest form copies a sealed bundle into a
|
|
26
|
+
* file bucket, this one creates a hosted listener. The positional decides.
|
|
27
|
+
*/
|
|
28
|
+
export function parseCloudDeployArgs(args: readonly string[]): CloudDeployArgs | undefined {
|
|
29
|
+
let value: string | undefined;
|
|
30
|
+
let repo: string | undefined;
|
|
31
|
+
let approver: string | undefined;
|
|
32
|
+
let name: string | undefined;
|
|
33
|
+
let agents: string | undefined;
|
|
34
|
+
let draft = false;
|
|
35
|
+
let json = false;
|
|
36
|
+
const on: string[] = [];
|
|
37
|
+
for (let i = 0; i < args.length; i++) {
|
|
38
|
+
const arg = args[i]!;
|
|
39
|
+
if (arg === '--json') {
|
|
40
|
+
if (json) return undefined;
|
|
41
|
+
json = true;
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
if (arg === '--draft') {
|
|
45
|
+
if (draft) return undefined;
|
|
46
|
+
draft = true;
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
if (arg === '--agents') {
|
|
50
|
+
const next = args[i + 1];
|
|
51
|
+
if (agents !== undefined || next === undefined || next.startsWith('-')) return undefined;
|
|
52
|
+
agents = next;
|
|
53
|
+
i += 1;
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
if (arg === '--repo' || arg === '--approver' || arg === '--name' || arg === '--on') {
|
|
57
|
+
const next = args[i + 1];
|
|
58
|
+
if (next === undefined || next.startsWith('-')) return undefined;
|
|
59
|
+
i += 1;
|
|
60
|
+
if (arg === '--on') { on.push(next); continue; }
|
|
61
|
+
if (arg === '--repo') { if (repo !== undefined) return undefined; repo = next; continue; }
|
|
62
|
+
if (arg === '--approver') { if (approver !== undefined) return undefined; approver = next; continue; }
|
|
63
|
+
if (name !== undefined) return undefined;
|
|
64
|
+
name = next;
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
if (arg.startsWith('-') || value !== undefined) return undefined;
|
|
68
|
+
value = arg;
|
|
69
|
+
}
|
|
70
|
+
if (value === undefined || repo === undefined || on.length === 0) return undefined;
|
|
71
|
+
return { command: 'cloud-deploy', value, repo, on, approver, name, agents, draft, json };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function describeSource(source: FlowTriggerSource): string {
|
|
75
|
+
const settings = Object.entries(source.settings).map(([k, v]) => `${k}=${v}`).join(' ');
|
|
76
|
+
return settings ? `${source.provider} ${settings}` : source.provider;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export async function runCloudDeployCli(args: CloudDeployArgs, io: CliIo): Promise<0 | 1 | 2> {
|
|
80
|
+
try {
|
|
81
|
+
if (args.approver === undefined) {
|
|
82
|
+
throw new CloudFlowError('invalid_input',
|
|
83
|
+
'--approver <handle> is required: every launched run receives it as input.approver for f.human.');
|
|
84
|
+
}
|
|
85
|
+
const deployment = await deployToCloud({
|
|
86
|
+
path: args.value,
|
|
87
|
+
repository: parseRepository(args.repo),
|
|
88
|
+
sources: args.on.map(parseTriggerSource),
|
|
89
|
+
approver: args.approver,
|
|
90
|
+
draft: args.draft,
|
|
91
|
+
...(args.name === undefined ? {} : { name: args.name }),
|
|
92
|
+
...(args.agents === undefined ? {} : { agents: parseAgentHarnesses(args.agents) }),
|
|
93
|
+
});
|
|
94
|
+
if (args.json) {
|
|
95
|
+
io.stdout(JSON.stringify({ ok: true, ...deployment }));
|
|
96
|
+
return 0;
|
|
97
|
+
}
|
|
98
|
+
io.stdout(`${deployment.status === 'draft' ? 'SAVED' : 'DEPLOYED'} ${deployment.agentId} ${deployment.status}`);
|
|
99
|
+
io.stdout(` flow: ${deployment.name} (${args.value}, sha256 ${deployment.sourceSha256.slice(0, 12)})`);
|
|
100
|
+
io.stdout(` repository: ${deployment.repository.owner}/${deployment.repository.name}`);
|
|
101
|
+
for (const source of deployment.sources) io.stdout(` on: ${describeSource(source)}`);
|
|
102
|
+
io.stdout(deployment.status === 'draft'
|
|
103
|
+
? 'Saved without activating; activate it from the Cloud dashboard, or redeploy without --draft.'
|
|
104
|
+
: 'Each matching ticket launches a run of this source in a fresh branch; list with: flows deployments');
|
|
105
|
+
return 0;
|
|
106
|
+
} catch (error) {
|
|
107
|
+
return reportCloudFailure(error, args.json, io);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export async function runCloudDeploymentsCli({ json }: { json: boolean }, io: CliIo): Promise<0 | 1 | 2> {
|
|
112
|
+
try {
|
|
113
|
+
const deployments = await listCloudDeployments();
|
|
114
|
+
if (json) {
|
|
115
|
+
io.stdout(JSON.stringify({ ok: true, deployments }));
|
|
116
|
+
return 0;
|
|
117
|
+
}
|
|
118
|
+
if (deployments.length === 0) {
|
|
119
|
+
io.stdout('No flow deployments in this workspace.');
|
|
120
|
+
return 0;
|
|
121
|
+
}
|
|
122
|
+
for (const d of deployments) {
|
|
123
|
+
const repo = d.repository ? ` ${d.repository.owner}/${d.repository.name}` : '';
|
|
124
|
+
io.stdout(`${d.agentId} ${d.status} ${JSON.stringify(d.name)}${repo}`);
|
|
125
|
+
for (const source of d.sources) io.stdout(` on: ${describeSource(source)}`);
|
|
126
|
+
}
|
|
127
|
+
return 0;
|
|
128
|
+
} catch (error) {
|
|
129
|
+
return reportCloudFailure(error, json, io);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export async function runCloudUndeployCli({ agentId, json }: { agentId: string; json: boolean }, io: CliIo): Promise<0 | 1 | 2> {
|
|
134
|
+
try {
|
|
135
|
+
await undeployFromCloud(agentId);
|
|
136
|
+
io.stdout(json ? JSON.stringify({ ok: true, agentId, status: 'deleted' }) : `UNDEPLOYED ${agentId}`);
|
|
137
|
+
return 0;
|
|
138
|
+
} catch (error) {
|
|
139
|
+
return reportCloudFailure(error, json, io);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function reportCloudFailure(error: unknown, json: boolean, io: CliIo): 1 | 2 {
|
|
144
|
+
const code = error instanceof CloudFlowError ? error.code : 'cloud_deploy_failed';
|
|
145
|
+
let message = error instanceof Error ? error.message : 'Cloud deploy failed.';
|
|
146
|
+
// The deploy routes take a browser session or a `cli:auth` token. A
|
|
147
|
+
// deployment (CI) token gets 403 `session_required`; say what fixes it.
|
|
148
|
+
if (error instanceof CloudFlowError && error.status === 403) {
|
|
149
|
+
message += ' Deploying needs an interactive `cli:auth` credential: run `agent-relay cloud login` '
|
|
150
|
+
+ '(a deployment token can run flows but not deploy them).';
|
|
151
|
+
}
|
|
152
|
+
if (json) io.stdout(JSON.stringify({ ok: false, code, message }));
|
|
153
|
+
else io.stderr(`${code}: ${message}`);
|
|
154
|
+
return error instanceof CloudFlowError
|
|
155
|
+
&& (['configuration', 'unsupported_source', 'invalid_input'].includes(error.code) || error.status === 403 || error.status === 401)
|
|
156
|
+
? 2 : 1;
|
|
157
|
+
}
|
package/src/cli/cloud-run.ts
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import { CloudFlowError } from '../cloud-http.js';
|
|
2
|
-
import { runInCloud, waitForCloudFlowRun } from '../cloud-run.js';
|
|
2
|
+
import { runInCloud, waitForCloudFlowRun, type RunInCloudOptions } from '../cloud-run.js';
|
|
3
|
+
import { DirectInputError, isAuthoredFlowPath, parseDirectInput } from '../direct-input.js';
|
|
4
|
+
import { snapshotJsonValue } from '../json-value.js';
|
|
3
5
|
import type { CliIo } from '../cli.js';
|
|
4
6
|
|
|
5
7
|
/** Presentation only: the central CLI parser owns argv; the SDK owns the lifecycle. */
|
|
6
8
|
export async function runCloudCli(
|
|
7
|
-
{ value: path, json, wait }: {
|
|
9
|
+
{ value: path, json, wait, input, syncCode }: {
|
|
10
|
+
value: string; json: boolean; wait: boolean; input: string | undefined; syncCode: boolean;
|
|
11
|
+
},
|
|
8
12
|
io: CliIo,
|
|
9
13
|
): Promise<0 | 1 | 2> {
|
|
10
14
|
const controller = new AbortController();
|
|
@@ -12,12 +16,36 @@ export async function runCloudCli(
|
|
|
12
16
|
process.once('SIGINT', abort);
|
|
13
17
|
process.once('SIGTERM', abort);
|
|
14
18
|
let runId: string | undefined;
|
|
19
|
+
// Flips at the run submission. An interruption before it — during prepare,
|
|
20
|
+
// packing or upload — admitted nothing and is safe to retry; only an
|
|
21
|
+
// interrupted submission has unknown admission.
|
|
22
|
+
let submitting = false;
|
|
15
23
|
try {
|
|
16
|
-
const
|
|
24
|
+
const options: RunInCloudOptions = { signal: controller.signal, onSubmit: () => { submitting = true; } };
|
|
25
|
+
if (isAuthoredFlowPath(path)) {
|
|
26
|
+
// Same parse as a local direct run, so a file-or-inline argument means
|
|
27
|
+
// the same thing on both sides of `--cloud`.
|
|
28
|
+
try {
|
|
29
|
+
options.input = snapshotJsonValue(parseDirectInput(input), 'Cloud authored input');
|
|
30
|
+
} catch (error) {
|
|
31
|
+
if (error instanceof DirectInputError) throw new CloudFlowError('invalid_input', error.message);
|
|
32
|
+
throw error;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
// The tree is the invoking directory, as with v1: the flow path is where
|
|
36
|
+
// the body lives, not the boundary of what the run may read.
|
|
37
|
+
if (syncCode) options.syncCode = { root: process.cwd() };
|
|
38
|
+
const receipt = await runInCloud({ path }, options);
|
|
17
39
|
runId = receipt.runId;
|
|
18
40
|
if (!json) {
|
|
19
41
|
io.stdout(`ACCEPTED ${receipt.runId} (${receipt.status})`);
|
|
20
42
|
io.stdout(receipt.apiUrl);
|
|
43
|
+
if (receipt.synced) {
|
|
44
|
+
io.stdout(`SYNCED ${receipt.synced.files} files (${receipt.synced.bytes} bytes); pull changes with: flows sync ${receipt.runId}`);
|
|
45
|
+
for (const link of receipt.synced.skippedLinks) {
|
|
46
|
+
io.stderr(`WARNING [sync_link_skipped] ${link} points outside the synced tree and was not uploaded`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
21
49
|
}
|
|
22
50
|
if (!wait) {
|
|
23
51
|
if (json) io.stdout(JSON.stringify({ ok: true, ...receipt }));
|
|
@@ -29,16 +57,19 @@ export async function runCloudCli(
|
|
|
29
57
|
else io.stdout(`${run.status.toUpperCase()} ${run.runId} completionReason: ${'completionReason' in run ? run.completionReason : 'unavailable'}`);
|
|
30
58
|
return ok ? 0 : 1;
|
|
31
59
|
} catch (error) {
|
|
32
|
-
const code = controller.signal.aborted
|
|
60
|
+
const code = controller.signal.aborted
|
|
61
|
+
? runId ? 'observation_aborted' : submitting ? 'admission_unknown' : 'submission_aborted'
|
|
33
62
|
: error instanceof CloudFlowError ? error.code : 'cloud_run_failed';
|
|
34
63
|
const message = controller.signal.aborted
|
|
35
64
|
? runId ? 'Stopped observing; the hosted run has not been cancelled.'
|
|
36
|
-
:
|
|
65
|
+
: submitting
|
|
66
|
+
? 'Submission interrupted before a receipt was received. Admission is unknown; Cloud may have started the run. Do not resubmit blindly.'
|
|
67
|
+
: 'Interrupted before the run was submitted; nothing was admitted. Safe to run again.'
|
|
37
68
|
: error instanceof Error ? error.message : 'Cloud run failed.';
|
|
38
69
|
if (json) io.stdout(JSON.stringify({ ok: false, code, message, ...(runId ? { runId } : {}) }));
|
|
39
70
|
else io.stderr(`${code}: ${message}${runId ? ` (run ${runId})` : ''}`);
|
|
40
71
|
return error instanceof CloudFlowError
|
|
41
|
-
&& (['configuration', 'unsupported_source', 'invalid_input'].includes(error.code)
|
|
72
|
+
&& (['configuration', 'unsupported_source', 'invalid_input', 'unsupported_storage_backend', 'sync_too_large', 'sync_unsupported'].includes(error.code)
|
|
42
73
|
|| (runId === undefined && error.code === 'http_error' && [401, 403].includes(error.status ?? 0))) ? 2 : 1;
|
|
43
74
|
} finally {
|
|
44
75
|
process.off('SIGINT', abort);
|