@relayflows/sdk 2.0.0
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 +9 -0
- package/dist/authored-flow-error.d.ts.map +1 -0
- package/dist/authored-flow-error.js +13 -0
- package/dist/authored-flow-error.js.map +1 -0
- package/dist/authored-flow-executor.d.ts +28 -0
- package/dist/authored-flow-executor.d.ts.map +1 -0
- package/dist/authored-flow-executor.js +203 -0
- package/dist/authored-flow-executor.js.map +1 -0
- package/dist/authored-flow-lifecycle.d.ts +61 -0
- package/dist/authored-flow-lifecycle.d.ts.map +1 -0
- package/dist/authored-flow-lifecycle.js +284 -0
- package/dist/authored-flow-lifecycle.js.map +1 -0
- package/dist/authored-flow-loader.d.ts +7 -0
- package/dist/authored-flow-loader.d.ts.map +1 -0
- package/dist/authored-flow-loader.js +39 -0
- package/dist/authored-flow-loader.js.map +1 -0
- package/dist/authored-flow-operation.d.ts +43 -0
- package/dist/authored-flow-operation.d.ts.map +1 -0
- package/dist/authored-flow-operation.js +176 -0
- package/dist/authored-flow-operation.js.map +1 -0
- package/dist/authored-flow.d.ts +12 -0
- package/dist/authored-flow.d.ts.map +1 -0
- package/dist/authored-flow.js +13 -0
- package/dist/authored-flow.js.map +1 -0
- package/dist/authored-promise-graph.d.ts +67 -0
- package/dist/authored-promise-graph.d.ts.map +1 -0
- package/dist/authored-promise-graph.js +206 -0
- package/dist/authored-promise-graph.js.map +1 -0
- package/dist/backlog-picker.d.ts +55 -0
- package/dist/backlog-picker.d.ts.map +1 -0
- package/dist/backlog-picker.js +117 -0
- package/dist/backlog-picker.js.map +1 -0
- package/dist/canonical.d.ts +18 -0
- package/dist/canonical.d.ts.map +1 -0
- package/dist/canonical.js +55 -0
- package/dist/canonical.js.map +1 -0
- package/dist/cli/check.d.ts +30 -0
- package/dist/cli/check.d.ts.map +1 -0
- package/dist/cli/check.js +324 -0
- package/dist/cli/check.js.map +1 -0
- package/dist/cli/direct-run.d.ts +3 -0
- package/dist/cli/direct-run.d.ts.map +1 -0
- package/dist/cli/direct-run.js +69 -0
- package/dist/cli/direct-run.js.map +1 -0
- package/dist/cli/hn-monitor.d.ts +107 -0
- package/dist/cli/hn-monitor.d.ts.map +1 -0
- package/dist/cli/hn-monitor.js +220 -0
- package/dist/cli/hn-monitor.js.map +1 -0
- package/dist/cli/interruptible-sleep.d.ts +13 -0
- package/dist/cli/interruptible-sleep.d.ts.map +1 -0
- package/dist/cli/interruptible-sleep.js +31 -0
- package/dist/cli/interruptible-sleep.js.map +1 -0
- package/dist/cli/run.d.ts +54 -0
- package/dist/cli/run.d.ts.map +1 -0
- package/dist/cli/run.js +316 -0
- package/dist/cli/run.js.map +1 -0
- package/dist/cli/tick-runner.d.ts +151 -0
- package/dist/cli/tick-runner.d.ts.map +1 -0
- package/dist/cli/tick-runner.js +258 -0
- package/dist/cli/tick-runner.js.map +1 -0
- package/dist/cli-adapter.d.ts +29 -0
- package/dist/cli-adapter.d.ts.map +1 -0
- package/dist/cli-adapter.js +92 -0
- package/dist/cli-adapter.js.map +1 -0
- package/dist/cli-executable.d.ts +3 -0
- package/dist/cli-executable.d.ts.map +1 -0
- package/dist/cli-executable.js +10 -0
- package/dist/cli-executable.js.map +1 -0
- package/dist/cli.d.ts +9 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +348 -0
- package/dist/cli.js.map +1 -0
- package/dist/compile.d.ts +42 -0
- package/dist/compile.d.ts.map +1 -0
- package/dist/compile.js +515 -0
- package/dist/compile.js.map +1 -0
- package/dist/demo-hn-monitor.d.ts +2 -0
- package/dist/demo-hn-monitor.d.ts.map +1 -0
- package/dist/demo-hn-monitor.js +118 -0
- package/dist/demo-hn-monitor.js.map +1 -0
- package/dist/dir-watcher-poller.d.ts +73 -0
- package/dist/dir-watcher-poller.d.ts.map +1 -0
- package/dist/dir-watcher-poller.js +80 -0
- package/dist/dir-watcher-poller.js.map +1 -0
- package/dist/direct-input.d.ts +10 -0
- package/dist/direct-input.d.ts.map +1 -0
- package/dist/direct-input.js +61 -0
- package/dist/direct-input.js.map +1 -0
- package/dist/failure-kinds.d.ts +25 -0
- package/dist/failure-kinds.d.ts.map +1 -0
- package/dist/failure-kinds.js +64 -0
- package/dist/failure-kinds.js.map +1 -0
- package/dist/gate-contract.d.ts +24 -0
- package/dist/gate-contract.d.ts.map +1 -0
- package/dist/gate-contract.js +50 -0
- package/dist/gate-contract.js.map +1 -0
- package/dist/hn-poller.d.ts +50 -0
- package/dist/hn-poller.d.ts.map +1 -0
- package/dist/hn-poller.js +79 -0
- package/dist/hn-poller.js.map +1 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +30 -0
- package/dist/index.js.map +1 -0
- package/dist/journal-client.d.ts +122 -0
- package/dist/journal-client.d.ts.map +1 -0
- package/dist/journal-client.js +277 -0
- package/dist/journal-client.js.map +1 -0
- package/dist/json-schema-bound.d.ts +7 -0
- package/dist/json-schema-bound.d.ts.map +1 -0
- package/dist/json-schema-bound.js +353 -0
- package/dist/json-schema-bound.js.map +1 -0
- package/dist/json-schema.d.ts +5 -0
- package/dist/json-schema.d.ts.map +1 -0
- package/dist/json-schema.js +72 -0
- package/dist/json-schema.js.map +1 -0
- package/dist/json-value.d.ts +6 -0
- package/dist/json-value.d.ts.map +1 -0
- package/dist/json-value.js +94 -0
- package/dist/json-value.js.map +1 -0
- package/dist/model-name.d.ts +7 -0
- package/dist/model-name.d.ts.map +1 -0
- package/dist/model-name.js +21 -0
- package/dist/model-name.js.map +1 -0
- package/dist/output-schema.d.ts +8 -0
- package/dist/output-schema.d.ts.map +1 -0
- package/dist/output-schema.js +28 -0
- package/dist/output-schema.js.map +1 -0
- package/dist/preflight.d.ts +80 -0
- package/dist/preflight.d.ts.map +1 -0
- package/dist/preflight.js +361 -0
- package/dist/preflight.js.map +1 -0
- package/dist/protocol.d.ts +359 -0
- package/dist/protocol.d.ts.map +1 -0
- package/dist/protocol.js +15 -0
- package/dist/protocol.js.map +1 -0
- package/dist/spec.d.ts +294 -0
- package/dist/spec.d.ts.map +1 -0
- package/dist/spec.js +13 -0
- package/dist/spec.js.map +1 -0
- package/dist/step-dependencies.d.ts +2 -0
- package/dist/step-dependencies.d.ts.map +1 -0
- package/dist/step-dependencies.js +88 -0
- package/dist/step-dependencies.js.map +1 -0
- package/dist/step-fields.d.ts +17 -0
- package/dist/step-fields.d.ts.map +1 -0
- package/dist/step-fields.js +32 -0
- package/dist/step-fields.js.map +1 -0
- package/dist/tick-source.d.ts +205 -0
- package/dist/tick-source.d.ts.map +1 -0
- package/dist/tick-source.js +234 -0
- package/dist/tick-source.js.map +1 -0
- package/dist/unknown-keys.d.ts +7 -0
- package/dist/unknown-keys.d.ts.map +1 -0
- package/dist/unknown-keys.js +47 -0
- package/dist/unknown-keys.js.map +1 -0
- package/dist/validate.d.ts +7 -0
- package/dist/validate.d.ts.map +1 -0
- package/dist/validate.js +458 -0
- package/dist/validate.js.map +1 -0
- package/dist/work-package-consumer.d.ts +33 -0
- package/dist/work-package-consumer.d.ts.map +1 -0
- package/dist/work-package-consumer.js +44 -0
- package/dist/work-package-consumer.js.map +1 -0
- package/dist/work-package-validator.d.ts +15 -0
- package/dist/work-package-validator.d.ts.map +1 -0
- package/dist/work-package-validator.js +72 -0
- package/dist/work-package-validator.js.map +1 -0
- package/dist/worker-cli.d.ts +16 -0
- package/dist/worker-cli.d.ts.map +1 -0
- package/dist/worker-cli.js +77 -0
- package/dist/worker-cli.js.map +1 -0
- package/dist/worker.d.ts +57 -0
- package/dist/worker.d.ts.map +1 -0
- package/dist/worker.js +129 -0
- package/dist/worker.js.map +1 -0
- package/dist/wrapper-runtime.d.ts +10 -0
- package/dist/wrapper-runtime.d.ts.map +1 -0
- package/dist/wrapper-runtime.js +83 -0
- package/dist/wrapper-runtime.js.map +1 -0
- package/dist/wrapper-session.d.ts +16 -0
- package/dist/wrapper-session.d.ts.map +1 -0
- package/dist/wrapper-session.js +244 -0
- package/dist/wrapper-session.js.map +1 -0
- package/package.json +50 -0
- package/src/authored-flow-error.ts +31 -0
- package/src/authored-flow-executor.ts +365 -0
- package/src/authored-flow-lifecycle.ts +344 -0
- package/src/authored-flow-loader.ts +44 -0
- package/src/authored-flow-operation.ts +252 -0
- package/src/authored-flow.ts +21 -0
- package/src/authored-promise-graph.ts +196 -0
- package/src/backlog-picker.ts +153 -0
- package/src/canonical.ts +57 -0
- package/src/cli/check.ts +391 -0
- package/src/cli/direct-run.ts +86 -0
- package/src/cli/hn-monitor.ts +286 -0
- package/src/cli/interruptible-sleep.ts +27 -0
- package/src/cli/run.ts +439 -0
- package/src/cli/tick-runner.ts +378 -0
- package/src/cli-adapter.ts +115 -0
- package/src/cli-executable.ts +12 -0
- package/src/cli.ts +364 -0
- package/src/compile.ts +573 -0
- package/src/demo-hn-monitor.ts +132 -0
- package/src/dir-watcher-poller.ts +122 -0
- package/src/direct-input.ts +69 -0
- package/src/failure-kinds.ts +77 -0
- package/src/gate-contract.ts +72 -0
- package/src/hn-poller.ts +104 -0
- package/src/index.ts +195 -0
- package/src/journal-client.ts +379 -0
- package/src/json-schema-bound.ts +357 -0
- package/src/json-schema.ts +65 -0
- package/src/json-value.ts +110 -0
- package/src/model-name.ts +20 -0
- package/src/output-schema.ts +33 -0
- package/src/preflight.ts +482 -0
- package/src/protocol.ts +364 -0
- package/src/spec.ts +343 -0
- package/src/step-dependencies.ts +100 -0
- package/src/step-fields.ts +36 -0
- package/src/tick-source.ts +334 -0
- package/src/unknown-keys.ts +52 -0
- package/src/validate.ts +503 -0
- package/src/work-package-consumer.ts +73 -0
- package/src/work-package-validator.ts +90 -0
- package/src/worker-cli.ts +110 -0
- package/src/worker.ts +150 -0
- package/src/wrapper-runtime.ts +93 -0
- package/src/wrapper-session.ts +287 -0
package/src/preflight.ts
ADDED
|
@@ -0,0 +1,482 @@
|
|
|
1
|
+
import type { FlowSpec, StepSpec, TriggerSpec } from './spec.js';
|
|
2
|
+
import { acceptsAnyOutput, inspectStepGate, type StepGateInspection } from './gate-contract.js';
|
|
3
|
+
import { compileSpec, CompileError } from './compile.js';
|
|
4
|
+
import type {
|
|
5
|
+
PreflightFailureKind,
|
|
6
|
+
PreflightWarningKind,
|
|
7
|
+
} from './failure-kinds.js';
|
|
8
|
+
|
|
9
|
+
export type CliResolutionSource = 'step' | 'named' | 'flow' | 'project';
|
|
10
|
+
|
|
11
|
+
export interface CliResolution {
|
|
12
|
+
stepId: string;
|
|
13
|
+
cli: string;
|
|
14
|
+
source: CliResolutionSource;
|
|
15
|
+
/** Model the step or selected named agent declared, probed with the CLI. */
|
|
16
|
+
model?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface CliProbeResult {
|
|
20
|
+
exists: boolean;
|
|
21
|
+
authenticated: boolean;
|
|
22
|
+
/** False when a custom executable did not identify as a wrapper adapter. */
|
|
23
|
+
supported?: boolean;
|
|
24
|
+
/** Exact declared model passed the CLI's model-scoped readiness probe. */
|
|
25
|
+
modelAvailable?: boolean;
|
|
26
|
+
authCommand?: string;
|
|
27
|
+
modelCommand?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export type CliProbeFailureDetail =
|
|
31
|
+
| 'spawn_failed'
|
|
32
|
+
| `timeout:${number}ms`
|
|
33
|
+
| `signal:${string}`;
|
|
34
|
+
|
|
35
|
+
export class CliProbeError extends Error {
|
|
36
|
+
constructor(readonly detail: CliProbeFailureDetail) {
|
|
37
|
+
super('CLI probe failed');
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
type CliProbeOutcome =
|
|
42
|
+
| { result: CliProbeResult }
|
|
43
|
+
| { failure: CliProbeFailureDetail | null };
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Environment facts are injected; this module performs no I/O. A probe may
|
|
47
|
+
* throw when its fact cannot be collected. Preflight catches that boundary and
|
|
48
|
+
* emits `probe_failed` (or `command_unprovable` for a deterministic command).
|
|
49
|
+
*/
|
|
50
|
+
export interface PreflightProbes {
|
|
51
|
+
/**
|
|
52
|
+
* Resolve relative paths against the file implied by `source`, then probe
|
|
53
|
+
* `auth status`. When the step declared a `model`, the probe runs with that
|
|
54
|
+
* model in scope, so readiness answers "can this CLI use THIS model" rather
|
|
55
|
+
* than the weaker "is this CLI authenticated at all".
|
|
56
|
+
*/
|
|
57
|
+
cli(cli: string, source: CliResolutionSource, model?: string): CliProbeResult;
|
|
58
|
+
executor(trigger: TriggerSpec): boolean;
|
|
59
|
+
command(binary: string): boolean;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface PreflightOptions {
|
|
63
|
+
projectCli?: string;
|
|
64
|
+
projectConfigPath?: string;
|
|
65
|
+
projectSearchStart?: string;
|
|
66
|
+
/** Exact, project-owned model allowlist from the nearest flows.json. */
|
|
67
|
+
models?: readonly string[];
|
|
68
|
+
modelRegistryPath?: string;
|
|
69
|
+
probes: PreflightProbes;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface PreflightRefusal {
|
|
73
|
+
severity: 'refusal';
|
|
74
|
+
kind: PreflightFailureKind;
|
|
75
|
+
message: string;
|
|
76
|
+
stepId?: string;
|
|
77
|
+
cli?: string;
|
|
78
|
+
agent?: string;
|
|
79
|
+
model?: string;
|
|
80
|
+
triggerId?: string;
|
|
81
|
+
executor?: string;
|
|
82
|
+
detail?: CliProbeFailureDetail;
|
|
83
|
+
/** Author-facing validation errors when kind is `invalid_spec`. */
|
|
84
|
+
errors?: string[];
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export interface PreflightWarning {
|
|
88
|
+
severity: 'warning';
|
|
89
|
+
kind: PreflightWarningKind;
|
|
90
|
+
message: string;
|
|
91
|
+
stepId?: string;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export type PreflightDiagnostic = PreflightRefusal | PreflightWarning;
|
|
95
|
+
|
|
96
|
+
export interface PreflightResult {
|
|
97
|
+
ok: boolean;
|
|
98
|
+
gates: StepGateInspection[];
|
|
99
|
+
resolutions: CliResolution[];
|
|
100
|
+
diagnostics: PreflightDiagnostic[];
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function preflight(flow: FlowSpec, options: PreflightOptions): PreflightResult {
|
|
104
|
+
// Compile before touching any environment fact. `compileSpec` snapshots raw
|
|
105
|
+
// input into inert data, validates it against the closed authoring schema,
|
|
106
|
+
// and lowers `output` sugar into its json_schema gate — so the gate plan
|
|
107
|
+
// below describes what the kernel will actually judge, and no probe or gate
|
|
108
|
+
// inspection ever reads a live accessor. The failure is a named refusal
|
|
109
|
+
// rather than a thrown error (RFC covenant 2), which is the contract main
|
|
110
|
+
// settled for this boundary.
|
|
111
|
+
let compiled: FlowSpec;
|
|
112
|
+
try {
|
|
113
|
+
compiled = compileSpec(flow);
|
|
114
|
+
} catch (error) {
|
|
115
|
+
const errors = error instanceof CompileError
|
|
116
|
+
? error.errors
|
|
117
|
+
: [error instanceof Error ? error.message : 'spec: expected JSON-compatible data'];
|
|
118
|
+
return {
|
|
119
|
+
ok: false,
|
|
120
|
+
gates: [],
|
|
121
|
+
resolutions: [],
|
|
122
|
+
diagnostics: [{
|
|
123
|
+
severity: 'refusal',
|
|
124
|
+
kind: 'invalid_spec',
|
|
125
|
+
message: `Relayflow spec is invalid: ${errors.join('; ')}`,
|
|
126
|
+
errors,
|
|
127
|
+
}],
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
const diagnostics: PreflightDiagnostic[] = [];
|
|
131
|
+
const resolutions: CliResolution[] = [];
|
|
132
|
+
const resolutionByStep = new Map<string, CliResolution>();
|
|
133
|
+
const cliProbeResults = new Map<string, CliProbeOutcome>();
|
|
134
|
+
|
|
135
|
+
diagnostics.push(...unknownModelDiagnostics(compiled, options));
|
|
136
|
+
// Resolve the complete flow before touching any environment fact. A later
|
|
137
|
+
// statically unresolved CLI makes the whole submission impossible, so no
|
|
138
|
+
// earlier command, provider/model, or trigger probe may run first.
|
|
139
|
+
for (const step of compiled.steps) {
|
|
140
|
+
if (step.type === 'deterministic') continue;
|
|
141
|
+
const resolution = resolveCli(step, compiled, options.projectCli);
|
|
142
|
+
if (resolution === undefined) {
|
|
143
|
+
diagnostics.push({
|
|
144
|
+
severity: 'refusal',
|
|
145
|
+
kind: 'cli_unresolved',
|
|
146
|
+
stepId: step.id,
|
|
147
|
+
message: unresolvedCliMessage(step.id, options),
|
|
148
|
+
});
|
|
149
|
+
} else {
|
|
150
|
+
resolutions.push(resolution);
|
|
151
|
+
resolutionByStep.set(step.id, resolution);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
if (diagnostics.length > 0) {
|
|
155
|
+
return { ok: false, gates: compiled.steps.map(inspectStepGate), resolutions, diagnostics };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
for (const step of compiled.steps) {
|
|
159
|
+
warnOnVacuousGate(step, diagnostics);
|
|
160
|
+
warnOnUnprovableEffects(step, options.probes, diagnostics);
|
|
161
|
+
if (step.type === 'deterministic') continue;
|
|
162
|
+
const resolution = resolutionByStep.get(step.id)!;
|
|
163
|
+
probeResolvedCli(resolution, options.probes, cliProbeResults, diagnostics);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
for (const trigger of compiled.triggers ?? []) {
|
|
167
|
+
probeTrigger(trigger, options.probes, diagnostics);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return {
|
|
171
|
+
ok: !diagnostics.some((diagnostic) => diagnostic.severity === 'refusal'),
|
|
172
|
+
gates: compiled.steps.map(inspectStepGate),
|
|
173
|
+
resolutions,
|
|
174
|
+
diagnostics,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** Pure authoring validation: no executable, command, trigger, or daemon probe. */
|
|
179
|
+
function unknownModelDiagnostics(
|
|
180
|
+
flow: FlowSpec,
|
|
181
|
+
options: PreflightOptions,
|
|
182
|
+
): PreflightRefusal[] {
|
|
183
|
+
const diagnostics: PreflightRefusal[] = [];
|
|
184
|
+
|
|
185
|
+
// Named declarations remain in the normalized authoring object until this
|
|
186
|
+
// boundary so even unused or step-shadowed models are checked. toKernelSpec
|
|
187
|
+
// erases the map and selector only after this pass has had a chance to fail.
|
|
188
|
+
for (const [agent, declaration] of Object.entries(flow.agents ?? {})) {
|
|
189
|
+
if (isKnownModel(declaration.model, options.models)) continue;
|
|
190
|
+
diagnostics.push({
|
|
191
|
+
severity: 'refusal',
|
|
192
|
+
kind: 'model_unknown',
|
|
193
|
+
agent,
|
|
194
|
+
cli: declaration.cli,
|
|
195
|
+
model: declaration.model,
|
|
196
|
+
message: unknownNamedAgentModelMessage(agent, declaration.cli, declaration.model, options.modelRegistryPath),
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
for (const step of flow.steps) {
|
|
201
|
+
if (step.type === 'deterministic' || step.model === undefined) continue;
|
|
202
|
+
if (isKnownModel(step.model, options.models)) continue;
|
|
203
|
+
const resolution = resolveCli(step, flow, options.projectCli);
|
|
204
|
+
diagnostics.push({
|
|
205
|
+
severity: 'refusal',
|
|
206
|
+
kind: 'model_unknown',
|
|
207
|
+
stepId: step.id,
|
|
208
|
+
...(resolution === undefined ? {} : { cli: resolution.cli }),
|
|
209
|
+
model: step.model,
|
|
210
|
+
message: unknownModelMessage(step.id, step.model, resolution?.cli, options.modelRegistryPath),
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return diagnostics;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function unknownNamedAgentModelMessage(
|
|
218
|
+
agent: string,
|
|
219
|
+
cli: string,
|
|
220
|
+
model: string,
|
|
221
|
+
registryPath: string | undefined,
|
|
222
|
+
): string {
|
|
223
|
+
const source = registryPath === undefined
|
|
224
|
+
? 'the nearest project config (no model registry was found)'
|
|
225
|
+
: `project model registry "${registryPath}"`;
|
|
226
|
+
return `Named agent "${agent}" declares model "${model}" for CLI "${cli}", but it is not listed in ${source}; add the exact model only after verifying that project is allowed to use it.`;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function isKnownModel(model: string, models: readonly string[] | undefined): boolean {
|
|
230
|
+
return models?.includes(model) === true;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function unknownModelMessage(
|
|
234
|
+
stepId: string,
|
|
235
|
+
model: string,
|
|
236
|
+
cli: string | undefined,
|
|
237
|
+
registryPath: string | undefined,
|
|
238
|
+
): string {
|
|
239
|
+
const source = registryPath === undefined
|
|
240
|
+
? 'the nearest project config (no model registry was found)'
|
|
241
|
+
: `project model registry "${registryPath}"`;
|
|
242
|
+
const cliContext = cli === undefined ? '' : ` for CLI "${cli}"`;
|
|
243
|
+
return `Step "${stepId}" declares model "${model}"${cliContext}, but it is not listed in ${source}; add the exact model only after verifying that project is allowed to use it.`;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function unresolvedCliMessage(stepId: string, options: PreflightOptions): string {
|
|
247
|
+
const context = options.projectConfigPath !== undefined
|
|
248
|
+
? ` Nearest project config "${options.projectConfigPath}" declares no cli; outer configs are shadowed.`
|
|
249
|
+
: options.projectSearchStart !== undefined
|
|
250
|
+
? ` No flows.json was found from "${options.projectSearchStart}" to the filesystem root.`
|
|
251
|
+
: '';
|
|
252
|
+
return `Step "${stepId}" has no CLI at step, flow, or project level.${context}`;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function resolveCli(
|
|
256
|
+
step: Extract<StepSpec, { type: 'llm' | 'agent' }>,
|
|
257
|
+
flow: FlowSpec,
|
|
258
|
+
projectCli: string | undefined,
|
|
259
|
+
): CliResolution | undefined {
|
|
260
|
+
const named = step.type === 'agent' && step.agent !== undefined
|
|
261
|
+
? flow.agents?.[step.agent]
|
|
262
|
+
: undefined;
|
|
263
|
+
// Model comes only from the step or its explicitly selected declaration.
|
|
264
|
+
// There is deliberately no flow/project or host default.
|
|
265
|
+
const effectiveModel = step.model ?? named?.model;
|
|
266
|
+
const model = effectiveModel !== undefined ? { model: effectiveModel } : {};
|
|
267
|
+
if (step.cli !== undefined) return { stepId: step.id, cli: step.cli, source: 'step', ...model };
|
|
268
|
+
if (named !== undefined) return { stepId: step.id, cli: named.cli, source: 'named', ...model };
|
|
269
|
+
if (flow.cli !== undefined) return { stepId: step.id, cli: flow.cli, source: 'flow', ...model };
|
|
270
|
+
if (projectCli !== undefined) return { stepId: step.id, cli: projectCli, source: 'project', ...model };
|
|
271
|
+
return undefined;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function probeResolvedCli(
|
|
275
|
+
resolution: CliResolution,
|
|
276
|
+
probes: PreflightProbes,
|
|
277
|
+
cache: Map<string, CliProbeOutcome>,
|
|
278
|
+
diagnostics: PreflightDiagnostic[],
|
|
279
|
+
): void {
|
|
280
|
+
// Source is load-bearing: the same relative CLI string resolves from the
|
|
281
|
+
// flow directory for step/named/flow declarations and the config directory for
|
|
282
|
+
// project declarations.
|
|
283
|
+
// Model is part of the key: the same CLI probed with two different models
|
|
284
|
+
// is two different questions, and caching on the CLI alone would let a
|
|
285
|
+
// model that the CLI cannot resolve inherit an earlier model's pass.
|
|
286
|
+
const cacheKey = JSON.stringify([resolution.cli, resolution.source, resolution.model ?? null]);
|
|
287
|
+
let outcome = cache.get(cacheKey);
|
|
288
|
+
if (outcome === undefined) {
|
|
289
|
+
try {
|
|
290
|
+
outcome = { result: probes.cli(resolution.cli, resolution.source, resolution.model) };
|
|
291
|
+
} catch (error) {
|
|
292
|
+
const detail = error instanceof CliProbeError ? error.detail : undefined;
|
|
293
|
+
outcome = { failure: detail ?? null };
|
|
294
|
+
}
|
|
295
|
+
cache.set(cacheKey, outcome);
|
|
296
|
+
}
|
|
297
|
+
if ('failure' in outcome) {
|
|
298
|
+
const detail = outcome.failure ?? undefined;
|
|
299
|
+
diagnostics.push({
|
|
300
|
+
severity: 'refusal',
|
|
301
|
+
kind: 'probe_failed',
|
|
302
|
+
stepId: resolution.stepId,
|
|
303
|
+
cli: resolution.cli,
|
|
304
|
+
...(detail !== undefined ? { detail } : {}),
|
|
305
|
+
message: probeFailedMessage(resolution, detail),
|
|
306
|
+
});
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
const { result } = outcome;
|
|
310
|
+
if (!result.exists) {
|
|
311
|
+
diagnostics.push({
|
|
312
|
+
severity: 'refusal',
|
|
313
|
+
kind: 'cli_missing',
|
|
314
|
+
stepId: resolution.stepId,
|
|
315
|
+
cli: resolution.cli,
|
|
316
|
+
message: `Step "${resolution.stepId}" declares CLI "${resolution.cli}", but it does not resolve as an executable.`,
|
|
317
|
+
});
|
|
318
|
+
} else if (result.supported === false) {
|
|
319
|
+
diagnostics.push({
|
|
320
|
+
severity: 'refusal',
|
|
321
|
+
kind: 'cli_unsupported',
|
|
322
|
+
stepId: resolution.stepId,
|
|
323
|
+
cli: resolution.cli,
|
|
324
|
+
message: `Step "${resolution.stepId}" declares CLI "${resolution.cli}", but it is neither a supported raw Claude/Codex executable nor a conforming Relayflows wrapper; custom wrappers must identify with the relayflows-agent-cli-v1 contract.`,
|
|
325
|
+
});
|
|
326
|
+
} else if (!result.authenticated) {
|
|
327
|
+
const command = result.authCommand ?? `${resolution.cli} auth status`;
|
|
328
|
+
diagnostics.push({
|
|
329
|
+
severity: 'refusal',
|
|
330
|
+
kind: 'cli_unauthenticated',
|
|
331
|
+
stepId: resolution.stepId,
|
|
332
|
+
cli: resolution.cli,
|
|
333
|
+
message: `Step "${resolution.stepId}" declares CLI "${resolution.cli}", but "${command}" exited non-zero; authenticate it or repair that adapter's authentication probe.`,
|
|
334
|
+
});
|
|
335
|
+
} else if (resolution.model !== undefined && result.modelAvailable !== true) {
|
|
336
|
+
diagnostics.push({
|
|
337
|
+
severity: 'refusal',
|
|
338
|
+
kind: 'model_unavailable',
|
|
339
|
+
stepId: resolution.stepId,
|
|
340
|
+
cli: resolution.cli,
|
|
341
|
+
model: resolution.model,
|
|
342
|
+
message: `Step "${resolution.stepId}" declares model "${resolution.model}" for CLI "${resolution.cli}", but its model-scoped "${result.modelCommand ?? `${resolution.cli} auth status`}" probe exited non-zero; verify the model name and this credential's access.`,
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function probeFailedMessage(
|
|
348
|
+
resolution: CliResolution,
|
|
349
|
+
detail?: CliProbeFailureDetail,
|
|
350
|
+
): string {
|
|
351
|
+
const prefix = `Could not verify CLI "${resolution.cli}" for step "${resolution.stepId}"`;
|
|
352
|
+
if (detail === undefined) return `${prefix}.`;
|
|
353
|
+
if (detail === 'spawn_failed') return `${prefix}: the probe process could not be started.`;
|
|
354
|
+
if (detail.startsWith('signal:')) {
|
|
355
|
+
return `${prefix}: the probe was terminated by signal "${detail.slice('signal:'.length)}".`;
|
|
356
|
+
}
|
|
357
|
+
return `${prefix}: the probe timed out after ${detail.slice('timeout:'.length)}.`;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function probeTrigger(
|
|
361
|
+
trigger: TriggerSpec,
|
|
362
|
+
probes: PreflightProbes,
|
|
363
|
+
diagnostics: PreflightDiagnostic[],
|
|
364
|
+
): void {
|
|
365
|
+
let registered: boolean;
|
|
366
|
+
try {
|
|
367
|
+
registered = probes.executor(trigger);
|
|
368
|
+
} catch {
|
|
369
|
+
diagnostics.push({
|
|
370
|
+
severity: 'refusal',
|
|
371
|
+
kind: 'probe_failed',
|
|
372
|
+
triggerId: trigger.id,
|
|
373
|
+
executor: trigger.executor,
|
|
374
|
+
message: `Could not verify executor "${trigger.executor}" for trigger "${trigger.id}".`,
|
|
375
|
+
});
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
if (!registered) {
|
|
379
|
+
diagnostics.push({
|
|
380
|
+
severity: 'refusal',
|
|
381
|
+
kind: 'no_executor',
|
|
382
|
+
triggerId: trigger.id,
|
|
383
|
+
executor: trigger.executor,
|
|
384
|
+
message: `Trigger "${trigger.id}" has no registered executor "${trigger.executor}".`,
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* A deterministic step is never silently accepted. An unresolved bare command
|
|
391
|
+
* may still be a shell builtin, function, or assignment, so it warns. A command
|
|
392
|
+
* containing `/` names a path rather than relying on shell resolution, so a
|
|
393
|
+
* failed existence probe refuses the flow.
|
|
394
|
+
*/
|
|
395
|
+
/**
|
|
396
|
+
* A declared `json_schema` gate that accepts every output is legal and stays
|
|
397
|
+
* legal — but it is indistinguishable in the gate plan from one that judges
|
|
398
|
+
* something, which is exactly the confusion AGENTS.md's "never edit a gate that
|
|
399
|
+
* judges your own work" rail exists to prevent.
|
|
400
|
+
*/
|
|
401
|
+
function warnOnVacuousGate(step: StepSpec, diagnostics: PreflightDiagnostic[]): void {
|
|
402
|
+
if (step.verification?.type !== 'json_schema') return;
|
|
403
|
+
if (!acceptsAnyOutput(step.verification.schema)) return;
|
|
404
|
+
diagnostics.push({
|
|
405
|
+
severity: 'warning',
|
|
406
|
+
kind: 'vacuous_gate',
|
|
407
|
+
stepId: step.id,
|
|
408
|
+
message: `Step "${step.id}" declares a json_schema gate that accepts every possible output, so it judges nothing.`,
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function warnOnUnprovableEffects(
|
|
413
|
+
step: StepSpec,
|
|
414
|
+
probes: PreflightProbes,
|
|
415
|
+
diagnostics: PreflightDiagnostic[],
|
|
416
|
+
): void {
|
|
417
|
+
if (step.type !== 'deterministic') return;
|
|
418
|
+
const binary = firstCommandWord(step.command);
|
|
419
|
+
if (binary === undefined) {
|
|
420
|
+
diagnostics.push({
|
|
421
|
+
severity: 'warning',
|
|
422
|
+
kind: 'command_unprovable',
|
|
423
|
+
stepId: step.id,
|
|
424
|
+
message: `Step "${step.id}" has no command word to check, so nothing about it can be proven before execution.`,
|
|
425
|
+
});
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
let exists: boolean;
|
|
429
|
+
try {
|
|
430
|
+
exists = probes.command(binary);
|
|
431
|
+
} catch {
|
|
432
|
+
diagnostics.push({
|
|
433
|
+
severity: 'warning',
|
|
434
|
+
kind: 'command_unprovable',
|
|
435
|
+
stepId: step.id,
|
|
436
|
+
message: `Step "${step.id}" command "${binary}" could not be probed, so its presence is unproven before execution.`,
|
|
437
|
+
});
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
diagnostics.push(exists
|
|
441
|
+
? {
|
|
442
|
+
severity: 'warning',
|
|
443
|
+
kind: 'unprovable_effects',
|
|
444
|
+
stepId: step.id,
|
|
445
|
+
message: `Step "${step.id}" command "${binary}" resolves, but its effects cannot be proven before execution.`,
|
|
446
|
+
}
|
|
447
|
+
: binary.includes('/')
|
|
448
|
+
? {
|
|
449
|
+
severity: 'refusal',
|
|
450
|
+
kind: 'command_missing',
|
|
451
|
+
stepId: step.id,
|
|
452
|
+
message: `Step "${step.id}" command path "${binary}" does not exist.`,
|
|
453
|
+
}
|
|
454
|
+
: {
|
|
455
|
+
severity: 'warning',
|
|
456
|
+
kind: 'command_unresolved',
|
|
457
|
+
stepId: step.id,
|
|
458
|
+
message: `Step "${step.id}" command "${binary}" does not resolve as an executable; it runs only if the shell supplies it.`,
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
function firstCommandWord(command: string): string | undefined {
|
|
463
|
+
// Skip the shell prefixes that can legally precede the command word.
|
|
464
|
+
//
|
|
465
|
+
// Review caught this on PR #47: the new path-like refusal keys on the first
|
|
466
|
+
// word containing a slash, and `TMPDIR=/tmp printf ok`, `>/tmp/out echo hi`
|
|
467
|
+
// and `PATH=/usr/bin:$PATH mkdir x` all have one — but none of them names a
|
|
468
|
+
// path to execute. All three are valid and were being refused outright,
|
|
469
|
+
// which is exactly the "refusing would reject valid flows" failure the warn
|
|
470
|
+
// behaviour exists to avoid.
|
|
471
|
+
//
|
|
472
|
+
// An assignment is NAME=value with a shell-legal name; a redirection starts
|
|
473
|
+
// with < or > (optionally with a leading fd number). Neither is the command.
|
|
474
|
+
let rest = command.trim();
|
|
475
|
+
for (;;) {
|
|
476
|
+
const prefix = rest.match(/^(?:[A-Za-z_][A-Za-z0-9_]*=(?:"[^"]*"|'[^']*'|[^\s]*)|[0-9]*[<>]{1,2}\s*[^\s]+)\s+/);
|
|
477
|
+
if (prefix === null) break;
|
|
478
|
+
rest = rest.slice(prefix[0].length);
|
|
479
|
+
}
|
|
480
|
+
const match = rest.match(/^(?:"([^"]+)"|'([^']+)'|([^\s]+))/);
|
|
481
|
+
return match?.[1] ?? match?.[2] ?? match?.[3];
|
|
482
|
+
}
|