n8n-nodes-safeagent 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +229 -0
- package/dist/credentials/PostgresApiCredentials.credentials.d.ts +7 -0
- package/dist/credentials/PostgresApiCredentials.credentials.js +58 -0
- package/dist/credentials/PostgresApiCredentials.credentials.js.map +1 -0
- package/dist/nodes/SafeAgent/SafeAgent.node.d.ts +5 -0
- package/dist/nodes/SafeAgent/SafeAgent.node.js +215 -0
- package/dist/nodes/SafeAgent/SafeAgent.node.js.map +1 -0
- package/package.json +50 -0
package/README.md
ADDED
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
# n8n-nodes-safeagent
|
|
2
|
+
|
|
3
|
+
## Requirements
|
|
4
|
+
|
|
5
|
+
- Python 3.10+
|
|
6
|
+
- `pip install safeagent-exec-guard`
|
|
7
|
+
- `python3` must be available on PATH in the environment where n8n is running
|
|
8
|
+
|
|
9
|
+
> **Note:** If you're running n8n via Docker, you'll need a custom image with Python installed, or use the Postgres backend via an external SafeAgent API endpoint.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
An [n8n](https://n8n.io) community node that wraps the
|
|
14
|
+
[`safeagent-exec-guard`](https://pypi.org/project/safeagent-exec-guard/) Python library to bring
|
|
15
|
+
the **claim-before-execute** idempotency pattern to your n8n workflows.
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## The Claim-Before-Execute Pattern
|
|
20
|
+
|
|
21
|
+
AI agents and event-driven workflows face a hard problem: the same logical request can arrive more
|
|
22
|
+
than once (webhook retries, queue redeliveries, user double-clicks). Without a guard, every
|
|
23
|
+
duplicate triggers the side effect again — double charges, duplicate emails, duplicate DB rows.
|
|
24
|
+
|
|
25
|
+
**Claim-before-execute** solves this with a single atomic database write:
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
1. Before doing anything irreversible, claim the (requestId, actionName) pair.
|
|
29
|
+
2. If the claim succeeds → you are the first runner. Proceed with the action.
|
|
30
|
+
3. If the claim is denied → someone else already ran it. Return the cached receipt and stop.
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
The claim is implemented as a SQL `INSERT … ON CONFLICT DO NOTHING` (or equivalent), making it
|
|
34
|
+
naturally atomic and race-condition-safe even under concurrent workers.
|
|
35
|
+
|
|
36
|
+
```
|
|
37
|
+
Incoming event
|
|
38
|
+
│
|
|
39
|
+
▼
|
|
40
|
+
┌────────────────────┐
|
|
41
|
+
│ SafeAgent Guard │ ← this n8n node
|
|
42
|
+
│ insert_if_not_ │
|
|
43
|
+
│ exists(rid, act) │
|
|
44
|
+
└────────┬───────────┘
|
|
45
|
+
│
|
|
46
|
+
┌─────┴──────┐
|
|
47
|
+
│ │
|
|
48
|
+
inserted=true inserted=false
|
|
49
|
+
│ │
|
|
50
|
+
▼ ▼
|
|
51
|
+
Proceed Return cached
|
|
52
|
+
with receipt → skip
|
|
53
|
+
action downstream nodes
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## Installation
|
|
59
|
+
|
|
60
|
+
### Prerequisites
|
|
61
|
+
|
|
62
|
+
- n8n ≥ 0.190.0
|
|
63
|
+
- Python 3.9+ in `PATH`
|
|
64
|
+
- The `safeagent-exec-guard` Python package:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
pip install safeagent-exec-guard
|
|
68
|
+
# or, for Postgres support:
|
|
69
|
+
pip install "safeagent-exec-guard[postgres]"
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### Add to n8n
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
# In your n8n data directory
|
|
76
|
+
npm install n8n-nodes-safeagent
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Or use the n8n **Settings → Community Nodes → Install** UI and enter `n8n-nodes-safeagent`.
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
## Node Fields
|
|
84
|
+
|
|
85
|
+
| Field | Type | Description |
|
|
86
|
+
|---|---|---|
|
|
87
|
+
| **Request ID** | String | Unique identifier for the logical request (e.g. webhook event ID, message UUID). Supports n8n expressions like `{{ $json["id"] }}`. |
|
|
88
|
+
| **Action Name** | String | Name of the side-effectful action being guarded (e.g. `send_invoice`, `charge_card`). Together with Request ID it forms the idempotency key. |
|
|
89
|
+
| **Backend** | Dropdown | `SQLite (local file)` — zero-config, good for dev / single-instance. `PostgreSQL` — distributed-safe, recommended for production / multi-worker. |
|
|
90
|
+
| **SQLite Database Path** | String *(SQLite only)* | Path to the `.db` file. Defaults to `safeagent.db` in the n8n working directory. |
|
|
91
|
+
| **Postgres Credentials** | Credential *(Postgres only)* | Host, port, database, user, password, SSL toggle. |
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
## Output Fields
|
|
96
|
+
|
|
97
|
+
The node adds the following fields to the item's JSON:
|
|
98
|
+
|
|
99
|
+
| Field | Type | Meaning |
|
|
100
|
+
|---|---|---|
|
|
101
|
+
| `requestId` | string | Echo of the input Request ID |
|
|
102
|
+
| `actionName` | string | Echo of the input Action Name |
|
|
103
|
+
| `backend` | string | `"sqlite"` or `"postgres"` |
|
|
104
|
+
| `isDuplicate` | boolean | `true` if this (requestId, actionName) was already claimed |
|
|
105
|
+
| `shouldProceed` | boolean | `true` if this is a new claim and the action should run |
|
|
106
|
+
| `inserted` | boolean | Raw value from the guard library |
|
|
107
|
+
| `receipt` | object | The stored execution record (timestamps, metadata, etc.) |
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
## Usage in a Workflow
|
|
112
|
+
|
|
113
|
+
### Pattern A — IF branch
|
|
114
|
+
|
|
115
|
+
```
|
|
116
|
+
Webhook → SafeAgent Guard → IF (shouldProceed == true)
|
|
117
|
+
├─ true → Charge Card → Mark Complete
|
|
118
|
+
└─ false → Return Cached Receipt
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### Pattern B — Stop-and-error on duplicate
|
|
122
|
+
|
|
123
|
+
Add an **IF** node after the guard:
|
|
124
|
+
|
|
125
|
+
- **Condition**: `{{ $json.isDuplicate }}` equals `true`
|
|
126
|
+
- **True branch**: Stop And Error (or Respond to Webhook with the cached receipt)
|
|
127
|
+
- **False branch**: continue with the real work
|
|
128
|
+
|
|
129
|
+
### Minimal inline example
|
|
130
|
+
|
|
131
|
+
```json
|
|
132
|
+
{
|
|
133
|
+
"nodes": [
|
|
134
|
+
{
|
|
135
|
+
"type": "n8n-nodes-safeagent.safeAgent",
|
|
136
|
+
"parameters": {
|
|
137
|
+
"requestId": "={{ $json[\"webhookEventId\"] }}",
|
|
138
|
+
"actionName": "send_welcome_email",
|
|
139
|
+
"backend": "sqlite",
|
|
140
|
+
"sqlitePath": "/data/safeagent.db"
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
]
|
|
144
|
+
}
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
---
|
|
148
|
+
|
|
149
|
+
## Postgres Setup
|
|
150
|
+
|
|
151
|
+
1. Create a dedicated database and user:
|
|
152
|
+
|
|
153
|
+
```sql
|
|
154
|
+
CREATE DATABASE safeagent;
|
|
155
|
+
CREATE USER safeagent_user WITH PASSWORD 'secret';
|
|
156
|
+
GRANT ALL PRIVILEGES ON DATABASE safeagent TO safeagent_user;
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
2. The guard library creates the `execution_claims` table automatically on first run (`init_db()`).
|
|
160
|
+
|
|
161
|
+
3. In n8n, add a **SafeAgent Postgres Credentials** credential and select it in the node.
|
|
162
|
+
|
|
163
|
+
For high-throughput use-cases add an index:
|
|
164
|
+
|
|
165
|
+
```sql
|
|
166
|
+
CREATE UNIQUE INDEX IF NOT EXISTS ux_execution_claims
|
|
167
|
+
ON execution_claims (request_id, action_name);
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
---
|
|
171
|
+
|
|
172
|
+
## How the Subprocess Works
|
|
173
|
+
|
|
174
|
+
The node calls the guard library via `python3 -c "..."` so that:
|
|
175
|
+
|
|
176
|
+
- No additional n8n-side dependencies are needed beyond Node.js.
|
|
177
|
+
- The Python environment (virtualenv, system packages, etc.) is fully under your control.
|
|
178
|
+
- The library can be upgraded independently of the n8n node.
|
|
179
|
+
|
|
180
|
+
The script follows this pattern:
|
|
181
|
+
|
|
182
|
+
```python
|
|
183
|
+
import json
|
|
184
|
+
from safeagent_exec_guard.sqlite_store import SQLiteExecutionStore
|
|
185
|
+
|
|
186
|
+
store = SQLiteExecutionStore('safeagent.db')
|
|
187
|
+
store.init_db()
|
|
188
|
+
result = store.insert_if_not_exists('<requestId>', '<actionName>')
|
|
189
|
+
print(json.dumps(result))
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
The JSON printed to stdout is parsed and merged into the n8n item.
|
|
193
|
+
|
|
194
|
+
---
|
|
195
|
+
|
|
196
|
+
## Security Considerations
|
|
197
|
+
|
|
198
|
+
- Request ID and Action Name values are single-quote–escaped before being embedded in the Python
|
|
199
|
+
string literal to prevent injection.
|
|
200
|
+
- Postgres credentials are never written to disk; they are passed via an in-memory DSN string
|
|
201
|
+
constructed at runtime.
|
|
202
|
+
- The subprocess timeout is 15 seconds. Long-running `init_db()` migrations (first run on a large
|
|
203
|
+
Postgres cluster) may need the timeout raised in the source.
|
|
204
|
+
|
|
205
|
+
---
|
|
206
|
+
|
|
207
|
+
## Development
|
|
208
|
+
|
|
209
|
+
```bash
|
|
210
|
+
git clone https://github.com/your-org/n8n-nodes-safeagent
|
|
211
|
+
cd n8n-nodes-safeagent
|
|
212
|
+
npm install
|
|
213
|
+
npm run build # compiles TypeScript → dist/
|
|
214
|
+
npm run dev # watch mode
|
|
215
|
+
npm run lint
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
To test locally against a running n8n instance:
|
|
219
|
+
|
|
220
|
+
```bash
|
|
221
|
+
export N8N_CUSTOM_EXTENSIONS="/path/to/n8n-nodes-safeagent"
|
|
222
|
+
n8n start
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
---
|
|
226
|
+
|
|
227
|
+
## License
|
|
228
|
+
|
|
229
|
+
MIT © Your Name
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.PostgresApiCredentials = void 0;
|
|
4
|
+
class PostgresApiCredentials {
|
|
5
|
+
constructor() {
|
|
6
|
+
this.name = 'postgresApiCredentials';
|
|
7
|
+
this.displayName = 'SafeAgent Postgres Credentials';
|
|
8
|
+
this.documentationUrl = 'https://github.com/your-org/n8n-nodes-safeagent#postgres-setup';
|
|
9
|
+
this.properties = [
|
|
10
|
+
{
|
|
11
|
+
displayName: 'Host',
|
|
12
|
+
name: 'host',
|
|
13
|
+
type: 'string',
|
|
14
|
+
default: 'localhost',
|
|
15
|
+
placeholder: 'localhost',
|
|
16
|
+
description: 'PostgreSQL server hostname',
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
displayName: 'Port',
|
|
20
|
+
name: 'port',
|
|
21
|
+
type: 'number',
|
|
22
|
+
default: 5432,
|
|
23
|
+
description: 'PostgreSQL server port',
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
displayName: 'Database',
|
|
27
|
+
name: 'database',
|
|
28
|
+
type: 'string',
|
|
29
|
+
default: 'safeagent',
|
|
30
|
+
description: 'Name of the database that holds the execution-guard table',
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
displayName: 'User',
|
|
34
|
+
name: 'user',
|
|
35
|
+
type: 'string',
|
|
36
|
+
default: '',
|
|
37
|
+
description: 'Database user',
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
displayName: 'Password',
|
|
41
|
+
name: 'password',
|
|
42
|
+
type: 'string',
|
|
43
|
+
typeOptions: { password: true },
|
|
44
|
+
default: '',
|
|
45
|
+
description: 'Database password',
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
displayName: 'SSL',
|
|
49
|
+
name: 'ssl',
|
|
50
|
+
type: 'boolean',
|
|
51
|
+
default: false,
|
|
52
|
+
description: 'Whether to connect using SSL/TLS',
|
|
53
|
+
},
|
|
54
|
+
];
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
exports.PostgresApiCredentials = PostgresApiCredentials;
|
|
58
|
+
//# sourceMappingURL=PostgresApiCredentials.credentials.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"PostgresApiCredentials.credentials.js","sourceRoot":"","sources":["../../credentials/PostgresApiCredentials.credentials.ts"],"names":[],"mappings":";;;AAKA,MAAa,sBAAsB;IAAnC;QACE,SAAI,GAAG,wBAAwB,CAAC;QAChC,gBAAW,GAAG,gCAAgC,CAAC;QAC/C,qBAAgB,GAAG,gEAAgE,CAAC;QAEpF,eAAU,GAAsB;YAC9B;gBACE,WAAW,EAAE,MAAM;gBACnB,IAAI,EAAE,MAAM;gBACZ,IAAI,EAAE,QAAQ;gBACd,OAAO,EAAE,WAAW;gBACpB,WAAW,EAAE,WAAW;gBACxB,WAAW,EAAE,4BAA4B;aAC1C;YACD;gBACE,WAAW,EAAE,MAAM;gBACnB,IAAI,EAAE,MAAM;gBACZ,IAAI,EAAE,QAAQ;gBACd,OAAO,EAAE,IAAI;gBACb,WAAW,EAAE,wBAAwB;aACtC;YACD;gBACE,WAAW,EAAE,UAAU;gBACvB,IAAI,EAAE,UAAU;gBAChB,IAAI,EAAE,QAAQ;gBACd,OAAO,EAAE,WAAW;gBACpB,WAAW,EAAE,2DAA2D;aACzE;YACD;gBACE,WAAW,EAAE,MAAM;gBACnB,IAAI,EAAE,MAAM;gBACZ,IAAI,EAAE,QAAQ;gBACd,OAAO,EAAE,EAAE;gBACX,WAAW,EAAE,eAAe;aAC7B;YACD;gBACE,WAAW,EAAE,UAAU;gBACvB,IAAI,EAAE,UAAU;gBAChB,IAAI,EAAE,QAAQ;gBACd,WAAW,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE;gBAC/B,OAAO,EAAE,EAAE;gBACX,WAAW,EAAE,mBAAmB;aACjC;YACD;gBACE,WAAW,EAAE,KAAK;gBAClB,IAAI,EAAE,KAAK;gBACX,IAAI,EAAE,SAAS;gBACf,OAAO,EAAE,KAAK;gBACd,WAAW,EAAE,kCAAkC;aAChD;SACF,CAAC;IACJ,CAAC;CAAA;AAnDD,wDAmDC"}
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SafeAgent = void 0;
|
|
4
|
+
const n8n_workflow_1 = require("n8n-workflow");
|
|
5
|
+
const child_process_1 = require("child_process");
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
// Helpers
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
/** Escape single-quotes so the value is safe to embed in a Python string literal. */
|
|
10
|
+
function pyStr(value) {
|
|
11
|
+
return value.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Build the inline Python snippet for the SQLite backend.
|
|
15
|
+
*
|
|
16
|
+
* Uses safeagent_exec_guard.sqlite_store.SQLiteExecutionStore which exposes:
|
|
17
|
+
* insert_if_not_exists(request_id, action_name) -> dict
|
|
18
|
+
*
|
|
19
|
+
* The dict has at minimum:
|
|
20
|
+
* { "inserted": bool, "receipt": { ... } }
|
|
21
|
+
*/
|
|
22
|
+
function buildSQLiteScript(requestId, actionName, dbPath) {
|
|
23
|
+
const rid = pyStr(requestId);
|
|
24
|
+
const act = pyStr(actionName);
|
|
25
|
+
const db = pyStr(dbPath);
|
|
26
|
+
return ('import json; ' +
|
|
27
|
+
'from safeagent_exec_guard.sqlite_store import SQLiteExecutionStore; ' +
|
|
28
|
+
`store = SQLiteExecutionStore('${db}'); ` +
|
|
29
|
+
'store.init_db(); ' +
|
|
30
|
+
`result = store.insert_if_not_exists('${rid}', '${act}'); ` +
|
|
31
|
+
'print(json.dumps(result))');
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Build the inline Python snippet for the Postgres backend.
|
|
35
|
+
*
|
|
36
|
+
* Uses safeagent_exec_guard.pg_store.PostgresExecutionStore which accepts a
|
|
37
|
+
* libpq DSN string.
|
|
38
|
+
*/
|
|
39
|
+
function buildPostgresScript(requestId, actionName, dsn) {
|
|
40
|
+
const rid = pyStr(requestId);
|
|
41
|
+
const act = pyStr(actionName);
|
|
42
|
+
const d = pyStr(dsn);
|
|
43
|
+
return ('import json; ' +
|
|
44
|
+
'from safeagent_exec_guard.pg_store import PostgresExecutionStore; ' +
|
|
45
|
+
`store = PostgresExecutionStore('${d}'); ` +
|
|
46
|
+
'store.init_db(); ' +
|
|
47
|
+
`result = store.insert_if_not_exists('${rid}', '${act}'); ` +
|
|
48
|
+
'print(json.dumps(result))');
|
|
49
|
+
}
|
|
50
|
+
// ---------------------------------------------------------------------------
|
|
51
|
+
// Node definition
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
class SafeAgent {
|
|
54
|
+
constructor() {
|
|
55
|
+
this.description = {
|
|
56
|
+
displayName: 'SafeAgent Execution Guard',
|
|
57
|
+
name: 'safeAgent',
|
|
58
|
+
// Built-in placeholder icon; replace with a custom SVG in nodes/SafeAgent/safeagent.svg
|
|
59
|
+
icon: 'fa:shield-alt',
|
|
60
|
+
group: ['transform'],
|
|
61
|
+
version: 1,
|
|
62
|
+
subtitle: '={{$parameter["actionName"]}}',
|
|
63
|
+
description: 'Claim-before-execute idempotency guard powered by the safeagent-exec-guard Python library. ' +
|
|
64
|
+
'Returns a cached receipt when the (requestId, actionName) pair was already processed, ' +
|
|
65
|
+
'or a "proceed" signal when it is new.',
|
|
66
|
+
defaults: {
|
|
67
|
+
name: 'SafeAgent Guard',
|
|
68
|
+
},
|
|
69
|
+
inputs: ['main'],
|
|
70
|
+
outputs: ['main'],
|
|
71
|
+
credentials: [
|
|
72
|
+
{
|
|
73
|
+
name: 'postgresApiCredentials',
|
|
74
|
+
required: false,
|
|
75
|
+
displayOptions: {
|
|
76
|
+
show: {
|
|
77
|
+
backend: ['postgres'],
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
],
|
|
82
|
+
properties: [
|
|
83
|
+
// ── Request ID ────────────────────────────────────────────────────────
|
|
84
|
+
{
|
|
85
|
+
displayName: 'Request ID',
|
|
86
|
+
name: 'requestId',
|
|
87
|
+
type: 'string',
|
|
88
|
+
default: '',
|
|
89
|
+
required: true,
|
|
90
|
+
placeholder: '={{ $json["requestId"] }}',
|
|
91
|
+
description: 'Unique identifier for this logical request (e.g. webhook event ID, message UUID). ' +
|
|
92
|
+
'Used together with Action Name to form the idempotency key.',
|
|
93
|
+
},
|
|
94
|
+
// ── Action Name ───────────────────────────────────────────────────────
|
|
95
|
+
{
|
|
96
|
+
displayName: 'Action Name',
|
|
97
|
+
name: 'actionName',
|
|
98
|
+
type: 'string',
|
|
99
|
+
default: '',
|
|
100
|
+
required: true,
|
|
101
|
+
placeholder: 'send_invoice',
|
|
102
|
+
description: 'Name of the side-effectful action being guarded (e.g. "send_invoice", "charge_card"). ' +
|
|
103
|
+
'Combined with Request ID to create a unique execution claim.',
|
|
104
|
+
},
|
|
105
|
+
// ── Backend ───────────────────────────────────────────────────────────
|
|
106
|
+
{
|
|
107
|
+
displayName: 'Backend',
|
|
108
|
+
name: 'backend',
|
|
109
|
+
type: 'options',
|
|
110
|
+
options: [
|
|
111
|
+
{
|
|
112
|
+
name: 'SQLite (local file)',
|
|
113
|
+
value: 'sqlite',
|
|
114
|
+
description: 'Lightweight; good for single-instance or development use',
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
name: 'PostgreSQL',
|
|
118
|
+
value: 'postgres',
|
|
119
|
+
description: 'Distributed-safe; recommended for multi-worker / production deployments',
|
|
120
|
+
},
|
|
121
|
+
],
|
|
122
|
+
default: 'sqlite',
|
|
123
|
+
description: 'Storage backend used by the execution-guard library',
|
|
124
|
+
},
|
|
125
|
+
// ── SQLite path (only shown when backend = sqlite) ────────────────────
|
|
126
|
+
{
|
|
127
|
+
displayName: 'SQLite Database Path',
|
|
128
|
+
name: 'sqlitePath',
|
|
129
|
+
type: 'string',
|
|
130
|
+
default: 'safeagent.db',
|
|
131
|
+
displayOptions: {
|
|
132
|
+
show: {
|
|
133
|
+
backend: ['sqlite'],
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
description: 'Filesystem path to the SQLite database file. ' +
|
|
137
|
+
'Relative paths are resolved from the n8n working directory.',
|
|
138
|
+
},
|
|
139
|
+
],
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
// ── Execute ──────────────────────────────────────────────────────────────
|
|
143
|
+
async execute() {
|
|
144
|
+
const items = this.getInputData();
|
|
145
|
+
const returnData = [];
|
|
146
|
+
for (let i = 0; i < items.length; i++) {
|
|
147
|
+
const requestId = this.getNodeParameter('requestId', i);
|
|
148
|
+
const actionName = this.getNodeParameter('actionName', i);
|
|
149
|
+
const backend = this.getNodeParameter('backend', i);
|
|
150
|
+
if (!requestId.trim()) {
|
|
151
|
+
throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'Request ID must not be empty.', {
|
|
152
|
+
itemIndex: i,
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
if (!actionName.trim()) {
|
|
156
|
+
throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'Action Name must not be empty.', {
|
|
157
|
+
itemIndex: i,
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
let script;
|
|
161
|
+
if (backend === 'sqlite') {
|
|
162
|
+
const dbPath = this.getNodeParameter('sqlitePath', i);
|
|
163
|
+
script = buildSQLiteScript(requestId, actionName, dbPath || 'safeagent.db');
|
|
164
|
+
}
|
|
165
|
+
else {
|
|
166
|
+
// Build a libpq DSN from the stored credentials
|
|
167
|
+
const creds = await this.getCredentials('postgresApiCredentials');
|
|
168
|
+
const host = creds.host;
|
|
169
|
+
const port = creds.port;
|
|
170
|
+
const database = creds.database;
|
|
171
|
+
const user = creds.user;
|
|
172
|
+
const password = creds.password;
|
|
173
|
+
const ssl = creds.ssl;
|
|
174
|
+
const sslMode = ssl ? 'require' : 'disable';
|
|
175
|
+
const dsn = `postgresql://${encodeURIComponent(user)}:${encodeURIComponent(password)}@${host}:${port}/${database}?sslmode=${sslMode}`;
|
|
176
|
+
script = buildPostgresScript(requestId, actionName, dsn);
|
|
177
|
+
}
|
|
178
|
+
let rawOutput;
|
|
179
|
+
try {
|
|
180
|
+
rawOutput = (0, child_process_1.execSync)(`python3 -c "${script.replace(/"/g, '\\"')}"`, {
|
|
181
|
+
encoding: 'utf8',
|
|
182
|
+
timeout: 15000,
|
|
183
|
+
}).trim();
|
|
184
|
+
}
|
|
185
|
+
catch (err) {
|
|
186
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
187
|
+
throw new n8n_workflow_1.NodeOperationError(this.getNode(), `safeagent-exec-guard subprocess failed: ${message}`, { itemIndex: i });
|
|
188
|
+
}
|
|
189
|
+
let guardResult;
|
|
190
|
+
try {
|
|
191
|
+
guardResult = JSON.parse(rawOutput);
|
|
192
|
+
}
|
|
193
|
+
catch {
|
|
194
|
+
throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Unexpected output from safeagent-exec-guard (expected JSON): ${rawOutput}`, { itemIndex: i });
|
|
195
|
+
}
|
|
196
|
+
// `inserted: true` → new claim; caller should proceed with the action.
|
|
197
|
+
// `inserted: false` → already executed; caller should skip and use receipt.
|
|
198
|
+
const isDuplicate = guardResult.inserted === false;
|
|
199
|
+
returnData.push({
|
|
200
|
+
json: {
|
|
201
|
+
requestId,
|
|
202
|
+
actionName,
|
|
203
|
+
backend,
|
|
204
|
+
isDuplicate,
|
|
205
|
+
shouldProceed: !isDuplicate,
|
|
206
|
+
...guardResult,
|
|
207
|
+
},
|
|
208
|
+
pairedItem: { item: i },
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
return [returnData];
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
exports.SafeAgent = SafeAgent;
|
|
215
|
+
//# sourceMappingURL=SafeAgent.node.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SafeAgent.node.js","sourceRoot":"","sources":["../../../nodes/SafeAgent/SafeAgent.node.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "n8n-nodes-safeagent",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "n8n community node — SafeAgent execution guard (claim-before-execute idempotency pattern)",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"n8n-community-node-package",
|
|
7
|
+
"safeagent",
|
|
8
|
+
"idempotency",
|
|
9
|
+
"execution-guard"
|
|
10
|
+
],
|
|
11
|
+
"license": "MIT",
|
|
12
|
+
"homepage": "https://github.com/your-org/n8n-nodes-safeagent",
|
|
13
|
+
"author": {
|
|
14
|
+
"name": "Your Name",
|
|
15
|
+
"email": "you@example.com"
|
|
16
|
+
},
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "https://github.com/your-org/n8n-nodes-safeagent.git"
|
|
20
|
+
},
|
|
21
|
+
"main": "index.js",
|
|
22
|
+
"scripts": {
|
|
23
|
+
"build": "tsc",
|
|
24
|
+
"dev": "tsc --watch",
|
|
25
|
+
"format": "prettier nodes credentials --write",
|
|
26
|
+
"lint": "eslint nodes credentials package.json",
|
|
27
|
+
"lintfix": "eslint nodes credentials package.json --fix",
|
|
28
|
+
"prepublishOnly": "npm run build"
|
|
29
|
+
},
|
|
30
|
+
"files": [
|
|
31
|
+
"dist"
|
|
32
|
+
],
|
|
33
|
+
"n8n": {
|
|
34
|
+
"n8nNodesApiVersion": 1,
|
|
35
|
+
"credentials": [
|
|
36
|
+
"dist/credentials/PostgresApiCredentials.credentials.js"
|
|
37
|
+
],
|
|
38
|
+
"nodes": [
|
|
39
|
+
"dist/nodes/SafeAgent/SafeAgent.node.js"
|
|
40
|
+
]
|
|
41
|
+
},
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"@types/node": "^18.16.0",
|
|
44
|
+
"n8n-workflow": "*",
|
|
45
|
+
"typescript": "^5.1.0"
|
|
46
|
+
},
|
|
47
|
+
"peerDependencies": {
|
|
48
|
+
"n8n-workflow": "*"
|
|
49
|
+
}
|
|
50
|
+
}
|