Connect your AI Node
Step by step: get your credentials, run your AI Node, and connect it to n8n, Make, Zapier, LangChain, MCP clients, GitHub Actions or your own app.
The Vrymdor AI Node is a checkpoint that runs inside your own network, in front of your AI agents and the tools they use. n8n, Make, Zapier, LangChain, LlamaIndex, Claude Desktop, IDEs (integrated development environments) and your own code all call the node the same way, and every call gets the same protection:
- The agent's identity and status are checked (a stopped or expired agent is refused).
- Your policy decides whether the action is allowed for the agent's role.
- PII (personally identifiable information, such as email addresses and card numbers) and secrets (such as API keys, where API means application programming interface) are replaced with
[REDACTED:<TYPE>]before anything leaves, and again on the way back. - The request is signed and a metadata-only audit record is written. Prompts and payloads stay in your network.
The node never runs code it is sent. It runs one allowed action (remove PII, run a tool, call a model or send a webhook) against a destination you configured.
This guide takes you from a new account to a first governed call on each platform. Every command and response below matches the current product.
Before you start
You need an AI Node account. Create one at https://app.vrymdor.com/node/start. Community is free; Pro and Business are paid monthly; Enterprise is a custom contract. After you sign up you get an activation email: open the link, sign the service agreements, and set your password. Your workspace is then at https://app.vrymdor.com/app.
| Community | Pro | Business | Enterprise | |
|---|---|---|---|---|
| Price | $0 | $29 a month | $99 a month | Custom annual contract |
| Active agents | up to 2 | up to 15 | up to 50 | Custom |
| Requests included each month | 10,000 | 100,000 | 500,000 | Custom |
| Past the included requests | Blocked until the next month | $0.001 each | $0.0005 each | Custom |
| Custom redaction patterns, response filtering, audit export, priority routing | Not included | Included | Included | Included |
| Custom policy sections, admin roles, advanced telemetry | Not included | Not included | Included | Included |
Community is a hard cap: at 10,000 requests in a calendar month (UTC, Coordinated Universal Time) the node answers with HTTP (Hypertext Transfer Protocol) status 402 and the code quota_exhausted until the month resets or you upgrade. Paid plans are never blocked for volume. One request is one call the node evaluated, whatever the decision; calls refused for a bad credential, a plan limit or a rate limit do not count.
You also need Docker (for the Docker and Docker Compose options), or a Kubernetes cluster with Helm 3 or Helm 4 (for the Helm option), on a machine inside your network.
Step 1: get your credentials
There are two kinds of credential. Keep both like passwords.
| Credential | Looks like | What uses it | Where you get it |
|---|---|---|---|
| Node token | vrym_live_ and 48 characters | The node itself, so it can register with your workspace and sync your agents and policy | Home base, Connect your node, Show my node token |
| Agent ID and Agent Secret | agt_... and vrym_agent_... | Each platform or program that calls the node (n8n, Make, Zapier, your code, and MCP clients, where MCP means Model Context Protocol) | AI agents, Register an agent |
The gateway URL (uniform resource locator, the web address) that platforms use is your node's own address as that platform can reach it, for example http://vrymdor-node:8080 inside Docker or https://ai-gateway.example.com behind your reverse proxy. The node listens on port 8080.
Get your node token
- Sign in at https://app.vrymdor.com/app. AI Node accounts open on the Home base tab.
- Find the Connect your node card. Its badge reads Waiting for node until a node reports in.
- Select Show my node token. A workspace admin can do this. The token is filled into the Docker, Docker Compose and Helm commands on the card, each with a Copy button (copy buttons work in the web app; in the phone app, select the text). The card's MCP entry and its n8n, Make and Zapier entry never use the node token: they take an agent's credentials (next section).
The card also shows your Workspace id. Only an air-gapped node needs it (Option D); a connected node looks it up by itself.
Register an agent for each platform
- Open the AI agents tab.
- In Register an agent, fill in Name (for example
n8n production), Role (lowercase, as in your policy) (for exampledevops), Owner email (the person accountable for the agent) and Valid for (days) (90 or less). - Select Register. The app shows the agent id and its secret once: "Agent agt.... Its secret, shown only now: vrymagent_.... Put it in the agent's configuration; Vrymdor keeps only a hash of it." Copy both now.
Use one agent per platform connection. Stopping an agent on the AI agents tab ends every token it holds at once, without touching your other agents.
Step 2: run your node
Pick one option. In every option the node registers with your workspace on start, and the Connect your node badge changes from Waiting for node to Connected and active within about a minute (the node reports every 60 seconds). If reports stop for three minutes the badge reads Node offline.
Not yet on Docker Hub. The imagevrymdor/node-proxyis being published. Until it is, Vrymnox support provides it as a file,vrymdor-node-proxy.tar, with its checksum filevrymdor-node-proxy.tar.sha256. On the machine that runs the node, check and load it with the two commands below (on macOS, check it withshasum -a 256 -c vrymdor-node-proxy.tar.sha256). The file carries the image under two tags,vrymdor/node-proxy:1.0.0(what Docker Compose and the Helm chart run) andvrymdor/node-proxy:latest(what thedocker runcommands below run), so every option finds it. (Remove this note and the two commands below it once the image is on Docker Hub.)
sha256sum -c vrymdor-node-proxy.tar.sha256 # prints: vrymdor-node-proxy.tar: OK
docker load -i vrymdor-node-proxy.tarOption A: Docker
- Start the node with your node token:
docker run -d --name vrymdor-node -p 127.0.0.1:8080:8080 \
-v vrymdor-node-data:/data \
-e VRYMDOR_NODE_TOKEN=vrym_live_... \
vrymdor/node-proxy:latest VRYMDOR_NODE_TOKEN is the only required setting. The node then contacts https://api.vrymdor.com (outbound only, over HTTPS, the encrypted form of HTTP; nothing needs to reach the node from the internet). The vrymdor-node-data volume keeps the node's signing key, admin token and audit across restarts.
-p 127.0.0.1:8080:8080 makes the node reachable only from this machine, which is the safe choice for a first test. When other machines, or other containers such as n8n or the MCP bridge on Linux, must reach it, use -p 8080:8080 instead, and limit who can reach port 8080 with your firewall.
Because of -d the container runs in the background, so its messages, including the Activate License message described below, are in docker logs vrymdor-node.
- Check it is healthy:
curl -s http://127.0.0.1:8080/healthz A ready node answers {"status":"ready","checks":{"redaction_engine":true,"token_key":true,"signing_key":true,"state_store":true,"audit_writable":true},...,"fail_mode":"FAIL_CLOSED"}. docker ps also shows the container as healthy after its first health check.
- Watch the Connect your node card switch to Connected and active.
- Optional: tell the node where agents may go. Without this, only Remove PII and secrets and the policy checks work. Add the destinations as JSON (JavaScript Object Notation) in
VRYMNOX_UPSTREAMS, with the API keys in their own variables (the node holds the keys; platforms and agents never see them). Remove the first container first (thevrymdor-node-datavolume keeps the keys), then start it again with the new settings:
docker rm -f vrymdor-node
docker run -d --name vrymdor-node -p 127.0.0.1:8080:8080 \
-v vrymdor-node-data:/data \
-e VRYMDOR_NODE_TOKEN=vrym_live_... \
-e VRYMNOX_UPSTREAMS='{"anthropic":{"url":"https://api.anthropic.com","kind":"llm","auth_header":"x-api-key","auth_env":"ANTHROPIC_API_KEY"},"github":{"url":"https://mcp.example.com/github","kind":"mcp"},"hooks":{"url":"https://hooks.example.com/vrymdor","kind":"webhook"}}' \
-e ANTHROPIC_API_KEY=sk-ant-... \
vrymdor/node-proxy:latest kind is llm (a model API), mcp (an MCP server), webhook, websocket or grpc. The name before each object (anthropic, github, hooks) is the Upstream you enter in each platform.
- Find the admin token for the node's local admin API (used in Step 3):
docker exec vrymdor-node cat /data/admin.tokenWhat the Activate License message means. If the container starts without a node token, it prints the message below and stops with exit code 2 (with -d, read it with docker logs vrymdor-node). Follow the link to create a free Community account, then copy your token from Connect your node, remove the stopped container with docker rm vrymdor-node, and run the command again.
Vrymdor AI Node: no node token yet.
Activate License: https://app.vrymdor.com/node/start?plan=node_communityOption B: Docker Compose
Use this to run the node with its own Redis (needed for more than one node replica) on one host. The package folder holds docker-compose.yml and .env.example.
Not yet published. Until the image is on Docker Hub, support provides the package folder and the image file. Load the image first (see the note at the start of Step 2). The compose file runsvrymdor/node-proxy:1.0.0unless.envsetsVRYMNOX_GATEWAY_VERSION; without that image on the machine,docker compose up -dstops with "failed to read dockerfile" (the package folder has no Dockerfile to build from). If your image file has only thelatesttag, either tag it withdocker tag vrymdor/node-proxy:latest vrymdor/node-proxy:1.0.0, or putVRYMNOX_GATEWAY_VERSION=latestin.env. (Remove this note after publishing.)
- In the package folder, copy the settings file and open it:
cp .env.example .env
nano .env- Set your token. This is the only line you must fill in:
VRYMDOR_NODE_TOKEN=vrym_live_... Optional lines in the same file: VRYMNOX_CONTROL_PLANE_URL (default https://api.vrymdor.com), VRYMNOX_GATEWAY_VERSION (the image tag, default 1.0.0), VRYMNOX_UPSTREAMS and the API keys it names (see Option A, step 4), and VRYMNOX_AUDIT_RETENTION_DAYS (183 in the example, which meets the EU (European Union) AI Act six-month log duty for high-risk deployers). Leave VRYMNOX_SIGNING_KEY and VRYMNOX_ADMIN_TOKEN unset and the node generates them into its data volume.
- Start it:
docker compose up -d- Check it is ready:
docker compose exec gateway python -c "import urllib.request;print(urllib.request.urlopen('http://127.0.0.1:8080/healthz').read().decode())"- Find the admin token for the node's local admin API (used in Step 3):
docker compose exec gateway cat /data/admin.tokenThe compose file keeps the node on an internal Docker network named vrymnox-gateway_agents and does not publish a port, so only containers on that network can call it, at http://gateway:8080. To reach it from the host or other machines, add a docker-compose.override.yml beside it (127.0.0.1:8080:8080 keeps it to the host; 8080:8080 opens it to other machines, as in Option A):
services:
gateway:
ports: ["127.0.0.1:8080:8080"]Option C: Helm, Kubernetes or OpenShift
The chart is vrymdor-node. It runs the node as a non-root user (user id 10002) with a read-only root filesystem, every Linux capability dropped, liveness and readiness probes on /healthz, and a Secret that holds your node token.
Not yet in a public chart repository. Until it is, support provides the chart as a folder (vrymdor-node) or a file (vrymdor-node-1.0.0.tgz); use that path in place of./vrymdor-nodebelow. The chart runs the imagevrymdor/node-proxy:1.0.0, which is not on Docker Hub yet either, so give your cluster the image from the support file (see the note at the start of Step 2) in one of two ways: - Push it to a registry your cluster can pull from and point the chart at it:docker tag vrymdor/node-proxy:1.0.0 registry.example.com/vrymdor/node-proxy:1.0.0,docker push registry.example.com/vrymdor/node-proxy:1.0.0, then add--set image.repository=registry.example.com/vrymdor/node-proxyto thehelm installcommand. - Or import it on every Kubernetes node that may run the pod:sudo ctr -n k8s.io images import vrymdor-node-proxy.tar(containerd; on k3s,sudo k3s ctr images import vrymdor-node-proxy.tar), or, on a kind test cluster, afterdocker load,kind load docker-image vrymdor/node-proxy:1.0.0. (Remove this note after the chart and the image are published.)
- Install with your node token:
helm install vrymdor-node ./vrymdor-node --set nodeKey=vrym_live_...To keep the token out of your shell history, create the Secret yourself and point the chart at it:
kubectl create secret generic vrymdor-node-token --from-literal=VRYMDOR_NODE_TOKEN=vrym_live_...
helm install vrymdor-node ./vrymdor-node --set existingSecret=vrymdor-node-token- Verify. The
port-forwardcommand keeps running and holds the terminal, so runcurlin a second terminal:
kubectl get pods -l app.kubernetes.io/name=vrymdor-node
kubectl port-forward svc/vrymdor-node 8080:8080curl -s http://127.0.0.1:8080/healthz # in a second terminal Stop the port forward with Ctrl+C when you are done. Inside the cluster, platforms call http://vrymdor-node.<namespace>.svc:8080. The admin token for Step 3 is in the pod: kubectl exec deploy/vrymdor-node -- cat /data/admin.token.
The values you can set:
| Value | Default | What it does |
|---|---|---|
nodeKey | empty | Your node token. Required unless existingSecret or airGapped is set |
existingSecret, existingSecretKey | empty, VRYMDOR_NODE_TOKEN | Use a Secret you manage |
image.repository, image.tag | vrymdor/node-proxy, the chart's app version (1.0.0) | Where the image comes from |
controlPlaneUrl | https://api.vrymdor.com | Where the node syncs agents and policy |
upstreams | empty | The VRYMNOX_UPSTREAMS JSON (Option A, step 4) |
extraEnvFrom | None | For example a secretRef to a Secret holding the upstream API keys |
policy | empty | A vrymnox-policy.yaml, mounted read-only and loaded at start |
replicaCount, redisUrl | 1, empty | More than one replica needs a shared Redis at redisUrl |
persistence.enabled | false | true keeps /data on a PersistentVolumeClaim instead of an emptyDir |
ingress.enabled | false | An optional Ingress; the node needs no inbound route from the internet |
openshift | false | true leaves the user and group ids to OpenShift's restricted SCC (Security Context Constraints) |
airGapped, tenantId, airGappedAgents, airGappedAgentsSecret | Off | Air-gapped mode (Option D) |
resources | 100m CPU (central processing unit) and 256Mi requested, 1 CPU and 512Mi limit | Container resources |
An install that cannot start is refused with the fix in the message, for example "set nodeKey (your node token from Connect your node in the Vrymdor app) or existingSecret".
Option D: air-gapped (Enterprise)
Air-gapped mode is for a network with no route to the internet. Set VRYMDOR_AIR_GAPPED=1 and the node never contacts Vrymdor: no registry sync, no reports, no telemetry. Because it cannot fetch your agents, it reads them from a file. The Connect your node card stays at Waiting for node for an air-gapped node, because it never reports.
An Enterprise air-gapped install is arranged with Vrymnox. Vrymnox support provides the node image as the file vrymdor-node-proxy.tar with its checksum file vrymdor-node-proxy.tar.sha256, on a USB (Universal Serial Bus) drive, and prepares the agents file with you; the agent secrets themselves are never on the drive. On the server:
- Check and load the image from the drive:
sha256sum -c vrymdor-node-proxy.tar.sha256 # prints: vrymdor-node-proxy.tar: OK
docker load -i vrymdor-node-proxy.tar # loads vrymdor/node-proxy:1.0.0 and :latest- Put the agents file and your policy in a folder, for example
/opt/vrymdor/node. The agents file is a JSON list; each agent hasagent_id,name,role,owner_email,expires_at(a date and time in ISO 8601, the international standard format, for example2027-01-31T00:00:00Z) andsecret_sha256(the hex digest of the agent secret under SHA-256, the Secure Hash Algorithm with a 256-bit digest). - Start the node.
VRYMNOX_TENANT_IDis your Workspace id, shown on the Connect your node card (Vrymnox support also has it):
docker run -d --name vrymdor-node -p 127.0.0.1:8080:8080 -v vrymdor-node-data:/data \
-v /opt/vrymdor/node:/etc/vrymdor:ro \
-e VRYMDOR_AIR_GAPPED=1 \
-e VRYMNOX_TENANT_ID=<your workspace id> \
-e VRYMNOX_AGENTS_FILE=/etc/vrymdor/agents.json \
-e VRYMNOX_POLICY_FILE=/etc/vrymdor/vrymnox-policy.yaml \
vrymdor/node-proxy:latest- Check
curl -s http://127.0.0.1:8080/healthzanswers"status":"ready". As in Option A, use-p 8080:8080when other machines must reach the node.
Starting air-gapped without a tenant id is refused. With a node token set, docker logs vrymdor-node shows "Vrymnox gateway cannot start: air-gapped mode (VRYMDORAIRGAPPED) needs VRYMNOXTENANTID, and the agents in VRYMNOXAGENTSFILE, because the gateway never talks to the control plane". With neither a token nor a tenant id, the node shows the Activate License message (Option A) instead, because it checks for those first.
With Helm, set airGapped=true, tenantId and the agents. A JSON list cannot go through --set (Helm splits the value at its commas), so pass the agents file with --set-file, or name a Secret you manage that holds it under the key agents.json:
helm install vrymdor-node ./vrymdor-node --set airGapped=true --set tenantId=<your workspace id> \
--set-file airGappedAgents=agents.json
# or keep the agents in a Secret you manage:
kubectl create secret generic vrymdor-node-agents --from-file=agents.json
helm install vrymdor-node ./vrymdor-node --set airGapped=true --set tenantId=<your workspace id> \
--set airGappedAgentsSecret=vrymdor-node-agentsThe chart refuses an air-gapped install without a tenant id ("airGapped needs tenantId (the node never asks the cloud which tenant it serves)") or without agents ("airGapped needs airGappedAgents (a JSON list) or airGappedAgentsSecret").
The separate offline USB kit for Vrymdor Business Security servers is described in the onboarding guide; it installs the security connector, not the AI Node.
Step 3: set your policy
The policy decides which roles may use which upstreams and tools, and which tools wait for a person. Set it before you connect platforms that run tools: a tool call is refused with tool_not_allowed until the policy lists the tool for the agent's role. Removing PII works without a policy, and so do model calls and webhooks until a role limits its upstreams (a role with an upstreams list reaches only those).
- Start from the example in the package,
vrymnox-policy.example.yaml, and keep your copy asvrymnox-policy.yamlin your own repository. A small policy:
version: 1
roles:
devops:
upstreams: [github, hooks, anthropic] # the upstreams this role may reach
mcp:
tools:
- name: create_issue
upstream: github
roles: [devops]
input_schema: # the tool's arguments are checked against this
type: object
required: [title]
properties:
title: {type: string, maxLength: 200}
- name: delete_branch
upstream: github
roles: [devops]
require_approval: true # every call waits for a person The example file also shows redaction (your own patterns and literal terms), injection (flag or block prompt injection markers) and defaults (rate limits and token lifetime).
- Check it without starting anything:
docker run --rm -v "$PWD":/w:ro vrymdor/node-proxy:latest policy-check /w/vrymnox-policy.yaml A valid file prints Policy valid. sha256 <hex>, 2 tool(s), 14 detector(s).; an invalid one prints Policy rejected: <reason> and exits 1, so you can run it in CI (continuous integration).
- Apply it to the node with the admin token from Step 2. On the machine that runs the node with Option A:
GW=http://127.0.0.1:8080
ADMIN_TOKEN=$(docker exec vrymdor-node cat /data/admin.token)
curl -X POST --data-binary @vrymnox-policy.yaml -H "Authorization: Bearer $ADMIN_TOKEN" \
-H 'Content-Type: application/yaml' "$GW/api/v1/policies/import" With Option B or C, get the admin token as shown there, and set GW to an address where you reach the node. The answer is {"status":"applied","policy_sha256":"<hex>","tools":2}. The node saves the policy in its data volume, so it also survives a restart.
If your workspace also stores a policy, the workspace policy wins at the next full sync. The node asks for your registry every 5 seconds, and while nothing changed the answer is a short "not modified" (HTTP 304), so a policy you imported locally stays in force until your workspace registry changes (an agent is registered, stopped, released or extended, the plan changes, or the workspace policy changes) or the node restarts. Then the workspace policy replaces it. Use one place for your policy.
Holds and approvals. A tool with require_approval: true answers HTTP 202 with "decision":"hold" and a hold_id. The person approving sees the agent's stated intent beside the exact request. The agent states it with the X-Vrymnox-Intent header (up to 500 characters) or, in a node request, as "context": {"intent": "..."} (up to 200 characters; the header wins when both are sent). From the Python client: node.tool("github", "delete_branch", {...}, context={"intent": "..."}). Approve or reject on the node, inside your network, so the request never leaves it:
curl -s -H "Authorization: Bearer $ADMIN_TOKEN" "$GW/api/v1/holds"
curl -s -X POST -H "Authorization: Bearer $ADMIN_TOKEN" "$GW/api/v1/holds/<hold_id>/approve" # or /rejectThe caller then gets the result from GET $GW/v1/node/holds/<hold_id> (202 while waiting; the final answer once, then it is forgotten). A rejected hold answers with the code rejected_by_reviewer; a hold nobody decides is rejected after hold_timeout_seconds (900 by default) with approval_expired. The AI agents tab shows how many requests are waiting for approval in total across your nodes.
Where decisions show up. Every decision is in the node's hash-chained audit (in /data/audit). The AI agents tab shows each node's report (counts and readiness, never payloads) and every agent's state, and Home base shows this month's requests against your plan.
Step 4: connect your platform
Every platform below needs three values from Step 1: the gateway URL (your node), an Agent ID and an Agent Secret. The examples use an agent with the role devops, an upstream named github that offers the tool create_issue, an upstream named anthropic and a webhook upstream named hooks, as set in Step 2 (Option A, step 4) and Step 3. Replace them with the names in your node's settings and policy. Every tool example needs the tool in your policy (Step 3); without it the call is refused with tool_not_allowed.
n8n
The community node is n8n-nodes-vrymnox. It adds the credential Vrymnox Agent API and the node Vrymnox, which n8n also offers as an AI agent tool. For the AI agent tool, start the n8n instance with N8N_COMMUNITY_PACKAGES_ALLOW_TOOL_USAGE=true; n8n offers community nodes as tools only with that setting.
Not yet on npm. Until it is, self-hosted n8n can load the package file from support: unpackn8n-nodes-vrymnox-1.0.0.tgzinto a folder, rename the unpackedpackagefolder ton8n-nodes-vrymnox, and start n8n withN8N_CUSTOM_EXTENSIONSset to the folder that contains it. (Remove this note once the package is on npm.)
- In n8n, open Settings, Community Nodes, Install, enter
n8n-nodes-vrymnox, accept the risk notice and install. - Create the credential: Credentials, Add credential, search Vrymnox Agent API, and fill in:
- Gateway URL: your node as n8n reaches it, for example
http://vrymdor-node:8080(defaulthttp://vrymnox-gateway:8080). - Agent ID:
agt_... - Agent Secret:
vrym_agent_...
Save. n8n tests the credential by exchanging it at your node's /v1/token. n8n then keeps the short-lived access token from /v1/token in the credential and exchanges the secret again whenever the node answers that the token expired, so you never handle tokens yourself.
- Add the Vrymnox node to a workflow and choose its credential.
- Choose an Operation. The fields shown depend on it:
| Operation | Fields | |
|---|---|---|
| Remove PII and Secrets | Input | |
| Run a Tool | Upstream, Tool, Input (the tool's arguments), Wait for Approval, Wait Up To (Minutes) | |
| Call a Model | Upstream, API Path (default v1/messages), Input (the model request body), Wait for Approval, Wait Up To (Minutes) | |
| Send a Webhook | Upstream, Input (the payload), Wait for Approval, Wait Up To (Minutes) | |
| Check Only (Dry Run) | Action Type (Tool, Model or Webhook), Upstream, Tool (for Tool), Input |
Input defaults to the incoming item ({{ $json }}); it also accepts JSON text.
- First test: set Operation to Remove PII and Secrets and Input to
{"note": "mail [email protected], card 4111 1111 1111 1111"}, then select Execute step. The output item has"ok": true,"decision": "allow"and"output": {"note": "mail [REDACTED:EMAIL], card [REDACTED:CREDIT_CARD]"}.
How it behaves:
- Wait for Approval (on by default, 1 to 60 minutes, default 15): when a tool needs a person's approval, the node checks every 5 seconds until it is decided or the time runs out. Off, the step returns at once with
"decision": "hold"and ahold_id. - A refusal stops the workflow with an error such as
Vrymnox tool_not_allowed: Tool drop_db is not on the allowlist.In the node's Settings, set On Error to Continue to get the refusal as an item with anerrorfield on the normal output instead, or to Continue (using error output) to send that item to the node's separate error output. - Each step sends an idempotency key made from the execution, node and item, so an n8n retry never runs an action twice.
Make
The Make custom app has one connection and one module, Run a governed action.
Not yet public in Make. Until Make approves the listing, Vrymnox can invite your Make organization to the app. (Remove this note once the app is public.)
- In a scenario, add the Vrymnox AI Node app and the module Run a governed action.
- Create a connection with:
- Gateway URL: your node as Make's cloud can reach it, with no trailing slash, for example
https://ai-gateway.example.com. Make runs in the cloud, so the node must be reachable over HTTPS from the internet (for example behind your reverse proxy). - Agent ID:
agt_... - Agent secret:
vrym_agent_...
A wrong credential shows "The gateway refused this agent's credential."
- Fill in the module:
| Field | Value | |
|---|---|---|
| Action | Remove PII and secrets, Run a tool, Call a model, or Send a webhook | |
| Upstream | A name configured on your node, for example github. Not needed to remove PII | |
| Tool | For Run a tool only, for example create_issue (it must be in your policy, Step 3) | |
| API path (models) | For Call a model only, default v1/messages | |
| Input | The data to clean, the tool arguments, the model request body or the webhook payload | |
| Check only (dry run) | Yes runs every check and forwards nothing |
- First test: Action Remove PII and secrets, Input
mail [email protected], then Run once. The output showsdecisionallow andoutputmail [REDACTED:EMAIL].
The module's outputs are ok, decision, output, redactions, rules, hold_id, error and audit. A refusal shows as an error such as Vrymnox tool_not_allowed: .... A held action returns decision hold and a hold_id; the module does not wait.
Zapier (Webhooks by Zapier)
Zapier uses two Webhooks by Zapier steps: one exchanges the agent secret for a short-lived access token, the next sends the action. The node must be reachable over HTTPS from the internet.
- Add a step: Webhooks by Zapier, event POST.
- URL:
https://ai-gateway.example.com/v1/token - Payload Type:
json - Data:
agent_id=agt_...andagent_secret=vrym_agent_...
Test it. The result has access_token, token_type (Bearer) and expires_in (600 seconds by default).
- Add a second step: Webhooks by Zapier, event Custom Request.
- Method:
POST - URL:
https://ai-gateway.example.com/v1/node/run - Headers:
Authorization=Bearerfollowed by the Access Token from step 1, andContent-Type=application/json - Data:
{"action": {"type": "sanitize"}, "input": "Customer [email protected] paid with 4111 1111 1111 1111",
"context": {"platform": "zapier", "workflow": "zap-42"}} Map fields from earlier steps into input. A mapped value inside quotes must not contain an unescaped double quote; for free text, a Code by Zapier step that builds the JSON is safer.
- Test it. The answer has
"decision": "allow"and"output": "Customer [REDACTED:EMAIL] paid with [REDACTED:CREDIT_CARD]". Mapoutputinto the next step.
For a tool that your policy lists (Step 3), change Data to {"action": {"type": "tool", "upstream": "github", "tool": "create_issue"}, "input": {"title": "..."}}.
LangChain, LlamaIndex and Python
The Python client is the package vrymdor-node (module vrymnox_node, one dependency: httpx). The LangChain extra needs langchain-core 0.3.49 or newer, the first version that accepts the tools' JSON Schemas as they are.
Not yet on PyPI. Until it is, install the wheel file from support: pip install vrymdor_node-1.0.0-py3-none-any.whl. (Remove this note once the package is on PyPI.)- Install:
pip install vrymdor-node # the client
pip install "vrymdor-node[langchain]" # plus LangChain
pip install "vrymdor-node[llamaindex]" # plus LlamaIndex- Call the node.
sanitize,llmandwebhookwork before you set a policy;toolneeds the tool in your policy (Step 3):
from vrymnox_node import VrymnoxError, VrymnoxHeld, VrymnoxNode
node = VrymnoxNode("http://vrymdor-node:8080", agent_id="agt_...", agent_secret="vrym_agent_...")
print(node.sanitize("Customer [email protected] called")) # Customer [REDACTED:EMAIL] called
node.tool("github", "create_issue", {"title": "Login fails"})
node.llm("anthropic", "v1/messages", {"model": "claude-...", "max_tokens": 200,
"messages": [{"role": "user", "content": "Hello"}]})
node.webhook("hooks", {"customer": "[email protected]"})
try:
node.tool("github", "drop_db", {})
except VrymnoxHeld as held:
print(node.wait_for(held.hold_id)) # waits until a person decides (default up to 15 minutes)
except VrymnoxError as err:
print(err.code, err.retryable) # tool_not_allowed False- Give an agent framework only the tools your policy allows:
tools = node.langchain_tools() # one LangChain StructuredTool per allowed tool, named github__create_issue
tools = node.llamaindex_tools() # the same, as LlamaIndex FunctionTools with each tool's input schemaThe client exchanges the secret for a token and renews it a minute before it expires. Every call is governed like any other: a tool the role may not use is not in the list at all.
Claude Desktop, Cursor, VS Code and other MCP clients
The node is an MCP server. Its tool list shows vrymnox_sanitize, vrymnox_check (dry run), vrymnox_hold_status, vrymnox_llm (when a model upstream is configured) and every tool your policy allows for the agent's role, named <upstream>__<tool>, for example github__create_issue.
What the node protects here. Text you type into Claude Desktop or an IDE chat goes to that client's model provider before any tool runs, so the node cannot remove PII from your prompt. It protects what the tools send and receive: the arguments a tool call carries and the answer that comes back. Try it with test data, for example "use vrymnox_sanitize on: mail [email protected]", never with real personal data.
Desktop clients and IDEs start a local command, so they use the stdio (standard input and output) bridge, which runs from the node image and renews the agent's token by itself. It needs Docker on the computer running the client. Use http://host.docker.internal:8080 when the node runs on the same computer (Docker Desktop on macOS and Windows), or your node's address otherwise. With Docker Engine on Linux, host.docker.internal is not defined: add "--add-host=host.docker.internal:host-gateway" to args before the image name, and publish the node's port on an address the containers can reach (-p 8080:8080 does; -p 127.0.0.1:8080:8080 does not).
Claude Desktop. Open Settings, Developer, Edit Config, or edit the file directly: ~/Library/Application Support/Claude/claude_desktop_config.json on macOS, and %APPDATA%\Claude\claude_desktop_config.json on Windows. Add:
{
"mcpServers": {
"vrymdor": {
"command": "docker",
"args": ["run", "-i", "--rm",
"-e", "VRYMNOX_NODE_URL", "-e", "VRYMNOX_AGENT_ID", "-e", "VRYMNOX_AGENT_SECRET",
"vrymdor/node-proxy:latest", "mcp-stdio"],
"env": {
"VRYMNOX_NODE_URL": "http://host.docker.internal:8080",
"VRYMNOX_AGENT_ID": "agt_...",
"VRYMNOX_AGENT_SECRET": "vrym_agent_..."
}
}
}
}Restart Claude Desktop. The Vrymdor tools appear in the tools menu. Ask it to "use vrymnox_sanitize on: mail [email protected]" and the tool answers "mail [REDACTED:EMAIL]" (the output as JSON, so a text answer comes back in quotes).
Cursor. Put the same mcpServers block in ~/.cursor/mcp.json (all projects) or .cursor/mcp.json in a project.
VS Code. In .vscode/mcp.json, the key is servers and each server names its type:
{
"servers": {
"vrymdor": {
"type": "stdio",
"command": "docker",
"args": ["run", "-i", "--rm", "-e", "VRYMNOX_NODE_URL", "-e", "VRYMNOX_AGENT_ID", "-e", "VRYMNOX_AGENT_SECRET",
"vrymdor/node-proxy:latest", "mcp-stdio"],
"env": {"VRYMNOX_NODE_URL": "http://host.docker.internal:8080", "VRYMNOX_AGENT_ID": "agt_...",
"VRYMNOX_AGENT_SECRET": "vrym_agent_..."}
}
}
}If Python can import the node's package on that computer (for example on the node's own server), the command can be python with the arguments -m vrymnox_gateway mcp-stdio and the same three variables.
MCP over HTTP. POST /mcp speaks MCP over streamable HTTP (JSON responses; protocol versions 2025-11-25, 2025-06-18, 2025-03-26 and 2024-11-05). Every request needs Authorization: Bearer <token> from /v1/token, and that token lasts 600 seconds by default, so use /mcp directly from code that can fetch a fresh token, and the stdio bridge for desktop clients:
curl -s -X POST "$GW/mcp" -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'GitHub Actions
This step removes PII and secrets from a pull request description. The node must be reachable from the runner: a public HTTPS address, or a self-hosted runner inside your network. Add three repository secrets (Settings, Secrets and variables, Actions): VRYMDOR_NODE_URL, VRYMDOR_AGENT_ID and VRYMDOR_AGENT_SECRET.
GitHub gives no repository secrets to a pull_request run from a fork, so this workflow works for pull requests from branches of the same repository; on a pull request from a fork the token request fails, and so does the step. Do not switch to pull_request_target to get the secrets for forks if the job checks out or runs the pull request's code: that hands your agent secret to untrusted code. (The step below reads only the pull request body, never its code.)
name: Clean pull request text
on: pull_request
jobs:
sanitize:
runs-on: ubuntu-latest
steps:
- name: Remove PII and secrets from the pull request body
shell: bash # bash -eo pipefail: a refused or failed call fails the step
env:
GW: ${{ secrets.VRYMDOR_NODE_URL }}
AGENT_ID: ${{ secrets.VRYMDOR_AGENT_ID }}
AGENT_SECRET: ${{ secrets.VRYMDOR_AGENT_SECRET }}
PR_BODY: ${{ github.event.pull_request.body }}
run: |
TOKEN=$(jq -n --arg id "$AGENT_ID" --arg s "$AGENT_SECRET" '{agent_id:$id,agent_secret:$s}' \
| curl -sf -X POST "$GW/v1/token" -H 'Content-Type: application/json' -d @- | jq -r .access_token)
echo "::add-mask::$TOKEN"
jq -n --arg body "$PR_BODY" '{action:{type:"sanitize"},input:$body,context:{platform:"github-actions"}}' \
| curl -sf -X POST "$GW/v1/node/run" -H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' -d @- \
| jq -r .output > sanitized-body.txt
cat sanitized-body.txtThe pull request body is passed through an environment variable and jq, never pasted into the script, so text in the pull request cannot change the command. For a body with [email protected] and an Anthropic key, the file holds [REDACTED:EMAIL] and [REDACTED:ANTHROPIC_KEY] in their place.
Any HTTP client (curl)
Set GW to your node, exchange the agent secret, then call the node. The token request also accepts the OAuth 2.1 (Open Authorization) client credentials form (grant_type=client_credentials, client_id, client_secret).
GW=http://vrymdor-node:8080
TOKEN=$(curl -s -X POST "$GW/v1/token" -H 'Content-Type: application/json' \
-d '{"agent_id":"agt_...","agent_secret":"vrym_agent_..."}' | jq -r .access_token)Remove PII and secrets (nothing is sent anywhere):
curl -s -X POST "$GW/v1/node/run" -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"action":{"type":"sanitize"},"input":"Card 4111 1111 1111 1111, mail [email protected]"}'{"node_version":"1","ok":true,"decision":"allow","output":"Card [REDACTED:CREDIT_CARD], mail [REDACTED:EMAIL]",
"redactions":{"CREDIT_CARD":1,"EMAIL":1},"asi":[],"rules":["redacted"],"hold_id":null,"error":null,
"attestation":{"payload_sha256":"<hex>","signature":"t=<unix>,v1=<hex>"},
"audit":{"hash":"<hex>","trace_id":"<hex>"},"latency_ms":3.8}Run a tool (an MCP tool your policy allows, Step 3):
curl -s -X POST "$GW/v1/node/run" -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"action":{"type":"tool","upstream":"github","tool":"create_issue"},"input":{"title":"Login fails for [email protected]"},"idempotency_key":"run-881:item-0"}'The tool receives Login fails for [REDACTED:EMAIL] and its answer comes back in output. A repeated idempotency_key from the same agent gets the first answer back for ten minutes instead of running twice.
Call a model (the request body goes to action.path on the model upstream):
curl -s -X POST "$GW/v1/node/run" -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"action":{"type":"llm","upstream":"anthropic","path":"v1/messages"},"input":{"model":"claude-...","max_tokens":200,"messages":[{"role":"user","content":"Summarize the ticket from [email protected]"}]}}'Send a webhook (to an upstream of kind webhook):
curl -s -X POST "$GW/v1/node/run" -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"action":{"type":"webhook","upstream":"hooks"},"input":{"customer":"[email protected]"}}'Check only (dry run): add "params":{"dry_run":true} to any tool, model or webhook request. Every check runs, nothing is forwarded, and rules includes dry_run.
Inspect an envelope (webhook mode: the sanitized payload comes back, nothing is forwarded):
curl -s -X POST "$GW/v1/inspect" -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"vrymnox_version":"1","source":{"platform":"zapier","workflow_id":"zap-42","step":"2"},"action":{"kind":"webhook"},"payload":{"customer":"[email protected]","note":"Card 4111 1111 1111 1111 on file"}}'{"vrymnox_version":"1","decision":"allow","redactions":{"EMAIL":1,"CREDIT_CARD":1},"asi":[],"rules":["redacted"],
"payload":{"customer":"[REDACTED:EMAIL]","note":"Card [REDACTED:CREDIT_CARD] on file"},
"attestation":{"payload_sha256":"<hex>","signature":"t=<unix>,v1=<hex>"},"audit":{"hash":"<hex>","trace_id":"<hex>"}}source.platform is zapier, n8n, github or custom. GET $GW/v1/node/schema (no token needed) returns the request and response JSON Schemas and every error code.
Troubleshooting
Refusals carry a stable code and no internal detail, in the shape of the route that answered:
| Route | What a refusal looks like |
|---|---|
POST /v1/node/run and GET /v1/node/holds/<hold_id> (used by n8n, Make, Zapier and the Python client) | {"ok": false, "decision": "block", "error": {"code": "...", "message": "...", "retryable": false}, ...} with the HTTP status in the next table. Plan refusals add error.upgrade_url |
POST /v1/token, POST /v1/inspect and the admin API (/api/v1/...) | {"detail": "...", "rules": [...], "asi": [...]} with the HTTP status (plan refusals add code and upgrade_url). A policy refusal at /v1/inspect instead answers 200 with "decision": "block" and a message |
POST /mcp (MCP clients) | A JSON-RPC (JSON remote procedure call) error such as {"jsonrpc": "2.0", "id": 1, "error": {"code": -32001, "message": "..."}} for a bad token (-32002 for a plan limit). A refused tool call is a tool result with "isError": true and the text <code>: <message> |
| Any route, on an unexpected fault | HTTP 500 with {"error": {"code": "internal_error", "message": "...", "retryable": true}, "trace_id": "..."} |
| Code (HTTP status) | What it means | What to do |
|---|---|---|
unauthenticated (401) | The token is missing, malformed or expired, or /v1/token got a wrong secret ("Unknown agent or wrong secret."). Tokens of an agent that was killed or quarantined get "This token was revoked." | Copy the Agent ID and Agent Secret again and get a new token; n8n, the Python client and the stdio bridge renew tokens by themselves. For "This token was revoked.", see agent_stopped: a new token is refused until the agent is released |
agent_stopped (403) | The agent was killed or quarantined on the AI agents tab, for example while its action waited for approval ("The agent was stopped while the action waited."). /v1/token refuses it a new token with 403 "This agent may not receive tokens." | A quarantined agent: select Release on its card on the AI agents tab. A killed agent cannot be revived (the tab says "A killed identity cannot be revived"): register a new agent. An agent in drain and hold is not refused: its requests wait for approval (202, "decision": "hold") until you release it |
agent_expired (403) | "This agent's registration has expired." | Select Extend 30 days on the agent's card |
unregistered_agent (403) | "This agent is not registered." The agent is not in your registry on this node | Register it; a synced node picks it up within about 5 seconds |
tool_not_allowed (403) | "Tool drop_db is not on the allowlist." The tool is not in the policy for this role and upstream | Add it under mcp.tools with the role |
upstream_not_allowed (403) | "Role devops may not use hooks." | Add the upstream to the role's upstreams |
forbidden (403) | A refusal with no more specific code | Read error.message and rules |
unknown_upstream (404) | "No mcp upstream named nope is configured." The upstream name is not in the node's VRYMNOX_UPSTREAMS | Check the spelling, or add it |
not_found (404) | "No such hold for this agent." The hold_id is unknown, belongs to another agent, or its final answer was already collected | Use the hold_id from the 202 answer, with the same agent |
invalid_arguments (400) | "Tool arguments do not match the schema: arguments.title is required" | Send the arguments the tool's input_schema asks for |
invalid_request (422 or 400) | 422: the request does not match the NodeRequest schema, and the message names the field. 400: a tool's input is not a JSON object ("tools/call arguments must be an object") | Compare the request with GET /v1/node/schema; send a tool's arguments as a JSON object |
payload_too_large (413) | "Payload is larger than 1048576 bytes." (1 MiB by default) | Send less, or raise defaults.max_body_bytes in the policy (up to 16 MiB) |
tool_changed (403) | The tool's description or schema changed since it was approved | An admin approves it again on the node |
injection_blocked (403) | A prompt injection marker was found and the policy blocks them | Review the input; or set injection.action to flag |
rate_limited (429) | "Rate limit of 120 requests per minute reached." (default) | Retry later; raise the limit in defaults or the role |
budget_exhausted (429) | The agent's daily budget is used | Wait for the next day, or raise the agent's budget |
loop_detected (403) | Agents called each other too often or too deep | Fix the loop; the agent may be quarantined |
quota_exhausted (402) | Community only: the month's 10,000 included requests are used. error.upgrade_url links to your plan | Wait for the next month (UTC) or upgrade |
agent_limit (402) | More active agents than the plan allows | Stop an agent, or upgrade |
feature_not_in_plan (402) | An extra capability outside your plan (standard protection is never switched off) | Upgrade to the tier in the table above |
rejected_by_reviewer (200, from the holds route) | A person rejected the held action | Nothing ran; adjust and try again |
approval_expired (200, from the holds route) | Nobody decided the hold in time | Ask again, or raise hold_timeout_seconds |
tool_error (200, with "ok": false) | The tool ran and reported its own error; error.message is the tool's message. The node allowed the call ("decision": "allow") | Check the tool's arguments and the destination |
upstream_error (502) | The destination failed, for example "github answered HTTP 500." (its own text is not passed on) | Check the destination; retryable says whether to retry |
timeout (504) | The destination did not answer in time | Retry, or raise params.timeout_ms |
unavailable (503) | The node could not complete a check and refused (fail closed), or it is busy | Retry shortly; check /healthz |
internal_error (500) | An unexpected fault; the trace_id matches the node's log | Send the trace_id to support |
Plan headers: once the node has received your plan in its first sync, answers on the metered agent routes (/v1/node/run, /mcp, /v1/inspect, /v1/llm/..., /v1/mcp/... and /v1/webhooks/...) carry X-Vrymdor-Requests-Used and X-Vrymdor-Requests-Included, and from 80 percent of the allowance also X-Vrymdor-Quota-Warning. Other routes (/v1/token, /healthz, /v1/node/schema, the admin API) carry none, and an air-gapped node sends none, because it never receives a plan.
The node itself:
| What you see | Cause and fix |
|---|---|
| The container stops with "Activate License" | No node token. Set VRYMDOR_NODE_TOKEN (Step 2, Option A); with -d, the message is in docker logs vrymdor-node |
Conflict. The container name "/vrymdor-node" is already in use | A container with that name exists, running or stopped. Remove it with docker rm -f vrymdor-node (the vrymdor-node-data volume keeps the keys) and run the command again |
docker compose up -d stops with "failed to read dockerfile" | The image vrymdor/node-proxy:1.0.0 (or the tag in VRYMNOX_GATEWAY_VERSION) is not on the machine. Load the image file from support (Step 2) |
| "the control plane refused AWARENESS_TOKEN" | The token is wrong or was rotated. Copy it again from Connect your node |
| "could not reach https://api.vrymdor.com to look up the tenant" | The node has no outbound HTTPS. Allow it, or use air-gapped mode |
| Connect your node stays at Waiting for node | The node is not running, cannot reach https://api.vrymdor.com, or is air-gapped (it never reports) |
| Node offline | No report for three minutes. Check docker ps and the node's log |
/healthz answers 503 | A readiness check failed; the checks object names it, and the node blocks traffic until it passes |
docker ps shows unhealthy on a slow or emulated machine | The health check gives the node 10 seconds to answer. If curl -s http://127.0.0.1:8080/healthz answers ready, the node works and the machine is only slow |