@kb-labs/workflow-builtins 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 +23 -0
- package/dist/index.d.ts +82 -0
- package/dist/index.js +153 -0
- package/dist/index.js.map +1 -0
- package/dist/shell.d.ts +55 -0
- package/dist/shell.js +153 -0
- package/dist/shell.js.map +1 -0
- package/package.json +48 -0
package/README.md
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# @kb-labs/workflow-builtins
|
|
2
|
+
|
|
3
|
+
Built-in workflow handlers (shell, etc.)
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pnpm add @kb-labs/workflow-builtins
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```typescript
|
|
14
|
+
import { ... } from '@kb-labs/workflow-builtins';
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## API
|
|
18
|
+
|
|
19
|
+
See TypeScript types for detailed API documentation.
|
|
20
|
+
|
|
21
|
+
## License
|
|
22
|
+
|
|
23
|
+
MIT
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
export { ShellInput, ShellOutput, default as shell } from './shell.js';
|
|
2
|
+
import '@kb-labs/plugin-contracts';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @module @kb-labs/workflow-builtins/approval
|
|
6
|
+
* Types for builtin:approval step
|
|
7
|
+
*
|
|
8
|
+
* Approval steps pause the pipeline and wait for human decision.
|
|
9
|
+
* The worker handles polling; resolveApproval() on the engine resumes execution.
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* Input for builtin:approval step (spec.with)
|
|
13
|
+
*/
|
|
14
|
+
interface ApprovalInput {
|
|
15
|
+
/** Display title for the approval request */
|
|
16
|
+
title: string;
|
|
17
|
+
/** Contextual data shown to the approver (already interpolated) */
|
|
18
|
+
context?: Record<string, unknown>;
|
|
19
|
+
/** Optional instructions for the approver */
|
|
20
|
+
instructions?: string;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Output produced by a resolved approval step
|
|
24
|
+
*/
|
|
25
|
+
interface ApprovalOutput {
|
|
26
|
+
/** Whether the approval was granted */
|
|
27
|
+
approved: boolean;
|
|
28
|
+
/** Action taken: "approve" or "reject" */
|
|
29
|
+
action: 'approve' | 'reject';
|
|
30
|
+
/** Optional comment from the approver */
|
|
31
|
+
comment?: string;
|
|
32
|
+
/** Additional data provided by the approver */
|
|
33
|
+
[key: string]: unknown;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* @module @kb-labs/workflow-builtins/gate
|
|
38
|
+
* Types for builtin:gate step
|
|
39
|
+
*
|
|
40
|
+
* Gate steps act as automatic routers — they read a decision value
|
|
41
|
+
* from previous step outputs and route the pipeline accordingly:
|
|
42
|
+
* - continue: proceed to next step
|
|
43
|
+
* - fail: fail the pipeline
|
|
44
|
+
* - restartFrom: reset steps back to target and re-schedule with context
|
|
45
|
+
*/
|
|
46
|
+
/**
|
|
47
|
+
* Route action for a gate decision
|
|
48
|
+
*/
|
|
49
|
+
type GateRouteAction = 'continue' | 'fail' | {
|
|
50
|
+
/** Step ID to restart from */
|
|
51
|
+
restartFrom: string;
|
|
52
|
+
/** Additional context to pass (merged into trigger.payload) */
|
|
53
|
+
context?: Record<string, unknown>;
|
|
54
|
+
};
|
|
55
|
+
/**
|
|
56
|
+
* Input for builtin:gate step (spec.with)
|
|
57
|
+
*/
|
|
58
|
+
interface GateInput {
|
|
59
|
+
/** Expression path to the decision value (e.g. "steps.review.outputs.passed") */
|
|
60
|
+
decision: string;
|
|
61
|
+
/** Route map: decision value → action */
|
|
62
|
+
routes: Record<string, GateRouteAction>;
|
|
63
|
+
/** Default action if decision value doesn't match any route */
|
|
64
|
+
default?: 'continue' | 'fail';
|
|
65
|
+
/** Maximum number of restart iterations before failing (default: 3) */
|
|
66
|
+
maxIterations?: number;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Output produced by a resolved gate step
|
|
70
|
+
*/
|
|
71
|
+
interface GateOutput {
|
|
72
|
+
/** The decision value that was evaluated */
|
|
73
|
+
decisionValue: unknown;
|
|
74
|
+
/** The action that was taken */
|
|
75
|
+
action: 'continue' | 'fail' | 'restart';
|
|
76
|
+
/** Step ID that was restarted from (if restart) */
|
|
77
|
+
restartFrom?: string;
|
|
78
|
+
/** Current iteration count */
|
|
79
|
+
iteration: number;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export type { ApprovalInput, ApprovalOutput, GateInput, GateOutput, GateRouteAction };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { execaCommand } from 'execa';
|
|
2
|
+
|
|
3
|
+
// src/shell.ts
|
|
4
|
+
var BLOCKED_COMMANDS = [
|
|
5
|
+
"rm -rf /",
|
|
6
|
+
"rm -rf /*",
|
|
7
|
+
"mkfs",
|
|
8
|
+
"dd if=",
|
|
9
|
+
":(){:|:&};:",
|
|
10
|
+
// Fork bomb
|
|
11
|
+
"chmod -R 777 /",
|
|
12
|
+
"chown -R",
|
|
13
|
+
"> /dev/sda",
|
|
14
|
+
"mv /* ",
|
|
15
|
+
"fdisk"
|
|
16
|
+
];
|
|
17
|
+
var OUTPUT_MARKER = "::kb-output::";
|
|
18
|
+
function mergeJsonOutputs(output) {
|
|
19
|
+
const base = { ...output };
|
|
20
|
+
const trimmed = output.stdout.trim();
|
|
21
|
+
if (!trimmed) {
|
|
22
|
+
return base;
|
|
23
|
+
}
|
|
24
|
+
const lines = output.stdout.split("\n");
|
|
25
|
+
let foundMarker = false;
|
|
26
|
+
for (const line of lines) {
|
|
27
|
+
const idx = line.indexOf(OUTPUT_MARKER);
|
|
28
|
+
if (idx !== -1) {
|
|
29
|
+
foundMarker = true;
|
|
30
|
+
try {
|
|
31
|
+
const parsed = JSON.parse(line.slice(idx + OUTPUT_MARKER.length));
|
|
32
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
33
|
+
Object.assign(base, parsed);
|
|
34
|
+
}
|
|
35
|
+
} catch {
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
if (foundMarker) {
|
|
40
|
+
return base;
|
|
41
|
+
}
|
|
42
|
+
try {
|
|
43
|
+
const parsed = JSON.parse(trimmed);
|
|
44
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
45
|
+
Object.assign(base, parsed);
|
|
46
|
+
}
|
|
47
|
+
} catch {
|
|
48
|
+
}
|
|
49
|
+
return base;
|
|
50
|
+
}
|
|
51
|
+
async function shellHandler(ctx, input) {
|
|
52
|
+
const { command, env = {}, timeout = 3e5, throwOnError = false } = input;
|
|
53
|
+
const normalizedCommand = command.toLowerCase().trim();
|
|
54
|
+
for (const blocked of BLOCKED_COMMANDS) {
|
|
55
|
+
if (normalizedCommand.includes(blocked.toLowerCase())) {
|
|
56
|
+
throw new Error(
|
|
57
|
+
`Dangerous command blocked: "${blocked}". Command attempted: ${command.slice(0, 100)}`
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
const cwd = ctx.cwd;
|
|
62
|
+
const mergedEnv = {
|
|
63
|
+
...process.env,
|
|
64
|
+
...env
|
|
65
|
+
};
|
|
66
|
+
ctx.platform.logger.info("Executing shell command", {
|
|
67
|
+
command: command.slice(0, 200),
|
|
68
|
+
cwd,
|
|
69
|
+
timeout
|
|
70
|
+
});
|
|
71
|
+
try {
|
|
72
|
+
const proc = execaCommand(command, {
|
|
73
|
+
cwd,
|
|
74
|
+
env: mergedEnv,
|
|
75
|
+
shell: true,
|
|
76
|
+
stdio: "pipe",
|
|
77
|
+
timeout,
|
|
78
|
+
reject: false
|
|
79
|
+
// We handle exit codes ourselves
|
|
80
|
+
});
|
|
81
|
+
let lineNo = 0;
|
|
82
|
+
let stdoutBuf = "";
|
|
83
|
+
let stderrBuf = "";
|
|
84
|
+
const emitLine = (stream, line) => {
|
|
85
|
+
lineNo++;
|
|
86
|
+
void ctx.api.events.emit("log.line", { stream, line, lineNo, level: stream === "stderr" ? "error" : "info" });
|
|
87
|
+
};
|
|
88
|
+
proc.stdout?.on("data", (chunk) => {
|
|
89
|
+
stdoutBuf += chunk.toString();
|
|
90
|
+
const lines = stdoutBuf.split("\n");
|
|
91
|
+
stdoutBuf = lines.pop() ?? "";
|
|
92
|
+
for (const line of lines) emitLine("stdout", line);
|
|
93
|
+
});
|
|
94
|
+
proc.stderr?.on("data", (chunk) => {
|
|
95
|
+
stderrBuf += chunk.toString();
|
|
96
|
+
const lines = stderrBuf.split("\n");
|
|
97
|
+
stderrBuf = lines.pop() ?? "";
|
|
98
|
+
for (const line of lines) emitLine("stderr", line);
|
|
99
|
+
});
|
|
100
|
+
const result = await proc;
|
|
101
|
+
if (stdoutBuf) emitLine("stdout", stdoutBuf);
|
|
102
|
+
if (stderrBuf) emitLine("stderr", stderrBuf);
|
|
103
|
+
const output = {
|
|
104
|
+
stdout: result.stdout,
|
|
105
|
+
stderr: result.stderr,
|
|
106
|
+
exitCode: result.exitCode ?? 0,
|
|
107
|
+
ok: (result.exitCode ?? 0) === 0
|
|
108
|
+
};
|
|
109
|
+
if (output.ok) {
|
|
110
|
+
ctx.platform.logger.info("Shell command completed successfully", {
|
|
111
|
+
exitCode: output.exitCode,
|
|
112
|
+
stdoutLines: output.stdout.split("\n").length
|
|
113
|
+
});
|
|
114
|
+
} else {
|
|
115
|
+
ctx.platform.logger.warn("Shell command failed", {
|
|
116
|
+
exitCode: output.exitCode,
|
|
117
|
+
stderrLines: output.stderr.split("\n").length
|
|
118
|
+
});
|
|
119
|
+
if (throwOnError) {
|
|
120
|
+
throw new Error(`Shell command failed with exit code ${output.exitCode}: ${output.stderr.slice(0, 500)}`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return mergeJsonOutputs(output);
|
|
124
|
+
} catch (error) {
|
|
125
|
+
if (error && typeof error === "object" && "timedOut" in error && error.timedOut) {
|
|
126
|
+
throw new Error(`Shell command timed out after ${timeout}ms`);
|
|
127
|
+
}
|
|
128
|
+
if (error && typeof error === "object" && "exitCode" in error) {
|
|
129
|
+
const execError = error;
|
|
130
|
+
const output = {
|
|
131
|
+
stdout: execError.stdout ?? "",
|
|
132
|
+
stderr: execError.stderr ?? "",
|
|
133
|
+
exitCode: execError.exitCode ?? 1,
|
|
134
|
+
ok: false
|
|
135
|
+
};
|
|
136
|
+
ctx.platform.logger.error("Shell command execution failed", void 0, {
|
|
137
|
+
exitCode: output.exitCode,
|
|
138
|
+
stderr: output.stderr.slice(0, 500)
|
|
139
|
+
});
|
|
140
|
+
if (!throwOnError) {
|
|
141
|
+
return { ...output };
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
throw error;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
var shell_default = {
|
|
148
|
+
execute: shellHandler
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
export { shell_default as shell };
|
|
152
|
+
//# sourceMappingURL=index.js.map
|
|
153
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/shell.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * @module @kb-labs/workflow-runtime/builtin-handlers/shell\n * Built-in shell execution handler for workflows\n *\n * Security features:\n * - Blocks dangerous commands (rm -rf /, fork bombs, etc.)\n * - Timeout enforcement (default 5 minutes)\n * - Environment variable isolation\n * - Working directory restrictions\n */\n\nimport { execaCommand } from 'execa';\nimport type { PluginContextV3 } from '@kb-labs/plugin-contracts';\n\n/**\n * Commands that are always blocked (dangerous)\n */\nconst BLOCKED_COMMANDS = [\n 'rm -rf /',\n 'rm -rf /*',\n 'mkfs',\n 'dd if=',\n ':(){:|:&};:', // Fork bomb\n 'chmod -R 777 /',\n 'chown -R',\n '> /dev/sda',\n 'mv /* ',\n 'fdisk',\n];\n\n/**\n * Split string into chunks of specified size\n */\nfunction chunkString(str: string, chunkSize: number): string[] {\n const chunks: string[] = [];\n for (let i = 0; i < str.length; i += chunkSize) {\n chunks.push(str.slice(i, i + chunkSize));\n }\n return chunks;\n}\n\n/**\n * Shell handler input\n */\nexport interface ShellInput {\n /** Command to execute */\n command: string;\n\n /** Additional environment variables */\n env?: Record<string, string>;\n\n /** Timeout in milliseconds (default: 300000 = 5 min) */\n timeout?: number;\n\n /** Throw on non-zero exit code (default: false) */\n throwOnError?: boolean;\n}\n\n/**\n * Shell handler output\n */\nexport interface ShellOutput {\n /** Standard output */\n stdout: string;\n\n /** Standard error */\n stderr: string;\n\n /** Exit code */\n exitCode: number;\n\n /** Whether command succeeded (exitCode === 0) */\n ok: boolean;\n}\n\n/**\n * Output marker prefix. Shell commands emit structured outputs via:\n * echo '::kb-output::{\"passed\":true}'\n *\n * This separates logs (plain stdout) from structured data (outputs).\n * Similar to GitHub Actions ::set-output:: pattern.\n */\nconst OUTPUT_MARKER = '::kb-output::';\n\n/**\n * Extract structured outputs from shell stdout.\n *\n * Priority:\n * 1. ::kb-output::{...} marker lines — explicit, recommended\n * 2. Entire stdout as JSON — fallback for backward compat (simple commands)\n *\n * Logs and other stdout content are ignored for output purposes.\n */\nfunction mergeJsonOutputs(output: ShellOutput): Record<string, unknown> {\n const base: Record<string, unknown> = { ...output };\n const trimmed = output.stdout.trim();\n if (!trimmed) {return base;}\n\n // Priority 1: Look for ::kb-output:: marker lines\n const lines = output.stdout.split('\\n');\n let foundMarker = false;\n for (const line of lines) {\n const idx = line.indexOf(OUTPUT_MARKER);\n if (idx !== -1) {\n foundMarker = true;\n try {\n const parsed = JSON.parse(line.slice(idx + OUTPUT_MARKER.length));\n if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {\n Object.assign(base, parsed);\n }\n } catch {\n // Malformed marker — skip\n }\n }\n }\n\n if (foundMarker) {return base;}\n\n // Priority 2: Fallback — entire stdout as JSON (backward compat)\n try {\n const parsed = JSON.parse(trimmed);\n if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {\n Object.assign(base, parsed);\n }\n } catch {\n // Not JSON — return as-is\n }\n\n return base;\n}\n\n/**\n * Built-in shell execution handler.\n *\n * Executes shell commands with safety checks and timeout enforcement.\n *\n * @param ctx - Handler execution context\n * @param input - Shell command input\n * @returns Shell execution result\n * @throws Error if dangerous command detected or timeout exceeded\n */\nasync function shellHandler(\n ctx: PluginContextV3,\n input: ShellInput,\n): Promise<Record<string, unknown>> {\n const { command, env = {}, timeout = 300000, throwOnError = false } = input;\n\n // Security: Check for dangerous commands\n const normalizedCommand = command.toLowerCase().trim();\n for (const blocked of BLOCKED_COMMANDS) {\n if (normalizedCommand.includes(blocked.toLowerCase())) {\n throw new Error(\n `Dangerous command blocked: \"${blocked}\". Command attempted: ${command.slice(0, 100)}`,\n );\n }\n }\n\n // Get working directory from context (workflow workspace)\n const cwd = ctx.cwd;\n\n // Merge environment variables\n const mergedEnv = {\n ...process.env,\n ...env,\n };\n\n ctx.platform.logger.info('Executing shell command', {\n command: command.slice(0, 200),\n cwd,\n timeout,\n });\n\n try {\n const proc = execaCommand(command, {\n cwd,\n env: mergedEnv,\n shell: true,\n stdio: 'pipe',\n timeout,\n reject: false, // We handle exit codes ourselves\n });\n\n // Stream stdout/stderr line-by-line in real-time\n let lineNo = 0;\n let stdoutBuf = '';\n let stderrBuf = '';\n\n const emitLine = (stream: 'stdout' | 'stderr', line: string) => {\n lineNo++;\n void ctx.api.events.emit('log.line', { stream, line, lineNo, level: stream === 'stderr' ? 'error' : 'info' });\n };\n\n proc.stdout?.on('data', (chunk: Buffer) => {\n stdoutBuf += chunk.toString();\n const lines = stdoutBuf.split('\\n');\n stdoutBuf = lines.pop() ?? '';\n for (const line of lines) emitLine('stdout', line);\n });\n\n proc.stderr?.on('data', (chunk: Buffer) => {\n stderrBuf += chunk.toString();\n const lines = stderrBuf.split('\\n');\n stderrBuf = lines.pop() ?? '';\n for (const line of lines) emitLine('stderr', line);\n });\n\n const result = await proc;\n\n // Flush remaining buffered content\n if (stdoutBuf) emitLine('stdout', stdoutBuf);\n if (stderrBuf) emitLine('stderr', stderrBuf);\n\n const output: ShellOutput = {\n stdout: result.stdout,\n stderr: result.stderr,\n exitCode: result.exitCode ?? 0,\n ok: (result.exitCode ?? 0) === 0,\n };\n\n if (output.ok) {\n ctx.platform.logger.info('Shell command completed successfully', {\n exitCode: output.exitCode,\n stdoutLines: output.stdout.split('\\n').length,\n });\n } else {\n ctx.platform.logger.warn('Shell command failed', {\n exitCode: output.exitCode,\n stderrLines: output.stderr.split('\\n').length,\n });\n\n if (throwOnError) {\n throw new Error(`Shell command failed with exit code ${output.exitCode}: ${output.stderr.slice(0, 500)}`);\n }\n }\n\n return mergeJsonOutputs(output);\n } catch (error) {\n // Handle timeout\n if (error && typeof error === 'object' && 'timedOut' in error && error.timedOut) {\n throw new Error(`Shell command timed out after ${timeout}ms`);\n }\n\n // Handle execution error\n if (error && typeof error === 'object' && 'exitCode' in error) {\n const execError = error as { exitCode?: number; stdout?: string; stderr?: string };\n const output: ShellOutput = {\n stdout: execError.stdout ?? '',\n stderr: execError.stderr ?? '',\n exitCode: execError.exitCode ?? 1,\n ok: false,\n };\n\n ctx.platform.logger.error('Shell command execution failed', undefined, {\n exitCode: output.exitCode,\n stderr: output.stderr.slice(0, 500),\n });\n\n if (!throwOnError) {\n return { ...output };\n }\n }\n\n throw error;\n }\n}\n\n// Export handler in format expected by ExecutionBackend\nexport default {\n execute: shellHandler,\n};\n"]}
|
package/dist/shell.d.ts
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { PluginContextV3 } from '@kb-labs/plugin-contracts';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @module @kb-labs/workflow-runtime/builtin-handlers/shell
|
|
5
|
+
* Built-in shell execution handler for workflows
|
|
6
|
+
*
|
|
7
|
+
* Security features:
|
|
8
|
+
* - Blocks dangerous commands (rm -rf /, fork bombs, etc.)
|
|
9
|
+
* - Timeout enforcement (default 5 minutes)
|
|
10
|
+
* - Environment variable isolation
|
|
11
|
+
* - Working directory restrictions
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Shell handler input
|
|
16
|
+
*/
|
|
17
|
+
interface ShellInput {
|
|
18
|
+
/** Command to execute */
|
|
19
|
+
command: string;
|
|
20
|
+
/** Additional environment variables */
|
|
21
|
+
env?: Record<string, string>;
|
|
22
|
+
/** Timeout in milliseconds (default: 300000 = 5 min) */
|
|
23
|
+
timeout?: number;
|
|
24
|
+
/** Throw on non-zero exit code (default: false) */
|
|
25
|
+
throwOnError?: boolean;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Shell handler output
|
|
29
|
+
*/
|
|
30
|
+
interface ShellOutput {
|
|
31
|
+
/** Standard output */
|
|
32
|
+
stdout: string;
|
|
33
|
+
/** Standard error */
|
|
34
|
+
stderr: string;
|
|
35
|
+
/** Exit code */
|
|
36
|
+
exitCode: number;
|
|
37
|
+
/** Whether command succeeded (exitCode === 0) */
|
|
38
|
+
ok: boolean;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Built-in shell execution handler.
|
|
42
|
+
*
|
|
43
|
+
* Executes shell commands with safety checks and timeout enforcement.
|
|
44
|
+
*
|
|
45
|
+
* @param ctx - Handler execution context
|
|
46
|
+
* @param input - Shell command input
|
|
47
|
+
* @returns Shell execution result
|
|
48
|
+
* @throws Error if dangerous command detected or timeout exceeded
|
|
49
|
+
*/
|
|
50
|
+
declare function shellHandler(ctx: PluginContextV3, input: ShellInput): Promise<Record<string, unknown>>;
|
|
51
|
+
declare const _default: {
|
|
52
|
+
execute: typeof shellHandler;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export { type ShellInput, type ShellOutput, _default as default };
|
package/dist/shell.js
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { execaCommand } from 'execa';
|
|
2
|
+
|
|
3
|
+
// src/shell.ts
|
|
4
|
+
var BLOCKED_COMMANDS = [
|
|
5
|
+
"rm -rf /",
|
|
6
|
+
"rm -rf /*",
|
|
7
|
+
"mkfs",
|
|
8
|
+
"dd if=",
|
|
9
|
+
":(){:|:&};:",
|
|
10
|
+
// Fork bomb
|
|
11
|
+
"chmod -R 777 /",
|
|
12
|
+
"chown -R",
|
|
13
|
+
"> /dev/sda",
|
|
14
|
+
"mv /* ",
|
|
15
|
+
"fdisk"
|
|
16
|
+
];
|
|
17
|
+
var OUTPUT_MARKER = "::kb-output::";
|
|
18
|
+
function mergeJsonOutputs(output) {
|
|
19
|
+
const base = { ...output };
|
|
20
|
+
const trimmed = output.stdout.trim();
|
|
21
|
+
if (!trimmed) {
|
|
22
|
+
return base;
|
|
23
|
+
}
|
|
24
|
+
const lines = output.stdout.split("\n");
|
|
25
|
+
let foundMarker = false;
|
|
26
|
+
for (const line of lines) {
|
|
27
|
+
const idx = line.indexOf(OUTPUT_MARKER);
|
|
28
|
+
if (idx !== -1) {
|
|
29
|
+
foundMarker = true;
|
|
30
|
+
try {
|
|
31
|
+
const parsed = JSON.parse(line.slice(idx + OUTPUT_MARKER.length));
|
|
32
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
33
|
+
Object.assign(base, parsed);
|
|
34
|
+
}
|
|
35
|
+
} catch {
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
if (foundMarker) {
|
|
40
|
+
return base;
|
|
41
|
+
}
|
|
42
|
+
try {
|
|
43
|
+
const parsed = JSON.parse(trimmed);
|
|
44
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
45
|
+
Object.assign(base, parsed);
|
|
46
|
+
}
|
|
47
|
+
} catch {
|
|
48
|
+
}
|
|
49
|
+
return base;
|
|
50
|
+
}
|
|
51
|
+
async function shellHandler(ctx, input) {
|
|
52
|
+
const { command, env = {}, timeout = 3e5, throwOnError = false } = input;
|
|
53
|
+
const normalizedCommand = command.toLowerCase().trim();
|
|
54
|
+
for (const blocked of BLOCKED_COMMANDS) {
|
|
55
|
+
if (normalizedCommand.includes(blocked.toLowerCase())) {
|
|
56
|
+
throw new Error(
|
|
57
|
+
`Dangerous command blocked: "${blocked}". Command attempted: ${command.slice(0, 100)}`
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
const cwd = ctx.cwd;
|
|
62
|
+
const mergedEnv = {
|
|
63
|
+
...process.env,
|
|
64
|
+
...env
|
|
65
|
+
};
|
|
66
|
+
ctx.platform.logger.info("Executing shell command", {
|
|
67
|
+
command: command.slice(0, 200),
|
|
68
|
+
cwd,
|
|
69
|
+
timeout
|
|
70
|
+
});
|
|
71
|
+
try {
|
|
72
|
+
const proc = execaCommand(command, {
|
|
73
|
+
cwd,
|
|
74
|
+
env: mergedEnv,
|
|
75
|
+
shell: true,
|
|
76
|
+
stdio: "pipe",
|
|
77
|
+
timeout,
|
|
78
|
+
reject: false
|
|
79
|
+
// We handle exit codes ourselves
|
|
80
|
+
});
|
|
81
|
+
let lineNo = 0;
|
|
82
|
+
let stdoutBuf = "";
|
|
83
|
+
let stderrBuf = "";
|
|
84
|
+
const emitLine = (stream, line) => {
|
|
85
|
+
lineNo++;
|
|
86
|
+
void ctx.api.events.emit("log.line", { stream, line, lineNo, level: stream === "stderr" ? "error" : "info" });
|
|
87
|
+
};
|
|
88
|
+
proc.stdout?.on("data", (chunk) => {
|
|
89
|
+
stdoutBuf += chunk.toString();
|
|
90
|
+
const lines = stdoutBuf.split("\n");
|
|
91
|
+
stdoutBuf = lines.pop() ?? "";
|
|
92
|
+
for (const line of lines) emitLine("stdout", line);
|
|
93
|
+
});
|
|
94
|
+
proc.stderr?.on("data", (chunk) => {
|
|
95
|
+
stderrBuf += chunk.toString();
|
|
96
|
+
const lines = stderrBuf.split("\n");
|
|
97
|
+
stderrBuf = lines.pop() ?? "";
|
|
98
|
+
for (const line of lines) emitLine("stderr", line);
|
|
99
|
+
});
|
|
100
|
+
const result = await proc;
|
|
101
|
+
if (stdoutBuf) emitLine("stdout", stdoutBuf);
|
|
102
|
+
if (stderrBuf) emitLine("stderr", stderrBuf);
|
|
103
|
+
const output = {
|
|
104
|
+
stdout: result.stdout,
|
|
105
|
+
stderr: result.stderr,
|
|
106
|
+
exitCode: result.exitCode ?? 0,
|
|
107
|
+
ok: (result.exitCode ?? 0) === 0
|
|
108
|
+
};
|
|
109
|
+
if (output.ok) {
|
|
110
|
+
ctx.platform.logger.info("Shell command completed successfully", {
|
|
111
|
+
exitCode: output.exitCode,
|
|
112
|
+
stdoutLines: output.stdout.split("\n").length
|
|
113
|
+
});
|
|
114
|
+
} else {
|
|
115
|
+
ctx.platform.logger.warn("Shell command failed", {
|
|
116
|
+
exitCode: output.exitCode,
|
|
117
|
+
stderrLines: output.stderr.split("\n").length
|
|
118
|
+
});
|
|
119
|
+
if (throwOnError) {
|
|
120
|
+
throw new Error(`Shell command failed with exit code ${output.exitCode}: ${output.stderr.slice(0, 500)}`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return mergeJsonOutputs(output);
|
|
124
|
+
} catch (error) {
|
|
125
|
+
if (error && typeof error === "object" && "timedOut" in error && error.timedOut) {
|
|
126
|
+
throw new Error(`Shell command timed out after ${timeout}ms`);
|
|
127
|
+
}
|
|
128
|
+
if (error && typeof error === "object" && "exitCode" in error) {
|
|
129
|
+
const execError = error;
|
|
130
|
+
const output = {
|
|
131
|
+
stdout: execError.stdout ?? "",
|
|
132
|
+
stderr: execError.stderr ?? "",
|
|
133
|
+
exitCode: execError.exitCode ?? 1,
|
|
134
|
+
ok: false
|
|
135
|
+
};
|
|
136
|
+
ctx.platform.logger.error("Shell command execution failed", void 0, {
|
|
137
|
+
exitCode: output.exitCode,
|
|
138
|
+
stderr: output.stderr.slice(0, 500)
|
|
139
|
+
});
|
|
140
|
+
if (!throwOnError) {
|
|
141
|
+
return { ...output };
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
throw error;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
var shell_default = {
|
|
148
|
+
execute: shellHandler
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
export { shell_default as default };
|
|
152
|
+
//# sourceMappingURL=shell.js.map
|
|
153
|
+
//# sourceMappingURL=shell.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/shell.ts"],"names":[],"mappings":"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","file":"shell.js","sourcesContent":["/**\n * @module @kb-labs/workflow-runtime/builtin-handlers/shell\n * Built-in shell execution handler for workflows\n *\n * Security features:\n * - Blocks dangerous commands (rm -rf /, fork bombs, etc.)\n * - Timeout enforcement (default 5 minutes)\n * - Environment variable isolation\n * - Working directory restrictions\n */\n\nimport { execaCommand } from 'execa';\nimport type { PluginContextV3 } from '@kb-labs/plugin-contracts';\n\n/**\n * Commands that are always blocked (dangerous)\n */\nconst BLOCKED_COMMANDS = [\n 'rm -rf /',\n 'rm -rf /*',\n 'mkfs',\n 'dd if=',\n ':(){:|:&};:', // Fork bomb\n 'chmod -R 777 /',\n 'chown -R',\n '> /dev/sda',\n 'mv /* ',\n 'fdisk',\n];\n\n/**\n * Split string into chunks of specified size\n */\nfunction chunkString(str: string, chunkSize: number): string[] {\n const chunks: string[] = [];\n for (let i = 0; i < str.length; i += chunkSize) {\n chunks.push(str.slice(i, i + chunkSize));\n }\n return chunks;\n}\n\n/**\n * Shell handler input\n */\nexport interface ShellInput {\n /** Command to execute */\n command: string;\n\n /** Additional environment variables */\n env?: Record<string, string>;\n\n /** Timeout in milliseconds (default: 300000 = 5 min) */\n timeout?: number;\n\n /** Throw on non-zero exit code (default: false) */\n throwOnError?: boolean;\n}\n\n/**\n * Shell handler output\n */\nexport interface ShellOutput {\n /** Standard output */\n stdout: string;\n\n /** Standard error */\n stderr: string;\n\n /** Exit code */\n exitCode: number;\n\n /** Whether command succeeded (exitCode === 0) */\n ok: boolean;\n}\n\n/**\n * Output marker prefix. Shell commands emit structured outputs via:\n * echo '::kb-output::{\"passed\":true}'\n *\n * This separates logs (plain stdout) from structured data (outputs).\n * Similar to GitHub Actions ::set-output:: pattern.\n */\nconst OUTPUT_MARKER = '::kb-output::';\n\n/**\n * Extract structured outputs from shell stdout.\n *\n * Priority:\n * 1. ::kb-output::{...} marker lines — explicit, recommended\n * 2. Entire stdout as JSON — fallback for backward compat (simple commands)\n *\n * Logs and other stdout content are ignored for output purposes.\n */\nfunction mergeJsonOutputs(output: ShellOutput): Record<string, unknown> {\n const base: Record<string, unknown> = { ...output };\n const trimmed = output.stdout.trim();\n if (!trimmed) {return base;}\n\n // Priority 1: Look for ::kb-output:: marker lines\n const lines = output.stdout.split('\\n');\n let foundMarker = false;\n for (const line of lines) {\n const idx = line.indexOf(OUTPUT_MARKER);\n if (idx !== -1) {\n foundMarker = true;\n try {\n const parsed = JSON.parse(line.slice(idx + OUTPUT_MARKER.length));\n if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {\n Object.assign(base, parsed);\n }\n } catch {\n // Malformed marker — skip\n }\n }\n }\n\n if (foundMarker) {return base;}\n\n // Priority 2: Fallback — entire stdout as JSON (backward compat)\n try {\n const parsed = JSON.parse(trimmed);\n if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {\n Object.assign(base, parsed);\n }\n } catch {\n // Not JSON — return as-is\n }\n\n return base;\n}\n\n/**\n * Built-in shell execution handler.\n *\n * Executes shell commands with safety checks and timeout enforcement.\n *\n * @param ctx - Handler execution context\n * @param input - Shell command input\n * @returns Shell execution result\n * @throws Error if dangerous command detected or timeout exceeded\n */\nasync function shellHandler(\n ctx: PluginContextV3,\n input: ShellInput,\n): Promise<Record<string, unknown>> {\n const { command, env = {}, timeout = 300000, throwOnError = false } = input;\n\n // Security: Check for dangerous commands\n const normalizedCommand = command.toLowerCase().trim();\n for (const blocked of BLOCKED_COMMANDS) {\n if (normalizedCommand.includes(blocked.toLowerCase())) {\n throw new Error(\n `Dangerous command blocked: \"${blocked}\". Command attempted: ${command.slice(0, 100)}`,\n );\n }\n }\n\n // Get working directory from context (workflow workspace)\n const cwd = ctx.cwd;\n\n // Merge environment variables\n const mergedEnv = {\n ...process.env,\n ...env,\n };\n\n ctx.platform.logger.info('Executing shell command', {\n command: command.slice(0, 200),\n cwd,\n timeout,\n });\n\n try {\n const proc = execaCommand(command, {\n cwd,\n env: mergedEnv,\n shell: true,\n stdio: 'pipe',\n timeout,\n reject: false, // We handle exit codes ourselves\n });\n\n // Stream stdout/stderr line-by-line in real-time\n let lineNo = 0;\n let stdoutBuf = '';\n let stderrBuf = '';\n\n const emitLine = (stream: 'stdout' | 'stderr', line: string) => {\n lineNo++;\n void ctx.api.events.emit('log.line', { stream, line, lineNo, level: stream === 'stderr' ? 'error' : 'info' });\n };\n\n proc.stdout?.on('data', (chunk: Buffer) => {\n stdoutBuf += chunk.toString();\n const lines = stdoutBuf.split('\\n');\n stdoutBuf = lines.pop() ?? '';\n for (const line of lines) emitLine('stdout', line);\n });\n\n proc.stderr?.on('data', (chunk: Buffer) => {\n stderrBuf += chunk.toString();\n const lines = stderrBuf.split('\\n');\n stderrBuf = lines.pop() ?? '';\n for (const line of lines) emitLine('stderr', line);\n });\n\n const result = await proc;\n\n // Flush remaining buffered content\n if (stdoutBuf) emitLine('stdout', stdoutBuf);\n if (stderrBuf) emitLine('stderr', stderrBuf);\n\n const output: ShellOutput = {\n stdout: result.stdout,\n stderr: result.stderr,\n exitCode: result.exitCode ?? 0,\n ok: (result.exitCode ?? 0) === 0,\n };\n\n if (output.ok) {\n ctx.platform.logger.info('Shell command completed successfully', {\n exitCode: output.exitCode,\n stdoutLines: output.stdout.split('\\n').length,\n });\n } else {\n ctx.platform.logger.warn('Shell command failed', {\n exitCode: output.exitCode,\n stderrLines: output.stderr.split('\\n').length,\n });\n\n if (throwOnError) {\n throw new Error(`Shell command failed with exit code ${output.exitCode}: ${output.stderr.slice(0, 500)}`);\n }\n }\n\n return mergeJsonOutputs(output);\n } catch (error) {\n // Handle timeout\n if (error && typeof error === 'object' && 'timedOut' in error && error.timedOut) {\n throw new Error(`Shell command timed out after ${timeout}ms`);\n }\n\n // Handle execution error\n if (error && typeof error === 'object' && 'exitCode' in error) {\n const execError = error as { exitCode?: number; stdout?: string; stderr?: string };\n const output: ShellOutput = {\n stdout: execError.stdout ?? '',\n stderr: execError.stderr ?? '',\n exitCode: execError.exitCode ?? 1,\n ok: false,\n };\n\n ctx.platform.logger.error('Shell command execution failed', undefined, {\n exitCode: output.exitCode,\n stderr: output.stderr.slice(0, 500),\n });\n\n if (!throwOnError) {\n return { ...output };\n }\n }\n\n throw error;\n }\n}\n\n// Export handler in format expected by ExecutionBackend\nexport default {\n execute: shellHandler,\n};\n"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@kb-labs/workflow-builtins",
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "Built-in workflow handlers (shell, etc.)",
|
|
5
|
+
"files": [
|
|
6
|
+
"dist"
|
|
7
|
+
],
|
|
8
|
+
"type": "module",
|
|
9
|
+
"main": "./dist/index.js",
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"types": "./dist/index.d.ts",
|
|
14
|
+
"import": "./dist/index.js"
|
|
15
|
+
},
|
|
16
|
+
"./shell": {
|
|
17
|
+
"types": "./dist/shell.d.ts",
|
|
18
|
+
"import": "./dist/shell.js"
|
|
19
|
+
},
|
|
20
|
+
"./package.json": "./package.json"
|
|
21
|
+
},
|
|
22
|
+
"scripts": {
|
|
23
|
+
"build": "pnpm clean && tsup --config tsup.config.ts",
|
|
24
|
+
"clean": "rimraf dist",
|
|
25
|
+
"dev": "tsup --config tsup.config.ts --watch",
|
|
26
|
+
"lint": "eslint src --ext .ts",
|
|
27
|
+
"lint:fix": "eslint . --fix",
|
|
28
|
+
"type-check": "tsc --noEmit",
|
|
29
|
+
"test": "vitest run --passWithNoTests",
|
|
30
|
+
"test:watch": "vitest"
|
|
31
|
+
},
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"@kb-labs/plugin-contracts": "^1.1.0",
|
|
34
|
+
"execa": "^8.0.0"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"@types/node": "^24.3.3",
|
|
38
|
+
"rimraf": "^6.0.1",
|
|
39
|
+
"tsup": "^8.5.0",
|
|
40
|
+
"typescript": "^5.6.3",
|
|
41
|
+
"@kb-labs/devkit": "link:../../../../infra/kb-labs-devkit",
|
|
42
|
+
"vitest": "^3.2.4"
|
|
43
|
+
},
|
|
44
|
+
"engines": {
|
|
45
|
+
"node": ">=20.0.0",
|
|
46
|
+
"pnpm": ">=9.0.0"
|
|
47
|
+
}
|
|
48
|
+
}
|