@tbrandenburg/node-red-cli 0.2.11 → 0.2.13

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 CHANGED
@@ -54,6 +54,7 @@ make install-global
54
54
  - [Passing flow JSON inline](#passing-flow-json-inline)
55
55
  - [Installing additional Node-RED node packages](#installing-additional-node-red-node-packages)
56
56
  - [Running sandboxed in Docker](#running-sandboxed-in-docker-)
57
+ - [Agentic workflows](#agentic-workflows-)
57
58
  - [Host API](#host-api-)
58
59
  - [Technical approach](#technical-approach-)
59
60
  - [Preflight and limitations](#preflight-and-limitations-)
@@ -277,7 +278,9 @@ Sandboxing defaults applied to every `--docker` run:
277
278
 
278
279
  - `--rm -i` (always disposable)
279
280
  - `--network none`, unless `--node-modules` is also given (needs registry
280
- access) narrowest network exposure by default
281
+ access) or `--network` is passed explicitly (enables network access for a
282
+ flow that needs to call out, independent of installing any package) —
283
+ narrowest network exposure by default
281
284
  - `--read-only` root filesystem + a `/tmp` tmpfs mount
282
285
  - `--cap-drop=ALL`
283
286
  - `--security-opt=no-new-privileges`
@@ -293,6 +296,45 @@ failed: ...` if the image build fails (e.g. the local version isn't yet
293
296
  published to npm — use `--docker <image>` or `--docker @path` as an
294
297
  escape hatch in that case).
295
298
 
299
+ ### Agentic workflows 🤖
300
+
301
+ Node-RED nodes such as [`agent`](https://www.npmjs.com/package/@tbrandenburg/node-red-agents)
302
+ (OpenCode, `pi`) turn a `link in -> agent -> link out (return)` flow into a
303
+ callable AI step, invoked like any other target — a JSON flow with an
304
+ inline multiline prompt, in one command:
305
+
306
+ ```bash
307
+ echo '{"payload":"Summarize this repo in one sentence.","cwd":"/repo"}' \
308
+ | node-red-cli --flow-json '[
309
+ {"id":"tab","type":"tab","label":"Agent"},
310
+ {"id":"ask","type":"link in","z":"tab","name":"ask","wires":[["agent"]]},
311
+ {"id":"agent","type":"agent","z":"tab","name":"opencode","agent":"opencode",
312
+ "runtime":"direct","prompt":"payload","promptType":"msg",
313
+ "cwd":"cwd","cwdType":"msg","wires":[["return"],[]]},
314
+ {"id":"return","type":"link out","z":"tab","name":"return","mode":"return"}
315
+ ]' ask --node-modules @tbrandenburg/node-red-agents --user-dir --timeout=120000 --format=json
316
+ ```
317
+
318
+ The same flow runs sandboxed via `--docker <image>` against an image that
319
+ already ships `opencode` + `node-red-agents`, e.g.
320
+ [`ghcr.io/tbrandenburg/agentic-workflow-dev-env`](https://github.com/tbrandenburg/agentic-workflow-dev-env)
321
+ (`--network` is required for network access, since the agent calls out to
322
+ its own API — the package is already in the image, so `--node-modules` is
323
+ not needed here):
324
+
325
+ ```bash
326
+ echo '{"payload":"Summarize this repo in one sentence.","cwd":"/repo"}' \
327
+ | node-red-cli --flow-json '[
328
+ {"id":"tab","type":"tab","label":"Agent"},
329
+ {"id":"ask","type":"link in","z":"tab","name":"ask","wires":[["agent"]]},
330
+ {"id":"agent","type":"agent","z":"tab","name":"opencode","agent":"opencode",
331
+ "runtime":"direct","prompt":"payload","promptType":"msg",
332
+ "cwd":"cwd","cwdType":"msg","wires":[["return"],[]]},
333
+ {"id":"return","type":"link out","z":"tab","name":"return","mode":"return"}
334
+ ]' ask --docker ghcr.io/tbrandenburg/agentic-workflow-dev-env:latest \
335
+ --network --timeout=120000 --format=json
336
+ ```
337
+
296
338
  ## Host API 🛠️
297
339
 
298
340
  The core interface is intentionally small:
@@ -70,10 +70,11 @@ const HELP_TEXT = [
70
70
  " - '@<path>' or an http(s) URL: build from a Dockerfile (local file or",
71
71
  " fetched URL), cached by content hash.",
72
72
  "Sandboxing defaults: --network none (unless --node-modules is also set,",
73
- "which needs registry access), --read-only rootfs with a /tmp tmpfs,",
74
- "--cap-drop=ALL, --security-opt=no-new-privileges. When combined with",
75
- "--user-dir, persistence uses a named Docker volume, never a host bind",
76
- "mount.",
73
+ "which needs registry access, or --network is passed explicitly to enable",
74
+ "network access independent of installing any package), --read-only",
75
+ "rootfs with a /tmp tmpfs, --cap-drop=ALL, --security-opt=no-new-privileges.",
76
+ "When combined with --user-dir, persistence uses a named Docker volume,",
77
+ "never a host bind mount.",
77
78
  "",
78
79
  "Example:",
79
80
  ' echo \'{"payload":{"x":4,"y":5}}\' | node-red-cli flows.json calculate',
@@ -231,7 +232,10 @@ async function run(args, options) {
231
232
  let result;
232
233
  try {
233
234
  image = await resolveImage(options.docker, { version });
234
- result = await runContainer(image, envelope, { networkNeeded: nodeModules.length > 0, volumeName });
235
+ result = await runContainer(image, envelope, {
236
+ networkNeeded: nodeModules.length > 0 || options.network,
237
+ volumeName
238
+ });
235
239
  } catch (error) {
236
240
  console.error(error.message);
237
241
  process.exitCode = 1;
@@ -296,6 +300,10 @@ program
296
300
  "run the invocation sandboxed in a disposable Docker container; bare = cached default image, " +
297
301
  "'<image[:tag]>' = explicit image (installed into if missing), '@path'/URL = build from a Dockerfile"
298
302
  )
303
+ .option(
304
+ "--network",
305
+ "enable network access in --docker mode, independent of --node-modules (default: --network none)"
306
+ )
299
307
  .addHelpText("after", HELP_TEXT)
300
308
  .version(version, "-v, --version", "print the installed node-red-cli version and exit")
301
309
  .action(run);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tbrandenburg/node-red-cli",
3
- "version": "0.2.11",
3
+ "version": "0.2.13",
4
4
  "description": "Call existing Node-RED flows from Node.js and the command line",
5
5
  "main": "src/link-call.js",
6
6
  "bin": {
@@ -87,9 +87,16 @@ function derivedDockerfile(image, version) {
87
87
  // The derived image is left running as root - restoring the original
88
88
  // image's default user would require inspecting the base image at build
89
89
  // time, which is out of scope here.
90
+ //
91
+ // Force the npm global prefix to /usr/local regardless of what the base
92
+ // image's own NPM_CONFIG_PREFIX/.npmrc sets: SANDBOX_ENTRY_PATH is
93
+ // hardcoded to /usr/local, so an install that lands elsewhere would
94
+ // silently succeed at build time but fail with MODULE_NOT_FOUND at
95
+ // `docker run`.
90
96
  return [
91
97
  `FROM ${image}`,
92
98
  "USER root",
99
+ "ENV NPM_CONFIG_PREFIX=/usr/local",
93
100
  `RUN npm install -g @tbrandenburg/node-red-cli@${version}`,
94
101
  `ENTRYPOINT ["node", "${SANDBOX_ENTRY_PATH}"]`,
95
102
  ""
package/src/docker-run.js CHANGED
@@ -8,8 +8,11 @@
8
8
  *
9
9
  * Sandboxing defaults (always applied):
10
10
  * - `--rm -i` (always disposable, interactive stdin)
11
- * - `--network none`, unless `networkNeeded` (i.e. `--node-modules` is set)
11
+ * - `--network none`, unless `networkNeeded` (i.e. `--node-modules` and/or
12
+ * the CLI's `--network` flag is set)
12
13
  * - `--read-only` root filesystem + a `/tmp` tmpfs mount
14
+ * - `-e HOME=/tmp`, so tools needing a writable `$HOME` (config/cache dirs)
15
+ * land on the writable `/tmp` tmpfs instead of the read-only rootfs
13
16
  * - `--cap-drop=ALL`
14
17
  * - `--security-opt=no-new-privileges`
15
18
  *
@@ -35,6 +38,7 @@ function buildRunArgs(image, { networkNeeded, volumeName }) {
35
38
  const args = ["run", "--rm", "-i"];
36
39
  if (!networkNeeded) args.push("--network", "none");
37
40
  args.push("--read-only", "--tmpfs", "/tmp", "--cap-drop=ALL", "--security-opt=no-new-privileges");
41
+ args.push("-e", "HOME=/tmp");
38
42
  if (networkNeeded) {
39
43
  // --node-modules runs a real `npm install`, which needs a writable cache
40
44
  // dir; point it at the /tmp tmpfs since the root filesystem is read-only.