@relayflows/sdk 2.0.56 → 2.0.58
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/adapters/base.d.ts +2 -0
- package/dist/adapters/base.d.ts.map +1 -1
- package/dist/adapters/devin.d.ts +4 -0
- package/dist/adapters/devin.d.ts.map +1 -0
- package/dist/adapters/devin.js +20 -0
- package/dist/adapters/devin.js.map +1 -0
- package/dist/adapters/index.d.ts +3 -2
- package/dist/adapters/index.d.ts.map +1 -1
- package/dist/adapters/index.js +5 -1
- package/dist/adapters/index.js.map +1 -1
- package/dist/budget-preflight.d.ts.map +1 -1
- package/dist/budget-preflight.js +8 -2
- package/dist/budget-preflight.js.map +1 -1
- package/dist/cli/cli-probe.d.ts.map +1 -1
- package/dist/cli/cli-probe.js +72 -11
- package/dist/cli/cli-probe.js.map +1 -1
- package/dist/communication/environment.d.ts.map +1 -1
- package/dist/communication/environment.js +1 -0
- package/dist/communication/environment.js.map +1 -1
- package/dist/communication/pty.d.ts.map +1 -1
- package/dist/communication/pty.js +6 -2
- package/dist/communication/pty.js.map +1 -1
- package/dist/communication/skills.d.ts.map +1 -1
- package/dist/communication/skills.js +2 -1
- package/dist/communication/skills.js.map +1 -1
- package/dist/devin-runtime.d.ts +10 -0
- package/dist/devin-runtime.d.ts.map +1 -0
- package/dist/devin-runtime.js +37 -0
- package/dist/devin-runtime.js.map +1 -0
- package/dist/preflight.d.ts +1 -1
- package/dist/preflight.d.ts.map +1 -1
- package/dist/preflight.js +12 -3
- package/dist/preflight.js.map +1 -1
- package/dist/step-secrets.d.ts.map +1 -1
- package/dist/step-secrets.js +1 -0
- package/dist/step-secrets.js.map +1 -1
- package/dist/worker-cli.d.ts.map +1 -1
- package/dist/worker-cli.js +15 -3
- package/dist/worker-cli.js.map +1 -1
- package/package.json +2 -2
- package/src/adapters/base.ts +2 -0
- package/src/adapters/devin.ts +22 -0
- package/src/adapters/index.ts +5 -2
- package/src/budget-preflight.ts +8 -2
- package/src/cli/cli-probe.ts +65 -11
- package/src/communication/environment.ts +1 -0
- package/src/communication/pty.ts +7 -3
- package/src/communication/skills.ts +2 -1
- package/src/devin-runtime.ts +33 -0
- package/src/preflight.ts +12 -3
- package/src/step-secrets.ts +1 -0
- package/src/worker-cli.ts +13 -5
package/src/cli/cli-probe.ts
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { accessSync, constants, realpathSync } from 'node:fs';
|
|
2
2
|
import { basename, isAbsolute, resolve } from 'node:path';
|
|
3
3
|
import { execFile, spawnSync } from 'node:child_process';
|
|
4
|
+
import { redactText } from '../agent-transcript.js';
|
|
4
5
|
import { agentEnvironment, brokerEnvironment } from '../communication/environment.js';
|
|
5
6
|
import { adapterIdentification, authenticationProbe, classifyModelProbeFailure, cliAdapterKind,
|
|
6
7
|
displayInvocation, modelReadinessProbe, type CliInvocation } from '../cli-adapter.js';
|
|
7
8
|
import { MODEL_ENV } from '../worker-cli.js';
|
|
8
9
|
import { CliProbeError, type CliProbeResult } from '../preflight.js';
|
|
9
10
|
import { pinCliAlias, pinCliAliasSync } from './pinned-cli-alias.js';
|
|
11
|
+
import { prepareDevinRuntime } from '../devin-runtime.js';
|
|
10
12
|
import { agentProbeEnvironment } from '../step-secrets.js';
|
|
11
13
|
|
|
12
14
|
interface ProbeRequest {
|
|
@@ -40,15 +42,18 @@ function driveSync<T>(sequence: Generator<ProbeRequest, T, ProbeOutput>): T {
|
|
|
40
42
|
while (!next.done) {
|
|
41
43
|
const request = next.value;
|
|
42
44
|
const pinned = pinCliAliasSync(request.executable, request.argv0 ?? basename(request.executable));
|
|
45
|
+
let prepared: ReturnType<typeof prepareProbe> | undefined;
|
|
43
46
|
const result = (() => {
|
|
44
47
|
try {
|
|
45
|
-
|
|
46
|
-
|
|
48
|
+
prepared = prepareProbe(request);
|
|
49
|
+
return spawnSync(pinned.executable, prepared.args, {
|
|
50
|
+
...probeOptions({ ...request, environment: prepared.env }),
|
|
47
51
|
// Preserve the old synchronous probe contract: provider CLIs must not
|
|
48
52
|
// inherit a readable stdin that can block auth/identify probes.
|
|
49
53
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
50
54
|
});
|
|
51
55
|
} finally {
|
|
56
|
+
prepared?.close();
|
|
52
57
|
pinned.release();
|
|
53
58
|
}
|
|
54
59
|
})();
|
|
@@ -69,9 +74,12 @@ function probeOptions({ argv0, directory, invocation, environment }: ProbeReques
|
|
|
69
74
|
|
|
70
75
|
async function runProbeAsync(request: ProbeRequest): Promise<ProbeOutput> {
|
|
71
76
|
const pinned = await pinCliAlias(request.executable, request.argv0 ?? basename(request.executable));
|
|
77
|
+
let prepared: ReturnType<typeof prepareProbe> | undefined;
|
|
72
78
|
try {
|
|
79
|
+
prepared = prepareProbe(request);
|
|
80
|
+
const current = prepared;
|
|
73
81
|
return await new Promise((resolve, reject) => {
|
|
74
|
-
const child = execFile(pinned.executable,
|
|
82
|
+
const child = execFile(pinned.executable, current.args, probeOptions({ ...request, environment: current.env }),
|
|
75
83
|
(error, stdout, stderr) => {
|
|
76
84
|
// Numeric exit codes are probe results; launch, timeout, signal and buffer
|
|
77
85
|
// errors are failures to collect a fact, just as in the synchronous driver.
|
|
@@ -86,6 +94,7 @@ async function runProbeAsync(request: ProbeRequest): Promise<ProbeOutput> {
|
|
|
86
94
|
child.stdin?.end();
|
|
87
95
|
});
|
|
88
96
|
} finally {
|
|
97
|
+
prepared?.close();
|
|
89
98
|
await pinned.release();
|
|
90
99
|
}
|
|
91
100
|
}
|
|
@@ -133,12 +142,16 @@ function* probeSequence(
|
|
|
133
142
|
const auth = authenticationProbe(kind);
|
|
134
143
|
const authCommand = displayInvocation(cli, auth);
|
|
135
144
|
if (model === undefined) {
|
|
145
|
+
const authProbe = yield probe(auth);
|
|
146
|
+
const authenticated = authProbe.status === 0;
|
|
136
147
|
return {
|
|
137
148
|
exists: true,
|
|
138
149
|
supported: true,
|
|
139
|
-
authenticated
|
|
150
|
+
authenticated,
|
|
140
151
|
executable,
|
|
141
152
|
authCommand,
|
|
153
|
+
...(authenticated ? {} : { authExitCode: authProbe.status,
|
|
154
|
+
authFailureDetail: redactProbeOutput(`${authProbe.stderr}${authProbe.stdout}`, sourceEnvironment).trim().slice(0, 500) }),
|
|
142
155
|
};
|
|
143
156
|
}
|
|
144
157
|
|
|
@@ -168,7 +181,7 @@ function* probeSequence(
|
|
|
168
181
|
// because Claude Code prints the provider's API error there and its own
|
|
169
182
|
// catalog warnings on stderr.
|
|
170
183
|
const failure = classifyModelProbeFailure(kind, `${scopedProbe.stdout}\n${scopedProbe.stderr}`);
|
|
171
|
-
const modelDetail = redactProbeOutput(scopedProbe.stdout.trim() || scopedProbe.stderr.trim()).slice(0, 500);
|
|
184
|
+
const modelDetail = redactProbeOutput(scopedProbe.stdout.trim() || scopedProbe.stderr.trim(), sourceEnvironment).slice(0, 500);
|
|
172
185
|
return {
|
|
173
186
|
exists: true,
|
|
174
187
|
supported: true,
|
|
@@ -187,7 +200,7 @@ function* probeSequence(
|
|
|
187
200
|
: {
|
|
188
201
|
authExitCode: authProbe.status,
|
|
189
202
|
authFailureDetail: redactProbeOutput(
|
|
190
|
-
`${authProbe.stderr}${authProbe.stdout}`,
|
|
203
|
+
`${authProbe.stderr}${authProbe.stdout}`, sourceEnvironment,
|
|
191
204
|
).trim().slice(0, 500),
|
|
192
205
|
}),
|
|
193
206
|
};
|
|
@@ -201,11 +214,42 @@ function* probeSequence(
|
|
|
201
214
|
* say WHY a probe failed, which survives redaction; leaking an identity into a
|
|
202
215
|
* refusal message that gets pasted into issues does not.
|
|
203
216
|
*/
|
|
204
|
-
function redactProbeOutput(text: string): string {
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
217
|
+
function redactProbeOutput(text: string, environment: NodeJS.ProcessEnv): string {
|
|
218
|
+
const key = environment.DEVIN_API_KEY?.trim();
|
|
219
|
+
const normalized = { ...environment, ...(key === undefined ? {} : { DEVIN_API_KEY: key }) };
|
|
220
|
+
// Preserve the shared redactor's known-value, header and cookie handling.
|
|
221
|
+
const redacted = redactText(text, normalized);
|
|
222
|
+
// One byte per diagnostic character bounds auxiliary memory even when a
|
|
223
|
+
// one-character credential matches the entire probe buffer. No span lists.
|
|
224
|
+
const covered = new Uint8Array(redacted.length);
|
|
225
|
+
const replacements = ['', '<redacted-email>', '<redacted-token>', '<redacted-hex>', '[redacted:DEVIN_API_KEY]'];
|
|
226
|
+
for (const [pattern, tag] of [
|
|
227
|
+
[/(?<![A-Za-z0-9._%+-])[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}/g, 1],
|
|
228
|
+
[/\b(sk|pk|oat|rt)[-_][A-Za-z0-9._-]{8,}/gi, 2],
|
|
229
|
+
[/\b[A-Fa-f0-9]{32,}\b/g, 3],
|
|
230
|
+
] as const) {
|
|
231
|
+
for (const match of redacted.matchAll(pattern)) covered.fill(tag, match.index, match.index + match[0].length);
|
|
232
|
+
}
|
|
233
|
+
// Mark short credentials AFTER the shapes, on the same text: replacing
|
|
234
|
+
// either first can split the other and leak a suffix. Connected marks are
|
|
235
|
+
// redacted together; a known credential's marker takes precedence.
|
|
236
|
+
if (key && key.length < 8) {
|
|
237
|
+
for (let start = redacted.indexOf(key); start !== -1; start = redacted.indexOf(key, start + 1)) {
|
|
238
|
+
covered.fill(4, start, start + key.length);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
let result = ''; let cursor = 0;
|
|
242
|
+
for (let start = 0; start < covered.length;) {
|
|
243
|
+
if (covered[start] === 0) { start++; continue; }
|
|
244
|
+
let tag = covered[start]!; let end = start + 1;
|
|
245
|
+
while (end < covered.length && covered[end] !== 0) {
|
|
246
|
+
if (covered[end] === 4) tag = 4;
|
|
247
|
+
end++;
|
|
248
|
+
}
|
|
249
|
+
result += redacted.slice(cursor, start) + replacements[tag];
|
|
250
|
+
cursor = end; start = end;
|
|
251
|
+
}
|
|
252
|
+
return result + redacted.slice(cursor);
|
|
209
253
|
}
|
|
210
254
|
|
|
211
255
|
function* executableSequence(command: string, directory: string, environment: NodeJS.ProcessEnv): Generator<ProbeRequest, string | undefined, ProbeOutput> {
|
|
@@ -245,3 +289,13 @@ function classifySpawnFailure(
|
|
|
245
289
|
}
|
|
246
290
|
return signal === null ? undefined : new CliProbeError(`signal:${signal}`);
|
|
247
291
|
}
|
|
292
|
+
|
|
293
|
+
function prepareProbe(request: ProbeRequest) {
|
|
294
|
+
if (cliAdapterKind(request.argv0 ?? request.executable) === 'devin'
|
|
295
|
+
&& request.environment.DEVIN_API_KEY !== undefined && !request.environment.DEVIN_API_KEY.trim()) {
|
|
296
|
+
throw new CliProbeError('devin_api_key_empty');
|
|
297
|
+
}
|
|
298
|
+
return cliAdapterKind(request.argv0 ?? request.executable) === 'devin'
|
|
299
|
+
? prepareDevinRuntime(request.environment, request.invocation)
|
|
300
|
+
: { args: request.invocation.args, env: request.environment, close() {} };
|
|
301
|
+
}
|
|
@@ -25,6 +25,7 @@ const providerNames: Readonly<Record<string, readonly string[]>> = {
|
|
|
25
25
|
// AgentWorkforce/cloud#4026's house-provider run contract gives Grok an
|
|
26
26
|
// isolated home plus an xAI-compatible base URL. These remain Grok-scoped.
|
|
27
27
|
grok: ['XAI_API_KEY', 'GROK_XAI_API_BASE_URL', 'GROK_HOME'],
|
|
28
|
+
devin: ['DEVIN_API_KEY', 'DEVIN_MODEL'],
|
|
28
29
|
...Object.fromEntries(['opencode', 'aider', 'goose', 'pi', 'deepagents'].map(cli => [cli, multiProvider])),
|
|
29
30
|
};
|
|
30
31
|
/** @internal Names-only test seam; credential lists remain private. */
|
package/src/communication/pty.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { mkdir, mkdtemp, rm, symlink } from 'node:fs/promises';
|
|
2
2
|
import { basename, join } from 'node:path';
|
|
3
|
+
import { prepareDevinRuntime } from '../devin-runtime.js';
|
|
3
4
|
import { agentEnvironment } from './environment.js';
|
|
4
5
|
import { resolveCliModel } from '../cli-adapter.js';
|
|
5
6
|
import type { RelayBroker } from './relay.js';
|
|
@@ -19,6 +20,9 @@ export async function spawnFlowPty(broker: Pick<RelayBroker, 'spawnPty'>, option
|
|
|
19
20
|
const argv0 = basename(identity);
|
|
20
21
|
let command = options.cli;
|
|
21
22
|
let link: string | undefined;
|
|
23
|
+
const environment = agentEnvironment(identity, options.environment ?? process.env,
|
|
24
|
+
options.secrets, options.scopeSecrets ?? true);
|
|
25
|
+
const prepared = argv0.replace(/\.exe$/i, '') === 'devin' ? prepareDevinRuntime(environment) : undefined;
|
|
22
26
|
try {
|
|
23
27
|
if (basename(command) !== argv0) {
|
|
24
28
|
const root = join(options.dataDir, 'communication');
|
|
@@ -32,15 +36,15 @@ export async function spawnFlowPty(broker: Pick<RelayBroker, 'spawnPty'>, option
|
|
|
32
36
|
...(options.team === undefined ? {} : { team: options.team }),
|
|
33
37
|
model: resolveCliModel(identity, options.model), cwd: options.cwd,
|
|
34
38
|
harnessConfig: { runtime: 'pty', command: shellQuote(command), args: [], cwd: options.cwd,
|
|
35
|
-
env: { ...
|
|
36
|
-
options.secrets, options.scopeSecrets ?? true),
|
|
39
|
+
env: { ...(prepared?.env ?? environment),
|
|
37
40
|
RELAYFLOW_COMMUNICATION_SOCKET: options.socket, RELAYFLOW_COMMUNICATION_TOKEN: options.token,
|
|
38
41
|
...(options.metadata === undefined ? {} : {
|
|
39
42
|
RELAYFLOW_RUN_ID: options.metadata.runId, RELAYFLOW_FLOW_NAME: options.metadata.flow,
|
|
40
43
|
RELAYFLOW_STEP_NAME: options.metadata.step,
|
|
41
44
|
}) }, delivery: { mode: 'pty-injection', format: 'relay-block' } } });
|
|
42
|
-
return { handle, close: async () => { if (link) await rm(link, { recursive: true, force: true }); } };
|
|
45
|
+
return { handle, close: async () => { prepared?.close(); if (link) await rm(link, { recursive: true, force: true }); } };
|
|
43
46
|
} catch (error) {
|
|
47
|
+
prepared?.close();
|
|
44
48
|
if (link) await rm(link, { recursive: true, force: true });
|
|
45
49
|
throw error;
|
|
46
50
|
}
|
|
@@ -7,8 +7,9 @@ const execute = promisify(execFile);
|
|
|
7
7
|
export async function installBrokerSkills(skills: string[], cli: string, cwd: string, signal: AbortSignal): Promise<void> {
|
|
8
8
|
if (!skills.length) return;
|
|
9
9
|
const identity = basename(cli).replace(/\.exe$/i, '');
|
|
10
|
+
// Devin reads .windsurf/skills, which the canonical skills installer supports.
|
|
10
11
|
const harness = ({ claude: 'claude-code', codex: 'codex', gemini: 'gemini-cli',
|
|
11
|
-
'cursor-agent': 'cursor', opencode: 'opencode' } as Record<string, string>)[identity];
|
|
12
|
+
'cursor-agent': 'cursor', opencode: 'opencode', devin: 'windsurf' } as Record<string, string>)[identity];
|
|
12
13
|
if (!harness) throw new Error(`Skill installation is unsupported for broker CLI ${identity}`);
|
|
13
14
|
await execute('npx', ['--yes', 'skills', 'add', 'https://github.com/AgentWorkforce/skills',
|
|
14
15
|
'--skill', ...skills, '--agent', harness, '--yes', '--copy'],
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { tmpdir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import type { CliInvocation } from './adapters/base.js';
|
|
5
|
+
|
|
6
|
+
/** Private attempt/probe state, never the user's interactive Devin login. */
|
|
7
|
+
export function prepareDevinRuntime(environment: NodeJS.ProcessEnv, invocation?: CliInvocation) {
|
|
8
|
+
let root: string | undefined;
|
|
9
|
+
const env = { ...environment };
|
|
10
|
+
const args = [...(invocation?.args ?? [])];
|
|
11
|
+
const directory = () => root ??= mkdtempSync(join(environment.TMPDIR ?? tmpdir(), 'relayflows-devin-'));
|
|
12
|
+
const close = () => { if (root !== undefined) rmSync(root, { recursive: true, force: true }); };
|
|
13
|
+
try {
|
|
14
|
+
if (env.DEVIN_API_KEY !== undefined) {
|
|
15
|
+
const key = env.DEVIN_API_KEY.trim();
|
|
16
|
+
if (!key) throw new Error('DEVIN_API_KEY must not be empty');
|
|
17
|
+
const data = join(directory(), 'data');
|
|
18
|
+
mkdirSync(join(data, 'devin'), { recursive: true, mode: 0o700 });
|
|
19
|
+
writeFileSync(join(data, 'devin', 'credentials.toml'),
|
|
20
|
+
`windsurf_api_key = ${JSON.stringify(key)}\n`, { mode: 0o600 });
|
|
21
|
+
env.XDG_DATA_HOME = data;
|
|
22
|
+
env.XDG_CONFIG_HOME = join(directory(), 'config');
|
|
23
|
+
env.XDG_CACHE_HOME = join(directory(), 'cache');
|
|
24
|
+
delete env.DEVIN_API_KEY;
|
|
25
|
+
}
|
|
26
|
+
if (invocation?.promptFile !== undefined) {
|
|
27
|
+
const path = join(directory(), 'prompt.txt');
|
|
28
|
+
writeFileSync(path, invocation.promptFile, { mode: 0o600 });
|
|
29
|
+
args.push('--prompt-file', path);
|
|
30
|
+
}
|
|
31
|
+
return { env, args, close };
|
|
32
|
+
} catch (error) { close(); throw error; }
|
|
33
|
+
}
|
package/src/preflight.ts
CHANGED
|
@@ -11,7 +11,7 @@ import type { TriggerSource } from '@relayflows/surface';
|
|
|
11
11
|
import { acceptsAnyOutput, inspectStepGate, type StepGateInspection } from './gate-contract.js';
|
|
12
12
|
import { compileSpec, CompileError } from './compile.js';
|
|
13
13
|
import { helperCall } from './yaml-helpers.js';
|
|
14
|
-
import { resolveCliModelSelection, type CliModelSource } from './cli-adapter.js';
|
|
14
|
+
import { cliAdapterKind, resolveCliModelSelection, type CliModelSource } from './cli-adapter.js';
|
|
15
15
|
import { isNamedGate, NAMED_GATE_FAILURE_KINDS, type NamedGateFailureKind } from './named-gates.js';
|
|
16
16
|
import { namedGateScanCoverageDiagnostics } from './named-gate-preflight.js';
|
|
17
17
|
import { compileScopes, type ScopeInput, type MountRegistry } from './scope-compiler.js';
|
|
@@ -65,6 +65,7 @@ export interface CliProbeResult {
|
|
|
65
65
|
|
|
66
66
|
export type CliProbeFailureDetail =
|
|
67
67
|
| 'spawn_failed'
|
|
68
|
+
| 'devin_api_key_empty'
|
|
68
69
|
| `timeout:${number}ms`
|
|
69
70
|
| `signal:${string}`;
|
|
70
71
|
|
|
@@ -286,6 +287,10 @@ export function resolvePreflight(flow: unknown, options: ResolutionOptions): Pre
|
|
|
286
287
|
message: unresolvedCliMessage(step.id, options),
|
|
287
288
|
});
|
|
288
289
|
} else {
|
|
290
|
+
if (step.type === 'llm' && cliAdapterKind(resolution.cli) === 'devin') {
|
|
291
|
+
cliResolutionDiagnostics.push({ severity: 'refusal', kind: 'cli_unsupported', stepId: step.id,
|
|
292
|
+
cli: resolution.cli, message: `Step "${step.id}" uses Devin for an llm step; Devin supports agent steps only.` });
|
|
293
|
+
}
|
|
289
294
|
resolutions.push(resolution);
|
|
290
295
|
resolutionByStep.set(step.id, resolution);
|
|
291
296
|
}
|
|
@@ -594,13 +599,16 @@ function probeResolvedCli(
|
|
|
594
599
|
kind: 'cli_unsupported',
|
|
595
600
|
stepId: resolution.stepId,
|
|
596
601
|
cli: resolution.cli,
|
|
597
|
-
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.`,
|
|
602
|
+
message: `Step "${resolution.stepId}" declares CLI "${resolution.cli}", but it is neither a supported raw Claude/Codex/Devin executable nor a conforming Relayflows wrapper; custom wrappers must identify with the relayflows-agent-cli-v1 contract.`,
|
|
598
603
|
});
|
|
599
604
|
} else if (managed && result.authenticated === 'unverified') {
|
|
600
605
|
diagnostics.push({ severity: 'warning', kind: 'managed_cli_unverified', stepId: resolution.stepId,
|
|
601
606
|
message: `Step "${resolution.stepId}" uses Relay's interactive CLI transport. The executable exists, but authentication${resolution.model ? ' and access to model "' + resolution.model + '"' : ''} could not be verified before startup. The managed session must execute its task and report completion; startup or task failure fails the step.` });
|
|
602
607
|
} else if (result.authenticated !== true) {
|
|
603
608
|
const command = result.authCommand ?? `${resolution.cli} auth status`;
|
|
609
|
+
const loginHint = cliAdapterKind(resolution.cli) === 'devin'
|
|
610
|
+
? ' For hosted flows, set FLOW_DEVIN_API_KEY to a Devin CLI login token; local runs use DEVIN_API_KEY. A REST API key is a different credential.'
|
|
611
|
+
: '';
|
|
604
612
|
// Say what the probe reported. A refusal that names only the command turns
|
|
605
613
|
// a transient provider rejection and a genuinely unauthenticated CLI into
|
|
606
614
|
// the same message, and the difference decides whether retrying is
|
|
@@ -616,7 +624,7 @@ function probeResolvedCli(
|
|
|
616
624
|
kind: 'cli_unauthenticated',
|
|
617
625
|
stepId: resolution.stepId,
|
|
618
626
|
cli: resolution.cli,
|
|
619
|
-
message: `Step "${resolution.stepId}" declares CLI "${resolution.cli}", but "${command}" exited non-zero${exitCode}; authenticate it or repair that adapter's authentication probe.${detail}`,
|
|
627
|
+
message: `Step "${resolution.stepId}" declares CLI "${resolution.cli}", but "${command}" exited non-zero${exitCode}; authenticate it or repair that adapter's authentication probe.${detail}${loginHint}`,
|
|
620
628
|
});
|
|
621
629
|
} else if (resolution.model !== undefined && result.modelAvailable !== true) {
|
|
622
630
|
diagnostics.push(modelProbeRefusal(resolution as CliResolution & { model: string }, result));
|
|
@@ -668,6 +676,7 @@ function probeFailedMessage(
|
|
|
668
676
|
): string {
|
|
669
677
|
const prefix = `Could not verify CLI "${resolution.cli}" for step "${resolution.stepId}"`;
|
|
670
678
|
if (detail === undefined) return `${prefix}.`;
|
|
679
|
+
if (detail === 'devin_api_key_empty') return `${prefix}: DEVIN_API_KEY must contain a non-empty Devin CLI login token. For hosted flows, set FLOW_DEVIN_API_KEY.`;
|
|
671
680
|
if (detail === 'spawn_failed') return `${prefix}: the probe process could not be started.`;
|
|
672
681
|
if (detail.startsWith('signal:')) {
|
|
673
682
|
return `${prefix}: the probe was terminated by signal "${detail.slice('signal:'.length)}".`;
|
package/src/step-secrets.ts
CHANGED
|
@@ -47,6 +47,7 @@ const CLI_LOGIN: Record<CliAdapterKind, readonly string[]> = {
|
|
|
47
47
|
'AWS_SESSION_TOKEN', 'AWS_REGION', 'AWS_DEFAULT_REGION',
|
|
48
48
|
],
|
|
49
49
|
codex: ['CODEX_HOME', 'OPENAI_API_KEY', 'OPENAI_BASE_URL', 'OPENAI_API_BASE', 'OPENROUTER_API_KEY'],
|
|
50
|
+
devin: ['DEVIN_API_KEY', 'DEVIN_MODEL'],
|
|
50
51
|
'relayflows-wrapper-v1': [],
|
|
51
52
|
};
|
|
52
53
|
|
package/src/worker-cli.ts
CHANGED
|
@@ -45,6 +45,7 @@ import {
|
|
|
45
45
|
type AgentTransport,
|
|
46
46
|
} from './agent-relay-transport.js';
|
|
47
47
|
import { pinCliAlias } from './cli/pinned-cli-alias.js';
|
|
48
|
+
import { prepareDevinRuntime } from './devin-runtime.js';
|
|
48
49
|
import { promptOperand } from './adapters/base.js';
|
|
49
50
|
|
|
50
51
|
export type { CliTransportCause, CliTransportEvidence, CliTransportPhase } from './cli-transport-evidence.js';
|
|
@@ -277,15 +278,22 @@ export async function runAgentCli(
|
|
|
277
278
|
// The flags go before the `--` that ends the task operand (promptOperand):
|
|
278
279
|
// after it they would reach the CLI as task text, not options.
|
|
279
280
|
const args = [...invocation.args];
|
|
280
|
-
args.splice(args.length - promptOperand('').length, 0, ...(kind === 'claude' ? ['--output-format', 'stream-json', '--verbose'] : ['--json']));
|
|
281
|
+
if (kind !== 'devin') args.splice(args.length - promptOperand('').length, 0, ...(kind === 'claude' ? ['--output-format', 'stream-json', '--verbose'] : ['--json']));
|
|
281
282
|
const completion = kind === 'claude' ? claudeResultOutcome : undefined;
|
|
282
283
|
// Retain the source values for output redaction even though the child
|
|
283
284
|
// receives only its filtered environment. A provider can echo a value from
|
|
284
285
|
// its prompt or fixture that was not granted as an environment variable.
|
|
285
|
-
const redactionEnvironment = { ...processEnvironment, ...env
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
286
|
+
const redactionEnvironment = { ...processEnvironment, ...env,
|
|
287
|
+
...(kind === 'devin' && env.DEVIN_API_KEY !== undefined ? { DEVIN_API_KEY: env.DEVIN_API_KEY.trim() } : {}) };
|
|
288
|
+
const prepared = kind === 'devin' ? prepareDevinRuntime(env, { ...invocation, args }) : undefined;
|
|
289
|
+
try {
|
|
290
|
+
const result = await spawnInvocation(cli,
|
|
291
|
+
{ ...invocation, args: prepared?.args ?? args }, prepared?.env ?? env,
|
|
292
|
+
signal, sidechannel, cwd, completion, argv0, kind, redactionEnvironment);
|
|
293
|
+
// Print mode has neither provider frames nor authoritative token counts.
|
|
294
|
+
return kind === 'devin' ? { ...result, stdout_tail: redactText(result.stdout_tail, redactionEnvironment) }
|
|
295
|
+
: requirePricedUsage(decodeProviderResult(result, kind, redactionEnvironment), effectiveModel);
|
|
296
|
+
} finally { prepared?.close(); }
|
|
289
297
|
}
|
|
290
298
|
}
|
|
291
299
|
|