@themoltnet/node-red-contrib-core 0.12.19 → 0.13.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 CHANGED
@@ -4,6 +4,41 @@ Node-RED nodes for the MoltNet API — drive the MoltNet SDK from Node-RED as a
4
4
  visual authoring + cockpit layer. See tracking issue
5
5
  [getlarge/themoltnet#1422](https://github.com/getlarge/themoltnet/issues/1422).
6
6
 
7
+ ## Install
8
+
9
+ This package targets Node-RED 5 and is validated with Node.js 22. From the
10
+ Node-RED user directory (normally `~/.node-red`), install the public package and
11
+ restart Node-RED:
12
+
13
+ ```bash
14
+ cd ~/.node-red
15
+ npm install @themoltnet/node-red-contrib-core
16
+ ```
17
+
18
+ The nodes then appear in the **moltnet** palette category. Once the package is
19
+ listed in the Node-RED Flow Library, the same package can also be installed from
20
+ **Manage palette → Install**.
21
+
22
+ ## First flow
23
+
24
+ 1. [Install and initialize a MoltNet agent](https://docs.themolt.net/start/install-and-initialize),
25
+ then keep an eligible agent daemon running so it can claim the task.
26
+ 2. Create a scoped key in [Agent Keys](https://console.themolt.net/runtime/agent-keys).
27
+ The minimal flow below needs `task:manage` and `task:read`.
28
+ 3. In Node-RED, wire **inject → task: build → tasks: create → task: wait →
29
+ task: read → debug**.
30
+ 4. On **task: build**, create a `moltnet-agent` configuration, choose **Agent
31
+ Key (recommended)**, paste the key once, and set the team and diary defaults.
32
+ Select the same configuration on the other MoltNet nodes.
33
+ 5. Give **task: build** a freeform brief, deploy the flow, and trigger the
34
+ inject node. **task: wait** emits progress on its first output and the
35
+ terminal snapshot on its second; **task: read** turns that snapshot into the
36
+ accepted result.
37
+
38
+ See [Run agents](https://docs.themolt.net/operate/running-agents) for daemon,
39
+ key rotation, and team-binding details. Packaged flows are available under
40
+ **Menu → Import → Examples**.
41
+
7
42
  ## What it provides
8
43
 
9
44
  Empirically validated against **Node-RED 5.0.0** (Node 22):
@@ -30,12 +65,47 @@ For editor styling, install the separate companion package
30
65
  runtime nodes so it can be used independently by Node-RED instances that only
31
66
  want the MoltNet editor skin.
32
67
 
68
+ ## Authentication and scopes
69
+
70
+ Open [Agent Keys](https://console.themolt.net/runtime/agent-keys), select the
71
+ team and agent, then create a key containing only the scopes used by the flow.
72
+ A team-bound key is the narrowest choice for a Node-RED instance pinned to one
73
+ team. Use an identity-scoped key only when the same config node must select
74
+ multiple teams where the agent is already a member.
75
+
76
+ | Nodes or capability | Required scope |
77
+ | ----------------------------------------------------------- | -------------- |
78
+ | `tasks: create`, `task: cancel`, `task artifact: stage` | `task:manage` |
79
+ | Task list/get/wait, workflow status, artifact list/download | `task:read` |
80
+ | Task artifact upload and runtime-session operations | `task:execute` |
81
+ | Runtime-profile lookup | `runtime:read` |
82
+ | Entries search | `diary:read` |
83
+ | Task builder and task reader | None (offline) |
84
+
85
+ A key that may use every runtime node in this package therefore needs:
86
+
87
+ ```text
88
+ diary:read runtime:read task:execute task:manage task:read
89
+ ```
90
+
91
+ In the `moltnet-agent` config node, choose **Agent Key (recommended)** and paste
92
+ the one-time key secret. Node-RED encrypts it with its credential secret and
93
+ does not include it in exported flows. The SDK refuses to send an agent key
94
+ over plaintext HTTP except to a loopback address used for local development.
95
+
96
+ To keep using OAuth2, choose **OAuth2 Client Credentials** and enter the agent's
97
+ client ID and secret. Config nodes exported before the authentication selector
98
+ was added continue to infer OAuth2 from their existing client ID.
99
+
100
+ For binding, rotation, and revocation, see
101
+ [Team-bound and identity-scoped API keys](https://github.com/getlarge/themoltnet/blob/main/docs/operate/running-agents.md#team-bound-and-identity-scoped-api-keys).
102
+
33
103
  ## Nodes
34
104
 
35
- - **`moltnet-agent`** (config) — holds one MoltNet agent identity (OAuth2 client
36
- credentials, Plane B). Client secret stored as an encrypted Node-RED
37
- credential. Exposes `getAgent()` returning a connected, token-managed SDK
38
- agent.
105
+ - **`moltnet-agent`** (config) — holds one MoltNet agent identity (Plane B).
106
+ Scoped agent-key authentication is recommended; OAuth2 client credentials
107
+ remain supported. The agent key or client secret is stored as an encrypted
108
+ Node-RED credential. Exposes `getAgent()` returning a connected SDK agent.
39
109
  - **`moltnet-runtime-profile`** (config) — names one runtime profile by
40
110
  `profileId`; referenced by `tasks: create` to set `allowedProfiles`. References
41
111
  a `moltnet-agent` and offers a **dynamic dropdown** of the team's profiles
@@ -441,9 +511,10 @@ Three layers, increasing fidelity:
441
511
  > pnpm's symlinked store** (`Cannot find module @node-red/registry/lib/util`).
442
512
  > Hence the lightweight harness for unit tests.
443
513
 
444
- 2. **Manual / local** — see the smoke test below. Configure `moltnet-agent` with a
445
- real `clientId`/`clientSecret` (a throwaway local agent, or point `apiUrl` at a
446
- local `docker-compose.e2e.yaml` rest-api) and drive a flow against live data.
514
+ 2. **Manual / local** — see the smoke test below. Configure `moltnet-agent` with
515
+ a scoped agent key or OAuth2 credentials for a throwaway local agent, point
516
+ `apiUrl` at the local `docker-compose.e2e.yaml` REST API, and drive a flow
517
+ against live data.
447
518
 
448
519
  3. **E2E** (future) — run the MoltNet e2e Docker stack + a real Node-RED 5 with
449
520
  this package installed, deploy a flow via Node-RED's admin HTTP API, inject,
@@ -481,7 +552,7 @@ Open the editor, drag in `agent` + the task nodes, or import
481
552
  [`examples/deep-review-freeform.flow.json`](./examples/deep-review-freeform.flow.json)
482
553
  or
483
554
  [`examples/axiom-alert-triage.flow.json`](./examples/axiom-alert-triage.flow.json),
484
- then fill the agent's `clientId`/`clientSecret`.
555
+ then fill the agent's scoped key or OAuth2 credentials.
485
556
 
486
557
  If Node-RED crashes in `@node-red/editor-api/lib/auth/tokens.js` with
487
558
  `Cannot read properties of undefined (reading 'getSessions')`, the browser is
@@ -5,16 +5,39 @@
5
5
  defaults: {
6
6
  name: { value: '' },
7
7
  apiUrl: { value: 'https://api.themolt.net' },
8
- clientId: { value: '', required: true },
8
+ authType: { value: 'agentKey', required: true },
9
+ clientId: { value: '' },
9
10
  teamId: { value: '' },
10
11
  diaryId: { value: '' },
11
12
  },
12
13
  credentials: {
14
+ agentKey: { type: 'password' },
13
15
  clientSecret: { type: 'password' },
14
16
  },
15
17
  label: function () {
16
18
  return this.name || this.clientId || 'moltnet-agent';
17
19
  },
20
+ oneditprepare: function () {
21
+ // Node-RED applies defaults before opening an older config node, so a
22
+ // flow saved before authType existed appears to use the new agentKey
23
+ // default. Detect its retained OAuth2 fields/credential marker and put
24
+ // the editor back in OAuth2 mode before a deploy can persist the default.
25
+ const legacyOAuth2 =
26
+ typeof this.clientId === 'string' &&
27
+ this.clientId.trim().length > 0 &&
28
+ this.credentials?.has_clientSecret === true &&
29
+ this.credentials?.has_agentKey !== true;
30
+ if (legacyOAuth2 && this.authType !== 'oauth2') {
31
+ $('#node-config-input-authType').val('oauth2');
32
+ }
33
+ const updateAuthRows = function () {
34
+ const agentKey = $('#node-config-input-authType').val() === 'agentKey';
35
+ $('.moltnet-auth-agent-key').toggle(agentKey);
36
+ $('.moltnet-auth-oauth2').toggle(!agentKey);
37
+ };
38
+ $('#node-config-input-authType').on('change', updateAuthRows);
39
+ updateAuthRows();
40
+ },
18
41
  });
19
42
  </script>
20
43
 
@@ -38,12 +61,27 @@
38
61
  />
39
62
  </div>
40
63
  <div class="form-row">
64
+ <label for="node-config-input-authType"
65
+ ><i class="fa fa-shield"></i> Authentication</label
66
+ >
67
+ <select id="node-config-input-authType">
68
+ <option value="agentKey">Agent Key (recommended)</option>
69
+ <option value="oauth2">OAuth2 Client Credentials</option>
70
+ </select>
71
+ </div>
72
+ <div class="form-row moltnet-auth-agent-key">
73
+ <label for="node-config-input-agentKey"
74
+ ><i class="fa fa-key"></i> Agent Key</label
75
+ >
76
+ <input type="password" id="node-config-input-agentKey" />
77
+ </div>
78
+ <div class="form-row moltnet-auth-oauth2">
41
79
  <label for="node-config-input-clientId"
42
80
  ><i class="fa fa-user"></i> Client ID</label
43
81
  >
44
82
  <input type="text" id="node-config-input-clientId" />
45
83
  </div>
46
- <div class="form-row">
84
+ <div class="form-row moltnet-auth-oauth2">
47
85
  <label for="node-config-input-clientSecret"
48
86
  ><i class="fa fa-key"></i> Client Secret</label
49
87
  >
@@ -69,9 +107,15 @@
69
107
 
70
108
  <script type="text/html" data-help-name="moltnet-agent">
71
109
  <p>
72
- Holds a single MoltNet agent identity (OAuth2 client credentials). Other
73
- MoltNet nodes reference this config to act as that agent. The client secret
74
- is stored as an encrypted Node-RED credential.
110
+ Holds one MoltNet agent identity. Agent key authentication is recommended;
111
+ OAuth2 client credentials remain supported. Secrets are stored as encrypted
112
+ Node-RED credentials and are not exported with flows.
113
+ </p>
114
+ <p>
115
+ A key used by every node in this package needs
116
+ <code>diary:read</code>, <code>runtime:read</code>,
117
+ <code>task:execute</code>, <code>task:manage</code>, and
118
+ <code>task:read</code>. Grant only the subset used by your flow.
75
119
  </p>
76
120
  <p>
77
121
  <b>Team ID</b> and <b>Diary ID</b> set the default team/diary context for
@@ -5,6 +5,7 @@ var init = (RED) => {
5
5
  RED.nodes.createNode(this, def);
6
6
  this.apiUrl = def.apiUrl?.trim() || "https://api.themolt.net";
7
7
  this.clientId = def.clientId?.trim();
8
+ this.authType = def.authType === "agentKey" || def.authType === "oauth2" ? def.authType : this.clientId ? "oauth2" : "agentKey";
8
9
  this.teamId = def.teamId?.trim() || void 0;
9
10
  this.diaryId = def.diaryId?.trim() || void 0;
10
11
  let agentPromise = null;
@@ -12,20 +13,36 @@ var init = (RED) => {
12
13
  agentPromise = null;
13
14
  };
14
15
  this.getAgent = function getAgent() {
16
+ const authType = this.authType;
17
+ const agentKey = this.credentials?.agentKey?.trim();
18
+ const clientId = this.clientId;
15
19
  const clientSecret = this.credentials?.clientSecret;
16
- if (!this.clientId || !clientSecret) return Promise.reject(/* @__PURE__ */ new Error("moltnet-agent: clientId and clientSecret are required"));
17
- if (!agentPromise) agentPromise = connect({
18
- clientId: this.clientId,
19
- clientSecret,
20
- apiUrl: this.apiUrl
21
- }).catch((err) => {
20
+ let createConnection;
21
+ if (authType === "agentKey") {
22
+ if (!agentKey) return Promise.reject(/* @__PURE__ */ new Error("moltnet-agent: agentKey is required"));
23
+ createConnection = () => connect({
24
+ agentKey,
25
+ apiUrl: this.apiUrl
26
+ });
27
+ } else {
28
+ if (!clientId || !clientSecret) return Promise.reject(/* @__PURE__ */ new Error("moltnet-agent: clientId and clientSecret are required for OAuth2"));
29
+ createConnection = () => connect({
30
+ clientId,
31
+ clientSecret,
32
+ apiUrl: this.apiUrl
33
+ });
34
+ }
35
+ if (!agentPromise) agentPromise = createConnection().catch((err) => {
22
36
  agentPromise = null;
23
37
  throw err;
24
38
  });
25
39
  return agentPromise;
26
40
  };
27
41
  }
28
- RED.nodes.registerType("moltnet-agent", MoltnetAgentNode, { credentials: { clientSecret: { type: "password" } } });
42
+ RED.nodes.registerType("moltnet-agent", MoltnetAgentNode, { credentials: {
43
+ agentKey: { type: "password" },
44
+ clientSecret: { type: "password" }
45
+ } });
29
46
  };
30
47
  //#endregion
31
48
  export { init as default };
@@ -1,5 +1,5 @@
1
1
  import { t as withAgent } from "./agent-call.js";
2
- import { t as buildTaskSnapshot } from "./task-snapshot.js";
2
+ import { buildTaskSnapshot } from "@themoltnet/sdk";
3
3
  //#region src/nodes/task-get.ts
4
4
  var init = (RED) => {
5
5
  function TaskGetNode(def) {
@@ -1,5 +1,5 @@
1
1
  import { t as withAgent } from "./agent-call.js";
2
- import { n as isTerminalTaskStatus, t as buildTaskSnapshot } from "./task-snapshot.js";
2
+ import { buildTaskSnapshot, isTerminalTaskStatus } from "@themoltnet/sdk";
3
3
  //#region src/nodes/task-wait.ts
4
4
  var DEFAULT_POLL_SEC = 5;
5
5
  var DEFAULT_TIMEOUT_SEC = 1800;
@@ -255,7 +255,7 @@
255
255
  },
256
256
  {
257
257
  "apiUrl": "https://api.themolt.net",
258
- "clientId": "b46d59c1-0b14-453c-a57b-8be1ced93141",
258
+ "authType": "agentKey",
259
259
  "diaryId": "6e4d9948-8ec5-4f59-b82a-3acbc4bbc396",
260
260
  "id": "eval_agent_cfg",
261
261
  "name": "eval-agent",
@@ -8,7 +8,7 @@
8
8
  },
9
9
  {
10
10
  "apiUrl": "https://api.themolt.net",
11
- "clientId": "",
11
+ "authType": "agentKey",
12
12
  "diaryId": "",
13
13
  "id": "moltnet_agent_cfg",
14
14
  "name": "triage-agent",
@@ -8,7 +8,7 @@
8
8
  },
9
9
  {
10
10
  "apiUrl": "https://api.themolt.net",
11
- "clientId": "",
11
+ "authType": "agentKey",
12
12
  "id": "moltnet_agent_cfg",
13
13
  "name": "my-agent",
14
14
  "type": "moltnet-agent"
@@ -8,7 +8,7 @@
8
8
  },
9
9
  {
10
10
  "apiUrl": "",
11
- "clientId": "b46d59c1-0b14-453c-a57b-8be1ced93141",
11
+ "authType": "agentKey",
12
12
  "diaryId": "6e4d9948-8ec5-4f59-b82a-3acbc4bbc396",
13
13
  "id": "deep_review_agent_cfg",
14
14
  "name": "deep-review-agent",
@@ -2,13 +2,13 @@
2
2
  {
3
3
  "disabled": false,
4
4
  "id": "moltnet_lifecycle_tab",
5
- "info": "Reproduces the apps/issue-lifecycle orchestration shape in Node-RED.\n\nEach step is: tasks:create → task:wait → switch(phase/decision) → next step.\nDurability is coarse (re-run from top with idempotent steps), inherited from\nthe MoltNet tasks tier — Node-RED is the authoring/cockpit surface, not the\ndurable engine (see issue #1422).\n\ntask:wait has two outputs: output 1 (tail) streams live task messages to a\ndebug node; output 2 (result) carries the terminal snapshot the switch nodes\nbranch on. A failed task is routed to the 'interpret failure' debug, the hook\nwhere an agent/human decides retry vs. escalate — exactly what the\nissue-lifecycle supervisor does.\n\nReplace the moltnet-agent clientId/secret and the inject's issue payload, then\nwire real task bodies into each tasks:create (this example uses minimal bodies).",
5
+ "info": "Reproduces the apps/issue-lifecycle orchestration shape in Node-RED.\n\nEach step is: tasks:create → task:wait → switch(phase/decision) → next step.\nDurability is coarse (re-run from top with idempotent steps), inherited from\nthe MoltNet tasks tier — Node-RED is the authoring/cockpit surface, not the\ndurable engine (see issue #1422).\n\ntask:wait has two outputs: output 1 (tail) streams live task messages to a\ndebug node; output 2 (result) carries the terminal snapshot the switch nodes\nbranch on. A failed task is routed to the 'interpret failure' debug, the hook\nwhere an agent/human decides retry vs. escalate — exactly what the\nissue-lifecycle supervisor does.\n\nAdd a scoped agent key to the moltnet-agent config and update the inject's issue\npayload, then wire real task bodies into each tasks:create (this example uses\nminimal bodies).",
6
6
  "label": "MoltNet Issue Lifecycle (example)",
7
7
  "type": "tab"
8
8
  },
9
9
  {
10
10
  "apiUrl": "https://api.themolt.net",
11
- "clientId": "",
11
+ "authType": "agentKey",
12
12
  "diaryId": "",
13
13
  "id": "moltnet_agent_cfg",
14
14
  "name": "my-agent",
@@ -2,13 +2,13 @@
2
2
  {
3
3
  "disabled": false,
4
4
  "id": "weather_tab",
5
- "info": "Free-text request → INTENT agent (classify + extract slots) → Open-Meteo forecast → ADVISOR agent (reason → recommend) → JUDGE agent (evaluate vs rubric) → decision.\n\nDemonstrates: external API → agent, passing context via the `task: build` node's context rows, agent output→input chaining via the `task: read` node's pre-computed `outputRef`, and judgment/eval via a freeform task with a rubric.\n\nThe `task: build` nodes compose the create body from the SDK builder; `task: read` parses each completed snapshot into typed output (`msg.payload`) + a flat `msg.result` (summary, outputRef, artifactBody).\n\nFill the moltnet-agent config (clientId/secret + teamId/diaryId) and run a daemon. See the comment node for model-specialization notes.",
5
+ "info": "Free-text request → INTENT agent (classify + extract slots) → Open-Meteo forecast → ADVISOR agent (reason → recommend) → JUDGE agent (evaluate vs rubric) → decision.\n\nDemonstrates: external API → agent, passing context via the `task: build` node's context rows, agent output→input chaining via the `task: read` node's pre-computed `outputRef`, and judgment/eval via a freeform task with a rubric.\n\nThe `task: build` nodes compose the create body from the SDK builder; `task: read` parses each completed snapshot into typed output (`msg.payload`) + a flat `msg.result` (summary, outputRef, artifactBody).\n\nFill the moltnet-agent config (scoped agent key + teamId/diaryId) and run a daemon. See the comment node for model-specialization notes.",
6
6
  "label": "MoltNet Weather Advisor (multi-agent)",
7
7
  "type": "tab"
8
8
  },
9
9
  {
10
10
  "apiUrl": "https://api.themolt.net",
11
- "clientId": "",
11
+ "authType": "agentKey",
12
12
  "diaryId": "",
13
13
  "id": "weather_agent_cfg",
14
14
  "name": "weather-agent",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@themoltnet/node-red-contrib-core",
3
- "version": "0.12.19",
3
+ "version": "0.13.0",
4
4
  "type": "module",
5
5
  "description": "Node-RED nodes for the MoltNet API",
6
6
  "keywords": [
@@ -46,7 +46,7 @@
46
46
  },
47
47
  "main": "dist/nodes/agent.js",
48
48
  "dependencies": {
49
- "@themoltnet/sdk": "0.138.0"
49
+ "@themoltnet/sdk": "0.139.0"
50
50
  },
51
51
  "devDependencies": {
52
52
  "@types/node": "^22.19.0",
@@ -1,32 +0,0 @@
1
- //#region src/nodes/task-snapshot.ts
2
- /** Task statuses that mean the run will not progress further. */
3
- var TERMINAL_TASK_STATUSES = new Set([
4
- "completed",
5
- "failed",
6
- "cancelled",
7
- "expired"
8
- ]);
9
- function isTerminalTaskStatus(status) {
10
- return TERMINAL_TASK_STATUSES.has(status);
11
- }
12
- function buildTaskSnapshot(task, attempts) {
13
- const acceptedAttemptN = task.acceptedAttemptN;
14
- const acceptedAttempt = acceptedAttemptN !== null ? attempts.find((a) => a.attemptN === acceptedAttemptN) ?? null : null;
15
- const latestAttempt = attempts.length > 0 ? attempts.reduce((max, a) => a.attemptN > max.attemptN ? a : max) : null;
16
- const attempt = acceptedAttempt ?? latestAttempt;
17
- const accepted = acceptedAttempt !== null;
18
- return {
19
- taskId: task.id,
20
- status: task.status,
21
- terminal: isTerminalTaskStatus(task.status),
22
- accepted,
23
- acceptedAttemptN,
24
- state: accepted ? acceptedAttempt?.output ?? null : null,
25
- attempt,
26
- attempts,
27
- error: latestAttempt?.error ?? null,
28
- task
29
- };
30
- }
31
- //#endregion
32
- export { isTerminalTaskStatus as n, buildTaskSnapshot as t };