@xshieldai/n8n-nodes-kavachos 0.0.0-stage โ 1.5.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/LICENSE +31 -0
- package/README.md +95 -2
- package/dist/credentials/AegisApi.credentials.d.ts +7 -0
- package/dist/credentials/AegisApi.credentials.js +29 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +14 -0
- package/dist/nodes/KavachAudit/KavachAudit.node.d.ts +5 -0
- package/dist/nodes/KavachAudit/KavachAudit.node.js +122 -0
- package/dist/nodes/KavachAudit/kavach-audit.svg +5 -0
- package/dist/nodes/KavachBudget/KavachBudget.node.d.ts +5 -0
- package/dist/nodes/KavachBudget/KavachBudget.node.js +92 -0
- package/dist/nodes/KavachBudget/kavach-budget.svg +5 -0
- package/dist/nodes/KavachGate/KavachGate.node.d.ts +5 -0
- package/dist/nodes/KavachGate/KavachGate.node.js +151 -0
- package/dist/nodes/KavachGate/kavach-gate.svg +5 -0
- package/dist/nodes/KavachRun/KavachRun.node.d.ts +5 -0
- package/dist/nodes/KavachRun/KavachRun.node.js +245 -0
- package/dist/nodes/KavachRun/kavach-run.svg +5 -0
- package/dist/nodes/shared.d.ts +28 -0
- package/dist/nodes/shared.js +139 -0
- package/package.json +57 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
GNU AFFERO GENERAL PUBLIC LICENSE
|
|
2
|
+
Version 3, 19 November 2007
|
|
3
|
+
|
|
4
|
+
Copyright (C) 2026 ANKR Labs / Capt. Anil Sharma
|
|
5
|
+
|
|
6
|
+
This program is free software: you can redistribute it and/or modify
|
|
7
|
+
it under the terms of the GNU Affero General Public License as published by
|
|
8
|
+
the Free Software Foundation, either version 3 of the License, or
|
|
9
|
+
(at your option) any later version.
|
|
10
|
+
|
|
11
|
+
This program is distributed in the hope that it will be useful,
|
|
12
|
+
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
13
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
14
|
+
GNU Affero General Public License for more details.
|
|
15
|
+
|
|
16
|
+
You should have received a copy of the GNU Affero General Public License
|
|
17
|
+
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
The full text of the GNU Affero General Public License v3 is available at:
|
|
22
|
+
https://www.gnu.org/licenses/agpl-3.0.txt
|
|
23
|
+
|
|
24
|
+
ADDITIONAL TERMS (permitted under AGPL ยง7):
|
|
25
|
+
|
|
26
|
+
If you run a modified version of this software as a network service,
|
|
27
|
+
you must make the complete source code of the modified version available
|
|
28
|
+
to all users of that service under the terms of this license.
|
|
29
|
+
|
|
30
|
+
Commercial use, including SaaS deployments and enterprise integrations,
|
|
31
|
+
requires a separate commercial license. Contact: captain@ankr.in
|
package/README.md
CHANGED
|
@@ -1,3 +1,96 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @xshieldai/n8n-nodes-kavachos
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
> **๐ Verification status (2026-10-05 IST โ v1.1.2)**
|
|
4
|
+
> - **Tests:** โ ๏ธ none yet โ n8n node test fixtures planned. See [PROOF-STACK.md](https://github.com/rocketlang/aegis/blob/master/PROOF-STACK.md).
|
|
5
|
+
> - **Examples:** โ
1 importable workflow, in the repository (not in the npm package): [examples/n8n-governed-agent.json](https://github.com/rocketlang/aegis/blob/master/packages/n8n-nodes-kavachos/examples/n8n-governed-agent.json). Drag into n8n, wire the Aegis API credential pointing at `http://localhost:4850`, run.
|
|
6
|
+
> - **Live demo:** โ ๏ธ planned (Tier 3)
|
|
7
|
+
> - **Phase-1 limits:** KavachRun is Linux-only; where enforcement is unavailable (non-Linux, or `kavachos` not installed) it **fails closed by default** (`On Non-Linux` = `throw`; `warn`/`skip` are opt-in). KavachGate works on any OS โ it's a thin HTTP relay to AEGIS.
|
|
8
|
+
>
|
|
9
|
+
> The KavachGate node's behaviour is end-to-end testable against a live AEGIS instance โ start AEGIS, import the example workflow, fire a `rm -rf /` command, watch it block. Automated CI fixtures still to come.
|
|
10
|
+
|
|
11
|
+
> **Renamed at 1.5.0.** This package was `@xshieldai/n8n-nodes` through 1.4.1. n8n's community-nodes screen
|
|
12
|
+
> only installs a package whose name starts with `n8n-nodes-` after the scope, and refused the old name. The
|
|
13
|
+
> nodes are the same. A workflow saved with the old package names its nodes `@xshieldai/n8n-nodes.kavachGate`
|
|
14
|
+
> and so on: after installing this package, change that prefix to `@xshieldai/n8n-nodes-kavachos.` in the
|
|
15
|
+
> workflow (or re-add the nodes), and remove the old package.
|
|
16
|
+
|
|
17
|
+
**KavachOS n8n community nodes** โ pre-execution DAN gate and kernel enforcement for AI agents.
|
|
18
|
+
|
|
19
|
+
Part of the **xShieldAI Posture Suite** ยท [kavachos.xshieldai.com](https://kavachos.xshieldai.com)
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## Nodes
|
|
24
|
+
|
|
25
|
+
### KavachGate
|
|
26
|
+
|
|
27
|
+
Intercepts AI agent actions **before execution**. Calls the Aegis KAVACH HTTP gate to:
|
|
28
|
+
|
|
29
|
+
1. Classify danger level (DAN 1โ4)
|
|
30
|
+
2. Notify approvers via Telegram / WhatsApp (opt-in, requires `webhook_url` in `~/.aegis/config.json`)
|
|
31
|
+
3. Wait for ALLOW or STOP
|
|
32
|
+
4. Block the workflow on STOP or timeout (silence = STOP)
|
|
33
|
+
|
|
34
|
+
**Framework-neutral**: all policy lives in Aegis (`localhost:4850`). The n8n node is a thin HTTP client.
|
|
35
|
+
|
|
36
|
+
### KavachRun
|
|
37
|
+
|
|
38
|
+
Wraps a subprocess in kavachos **kernel enforcement** (seccomp-bpf + cgroup BPF egress firewall).
|
|
39
|
+
|
|
40
|
+
**Requires `@xshieldai/agent-kernel` 2.5.0 or later** (since 1.3.0). The node starts
|
|
41
|
+
`kavachos run <options> -- <binary> <arguments>`, so the binary's arguments are passed on word for word and
|
|
42
|
+
cannot be read as kavachos options. With an older `kavachos` the node refuses to run. See the changelog.
|
|
43
|
+
|
|
44
|
+
Since 1.4.0 the node waits for the command without stopping n8n, stops it at **Time Limit (seconds)**, and
|
|
45
|
+
reports what `kavachos` itself said: a refused launch is an error, and `enforced` is true only when no layer
|
|
46
|
+
was reported missing (`egress_constrained`, `path_jail_loaded`). It reads kavachos's own lines for this, so
|
|
47
|
+
it is as true as they are.
|
|
48
|
+
|
|
49
|
+
**This node runs a command on the n8n host.** Anyone who can edit a workflow that contains it can run a
|
|
50
|
+
program there, as n8n's own Execute Command node can. Install it only where that is acceptable.
|
|
51
|
+
|
|
52
|
+
- Linux only โ where enforcement is unavailable it **fails closed by default** (`On Non-Linux` = `throw`; set `warn` to run unwrapped, or `skip`)
|
|
53
|
+
- Use **after** KavachGate for defense in depth: policy gate + kernel enforcement
|
|
54
|
+
- `trust_mask` controls which syscall groups the process may call
|
|
55
|
+
- `domain` selects the egress allowlist (maritime, logistics, OT, finance, general)
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## Install
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
# In your n8n data directory
|
|
63
|
+
npm install @xshieldai/n8n-nodes-kavachos
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Then restart n8n and add the **Aegis API** credential pointing to your Aegis dashboard (`http://localhost:4850` by default).
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## Quick Start
|
|
71
|
+
|
|
72
|
+
1. **Start Aegis**: `npx @xshieldai/aegis` (or `kavachos run --domain=general -- n8n`)
|
|
73
|
+
2. **Add credential**: Aegis API โ base URL `http://localhost:4850`
|
|
74
|
+
3. **Import template**: `examples/n8n-governed-agent.json`
|
|
75
|
+
4. **Run**: Trigger โ KavachGate โ AI Agent โ Audit
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## Gate API
|
|
80
|
+
|
|
81
|
+
The `KavachGate` node calls:
|
|
82
|
+
|
|
83
|
+
```
|
|
84
|
+
POST http://localhost:4850/api/v1/kavach/gate
|
|
85
|
+
{ "command": "...", "tool_name": "n8n-agent", "session_id": "...", "dry_run": false }
|
|
86
|
+
|
|
87
|
+
โ { "allow": true|false, "level": 0-4, "reason": "...", "decision": "ALLOW|STOP|TIMEOUT", "approval_id": "...", "_meta": {...} }
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
`allow: false` โ KavachGate throws (halt workflow) or passes `allow=false` downstream โ configurable per node.
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
## License
|
|
95
|
+
|
|
96
|
+
AGPL-3.0 โ governance layer is open. Enterprise Edition (multi-tenant, Merkle ledger, EU AI Act evidence): [xshieldai.com](https://xshieldai.com)
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.AegisApi = void 0;
|
|
4
|
+
class AegisApi {
|
|
5
|
+
constructor() {
|
|
6
|
+
this.name = "aegisApi";
|
|
7
|
+
this.displayName = "Aegis API";
|
|
8
|
+
this.documentationUrl = "https://kavachos.xshieldai.com";
|
|
9
|
+
this.properties = [
|
|
10
|
+
{
|
|
11
|
+
displayName: "Aegis Base URL",
|
|
12
|
+
name: "baseUrl",
|
|
13
|
+
type: "string",
|
|
14
|
+
default: "http://localhost:4850",
|
|
15
|
+
placeholder: "http://localhost:4850",
|
|
16
|
+
description: "URL of the Aegis dashboard server (runs POST /api/v1/kavach/gate)",
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
displayName: "API Token (optional)",
|
|
20
|
+
name: "token",
|
|
21
|
+
type: "string",
|
|
22
|
+
typeOptions: { password: true },
|
|
23
|
+
default: "",
|
|
24
|
+
description: "Bearer token if Aegis dashboard auth is enabled",
|
|
25
|
+
},
|
|
26
|
+
];
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
exports.AegisApi = AegisApi;
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { KavachGate } from "./nodes/KavachGate/KavachGate.node";
|
|
2
|
+
export { KavachRun } from "./nodes/KavachRun/KavachRun.node";
|
|
3
|
+
export { KavachBudget } from "./nodes/KavachBudget/KavachBudget.node";
|
|
4
|
+
export { KavachAudit } from "./nodes/KavachAudit/KavachAudit.node";
|
|
5
|
+
export { AegisApi } from "./credentials/AegisApi.credentials";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.AegisApi = exports.KavachAudit = exports.KavachBudget = exports.KavachRun = exports.KavachGate = void 0;
|
|
4
|
+
// SPDX-License-Identifier: AGPL-3.0-only
|
|
5
|
+
var KavachGate_node_1 = require("./nodes/KavachGate/KavachGate.node");
|
|
6
|
+
Object.defineProperty(exports, "KavachGate", { enumerable: true, get: function () { return KavachGate_node_1.KavachGate; } });
|
|
7
|
+
var KavachRun_node_1 = require("./nodes/KavachRun/KavachRun.node");
|
|
8
|
+
Object.defineProperty(exports, "KavachRun", { enumerable: true, get: function () { return KavachRun_node_1.KavachRun; } });
|
|
9
|
+
var KavachBudget_node_1 = require("./nodes/KavachBudget/KavachBudget.node");
|
|
10
|
+
Object.defineProperty(exports, "KavachBudget", { enumerable: true, get: function () { return KavachBudget_node_1.KavachBudget; } });
|
|
11
|
+
var KavachAudit_node_1 = require("./nodes/KavachAudit/KavachAudit.node");
|
|
12
|
+
Object.defineProperty(exports, "KavachAudit", { enumerable: true, get: function () { return KavachAudit_node_1.KavachAudit; } });
|
|
13
|
+
var AegisApi_credentials_1 = require("./credentials/AegisApi.credentials");
|
|
14
|
+
Object.defineProperty(exports, "AegisApi", { enumerable: true, get: function () { return AegisApi_credentials_1.AegisApi; } });
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { IExecuteFunctions, INodeExecutionData, INodeType, INodeTypeDescription } from "n8n-workflow";
|
|
2
|
+
export declare class KavachAudit implements INodeType {
|
|
3
|
+
description: INodeTypeDescription;
|
|
4
|
+
execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]>;
|
|
5
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.KavachAudit = void 0;
|
|
4
|
+
const n8n_workflow_1 = require("n8n-workflow");
|
|
5
|
+
const shared_1 = require("../shared");
|
|
6
|
+
class KavachAudit {
|
|
7
|
+
constructor() {
|
|
8
|
+
this.description = {
|
|
9
|
+
displayName: "Kavach Audit",
|
|
10
|
+
name: "kavachAudit",
|
|
11
|
+
icon: "file:kavach-audit.svg",
|
|
12
|
+
group: ["transform"],
|
|
13
|
+
version: 1,
|
|
14
|
+
subtitle: "Query DAN gate audit log",
|
|
15
|
+
description: "Queries the AEGIS audit log (kavach_approvals table). Returns gate decisions filtered by " +
|
|
16
|
+
"session ID, status, DAN level, or date range. Use for compliance reporting or post-incident review.",
|
|
17
|
+
defaults: { name: "Kavach Audit" },
|
|
18
|
+
inputs: ["main"],
|
|
19
|
+
outputs: ["main"],
|
|
20
|
+
credentials: [
|
|
21
|
+
{
|
|
22
|
+
name: "aegisApi",
|
|
23
|
+
required: true,
|
|
24
|
+
},
|
|
25
|
+
],
|
|
26
|
+
properties: [
|
|
27
|
+
{
|
|
28
|
+
displayName: "Filter by Session ID",
|
|
29
|
+
name: "sessionId",
|
|
30
|
+
type: "string",
|
|
31
|
+
default: "",
|
|
32
|
+
placeholder: "exec_abc123",
|
|
33
|
+
description: "Return only records for this session. Leave empty for all sessions.",
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
displayName: "Filter by Status",
|
|
37
|
+
name: "status",
|
|
38
|
+
type: "options",
|
|
39
|
+
options: [
|
|
40
|
+
{ name: "All", value: "all" },
|
|
41
|
+
{ name: "Pending", value: "pending" },
|
|
42
|
+
{ name: "Allowed", value: "allow" },
|
|
43
|
+
{ name: "Stopped", value: "stop" },
|
|
44
|
+
{ name: "Timed Out", value: "timeout" },
|
|
45
|
+
],
|
|
46
|
+
default: "all",
|
|
47
|
+
description: "Return only records matching this approval status.",
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
displayName: "Filter by DAN Level",
|
|
51
|
+
name: "danLevel",
|
|
52
|
+
type: "options",
|
|
53
|
+
options: [
|
|
54
|
+
{ name: "All levels", value: 0 },
|
|
55
|
+
{ name: "DAN-1 (safe)", value: 1 },
|
|
56
|
+
{ name: "DAN-2 (elevated)", value: 2 },
|
|
57
|
+
{ name: "DAN-3 (dangerous)", value: 3 },
|
|
58
|
+
{ name: "DAN-4 (catastrophic)", value: 4 },
|
|
59
|
+
],
|
|
60
|
+
default: 0,
|
|
61
|
+
description: "Return only records at this DAN level. 0 = all levels.",
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
displayName: "Limit",
|
|
65
|
+
name: "limit",
|
|
66
|
+
type: "number",
|
|
67
|
+
default: 50,
|
|
68
|
+
description: "Maximum number of records to return.",
|
|
69
|
+
},
|
|
70
|
+
],
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
async execute() {
|
|
74
|
+
const items = this.getInputData();
|
|
75
|
+
const results = [];
|
|
76
|
+
const credentials = await this.getCredentials("aegisApi");
|
|
77
|
+
const baseUrl = credentials.baseUrl.replace(/\/$/, "");
|
|
78
|
+
const token = credentials.token;
|
|
79
|
+
for (let i = 0; i < items.length; i++) {
|
|
80
|
+
const sessionId = this.getNodeParameter("sessionId", i).trim();
|
|
81
|
+
const status = this.getNodeParameter("status", i);
|
|
82
|
+
const danLevel = this.getNodeParameter("danLevel", i);
|
|
83
|
+
const limit = this.getNodeParameter("limit", i);
|
|
84
|
+
const params = new URLSearchParams();
|
|
85
|
+
if (sessionId)
|
|
86
|
+
params.set("session_id", sessionId);
|
|
87
|
+
if (status !== "all")
|
|
88
|
+
params.set("status", status);
|
|
89
|
+
if (danLevel > 0)
|
|
90
|
+
params.set("level", danLevel.toString());
|
|
91
|
+
params.set("limit", limit.toString());
|
|
92
|
+
const headers = {};
|
|
93
|
+
if (token)
|
|
94
|
+
headers["Authorization"] = `Bearer ${token}`;
|
|
95
|
+
let auditResult;
|
|
96
|
+
let reply;
|
|
97
|
+
try {
|
|
98
|
+
reply = await (0, shared_1.requestJson)("GET", `${baseUrl}/api/v1/kavach/audit?${params.toString()}`, headers, undefined, 30000);
|
|
99
|
+
}
|
|
100
|
+
catch (err) {
|
|
101
|
+
throw new n8n_workflow_1.NodeOperationError(this.getNode(), err?.message === "TIMEOUT" ? `Aegis audit did not answer within 30 s at ${baseUrl}` : `Cannot reach Aegis audit at ${baseUrl}: ${err?.message ?? err}`, { itemIndex: i });
|
|
102
|
+
}
|
|
103
|
+
if (reply.status !== 200 || !reply.json || typeof reply.json !== "object") {
|
|
104
|
+
throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Aegis audit answered HTTP ${reply.status}: ${reply.text.slice(0, 300)}. On a dashboard with a password the audit needs the API token (dashboard.auth.api_token), set in this credential.`, { itemIndex: i });
|
|
105
|
+
}
|
|
106
|
+
auditResult = reply.json;
|
|
107
|
+
results.push({
|
|
108
|
+
json: {
|
|
109
|
+
...items[i].json,
|
|
110
|
+
kavach_audit: {
|
|
111
|
+
records: auditResult.records,
|
|
112
|
+
total: auditResult.total,
|
|
113
|
+
_meta: auditResult._meta,
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
pairedItem: i,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
return [results];
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
exports.KavachAudit = KavachAudit;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64" role="img" aria-label="Kavach Audit">
|
|
2
|
+
<path d="M32 4 54 12v18c0 14-9 25-22 30C19 55 10 44 10 30V12z" fill="#1f3a5f"/>
|
|
3
|
+
<path d="M32 9 49 15.5V30c0 11.5-7 20.5-17 24.6C22 50.500 15 41.500 15 30V15.500z" fill="#f4f1e8"/>
|
|
4
|
+
<path d="M23 23h18M23 31h18M23 39h11" stroke="#1f3a5f" stroke-width="3.500" stroke-linecap="round"/><path d="m37 40 3 3 6-7" fill="none" stroke="#c2532d" stroke-width="3.500" stroke-linecap="round" stroke-linejoin="round"/>
|
|
5
|
+
</svg>
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { IExecuteFunctions, INodeExecutionData, INodeType, INodeTypeDescription } from "n8n-workflow";
|
|
2
|
+
export declare class KavachBudget implements INodeType {
|
|
3
|
+
description: INodeTypeDescription;
|
|
4
|
+
execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]>;
|
|
5
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.KavachBudget = void 0;
|
|
4
|
+
const n8n_workflow_1 = require("n8n-workflow");
|
|
5
|
+
const shared_1 = require("../shared");
|
|
6
|
+
class KavachBudget {
|
|
7
|
+
constructor() {
|
|
8
|
+
this.description = {
|
|
9
|
+
displayName: "Kavach Budget",
|
|
10
|
+
name: "kavachBudget",
|
|
11
|
+
icon: "file:kavach-budget.svg",
|
|
12
|
+
group: ["transform"],
|
|
13
|
+
version: 1,
|
|
14
|
+
subtitle: "Budget + valve state",
|
|
15
|
+
description: "Reads the current AEGIS budget state (daily spend, limit, remaining, breach flag) and " +
|
|
16
|
+
"gate-valve status. Use to guard expensive workflows or to surface cost data in dashboards.",
|
|
17
|
+
defaults: { name: "Kavach Budget" },
|
|
18
|
+
inputs: ["main"],
|
|
19
|
+
outputs: ["main", "main"],
|
|
20
|
+
outputNames: ["within budget", "breached"],
|
|
21
|
+
credentials: [
|
|
22
|
+
{
|
|
23
|
+
name: "aegisApi",
|
|
24
|
+
required: true,
|
|
25
|
+
},
|
|
26
|
+
],
|
|
27
|
+
properties: [
|
|
28
|
+
{
|
|
29
|
+
displayName: "Halt on Breach",
|
|
30
|
+
name: "haltOnBreach",
|
|
31
|
+
type: "boolean",
|
|
32
|
+
default: false,
|
|
33
|
+
description: "When true and budget is breached, throw an error instead of routing to the 'breached' output.",
|
|
34
|
+
},
|
|
35
|
+
],
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
async execute() {
|
|
39
|
+
const items = this.getInputData();
|
|
40
|
+
const withinBudget = [];
|
|
41
|
+
const breachedItems = [];
|
|
42
|
+
const credentials = await this.getCredentials("aegisApi");
|
|
43
|
+
const baseUrl = credentials.baseUrl.replace(/\/$/, "");
|
|
44
|
+
const token = credentials.token;
|
|
45
|
+
for (let i = 0; i < items.length; i++) {
|
|
46
|
+
const haltOnBreach = this.getNodeParameter("haltOnBreach", i);
|
|
47
|
+
const headers = {};
|
|
48
|
+
if (token)
|
|
49
|
+
headers["Authorization"] = `Bearer ${token}`;
|
|
50
|
+
let state;
|
|
51
|
+
let reply;
|
|
52
|
+
try {
|
|
53
|
+
reply = await (0, shared_1.requestJson)("GET", `${baseUrl}/api/v1/kavach/state`, headers, undefined, 30000);
|
|
54
|
+
}
|
|
55
|
+
catch (err) {
|
|
56
|
+
throw new n8n_workflow_1.NodeOperationError(this.getNode(), err?.message === "TIMEOUT" ? `Aegis state did not answer within 30 s at ${baseUrl}` : `Cannot reach Aegis state at ${baseUrl}: ${err?.message ?? err}`, { itemIndex: i });
|
|
57
|
+
}
|
|
58
|
+
if (reply.status !== 200 || !reply.json || typeof reply.json !== "object") {
|
|
59
|
+
throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Aegis state answered HTTP ${reply.status}: ${reply.text.slice(0, 300)}.`, { itemIndex: i });
|
|
60
|
+
}
|
|
61
|
+
state = reply.json;
|
|
62
|
+
if (!state.budget || typeof state.budget.breached !== "boolean") {
|
|
63
|
+
throw new n8n_workflow_1.NodeOperationError(this.getNode(), `what answered at ${baseUrl} is not the Aegis budget state`, { itemIndex: i });
|
|
64
|
+
}
|
|
65
|
+
const output = {
|
|
66
|
+
json: {
|
|
67
|
+
...items[i].json,
|
|
68
|
+
kavach_budget: {
|
|
69
|
+
daily_spent_usd: state.budget.daily_spent_usd,
|
|
70
|
+
daily_limit_usd: state.budget.daily_limit_usd,
|
|
71
|
+
daily_remaining_usd: state.budget.daily_remaining_usd,
|
|
72
|
+
breached: state.budget.breached,
|
|
73
|
+
kavach_enabled: state.kavach_enabled,
|
|
74
|
+
_meta: state._meta,
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
pairedItem: i,
|
|
78
|
+
};
|
|
79
|
+
if (state.budget.breached) {
|
|
80
|
+
if (haltOnBreach) {
|
|
81
|
+
throw new n8n_workflow_1.NodeOperationError(this.getNode(), `KAVACH budget breached: $${state.budget.daily_spent_usd.toFixed(4)} of $${state.budget.daily_limit_usd} daily limit used`, { itemIndex: i });
|
|
82
|
+
}
|
|
83
|
+
breachedItems.push(output);
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
withinBudget.push(output);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return [withinBudget, breachedItems];
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
exports.KavachBudget = KavachBudget;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64" role="img" aria-label="Kavach Budget">
|
|
2
|
+
<path d="M32 4 54 12v18c0 14-9 25-22 30C19 55 10 44 10 30V12z" fill="#1f3a5f"/>
|
|
3
|
+
<path d="M32 9 49 15.5V30c0 11.5-7 20.5-17 24.6C22 50.500 15 41.500 15 30V15.500z" fill="#f4f1e8"/>
|
|
4
|
+
<circle cx="32" cy="32" r="12" fill="none" stroke="#1f3a5f" stroke-width="3.500"/><path d="M32 32V22M32 32l8 5" stroke="#c2532d" stroke-width="3.500" stroke-linecap="round"/>
|
|
5
|
+
</svg>
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { IExecuteFunctions, INodeExecutionData, INodeType, INodeTypeDescription } from "n8n-workflow";
|
|
2
|
+
export declare class KavachGate implements INodeType {
|
|
3
|
+
description: INodeTypeDescription;
|
|
4
|
+
execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]>;
|
|
5
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.KavachGate = void 0;
|
|
4
|
+
const n8n_workflow_1 = require("n8n-workflow");
|
|
5
|
+
const shared_1 = require("../shared");
|
|
6
|
+
class KavachGate {
|
|
7
|
+
constructor() {
|
|
8
|
+
this.description = {
|
|
9
|
+
displayName: "Kavach Gate",
|
|
10
|
+
name: "kavachGate",
|
|
11
|
+
icon: "file:kavach-gate.svg",
|
|
12
|
+
group: ["transform"],
|
|
13
|
+
version: 1,
|
|
14
|
+
subtitle: "Pre-execution DAN gate",
|
|
15
|
+
description: "Intercepts AI agent actions before execution. Calls the Aegis KAVACH gate to classify danger level (DAN 1-4), notify approvers, and wait for ALLOW/STOP. Blocks the workflow on STOP or timeout.",
|
|
16
|
+
defaults: { name: "Kavach Gate" },
|
|
17
|
+
inputs: ["main"],
|
|
18
|
+
outputs: ["main"],
|
|
19
|
+
credentials: [
|
|
20
|
+
{
|
|
21
|
+
name: "aegisApi",
|
|
22
|
+
required: true,
|
|
23
|
+
},
|
|
24
|
+
],
|
|
25
|
+
properties: [
|
|
26
|
+
{
|
|
27
|
+
displayName: "Command",
|
|
28
|
+
name: "command",
|
|
29
|
+
type: "string",
|
|
30
|
+
default: "={{ $json.command }}",
|
|
31
|
+
required: true,
|
|
32
|
+
description: "The command or action the agent wants to execute. Accepts an expression referencing upstream node output.",
|
|
33
|
+
placeholder: "prisma migrate reset",
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
displayName: "Tool Name",
|
|
37
|
+
name: "toolName",
|
|
38
|
+
type: "string",
|
|
39
|
+
default: "n8n-agent",
|
|
40
|
+
description: "Name of the tool/node calling this gate. Appears in audit records.",
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
displayName: "Session ID",
|
|
44
|
+
name: "sessionId",
|
|
45
|
+
type: "string",
|
|
46
|
+
default: "={{ $execution.id }}",
|
|
47
|
+
description: "Workflow execution ID used for audit trail grouping.",
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
displayName: "Dry Run",
|
|
51
|
+
name: "dryRun",
|
|
52
|
+
type: "boolean",
|
|
53
|
+
default: false,
|
|
54
|
+
description: "Classify danger level only โ no notification sent, no polling. Use for testing the gate without blocking.",
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
displayName: "Wait For A Decision (seconds)",
|
|
58
|
+
name: "timeoutSeconds",
|
|
59
|
+
type: "number",
|
|
60
|
+
default: 660,
|
|
61
|
+
description: "How long to wait for the gate's answer. Aegis itself waits up to 600 s for an approver on the lowest level, " +
|
|
62
|
+
"so the default is a little longer. No answer in this time is treated as a block.",
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
displayName: "On STOP / Timeout",
|
|
66
|
+
name: "onBlock",
|
|
67
|
+
type: "options",
|
|
68
|
+
options: [
|
|
69
|
+
{ name: "Throw Error (halt workflow)", value: "throw" },
|
|
70
|
+
{ name: "Continue with allow=false", value: "continue" },
|
|
71
|
+
],
|
|
72
|
+
default: "throw",
|
|
73
|
+
description: "What to do when the gate blocks the action. Default: throw error to halt the workflow.",
|
|
74
|
+
},
|
|
75
|
+
],
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
async execute() {
|
|
79
|
+
const items = this.getInputData();
|
|
80
|
+
const results = [];
|
|
81
|
+
const credentials = await this.getCredentials("aegisApi");
|
|
82
|
+
const baseUrl = credentials.baseUrl.replace(/\/$/, "");
|
|
83
|
+
const token = credentials.token;
|
|
84
|
+
for (let i = 0; i < items.length; i++) {
|
|
85
|
+
const command = this.getNodeParameter("command", i);
|
|
86
|
+
const toolName = this.getNodeParameter("toolName", i);
|
|
87
|
+
const sessionId = this.getNodeParameter("sessionId", i);
|
|
88
|
+
const dryRun = this.getNodeParameter("dryRun", i);
|
|
89
|
+
const onBlock = this.getNodeParameter("onBlock", i);
|
|
90
|
+
const limit = Number(this.getNodeParameter("timeoutSeconds", i, 660));
|
|
91
|
+
const timeoutMs = (Number.isFinite(limit) && limit > 0 ? limit : 660) * 1000;
|
|
92
|
+
if (!command?.trim()) {
|
|
93
|
+
throw new n8n_workflow_1.NodeOperationError(this.getNode(), "command must not be empty", { itemIndex: i });
|
|
94
|
+
}
|
|
95
|
+
const headers = { "Content-Type": "application/json" };
|
|
96
|
+
if (token)
|
|
97
|
+
headers["Authorization"] = `Bearer ${token}`;
|
|
98
|
+
let gateResult;
|
|
99
|
+
// Three different failures, said as what they are: no answer in time (a block), an answer that is
|
|
100
|
+
// not the gate's (an error), and Aegis not reachable (an error). Through 1.3.0 all three read
|
|
101
|
+
// "Cannot reach Aegis gate".
|
|
102
|
+
let reply;
|
|
103
|
+
try {
|
|
104
|
+
reply = await (0, shared_1.requestJson)("POST", `${baseUrl}/api/v1/kavach/gate`, headers, JSON.stringify({ command, tool_name: toolName, session_id: sessionId, dry_run: dryRun }), timeoutMs);
|
|
105
|
+
}
|
|
106
|
+
catch (err) {
|
|
107
|
+
if (err?.message === "TIMEOUT") {
|
|
108
|
+
gateResult = { allow: false, reason: `no decision from the Aegis gate within ${timeoutMs / 1000} s`, decision: "TIMEOUT" };
|
|
109
|
+
reply = undefined;
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Cannot reach Aegis gate at ${baseUrl}: ${err?.message ?? err}`, { itemIndex: i });
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
if (reply) {
|
|
116
|
+
if (reply.status !== 200) {
|
|
117
|
+
throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Aegis gate answered HTTP ${reply.status}, not a decision: ${reply.text.slice(0, 300)}`, { itemIndex: i });
|
|
118
|
+
}
|
|
119
|
+
const j = reply.json;
|
|
120
|
+
if (!j || typeof j !== "object" || typeof j.allow !== "boolean") {
|
|
121
|
+
throw new n8n_workflow_1.NodeOperationError(this.getNode(), `what answered at ${baseUrl} is not the Aegis gate: no true/false "allow" in its answer. Treated as a block.`, { itemIndex: i });
|
|
122
|
+
}
|
|
123
|
+
gateResult = j;
|
|
124
|
+
}
|
|
125
|
+
gateResult = gateResult;
|
|
126
|
+
// Only the boolean true is an allow. A string, a number or anything else is not.
|
|
127
|
+
if (gateResult.allow !== true) {
|
|
128
|
+
if (onBlock === "throw") {
|
|
129
|
+
throw new n8n_workflow_1.NodeOperationError(this.getNode(), `KAVACH blocked action (DAN ${gateResult.level ?? "?"}): ${gateResult.reason}`, { itemIndex: i });
|
|
130
|
+
}
|
|
131
|
+
// continue mode โ pass allow=false downstream
|
|
132
|
+
}
|
|
133
|
+
results.push({
|
|
134
|
+
json: {
|
|
135
|
+
...items[i].json,
|
|
136
|
+
kavach: {
|
|
137
|
+
allow: gateResult.allow === true,
|
|
138
|
+
level: gateResult.level,
|
|
139
|
+
reason: gateResult.reason,
|
|
140
|
+
decision: gateResult.decision,
|
|
141
|
+
approval_id: gateResult.approval_id,
|
|
142
|
+
_meta: gateResult._meta,
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
pairedItem: i,
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
return [results];
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
exports.KavachGate = KavachGate;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64" role="img" aria-label="Kavach Gate">
|
|
2
|
+
<path d="M32 4 54 12v18c0 14-9 25-22 30C19 55 10 44 10 30V12z" fill="#1f3a5f"/>
|
|
3
|
+
<path d="M32 9 49 15.5V30c0 11.5-7 20.5-17 24.6C22 50.500 15 41.500 15 30V15.500z" fill="#f4f1e8"/>
|
|
4
|
+
<path d="M22 44V28a10 10 0 0 1 20 0v16M27 44V30a5 5 0 0 1 10 0v14" fill="none" stroke="#1f3a5f" stroke-width="3.500" stroke-linecap="round"/><path d="M20 44h24" stroke="#c2532d" stroke-width="3.500" stroke-linecap="round"/>
|
|
5
|
+
</svg>
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { IExecuteFunctions, INodeExecutionData, INodeType, INodeTypeDescription } from "n8n-workflow";
|
|
2
|
+
export declare class KavachRun implements INodeType {
|
|
3
|
+
description: INodeTypeDescription;
|
|
4
|
+
execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]>;
|
|
5
|
+
}
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.KavachRun = void 0;
|
|
4
|
+
const n8n_workflow_1 = require("n8n-workflow");
|
|
5
|
+
const child_process_1 = require("child_process");
|
|
6
|
+
const shared_1 = require("../shared");
|
|
7
|
+
function isLinux() {
|
|
8
|
+
return process.platform === "linux";
|
|
9
|
+
}
|
|
10
|
+
function kavachosAvailable() {
|
|
11
|
+
try {
|
|
12
|
+
(0, child_process_1.execSync)("which kavachos", { stdio: "pipe" });
|
|
13
|
+
return true;
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
// kavachos reads a bare "--" as the end of its own options from 2.5.0. Before that, any word starting
|
|
20
|
+
// with "--" among the binary's arguments was read as a kavachos option (a dry run, an unconstrained
|
|
21
|
+
// launch, another trust mask). The arguments of this node often come from an expression, so the node
|
|
22
|
+
// uses "--" and refuses a kavachos that does not know it.
|
|
23
|
+
const KAVACHOS_MIN = "2.5.0";
|
|
24
|
+
function kavachosVersion() {
|
|
25
|
+
const out = (0, child_process_1.spawnSync)("kavachos", ["version"], { encoding: "utf8", timeout: 15000 });
|
|
26
|
+
const m = /^kavachos (\d+\.\d+\.\d+)/m.exec(out.stdout ?? "");
|
|
27
|
+
return m ? m[1] : null;
|
|
28
|
+
}
|
|
29
|
+
function versionAtLeast(have, want) {
|
|
30
|
+
const h = have.split(".").map((n) => parseInt(n, 10));
|
|
31
|
+
const w = want.split(".").map((n) => parseInt(n, 10));
|
|
32
|
+
for (let i = 0; i < 3; i++) {
|
|
33
|
+
if ((h[i] ?? 0) !== (w[i] ?? 0))
|
|
34
|
+
return (h[i] ?? 0) > (w[i] ?? 0);
|
|
35
|
+
}
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
class KavachRun {
|
|
39
|
+
constructor() {
|
|
40
|
+
this.description = {
|
|
41
|
+
displayName: "Kavach Run",
|
|
42
|
+
name: "kavachRun",
|
|
43
|
+
icon: "file:kavach-run.svg",
|
|
44
|
+
group: ["transform"],
|
|
45
|
+
version: 1,
|
|
46
|
+
subtitle: "Kernel-enforced subprocess (Linux)",
|
|
47
|
+
description: "Wraps a subprocess in kavachos kernel enforcement (seccomp-bpf + cgroup BPF egress). " +
|
|
48
|
+
"Linux only โ where enforcement is unavailable (non-Linux, or kavachos not installed) it " +
|
|
49
|
+
"FAILS CLOSED by default (configurable to warn or skip). " +
|
|
50
|
+
"Use after KavachGate for defense in depth: policy gate + kernel enforcement.",
|
|
51
|
+
defaults: { name: "Kavach Run" },
|
|
52
|
+
inputs: ["main"],
|
|
53
|
+
outputs: ["main"],
|
|
54
|
+
properties: [
|
|
55
|
+
{
|
|
56
|
+
displayName: "Binary",
|
|
57
|
+
name: "binary",
|
|
58
|
+
type: "string",
|
|
59
|
+
default: "",
|
|
60
|
+
required: true,
|
|
61
|
+
placeholder: "bun",
|
|
62
|
+
description: "The binary to execute under kernel enforcement.",
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
displayName: "Arguments",
|
|
66
|
+
name: "args",
|
|
67
|
+
type: "string",
|
|
68
|
+
default: "",
|
|
69
|
+
placeholder: "src/my-agent.ts --port 4900",
|
|
70
|
+
description: "Space-separated arguments to pass to the binary.",
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
displayName: "Trust Mask (hex)",
|
|
74
|
+
name: "trustMask",
|
|
75
|
+
type: "string",
|
|
76
|
+
default: "0xFF",
|
|
77
|
+
description: "Seccomp-bpf trust mask. Controls which syscall groups the process is allowed to call. " +
|
|
78
|
+
"0xFF = infrastructure bits. See KavachOS trust_mask docs.",
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
displayName: "Domain",
|
|
82
|
+
name: "domain",
|
|
83
|
+
type: "options",
|
|
84
|
+
options: [
|
|
85
|
+
{ name: "general", value: "general" },
|
|
86
|
+
{ name: "maritime", value: "maritime" },
|
|
87
|
+
{ name: "logistics", value: "logistics" },
|
|
88
|
+
{ name: "ot", value: "ot" },
|
|
89
|
+
{ name: "finance", value: "finance" },
|
|
90
|
+
],
|
|
91
|
+
default: "general",
|
|
92
|
+
description: "Domain profile โ controls egress allowlist and Falco rule set.",
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
displayName: "Enable Falco",
|
|
96
|
+
name: "falco",
|
|
97
|
+
type: "boolean",
|
|
98
|
+
default: false,
|
|
99
|
+
description: "Enable Falco CRITICAL rule monitoring (requires Falco installed on host).",
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
displayName: "Time Limit (seconds)",
|
|
103
|
+
name: "timeoutSeconds",
|
|
104
|
+
type: "number",
|
|
105
|
+
default: 300,
|
|
106
|
+
description: "The command is stopped after this long and the node fails. n8n keeps running other workflows meanwhile.",
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
displayName: "On Non-Linux",
|
|
110
|
+
name: "onNonLinux",
|
|
111
|
+
type: "options",
|
|
112
|
+
options: [
|
|
113
|
+
{ name: "Throw Error (fail closed)", value: "throw" },
|
|
114
|
+
{ name: "Warn and run unwrapped", value: "warn" },
|
|
115
|
+
{ name: "Skip execution entirely", value: "skip" },
|
|
116
|
+
],
|
|
117
|
+
default: "throw",
|
|
118
|
+
description: "Behavior when kernel enforcement is unavailable (non-Linux, or kavachos not installed). " +
|
|
119
|
+
"Default THROW = fail closed: a defense-in-depth node must not silently run a command " +
|
|
120
|
+
"unwrapped. Choose 'warn' to run unwrapped with an enforced:false flag, or 'skip' to not run it.",
|
|
121
|
+
},
|
|
122
|
+
],
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
async execute() {
|
|
126
|
+
const items = this.getInputData();
|
|
127
|
+
const results = [];
|
|
128
|
+
for (let i = 0; i < items.length; i++) {
|
|
129
|
+
const binary = this.getNodeParameter("binary", i);
|
|
130
|
+
const args = this.getNodeParameter("args", i).trim();
|
|
131
|
+
const trustMask = this.getNodeParameter("trustMask", i);
|
|
132
|
+
const domain = this.getNodeParameter("domain", i);
|
|
133
|
+
const falco = this.getNodeParameter("falco", i);
|
|
134
|
+
const onNonLinux = this.getNodeParameter("onNonLinux", i);
|
|
135
|
+
const limit = Number(this.getNodeParameter("timeoutSeconds", i, 300));
|
|
136
|
+
const timeoutMs = (Number.isFinite(limit) && limit > 0 ? limit : 300) * 1000;
|
|
137
|
+
if (!binary?.trim()) {
|
|
138
|
+
throw new n8n_workflow_1.NodeOperationError(this.getNode(), "binary must not be empty", { itemIndex: i });
|
|
139
|
+
}
|
|
140
|
+
const linux = isLinux();
|
|
141
|
+
const hasKavachos = linux && kavachosAvailable();
|
|
142
|
+
if (!linux || !hasKavachos) {
|
|
143
|
+
const platform = process.platform;
|
|
144
|
+
const reason = !linux
|
|
145
|
+
? `KavachRun kernel enforcement not available on ${platform} โ requires Linux with seccomp-bpf`
|
|
146
|
+
: "kavachos CLI not found โ install with: npm install -g @xshieldai/agent-kernel";
|
|
147
|
+
if (onNonLinux === "throw") {
|
|
148
|
+
throw new n8n_workflow_1.NodeOperationError(this.getNode(), reason, { itemIndex: i });
|
|
149
|
+
}
|
|
150
|
+
if (onNonLinux === "skip") {
|
|
151
|
+
results.push({
|
|
152
|
+
json: { ...items[i].json, kavach_run: { skipped: true, reason } },
|
|
153
|
+
pairedItem: i,
|
|
154
|
+
});
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
// warn: run unwrapped with a flag in output
|
|
158
|
+
process.stderr.write(`[KavachRun] WARNING: ${reason} โ running ${binary} unwrapped\n`);
|
|
159
|
+
const argList = args ? args.split(/\s+/) : [];
|
|
160
|
+
const proc = await (0, shared_1.runProcess)(binary, argList, timeoutMs);
|
|
161
|
+
if (proc.error) {
|
|
162
|
+
throw new n8n_workflow_1.NodeOperationError(this.getNode(), `${binary} could not be started: ${proc.error}`, { itemIndex: i });
|
|
163
|
+
}
|
|
164
|
+
if (proc.timedOut) {
|
|
165
|
+
throw new n8n_workflow_1.NodeOperationError(this.getNode(), `${binary} was stopped after ${timeoutMs / 1000} s (the node's time limit)`, { itemIndex: i });
|
|
166
|
+
}
|
|
167
|
+
results.push({
|
|
168
|
+
json: {
|
|
169
|
+
...items[i].json,
|
|
170
|
+
kavach_run: {
|
|
171
|
+
enforced: false,
|
|
172
|
+
reason,
|
|
173
|
+
stdout: proc.stdout,
|
|
174
|
+
stderr: proc.stderr,
|
|
175
|
+
exit_code: proc.code,
|
|
176
|
+
output_truncated: proc.truncated,
|
|
177
|
+
},
|
|
178
|
+
},
|
|
179
|
+
pairedItem: i,
|
|
180
|
+
});
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
// Linux + kavachos available. It must be one that reads "--": with an older one the words
|
|
184
|
+
// below the separator would still be read as its options, so that is a refusal, not a warning.
|
|
185
|
+
const have = kavachosVersion();
|
|
186
|
+
if (!have || !versionAtLeast(have, KAVACHOS_MIN)) {
|
|
187
|
+
throw new n8n_workflow_1.NodeOperationError(this.getNode(), `kavachos ${have ?? "(version not readable)"} is too old for this node: ${KAVACHOS_MIN} or later is needed ` +
|
|
188
|
+
`(npm install -g @xshieldai/agent-kernel@latest). Not run.`, { itemIndex: i });
|
|
189
|
+
}
|
|
190
|
+
if (/\s/.test(trustMask) || /\s/.test(domain)) {
|
|
191
|
+
throw new n8n_workflow_1.NodeOperationError(this.getNode(), "trust mask and domain must be single words", { itemIndex: i });
|
|
192
|
+
}
|
|
193
|
+
// Everything after "--" is the binary and its arguments, passed on word for word.
|
|
194
|
+
const kavachArgs = [
|
|
195
|
+
"run",
|
|
196
|
+
`--trust-mask=${trustMask}`,
|
|
197
|
+
`--domain=${domain}`,
|
|
198
|
+
...(falco ? ["--falco"] : []),
|
|
199
|
+
"--",
|
|
200
|
+
binary,
|
|
201
|
+
...(args ? args.split(/\s+/) : []),
|
|
202
|
+
];
|
|
203
|
+
const proc = await (0, shared_1.runProcess)("kavachos", kavachArgs, timeoutMs);
|
|
204
|
+
if (proc.error) {
|
|
205
|
+
throw new n8n_workflow_1.NodeOperationError(this.getNode(), `kavachos could not be started: ${proc.error}`, { itemIndex: i });
|
|
206
|
+
}
|
|
207
|
+
if (proc.timedOut) {
|
|
208
|
+
throw new n8n_workflow_1.NodeOperationError(this.getNode(), `the command was stopped after ${timeoutMs / 1000} s (the node's time limit)`, { itemIndex: i });
|
|
209
|
+
}
|
|
210
|
+
// What kavachos itself said about this launch. Through 1.3.0 the node wrote "enforced: true"
|
|
211
|
+
// whenever kavachos had been started, whatever it then did.
|
|
212
|
+
// refused kavachos would not launch the command at all (its exit 3, with its own line)
|
|
213
|
+
// unconstrained it launched, and said the egress gate was NOT in force for this launch
|
|
214
|
+
const refusal = /^\[kavachos\] launch refused: (.*)$/m.exec(proc.stderr);
|
|
215
|
+
if (refusal) {
|
|
216
|
+
throw new n8n_workflow_1.NodeOperationError(this.getNode(), `kavachos refused to launch the command: ${refusal[1]}. Nothing was run.`, { itemIndex: i });
|
|
217
|
+
}
|
|
218
|
+
const egressConstrained = !/NOT egress-constrained|egress_enforced=false/.test(proc.stderr);
|
|
219
|
+
const jailMissing = /path jail .* not loaded/.test(proc.stderr);
|
|
220
|
+
results.push({
|
|
221
|
+
json: {
|
|
222
|
+
...items[i].json,
|
|
223
|
+
kavach_run: {
|
|
224
|
+
// True only when kavachos launched the command and reported no layer missing.
|
|
225
|
+
enforced: egressConstrained && !jailMissing,
|
|
226
|
+
launched_by_kavachos: true,
|
|
227
|
+
egress_constrained: egressConstrained,
|
|
228
|
+
path_jail_loaded: !jailMissing,
|
|
229
|
+
kavachos_version: have,
|
|
230
|
+
trust_mask: trustMask,
|
|
231
|
+
domain,
|
|
232
|
+
falco_enabled: falco,
|
|
233
|
+
stdout: proc.stdout,
|
|
234
|
+
stderr: proc.stderr,
|
|
235
|
+
exit_code: proc.code,
|
|
236
|
+
output_truncated: proc.truncated,
|
|
237
|
+
},
|
|
238
|
+
},
|
|
239
|
+
pairedItem: i,
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
return [results];
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
exports.KavachRun = KavachRun;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64" role="img" aria-label="Kavach Run">
|
|
2
|
+
<path d="M32 4 54 12v18c0 14-9 25-22 30C19 55 10 44 10 30V12z" fill="#1f3a5f"/>
|
|
3
|
+
<path d="M32 9 49 15.5V30c0 11.5-7 20.5-17 24.6C22 50.500 15 41.500 15 30V15.500z" fill="#f4f1e8"/>
|
|
4
|
+
<path d="M26 21v22l18-11z" fill="#c2532d"/><rect x="20" y="17" width="26" height="30" rx="4" fill="none" stroke="#1f3a5f" stroke-width="3"/>
|
|
5
|
+
</svg>
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export interface ProcessResult {
|
|
2
|
+
stdout: string;
|
|
3
|
+
stderr: string;
|
|
4
|
+
/** The exit status, or null when the process was killed or never started. */
|
|
5
|
+
code: number | null;
|
|
6
|
+
timedOut: boolean;
|
|
7
|
+
/** Output beyond the cap was dropped (the process was left to finish). */
|
|
8
|
+
truncated: boolean;
|
|
9
|
+
/** Set when the process could not be started at all. */
|
|
10
|
+
error?: string;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Run a command and wait for it WITHOUT blocking n8n's event loop. Through 1.3.0 the nodes used a blocking
|
|
14
|
+
* call with no time limit: every other workflow, and n8n's own interface, stopped until the command ended.
|
|
15
|
+
*/
|
|
16
|
+
export declare function runProcess(command: string, args: string[], timeoutMs: number, maxBytes?: number): Promise<ProcessResult>;
|
|
17
|
+
export interface JsonReply {
|
|
18
|
+
status: number;
|
|
19
|
+
/** The parsed body, or undefined when it was not JSON. */
|
|
20
|
+
json?: unknown;
|
|
21
|
+
text: string;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* One HTTP request to Aegis with OUR time limit. A redirect is not followed: it comes back as its 3xx status.
|
|
25
|
+
* (The global fetch gives up waiting for an answer after about five minutes whatever the caller wants, which
|
|
26
|
+
* is shorter than the gate's own wait for a level-1 approval.)
|
|
27
|
+
*/
|
|
28
|
+
export declare function requestJson(method: "GET" | "POST", url: string, headers: Record<string, string>, body: string | undefined, timeoutMs: number): Promise<JsonReply>;
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.runProcess = runProcess;
|
|
37
|
+
exports.requestJson = requestJson;
|
|
38
|
+
// SPDX-License-Identifier: AGPL-3.0-only
|
|
39
|
+
// Shared by the Kavach nodes: running a command without stopping n8n, and asking Aegis with a time limit.
|
|
40
|
+
const child_process_1 = require("child_process");
|
|
41
|
+
const http = __importStar(require("http"));
|
|
42
|
+
const https = __importStar(require("https"));
|
|
43
|
+
/**
|
|
44
|
+
* Run a command and wait for it WITHOUT blocking n8n's event loop. Through 1.3.0 the nodes used a blocking
|
|
45
|
+
* call with no time limit: every other workflow, and n8n's own interface, stopped until the command ended.
|
|
46
|
+
*/
|
|
47
|
+
function runProcess(command, args, timeoutMs, maxBytes = 5 * 1024 * 1024) {
|
|
48
|
+
return new Promise((resolve) => {
|
|
49
|
+
let stdout = "", stderr = "", size = 0, truncated = false, timedOut = false, done = false;
|
|
50
|
+
const finish = (r) => { if (done)
|
|
51
|
+
return; done = true; clearTimeout(timer); resolve({ stdout, stderr, code: null, timedOut, truncated, ...r }); };
|
|
52
|
+
let child;
|
|
53
|
+
try {
|
|
54
|
+
child = (0, child_process_1.spawn)(command, args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
55
|
+
}
|
|
56
|
+
catch (e) {
|
|
57
|
+
resolve({ stdout, stderr, code: null, timedOut, truncated, error: e.message });
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
const timer = setTimeout(() => {
|
|
61
|
+
timedOut = true;
|
|
62
|
+
try {
|
|
63
|
+
child.kill("SIGTERM");
|
|
64
|
+
}
|
|
65
|
+
catch { /* already gone */ }
|
|
66
|
+
setTimeout(() => { try {
|
|
67
|
+
child.kill("SIGKILL");
|
|
68
|
+
}
|
|
69
|
+
catch { /* already gone */ } }, 2000).unref();
|
|
70
|
+
}, timeoutMs);
|
|
71
|
+
const take = (which) => (d) => {
|
|
72
|
+
if (size >= maxBytes) {
|
|
73
|
+
truncated = true;
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
const room = maxBytes - size, piece = d.length > room ? d.subarray(0, room) : d;
|
|
77
|
+
if (piece.length < d.length)
|
|
78
|
+
truncated = true;
|
|
79
|
+
size += piece.length;
|
|
80
|
+
if (which === "out")
|
|
81
|
+
stdout += piece.toString("utf8");
|
|
82
|
+
else
|
|
83
|
+
stderr += piece.toString("utf8");
|
|
84
|
+
};
|
|
85
|
+
child.stdout?.on("data", take("out"));
|
|
86
|
+
child.stderr?.on("data", take("err"));
|
|
87
|
+
child.on("error", (e) => finish({ error: e.message }));
|
|
88
|
+
child.on("close", (code) => finish({ code }));
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* One HTTP request to Aegis with OUR time limit. A redirect is not followed: it comes back as its 3xx status.
|
|
93
|
+
* (The global fetch gives up waiting for an answer after about five minutes whatever the caller wants, which
|
|
94
|
+
* is shorter than the gate's own wait for a level-1 approval.)
|
|
95
|
+
*/
|
|
96
|
+
function requestJson(method, url, headers, body, timeoutMs) {
|
|
97
|
+
return new Promise((resolve, reject) => {
|
|
98
|
+
let u;
|
|
99
|
+
try {
|
|
100
|
+
u = new URL(url);
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
reject(new Error(`not a URL: ${url}`));
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
if (u.protocol !== "http:" && u.protocol !== "https:") {
|
|
107
|
+
reject(new Error(`not an http(s) URL: ${url}`));
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
const lib = u.protocol === "https:" ? https : http;
|
|
111
|
+
const h = { ...headers };
|
|
112
|
+
if (body !== undefined)
|
|
113
|
+
h["Content-Length"] = String(Buffer.byteLength(body));
|
|
114
|
+
const req = lib.request(u, { method, headers: h }, (res) => {
|
|
115
|
+
const chunks = [];
|
|
116
|
+
let size = 0;
|
|
117
|
+
res.on("data", (d) => { size += d.length; if (size <= 10 * 1024 * 1024)
|
|
118
|
+
chunks.push(d); });
|
|
119
|
+
res.on("end", () => {
|
|
120
|
+
clearTimeout(timer);
|
|
121
|
+
const text = Buffer.concat(chunks).toString("utf8");
|
|
122
|
+
let json;
|
|
123
|
+
try {
|
|
124
|
+
json = JSON.parse(text);
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
json = undefined;
|
|
128
|
+
}
|
|
129
|
+
resolve({ status: res.statusCode ?? 0, json, text });
|
|
130
|
+
});
|
|
131
|
+
res.on("error", (e) => { clearTimeout(timer); reject(e); });
|
|
132
|
+
});
|
|
133
|
+
const timer = setTimeout(() => { req.destroy(new Error("TIMEOUT")); }, timeoutMs);
|
|
134
|
+
req.on("error", (e) => { clearTimeout(timer); reject(e); });
|
|
135
|
+
if (body !== undefined)
|
|
136
|
+
req.write(body);
|
|
137
|
+
req.end();
|
|
138
|
+
});
|
|
139
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,59 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xshieldai/n8n-nodes-kavachos",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "1.5.0",
|
|
4
|
+
"description": "xShieldAI n8n community nodes โ DAN gate, kernel enforcement, budget check, and audit log for AI agents.",
|
|
5
|
+
"license": "AGPL-3.0-only",
|
|
6
|
+
"author": "Capt. Anil Sharma <capt.anil.sharma@powerpbox.org>",
|
|
7
|
+
"homepage": "https://kavachos.xshieldai.com",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/rocketlang/aegis.git",
|
|
11
|
+
"directory": "packages/n8n-nodes-kavachos"
|
|
12
|
+
},
|
|
13
|
+
"keywords": [
|
|
14
|
+
"n8n-community-node-package",
|
|
15
|
+
"kavachos",
|
|
16
|
+
"xshieldai",
|
|
17
|
+
"ai-governance",
|
|
18
|
+
"seccomp",
|
|
19
|
+
"agent-safety"
|
|
20
|
+
],
|
|
21
|
+
"main": "dist/index.js",
|
|
22
|
+
"types": "dist/index.d.ts",
|
|
23
|
+
"files": [
|
|
24
|
+
"dist/",
|
|
25
|
+
"README.md",
|
|
26
|
+
"LICENSE"
|
|
27
|
+
],
|
|
28
|
+
"n8n": {
|
|
29
|
+
"n8nNodesApiVersion": 1,
|
|
30
|
+
"credentials": [
|
|
31
|
+
"dist/credentials/AegisApi.credentials.js"
|
|
32
|
+
],
|
|
33
|
+
"nodes": [
|
|
34
|
+
"dist/nodes/KavachGate/KavachGate.node.js",
|
|
35
|
+
"dist/nodes/KavachRun/KavachRun.node.js",
|
|
36
|
+
"dist/nodes/KavachBudget/KavachBudget.node.js",
|
|
37
|
+
"dist/nodes/KavachAudit/KavachAudit.node.js"
|
|
38
|
+
]
|
|
39
|
+
},
|
|
40
|
+
"scripts": {
|
|
41
|
+
"build": "tsc && node -e \"const fs=require('fs'),p=require('path');for(const d of fs.readdirSync('nodes',{withFileTypes:true})){if(!d.isDirectory())continue;for(const f of fs.readdirSync(p.join('nodes',d.name))){if(f.endsWith('.svg'))fs.copyFileSync(p.join('nodes',d.name,f),p.join('dist','nodes',d.name,f))}}\"",
|
|
42
|
+
"dev": "tsc --watch",
|
|
43
|
+
"prepublishOnly": "node ../../scripts/publish-guard.mjs && npm run build"
|
|
44
|
+
},
|
|
45
|
+
"devDependencies": {
|
|
46
|
+
"typescript": "^5.0.0",
|
|
47
|
+
"n8n-workflow": "^1.0.0",
|
|
48
|
+
"@types/node": "^20.0.0"
|
|
49
|
+
},
|
|
50
|
+
"peerDependencies": {
|
|
51
|
+
"n8n-workflow": ">=1.0.0"
|
|
52
|
+
},
|
|
53
|
+
"publishConfig": {
|
|
54
|
+
"access": "public"
|
|
55
|
+
},
|
|
56
|
+
"engines": {
|
|
57
|
+
"node": ">=18.0.0"
|
|
58
|
+
}
|
|
59
|
+
}
|