@kb-labs/workflow-runtime 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/README.md +313 -0
- package/dist/index.d.ts +524 -0
- package/dist/index.js +873 -0
- package/dist/index.js.map +1 -0
- package/package.json +61 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,873 @@
|
|
|
1
|
+
import { resolve, dirname, join, relative } from 'path';
|
|
2
|
+
import { access, readFile, mkdir, writeFile } from 'fs/promises';
|
|
3
|
+
import { execaCommand } from 'execa';
|
|
4
|
+
import { randomUUID } from 'crypto';
|
|
5
|
+
import { getHandlerPermissions } from '@kb-labs/plugin-contracts';
|
|
6
|
+
import { z } from 'zod';
|
|
7
|
+
import fg from 'fast-glob';
|
|
8
|
+
import { parse } from 'yaml';
|
|
9
|
+
import { WorkflowSpecSchema } from '@kb-labs/workflow-contracts';
|
|
10
|
+
export { FileSystemArtifactClient, createFileSystemArtifactClient } from '@kb-labs/workflow-artifacts';
|
|
11
|
+
|
|
12
|
+
var __defProp = Object.defineProperty;
|
|
13
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
14
|
+
var __esm = (fn, res) => function __init() {
|
|
15
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
16
|
+
};
|
|
17
|
+
var __export = (target, all) => {
|
|
18
|
+
for (var name in all)
|
|
19
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
// src/registry/plugin-workflows.ts
|
|
23
|
+
var plugin_workflows_exports = {};
|
|
24
|
+
__export(plugin_workflows_exports, {
|
|
25
|
+
extractWorkflows: () => extractWorkflows,
|
|
26
|
+
findWorkflow: () => findWorkflow
|
|
27
|
+
});
|
|
28
|
+
function extractSnapshotManifests(snapshot) {
|
|
29
|
+
return (snapshot.manifests || []).map((entry) => ({
|
|
30
|
+
pluginId: entry.pluginId,
|
|
31
|
+
manifest: entry.manifest,
|
|
32
|
+
pluginRoot: entry.pluginRoot
|
|
33
|
+
}));
|
|
34
|
+
}
|
|
35
|
+
async function extractWorkflows(snapshot) {
|
|
36
|
+
const manifests = extractSnapshotManifests(snapshot);
|
|
37
|
+
const workflows = [];
|
|
38
|
+
for (const entry of manifests) {
|
|
39
|
+
const { manifest, pluginRoot } = entry;
|
|
40
|
+
const workflowHandlers = manifest.workflows?.handlers ?? [];
|
|
41
|
+
if (workflowHandlers.length === 0) {
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
let packageRoot = pluginRoot;
|
|
45
|
+
let currentDir = resolve(pluginRoot);
|
|
46
|
+
while (currentDir !== dirname(currentDir)) {
|
|
47
|
+
try {
|
|
48
|
+
const pkgPath = join(currentDir, "package.json");
|
|
49
|
+
await access(pkgPath);
|
|
50
|
+
packageRoot = currentDir;
|
|
51
|
+
break;
|
|
52
|
+
} catch {
|
|
53
|
+
currentDir = dirname(currentDir);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
for (const wfHandler of workflowHandlers) {
|
|
57
|
+
const id = `plugin:${entry.pluginId}/${wfHandler.id}`;
|
|
58
|
+
const filePath = resolve(packageRoot, wfHandler.handler);
|
|
59
|
+
workflows.push({
|
|
60
|
+
id,
|
|
61
|
+
source: "plugin",
|
|
62
|
+
filePath,
|
|
63
|
+
description: wfHandler.describe,
|
|
64
|
+
tags: void 0,
|
|
65
|
+
// V3 doesn't have tags on workflow handlers
|
|
66
|
+
metadata: {
|
|
67
|
+
pluginId: entry.pluginId,
|
|
68
|
+
pluginVersion: manifest.version
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return workflows;
|
|
74
|
+
}
|
|
75
|
+
async function findWorkflow(snapshot, id) {
|
|
76
|
+
const cleanId = id.startsWith("plugin:") ? id.slice("plugin:".length) : id;
|
|
77
|
+
const workflows = await extractWorkflows(snapshot);
|
|
78
|
+
return workflows.find((w) => w.id === id || w.id.endsWith(":" + cleanId)) ?? null;
|
|
79
|
+
}
|
|
80
|
+
var init_plugin_workflows = __esm({
|
|
81
|
+
"src/registry/plugin-workflows.ts"() {
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// src/context.ts
|
|
86
|
+
function createStepContext(input) {
|
|
87
|
+
return {
|
|
88
|
+
runId: input.runId,
|
|
89
|
+
jobId: input.jobId,
|
|
90
|
+
stepId: input.stepId,
|
|
91
|
+
attempt: input.attempt ?? 0,
|
|
92
|
+
env: { ...process.env, ...input.env ?? {} },
|
|
93
|
+
secrets: input.secrets ?? {},
|
|
94
|
+
artifacts: input.artifacts,
|
|
95
|
+
events: input.events,
|
|
96
|
+
logger: input.logger,
|
|
97
|
+
// Use provided logger (platform.logger)
|
|
98
|
+
trace: input.trace
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
function resolveCommand(step) {
|
|
102
|
+
const withBlock = step.with ?? {};
|
|
103
|
+
const commandField = withBlock.command ?? withBlock.run ?? withBlock.script;
|
|
104
|
+
return typeof commandField === "string" ? commandField : null;
|
|
105
|
+
}
|
|
106
|
+
var LocalRunner = class {
|
|
107
|
+
shell;
|
|
108
|
+
constructor(options = {}) {
|
|
109
|
+
this.shell = options.shell ?? process.env.SHELL ?? "bash";
|
|
110
|
+
}
|
|
111
|
+
async execute(request) {
|
|
112
|
+
const { spec, context } = request;
|
|
113
|
+
const command = resolveCommand(spec);
|
|
114
|
+
if (spec.uses && spec.uses !== "builtin:shell") {
|
|
115
|
+
context.logger.error(`LocalRunner cannot execute ${spec.uses}`, {
|
|
116
|
+
stepId: context.stepId
|
|
117
|
+
});
|
|
118
|
+
return {
|
|
119
|
+
status: "failed",
|
|
120
|
+
error: {
|
|
121
|
+
message: `Local runner cannot execute step with uses="${spec.uses}"`,
|
|
122
|
+
code: "UNSUPPORTED_STEP"
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
if (!command) {
|
|
127
|
+
context.logger.error("LocalRunner missing command", {
|
|
128
|
+
stepId: context.stepId
|
|
129
|
+
});
|
|
130
|
+
return {
|
|
131
|
+
status: "failed",
|
|
132
|
+
error: {
|
|
133
|
+
message: 'Local runner requires "with.command" (or with.run/with.script) to be specified',
|
|
134
|
+
code: "INVALID_STEP"
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
const cwd = request.workspace ?? process.cwd();
|
|
139
|
+
const env = {
|
|
140
|
+
...process.env,
|
|
141
|
+
...context.env,
|
|
142
|
+
...context.secrets
|
|
143
|
+
};
|
|
144
|
+
if (request.signal?.aborted) {
|
|
145
|
+
return buildCancelledResult(request.signal);
|
|
146
|
+
}
|
|
147
|
+
context.logger.info("Executing builtin shell step", {
|
|
148
|
+
command,
|
|
149
|
+
cwd,
|
|
150
|
+
stepId: context.stepId
|
|
151
|
+
});
|
|
152
|
+
try {
|
|
153
|
+
const result = await execaCommand(command, {
|
|
154
|
+
cwd,
|
|
155
|
+
shell: this.shell,
|
|
156
|
+
env,
|
|
157
|
+
stdio: "pipe",
|
|
158
|
+
signal: request.signal
|
|
159
|
+
});
|
|
160
|
+
context.logger.info("Shell step completed", {
|
|
161
|
+
stepId: context.stepId,
|
|
162
|
+
exitCode: result.exitCode
|
|
163
|
+
});
|
|
164
|
+
return {
|
|
165
|
+
status: "success",
|
|
166
|
+
outputs: {
|
|
167
|
+
stdout: result.stdout,
|
|
168
|
+
stderr: result.stderr,
|
|
169
|
+
exitCode: result.exitCode
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
} catch (error) {
|
|
173
|
+
if (request.signal?.aborted) {
|
|
174
|
+
return buildCancelledResult(request.signal, error);
|
|
175
|
+
}
|
|
176
|
+
const message = error instanceof Error ? error.message : "Shell step failed";
|
|
177
|
+
const exitCode = typeof error?.exitCode === "number" ? error.exitCode : void 0;
|
|
178
|
+
context.logger.error("Shell step failed", {
|
|
179
|
+
stepId: context.stepId,
|
|
180
|
+
error: message,
|
|
181
|
+
exitCode
|
|
182
|
+
});
|
|
183
|
+
return {
|
|
184
|
+
status: "failed",
|
|
185
|
+
error: {
|
|
186
|
+
message,
|
|
187
|
+
code: "STEP_EXECUTION_FAILED",
|
|
188
|
+
details: {
|
|
189
|
+
exitCode
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
function buildCancelledResult(signal, error) {
|
|
197
|
+
const reason = error instanceof Error ? error.message : signalReason(signal) ?? "Step execution cancelled";
|
|
198
|
+
return {
|
|
199
|
+
status: "cancelled",
|
|
200
|
+
error: {
|
|
201
|
+
message: reason,
|
|
202
|
+
code: "STEP_CANCELLED"
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
function signalReason(signal) {
|
|
207
|
+
if (!signal.aborted) {
|
|
208
|
+
return void 0;
|
|
209
|
+
}
|
|
210
|
+
const reason = signal.reason;
|
|
211
|
+
if (reason instanceof Error) {
|
|
212
|
+
return reason.message;
|
|
213
|
+
}
|
|
214
|
+
if (typeof reason === "string") {
|
|
215
|
+
return reason;
|
|
216
|
+
}
|
|
217
|
+
return void 0;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// src/runners/output-normalizer.ts
|
|
221
|
+
function isCommandResult(value) {
|
|
222
|
+
return typeof value === "object" && value !== null && "exitCode" in value && "result" in value && typeof value.exitCode === "number";
|
|
223
|
+
}
|
|
224
|
+
function toWorkflowOutputs(data) {
|
|
225
|
+
if (isCommandResult(data)) {
|
|
226
|
+
const inner = data.result;
|
|
227
|
+
if (typeof inner === "object" && inner !== null) {
|
|
228
|
+
return inner;
|
|
229
|
+
}
|
|
230
|
+
return inner !== void 0 && inner !== null ? { result: inner } : {};
|
|
231
|
+
}
|
|
232
|
+
if (typeof data === "object" && data !== null) {
|
|
233
|
+
return data;
|
|
234
|
+
}
|
|
235
|
+
if (data !== void 0 && data !== null) {
|
|
236
|
+
return { result: data };
|
|
237
|
+
}
|
|
238
|
+
return {};
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// src/runners/sandbox-runner.ts
|
|
242
|
+
var SandboxRunner = class {
|
|
243
|
+
backend;
|
|
244
|
+
cliApi;
|
|
245
|
+
workspaceRoot;
|
|
246
|
+
defaultTimeout;
|
|
247
|
+
analytics;
|
|
248
|
+
logger;
|
|
249
|
+
constructor(options) {
|
|
250
|
+
this.backend = options.backend;
|
|
251
|
+
this.cliApi = options.cliApi;
|
|
252
|
+
this.workspaceRoot = options.workspaceRoot ?? process.cwd();
|
|
253
|
+
this.defaultTimeout = options.defaultTimeout ?? 12e4;
|
|
254
|
+
this.analytics = options.analytics;
|
|
255
|
+
}
|
|
256
|
+
async execute(request) {
|
|
257
|
+
const { spec, context, signal } = request;
|
|
258
|
+
const startTime = Date.now();
|
|
259
|
+
if (signal?.aborted) {
|
|
260
|
+
return buildCancelledResult2(signal);
|
|
261
|
+
}
|
|
262
|
+
if (!spec.uses) {
|
|
263
|
+
return this.buildValidationError(context, 'Sandbox runner requires "uses" field to specify plugin handler');
|
|
264
|
+
}
|
|
265
|
+
const resolution = await this.tryResolveCommand(spec, request, context);
|
|
266
|
+
if (!resolution.ok) {
|
|
267
|
+
return resolution.error;
|
|
268
|
+
}
|
|
269
|
+
const executionRequest = this.buildExecutionRequest(resolution.value, request, context);
|
|
270
|
+
context.logger.info("Executing plugin handler", {
|
|
271
|
+
stepId: context.stepId,
|
|
272
|
+
pluginId: resolution.value.pluginId,
|
|
273
|
+
handler: resolution.value.handler,
|
|
274
|
+
executionId: executionRequest.executionId
|
|
275
|
+
});
|
|
276
|
+
this.analytics?.track("workflow.sandbox.execution.started", {
|
|
277
|
+
stepId: context.stepId,
|
|
278
|
+
pluginId: resolution.value.pluginId,
|
|
279
|
+
handler: resolution.value.handler,
|
|
280
|
+
uses: spec.uses
|
|
281
|
+
}).catch(() => {
|
|
282
|
+
});
|
|
283
|
+
const result = await this.backend.execute(executionRequest, { signal, onLog: context.onLog });
|
|
284
|
+
const duration = Date.now() - startTime;
|
|
285
|
+
if (result.ok) {
|
|
286
|
+
this.analytics?.track("workflow.sandbox.execution.completed", {
|
|
287
|
+
stepId: context.stepId,
|
|
288
|
+
pluginId: resolution.value.pluginId,
|
|
289
|
+
handler: resolution.value.handler,
|
|
290
|
+
durationMs: duration
|
|
291
|
+
}).catch(() => {
|
|
292
|
+
});
|
|
293
|
+
} else {
|
|
294
|
+
this.analytics?.track("workflow.sandbox.execution.failed", {
|
|
295
|
+
stepId: context.stepId,
|
|
296
|
+
pluginId: resolution.value.pluginId,
|
|
297
|
+
handler: resolution.value.handler,
|
|
298
|
+
errorCode: result.error?.code,
|
|
299
|
+
errorMessage: result.error?.message,
|
|
300
|
+
durationMs: duration
|
|
301
|
+
}).catch(() => {
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
return this.mapExecutionResult(result, executionRequest.executionId, context, signal);
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* Validate step spec and build error result if invalid
|
|
308
|
+
*/
|
|
309
|
+
buildValidationError(context, message) {
|
|
310
|
+
context.logger.error("SandboxRunner validation failed", { stepId: context.stepId, message });
|
|
311
|
+
return {
|
|
312
|
+
status: "failed",
|
|
313
|
+
error: { message, code: "INVALID_STEP" }
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* Try to resolve command, returning result wrapper
|
|
318
|
+
*/
|
|
319
|
+
async tryResolveCommand(spec, request, context) {
|
|
320
|
+
try {
|
|
321
|
+
const resolution = await this.resolveCommand(spec, request);
|
|
322
|
+
return { ok: true, value: resolution };
|
|
323
|
+
} catch (error) {
|
|
324
|
+
const message = error instanceof Error ? error.message : "Failed to resolve plugin command";
|
|
325
|
+
context.logger.error("Plugin command resolution failed", {
|
|
326
|
+
stepId: context.stepId,
|
|
327
|
+
uses: spec.uses,
|
|
328
|
+
error: message
|
|
329
|
+
});
|
|
330
|
+
return {
|
|
331
|
+
ok: false,
|
|
332
|
+
error: {
|
|
333
|
+
status: "failed",
|
|
334
|
+
error: { message, code: "COMMAND_RESOLUTION_FAILED" }
|
|
335
|
+
}
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
/**
|
|
340
|
+
* Build ExecutionRequest from resolved command
|
|
341
|
+
*/
|
|
342
|
+
buildExecutionRequest(resolution, request, context) {
|
|
343
|
+
const requestId = context.trace?.traceId ?? randomUUID();
|
|
344
|
+
const traceId = context.trace?.traceId ?? requestId;
|
|
345
|
+
const executionId = `exec_${context.stepId}_${Date.now()}_${randomUUID().slice(0, 8)}`;
|
|
346
|
+
const spanId = executionId;
|
|
347
|
+
const invocationId = executionId;
|
|
348
|
+
const hostContext = {
|
|
349
|
+
host: "workflow",
|
|
350
|
+
workflowId: context.runId,
|
|
351
|
+
runId: context.runId,
|
|
352
|
+
jobId: context.jobId,
|
|
353
|
+
stepId: context.stepId,
|
|
354
|
+
attempt: context.attempt,
|
|
355
|
+
input: resolution.input
|
|
356
|
+
};
|
|
357
|
+
const descriptor = {
|
|
358
|
+
hostType: "workflow",
|
|
359
|
+
pluginId: resolution.pluginId,
|
|
360
|
+
pluginVersion: resolution.pluginVersion,
|
|
361
|
+
requestId,
|
|
362
|
+
permissions: resolution.permissions,
|
|
363
|
+
hostContext,
|
|
364
|
+
configSection: resolution.configSection
|
|
365
|
+
// For useConfig() auto-detection
|
|
366
|
+
};
|
|
367
|
+
Object.assign(descriptor, {
|
|
368
|
+
traceId,
|
|
369
|
+
spanId,
|
|
370
|
+
invocationId,
|
|
371
|
+
executionId
|
|
372
|
+
});
|
|
373
|
+
return {
|
|
374
|
+
executionId,
|
|
375
|
+
descriptor,
|
|
376
|
+
pluginRoot: resolution.pluginRoot,
|
|
377
|
+
handlerRef: resolution.handler,
|
|
378
|
+
input: resolution.input,
|
|
379
|
+
workspace: request.workspace ? {
|
|
380
|
+
type: "local",
|
|
381
|
+
cwd: request.workspace
|
|
382
|
+
} : void 0,
|
|
383
|
+
timeoutMs: resolution.permissions.quotas?.timeoutMs ?? this.defaultTimeout,
|
|
384
|
+
target: request.target
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
/**
|
|
388
|
+
* Map ExecutionResult to StepExecutionResult
|
|
389
|
+
*/
|
|
390
|
+
// eslint-disable-next-line sonarjs/cognitive-complexity -- Result mapping logic: handles success/failure/cancelled states, conditional stdout/stderr logging, debug metadata extraction, and error code translation
|
|
391
|
+
mapExecutionResult(result, executionId, context, signal) {
|
|
392
|
+
if (result.ok) {
|
|
393
|
+
const data = result.data;
|
|
394
|
+
context.logger.debug("mapExecutionResult received data", {
|
|
395
|
+
stepId: context.stepId,
|
|
396
|
+
executionId,
|
|
397
|
+
dataType: typeof data,
|
|
398
|
+
dataKeys: data && typeof data === "object" ? Object.keys(data) : [],
|
|
399
|
+
hasStdout: !!(data && typeof data === "object" && data.stdout),
|
|
400
|
+
hasStderr: !!(data && typeof data === "object" && data.stderr)
|
|
401
|
+
});
|
|
402
|
+
const logMeta = {
|
|
403
|
+
stepId: context.stepId,
|
|
404
|
+
executionId,
|
|
405
|
+
executionTimeMs: result.executionTimeMs
|
|
406
|
+
};
|
|
407
|
+
if (data && typeof data === "object") {
|
|
408
|
+
if (data.stdout) {
|
|
409
|
+
logMeta.stdout = data.stdout;
|
|
410
|
+
}
|
|
411
|
+
if (data.stderr) {
|
|
412
|
+
logMeta.stderr = data.stderr;
|
|
413
|
+
}
|
|
414
|
+
if (data.exitCode !== void 0) {
|
|
415
|
+
logMeta.exitCode = data.exitCode;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
context.logger.info("Plugin handler completed", logMeta);
|
|
419
|
+
if (data && typeof data === "object" && data.ok === false) {
|
|
420
|
+
const message = data.stderr ? String(data.stderr).slice(0, 500) : `Step handler reported failure (exitCode: ${data.exitCode ?? "unknown"})`;
|
|
421
|
+
context.logger.error("Plugin handler reported failure via ok:false", {
|
|
422
|
+
stepId: context.stepId,
|
|
423
|
+
exitCode: data.exitCode,
|
|
424
|
+
stderr: data.stderr
|
|
425
|
+
});
|
|
426
|
+
return {
|
|
427
|
+
status: "failed",
|
|
428
|
+
error: {
|
|
429
|
+
message,
|
|
430
|
+
code: "HANDLER_REPORTED_FAILURE"
|
|
431
|
+
}
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
return {
|
|
435
|
+
status: "success",
|
|
436
|
+
outputs: toWorkflowOutputs(result.data)
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
if (signal?.aborted || result.error?.code === "ABORTED") {
|
|
440
|
+
return buildCancelledResult2(signal, result.error);
|
|
441
|
+
}
|
|
442
|
+
context.logger.error("Plugin handler failed", {
|
|
443
|
+
stepId: context.stepId,
|
|
444
|
+
executionId,
|
|
445
|
+
error: result.error?.message,
|
|
446
|
+
code: result.error?.code
|
|
447
|
+
});
|
|
448
|
+
return {
|
|
449
|
+
status: "failed",
|
|
450
|
+
error: {
|
|
451
|
+
message: result.error?.message ?? "Plugin execution failed",
|
|
452
|
+
code: result.error?.code ?? "UNKNOWN_ERROR",
|
|
453
|
+
stack: result.error?.stack,
|
|
454
|
+
details: result.error?.details
|
|
455
|
+
}
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
/**
|
|
459
|
+
* Resolve command reference to plugin handler.
|
|
460
|
+
*
|
|
461
|
+
* Supports three formats:
|
|
462
|
+
* - `plugin:id/handler` - workflow handler (native)
|
|
463
|
+
* - `command:name` - CLI command (via adapter)
|
|
464
|
+
* - `builtin:shell` - built-in shell execution
|
|
465
|
+
*/
|
|
466
|
+
async resolveCommand(spec, request) {
|
|
467
|
+
const uses = spec.uses;
|
|
468
|
+
const input = spec.with ?? {};
|
|
469
|
+
if (uses.startsWith("plugin:")) {
|
|
470
|
+
return this.resolvePluginHandler(uses, input);
|
|
471
|
+
}
|
|
472
|
+
if (uses.startsWith("command:")) {
|
|
473
|
+
return this.resolveCLICommand(uses, input, request);
|
|
474
|
+
}
|
|
475
|
+
if (uses === "builtin:shell") {
|
|
476
|
+
return this.resolveBuiltinShell(spec);
|
|
477
|
+
}
|
|
478
|
+
if (uses === "builtin:approval" || uses === "builtin:gate") {
|
|
479
|
+
throw new Error(`${uses} is handled by the workflow worker, not the sandbox runner`);
|
|
480
|
+
}
|
|
481
|
+
throw new Error(`Unsupported uses format: ${uses}. Expected "plugin:...", "command:...", or "builtin:shell"`);
|
|
482
|
+
}
|
|
483
|
+
/**
|
|
484
|
+
* Resolve plugin handler reference.
|
|
485
|
+
* Format: `plugin:id/handler` or `plugin:id/path/to/handler`
|
|
486
|
+
*/
|
|
487
|
+
async resolvePluginHandler(uses, input) {
|
|
488
|
+
const pluginRef = uses.slice("plugin:".length);
|
|
489
|
+
const [pluginId, ...handlerParts] = pluginRef.split("/");
|
|
490
|
+
if (!pluginId || handlerParts.length === 0) {
|
|
491
|
+
throw new Error(`Invalid plugin reference: ${uses}. Expected "plugin:id/handler"`);
|
|
492
|
+
}
|
|
493
|
+
const handlerName = handlerParts.join("/");
|
|
494
|
+
const snapshot = this.cliApi.snapshot();
|
|
495
|
+
const entry = snapshot.manifests?.find(
|
|
496
|
+
(m) => m.pluginId === pluginId || m.pluginId.endsWith(`/${pluginId}`)
|
|
497
|
+
);
|
|
498
|
+
if (!entry) {
|
|
499
|
+
throw new Error(`Plugin not found: ${pluginId}`);
|
|
500
|
+
}
|
|
501
|
+
const workflowHandlers = entry.manifest.workflows?.handlers ?? [];
|
|
502
|
+
const handler = workflowHandlers.find((h) => h.id === handlerName);
|
|
503
|
+
if (!handler) {
|
|
504
|
+
throw new Error(`Workflow handler not found: ${handlerName} in plugin ${pluginId}`);
|
|
505
|
+
}
|
|
506
|
+
return {
|
|
507
|
+
pluginId,
|
|
508
|
+
pluginVersion: entry.manifest.version,
|
|
509
|
+
pluginRoot: entry.pluginRoot,
|
|
510
|
+
handler: handler.handler,
|
|
511
|
+
// File path from manifest
|
|
512
|
+
input,
|
|
513
|
+
permissions: getHandlerPermissions(entry.manifest, "workflow", handlerName)
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
/**
|
|
517
|
+
* Resolve CLI command to plugin handler (with adapter).
|
|
518
|
+
*
|
|
519
|
+
* Format: `command:name` (e.g., `command:mind:rag-index`)
|
|
520
|
+
*
|
|
521
|
+
* This uses the CLI Adapter pattern to make CLI commands work in workflow context:
|
|
522
|
+
* - Searches for CLI command in plugin manifests
|
|
523
|
+
* - Wraps workflow input in CLI-compatible format { argv, flags, cwd }
|
|
524
|
+
* - Allows reusing existing CLI commands without writing workflow handlers
|
|
525
|
+
*/
|
|
526
|
+
async resolveCLICommand(uses, input, request) {
|
|
527
|
+
const commandName = uses.slice("command:".length);
|
|
528
|
+
const snapshot = this.cliApi.snapshot();
|
|
529
|
+
for (const entry of snapshot.manifests ?? []) {
|
|
530
|
+
const commands = entry.manifest.cli?.commands ?? [];
|
|
531
|
+
const command = commands.find((c) => c.id === commandName);
|
|
532
|
+
if (command) {
|
|
533
|
+
return {
|
|
534
|
+
pluginId: entry.pluginId,
|
|
535
|
+
pluginVersion: entry.manifest.version,
|
|
536
|
+
pluginRoot: entry.pluginRoot,
|
|
537
|
+
handler: command.handler,
|
|
538
|
+
input: this.adaptToCLIFormat(input, request),
|
|
539
|
+
// CLI Adapter
|
|
540
|
+
permissions: getHandlerPermissions(entry.manifest, "cli", commandName),
|
|
541
|
+
configSection: entry.manifest.configSection
|
|
542
|
+
// For useConfig() auto-detection
|
|
543
|
+
};
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
throw new Error(`CLI command not found: ${commandName}`);
|
|
547
|
+
}
|
|
548
|
+
/**
|
|
549
|
+
* CLI Adapter: Convert workflow input to CLI-compatible format.
|
|
550
|
+
*
|
|
551
|
+
* Transforms:
|
|
552
|
+
* { scope: "default", incremental: true }
|
|
553
|
+
* Into:
|
|
554
|
+
* { argv: [], flags: { scope: "default", incremental: true }, cwd: "/workspace" }
|
|
555
|
+
*
|
|
556
|
+
* This allows CLI commands to work in workflow context without modification.
|
|
557
|
+
*/
|
|
558
|
+
adaptToCLIFormat(input, request) {
|
|
559
|
+
if (input && typeof input === "object" && ("argv" in input || "flags" in input)) {
|
|
560
|
+
return input;
|
|
561
|
+
}
|
|
562
|
+
return {
|
|
563
|
+
argv: [],
|
|
564
|
+
flags: input || {},
|
|
565
|
+
cwd: request.workspace || this.workspaceRoot
|
|
566
|
+
};
|
|
567
|
+
}
|
|
568
|
+
/**
|
|
569
|
+
* Resolve builtin:shell to built-in shell handler.
|
|
570
|
+
*
|
|
571
|
+
* Returns a resolution pointing to the builtin-handlers/shell.js file
|
|
572
|
+
* that will be executed through ExecutionBackend.
|
|
573
|
+
*/
|
|
574
|
+
async resolveBuiltinShell(spec) {
|
|
575
|
+
const builtinsUrl = await import.meta.resolve("@kb-labs/workflow-builtins");
|
|
576
|
+
const builtinsPath = builtinsUrl.replace("file://", "").replace("/dist/index.js", "");
|
|
577
|
+
const withBlock = spec.with ?? {};
|
|
578
|
+
const command = withBlock.command ?? withBlock.run ?? withBlock.script;
|
|
579
|
+
if (typeof command !== "string") {
|
|
580
|
+
throw new Error(
|
|
581
|
+
'builtin:shell requires "with.command" (or with.run/with.script) to be a string'
|
|
582
|
+
);
|
|
583
|
+
}
|
|
584
|
+
const shellInput = {
|
|
585
|
+
command,
|
|
586
|
+
env: typeof withBlock.env === "object" ? withBlock.env : void 0,
|
|
587
|
+
timeout: typeof withBlock.timeout === "number" ? withBlock.timeout : void 0,
|
|
588
|
+
throwOnError: typeof withBlock.throwOnError === "boolean" ? withBlock.throwOnError : false
|
|
589
|
+
};
|
|
590
|
+
return {
|
|
591
|
+
pluginId: "@kb-labs/workflow-builtins",
|
|
592
|
+
pluginVersion: "0.1.0",
|
|
593
|
+
pluginRoot: builtinsPath,
|
|
594
|
+
handler: "dist/shell.js",
|
|
595
|
+
// Relative path from pluginRoot
|
|
596
|
+
input: shellInput,
|
|
597
|
+
permissions: {
|
|
598
|
+
shell: { allow: ["*"] }
|
|
599
|
+
// builtin:shell needs shell access by definition
|
|
600
|
+
}
|
|
601
|
+
};
|
|
602
|
+
}
|
|
603
|
+
};
|
|
604
|
+
function buildCancelledResult2(signal, error) {
|
|
605
|
+
const reason = error?.message ?? signalReason2(signal) ?? "Step execution cancelled";
|
|
606
|
+
return {
|
|
607
|
+
status: "cancelled",
|
|
608
|
+
error: {
|
|
609
|
+
message: reason,
|
|
610
|
+
code: "STEP_CANCELLED"
|
|
611
|
+
}
|
|
612
|
+
};
|
|
613
|
+
}
|
|
614
|
+
function signalReason2(signal) {
|
|
615
|
+
if (!signal?.aborted) {
|
|
616
|
+
return void 0;
|
|
617
|
+
}
|
|
618
|
+
const reason = signal.reason;
|
|
619
|
+
if (reason instanceof Error) {
|
|
620
|
+
return reason.message;
|
|
621
|
+
}
|
|
622
|
+
if (typeof reason === "string") {
|
|
623
|
+
return reason;
|
|
624
|
+
}
|
|
625
|
+
return void 0;
|
|
626
|
+
}
|
|
627
|
+
var RemoteMarketplaceSourceSchema = z.object({
|
|
628
|
+
name: z.string().min(1),
|
|
629
|
+
url: z.string().url(),
|
|
630
|
+
ref: z.string().optional(),
|
|
631
|
+
// branch/tag, default: 'main'
|
|
632
|
+
path: z.string().optional()
|
|
633
|
+
// subdirectory in repo, default: '/'
|
|
634
|
+
});
|
|
635
|
+
var BudgetConfigSchema = z.object({
|
|
636
|
+
enabled: z.boolean().default(false),
|
|
637
|
+
limit: z.number().positive().optional(),
|
|
638
|
+
// Total budget limit (in cost units)
|
|
639
|
+
period: z.enum(["run", "day", "week", "month"]).default("run"),
|
|
640
|
+
action: z.enum(["warn", "fail", "cancel"]).default("warn"),
|
|
641
|
+
// Extension point: custom cost calculator plugin
|
|
642
|
+
costCalculator: z.string().optional()
|
|
643
|
+
});
|
|
644
|
+
var WorkflowConfigSchema = z.object({
|
|
645
|
+
workspaces: z.array(z.string()).default([".kb/workflows/**/*.yml"]),
|
|
646
|
+
plugins: z.boolean().default(true),
|
|
647
|
+
remotes: z.array(RemoteMarketplaceSourceSchema).optional(),
|
|
648
|
+
maxDepth: z.number().int().positive().default(2),
|
|
649
|
+
budget: BudgetConfigSchema.optional(),
|
|
650
|
+
defaults: z.object({
|
|
651
|
+
mode: z.enum(["wait", "fire-and-forget"]).default("wait"),
|
|
652
|
+
inheritEnv: z.boolean().default(true)
|
|
653
|
+
}).optional()
|
|
654
|
+
});
|
|
655
|
+
async function loadWorkflowConfig(workspaceRoot) {
|
|
656
|
+
const configPath = join(workspaceRoot, "kb.config.json");
|
|
657
|
+
try {
|
|
658
|
+
const raw = await readFile(configPath, "utf-8");
|
|
659
|
+
const config = JSON.parse(raw);
|
|
660
|
+
if (!config.workflow) {
|
|
661
|
+
return WorkflowConfigSchema.parse({});
|
|
662
|
+
}
|
|
663
|
+
return WorkflowConfigSchema.parse(config.workflow);
|
|
664
|
+
} catch (error) {
|
|
665
|
+
if (error instanceof Error && (error.message.includes("ENOENT") || error.message.includes("Unexpected token"))) {
|
|
666
|
+
return WorkflowConfigSchema.parse({});
|
|
667
|
+
}
|
|
668
|
+
throw error;
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
async function saveWorkflowConfig(workspaceRoot, updates) {
|
|
672
|
+
const configPath = join(workspaceRoot, "kb.config.json");
|
|
673
|
+
let existingConfig = {};
|
|
674
|
+
try {
|
|
675
|
+
const raw = await readFile(configPath, "utf-8");
|
|
676
|
+
existingConfig = JSON.parse(raw);
|
|
677
|
+
} catch {
|
|
678
|
+
existingConfig = {};
|
|
679
|
+
}
|
|
680
|
+
const currentWorkflow = existingConfig.workflow ?? {};
|
|
681
|
+
const updatedWorkflow = {
|
|
682
|
+
...currentWorkflow,
|
|
683
|
+
...updates,
|
|
684
|
+
// Deep merge for arrays (remotes)
|
|
685
|
+
remotes: updates.remotes ?? currentWorkflow.remotes
|
|
686
|
+
};
|
|
687
|
+
const updatedConfig = {
|
|
688
|
+
...existingConfig,
|
|
689
|
+
workflow: updatedWorkflow
|
|
690
|
+
};
|
|
691
|
+
await mkdir(dirname(configPath), { recursive: true });
|
|
692
|
+
await writeFile(
|
|
693
|
+
configPath,
|
|
694
|
+
JSON.stringify(updatedConfig, null, 2) + "\n",
|
|
695
|
+
"utf-8"
|
|
696
|
+
);
|
|
697
|
+
}
|
|
698
|
+
var WorkspaceWorkflowRegistry = class {
|
|
699
|
+
constructor(config) {
|
|
700
|
+
this.config = config;
|
|
701
|
+
}
|
|
702
|
+
cache = null;
|
|
703
|
+
async list() {
|
|
704
|
+
if (this.cache) {
|
|
705
|
+
return this.cache;
|
|
706
|
+
}
|
|
707
|
+
const workflows = [];
|
|
708
|
+
const files = await fg(this.config.patterns, {
|
|
709
|
+
cwd: this.config.workspaceRoot,
|
|
710
|
+
absolute: true,
|
|
711
|
+
onlyFiles: true,
|
|
712
|
+
ignore: ["node_modules/**", "dist/**", ".git/**"]
|
|
713
|
+
});
|
|
714
|
+
const results = await Promise.allSettled(
|
|
715
|
+
files.map(async (file) => {
|
|
716
|
+
const spec = await this.loadWorkflowSpec(file);
|
|
717
|
+
if (!spec) {
|
|
718
|
+
return null;
|
|
719
|
+
}
|
|
720
|
+
const relativePath = relative(this.config.workspaceRoot, file);
|
|
721
|
+
const id = this.generateId(relativePath);
|
|
722
|
+
return {
|
|
723
|
+
id,
|
|
724
|
+
source: "workspace",
|
|
725
|
+
filePath: file,
|
|
726
|
+
description: spec.description
|
|
727
|
+
// tags: spec.tags, // TODO: Add tags to WorkflowSpec if needed
|
|
728
|
+
};
|
|
729
|
+
})
|
|
730
|
+
);
|
|
731
|
+
for (const result of results) {
|
|
732
|
+
if (result.status === "fulfilled" && result.value !== null) {
|
|
733
|
+
workflows.push(result.value);
|
|
734
|
+
} else if (result.status === "rejected") {
|
|
735
|
+
console.warn(
|
|
736
|
+
"[WorkspaceWorkflowRegistry] Failed to load workflow:",
|
|
737
|
+
result.reason instanceof Error ? result.reason.message : String(result.reason)
|
|
738
|
+
);
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
this.cache = workflows;
|
|
742
|
+
return workflows;
|
|
743
|
+
}
|
|
744
|
+
async resolve(id) {
|
|
745
|
+
const cleanId = id.startsWith("workspace:") ? id.slice("workspace:".length) : id;
|
|
746
|
+
const all = await this.list();
|
|
747
|
+
return all.find((w) => w.id === id || w.id.endsWith(":" + cleanId)) ?? null;
|
|
748
|
+
}
|
|
749
|
+
async refresh() {
|
|
750
|
+
this.cache = null;
|
|
751
|
+
}
|
|
752
|
+
async dispose() {
|
|
753
|
+
}
|
|
754
|
+
async loadWorkflowSpec(filePath) {
|
|
755
|
+
try {
|
|
756
|
+
const raw = await readFile(filePath, "utf-8");
|
|
757
|
+
const parsed = filePath.endsWith(".json") ? JSON.parse(raw) : parse(raw);
|
|
758
|
+
const result = WorkflowSpecSchema.safeParse(parsed);
|
|
759
|
+
if (!result.success) {
|
|
760
|
+
return null;
|
|
761
|
+
}
|
|
762
|
+
return result.data;
|
|
763
|
+
} catch {
|
|
764
|
+
return null;
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
generateId(relativePath) {
|
|
768
|
+
const withoutExt = relativePath.replace(/\.(yml|yaml|json)$/, "");
|
|
769
|
+
const normalized = withoutExt.replace(/\\/g, "/");
|
|
770
|
+
return `workspace:${normalized}`;
|
|
771
|
+
}
|
|
772
|
+
};
|
|
773
|
+
|
|
774
|
+
// src/registry/composite-registry.ts
|
|
775
|
+
init_plugin_workflows();
|
|
776
|
+
|
|
777
|
+
// src/registry/errors.ts
|
|
778
|
+
var WorkflowRegistryError = class extends Error {
|
|
779
|
+
constructor(message, workflowId) {
|
|
780
|
+
super(message);
|
|
781
|
+
this.workflowId = workflowId;
|
|
782
|
+
this.name = "WorkflowRegistryError";
|
|
783
|
+
}
|
|
784
|
+
};
|
|
785
|
+
|
|
786
|
+
// src/registry/composite-registry.ts
|
|
787
|
+
var CompositeWorkflowRegistry = class {
|
|
788
|
+
constructor(workspace, cliApi, remote) {
|
|
789
|
+
this.workspace = workspace;
|
|
790
|
+
this.cliApi = cliApi;
|
|
791
|
+
this.remote = remote;
|
|
792
|
+
}
|
|
793
|
+
cache = null;
|
|
794
|
+
async list() {
|
|
795
|
+
if (this.cache) {
|
|
796
|
+
return this.cache;
|
|
797
|
+
}
|
|
798
|
+
const registries = [
|
|
799
|
+
this.workspace.list()
|
|
800
|
+
];
|
|
801
|
+
if (this.cliApi) {
|
|
802
|
+
const snapshot = this.cliApi.snapshot();
|
|
803
|
+
registries.push(extractWorkflows(snapshot));
|
|
804
|
+
}
|
|
805
|
+
if (this.remote) {
|
|
806
|
+
registries.push(this.remote.list());
|
|
807
|
+
}
|
|
808
|
+
const results = await Promise.all(registries);
|
|
809
|
+
const allWorkflows = results.flat();
|
|
810
|
+
const ids = /* @__PURE__ */ new Set();
|
|
811
|
+
const conflicts = [];
|
|
812
|
+
for (const wf of allWorkflows) {
|
|
813
|
+
if (ids.has(wf.id)) {
|
|
814
|
+
conflicts.push(wf.id);
|
|
815
|
+
}
|
|
816
|
+
ids.add(wf.id);
|
|
817
|
+
}
|
|
818
|
+
if (conflicts.length > 0) {
|
|
819
|
+
throw new WorkflowRegistryError(
|
|
820
|
+
`Workflow ID conflicts detected: ${conflicts.join(", ")}. Use explicit prefixes (workspace:, plugin:, or remote:) to disambiguate.`
|
|
821
|
+
);
|
|
822
|
+
}
|
|
823
|
+
this.cache = allWorkflows;
|
|
824
|
+
return this.cache;
|
|
825
|
+
}
|
|
826
|
+
async resolve(id) {
|
|
827
|
+
if (id.startsWith("workspace:")) {
|
|
828
|
+
return this.workspace.resolve(id);
|
|
829
|
+
}
|
|
830
|
+
if (id.startsWith("plugin:") && this.cliApi) {
|
|
831
|
+
const snapshot = this.cliApi.snapshot();
|
|
832
|
+
const { findWorkflow: findWorkflow2 } = await Promise.resolve().then(() => (init_plugin_workflows(), plugin_workflows_exports));
|
|
833
|
+
return findWorkflow2(snapshot, id);
|
|
834
|
+
}
|
|
835
|
+
if (id.startsWith("remote:") && this.remote) {
|
|
836
|
+
return this.remote.resolve(id);
|
|
837
|
+
}
|
|
838
|
+
const all = await this.list();
|
|
839
|
+
const matches = all.filter(
|
|
840
|
+
(w) => w.id === id || w.id.endsWith(":" + id)
|
|
841
|
+
);
|
|
842
|
+
if (matches.length > 1) {
|
|
843
|
+
throw new WorkflowRegistryError(
|
|
844
|
+
`Ambiguous workflow ID "${id}". Multiple matches: ${matches.map((m) => m.id).join(", ")}. Use explicit prefix (workspace:, plugin:, or remote:).`,
|
|
845
|
+
id
|
|
846
|
+
);
|
|
847
|
+
}
|
|
848
|
+
return matches[0] ?? null;
|
|
849
|
+
}
|
|
850
|
+
async refresh() {
|
|
851
|
+
this.cache = null;
|
|
852
|
+
const refreshTasks = [
|
|
853
|
+
this.workspace.refresh()
|
|
854
|
+
];
|
|
855
|
+
if (this.remote) {
|
|
856
|
+
refreshTasks.push(this.remote.refresh());
|
|
857
|
+
}
|
|
858
|
+
await Promise.all(refreshTasks);
|
|
859
|
+
}
|
|
860
|
+
async dispose() {
|
|
861
|
+
await Promise.all([
|
|
862
|
+
this.workspace.dispose?.() ?? Promise.resolve(),
|
|
863
|
+
this.remote?.dispose?.() ?? Promise.resolve()
|
|
864
|
+
]);
|
|
865
|
+
}
|
|
866
|
+
};
|
|
867
|
+
|
|
868
|
+
// src/registry/index.ts
|
|
869
|
+
init_plugin_workflows();
|
|
870
|
+
|
|
871
|
+
export { BudgetConfigSchema, CompositeWorkflowRegistry, LocalRunner, RemoteMarketplaceSourceSchema, SandboxRunner, WorkflowConfigSchema, WorkflowRegistryError, WorkspaceWorkflowRegistry, createStepContext, extractWorkflows, findWorkflow, loadWorkflowConfig, saveWorkflowConfig };
|
|
872
|
+
//# sourceMappingURL=index.js.map
|
|
873
|
+
//# sourceMappingURL=index.js.map
|