@kb-labs/workflow-steps 2.116.14 → 2.118.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/dist/index.js +16 -1
- package/dist/index.js.map +1 -1
- package/dist/shell.d.ts +7 -1
- package/dist/shell.js +17 -2
- package/dist/shell.js.map +1 -1
- package/package.json +4 -4
package/dist/index.js
CHANGED
|
@@ -15,6 +15,14 @@ var BLOCKED_COMMANDS = [
|
|
|
15
15
|
"mv /* ",
|
|
16
16
|
"fdisk"
|
|
17
17
|
];
|
|
18
|
+
var FAILURE_OUTPUT_TAIL_CHARS = 4e3;
|
|
19
|
+
function tail(text, maxChars) {
|
|
20
|
+
if (text.length <= maxChars) {
|
|
21
|
+
return text;
|
|
22
|
+
}
|
|
23
|
+
return `\u2026(truncated, showing last ${maxChars} of ${text.length} chars)
|
|
24
|
+
${text.slice(-maxChars)}`;
|
|
25
|
+
}
|
|
18
26
|
var OUTPUT_MARKER = "::kb-output::";
|
|
19
27
|
var OUTPUT_MARKER_B64 = "::kb-output:base64::";
|
|
20
28
|
function parseOutputMarkerLine(line) {
|
|
@@ -170,6 +178,11 @@ async function shellHandler(ctx, input) {
|
|
|
170
178
|
emitLine("stderr", stderrBuf);
|
|
171
179
|
}
|
|
172
180
|
if (timedOut) {
|
|
181
|
+
ctx.platform.logger.warn("Shell command timed out", {
|
|
182
|
+
timeoutMs: timeout,
|
|
183
|
+
stderrTail: tail(stderrFull, FAILURE_OUTPUT_TAIL_CHARS),
|
|
184
|
+
stdoutTail: tail(stdoutFull, FAILURE_OUTPUT_TAIL_CHARS)
|
|
185
|
+
});
|
|
173
186
|
throw new Error(`Shell command timed out after ${timeout}ms`);
|
|
174
187
|
}
|
|
175
188
|
const output = {
|
|
@@ -188,7 +201,9 @@ async function shellHandler(ctx, input) {
|
|
|
188
201
|
} else {
|
|
189
202
|
ctx.platform.logger.warn("Shell command failed", {
|
|
190
203
|
exitCode: output.exitCode,
|
|
191
|
-
stderrLines: output.stderr.split("\n").length
|
|
204
|
+
stderrLines: output.stderr.split("\n").length,
|
|
205
|
+
stderrTail: tail(output.stderr, FAILURE_OUTPUT_TAIL_CHARS),
|
|
206
|
+
stdoutTail: tail(output.stdout, FAILURE_OUTPUT_TAIL_CHARS)
|
|
192
207
|
});
|
|
193
208
|
if (throwOnError) {
|
|
194
209
|
throw new Error(`Shell command failed with exit code ${output.exitCode}: ${output.stderr.slice(0, 500)}`);
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/shell.ts","../src/approval.ts","../src/gate.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 * Plain-JSON output marker.\n * echo '::kb-output::{\"passed\":true}'\n *\n * Only safe for values without embedded newlines, quotes, or control chars.\n * For multi-line text, binary data, or any value produced by a sub-command,\n * prefer the base64 variant below.\n */\nconst OUTPUT_MARKER = '::kb-output::';\n\n/**\n * Base64-encoded JSON output marker — JSON-safe for any payload.\n *\n * Usage (shell):\n * PAYLOAD=$(jq -cn --arg plan \"$(cat /tmp/plan.md)\" '{plan: $plan}' | base64 -w0)\n * echo \"::kb-output:base64::${PAYLOAD}\"\n *\n * This handles multi-line text, special chars, and any binary-safe content.\n * The base64 string is decoded and JSON-parsed by the runtime.\n */\nconst OUTPUT_MARKER_B64 = '::kb-output:base64::';\n\n/**\n * Parse a single ::kb-output:: or ::kb-output:base64:: marker line into key-value pairs.\n *\n * Returns the parsed object, or null if the line is not a recognized marker.\n * Throws a descriptive error if the marker payload is malformed so callers can warn.\n */\nexport function parseOutputMarkerLine(line: string): Record<string, unknown> | null {\n // Base64-encoded variant — JSON-safe for any payload\n const b64Idx = line.indexOf(OUTPUT_MARKER_B64);\n if (b64Idx !== -1) {\n const raw = line.slice(b64Idx + OUTPUT_MARKER_B64.length).trim();\n let decoded: string;\n try {\n decoded = Buffer.from(raw, 'base64').toString('utf8');\n } catch {\n throw new Error(`::kb-output:base64:: payload is not valid base64`);\n }\n let parsed: unknown;\n try {\n parsed = JSON.parse(decoded);\n } catch (err) {\n const detail = err instanceof Error ? err.message : String(err);\n throw new Error(`::kb-output:base64:: malformed JSON payload after decode: ${detail}`);\n }\n if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {\n return parsed as Record<string, unknown>;\n }\n throw new Error(`::kb-output:base64:: JSON must be an object, got ${Array.isArray(parsed) ? 'array' : typeof parsed}`);\n }\n\n // Plain-JSON variant — only safe for simple scalar values\n const idx = line.indexOf(OUTPUT_MARKER);\n if (idx !== -1) {\n const raw = line.slice(idx + OUTPUT_MARKER.length);\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch (err) {\n const detail = err instanceof Error ? err.message : String(err);\n throw new Error(`::kb-output:: malformed JSON payload: ${detail}`);\n }\n if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {\n return parsed as Record<string, unknown>;\n }\n throw new Error(`::kb-output:: JSON must be an object, got ${Array.isArray(parsed) ? 'array' : typeof parsed}`);\n }\n\n return null;\n}\n\n/**\n * Extract structured outputs from shell stdout.\n *\n * Priority:\n * 1. ::kb-output:base64:: marker lines — JSON-safe, recommended for complex values\n * 2. ::kb-output:: marker lines — plain JSON, ok for simple scalars\n * 3. Entire stdout as JSON — fallback for backward compat (simple commands)\n *\n * Malformed markers emit a warning instead of silently dropping the output.\n * Logs and other stdout content are ignored for output purposes.\n */\nexport function mergeJsonOutputs(output: ShellOutput, warn?: (msg: string) => void): 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 + 2: Look for ::kb-output:: or ::kb-output:base64:: marker lines\n const lines = output.stdout.split('\\n');\n let foundMarker = false;\n for (const line of lines) {\n if (!line.includes('::kb-output')) {continue;}\n foundMarker = true;\n try {\n const parsed = parseOutputMarkerLine(line);\n if (parsed) {\n Object.assign(base, parsed);\n }\n } catch (err) {\n // Malformed marker — warn instead of silently dropping\n const msg = err instanceof Error ? err.message : String(err);\n warn?.(`Malformed ::kb-output:: marker (outputs not populated): ${msg}`);\n }\n }\n\n if (foundMarker) {return base;}\n\n // Priority 3: Fallback — entire stdout as JSON (backward compat)\n try {\n const parsed: unknown = JSON.parse(trimmed);\n if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {\n Object.assign(base, parsed as Record<string, unknown>);\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 // detached: true puts the subprocess in its own process group. On timeout we kill\n // the entire group (process.kill(-pid, 'SIGKILL')), which takes down both the shell\n // and any children it spawned. Without this, SIGTERM reaches only the shell; child\n // processes (e.g. `sleep N`) inherit the pipe write-end and block await indefinitely.\n const proc = execaCommand(command, {\n cwd,\n env: mergedEnv,\n shell: true,\n stdio: 'pipe',\n reject: false, // We handle exit codes ourselves\n detached: true,\n });\n\n // Stream stdout/stderr line-by-line in real-time.\n // We consume the streams ourselves via 'data' events for live log streaming.\n // Because attaching a 'data' listener drains the stream, execa's result.stdout\n // will be empty — we must reconstruct the full output from collected chunks.\n let lineNo = 0;\n let stdoutBuf = '';\n let stderrBuf = '';\n let stdoutFull = '';\n let stderrFull = '';\n\n const emitLine = (stream: 'stdout' | 'stderr', line: string) => {\n lineNo++;\n // stderr is an output stream, not an exit-status signal. Several\n // perfectly successful tools (notably command discovery) deliberately\n // write diagnostics there. The step result is evaluated from exitCode;\n // keep stderr visible without turning every diagnostic line into an\n // error event.\n void ctx.api.events.emit('log.line', { stream, line, lineNo, level: stream === 'stderr' ? 'warn' : 'info' });\n };\n\n proc.stdout?.on('data', (chunk: Buffer) => {\n const text = chunk.toString();\n stdoutFull += text;\n stdoutBuf += text;\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 const text = chunk.toString();\n stderrFull += text;\n stderrBuf += text;\n const lines = stderrBuf.split('\\n');\n stderrBuf = lines.pop() ?? '';\n for (const line of lines) {emitLine('stderr', line);}\n });\n\n // Self-managed timeout: kill the entire process group so orphaned children\n // release the pipe write-end and await proc resolves immediately.\n let timedOut = false;\n const killTimer = setTimeout(() => {\n timedOut = true;\n const pid = proc.pid;\n if (pid !== undefined) {\n try {\n process.kill(-pid, 'SIGKILL');\n } catch {\n // process already exited\n }\n }\n }, timeout);\n\n const result = await proc;\n clearTimeout(killTimer);\n\n // Flush remaining buffered content\n if (stdoutBuf) {emitLine('stdout', stdoutBuf);}\n if (stderrBuf) {emitLine('stderr', stderrBuf);}\n\n if (timedOut) {\n throw new Error(`Shell command timed out after ${timeout}ms`);\n }\n\n const output: ShellOutput = {\n // Use our accumulated buffers — result.stdout/stderr are empty because\n // we consumed the streams via 'data' listeners above.\n stdout: stdoutFull || result.stdout,\n stderr: stderrFull || 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, (msg) => ctx.platform.logger.warn(`[shell] ${msg}`));\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","/**\n * @module @kb-labs/workflow-steps/approval\n * Types and handler for builtin:approval step\n *\n * Approval steps pause the pipeline and wait for human decision.\n * The worker handles polling; resolveApproval() on the engine resumes execution.\n */\n\n/**\n * Input for builtin:approval step (spec.with)\n */\nexport interface ApprovalInput {\n /** Display title for the approval request */\n title: string;\n\n /** Contextual data shown to the approver (already interpolated) */\n context?: Record<string, unknown>;\n\n /** Optional instructions for the approver */\n instructions?: string;\n}\n\n/**\n * Output produced by a resolved approval step\n */\nexport interface ApprovalOutput {\n /** Whether the approval was granted */\n approved: boolean;\n\n /** Action taken: \"approve\" or \"reject\" */\n action: 'approve' | 'reject';\n\n /** Optional comment from the approver */\n comment?: string;\n\n /** Additional data provided by the approver */\n [key: string]: unknown;\n}\n\n/**\n * ApprovalHandler — signals the worker to pause and wait for human approval.\n * The worker owns the polling loop; this handler only marks the intent.\n */\nexport class ApprovalHandler {\n static readonly uses = 'builtin:approval';\n\n handle(_input: ApprovalInput): { status: 'waiting'; reason: 'approval' } {\n return { status: 'waiting', reason: 'approval' };\n }\n}\n","/**\n * @module @kb-labs/workflow-steps/gate\n * Types and handler for builtin:gate step\n *\n * Gate steps act as automatic routers — they read a decision value\n * from previous step outputs and route the pipeline accordingly:\n * - continue: proceed to next step\n * - fail: fail the pipeline\n * - restart: reset steps back to target and re-schedule with context\n * - skip: mark intermediate steps as skipped and jump forward to a target step\n */\n\nimport { resolveValue, type ExpressionContext } from '@kb-labs/workflow-contracts';\n\n/**\n * Route action for a gate decision\n */\nexport type GateRouteAction =\n | 'continue'\n | 'fail'\n | {\n /** Step ID to restart from (go backward) */\n restartFrom: string;\n /** Additional context to pass (merged into trigger.payload) */\n context?: Record<string, unknown>;\n }\n | {\n /** Step ID to skip forward to — all steps between gate and target are marked skipped */\n skipTo: string;\n };\n\n/**\n * Input for builtin:gate step (spec.with)\n */\nexport interface GateInput {\n /** Expression path to the decision value (e.g. \"steps.review.outputs.passed\") */\n decision: string;\n\n /** Route map: decision value → action */\n routes: Record<string, GateRouteAction>;\n\n /** Default action if decision value doesn't match any route */\n default?: 'continue' | 'fail';\n\n /** Maximum number of restart iterations before failing (default: 3) */\n maxIterations?: number;\n}\n\n/**\n * Output produced by a resolved gate step\n */\nexport interface GateOutput {\n /** The decision value that was evaluated */\n decisionValue: unknown;\n\n /** The action that was taken */\n action: 'continue' | 'fail' | 'restart' | 'skip';\n\n /** Step ID that was restarted from (if restart) */\n restartFrom?: string;\n\n /** Step ID that was skipped to (if skip) */\n skipTo?: string;\n\n /** Current iteration count */\n iteration: number;\n\n /** Set to true when the gate exhausted its maxIterations budget */\n maxIterationsReached?: boolean;\n\n [key: string]: unknown;\n}\n\n/**\n * Pure decision result from GateHandler.\n * Worker applies the state mutations based on the action.\n */\nexport type GateDecision =\n | { action: 'continue'; outputs: GateOutput }\n | { action: 'fail'; error: Error; outputs: GateOutput }\n | {\n action: 'restart';\n restartFrom: string;\n context?: Record<string, unknown>;\n outputs: GateOutput;\n nextIteration: number;\n }\n | {\n action: 'skip';\n skipTo: string;\n outputs: GateOutput;\n };\n\n/**\n * GateHandler — pure decision function.\n * No I/O, no state mutations. Worker owns the side effects.\n */\nexport class GateHandler {\n static readonly uses = 'builtin:gate';\n\n /**\n * @param validRestartTargets Identifiers the worker can reset on restart —\n * step ids/spec.ids within the gate's own job AND other job names (for\n * cross-job restart). When provided and `restartFrom` matches none of them,\n * the gate FAILS instead of attempting a restart the worker cannot apply —\n * which would otherwise silently degrade into \"complete the gate and\n * proceed\" (a false green).\n */\n handle(\n input: GateInput,\n exprCtx: ExpressionContext,\n currentIteration: number,\n validRestartTargets?: string[],\n ): GateDecision {\n const maxIterations = input.maxIterations ?? 3;\n const decisionValue = resolveValue(input.decision, exprCtx);\n const decisionKey = String(decisionValue);\n\n const route: GateRouteAction | undefined =\n input.routes[decisionKey] ?? input.routes[decisionValue as string];\n const action = route ?? input.default ?? 'fail';\n\n if (action === 'continue') {\n return {\n action: 'continue',\n outputs: { decisionValue, action: 'continue', iteration: currentIteration },\n };\n }\n\n if (action === 'fail') {\n const failReason =\n (route as { message?: string } | undefined)?.message ??\n `No matching route for value \"${decisionKey}\"`;\n return {\n action: 'fail',\n error: new Error(`Gate failed: ${failReason}`),\n outputs: { decisionValue, action: 'fail', iteration: currentIteration },\n };\n }\n\n // skipTo action — jump forward, marking intermediate steps as skipped\n if (typeof action === 'object' && 'skipTo' in action) {\n if (validRestartTargets && !validRestartTargets.includes(action.skipTo)) {\n return {\n action: 'fail',\n error: new Error(\n `Gate skipTo \"${action.skipTo}\" matches no step in this job — cannot skip.`,\n ),\n outputs: { decisionValue, action: 'fail', iteration: currentIteration },\n };\n }\n return {\n action: 'skip',\n skipTo: action.skipTo,\n outputs: {\n decisionValue,\n action: 'skip',\n skipTo: action.skipTo,\n iteration: currentIteration,\n },\n };\n }\n\n // restart action\n const restartAction = action as { restartFrom: string; context?: Record<string, unknown> };\n\n // Guard: restartFrom must name something the worker can reset — a step in\n // this job or another job by name. If it matches neither, the gate cannot\n // recover; fail honestly rather than let the worker silently complete the\n // gate and proceed (which would ship unverified work as a false green).\n if (validRestartTargets && !validRestartTargets.includes(restartAction.restartFrom)) {\n return {\n action: 'fail',\n error: new Error(\n `Gate restartFrom \"${restartAction.restartFrom}\" matches no step in this job ` +\n `or any job in the run — cannot restart. Failing instead of silently passing.`,\n ),\n outputs: { decisionValue, action: 'fail', iteration: currentIteration },\n };\n }\n\n const nextIteration = currentIteration + 1;\n\n if (nextIteration >= maxIterations) {\n return {\n action: 'fail',\n error: new Error(`Gate max iterations reached (${maxIterations}) for step`),\n outputs: {\n decisionValue,\n action: 'fail',\n iteration: currentIteration,\n maxIterationsReached: true,\n },\n };\n }\n\n return {\n action: 'restart',\n restartFrom: restartAction.restartFrom,\n context: restartAction.context,\n outputs: {\n decisionValue,\n action: 'restart',\n restartFrom: restartAction.restartFrom,\n iteration: nextIteration,\n },\n nextIteration,\n };\n }\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/shell.ts","../src/approval.ts","../src/gate.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/** Cap on how much of a failed command's output gets attached to its log entry. */\nconst FAILURE_OUTPUT_TAIL_CHARS = 4000;\n\n/**\n * Last N characters of `text`, prefixed with a marker when it was truncated.\n * Tail (not head) because the actionable error is almost always at the end —\n * a stack trace, an assertion failure, the final \"Error:\" line.\n */\nexport function tail(text: string, maxChars: number): string {\n if (text.length <= maxChars) {return text;}\n return `…(truncated, showing last ${maxChars} of ${text.length} chars)\\n${text.slice(-maxChars)}`;\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 * Plain-JSON output marker.\n * echo '::kb-output::{\"passed\":true}'\n *\n * Only safe for values without embedded newlines, quotes, or control chars.\n * For multi-line text, binary data, or any value produced by a sub-command,\n * prefer the base64 variant below.\n */\nconst OUTPUT_MARKER = '::kb-output::';\n\n/**\n * Base64-encoded JSON output marker — JSON-safe for any payload.\n *\n * Usage (shell):\n * PAYLOAD=$(jq -cn --arg plan \"$(cat /tmp/plan.md)\" '{plan: $plan}' | base64 -w0)\n * echo \"::kb-output:base64::${PAYLOAD}\"\n *\n * This handles multi-line text, special chars, and any binary-safe content.\n * The base64 string is decoded and JSON-parsed by the runtime.\n */\nconst OUTPUT_MARKER_B64 = '::kb-output:base64::';\n\n/**\n * Parse a single ::kb-output:: or ::kb-output:base64:: marker line into key-value pairs.\n *\n * Returns the parsed object, or null if the line is not a recognized marker.\n * Throws a descriptive error if the marker payload is malformed so callers can warn.\n */\nexport function parseOutputMarkerLine(line: string): Record<string, unknown> | null {\n // Base64-encoded variant — JSON-safe for any payload\n const b64Idx = line.indexOf(OUTPUT_MARKER_B64);\n if (b64Idx !== -1) {\n const raw = line.slice(b64Idx + OUTPUT_MARKER_B64.length).trim();\n let decoded: string;\n try {\n decoded = Buffer.from(raw, 'base64').toString('utf8');\n } catch {\n throw new Error(`::kb-output:base64:: payload is not valid base64`);\n }\n let parsed: unknown;\n try {\n parsed = JSON.parse(decoded);\n } catch (err) {\n const detail = err instanceof Error ? err.message : String(err);\n throw new Error(`::kb-output:base64:: malformed JSON payload after decode: ${detail}`);\n }\n if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {\n return parsed as Record<string, unknown>;\n }\n throw new Error(`::kb-output:base64:: JSON must be an object, got ${Array.isArray(parsed) ? 'array' : typeof parsed}`);\n }\n\n // Plain-JSON variant — only safe for simple scalar values\n const idx = line.indexOf(OUTPUT_MARKER);\n if (idx !== -1) {\n const raw = line.slice(idx + OUTPUT_MARKER.length);\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch (err) {\n const detail = err instanceof Error ? err.message : String(err);\n throw new Error(`::kb-output:: malformed JSON payload: ${detail}`);\n }\n if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {\n return parsed as Record<string, unknown>;\n }\n throw new Error(`::kb-output:: JSON must be an object, got ${Array.isArray(parsed) ? 'array' : typeof parsed}`);\n }\n\n return null;\n}\n\n/**\n * Extract structured outputs from shell stdout.\n *\n * Priority:\n * 1. ::kb-output:base64:: marker lines — JSON-safe, recommended for complex values\n * 2. ::kb-output:: marker lines — plain JSON, ok for simple scalars\n * 3. Entire stdout as JSON — fallback for backward compat (simple commands)\n *\n * Malformed markers emit a warning instead of silently dropping the output.\n * Logs and other stdout content are ignored for output purposes.\n */\nexport function mergeJsonOutputs(output: ShellOutput, warn?: (msg: string) => void): 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 + 2: Look for ::kb-output:: or ::kb-output:base64:: marker lines\n const lines = output.stdout.split('\\n');\n let foundMarker = false;\n for (const line of lines) {\n if (!line.includes('::kb-output')) {continue;}\n foundMarker = true;\n try {\n const parsed = parseOutputMarkerLine(line);\n if (parsed) {\n Object.assign(base, parsed);\n }\n } catch (err) {\n // Malformed marker — warn instead of silently dropping\n const msg = err instanceof Error ? err.message : String(err);\n warn?.(`Malformed ::kb-output:: marker (outputs not populated): ${msg}`);\n }\n }\n\n if (foundMarker) {return base;}\n\n // Priority 3: Fallback — entire stdout as JSON (backward compat)\n try {\n const parsed: unknown = JSON.parse(trimmed);\n if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {\n Object.assign(base, parsed as Record<string, unknown>);\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 // detached: true puts the subprocess in its own process group. On timeout we kill\n // the entire group (process.kill(-pid, 'SIGKILL')), which takes down both the shell\n // and any children it spawned. Without this, SIGTERM reaches only the shell; child\n // processes (e.g. `sleep N`) inherit the pipe write-end and block await indefinitely.\n const proc = execaCommand(command, {\n cwd,\n env: mergedEnv,\n shell: true,\n stdio: 'pipe',\n reject: false, // We handle exit codes ourselves\n detached: true,\n });\n\n // Stream stdout/stderr line-by-line in real-time.\n // We consume the streams ourselves via 'data' events for live log streaming.\n // Because attaching a 'data' listener drains the stream, execa's result.stdout\n // will be empty — we must reconstruct the full output from collected chunks.\n let lineNo = 0;\n let stdoutBuf = '';\n let stderrBuf = '';\n let stdoutFull = '';\n let stderrFull = '';\n\n const emitLine = (stream: 'stdout' | 'stderr', line: string) => {\n lineNo++;\n // stderr is an output stream, not an exit-status signal. Several\n // perfectly successful tools (notably command discovery) deliberately\n // write diagnostics there. The step result is evaluated from exitCode;\n // keep stderr visible without turning every diagnostic line into an\n // error event.\n void ctx.api.events.emit('log.line', { stream, line, lineNo, level: stream === 'stderr' ? 'warn' : 'info' });\n };\n\n proc.stdout?.on('data', (chunk: Buffer) => {\n const text = chunk.toString();\n stdoutFull += text;\n stdoutBuf += text;\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 const text = chunk.toString();\n stderrFull += text;\n stderrBuf += text;\n const lines = stderrBuf.split('\\n');\n stderrBuf = lines.pop() ?? '';\n for (const line of lines) {emitLine('stderr', line);}\n });\n\n // Self-managed timeout: kill the entire process group so orphaned children\n // release the pipe write-end and await proc resolves immediately.\n let timedOut = false;\n const killTimer = setTimeout(() => {\n timedOut = true;\n const pid = proc.pid;\n if (pid !== undefined) {\n try {\n process.kill(-pid, 'SIGKILL');\n } catch {\n // process already exited\n }\n }\n }, timeout);\n\n const result = await proc;\n clearTimeout(killTimer);\n\n // Flush remaining buffered content\n if (stdoutBuf) {emitLine('stdout', stdoutBuf);}\n if (stderrBuf) {emitLine('stderr', stderrBuf);}\n\n if (timedOut) {\n // Whatever the command printed before the kill is the only clue to\n // whether it was hung or just slow — without it the run log shows a\n // multi-hour gap between \"Executing shell command\" and this error and\n // nothing in between (see kb-labs incident: 20min Build-step timeout\n // debugged blind because of exactly this).\n ctx.platform.logger.warn('Shell command timed out', {\n timeoutMs: timeout,\n stderrTail: tail(stderrFull, FAILURE_OUTPUT_TAIL_CHARS),\n stdoutTail: tail(stdoutFull, FAILURE_OUTPUT_TAIL_CHARS),\n });\n throw new Error(`Shell command timed out after ${timeout}ms`);\n }\n\n const output: ShellOutput = {\n // Use our accumulated buffers — result.stdout/stderr are empty because\n // we consumed the streams via 'data' listeners above.\n stdout: stdoutFull || result.stdout,\n stderr: stderrFull || 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 // Attach the actual output, not just its shape — this is the log entry\n // that shows up in `kb workflow runs logs`, and it used to carry only\n // exitCode + a line count. Debugging a failed step meant re-running the\n // command outside the workflow just to see why it failed.\n ctx.platform.logger.warn('Shell command failed', {\n exitCode: output.exitCode,\n stderrLines: output.stderr.split('\\n').length,\n stderrTail: tail(output.stderr, FAILURE_OUTPUT_TAIL_CHARS),\n stdoutTail: tail(output.stdout, FAILURE_OUTPUT_TAIL_CHARS),\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, (msg) => ctx.platform.logger.warn(`[shell] ${msg}`));\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","/**\n * @module @kb-labs/workflow-steps/approval\n * Types and handler for builtin:approval step\n *\n * Approval steps pause the pipeline and wait for human decision.\n * The worker handles polling; resolveApproval() on the engine resumes execution.\n */\n\n/**\n * Input for builtin:approval step (spec.with)\n */\nexport interface ApprovalInput {\n /** Display title for the approval request */\n title: string;\n\n /** Contextual data shown to the approver (already interpolated) */\n context?: Record<string, unknown>;\n\n /** Optional instructions for the approver */\n instructions?: string;\n}\n\n/**\n * Output produced by a resolved approval step\n */\nexport interface ApprovalOutput {\n /** Whether the approval was granted */\n approved: boolean;\n\n /** Action taken: \"approve\" or \"reject\" */\n action: 'approve' | 'reject';\n\n /** Optional comment from the approver */\n comment?: string;\n\n /** Additional data provided by the approver */\n [key: string]: unknown;\n}\n\n/**\n * ApprovalHandler — signals the worker to pause and wait for human approval.\n * The worker owns the polling loop; this handler only marks the intent.\n */\nexport class ApprovalHandler {\n static readonly uses = 'builtin:approval';\n\n handle(_input: ApprovalInput): { status: 'waiting'; reason: 'approval' } {\n return { status: 'waiting', reason: 'approval' };\n }\n}\n","/**\n * @module @kb-labs/workflow-steps/gate\n * Types and handler for builtin:gate step\n *\n * Gate steps act as automatic routers — they read a decision value\n * from previous step outputs and route the pipeline accordingly:\n * - continue: proceed to next step\n * - fail: fail the pipeline\n * - restart: reset steps back to target and re-schedule with context\n * - skip: mark intermediate steps as skipped and jump forward to a target step\n */\n\nimport { resolveValue, type ExpressionContext } from '@kb-labs/workflow-contracts';\n\n/**\n * Route action for a gate decision\n */\nexport type GateRouteAction =\n | 'continue'\n | 'fail'\n | {\n /** Step ID to restart from (go backward) */\n restartFrom: string;\n /** Additional context to pass (merged into trigger.payload) */\n context?: Record<string, unknown>;\n }\n | {\n /** Step ID to skip forward to — all steps between gate and target are marked skipped */\n skipTo: string;\n };\n\n/**\n * Input for builtin:gate step (spec.with)\n */\nexport interface GateInput {\n /** Expression path to the decision value (e.g. \"steps.review.outputs.passed\") */\n decision: string;\n\n /** Route map: decision value → action */\n routes: Record<string, GateRouteAction>;\n\n /** Default action if decision value doesn't match any route */\n default?: 'continue' | 'fail';\n\n /** Maximum number of restart iterations before failing (default: 3) */\n maxIterations?: number;\n}\n\n/**\n * Output produced by a resolved gate step\n */\nexport interface GateOutput {\n /** The decision value that was evaluated */\n decisionValue: unknown;\n\n /** The action that was taken */\n action: 'continue' | 'fail' | 'restart' | 'skip';\n\n /** Step ID that was restarted from (if restart) */\n restartFrom?: string;\n\n /** Step ID that was skipped to (if skip) */\n skipTo?: string;\n\n /** Current iteration count */\n iteration: number;\n\n /** Set to true when the gate exhausted its maxIterations budget */\n maxIterationsReached?: boolean;\n\n [key: string]: unknown;\n}\n\n/**\n * Pure decision result from GateHandler.\n * Worker applies the state mutations based on the action.\n */\nexport type GateDecision =\n | { action: 'continue'; outputs: GateOutput }\n | { action: 'fail'; error: Error; outputs: GateOutput }\n | {\n action: 'restart';\n restartFrom: string;\n context?: Record<string, unknown>;\n outputs: GateOutput;\n nextIteration: number;\n }\n | {\n action: 'skip';\n skipTo: string;\n outputs: GateOutput;\n };\n\n/**\n * GateHandler — pure decision function.\n * No I/O, no state mutations. Worker owns the side effects.\n */\nexport class GateHandler {\n static readonly uses = 'builtin:gate';\n\n /**\n * @param validRestartTargets Identifiers the worker can reset on restart —\n * step ids/spec.ids within the gate's own job AND other job names (for\n * cross-job restart). When provided and `restartFrom` matches none of them,\n * the gate FAILS instead of attempting a restart the worker cannot apply —\n * which would otherwise silently degrade into \"complete the gate and\n * proceed\" (a false green).\n */\n handle(\n input: GateInput,\n exprCtx: ExpressionContext,\n currentIteration: number,\n validRestartTargets?: string[],\n ): GateDecision {\n const maxIterations = input.maxIterations ?? 3;\n const decisionValue = resolveValue(input.decision, exprCtx);\n const decisionKey = String(decisionValue);\n\n const route: GateRouteAction | undefined =\n input.routes[decisionKey] ?? input.routes[decisionValue as string];\n const action = route ?? input.default ?? 'fail';\n\n if (action === 'continue') {\n return {\n action: 'continue',\n outputs: { decisionValue, action: 'continue', iteration: currentIteration },\n };\n }\n\n if (action === 'fail') {\n const failReason =\n (route as { message?: string } | undefined)?.message ??\n `No matching route for value \"${decisionKey}\"`;\n return {\n action: 'fail',\n error: new Error(`Gate failed: ${failReason}`),\n outputs: { decisionValue, action: 'fail', iteration: currentIteration },\n };\n }\n\n // skipTo action — jump forward, marking intermediate steps as skipped\n if (typeof action === 'object' && 'skipTo' in action) {\n if (validRestartTargets && !validRestartTargets.includes(action.skipTo)) {\n return {\n action: 'fail',\n error: new Error(\n `Gate skipTo \"${action.skipTo}\" matches no step in this job — cannot skip.`,\n ),\n outputs: { decisionValue, action: 'fail', iteration: currentIteration },\n };\n }\n return {\n action: 'skip',\n skipTo: action.skipTo,\n outputs: {\n decisionValue,\n action: 'skip',\n skipTo: action.skipTo,\n iteration: currentIteration,\n },\n };\n }\n\n // restart action\n const restartAction = action as { restartFrom: string; context?: Record<string, unknown> };\n\n // Guard: restartFrom must name something the worker can reset — a step in\n // this job or another job by name. If it matches neither, the gate cannot\n // recover; fail honestly rather than let the worker silently complete the\n // gate and proceed (which would ship unverified work as a false green).\n if (validRestartTargets && !validRestartTargets.includes(restartAction.restartFrom)) {\n return {\n action: 'fail',\n error: new Error(\n `Gate restartFrom \"${restartAction.restartFrom}\" matches no step in this job ` +\n `or any job in the run — cannot restart. Failing instead of silently passing.`,\n ),\n outputs: { decisionValue, action: 'fail', iteration: currentIteration },\n };\n }\n\n const nextIteration = currentIteration + 1;\n\n if (nextIteration >= maxIterations) {\n return {\n action: 'fail',\n error: new Error(`Gate max iterations reached (${maxIterations}) for step`),\n outputs: {\n decisionValue,\n action: 'fail',\n iteration: currentIteration,\n maxIterationsReached: true,\n },\n };\n }\n\n return {\n action: 'restart',\n restartFrom: restartAction.restartFrom,\n context: restartAction.context,\n outputs: {\n decisionValue,\n action: 'restart',\n restartFrom: restartAction.restartFrom,\n iteration: nextIteration,\n },\n nextIteration,\n };\n }\n}\n"]}
|
package/dist/shell.d.ts
CHANGED
|
@@ -11,6 +11,12 @@ import { PluginContextV3 } from '@kb-labs/plugin-contracts';
|
|
|
11
11
|
* - Working directory restrictions
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
+
/**
|
|
15
|
+
* Last N characters of `text`, prefixed with a marker when it was truncated.
|
|
16
|
+
* Tail (not head) because the actionable error is almost always at the end —
|
|
17
|
+
* a stack trace, an assertion failure, the final "Error:" line.
|
|
18
|
+
*/
|
|
19
|
+
declare function tail(text: string, maxChars: number): string;
|
|
14
20
|
/**
|
|
15
21
|
* Shell handler input
|
|
16
22
|
*/
|
|
@@ -71,4 +77,4 @@ declare const _default: {
|
|
|
71
77
|
execute: typeof shellHandler;
|
|
72
78
|
};
|
|
73
79
|
|
|
74
|
-
export { type ShellInput, type ShellOutput, _default as default, mergeJsonOutputs, parseOutputMarkerLine };
|
|
80
|
+
export { type ShellInput, type ShellOutput, _default as default, mergeJsonOutputs, parseOutputMarkerLine, tail };
|
package/dist/shell.js
CHANGED
|
@@ -14,6 +14,14 @@ var BLOCKED_COMMANDS = [
|
|
|
14
14
|
"mv /* ",
|
|
15
15
|
"fdisk"
|
|
16
16
|
];
|
|
17
|
+
var FAILURE_OUTPUT_TAIL_CHARS = 4e3;
|
|
18
|
+
function tail(text, maxChars) {
|
|
19
|
+
if (text.length <= maxChars) {
|
|
20
|
+
return text;
|
|
21
|
+
}
|
|
22
|
+
return `\u2026(truncated, showing last ${maxChars} of ${text.length} chars)
|
|
23
|
+
${text.slice(-maxChars)}`;
|
|
24
|
+
}
|
|
17
25
|
var OUTPUT_MARKER = "::kb-output::";
|
|
18
26
|
var OUTPUT_MARKER_B64 = "::kb-output:base64::";
|
|
19
27
|
function parseOutputMarkerLine(line) {
|
|
@@ -169,6 +177,11 @@ async function shellHandler(ctx, input) {
|
|
|
169
177
|
emitLine("stderr", stderrBuf);
|
|
170
178
|
}
|
|
171
179
|
if (timedOut) {
|
|
180
|
+
ctx.platform.logger.warn("Shell command timed out", {
|
|
181
|
+
timeoutMs: timeout,
|
|
182
|
+
stderrTail: tail(stderrFull, FAILURE_OUTPUT_TAIL_CHARS),
|
|
183
|
+
stdoutTail: tail(stdoutFull, FAILURE_OUTPUT_TAIL_CHARS)
|
|
184
|
+
});
|
|
172
185
|
throw new Error(`Shell command timed out after ${timeout}ms`);
|
|
173
186
|
}
|
|
174
187
|
const output = {
|
|
@@ -187,7 +200,9 @@ async function shellHandler(ctx, input) {
|
|
|
187
200
|
} else {
|
|
188
201
|
ctx.platform.logger.warn("Shell command failed", {
|
|
189
202
|
exitCode: output.exitCode,
|
|
190
|
-
stderrLines: output.stderr.split("\n").length
|
|
203
|
+
stderrLines: output.stderr.split("\n").length,
|
|
204
|
+
stderrTail: tail(output.stderr, FAILURE_OUTPUT_TAIL_CHARS),
|
|
205
|
+
stdoutTail: tail(output.stdout, FAILURE_OUTPUT_TAIL_CHARS)
|
|
191
206
|
});
|
|
192
207
|
if (throwOnError) {
|
|
193
208
|
throw new Error(`Shell command failed with exit code ${output.exitCode}: ${output.stderr.slice(0, 500)}`);
|
|
@@ -221,6 +236,6 @@ var shell_default = {
|
|
|
221
236
|
execute: shellHandler
|
|
222
237
|
};
|
|
223
238
|
|
|
224
|
-
export { shell_default as default, mergeJsonOutputs, parseOutputMarkerLine };
|
|
239
|
+
export { shell_default as default, mergeJsonOutputs, parseOutputMarkerLine, tail };
|
|
225
240
|
//# sourceMappingURL=shell.js.map
|
|
226
241
|
//# sourceMappingURL=shell.js.map
|
package/dist/shell.js.map
CHANGED
|
@@ -1 +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 * Plain-JSON output marker.\n * echo '::kb-output::{\"passed\":true}'\n *\n * Only safe for values without embedded newlines, quotes, or control chars.\n * For multi-line text, binary data, or any value produced by a sub-command,\n * prefer the base64 variant below.\n */\nconst OUTPUT_MARKER = '::kb-output::';\n\n/**\n * Base64-encoded JSON output marker — JSON-safe for any payload.\n *\n * Usage (shell):\n * PAYLOAD=$(jq -cn --arg plan \"$(cat /tmp/plan.md)\" '{plan: $plan}' | base64 -w0)\n * echo \"::kb-output:base64::${PAYLOAD}\"\n *\n * This handles multi-line text, special chars, and any binary-safe content.\n * The base64 string is decoded and JSON-parsed by the runtime.\n */\nconst OUTPUT_MARKER_B64 = '::kb-output:base64::';\n\n/**\n * Parse a single ::kb-output:: or ::kb-output:base64:: marker line into key-value pairs.\n *\n * Returns the parsed object, or null if the line is not a recognized marker.\n * Throws a descriptive error if the marker payload is malformed so callers can warn.\n */\nexport function parseOutputMarkerLine(line: string): Record<string, unknown> | null {\n // Base64-encoded variant — JSON-safe for any payload\n const b64Idx = line.indexOf(OUTPUT_MARKER_B64);\n if (b64Idx !== -1) {\n const raw = line.slice(b64Idx + OUTPUT_MARKER_B64.length).trim();\n let decoded: string;\n try {\n decoded = Buffer.from(raw, 'base64').toString('utf8');\n } catch {\n throw new Error(`::kb-output:base64:: payload is not valid base64`);\n }\n let parsed: unknown;\n try {\n parsed = JSON.parse(decoded);\n } catch (err) {\n const detail = err instanceof Error ? err.message : String(err);\n throw new Error(`::kb-output:base64:: malformed JSON payload after decode: ${detail}`);\n }\n if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {\n return parsed as Record<string, unknown>;\n }\n throw new Error(`::kb-output:base64:: JSON must be an object, got ${Array.isArray(parsed) ? 'array' : typeof parsed}`);\n }\n\n // Plain-JSON variant — only safe for simple scalar values\n const idx = line.indexOf(OUTPUT_MARKER);\n if (idx !== -1) {\n const raw = line.slice(idx + OUTPUT_MARKER.length);\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch (err) {\n const detail = err instanceof Error ? err.message : String(err);\n throw new Error(`::kb-output:: malformed JSON payload: ${detail}`);\n }\n if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {\n return parsed as Record<string, unknown>;\n }\n throw new Error(`::kb-output:: JSON must be an object, got ${Array.isArray(parsed) ? 'array' : typeof parsed}`);\n }\n\n return null;\n}\n\n/**\n * Extract structured outputs from shell stdout.\n *\n * Priority:\n * 1. ::kb-output:base64:: marker lines — JSON-safe, recommended for complex values\n * 2. ::kb-output:: marker lines — plain JSON, ok for simple scalars\n * 3. Entire stdout as JSON — fallback for backward compat (simple commands)\n *\n * Malformed markers emit a warning instead of silently dropping the output.\n * Logs and other stdout content are ignored for output purposes.\n */\nexport function mergeJsonOutputs(output: ShellOutput, warn?: (msg: string) => void): 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 + 2: Look for ::kb-output:: or ::kb-output:base64:: marker lines\n const lines = output.stdout.split('\\n');\n let foundMarker = false;\n for (const line of lines) {\n if (!line.includes('::kb-output')) {continue;}\n foundMarker = true;\n try {\n const parsed = parseOutputMarkerLine(line);\n if (parsed) {\n Object.assign(base, parsed);\n }\n } catch (err) {\n // Malformed marker — warn instead of silently dropping\n const msg = err instanceof Error ? err.message : String(err);\n warn?.(`Malformed ::kb-output:: marker (outputs not populated): ${msg}`);\n }\n }\n\n if (foundMarker) {return base;}\n\n // Priority 3: Fallback — entire stdout as JSON (backward compat)\n try {\n const parsed: unknown = JSON.parse(trimmed);\n if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {\n Object.assign(base, parsed as Record<string, unknown>);\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 // detached: true puts the subprocess in its own process group. On timeout we kill\n // the entire group (process.kill(-pid, 'SIGKILL')), which takes down both the shell\n // and any children it spawned. Without this, SIGTERM reaches only the shell; child\n // processes (e.g. `sleep N`) inherit the pipe write-end and block await indefinitely.\n const proc = execaCommand(command, {\n cwd,\n env: mergedEnv,\n shell: true,\n stdio: 'pipe',\n reject: false, // We handle exit codes ourselves\n detached: true,\n });\n\n // Stream stdout/stderr line-by-line in real-time.\n // We consume the streams ourselves via 'data' events for live log streaming.\n // Because attaching a 'data' listener drains the stream, execa's result.stdout\n // will be empty — we must reconstruct the full output from collected chunks.\n let lineNo = 0;\n let stdoutBuf = '';\n let stderrBuf = '';\n let stdoutFull = '';\n let stderrFull = '';\n\n const emitLine = (stream: 'stdout' | 'stderr', line: string) => {\n lineNo++;\n // stderr is an output stream, not an exit-status signal. Several\n // perfectly successful tools (notably command discovery) deliberately\n // write diagnostics there. The step result is evaluated from exitCode;\n // keep stderr visible without turning every diagnostic line into an\n // error event.\n void ctx.api.events.emit('log.line', { stream, line, lineNo, level: stream === 'stderr' ? 'warn' : 'info' });\n };\n\n proc.stdout?.on('data', (chunk: Buffer) => {\n const text = chunk.toString();\n stdoutFull += text;\n stdoutBuf += text;\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 const text = chunk.toString();\n stderrFull += text;\n stderrBuf += text;\n const lines = stderrBuf.split('\\n');\n stderrBuf = lines.pop() ?? '';\n for (const line of lines) {emitLine('stderr', line);}\n });\n\n // Self-managed timeout: kill the entire process group so orphaned children\n // release the pipe write-end and await proc resolves immediately.\n let timedOut = false;\n const killTimer = setTimeout(() => {\n timedOut = true;\n const pid = proc.pid;\n if (pid !== undefined) {\n try {\n process.kill(-pid, 'SIGKILL');\n } catch {\n // process already exited\n }\n }\n }, timeout);\n\n const result = await proc;\n clearTimeout(killTimer);\n\n // Flush remaining buffered content\n if (stdoutBuf) {emitLine('stdout', stdoutBuf);}\n if (stderrBuf) {emitLine('stderr', stderrBuf);}\n\n if (timedOut) {\n throw new Error(`Shell command timed out after ${timeout}ms`);\n }\n\n const output: ShellOutput = {\n // Use our accumulated buffers — result.stdout/stderr are empty because\n // we consumed the streams via 'data' listeners above.\n stdout: stdoutFull || result.stdout,\n stderr: stderrFull || 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, (msg) => ctx.platform.logger.warn(`[shell] ${msg}`));\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"]}
|
|
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/** Cap on how much of a failed command's output gets attached to its log entry. */\nconst FAILURE_OUTPUT_TAIL_CHARS = 4000;\n\n/**\n * Last N characters of `text`, prefixed with a marker when it was truncated.\n * Tail (not head) because the actionable error is almost always at the end —\n * a stack trace, an assertion failure, the final \"Error:\" line.\n */\nexport function tail(text: string, maxChars: number): string {\n if (text.length <= maxChars) {return text;}\n return `…(truncated, showing last ${maxChars} of ${text.length} chars)\\n${text.slice(-maxChars)}`;\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 * Plain-JSON output marker.\n * echo '::kb-output::{\"passed\":true}'\n *\n * Only safe for values without embedded newlines, quotes, or control chars.\n * For multi-line text, binary data, or any value produced by a sub-command,\n * prefer the base64 variant below.\n */\nconst OUTPUT_MARKER = '::kb-output::';\n\n/**\n * Base64-encoded JSON output marker — JSON-safe for any payload.\n *\n * Usage (shell):\n * PAYLOAD=$(jq -cn --arg plan \"$(cat /tmp/plan.md)\" '{plan: $plan}' | base64 -w0)\n * echo \"::kb-output:base64::${PAYLOAD}\"\n *\n * This handles multi-line text, special chars, and any binary-safe content.\n * The base64 string is decoded and JSON-parsed by the runtime.\n */\nconst OUTPUT_MARKER_B64 = '::kb-output:base64::';\n\n/**\n * Parse a single ::kb-output:: or ::kb-output:base64:: marker line into key-value pairs.\n *\n * Returns the parsed object, or null if the line is not a recognized marker.\n * Throws a descriptive error if the marker payload is malformed so callers can warn.\n */\nexport function parseOutputMarkerLine(line: string): Record<string, unknown> | null {\n // Base64-encoded variant — JSON-safe for any payload\n const b64Idx = line.indexOf(OUTPUT_MARKER_B64);\n if (b64Idx !== -1) {\n const raw = line.slice(b64Idx + OUTPUT_MARKER_B64.length).trim();\n let decoded: string;\n try {\n decoded = Buffer.from(raw, 'base64').toString('utf8');\n } catch {\n throw new Error(`::kb-output:base64:: payload is not valid base64`);\n }\n let parsed: unknown;\n try {\n parsed = JSON.parse(decoded);\n } catch (err) {\n const detail = err instanceof Error ? err.message : String(err);\n throw new Error(`::kb-output:base64:: malformed JSON payload after decode: ${detail}`);\n }\n if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {\n return parsed as Record<string, unknown>;\n }\n throw new Error(`::kb-output:base64:: JSON must be an object, got ${Array.isArray(parsed) ? 'array' : typeof parsed}`);\n }\n\n // Plain-JSON variant — only safe for simple scalar values\n const idx = line.indexOf(OUTPUT_MARKER);\n if (idx !== -1) {\n const raw = line.slice(idx + OUTPUT_MARKER.length);\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch (err) {\n const detail = err instanceof Error ? err.message : String(err);\n throw new Error(`::kb-output:: malformed JSON payload: ${detail}`);\n }\n if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {\n return parsed as Record<string, unknown>;\n }\n throw new Error(`::kb-output:: JSON must be an object, got ${Array.isArray(parsed) ? 'array' : typeof parsed}`);\n }\n\n return null;\n}\n\n/**\n * Extract structured outputs from shell stdout.\n *\n * Priority:\n * 1. ::kb-output:base64:: marker lines — JSON-safe, recommended for complex values\n * 2. ::kb-output:: marker lines — plain JSON, ok for simple scalars\n * 3. Entire stdout as JSON — fallback for backward compat (simple commands)\n *\n * Malformed markers emit a warning instead of silently dropping the output.\n * Logs and other stdout content are ignored for output purposes.\n */\nexport function mergeJsonOutputs(output: ShellOutput, warn?: (msg: string) => void): 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 + 2: Look for ::kb-output:: or ::kb-output:base64:: marker lines\n const lines = output.stdout.split('\\n');\n let foundMarker = false;\n for (const line of lines) {\n if (!line.includes('::kb-output')) {continue;}\n foundMarker = true;\n try {\n const parsed = parseOutputMarkerLine(line);\n if (parsed) {\n Object.assign(base, parsed);\n }\n } catch (err) {\n // Malformed marker — warn instead of silently dropping\n const msg = err instanceof Error ? err.message : String(err);\n warn?.(`Malformed ::kb-output:: marker (outputs not populated): ${msg}`);\n }\n }\n\n if (foundMarker) {return base;}\n\n // Priority 3: Fallback — entire stdout as JSON (backward compat)\n try {\n const parsed: unknown = JSON.parse(trimmed);\n if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {\n Object.assign(base, parsed as Record<string, unknown>);\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 // detached: true puts the subprocess in its own process group. On timeout we kill\n // the entire group (process.kill(-pid, 'SIGKILL')), which takes down both the shell\n // and any children it spawned. Without this, SIGTERM reaches only the shell; child\n // processes (e.g. `sleep N`) inherit the pipe write-end and block await indefinitely.\n const proc = execaCommand(command, {\n cwd,\n env: mergedEnv,\n shell: true,\n stdio: 'pipe',\n reject: false, // We handle exit codes ourselves\n detached: true,\n });\n\n // Stream stdout/stderr line-by-line in real-time.\n // We consume the streams ourselves via 'data' events for live log streaming.\n // Because attaching a 'data' listener drains the stream, execa's result.stdout\n // will be empty — we must reconstruct the full output from collected chunks.\n let lineNo = 0;\n let stdoutBuf = '';\n let stderrBuf = '';\n let stdoutFull = '';\n let stderrFull = '';\n\n const emitLine = (stream: 'stdout' | 'stderr', line: string) => {\n lineNo++;\n // stderr is an output stream, not an exit-status signal. Several\n // perfectly successful tools (notably command discovery) deliberately\n // write diagnostics there. The step result is evaluated from exitCode;\n // keep stderr visible without turning every diagnostic line into an\n // error event.\n void ctx.api.events.emit('log.line', { stream, line, lineNo, level: stream === 'stderr' ? 'warn' : 'info' });\n };\n\n proc.stdout?.on('data', (chunk: Buffer) => {\n const text = chunk.toString();\n stdoutFull += text;\n stdoutBuf += text;\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 const text = chunk.toString();\n stderrFull += text;\n stderrBuf += text;\n const lines = stderrBuf.split('\\n');\n stderrBuf = lines.pop() ?? '';\n for (const line of lines) {emitLine('stderr', line);}\n });\n\n // Self-managed timeout: kill the entire process group so orphaned children\n // release the pipe write-end and await proc resolves immediately.\n let timedOut = false;\n const killTimer = setTimeout(() => {\n timedOut = true;\n const pid = proc.pid;\n if (pid !== undefined) {\n try {\n process.kill(-pid, 'SIGKILL');\n } catch {\n // process already exited\n }\n }\n }, timeout);\n\n const result = await proc;\n clearTimeout(killTimer);\n\n // Flush remaining buffered content\n if (stdoutBuf) {emitLine('stdout', stdoutBuf);}\n if (stderrBuf) {emitLine('stderr', stderrBuf);}\n\n if (timedOut) {\n // Whatever the command printed before the kill is the only clue to\n // whether it was hung or just slow — without it the run log shows a\n // multi-hour gap between \"Executing shell command\" and this error and\n // nothing in between (see kb-labs incident: 20min Build-step timeout\n // debugged blind because of exactly this).\n ctx.platform.logger.warn('Shell command timed out', {\n timeoutMs: timeout,\n stderrTail: tail(stderrFull, FAILURE_OUTPUT_TAIL_CHARS),\n stdoutTail: tail(stdoutFull, FAILURE_OUTPUT_TAIL_CHARS),\n });\n throw new Error(`Shell command timed out after ${timeout}ms`);\n }\n\n const output: ShellOutput = {\n // Use our accumulated buffers — result.stdout/stderr are empty because\n // we consumed the streams via 'data' listeners above.\n stdout: stdoutFull || result.stdout,\n stderr: stderrFull || 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 // Attach the actual output, not just its shape — this is the log entry\n // that shows up in `kb workflow runs logs`, and it used to carry only\n // exitCode + a line count. Debugging a failed step meant re-running the\n // command outside the workflow just to see why it failed.\n ctx.platform.logger.warn('Shell command failed', {\n exitCode: output.exitCode,\n stderrLines: output.stderr.split('\\n').length,\n stderrTail: tail(output.stderr, FAILURE_OUTPUT_TAIL_CHARS),\n stdoutTail: tail(output.stdout, FAILURE_OUTPUT_TAIL_CHARS),\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, (msg) => ctx.platform.logger.warn(`[shell] ${msg}`));\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
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kb-labs/workflow-steps",
|
|
3
3
|
"description": "Built-in workflow step handlers (shell, approval, gate)",
|
|
4
|
-
"version": "2.
|
|
4
|
+
"version": "2.118.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
7
7
|
"types": "./dist/index.d.ts",
|
|
@@ -29,8 +29,8 @@
|
|
|
29
29
|
],
|
|
30
30
|
"dependencies": {
|
|
31
31
|
"execa": "^9.5.3",
|
|
32
|
-
"@kb-labs/plugin-contracts": "2.
|
|
33
|
-
"@kb-labs/workflow-contracts": "2.
|
|
32
|
+
"@kb-labs/plugin-contracts": "2.118.0",
|
|
33
|
+
"@kb-labs/workflow-contracts": "2.118.0"
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|
|
36
36
|
"@types/node": "^24.3.3",
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
"tsup": "^8.5.0",
|
|
39
39
|
"typescript": "^5.6.3",
|
|
40
40
|
"vitest": "^3.2.6",
|
|
41
|
-
"@kb-labs/devkit": "2.
|
|
41
|
+
"@kb-labs/devkit": "2.118.0"
|
|
42
42
|
},
|
|
43
43
|
"engines": {
|
|
44
44
|
"node": ">=22.0.0",
|