Connect your AI Node to every platform
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 start with a 14 day free trial (your card is saved at checkout), then 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.
The whole setup at a glance
- Sign up and activate. Choose a plan at https://app.vrymdor.com/node/start, then open the link in your activation email, accept the agreements and set your password.
- Sign in at https://app.vrymdor.com/app. You land on Home base.
- Get your node address. Either turn on Hosted node on Home base (Pro, Business and Enterprise; the card then shows Your node address), or run the node yourself and use the address of the machine it runs on, port 8080 (Step 2).
- Only if you run the node yourself: get your node token. On Home base, in Connect your node, select
Show my node token. It starts with
vrym_live_. - Get an agent ID and agent secret. Open the AI agents tab, fill in Register an agent and select
Register. Copy the Agent ID (
agt_...) and Agent secret (vrym_agent_...) right away: the secret is shown only once. - Connect your platform (Step 4) with three values: the node address, the agent ID and the agent secret.
- Check it works. For a node you run, Connect your node reads Connected and active; for a hosted node, Hosted node reads Running and your first call shows on the Activity tab.
| 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 card shows "Copy these now. The agent secret is shown only this once; Vrymdor™
keeps only a hash of it." under it, with Agent ID (
agt_...) and Agent secret (vrym_agent_...), each with a Copy button. Copy both now and keep them where the platform will use them. The secret cannot be shown again; if you lose it, register a new agent.
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
Hosted node: we run it, you paste one address
On AI Node Pro, Business and Enterprise, and on every Vrymdor™ Business Security plan, Vrymdor can run your node for you. Nothing to install, and it is the easiest way to use cloud platforms that cannot reach a computer on your own network: n8n Cloud, Make, Zapier and GitHub Actions on GitHub's own runners. (Community runs the node on your own computer with one of the options below.)
- In the app, open Home base, Hosted node, and press Turn on. Within about a minute the badge reads
Running and the card shows your node address, for example
https://node.vrymdor.com/t/k3v9q2m7x4ab. The last part is random and is not your company name. - Under Add a model or tool provider, add each provider your agents call: a name (for example
openai), its address (https://api.openai.com), the header that carries the key (Authorizationorx-api-key) and the key itself (forAuthorization, writeBearerand then the key). The key is locked the moment it arrives so that only your hosted node can read it, and it is never shown again. To change a key, save the provider again with the new one. - Register an agent for each platform (Step 1), then use your node address wherever this guide says
Gateway URL or
VRYMDOR_NODE_URL: - n8n Cloud: import the n8n Cloud workflow (Step 4, n8n), put your node address in the URL of its Vrymdor token step and the agent'sagent_idandagent_secretin that step's Custom Auth credential. - Make and Zapier: the HTTP module or Webhooks by Zapier step posts to your node address followed by the path in Step 4, for examplehttps://node.vrymdor.com/t/k3v9q2m7x4ab/v1/token. - GitHub Actions: store your node address as theVRYMDOR_NODE_URLrepository secret. GitHub's own runners reach it, so no self-hosted runner is needed.
Your prompts and responses pass through your own node and are not stored by Vrymdor. The Activity tab shows what the node did (time, agent, platform, target, decision, rule and how many values were masked), kept for your plan's retention, 30 days by default. The Holds tab lists requests a policy held for a person: open one to see the request (read live from your node, never stored), then approve or reject it.
Turn off stops the node and keeps its data for 30 days, then it is deleted. If your plan no longer includes hosting, the node is turned off the same way. The hosted address is covered by the uptime commitment in the SLA exhibit for paid plans.
Run it yourself
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.
Get the node. In the app, open Home base, Connect your node, and press Download the node. Choose amd64 for most servers and PCs, or arm64 for ARM servers and Apple silicon, and download the file and its checksum (the links work for 15 minutes). The node is Self-Hosted Software licensed to your workspace under the MSA; keep the file to your own machines. On the machine that runs the node, check and load it (on macOS, check it with
shasum -a 256 -cinstead). 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.
sha256sum -c vrymdor-node-proxy-amd64.tar.sha256 # prints: vrymdor-node-proxy-amd64.tar: OK
docker load -i vrymdor-node-proxy-amd64.tar
Option 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.token
What 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_community
Option 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.
Load the image first. Support provides the package folder; load the image file from Download the node first (see the note at the start of Step 2). The compose file runs
vrymdor/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.
- 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.token
The 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.
The chart is not public. 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 in a public registry, so give your cluster the image from Download the node (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.
- 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:8080
curl -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 (VRYMDOR_AIR_GAPPED) needs VRYMNOX_TENANT_ID, and the
agents in VRYMNOX_AGENTS_FILE, 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-agents
The 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).
2. 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).
3. 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 /reject
The 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.
The fastest start: a ready workflow. Download one, then in n8n create a workflow, open the menu at the top
right, choose Import from File and pick the file:
the workflow with the Vrymnox node for
self-hosted n8n, or the n8n Cloud workflow,
a Vrymdor token step that keeps your agent in an n8n credential and a Vrymdor AI Node step that cleans
the text (the steps are under "n8n Cloud" below). Each has a note on the canvas with its steps, and its run shows [REDACTED:EMAIL]
and [REDACTED:CREDIT_CARD] where the sample text had an email address and a card number. The same files are in
the examples folder of the n8n-nodes-vrymnox repository on GitHub.
- 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.
3. 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.
5. 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.
n8n Cloud, or any n8n without the Vrymnox node
n8n Cloud only offers community nodes that n8n has verified, and the Vrymnox node is waiting for that. Until then,
n8n's own steps do the same job. Your node must have an address n8n Cloud can reach: the hosted node
(https://node.vrymdor.com/t/..., Home base, Hosted node) is the simplest.
Your agent secret always goes in an n8n credential, never in a step. n8n keeps credentials encrypted, does not show a saved secret again, and leaves credentials out when a workflow is shared or exported.
With the ready workflow (the n8n Cloud workflow, imported with Import from File):
- Your node address. Open the Vrymdor token step. In URL, replace
PASTE_YOUR_NODE_IDso the URL is your node address from the Vrymdor app (Home base, Hosted node) followed by/v1/token, for examplehttps://node.vrymdor.com/t/k3v9q2m7x4ab/v1/token. - Your agent, in a credential. In the same step, under Custom Auth, choose Create new credential. In its JSON field paste your own agent from the Vrymdor app (AI agents), then Save:
json
{"body": {"agent_id": "agt_...", "agent_secret": "vrym_agent_..."}}
- Check that it connects. Select Execute step on Vrymdor token. An
access_tokenin the output means your node accepted the agent.Authorization failedmeans the agent id or secret is wrong, or the agent is not registered for this node: copy them again from AI agents, or register a new agent. - Run it. Select Execute workflow. The Vrymdor AI Node step shows
[REDACTED:EMAIL]and[REDACTED:CREDIT_CARD]where the sample text had an email address and a card number, andvrymdor.redactionscounts them. The step reads your node address from the Vrymdor token URL, so you paste it only once.
To use your own data, put your trigger in place of Sample text and set items_from at the top of the
Vrymdor AI Node step to your trigger's name. field names the field to clean (text by default).
Building it by hand with two HTTP Request nodes:
- Keep the secret in a credential. In the
Vrymdor tokenstep of step 2 (or Credentials, Add credential), Credential for Custom Auth, Create new credential. n8n names it Custom Auth account. In its JSON field paste, with your own agent:
json
{"body": {"agent_id": "agt_...", "agent_secret": "vrym_agent_..."}}
- Get a token. Add an HTTP Request node named
Vrymdor token: MethodPOST, URLhttps://node.vrymdor.com/t/<your node>/v1/token, AuthenticationGeneric Credential Type, Generic Auth TypeCustom Auth, Custom AuthCustom Auth account, Send Body on, Body Content TypeJSON, Specify BodyUsing JSON, JSON{}. Run it: the answer hasaccess_token, which lasts 10 minutes. - Remove PII and secrets. Add a second HTTP Request node: Method
POST, URL the same address ending in/v1/node/run, Send Headers on, header Authorization with the valueBearer {{ $('Vrymdor token').item.json.access_token }}, Send Body on, Body Content TypeJSON, Specify BodyUsing JSON, and the JSON (switch the field to Expression):
{{ JSON.stringify({ action: { type: "sanitize" }, input: $json.text }) }}
Run it on an item with a text field such as mail [email protected]. The answer's output is
mail [REDACTED:EMAIL], and redactions counts what was removed. Map output into the next step, and
stop the workflow (an If node on redactions) wherever text with personal data must not go on.
4. Call a model through the node the same way: URL /v1/node/run and the JSON
{"action":{"type":"llm","upstream":"anthropic","path":"v1/messages"},"input":{...}}, the model request
as the provider documents it. The node removes PII before the model sees the prompt.
Get a new token for each run (step 2 first in every workflow), since a token lasts 10 minutes. When the Vrymnox node is verified on n8n Cloud, it replaces both requests and renews the token by itself.
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.
- 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." 3. 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).
2. Add a second step: Webhooks by Zapier, event Custom Request.
- Method: POST
- URL: https://ai-gateway.example.com/v1/node/run
- Headers: Authorization = Bearer followed by the Access Token from step 1, and
Content-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.
3. Test it. The answer has "decision": "allow" and
"output": "Customer [REDACTED:EMAIL] paid with [REDACTED:CREDIT_CARD]". Map output into 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.
- 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 schema
The 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:
types: [opened, edited, synchronize, reopened] # edited: fixing the description runs the check again
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"
RESULT=$(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 <<<"$RESULT" > sanitized-body.txt
cat sanitized-body.txt
# The node flags a likely prompt injection (rule injection_marker, ASI01) and still returns the
# cleaned text, so live traffic is never refused on a guess. A pull request is different: this fails
# the check so a person reviews the text first. Make the check required to stop the merge.
if jq -e '.rules | index("injection_marker")' <<<"$RESULT" >/dev/null; then
# The node names each phrase it matched (its fixed list, never other text); point at the line.
jq -r '.markers // [] | .[]' <<<"$RESULT" | while IFS= read -r phrase; do
line=$(grep -n -i -F -m1 -- "$phrase" <<<"$PR_BODY" | cut -d: -f1)
echo "::error title=Possible prompt injection::Found \"$phrase\" in the pull request description${line:+ on line $line}. Edit the description and this check runs again."
done
echo "::error title=Possible prompt injection::The pull request description contains a known prompt injection phrase (ASI01). Review it before merging."
exit 1
fi
The 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.
Stopping a prompt injection before it merges. The node checks text for phrases attackers use to take
over an AI agent, such as "ignore previous instructions" or "print your system prompt" (OWASP ASI01, goal
hijack). By default it flags them (the rule injection_marker in the result) and still passes the cleaned
text on, so your live traffic is never refused on a guess. In a pull request you want a person to look
first, so the last lines of the step fail the check when the node flags the text, naming each phrase it
found and the line of the description it is on. Editing the description runs the check again, so fixing
the text is enough; no new commit is needed. To make that stop the
merge, turn the check into a required one: Settings, Branches, add a branch protection rule for your
main branch, check Require status checks to pass before merging, and choose this check. GitHub then
keeps the Merge button off until the text is fixed or a maintainer reviews it. The phrase check catches the
common, copied attacks, not every rewording, so treat it as one signal alongside your review.
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 |