mirror of
https://github.com/openclaw/lobster.git
synced 2026-08-14 08:52:48 +00:00
fix(workflows): expose args as env vars for shell-safe usage
This commit is contained in:
@@ -230,3 +230,26 @@ steps:
|
||||
### Passing data between steps (no temp files)
|
||||
|
||||
Use `stdin: $stepId.stdout` to pipe output from one step into the next.
|
||||
|
||||
## Args and shell-safety
|
||||
|
||||
`${arg}` substitution is a raw string replace into the shell command text.
|
||||
|
||||
For anything that may contain quotes, `$`, backticks, or newlines, prefer env vars:
|
||||
|
||||
- every resolved workflow arg is exposed as `LOBSTER_ARG_<NAME>` (uppercased, non-alnum → `_`)
|
||||
- the full args object is also available as `LOBSTER_ARGS_JSON`
|
||||
|
||||
Example:
|
||||
|
||||
```yaml
|
||||
args:
|
||||
text:
|
||||
default: ""
|
||||
steps:
|
||||
- id: safe
|
||||
env:
|
||||
TEXT: "$LOBSTER_ARG_TEXT"
|
||||
command: |
|
||||
jq -n --arg text "$TEXT" '{"result": $text}'
|
||||
```
|
||||
|
||||
@@ -270,6 +270,17 @@ function mergeEnv(
|
||||
results: Record<string, WorkflowStepResult>,
|
||||
) {
|
||||
const env = { ...base } as Record<string, string | undefined>;
|
||||
|
||||
// Expose resolved args as env vars so shell commands can safely reference them
|
||||
// without embedding raw values into the command string.
|
||||
// Example: $LOBSTER_ARG_TEXT
|
||||
env.LOBSTER_ARGS_JSON = JSON.stringify(args ?? {});
|
||||
for (const [key, value] of Object.entries(args ?? {})) {
|
||||
const normalized = normalizeArgEnvKey(key);
|
||||
if (!normalized) continue;
|
||||
env[`LOBSTER_ARG_${normalized}`] = String(value);
|
||||
}
|
||||
|
||||
const apply = (source?: Record<string, string>) => {
|
||||
if (!source) return;
|
||||
for (const [key, value] of Object.entries(source)) {
|
||||
@@ -278,11 +289,22 @@ function mergeEnv(
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Allow explicit env blocks to override injected defaults.
|
||||
apply(workflowEnv);
|
||||
apply(stepEnv);
|
||||
return env;
|
||||
}
|
||||
|
||||
function normalizeArgEnvKey(key: string): string | null {
|
||||
const trimmed = String(key ?? '').trim();
|
||||
if (!trimmed) return null;
|
||||
// Keep it predictable for shells: uppercase and [A-Z0-9_]
|
||||
const up = trimmed.toUpperCase();
|
||||
const normalized = up.replace(/[^A-Z0-9]+/g, '_').replace(/^_+|_+$/g, '');
|
||||
return normalized || null;
|
||||
}
|
||||
|
||||
function resolveCwd(cwd: string | undefined, args: Record<string, unknown>) {
|
||||
if (!cwd) return undefined;
|
||||
return resolveArgsTemplate(cwd, args);
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { promises as fsp } from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import os from 'node:os';
|
||||
|
||||
import { runWorkflowFile } from '../src/workflows/file.js';
|
||||
|
||||
test('workflow file exposes args as LOBSTER_ARG_* env vars (safe for quotes)', async () => {
|
||||
const workflow = {
|
||||
name: 'args-env',
|
||||
args: {
|
||||
text: { default: '' },
|
||||
},
|
||||
steps: [
|
||||
{
|
||||
id: 'echo',
|
||||
// Avoid embedding the arg into the shell command; read from env instead.
|
||||
command:
|
||||
"node -e \"process.stdout.write(JSON.stringify({text: process.env.LOBSTER_ARG_TEXT}))\"",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const tmpDir = await fsp.mkdtemp(path.join(os.tmpdir(), 'lobster-workflow-args-env-'));
|
||||
const stateDir = path.join(tmpDir, 'state');
|
||||
const filePath = path.join(tmpDir, 'workflow.lobster');
|
||||
await fsp.writeFile(filePath, JSON.stringify(workflow, null, 2), 'utf8');
|
||||
|
||||
const env = { ...process.env, LOBSTER_STATE_DIR: stateDir };
|
||||
const text = 'hello "world" $HOME `backticks` $(whoami)';
|
||||
|
||||
const result = await runWorkflowFile({
|
||||
filePath,
|
||||
args: { text },
|
||||
ctx: {
|
||||
stdin: process.stdin,
|
||||
stdout: process.stdout,
|
||||
stderr: process.stderr,
|
||||
env,
|
||||
mode: 'tool',
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(result.status, 'ok');
|
||||
assert.deepEqual(result.output, [{ text }]);
|
||||
});
|
||||
Reference in New Issue
Block a user