@hmj-ai/cflow 1.1.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/DESIGN.md +241 -0
- package/README.md +111 -0
- package/dist/public/assets/index-BqTfYp5s.js +15 -0
- package/dist/public/assets/index-D0BpmA_V.css +1 -0
- package/dist/public/index.html +15 -0
- package/dist/src/compiler.js +164 -0
- package/dist/src/contract.js +26 -0
- package/dist/src/db.js +208 -0
- package/dist/src/engine.js +487 -0
- package/dist/src/flow-agent.js +190 -0
- package/dist/src/hash.js +14 -0
- package/dist/src/proposal.js +595 -0
- package/dist/src/runtime-manifest.js +244 -0
- package/dist/src/runtime-process.js +175 -0
- package/dist/src/runtime.js +850 -0
- package/dist/src/server.js +652 -0
- package/dist/src/types.js +1 -0
- package/dist/src/workspace.js +26 -0
- package/package.json +68 -0
|
@@ -0,0 +1,850 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { isAbsolute, resolve } from 'node:path';
|
|
3
|
+
import { Readable, Writable } from 'node:stream';
|
|
4
|
+
import { client as acpClient, methods, ndJsonStream, PROTOCOL_VERSION, } from '@agentclientprotocol/sdk';
|
|
5
|
+
import { isWithinDirectory } from './workspace.js';
|
|
6
|
+
import { loadAgentManifestRecords, } from './runtime-manifest.js';
|
|
7
|
+
import { canChangeWorkspace, discoverAcpCommands, existingAbsoluteDirectory, needsWindowsShell, parseJsonOutput, resolveExecutable, runtimeIdFromCommand, runtimeNameFromCommand, } from './runtime-process.js';
|
|
8
|
+
const ADAPTER_BUILD = 'cf-runtime-adapter/3';
|
|
9
|
+
export class RuntimeExecutionException extends Error {
|
|
10
|
+
details;
|
|
11
|
+
constructor(details) {
|
|
12
|
+
super(details.message);
|
|
13
|
+
this.name = 'RuntimeExecutionException';
|
|
14
|
+
this.details = details;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
const defaultTraits = (overrides = {}) => ({
|
|
18
|
+
backendKind: 'acp',
|
|
19
|
+
sessionMode: 'per-cf-call',
|
|
20
|
+
structuredOutput: false,
|
|
21
|
+
streaming: false,
|
|
22
|
+
toolEvents: false,
|
|
23
|
+
permissionPrompts: false,
|
|
24
|
+
tokenAccounting: 'unavailable',
|
|
25
|
+
cancellation: 'cooperative',
|
|
26
|
+
filesystemIsolation: 'sandboxed',
|
|
27
|
+
networkIsolation: 'adapter-declared',
|
|
28
|
+
...overrides,
|
|
29
|
+
});
|
|
30
|
+
const profileFromManifest = (record) => {
|
|
31
|
+
const createdAt = new Date(0).toISOString();
|
|
32
|
+
const manifest = record.manifest;
|
|
33
|
+
const cli = manifest.backend === 'cli';
|
|
34
|
+
return {
|
|
35
|
+
id: manifest.id,
|
|
36
|
+
profileVersion: 1,
|
|
37
|
+
name: manifest.name,
|
|
38
|
+
description: manifest.description,
|
|
39
|
+
enabled: true,
|
|
40
|
+
backend: manifest.backend,
|
|
41
|
+
command: manifest.command,
|
|
42
|
+
args: manifest.args ?? [],
|
|
43
|
+
versionArgs: manifest.versionArgs ?? (cli ? ['--version'] : []),
|
|
44
|
+
promptTransport: manifest.promptTransport ?? (cli ? 'argument' : 'stdin'),
|
|
45
|
+
outputMode: manifest.outputMode ?? 'json',
|
|
46
|
+
timeoutMs: manifest.timeoutMs ?? 300_000,
|
|
47
|
+
maxOutputBytes: manifest.maxOutputBytes ?? 1_048_576,
|
|
48
|
+
envAllowlist: manifest.envAllowlist ?? ['HOME', 'PATH', 'LANG', 'LC_ALL'],
|
|
49
|
+
capabilities: manifest.capabilities ?? [
|
|
50
|
+
'reasoning',
|
|
51
|
+
'code',
|
|
52
|
+
'structured-output',
|
|
53
|
+
'workspace-read',
|
|
54
|
+
],
|
|
55
|
+
permissionArgs: manifest.permissionArgs,
|
|
56
|
+
discovery: {
|
|
57
|
+
source: record.source,
|
|
58
|
+
manifestPath: record.manifestPath,
|
|
59
|
+
manifestHash: record.manifestHash,
|
|
60
|
+
},
|
|
61
|
+
traits: defaultTraits({
|
|
62
|
+
backendKind: cli ? 'process' : 'acp',
|
|
63
|
+
structuredOutput: true,
|
|
64
|
+
streaming: false,
|
|
65
|
+
toolEvents: !cli,
|
|
66
|
+
permissionPrompts: false,
|
|
67
|
+
tokenAccounting: 'unavailable',
|
|
68
|
+
cancellation: cli ? 'process-kill' : 'cooperative',
|
|
69
|
+
filesystemIsolation: cli ? 'host-permissions' : 'sandboxed',
|
|
70
|
+
networkIsolation: cli ? 'unenforced' : 'adapter-declared',
|
|
71
|
+
...manifest.traits,
|
|
72
|
+
}),
|
|
73
|
+
adapterBuild: ADAPTER_BUILD,
|
|
74
|
+
createdAt,
|
|
75
|
+
};
|
|
76
|
+
};
|
|
77
|
+
export const discoverRuntimeProfiles = (options = {}) => {
|
|
78
|
+
const loaded = loadAgentManifestRecords(options);
|
|
79
|
+
const declared = loaded.records.map(profileFromManifest);
|
|
80
|
+
const declaredCommands = new Set(declared.map((profile) => profile.command));
|
|
81
|
+
const generic = discoverAcpCommands(options.projectRoot)
|
|
82
|
+
.filter((command) => !declaredCommands.has(command))
|
|
83
|
+
.sort()
|
|
84
|
+
.map((command) => ({
|
|
85
|
+
id: runtimeIdFromCommand(command),
|
|
86
|
+
profileVersion: 1,
|
|
87
|
+
name: runtimeNameFromCommand(command),
|
|
88
|
+
description: `通过本机 ACP server ${command} 执行步骤。`,
|
|
89
|
+
enabled: true,
|
|
90
|
+
backend: 'acp',
|
|
91
|
+
command,
|
|
92
|
+
args: [],
|
|
93
|
+
versionArgs: [],
|
|
94
|
+
promptTransport: 'stdin',
|
|
95
|
+
outputMode: 'json',
|
|
96
|
+
timeoutMs: 300_000,
|
|
97
|
+
maxOutputBytes: 1_048_576,
|
|
98
|
+
envAllowlist: ['HOME', 'PATH', 'LANG', 'LC_ALL'],
|
|
99
|
+
capabilities: ['reasoning', 'code', 'structured-output', 'workspace-read'],
|
|
100
|
+
discovery: {
|
|
101
|
+
source: 'path-acp',
|
|
102
|
+
manifestHash: `path-acp:${command}`,
|
|
103
|
+
},
|
|
104
|
+
traits: defaultTraits({ structuredOutput: true, toolEvents: true }),
|
|
105
|
+
adapterBuild: ADAPTER_BUILD,
|
|
106
|
+
createdAt: new Date(0).toISOString(),
|
|
107
|
+
}));
|
|
108
|
+
const selected = new Map(generic.map((profile) => [profile.id, profile]));
|
|
109
|
+
for (const profile of declared)
|
|
110
|
+
selected.set(profile.id, profile);
|
|
111
|
+
return { profiles: [...selected.values()], warnings: loaded.warnings };
|
|
112
|
+
};
|
|
113
|
+
export const defaultRuntimeProfiles = () => discoverRuntimeProfiles().profiles;
|
|
114
|
+
export const defaultWorkspaceSettings = (defaultRuntimeId = 'codex') => ({
|
|
115
|
+
defaultRuntimeId,
|
|
116
|
+
autoSaveDrafts: true,
|
|
117
|
+
testTimeoutMs: 300_000,
|
|
118
|
+
locale: 'zh-CN',
|
|
119
|
+
updatedAt: new Date().toISOString(),
|
|
120
|
+
});
|
|
121
|
+
export class RuntimeManager {
|
|
122
|
+
store;
|
|
123
|
+
discoveryOptions;
|
|
124
|
+
activeRuntimeIds = new Set();
|
|
125
|
+
lastDiscoveryWarnings = [];
|
|
126
|
+
constructor(store, discoveryOptions = {}) {
|
|
127
|
+
this.store = store;
|
|
128
|
+
this.discoveryOptions = discoveryOptions;
|
|
129
|
+
this.ensureDefaults();
|
|
130
|
+
}
|
|
131
|
+
ensureDefaults() {
|
|
132
|
+
this.discover();
|
|
133
|
+
const preferredRuntimeId = this.activeRuntimeIds.has('codex')
|
|
134
|
+
? 'codex'
|
|
135
|
+
: ([...this.activeRuntimeIds][0] ?? 'codex');
|
|
136
|
+
const settings = this.store.settings();
|
|
137
|
+
if (!settings) {
|
|
138
|
+
this.store.saveSettings(defaultWorkspaceSettings(preferredRuntimeId));
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
const { workspaceRoot: _removed, ...cleanSettings } = settings;
|
|
142
|
+
const current = this.store.runtimeProfile(settings.defaultRuntimeId);
|
|
143
|
+
if (!current || current.backend === 'builtin')
|
|
144
|
+
this.store.saveSettings({
|
|
145
|
+
...cleanSettings,
|
|
146
|
+
defaultRuntimeId: preferredRuntimeId,
|
|
147
|
+
updatedAt: new Date().toISOString(),
|
|
148
|
+
});
|
|
149
|
+
else if (Object.prototype.hasOwnProperty.call(settings, 'workspaceRoot'))
|
|
150
|
+
this.store.saveSettings(cleanSettings);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
discover() {
|
|
154
|
+
const discovery = discoverRuntimeProfiles(this.discoveryOptions);
|
|
155
|
+
const defaults = discovery.profiles;
|
|
156
|
+
this.activeRuntimeIds = new Set(defaults.map((profile) => profile.id));
|
|
157
|
+
this.lastDiscoveryWarnings = discovery.warnings;
|
|
158
|
+
const changed = [];
|
|
159
|
+
for (const profile of defaults) {
|
|
160
|
+
const current = this.store.runtimeProfile(profile.id);
|
|
161
|
+
if (!current) {
|
|
162
|
+
this.store.saveRuntimeProfile(profile);
|
|
163
|
+
changed.push(profile);
|
|
164
|
+
}
|
|
165
|
+
else if (current.adapterBuild !== ADAPTER_BUILD ||
|
|
166
|
+
current.discovery?.manifestHash !== profile.discovery?.manifestHash) {
|
|
167
|
+
const updated = {
|
|
168
|
+
...current,
|
|
169
|
+
...profile,
|
|
170
|
+
enabled: current.enabled,
|
|
171
|
+
model: current.model,
|
|
172
|
+
workingDirectory: current.workingDirectory,
|
|
173
|
+
profileVersion: this.store.nextRuntimeProfileVersion(profile.id),
|
|
174
|
+
createdAt: new Date().toISOString(),
|
|
175
|
+
};
|
|
176
|
+
this.store.saveRuntimeProfile(updated);
|
|
177
|
+
changed.push(updated);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return changed;
|
|
181
|
+
}
|
|
182
|
+
discoveryWarnings() {
|
|
183
|
+
return this.lastDiscoveryWarnings;
|
|
184
|
+
}
|
|
185
|
+
profiles() {
|
|
186
|
+
const defaultRuntimeId = this.store.settings()?.defaultRuntimeId;
|
|
187
|
+
return this.store
|
|
188
|
+
.currentRuntimeProfiles()
|
|
189
|
+
.filter((profile) => profile.backend !== 'builtin' &&
|
|
190
|
+
(this.activeRuntimeIds.has(profile.id) ||
|
|
191
|
+
profile.discovery?.source === 'manual' ||
|
|
192
|
+
profile.id === defaultRuntimeId));
|
|
193
|
+
}
|
|
194
|
+
profile(id) {
|
|
195
|
+
return this.store.runtimeProfile(id);
|
|
196
|
+
}
|
|
197
|
+
settings() {
|
|
198
|
+
const stored = this.store.settings();
|
|
199
|
+
if (!stored)
|
|
200
|
+
return defaultWorkspaceSettings();
|
|
201
|
+
const { workspaceRoot: _removed, ...settings } = stored;
|
|
202
|
+
return settings;
|
|
203
|
+
}
|
|
204
|
+
validateSettings(input) {
|
|
205
|
+
if (Object.prototype.hasOwnProperty.call(input, 'workspaceRoot'))
|
|
206
|
+
throw new Error('WORKSPACE_ROOT_SETTING_REMOVED');
|
|
207
|
+
const previous = this.settings();
|
|
208
|
+
const defaultRuntimeId = String(input.defaultRuntimeId ?? previous.defaultRuntimeId);
|
|
209
|
+
const defaultRuntime = this.profile(defaultRuntimeId);
|
|
210
|
+
if (!defaultRuntime)
|
|
211
|
+
throw new Error('DEFAULT_RUNTIME_NOT_FOUND');
|
|
212
|
+
if (defaultRuntime.backend === 'builtin')
|
|
213
|
+
throw new Error('DEFAULT_RUNTIME_NOT_SELECTABLE');
|
|
214
|
+
if (!defaultRuntime.enabled)
|
|
215
|
+
throw new Error('DEFAULT_RUNTIME_DISABLED');
|
|
216
|
+
const defaultResourceProfileId = Object.prototype.hasOwnProperty.call(input, 'defaultResourceProfileId')
|
|
217
|
+
? input.defaultResourceProfileId
|
|
218
|
+
: previous.defaultResourceProfileId;
|
|
219
|
+
if (defaultResourceProfileId &&
|
|
220
|
+
!this.store.getResourceProfile(String(defaultResourceProfileId)))
|
|
221
|
+
throw new Error('DEFAULT_RESOURCE_PROFILE_NOT_FOUND');
|
|
222
|
+
const testTimeoutMs = Number(input.testTimeoutMs ?? previous.testTimeoutMs);
|
|
223
|
+
if (!Number.isInteger(testTimeoutMs) || testTimeoutMs < 1_000 || testTimeoutMs > 3_600_000)
|
|
224
|
+
throw new Error('TEST_TIMEOUT_INVALID');
|
|
225
|
+
return {
|
|
226
|
+
...previous,
|
|
227
|
+
...input,
|
|
228
|
+
defaultRuntimeId,
|
|
229
|
+
defaultResourceProfileId: defaultResourceProfileId
|
|
230
|
+
? String(defaultResourceProfileId)
|
|
231
|
+
: undefined,
|
|
232
|
+
autoSaveDrafts: input.autoSaveDrafts ?? previous.autoSaveDrafts,
|
|
233
|
+
testTimeoutMs,
|
|
234
|
+
locale: String(input.locale ?? previous.locale).slice(0, 32),
|
|
235
|
+
updatedAt: new Date().toISOString(),
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
updateSettings(input) {
|
|
239
|
+
const value = this.validateSettings(input);
|
|
240
|
+
this.store.saveSettings(value);
|
|
241
|
+
return value;
|
|
242
|
+
}
|
|
243
|
+
validateProfile(input) {
|
|
244
|
+
const id = input.id.trim().toLowerCase();
|
|
245
|
+
if (!/^[a-z0-9][a-z0-9._-]{1,63}$/.test(id))
|
|
246
|
+
throw new Error('RUNTIME_ID_INVALID');
|
|
247
|
+
if (!input.name?.trim() || input.name.trim().length > 80)
|
|
248
|
+
throw new Error('RUNTIME_NAME_INVALID');
|
|
249
|
+
const backend = input.backend ?? 'acp';
|
|
250
|
+
if (!['builtin', 'acp', 'cli'].includes(backend))
|
|
251
|
+
throw new Error('RUNTIME_BACKEND_INVALID');
|
|
252
|
+
if (backend === 'builtin' && id !== 'echo')
|
|
253
|
+
throw new Error('RUNTIME_BUILTIN_RESERVED');
|
|
254
|
+
const command = input.command?.trim();
|
|
255
|
+
if ((backend === 'acp' || backend === 'cli') && !command)
|
|
256
|
+
throw new Error('RUNTIME_COMMAND_REQUIRED');
|
|
257
|
+
const timeoutMs = Number(input.timeoutMs ?? 300_000);
|
|
258
|
+
const maxOutputBytes = Number(input.maxOutputBytes ?? 1_048_576);
|
|
259
|
+
if (!Number.isInteger(timeoutMs) || timeoutMs < 1_000 || timeoutMs > 3_600_000)
|
|
260
|
+
throw new Error('RUNTIME_TIMEOUT_INVALID');
|
|
261
|
+
if (!Number.isInteger(maxOutputBytes) || maxOutputBytes < 1_024 || maxOutputBytes > 16_777_216)
|
|
262
|
+
throw new Error('RUNTIME_OUTPUT_LIMIT_INVALID');
|
|
263
|
+
const workingDirectoryInput = input.workingDirectory?.trim();
|
|
264
|
+
const workingDirectory = workingDirectoryInput
|
|
265
|
+
? existingAbsoluteDirectory(workingDirectoryInput, 'RUNTIME_CWD_INVALID')
|
|
266
|
+
: undefined;
|
|
267
|
+
const safeList = (items, max) => {
|
|
268
|
+
if (!Array.isArray(items) ||
|
|
269
|
+
items.length > max ||
|
|
270
|
+
items.some((value) => typeof value !== 'string'))
|
|
271
|
+
throw new Error('RUNTIME_LIST_INVALID');
|
|
272
|
+
return items.map((value) => value.trim()).filter(Boolean);
|
|
273
|
+
};
|
|
274
|
+
const previous = this.profile(id);
|
|
275
|
+
const envAllowlist = safeList(input.envAllowlist ?? previous?.envAllowlist ?? ['PATH', 'HOME'], 40);
|
|
276
|
+
if (envAllowlist.some((name) => !/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)))
|
|
277
|
+
throw new Error('RUNTIME_ENV_NAME_INVALID');
|
|
278
|
+
const permissionArgsInput = input.permissionArgs ?? previous?.permissionArgs;
|
|
279
|
+
const permissionArgs = permissionArgsInput
|
|
280
|
+
? Object.fromEntries(['none', 'read', 'write', 'full']
|
|
281
|
+
.map((mode) => [mode, safeList(permissionArgsInput[mode] ?? [], 30)])
|
|
282
|
+
.filter(([, args]) => args.length))
|
|
283
|
+
: undefined;
|
|
284
|
+
return {
|
|
285
|
+
id,
|
|
286
|
+
profileVersion: this.store.nextRuntimeProfileVersion(id),
|
|
287
|
+
name: input.name.trim(),
|
|
288
|
+
description: input.description?.trim().slice(0, 400),
|
|
289
|
+
enabled: input.enabled ?? true,
|
|
290
|
+
backend,
|
|
291
|
+
command: backend === 'acp' || backend === 'cli' ? (command ?? previous?.command ?? id) : command,
|
|
292
|
+
args: safeList(input.args ?? previous?.args ?? [], 40),
|
|
293
|
+
versionArgs: safeList(input.versionArgs ?? previous?.versionArgs ?? ['--version'], 10),
|
|
294
|
+
model: input.model?.trim().slice(0, 120),
|
|
295
|
+
workingDirectory,
|
|
296
|
+
promptTransport: input.promptTransport === 'argument' ? 'argument' : 'stdin',
|
|
297
|
+
outputMode: input.outputMode === 'text' ? 'text' : 'json',
|
|
298
|
+
timeoutMs,
|
|
299
|
+
maxOutputBytes,
|
|
300
|
+
envAllowlist: [...new Set(envAllowlist)],
|
|
301
|
+
capabilities: safeList(input.capabilities ?? previous?.capabilities ?? [], 40),
|
|
302
|
+
permissionArgs,
|
|
303
|
+
discovery: previous?.discovery ?? { source: 'manual' },
|
|
304
|
+
traits: {
|
|
305
|
+
...defaultTraits(backend === 'cli'
|
|
306
|
+
? {
|
|
307
|
+
backendKind: 'process',
|
|
308
|
+
cancellation: 'process-kill',
|
|
309
|
+
filesystemIsolation: 'host-permissions',
|
|
310
|
+
networkIsolation: 'unenforced',
|
|
311
|
+
}
|
|
312
|
+
: {}),
|
|
313
|
+
...(input.traits ?? previous?.traits ?? {}),
|
|
314
|
+
},
|
|
315
|
+
adapterBuild: ADAPTER_BUILD,
|
|
316
|
+
createdAt: new Date().toISOString(),
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
saveProfile(input) {
|
|
320
|
+
const profile = this.validateProfile(input);
|
|
321
|
+
this.store.saveRuntimeProfile(profile);
|
|
322
|
+
this.activeRuntimeIds.add(profile.id);
|
|
323
|
+
return profile;
|
|
324
|
+
}
|
|
325
|
+
register(registry, profile, exactOnly = false) {
|
|
326
|
+
if (profile.backend === 'builtin') {
|
|
327
|
+
const echo = {
|
|
328
|
+
execute: async (task, input) => ({ task, input }),
|
|
329
|
+
};
|
|
330
|
+
registry.register({ id: `${profile.id}@${profile.profileVersion}`, ...echo });
|
|
331
|
+
if (!exactOnly)
|
|
332
|
+
registry.register({ id: profile.id, ...echo });
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
const executor = (id) => registry.register({
|
|
336
|
+
id,
|
|
337
|
+
execute: (task, input, signal, resources, effects, context) => this.executeProfile(profile, task, input, signal, resources, undefined, effects, context),
|
|
338
|
+
});
|
|
339
|
+
executor(`${profile.id}@${profile.profileVersion}`);
|
|
340
|
+
if (!exactOnly)
|
|
341
|
+
executor(profile.id);
|
|
342
|
+
}
|
|
343
|
+
registerAll(registry) {
|
|
344
|
+
const current = new Map(this.profiles().map((profile) => [profile.id, profile.profileVersion]));
|
|
345
|
+
for (const profile of this.store.allRuntimeProfiles())
|
|
346
|
+
this.register(registry, profile, current.get(profile.id) !== profile.profileVersion);
|
|
347
|
+
}
|
|
348
|
+
async health(id) {
|
|
349
|
+
const profile = this.profile(id);
|
|
350
|
+
if (!profile)
|
|
351
|
+
throw new Error('RUNTIME_NOT_FOUND');
|
|
352
|
+
if (!profile.enabled)
|
|
353
|
+
return {
|
|
354
|
+
runtimeId: id,
|
|
355
|
+
profileVersion: profile.profileVersion,
|
|
356
|
+
status: 'disabled',
|
|
357
|
+
checkedAt: new Date().toISOString(),
|
|
358
|
+
latencyMs: 0,
|
|
359
|
+
};
|
|
360
|
+
if (profile.backend === 'builtin')
|
|
361
|
+
return {
|
|
362
|
+
runtimeId: id,
|
|
363
|
+
profileVersion: profile.profileVersion,
|
|
364
|
+
status: 'available',
|
|
365
|
+
checkedAt: new Date().toISOString(),
|
|
366
|
+
latencyMs: 0,
|
|
367
|
+
version: profile.adapterBuild,
|
|
368
|
+
};
|
|
369
|
+
if (profile.backend === 'acp') {
|
|
370
|
+
const started = Date.now();
|
|
371
|
+
try {
|
|
372
|
+
const version = await this.probeAcp(profile);
|
|
373
|
+
return {
|
|
374
|
+
runtimeId: id,
|
|
375
|
+
profileVersion: profile.profileVersion,
|
|
376
|
+
status: 'available',
|
|
377
|
+
checkedAt: new Date().toISOString(),
|
|
378
|
+
latencyMs: Date.now() - started,
|
|
379
|
+
stage: 'protocol-ready',
|
|
380
|
+
authentication: 'unknown',
|
|
381
|
+
version,
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
catch (error) {
|
|
385
|
+
return {
|
|
386
|
+
runtimeId: id,
|
|
387
|
+
profileVersion: profile.profileVersion,
|
|
388
|
+
status: 'unavailable',
|
|
389
|
+
checkedAt: new Date().toISOString(),
|
|
390
|
+
latencyMs: Date.now() - started,
|
|
391
|
+
stage: this.commandInstalled(profile, true) ? 'installed' : undefined,
|
|
392
|
+
authentication: 'unknown',
|
|
393
|
+
error: error instanceof Error ? error.message : String(error),
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
if (profile.backend === 'cli') {
|
|
398
|
+
const started = Date.now();
|
|
399
|
+
try {
|
|
400
|
+
const version = await this.probeCli(profile);
|
|
401
|
+
return {
|
|
402
|
+
runtimeId: id,
|
|
403
|
+
profileVersion: profile.profileVersion,
|
|
404
|
+
status: 'available',
|
|
405
|
+
checkedAt: new Date().toISOString(),
|
|
406
|
+
latencyMs: Date.now() - started,
|
|
407
|
+
stage: 'adapter-ready',
|
|
408
|
+
authentication: 'unknown',
|
|
409
|
+
version,
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
catch (error) {
|
|
413
|
+
return {
|
|
414
|
+
runtimeId: id,
|
|
415
|
+
profileVersion: profile.profileVersion,
|
|
416
|
+
status: 'unavailable',
|
|
417
|
+
checkedAt: new Date().toISOString(),
|
|
418
|
+
latencyMs: Date.now() - started,
|
|
419
|
+
stage: this.commandInstalled(profile) ? 'installed' : undefined,
|
|
420
|
+
authentication: 'unknown',
|
|
421
|
+
error: error instanceof Error ? error.message : String(error),
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
throw new Error(`RUNTIME_BACKEND_UNSUPPORTED:${profile.backend}`);
|
|
426
|
+
}
|
|
427
|
+
async execute(id, task, input, signal, resources = [], outputSchema, context) {
|
|
428
|
+
const profile = this.profile(id);
|
|
429
|
+
if (!profile)
|
|
430
|
+
throw new Error(`UNKNOWN_EXECUTOR:${id}`);
|
|
431
|
+
return this.executeProfile(profile, task, input, signal, resources, outputSchema, [], context);
|
|
432
|
+
}
|
|
433
|
+
async executeAnalysis(id, task, input, signal, options, outputSchema) {
|
|
434
|
+
const profile = this.profile(id);
|
|
435
|
+
if (!profile)
|
|
436
|
+
throw new Error(`UNKNOWN_EXECUTOR:${id}`);
|
|
437
|
+
return this.executeProfile(profile, task, input, signal, [], outputSchema, [], undefined, options);
|
|
438
|
+
}
|
|
439
|
+
async executeProfile(profile, task, input, signal, resources = [], outputSchema, effects = [], context, analysis) {
|
|
440
|
+
if (!profile.enabled)
|
|
441
|
+
throw new Error(`RUNTIME_DISABLED:${profile.id}`);
|
|
442
|
+
if (profile.backend === 'builtin')
|
|
443
|
+
return { task, input };
|
|
444
|
+
const prompt = [
|
|
445
|
+
'You are executing one bounded CF capability inside a fixed Flow.',
|
|
446
|
+
'Complete only the current task. Do not choose the next Flow node or change the Flow.',
|
|
447
|
+
profile.outputMode === 'json'
|
|
448
|
+
? 'Return only valid JSON. Do not wrap it in Markdown.'
|
|
449
|
+
: 'Return the final result without process commentary.',
|
|
450
|
+
`Task:\n${task}`,
|
|
451
|
+
`Input JSON:\n${JSON.stringify(input)}`,
|
|
452
|
+
'Input JSON contains flowInput and an upstream array with complete source outputs. Select and transform only data described by the capability input guidance.',
|
|
453
|
+
effects.length
|
|
454
|
+
? `Declared effects (authorized within workspace root ${context?.workspaceRoot ?? analysis?.allowedRoot}):\n${JSON.stringify(effects)}\nYou may perform these declared effects when required by the task; do not ask the user for a second authorization.`
|
|
455
|
+
: 'Declared effects: none. Do not perform file writes, reads, or commands.',
|
|
456
|
+
outputSchema ? `Expected output JSON Schema:\n${JSON.stringify(outputSchema)}` : undefined,
|
|
457
|
+
resources.length
|
|
458
|
+
? `Authorized resources (do not access outside these scopes):\n${JSON.stringify(resources)}`
|
|
459
|
+
: 'Authorized resources: none.',
|
|
460
|
+
]
|
|
461
|
+
.filter(Boolean)
|
|
462
|
+
.join('\n\n');
|
|
463
|
+
const text = profile.backend === 'acp'
|
|
464
|
+
? await this.runAcp(profile, prompt, signal, effects, context, analysis)
|
|
465
|
+
: profile.backend === 'cli'
|
|
466
|
+
? await this.runCli(profile, prompt, signal, effects, context, analysis)
|
|
467
|
+
: await Promise.reject(new Error(`RUNTIME_BACKEND_UNSUPPORTED:${profile.backend}`));
|
|
468
|
+
return profile.outputMode === 'json' ? parseJsonOutput(text) : { content: text.trim() };
|
|
469
|
+
}
|
|
470
|
+
commandInstalled(profile, includeProjectBin = false) {
|
|
471
|
+
if (!profile.command)
|
|
472
|
+
return false;
|
|
473
|
+
return Boolean(resolveExecutable(profile.command, { ...process.env }, {
|
|
474
|
+
includeProjectBin,
|
|
475
|
+
}));
|
|
476
|
+
}
|
|
477
|
+
async probeAcp(profile) {
|
|
478
|
+
const command = this.acpCommand(profile);
|
|
479
|
+
const child = spawn(command, profile.args, {
|
|
480
|
+
cwd: profile.workingDirectory ?? process.cwd(),
|
|
481
|
+
env: this.acpEnvironment(profile),
|
|
482
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
483
|
+
shell: needsWindowsShell(command),
|
|
484
|
+
windowsHide: true,
|
|
485
|
+
});
|
|
486
|
+
let stderr = Buffer.alloc(0);
|
|
487
|
+
child.stderr.on('data', (chunk) => {
|
|
488
|
+
stderr = Buffer.concat([stderr, chunk]);
|
|
489
|
+
if (stderr.length > 8_192)
|
|
490
|
+
stderr = stderr.subarray(stderr.length - 8_192);
|
|
491
|
+
});
|
|
492
|
+
const terminate = () => {
|
|
493
|
+
child.kill('SIGTERM');
|
|
494
|
+
setTimeout(() => child.kill('SIGKILL'), 1_000).unref();
|
|
495
|
+
};
|
|
496
|
+
let timeout;
|
|
497
|
+
try {
|
|
498
|
+
if (!child.stdin || !child.stdout)
|
|
499
|
+
throw new Error('ACP_SERVER_STDIO_UNAVAILABLE');
|
|
500
|
+
const stream = ndJsonStream(Writable.toWeb(child.stdin), Readable.toWeb(child.stdout));
|
|
501
|
+
const initialize = acpClient({ name: 'CFlow Health Check' }).connectWith(stream, async (ctx) => ctx.request(methods.agent.initialize, {
|
|
502
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
503
|
+
clientInfo: { name: 'CFlow', version: ADAPTER_BUILD },
|
|
504
|
+
clientCapabilities: { plan: {}, session: {} },
|
|
505
|
+
}));
|
|
506
|
+
const childError = new Promise((_, reject) => child.once('error', reject));
|
|
507
|
+
const timedOut = new Promise((_, reject) => {
|
|
508
|
+
timeout = setTimeout(() => {
|
|
509
|
+
terminate();
|
|
510
|
+
reject(new Error('ACP_HANDSHAKE_TIMEOUT'));
|
|
511
|
+
}, 5_000);
|
|
512
|
+
});
|
|
513
|
+
const response = await Promise.race([initialize, childError, timedOut]);
|
|
514
|
+
const agent = response.agentInfo
|
|
515
|
+
? [response.agentInfo.name, response.agentInfo.version].filter(Boolean).join(' ')
|
|
516
|
+
: undefined;
|
|
517
|
+
return [`ACP ${response.protocolVersion}`, agent, `via ${command}`]
|
|
518
|
+
.filter(Boolean)
|
|
519
|
+
.join(' · ');
|
|
520
|
+
}
|
|
521
|
+
catch (error) {
|
|
522
|
+
const detail = stderr.toString('utf8').trim().slice(-800);
|
|
523
|
+
if (error instanceof Error && detail)
|
|
524
|
+
throw new Error(`${error.message}:${detail}`);
|
|
525
|
+
throw error;
|
|
526
|
+
}
|
|
527
|
+
finally {
|
|
528
|
+
if (timeout)
|
|
529
|
+
clearTimeout(timeout);
|
|
530
|
+
terminate();
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
async probeCli(profile) {
|
|
534
|
+
const command = this.cliCommand(profile);
|
|
535
|
+
if (!profile.versionArgs.length)
|
|
536
|
+
return `CLI via ${command}`;
|
|
537
|
+
const child = spawn(command, profile.versionArgs, {
|
|
538
|
+
cwd: profile.workingDirectory ?? process.cwd(),
|
|
539
|
+
env: this.acpEnvironment(profile),
|
|
540
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
541
|
+
shell: needsWindowsShell(command),
|
|
542
|
+
windowsHide: true,
|
|
543
|
+
});
|
|
544
|
+
const output = [];
|
|
545
|
+
const errors = [];
|
|
546
|
+
child.stdout.on('data', (chunk) => output.push(chunk));
|
|
547
|
+
child.stderr.on('data', (chunk) => errors.push(chunk));
|
|
548
|
+
let timeout;
|
|
549
|
+
const terminate = () => {
|
|
550
|
+
child.kill('SIGTERM');
|
|
551
|
+
setTimeout(() => child.kill('SIGKILL'), 1_000).unref();
|
|
552
|
+
};
|
|
553
|
+
try {
|
|
554
|
+
const result = await new Promise((resolveExit, reject) => {
|
|
555
|
+
child.once('error', reject);
|
|
556
|
+
child.once('exit', (code, signal) => resolveExit({ code, signal }));
|
|
557
|
+
timeout = setTimeout(() => {
|
|
558
|
+
terminate();
|
|
559
|
+
reject(new Error('CLI_HEALTHCHECK_TIMEOUT'));
|
|
560
|
+
}, 5_000);
|
|
561
|
+
});
|
|
562
|
+
const detail = Buffer.concat(errors).toString('utf8').trim().slice(-800);
|
|
563
|
+
if (result.code !== 0)
|
|
564
|
+
throw new Error(`CLI_HEALTHCHECK_EXITED:${result.code ?? result.signal ?? 'unknown'}${detail ? `:${detail}` : ''}`);
|
|
565
|
+
const version = Buffer.concat(output).toString('utf8').trim().split(/\r?\n/, 1)[0];
|
|
566
|
+
return [version || profile.name, `via ${command}`].join(' · ');
|
|
567
|
+
}
|
|
568
|
+
finally {
|
|
569
|
+
if (timeout)
|
|
570
|
+
clearTimeout(timeout);
|
|
571
|
+
terminate();
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
acpCommand(profile) {
|
|
575
|
+
if (!profile.command)
|
|
576
|
+
throw new Error('RUNTIME_COMMAND_REQUIRED');
|
|
577
|
+
const env = this.acpEnvironment(profile);
|
|
578
|
+
const resolved = resolveExecutable(profile.command, env, { includeProjectBin: true });
|
|
579
|
+
if (!resolved)
|
|
580
|
+
throw new Error(`ACP_SERVER_NOT_FOUND:${profile.command}`);
|
|
581
|
+
return resolved;
|
|
582
|
+
}
|
|
583
|
+
cliCommand(profile) {
|
|
584
|
+
if (!profile.command)
|
|
585
|
+
throw new Error('RUNTIME_COMMAND_REQUIRED');
|
|
586
|
+
const resolved = resolveExecutable(profile.command, this.acpEnvironment(profile));
|
|
587
|
+
if (!resolved)
|
|
588
|
+
throw new Error(`AGENT_CLI_NOT_FOUND:${profile.command}`);
|
|
589
|
+
return resolved;
|
|
590
|
+
}
|
|
591
|
+
codexPath() {
|
|
592
|
+
const env = { ...process.env };
|
|
593
|
+
const command = process.env.CFLOW_CODEX_PATH ?? process.env.CODEX_PATH ?? 'codex';
|
|
594
|
+
const resolved = resolveExecutable(command, env);
|
|
595
|
+
if (!resolved)
|
|
596
|
+
throw new Error(`CODEX_CLI_NOT_FOUND:${command}`);
|
|
597
|
+
return resolved;
|
|
598
|
+
}
|
|
599
|
+
claudePath() {
|
|
600
|
+
const env = { ...process.env };
|
|
601
|
+
const command = process.env.CFLOW_CLAUDE_PATH ?? process.env.CLAUDE_CODE_EXECUTABLE ?? 'claude';
|
|
602
|
+
const resolved = resolveExecutable(command, env);
|
|
603
|
+
if (!resolved)
|
|
604
|
+
throw new Error(`CLAUDE_CODE_CLI_NOT_FOUND:${command}`);
|
|
605
|
+
return resolved;
|
|
606
|
+
}
|
|
607
|
+
acpEnvironment(profile, effects = []) {
|
|
608
|
+
const env = {};
|
|
609
|
+
for (const key of profile.envAllowlist) {
|
|
610
|
+
const value = process.env[key];
|
|
611
|
+
if (value !== undefined)
|
|
612
|
+
env[key] = value;
|
|
613
|
+
}
|
|
614
|
+
if (profile.id === 'codex') {
|
|
615
|
+
env.CODEX_PATH = this.codexPath();
|
|
616
|
+
env.INITIAL_AGENT_MODE = effects.some((effect) => effect.type === 'file-write')
|
|
617
|
+
? 'workspace-write'
|
|
618
|
+
: 'read-only';
|
|
619
|
+
env.NO_BROWSER ??= '1';
|
|
620
|
+
if (profile.model)
|
|
621
|
+
env.CODEX_CONFIG = JSON.stringify({ model: profile.model });
|
|
622
|
+
}
|
|
623
|
+
if (profile.id === 'claude-code') {
|
|
624
|
+
env.CLAUDE_CODE_EXECUTABLE = this.claudePath();
|
|
625
|
+
if (profile.model)
|
|
626
|
+
env.CLAUDE_MODEL_CONFIG = JSON.stringify({ model: profile.model });
|
|
627
|
+
}
|
|
628
|
+
return env;
|
|
629
|
+
}
|
|
630
|
+
cliPermissionArgs(profile, effects, analysis) {
|
|
631
|
+
const canRead = Boolean(analysis) || effects.some((effect) => effect.type === 'file-read');
|
|
632
|
+
const canWrite = effects.some((effect) => effect.type === 'file-write');
|
|
633
|
+
const canRun = effects.some((effect) => effect.type === 'command');
|
|
634
|
+
const mode = canRun ? 'full' : canWrite ? 'write' : canRead ? 'read' : 'none';
|
|
635
|
+
return profile.permissionArgs?.[mode] ?? [];
|
|
636
|
+
}
|
|
637
|
+
async runCli(profile, prompt, signal, effects = [], context, analysis) {
|
|
638
|
+
const cwd = analysis?.cwd ?? context?.workspaceRoot ?? profile.workingDirectory ?? process.cwd();
|
|
639
|
+
const command = this.cliCommand(profile);
|
|
640
|
+
const args = [
|
|
641
|
+
...profile.args,
|
|
642
|
+
...this.cliPermissionArgs(profile, effects, analysis),
|
|
643
|
+
...(profile.promptTransport === 'argument' ? [prompt] : []),
|
|
644
|
+
];
|
|
645
|
+
const child = spawn(command, args, {
|
|
646
|
+
cwd,
|
|
647
|
+
env: this.acpEnvironment(profile, effects),
|
|
648
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
649
|
+
shell: needsWindowsShell(command),
|
|
650
|
+
windowsHide: true,
|
|
651
|
+
});
|
|
652
|
+
const output = [];
|
|
653
|
+
const errors = [];
|
|
654
|
+
let outputBytes = 0;
|
|
655
|
+
let outputLimitExceeded = false;
|
|
656
|
+
const terminate = () => {
|
|
657
|
+
child.kill('SIGTERM');
|
|
658
|
+
setTimeout(() => child.kill('SIGKILL'), 1_000).unref();
|
|
659
|
+
};
|
|
660
|
+
const relayAbort = () => terminate();
|
|
661
|
+
signal.addEventListener('abort', relayAbort, { once: true });
|
|
662
|
+
child.stdout.on('data', (chunk) => {
|
|
663
|
+
outputBytes += chunk.length;
|
|
664
|
+
if (outputBytes > profile.maxOutputBytes) {
|
|
665
|
+
outputLimitExceeded = true;
|
|
666
|
+
terminate();
|
|
667
|
+
return;
|
|
668
|
+
}
|
|
669
|
+
output.push(chunk);
|
|
670
|
+
});
|
|
671
|
+
child.stderr.on('data', (chunk) => {
|
|
672
|
+
errors.push(chunk);
|
|
673
|
+
if (Buffer.concat(errors).length > 65_536)
|
|
674
|
+
errors.shift();
|
|
675
|
+
});
|
|
676
|
+
if (profile.promptTransport === 'stdin')
|
|
677
|
+
child.stdin.end(prompt);
|
|
678
|
+
else
|
|
679
|
+
child.stdin.end();
|
|
680
|
+
try {
|
|
681
|
+
const result = await new Promise((resolveExit, reject) => {
|
|
682
|
+
child.once('error', reject);
|
|
683
|
+
child.once('exit', (code, childSignal) => resolveExit({ code, childSignal }));
|
|
684
|
+
});
|
|
685
|
+
if (signal.aborted)
|
|
686
|
+
throw new RuntimeExecutionException({
|
|
687
|
+
layer: 'runtime',
|
|
688
|
+
code: signal.reason?.name === 'TimeoutError' ? 'RUNTIME_TIMEOUT' : 'RUN_CANCELLED',
|
|
689
|
+
message: signal.reason?.name === 'TimeoutError' ? '运行超时' : '运行已取消',
|
|
690
|
+
retryable: signal.reason?.name === 'TimeoutError',
|
|
691
|
+
effectState: canChangeWorkspace(effects) ? 'unknown' : 'none',
|
|
692
|
+
});
|
|
693
|
+
if (outputLimitExceeded)
|
|
694
|
+
throw new Error('RUNTIME_OUTPUT_LIMIT_EXCEEDED');
|
|
695
|
+
const stderr = Buffer.concat(errors).toString('utf8').trim().slice(-800);
|
|
696
|
+
if (result.code !== 0)
|
|
697
|
+
throw new Error(`AGENT_CLI_EXITED:${result.code ?? result.childSignal ?? 'unknown'}${stderr ? `:${stderr}` : ''}`);
|
|
698
|
+
return Buffer.concat(output).toString('utf8');
|
|
699
|
+
}
|
|
700
|
+
finally {
|
|
701
|
+
signal.removeEventListener('abort', relayAbort);
|
|
702
|
+
terminate();
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
async runAcp(profile, prompt, signal, effects = [], context, analysis) {
|
|
706
|
+
const cwd = analysis?.cwd ?? context?.workspaceRoot ?? profile.workingDirectory ?? process.cwd();
|
|
707
|
+
const command = this.acpCommand(profile);
|
|
708
|
+
const child = spawn(command, profile.args, {
|
|
709
|
+
cwd,
|
|
710
|
+
env: this.acpEnvironment(profile, effects),
|
|
711
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
712
|
+
shell: needsWindowsShell(command),
|
|
713
|
+
windowsHide: true,
|
|
714
|
+
});
|
|
715
|
+
let stderr = Buffer.alloc(0);
|
|
716
|
+
let stderrText = '';
|
|
717
|
+
child.stderr.on('data', (chunk) => {
|
|
718
|
+
stderr = Buffer.concat([stderr, chunk]);
|
|
719
|
+
if (stderr.length > 65_536)
|
|
720
|
+
stderr = stderr.subarray(stderr.length - 65_536);
|
|
721
|
+
stderrText = stderr.toString('utf8');
|
|
722
|
+
});
|
|
723
|
+
const terminate = () => {
|
|
724
|
+
child.kill('SIGTERM');
|
|
725
|
+
setTimeout(() => child.kill('SIGKILL'), 1_000).unref();
|
|
726
|
+
};
|
|
727
|
+
const relayAbort = () => terminate();
|
|
728
|
+
signal.addEventListener('abort', relayAbort, { once: true });
|
|
729
|
+
const exitPromise = new Promise((resolveExit) => child.once('exit', (code, childSignal) => resolveExit({ code, signal: childSignal })));
|
|
730
|
+
try {
|
|
731
|
+
if (!child.stdin || !child.stdout)
|
|
732
|
+
throw new Error('ACP_SERVER_STDIO_UNAVAILABLE');
|
|
733
|
+
const stream = ndJsonStream(Writable.toWeb(child.stdin), Readable.toWeb(child.stdout));
|
|
734
|
+
let permissionDenied = false;
|
|
735
|
+
return await acpClient({ name: 'CFlow' })
|
|
736
|
+
.onRequest(methods.client.session.requestPermission, async (ctx) => {
|
|
737
|
+
const tool = ctx?.params?.toolCall ?? ctx?.toolCall ?? {};
|
|
738
|
+
const kind = String(tool.kind ?? tool.name ?? tool.title ?? '').toLowerCase();
|
|
739
|
+
const effectType = kind.includes('read') || kind.includes('search')
|
|
740
|
+
? 'file-read'
|
|
741
|
+
: kind.includes('edit') ||
|
|
742
|
+
kind.includes('write') ||
|
|
743
|
+
kind.includes('delete') ||
|
|
744
|
+
kind.includes('move')
|
|
745
|
+
? 'file-write'
|
|
746
|
+
: kind.includes('exec') || kind.includes('command') || kind.includes('terminal')
|
|
747
|
+
? 'command'
|
|
748
|
+
: undefined;
|
|
749
|
+
const root = analysis?.allowedRoot ?? context?.workspaceRoot;
|
|
750
|
+
const declared = effectType
|
|
751
|
+
? analysis && effectType === 'file-read'
|
|
752
|
+
? true
|
|
753
|
+
: effects.some((effect) => effect.type === effectType && effect.scope === 'workspace')
|
|
754
|
+
: false;
|
|
755
|
+
const locations = Array.isArray(tool.locations) ? [...tool.locations] : [];
|
|
756
|
+
const raw = tool.rawInput;
|
|
757
|
+
if (raw && typeof raw === 'object') {
|
|
758
|
+
for (const key of ['path', 'file', 'filePath', 'filename']) {
|
|
759
|
+
const value = raw[key];
|
|
760
|
+
if (typeof value === 'string')
|
|
761
|
+
locations.push(value);
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
const inWorkspace = locations.every((location) => {
|
|
765
|
+
let value = typeof location === 'string' ? location : (location?.path ?? location?.uri);
|
|
766
|
+
if (typeof value === 'string' && value.startsWith('file://')) {
|
|
767
|
+
try {
|
|
768
|
+
value = new URL(value).pathname;
|
|
769
|
+
}
|
|
770
|
+
catch {
|
|
771
|
+
return false;
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
if (analysis && typeof value === 'string' && !isAbsolute(value))
|
|
775
|
+
value = resolve(cwd, value);
|
|
776
|
+
return Boolean(root) && (!value || isWithinDirectory(root, value));
|
|
777
|
+
});
|
|
778
|
+
const scopedRead = !analysis || effectType !== 'file-read' || locations.length > 0;
|
|
779
|
+
if (!declared || !inWorkspace || !scopedRead) {
|
|
780
|
+
permissionDenied = true;
|
|
781
|
+
const option = ctx?.params?.options?.find((o) => String(o.kind).startsWith('reject'));
|
|
782
|
+
return option
|
|
783
|
+
? { outcome: { outcome: 'selected', optionId: option.optionId } }
|
|
784
|
+
: { outcome: { outcome: 'cancelled' } };
|
|
785
|
+
}
|
|
786
|
+
const option = ctx?.params?.options?.find((o) => String(o.kind).startsWith('allow'));
|
|
787
|
+
return option
|
|
788
|
+
? { outcome: { outcome: 'selected', optionId: option.optionId } }
|
|
789
|
+
: { outcome: { outcome: 'cancelled' } };
|
|
790
|
+
})
|
|
791
|
+
.connectWith(stream, async (ctx) => {
|
|
792
|
+
await ctx.request(methods.agent.initialize, {
|
|
793
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
794
|
+
clientInfo: { name: 'CFlow', version: ADAPTER_BUILD },
|
|
795
|
+
clientCapabilities: {
|
|
796
|
+
plan: {},
|
|
797
|
+
session: {},
|
|
798
|
+
},
|
|
799
|
+
});
|
|
800
|
+
return ctx.buildSession(cwd).withSession(async (session) => {
|
|
801
|
+
const promptResult = session.prompt(prompt);
|
|
802
|
+
const text = await session.readText();
|
|
803
|
+
const response = await promptResult;
|
|
804
|
+
if (response.stopReason !== 'end_turn') {
|
|
805
|
+
const reason = String(response.stopReason).toUpperCase();
|
|
806
|
+
throw new RuntimeExecutionException({
|
|
807
|
+
layer: 'adapter',
|
|
808
|
+
code: permissionDenied ? 'PERMISSION_DENIED' : `ACP_STOP_${reason}`,
|
|
809
|
+
message: permissionDenied
|
|
810
|
+
? '无法执行请求的文件或命令操作:能力包未声明该权限或路径超出工作区。'
|
|
811
|
+
: reason === 'CANCELLED'
|
|
812
|
+
? 'Agent 在执行过程中停止'
|
|
813
|
+
: `Agent 停止执行(${String(response.stopReason)})`,
|
|
814
|
+
retryable: false,
|
|
815
|
+
effectState: permissionDenied ? 'none' : 'unknown',
|
|
816
|
+
});
|
|
817
|
+
}
|
|
818
|
+
if (Buffer.byteLength(text, 'utf8') > profile.maxOutputBytes)
|
|
819
|
+
throw new Error('RUNTIME_OUTPUT_LIMIT_EXCEEDED');
|
|
820
|
+
return text;
|
|
821
|
+
});
|
|
822
|
+
});
|
|
823
|
+
}
|
|
824
|
+
catch (error) {
|
|
825
|
+
const detail = stderrText.trim().slice(-800);
|
|
826
|
+
if (error instanceof Error) {
|
|
827
|
+
if (signal.aborted)
|
|
828
|
+
throw new RuntimeExecutionException({
|
|
829
|
+
layer: 'runtime',
|
|
830
|
+
code: signal.reason?.name === 'TimeoutError' ? 'RUNTIME_TIMEOUT' : 'RUN_CANCELLED',
|
|
831
|
+
message: signal.reason?.name === 'TimeoutError' ? '运行超时' : '运行已取消',
|
|
832
|
+
retryable: signal.reason?.name === 'TimeoutError',
|
|
833
|
+
effectState: effects.some((effect) => effect.type === 'file-write' || effect.type === 'command')
|
|
834
|
+
? 'unknown'
|
|
835
|
+
: 'none',
|
|
836
|
+
});
|
|
837
|
+
throw new Error(detail ? `${error.message}:${detail}` : error.message);
|
|
838
|
+
}
|
|
839
|
+
throw error;
|
|
840
|
+
}
|
|
841
|
+
finally {
|
|
842
|
+
signal.removeEventListener('abort', relayAbort);
|
|
843
|
+
terminate();
|
|
844
|
+
await Promise.race([
|
|
845
|
+
exitPromise,
|
|
846
|
+
new Promise((resolveDelay) => setTimeout(resolveDelay, 1_500)),
|
|
847
|
+
]);
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
}
|