Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 79 additions & 0 deletions examples/agent-frameworks/n8n/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# APort n8n Policy Check Node

Custom n8n community node for checking an agent against an APort policy inside an automation workflow.

## Features

- Calls the APort policy verification endpoint for every incoming item
- Supports n8n expressions for the policy ID, agent ID, and context fields
- Adds the verification result to the item JSON or returns only the APort result
- Provides credential storage for the APort API key and API base URL
- Handles invalid context JSON and failed API calls with item-level n8n errors

## Install Locally

```bash
cd examples/agent-frameworks/n8n
npm test
```

To test the node in a local n8n instance, follow the n8n community-node development workflow and link this package into your custom extensions directory.

## Credentials

Create an **APort API** credential in n8n:

| Field | Description |
| --- | --- |
| API Key | APort API key used for policy verification |
| Base URL | APort API base URL. Defaults to `https://aport.io` |

## Node Configuration

| Field | Description |
| --- | --- |
| Policy ID | APort policy pack identifier, for example `code.repository.merge.v1` |
| Agent ID | Agent passport identifier to verify |
| Context | Optional JSON object sent as the verification context |
| Include Input Data | When enabled, keeps incoming item data and writes the result to `aport` |

## Example Workflow Shape

1. Add any trigger node, such as **Manual Trigger**.
2. Add **APort Policy Check**.
3. Configure the APort credential.
4. Set:

```json
{
"policyId": "code.repository.merge.v1",
"agentId": "agt_123",
"context": {
"repository": "aporthq/aport-integrations",
"action": "merge"
}
}
```

The output includes:

```json
{
"aport": {
"allow": true,
"reasons": [],
"policyId": "code.repository.merge.v1",
"agentId": "agt_123",
"raw": {
"allow": true,
"reasons": []
}
}
}
```

## Verification

```bash
npm test
```
31 changes: 31 additions & 0 deletions examples/agent-frameworks/n8n/credentials/APortApi.credentials.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
class APortApi {
constructor() {
this.name = "aportApi";
this.displayName = "APort API";
this.documentationUrl = "https://aport.io/docs";
this.properties = [
{
displayName: "API Key",
name: "apiKey",
type: "string",
typeOptions: {
password: true,
},
default: "",
required: true,
},
{
displayName: "Base URL",
name: "baseUrl",
type: "string",
default: "https://aport.io",
required: true,
description: "Base URL for the APort API.",
},
];
}
}

module.exports = {
APortApi,
};
5 changes: 5 additions & 0 deletions examples/agent-frameworks/n8n/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
module.exports = {
APortApi: require("./credentials/APortApi.credentials").APortApi,
APortPolicyCheck:
require("./nodes/APortPolicyCheck/APortPolicyCheck.node").APortPolicyCheck,
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
function normalizeBaseUrl(baseUrl) {
return String(baseUrl || "https://aport.io").replace(/\/+$/, "");
}

function parseContext(rawContext) {
const trimmed = String(rawContext || "").trim();

if (!trimmed) {
return {};
}

const parsed = JSON.parse(trimmed);

if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
throw new Error("Context must be a JSON object.");
}

return parsed;
}

class APortPolicyCheck {
constructor() {
this.description = {
displayName: "APort Policy Check",
name: "aportPolicyCheck",
icon: "fa:shield-alt",
group: ["transform"],
version: 1,
description: "Verify an APort agent passport against a policy pack",
defaults: {
name: "APort Policy Check",
},
inputs: ["main"],
outputs: ["main"],
credentials: [
{
name: "aportApi",
required: true,
},
],
properties: [
{
displayName: "Policy ID",
name: "policyId",
type: "string",
default: "code.repository.merge.v1",
required: true,
description: "APort policy pack identifier to evaluate.",
},
{
displayName: "Agent ID",
name: "agentId",
type: "string",
default: "",
required: true,
placeholder: "agt_123",
description: "Agent passport identifier to verify.",
},
{
displayName: "Context",
name: "context",
type: "json",
default: "{}",
description: "JSON object sent as the APort verification context.",
},
{
displayName: "Include Input Data",
name: "includeInputData",
type: "boolean",
default: true,
description:
"Whether to keep the incoming item JSON and attach the APort result under the aport key.",
},
],
};
}

async execute() {
const items = this.getInputData();
const credentials = await this.getCredentials("aportApi");
const baseUrl = normalizeBaseUrl(credentials.baseUrl);
const returnData = [];

for (let itemIndex = 0; itemIndex < items.length; itemIndex += 1) {
try {
const policyId = this.getNodeParameter("policyId", itemIndex);
const agentId = this.getNodeParameter("agentId", itemIndex);
const context = parseContext(
this.getNodeParameter("context", itemIndex, "{}"),
);
const includeInputData = this.getNodeParameter(
"includeInputData",
itemIndex,
true,
);

const response = await this.helpers.httpRequest({
method: "POST",
url: `${baseUrl}/api/verify/policy/${encodeURIComponent(policyId)}`,
headers: {
Accept: "application/json",
Authorization: `Bearer ${credentials.apiKey}`,
"Content-Type": "application/json",
},
body: {
agent_id: agentId,
context,
policy_id: policyId,
},
json: true,
});

const aportResult = {
allow: Boolean(response.allow ?? response.allowed),
reasons: Array.isArray(response.reasons) ? response.reasons : [],
policyId,
agentId,
raw: response,
};

returnData.push({
json: includeInputData
? {
...items[itemIndex].json,
aport: aportResult,
}
: aportResult,
pairedItem: {
item: itemIndex,
},
});
} catch (error) {
if (this.continueOnFail()) {
returnData.push({
json: {
error: error.message,
},
pairedItem: {
item: itemIndex,
},
});
continue;
}

throw error;
}
}

return [returnData];
}
}

module.exports = {
APortPolicyCheck,
normalizeBaseUrl,
parseContext,
};
33 changes: 33 additions & 0 deletions examples/agent-frameworks/n8n/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
{
"name": "n8n-nodes-aport-policy-check",
"version": "0.1.0",
"description": "n8n community node for APort policy verification.",
"license": "MIT",
"author": "APort community",
"keywords": [
"n8n-community-node-package",
"n8n",
"aport",
"policy",
"verification"
],
"main": "index.js",
"files": [
"credentials",
"nodes",
"index.js",
"README.md"
],
"scripts": {
"smoke": "node test/smoke.js",
"test": "npm run smoke"
},
"n8n": {
"credentials": [
"credentials/APortApi.credentials.js"
],
"nodes": [
"nodes/APortPolicyCheck/APortPolicyCheck.node.js"
]
}
}
33 changes: 33 additions & 0 deletions examples/agent-frameworks/n8n/test/smoke.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
const assert = require("node:assert/strict");

const { APortApi } = require("../credentials/APortApi.credentials");
const {
APortPolicyCheck,
normalizeBaseUrl,
parseContext,
} = require("../nodes/APortPolicyCheck/APortPolicyCheck.node");

const credentials = new APortApi();
const node = new APortPolicyCheck();

assert.equal(credentials.name, "aportApi");
assert.equal(credentials.properties.some((property) => property.name === "apiKey"), true);
assert.equal(credentials.properties.some((property) => property.name === "baseUrl"), true);

assert.equal(node.description.name, "aportPolicyCheck");
assert.equal(node.description.credentials[0].name, "aportApi");
assert.deepEqual(node.description.inputs, ["main"]);
assert.deepEqual(node.description.outputs, ["main"]);
assert.equal(
node.description.properties.some((property) => property.name === "policyId"),
true,
);
assert.equal(
node.description.properties.some((property) => property.name === "agentId"),
true,
);
assert.deepEqual(parseContext('{"action":"merge"}'), { action: "merge" });
assert.equal(normalizeBaseUrl("https://aport.io/"), "https://aport.io");
assert.throws(() => parseContext("[]"), /JSON object/);

console.log("APort n8n node smoke test passed");