@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 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
@@ -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"]}
@@ -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
+ }