Vultrino

A credential proxy for the AI era — enabling AI agents to use credentials without seeing them.

What is Vultrino?

Vultrino keeps raw credential fields out of agent-facing requests and performs authenticated operations inside trusted connectors. An agent receives aliases and action results rather than direct access to the stored secret.

┌─────────────────┐     ┌─────────────────┐     ┌─────────────────┐
│   AI Agent      │────▶│    Vultrino     │────▶│  External API   │
│   (Claude, etc) │     │ (uses secrets)  │     │  or Operation   │
└─────────────────┘     └─────────────────┘     └─────────────────┘
        │                       │
        │ "Use my-credential"   │ Injects auth, signs data, etc.
        │                       │
        ▼                       ▼
   Alias-only request    Injects inside a trusted connector

Key Features

  • Credential Isolation — raw fields are absent from agent/MCP response schemas
  • Role-Based Access Control — Fine-grained permissions for different applications
  • Multiple Credential Types — API keys, Basic Auth, OAuth2, signing keys, and more
  • Plugin System — Extend public-data actions through the credential-confining WASM ABI v2
  • MCP Integration — Native Model Context Protocol support for LLM tools
  • Web UI — Clean admin interface for managing credentials and keys
  • Encrypted Storage — AES-256-GCM encryption with Argon2 key derivation
  • Policy Engine — URL patterns, method restrictions, rate limiting
  • Audit Logging — Track all credential usage

Use Cases

AI Agent Security

Give Claude, GPT, or other AI agents the ability to call APIs without exposing credentials. The agent requests actions through Vultrino, which handles authentication transparently.

Team Credential Management

Centralize API credentials for your team. Create scoped API keys for different applications with specific permissions.

Development Environments

Safely share credentials across development, staging, and production without exposing secrets in code or environment variables.

Quick Example

# Add a credential
vultrino add --alias github-api --key ghp_your_token_here

# Make an authenticated request
vultrino request github-api https://api.github.com/user

# Or use with AI agents via MCP
vultrino serve --mcp

Components

ComponentDescription
CLICommand-line interface for all operations
Web UIBrowser-based admin dashboard
HTTP APIPOST /api/v1/execute runs authenticated requests on behalf of agents (served by vultrino web)
MCP ServerModel Context Protocol server for LLM integration

Next Steps

Installation

Requirements

  • Rust 1.94.0 (pinned in rust-toolchain.toml; rustup installs it)
  • No system OpenSSL packages — TLS uses rustls
# Clone the repository
git clone https://github.com/FeirAI/vultrino.git
cd vultrino

# Build in release mode (uses committed Cargo.lock)
cargo build --release --locked

# The binary will be at target/release/vultrino
# Optionally, copy to your PATH
cp target/release/vultrino /usr/local/bin/

Using Cargo

cargo install --git https://github.com/FeirAI/vultrino --locked --bin vultrino

Pre-built Binaries

Download pre-built binaries from the GitHub Releases page (published when a v* tag is pushed).

macOS

# Intel
curl -L https://github.com/FeirAI/vultrino/releases/latest/download/vultrino-x86_64-apple-darwin.tar.gz | tar xz
sudo mv vultrino /usr/local/bin/

# Apple Silicon
curl -L https://github.com/FeirAI/vultrino/releases/latest/download/vultrino-aarch64-apple-darwin.tar.gz | tar xz
sudo mv vultrino /usr/local/bin/

Linux

# x86_64
curl -L https://github.com/FeirAI/vultrino/releases/latest/download/vultrino-x86_64-unknown-linux-gnu.tar.gz | tar xz
sudo mv vultrino /usr/local/bin/

# ARM64
curl -L https://github.com/FeirAI/vultrino/releases/latest/download/vultrino-aarch64-unknown-linux-gnu.tar.gz | tar xz
sudo mv vultrino /usr/local/bin/

Docker / GHCR

docker pull ghcr.io/feirai/vultrino:latest
docker run --rm -p 7879:7879 \
  -e VULTRINO_PASSWORD=your-secure-password \
  ghcr.io/feirai/vultrino:latest

See Docker deployment for compose and volume layout.

Verify Installation

vultrino --version
# vultrino 0.1.0

Next Steps

Continue to Quick Start to initialize Vultrino and add your first credential.

Quick Start

This guide will get you up and running with Vultrino in under 5 minutes.

1. Initialize Vultrino

First, initialize the configuration and set up your admin account:

vultrino init

You'll be prompted to:

  1. Set a storage password (encrypts your credentials at rest)
  2. Create an admin username for the web UI
  3. Set an admin password for the web UI

Tip: Set the VULTRINO_PASSWORD environment variable to avoid password prompts:

export VULTRINO_PASSWORD="your-secure-password"

2. Add Your First Credential

Add an API key credential:

vultrino add --alias github-api --key ghp_your_github_token

Add a Basic Auth credential:

vultrino add --alias my-service --type basic_auth --username admin --password secret123

3. List Your Credentials

vultrino list

Output:

ALIAS                TYPE            ID                                   DESCRIPTION
github-api           api_key         a1b2c3d4-...                        -
my-service           basic_auth      e5f6g7h8-...                        -

4. Make an Authenticated Request

Use the request command to make API calls with your stored credentials:

vultrino request github-api https://api.github.com/user

The credential is automatically injected — you never need to expose the actual token.

5. Start the Web UI

Launch the admin dashboard:

vultrino web

Open http://127.0.0.1:7879 and log in with your admin credentials.

6. Create an API Key for AI Agents

Before AI agents can use Vultrino, create a scoped API key:

vultrino key create my-agent --role executor

Output:

API key created successfully!

Key: vk_abc123...

*** SAVE THIS KEY - IT WILL NOT BE SHOWN AGAIN ***

Name:    my-agent
Role:    executor
Expires: Never

Important: Save this key securely. It provides scoped access to your credentials.

7. Start the Server

Start the web server (required for CLI with API key and web UI):

vultrino web

This starts:

  • Web UI at http://127.0.0.1:7879
  • JSON API for CLI and external apps

8. Use CLI with API Key (No Password)

Once the server is running, use CLI commands with just the API key:

# List credentials (no VULTRINO_PASSWORD needed!)
vultrino --key vk_abc123... request github-api https://api.github.com/user

9. Use with MCP (AI Agents)

Start the MCP server:

vultrino mcp

AI agents use your API key in every tool call:

{
  "tool": "http_request",
  "arguments": {
    "api_key": "vk_abc123...",
    "credential": "github-api",
    "method": "GET",
    "url": "https://api.github.com/user"
  }
}

See Using with AI Agents for detailed setup.

Common Commands

CommandDescription
vultrino initInitialize configuration and admin account
vultrino add --alias NAME --key TOKENAdd an API key credential
vultrino listList all credentials
vultrino remove <alias>Remove a credential
vultrino request <alias> <url>Make authenticated request (needs password)
vultrino --key KEY request <alias> <url>Make request with API key (no password)
vultrino webStart web UI and API server
vultrino mcpStart MCP server for AI agents
vultrino key create NAME --role ROLECreate API key
vultrino key listList API keys
vultrino key revoke NAMERevoke an API key
vultrino role listList available roles

Next Steps

Configuration

Vultrino uses a TOML configuration file located at:

  • macOS: ~/Library/Application Support/vultrino/config.toml
  • Linux: ~/.config/vultrino/config.toml
  • Windows: %APPDATA%\vultrino\config.toml

Default Configuration

# Vultrino Configuration

[server]
bind = "127.0.0.1:7878"
mode = "local"

[storage]
backend = "file"

[storage.file]
path = "~/.local/share/vultrino/credentials.enc"

[logging]
level = "info"
# audit_file = "~/.local/share/vultrino/audit.log"

[mcp]
enabled = true
transport = "stdio"

Configuration Options

Server Section

[server]
bind = "127.0.0.1:7878"  # legacy `serve` default; see note below
mode = "local"            # "local" or "server"
OptionDescriptionDefault
bindListen address for the serve subcommand. vultrino web (the HTTP JSON API + admin UI) defaults to 127.0.0.1:7879 and the --bind flag overrides per process.127.0.0.1:7878
modeDeployment mode: server sets require_auth = true on the in-process default (local otherwise).local

The JSON API and admin UI are served by vultrino web on 7879, not by this [server].bind. vultrino serve (which this key configures) no longer starts an API server on its own — see the CLI reference.

Storage Section

[storage]
backend = "file"  # Storage backend: "file", "keychain", or "vault"

[storage.file]
path = "~/.local/share/vultrino/credentials.enc"
OptionDescriptionDefault
backendStorage backend typefile
pathPath to encrypted credentials fileOS-specific

Logging Section

[logging]
level = "info"  # Log level: error, warn, info, debug, trace
# audit_file = "~/.local/share/vultrino/audit.log"  # Optional audit log
OptionDescriptionDefault
levelLogging verbosityinfo
audit_filePath to audit log (optional)disabled

MCP Section

[mcp]
enabled = true
transport = "stdio"  # "stdio" or "http"
OptionDescriptionDefault
enabledEnable MCP servertrue
transportTransport methodstdio

Enforcement Section

Controls what the policy engine decides for a credential that matches no policy at all.

[enforcement]
default_action = "deny"  # "deny" (fail-closed, default) or "allow" (fail-open)
OptionDescriptionDefault
default_actionDecision for a credential matched by no policy: deny or allowdeny

With deny (the default, and the recommended posture for shared/server deployments), an un-policied credential is denied with a distinct no_policy reason — closing the historical fail-open gap. Use allow for the legacy behavior where an un-policied credential is permitted. If the section is omitted the built-in default is deny. The config produced by vultrino init also ships with deny and prints a reminder that you must add an allow policy (or switch to allow) before credentials will work.

When default_action = "deny" and no policies are configured, every credential is denied. Vultrino logs a loud warning at startup in this case (and the symmetric allow + no-policies fail-open case is warned about too).

Upgrading (breaking change)

Before this change the engine was fail-open: a credential matching no policy was allowed. It is now fail-closed by default. A config that has no [enforcement] section will start denying un-policied credentials after upgrade. To preserve the pre-upgrade behavior, add:

[enforcement]
default_action = "allow"

Otherwise, add allow policies for the credentials your agents legitimately use.

Spend Extractors

For SpendCap policies (V3), Vultrino needs to know where the amount lives in the request body. Each extractor matches an action + credential and reads the amount (an integer in minor units, e.g. cents) from a JSON pointer, plus an asset (literal or a second pointer).

[[spend_extractors]]
action_pattern = "http.request"     # glob over plugin.action
credential_pattern = "stripe-*"     # glob over credential alias
amount_pointer = "/body/amount"     # JSON pointer to the integer amount
asset = "usd"                        # literal asset...
# asset_pointer = "/body/currency"  # ...or read it from the body

If a SpendCap policy applies to a credential but no extractor yields an amount (missing extractor or unparseable body), the request is denied (fail-closed) and a spend_unparseable warning is logged.

Egress Controls

Vultrino keeps proxied responses from carrying secrets back to the agent (V7), applied at the execution seam for every plugin:

  1. Always-on secret-material redaction. If an endpoint reflects the credential's own injected secret in its response (a header-echoing reflector, an open redirect, etc.), the secret — and its common re-encoded forms (percent-encoded, JSON-escaped) — is scrubbed from the body and headers and replaced with the constant [REDACTED] marker before the response is returned. This is not configurable. It is defense-in-depth, not absolute: an endpoint that transforms the secret (base64, hashing, splitting it) — or returns a compressed body (the http plugin requests Accept-Encoding: identity, but a server may compress anyway) — can still leak it. Use a block rule for endpoints you don't trust. Secrets shorter than 5 bytes are not scrubbed (too little entropy to match safely); a warning is logged when such a credential is created.
  2. Egress classification. For endpoints whose response is itself a secondary secret (an STS/login/secret-read endpoint), configure [[egress]] rules:
[[egress]]
credential_pattern = "sts-*"        # glob over credential alias
action_pattern = "http.request"     # glob over plugin.action (default "*")
block = true                         # withhold the body + headers entirely

[[egress]]
credential_pattern = "secrets-api-*"
redact_patterns = ['"token":\s*"[^"]+"', "AKIA[0-9A-Z]{16}"]  # extra regexes to redact

The first matching rule applies. block = true replaces the body with a marker and drops the headers; otherwise any redact_patterns (regexes) are scrubbed from the body (on top of the always-on redaction).

Downstream credentials. Blocking/redacting prevents an agent from reading a downstream secret out of a response. Deleting an OAuth2 credential that carries a revocation_url metadata key now propagates the revoke to the provider (R5/V7): Vultrino calls the RFC 7009 revocation endpoint for the credential's issued access and refresh tokens before removing it locally, so an already-issued downstream secret is actively revoked rather than left to expire, and a credential.revoked event is emitted to the signed outbox. Set it with vultrino meta set <oauth-cred> revocation_url https://idp/oauth/revoke (HTTPS required). Prefer credential types that mint short-lived, revocable downstream credentials (OAuth2 client-credentials, STS, SVIDs) so a revoke maps to a real resource-side revoke. OAuth2 in-path token rotation also emits a credential.rotated event.

Action Labels

Map a govder business verb to a canonical plugin.action (V8), so use-token scopes and the approval/audit trail can speak in business terms while vultrino executes the underlying plugin action. (Policy rules match on URL/method/credential/principal/spend — not on the action label — so the verb is a scoping and audit concept, not a policy-condition one.)

[[action_labels]]
label = "payments.refund"   # what govder / a token scopes against
action = "http.request"     # the canonical plugin.action vultrino runs

A request (or use-token action_scope) may then use the label payments.refund; it resolves to http.request for execution, the use-token scope is satisfied by either the label or the canonical action, and the approver sees the business verb in the approval. The typed /api/v1/execute endpoint also accepts an optional action field (default http.request) so it is no longer hardwired.

Event Outbox (V9)

Vultrino records security-relevant events to a durable, ordered, replayable, signed outbox: approval requested/approved/denied/escalated/expired, agent.halted, policy.changed, credential.rotated, credential.revoked (a downstream revoke propagated to the provider on delete), policy.observed_denial (an observe-only tenant's un-enforced denial), and policy.denied (an enforce-mode denial — a DETECT signal whose created_at is a per-incident detected_at that pairs, on the same subject, with the agent.halted contained_at for an MTTD/MTTC measurement). Configure push delivery with [outbox]:

[outbox]
url = "https://govder.example.com/vultrino/events"  # delivery endpoint
hmac_secret = "shared-signing-secret"                # required to push (deliveries are signed)
max_attempts = 8                                      # retries before dead-lettering (default 8)
retention_secs = 604800                               # replay window, default 7 days
  • Ordered + monotonic. Every event gets a process-global, gap-free sequence. Events for the same subject (e.g. an approval id) are delivered in order.
  • Signed. Each delivery carries Govder-Signature: sha256=<hex> = HMAC-SHA256(hmac_secret, body). A consumer recomputes it over the raw body to verify authenticity. Enabling the outbox requires both url and hmac_secret (an unsigned/undeliverable outbox is rejected at load).
  • Exactly-once-ish delivery across processes. Each event is atomically claimed (leased) under the vault lock before it is POSTed, so the web and MCP processes can't both deliver it; a failed delivery backs off (the lease holds it off the retry queue) before re-attempting, and a crashed deliverer's lease is reclaimed once stale.
  • Replayable. A consumer that drops offline replays from its last-seen sequence: GET /api/v1/events?after=<cursor> returns the next events — each as { "body": …, "signature": "sha256=…" }, the same body a push carries plus its signature — with no gaps and no dupes, within the retention window.
  • Dead-letter queue. An event that fails max_attempts deliveries is parked (GET /api/v1/events/dead) and re-queued with POST /api/v1/events/{sequence}/replay — it stops blocking its subject.
  • Events are appended even when push is unconfigured (still replayable via the API). GC prunes the oldest contiguous prefix past retention_secs (keeping the retained window gap-free); the window is the replay + dead-letter-resolution SLA.

Inbound Workload Identity (V10)

Resolve the principal vultrino evaluates from an inbound SPIFFE SVID or OIDC claims document instead of only the static vk_/vut_ id. A request carrying the configured header (an already transport-verified document — terminate mTLS / verify the token at the edge) has its principal resolved before policy evaluation.

[identity]
kind = "spiffe"                 # spiffe | oidc (the wireable resolvers)
header = "x-spiffe-verified"    # inbound header carrying the verified document
allowed = ["example.org"]       # SPIFFE trust domains (or OIDC issuers); empty = any

The resolved subject becomes the Principal.id a policy principal_pattern matches (and the SoD owner from an OIDC email/preferred_username). A malformed/untrusted document is ignored (the static principal stands). See Workload Identity.

Environment Variables

VariableDescription
VULTRINO_PASSWORDStorage encryption password (avoids prompts)
VULTRINO_CONFIGPath to config file
RUST_LOGOverride log level (e.g., vultrino=debug)

Policy Configuration

Policies control which requests are allowed for each credential:

[[policies]]
name = "github-readonly"
credential_pattern = "github-*"  # Glob pattern for credential aliases
default_action = "deny"

[[policies.rules]]
condition = { url_match = "https://api.github.com/*" }
action = "allow"

[[policies.rules]]
condition = { method_match = ["POST", "PUT", "DELETE"] }
action = "deny"

See Policy Configuration for detailed policy options.

Using a Custom Config File

vultrino --config /path/to/config.toml list

Regenerating Configuration

To reset to defaults:

vultrino init --force

Warning: This will overwrite your existing configuration and require re-entering admin credentials.

Deployment Overview

Vultrino can be deployed in several ways depending on your needs:

Deployment Options

MethodBest ForComplexity
Local DevelopmentPersonal use, testingSimple
VPS / ServerTeam deployment, productionModerate
Cloudflare WorkersEdge deployment, serverlessModerate
DockerContainerized environmentsSimple

Architecture Considerations

Single Binary

Vultrino is distributed as a single binary with no external dependencies. This includes:

  • CLI commands
  • HTTP proxy server
  • Web UI (embedded)
  • MCP server

Storage

Credentials are stored encrypted using AES-256-GCM. Storage options:

  • File (default) — Encrypted JSON file on disk
  • Keychain — OS keychain integration (coming soon)
  • Vault — HashiCorp Vault integration (coming soon)

Network Security

Local Mode (default):

  • Binds to 127.0.0.1 only
  • No external access
  • Suitable for single-machine use

Server Mode:

  • Can bind to all interfaces (0.0.0.0)
  • Requires additional security measures
  • Use with TLS termination proxy (nginx, Caddy)

Security Recommendations

  1. Always use HTTPS in production — Use a reverse proxy with TLS
  2. Restrict network access — Firewall rules, VPN, or private network
  3. Use strong passwords — Both storage and admin passwords
  4. Enable audit logging — Track credential usage
  5. Rotate API keys — Set expiration on Vultrino API keys
  6. Principle of least privilege — Create scoped roles for each application

Quick Comparison

┌─────────────────────────────────────────────────────────────────┐
│                        LOCAL                                     │
│  vultrino web  (+ vultrino mcp for AI agents)                    │
│  ├── Best for: Personal use, development                        │
│  ├── Security: Localhost only                                   │
│  └── Setup: Minimal                                              │
├─────────────────────────────────────────────────────────────────┤
│                        VPS/SERVER                                │
│  vultrino web + nginx/caddy                                     │
│  ├── Best for: Team use, production                             │
│  ├── Security: TLS, firewall, auth                              │
│  └── Setup: Moderate (systemd, reverse proxy)                   │
├─────────────────────────────────────────────────────────────────┤
│                        CLOUDFLARE                                │
│  Cloudflare Workers + KV/Durable Objects                        │
│  ├── Best for: Edge deployment, global access                   │
│  ├── Security: Cloudflare's infrastructure                      │
│  └── Setup: Moderate (requires adaptation)                      │
├─────────────────────────────────────────────────────────────────┤
│                        DOCKER                                    │
│  docker run vultrino                                            │
│  ├── Best for: Containerized environments                       │
│  ├── Security: Container isolation                              │
│  └── Setup: Simple (docker-compose)                             │
└─────────────────────────────────────────────────────────────────┘

Next Steps

Choose your deployment method:

Local Development

The simplest way to run Vultrino — perfect for personal use and development.

Quick Setup

# 1. Initialize
vultrino init

# 2. Add credentials
vultrino add --alias github-api --key ghp_xxx

# 3. Start services
vultrino web &          # HTTP API + web UI on :7879
vultrino mcp            # MCP server (stdio) for AI agents

Running Components

HTTP API + Web UI

The web process serves the JSON API (/api/v1/…), the connector routes (/mcp, /llm), and the HTML admin UI on one port:

export VULTRINO_PASSWORD="your-password"
vultrino web
# Access at http://127.0.0.1:7879

MCP Server Only

For local AI agent integration over stdio:

export VULTRINO_PASSWORD="your-password"
vultrino mcp   # equivalently: vultrino serve --mcp

vultrino serve on its own does not start an API server (it's a stub that redirects you to vultrino web). Use vultrino web for HTTP.

Configuration for Local Use

The default configuration is optimized for local development:

[server]
mode = "local"

[storage]
backend = "file"

Using with Claude Desktop

Add to your Claude Desktop MCP configuration (~/Library/Application Support/Claude/claude_desktop_config.json):

{
  "mcpServers": {
    "vultrino": {
      "command": "/path/to/vultrino",
      "args": ["mcp"],
      "env": {
        "VULTRINO_PASSWORD": "your-password"
      }
    }
  }
}

Running as Background Process

macOS (launchd)

Create ~/Library/LaunchAgents/dev.vultrino.web.plist:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>Label</key>
    <string>dev.vultrino.web</string>
    <key>ProgramArguments</key>
    <array>
        <string>/usr/local/bin/vultrino</string>
        <string>web</string>
    </array>
    <key>EnvironmentVariables</key>
    <dict>
        <key>VULTRINO_PASSWORD</key>
        <string>your-password</string>
    </dict>
    <key>RunAtLoad</key>
    <true/>
    <key>KeepAlive</key>
    <true/>
</dict>
</plist>

Load it:

launchctl load ~/Library/LaunchAgents/dev.vultrino.web.plist

Linux (systemd user service)

Create ~/.config/systemd/user/vultrino-web.service:

[Unit]
Description=Vultrino Web UI
After=network.target

[Service]
Type=simple
Environment="VULTRINO_PASSWORD=your-password"
ExecStart=/usr/local/bin/vultrino web
Restart=always

[Install]
WantedBy=default.target

Enable and start:

systemctl --user enable vultrino-web
systemctl --user start vultrino-web

Tips

  1. Store password in keychain — Use OS keychain to avoid plaintext passwords
  2. Use aliases — Add alias vreq='vultrino request' to your shell
  3. Tab completion — Generate with vultrino completions bash > /etc/bash_completion.d/vultrino

Troubleshooting

"Device not configured" error

The password prompt requires a terminal. Set VULTRINO_PASSWORD environment variable instead.

"Address already in use"

Another process is using the port. Check with:

lsof -i :7879

Credentials not loading

Ensure you're using the same VULTRINO_PASSWORD that was used when creating credentials.

VPS / Server Deployment

Deploy Vultrino on a VPS or dedicated server for team access and production use.

Prerequisites

  • Linux server (Ubuntu 22.04+ recommended)
  • Domain name (optional but recommended)
  • TLS certificate (Let's Encrypt)

Installation

# Download latest release
curl -L https://github.com/FeirAI/vultrino/releases/latest/download/vultrino-x86_64-unknown-linux-gnu.tar.gz | tar xz
sudo mv vultrino /usr/local/bin/

# Create vultrino user
sudo useradd -r -s /bin/false vultrino

# Create directories
sudo mkdir -p /etc/vultrino /var/lib/vultrino
sudo chown vultrino:vultrino /var/lib/vultrino

Configuration

Create /etc/vultrino/config.toml:

[server]
# `mode = "server"` enables the stricter server posture. `vultrino web` binds
# 127.0.0.1:7879 by default (the systemd unit below sets it explicitly with --bind).
mode = "server"

[storage]
backend = "file"

[storage.file]
path = "/var/lib/vultrino/credentials.enc"

[logging]
level = "info"
audit_file = "/var/log/vultrino/audit.log"

[mcp]
enabled = true
transport = "stdio"

Systemd Services

Web UI Service

Create /etc/systemd/system/vultrino-web.service:

[Unit]
Description=Vultrino Web UI
After=network.target

[Service]
Type=simple
User=vultrino
Group=vultrino
Environment="VULTRINO_PASSWORD=your-secure-password"
Environment="VULTRINO_CONFIG=/etc/vultrino/config.toml"
ExecStart=/usr/local/bin/vultrino web --bind 127.0.0.1:7879
Restart=always
RestartSec=5

# Security hardening
NoNewPrivileges=yes
ProtectSystem=strict
ProtectHome=yes
ReadWritePaths=/var/lib/vultrino
PrivateTmp=yes

[Install]
WantedBy=multi-user.target

The single vultrino web process serves the HTTP JSON API (/api/v1/…), the connector routes (/mcp, /llm), and the admin UI — there is no separate proxy service. (vultrino serve no longer starts an API server.)

Enable and start:

sudo systemctl daemon-reload
sudo systemctl enable vultrino-web
sudo systemctl start vultrino-web

Reverse Proxy Setup

Nginx

Install nginx and certbot:

sudo apt install nginx certbot python3-certbot-nginx

Create /etc/nginx/sites-available/vultrino:

server {
    listen 80;
    server_name vultrino.yourdomain.com;
    return 301 https://$server_name$request_uri;
}

server {
    listen 443 ssl http2;
    server_name vultrino.yourdomain.com;

    ssl_certificate /etc/letsencrypt/live/vultrino.yourdomain.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/vultrino.yourdomain.com/privkey.pem;

    # Web UI + JSON API (both served by vultrino web on 7879)
    location / {
        proxy_pass http://127.0.0.1:7879;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

Enable and get certificate:

sudo ln -s /etc/nginx/sites-available/vultrino /etc/nginx/sites-enabled/
sudo certbot --nginx -d vultrino.yourdomain.com
sudo systemctl reload nginx

Caddy (Alternative)

Create /etc/caddy/Caddyfile:

vultrino.yourdomain.com {
    reverse_proxy 127.0.0.1:7879
}

Firewall Configuration

# Allow only HTTPS
sudo ufw allow 443/tcp
sudo ufw allow 22/tcp  # SSH
sudo ufw enable

Initialize Credentials

# Set password
export VULTRINO_PASSWORD="your-secure-password"

# Initialize (as vultrino user)
sudo -u vultrino VULTRINO_PASSWORD="$VULTRINO_PASSWORD" vultrino init

# Add credentials
sudo -u vultrino VULTRINO_PASSWORD="$VULTRINO_PASSWORD" vultrino add --alias github-api --key ghp_xxx

Security Checklist

  • Strong storage password (32+ characters)
  • Strong admin password
  • TLS enabled (HTTPS only)
  • Firewall configured
  • Audit logging enabled
  • Regular backups of /var/lib/vultrino/
  • API keys have expiration dates
  • Roles use principle of least privilege

Monitoring

Check service status

sudo systemctl status vultrino-web

View logs

sudo journalctl -u vultrino-web -f
sudo tail -f /var/log/vultrino/audit.log

Health check

curl -s http://127.0.0.1:7879/login | head -1

Backup & Restore

Backup

sudo tar -czf vultrino-backup-$(date +%Y%m%d).tar.gz \
    /etc/vultrino \
    /var/lib/vultrino

Restore

sudo tar -xzf vultrino-backup-YYYYMMDD.tar.gz -C /
sudo systemctl restart vultrino-web

Cloudflare Workers Deployment

Note: Cloudflare Workers deployment requires adapting Vultrino to the Workers runtime. This guide covers the architecture and approach.

Overview

Deploying Vultrino to Cloudflare Workers provides:

  • Global edge deployment
  • Serverless scaling
  • Cloudflare's security infrastructure
  • Low-latency access worldwide

Architecture

┌─────────────────────────────────────────────────────────────────┐
│                     Cloudflare Edge                              │
├─────────────────────────────────────────────────────────────────┤
│                                                                  │
│   ┌─────────────────┐     ┌─────────────────────────────────┐   │
│   │  Workers        │     │  Durable Objects                │   │
│   │  (HTTP API)     │────▶│  (Session State, Credentials)   │   │
│   └─────────────────┘     └─────────────────────────────────┘   │
│           │                           │                          │
│           │                           ▼                          │
│           │               ┌─────────────────────────────────┐   │
│           │               │  KV                             │   │
│           │               │  (Encrypted Credential Storage) │   │
│           │               └─────────────────────────────────┘   │
│           │                                                      │
│           ▼                                                      │
│   ┌─────────────────┐                                           │
│   │  External APIs  │                                           │
│   │  (via fetch)    │                                           │
│   └─────────────────┘                                           │
│                                                                  │
└─────────────────────────────────────────────────────────────────┘

Components Mapping

Vultrino ComponentCloudflare Equivalent
Encrypted file storageKV with encryption
Session managementDurable Objects
HTTP handlersWorkers
Static assetsWorkers Sites or R2

Implementation Approach

1. Create Worker Project

npm create cloudflare@latest vultrino-edge -- --template hello-world
cd vultrino-edge

2. Configure wrangler.toml

name = "vultrino"
main = "src/index.ts"
compatibility_date = "2024-01-01"

[vars]
ENVIRONMENT = "production"

[[kv_namespaces]]
binding = "CREDENTIALS"
id = "your-kv-namespace-id"

[[durable_objects.bindings]]
name = "SESSIONS"
class_name = "SessionDO"

[[migrations]]
tag = "v1"
new_classes = ["SessionDO"]

3. Implement Core Logic

// src/index.ts
import { Hono } from 'hono';
import { cors } from 'hono/cors';

type Bindings = {
  CREDENTIALS: KVNamespace;
  SESSIONS: DurableObjectNamespace;
  ENCRYPTION_KEY: string;
};

const app = new Hono<{ Bindings: Bindings }>();

// Middleware
app.use('*', cors());

// Login
app.post('/login', async (c) => {
  const { username, password } = await c.req.json();
  // Verify credentials, create session
  // ...
});

// Execute request with credential (mirrors the shipped POST /api/v1/execute:
// a flat body with credential/method/url, not a nested `params` object)
app.post('/api/v1/execute', async (c) => {
  const { credential, method, url, headers, body } = await c.req.json();

  // Get encrypted credential from KV
  const encryptedCred = await c.env.CREDENTIALS.get(credential);
  if (!encryptedCred) {
    return c.json({ error: 'Credential not found' }, 404);
  }

  // Decrypt credential
  const cred = await decrypt(encryptedCred, c.env.ENCRYPTION_KEY);

  // Execute request with injected auth
  const response = await executeWithCredential(cred, { method, url, headers, body });

  return c.json(response);
});

export default app;

4. Credential Encryption

Use Web Crypto API for encryption:

async function encrypt(data: string, key: string): Promise<string> {
  const encoder = new TextEncoder();
  const keyMaterial = await crypto.subtle.importKey(
    'raw',
    encoder.encode(key),
    'PBKDF2',
    false,
    ['deriveBits', 'deriveKey']
  );

  const derivedKey = await crypto.subtle.deriveKey(
    {
      name: 'PBKDF2',
      salt: encoder.encode('vultrino-salt'),
      iterations: 100000,
      hash: 'SHA-256',
    },
    keyMaterial,
    { name: 'AES-GCM', length: 256 },
    false,
    ['encrypt', 'decrypt']
  );

  const iv = crypto.getRandomValues(new Uint8Array(12));
  const encrypted = await crypto.subtle.encrypt(
    { name: 'AES-GCM', iv },
    derivedKey,
    encoder.encode(data)
  );

  // Return iv + ciphertext as base64
  return btoa(String.fromCharCode(...iv, ...new Uint8Array(encrypted)));
}

5. Deploy

# Set secrets
wrangler secret put ENCRYPTION_KEY
wrangler secret put ADMIN_PASSWORD_HASH

# Deploy
wrangler deploy

Limitations

FeatureStatusNotes
HTTP ProxyVia fetch()
Web UIWorkers Sites
MCP ServerRequires stdio (not available)
File Storage⚠️Use KV instead
OS KeychainNot available

Security Considerations

  1. Secrets Management — Use Wrangler secrets for encryption keys
  2. KV Encryption — Always encrypt credentials before storing in KV
  3. Access Control — Use Cloudflare Access for additional auth layer
  4. Audit Logging — Log to Workers Analytics or external service

Alternative: Hybrid Deployment

For MCP support, consider a hybrid approach:

┌─────────────────┐     ┌─────────────────┐     ┌─────────────────┐
│  AI Agent       │────▶│  Local Vultrino │────▶│  Cloudflare     │
│  (MCP)          │     │  (MCP Server)   │     │  Vultrino Edge  │
└─────────────────┘     └─────────────────┘     └─────────────────┘
                               │
                               ▼
                        Syncs credentials
                        from edge storage

This allows:

  • MCP support via local instance
  • Centralized credential management on edge
  • Web UI accessible globally

Docker Deployment

Run Vultrino in containers for easy deployment and isolation.

Quick Start

docker run -d \
  --name vultrino \
  -p 7879:7879 \
  -e VULTRINO_PASSWORD=your-secure-password \
  -v vultrino-data:/data \
  ghcr.io/feirai/vultrino:latest web

Dockerfile

FROM rust:1.75-slim as builder

WORKDIR /app
COPY . .
RUN cargo build --release

FROM debian:bookworm-slim

RUN apt-get update && apt-get install -y ca-certificates && rm -rf /var/lib/apt/lists/*

COPY --from=builder /app/target/release/vultrino /usr/local/bin/

# Create non-root user
RUN useradd -r -s /bin/false vultrino
USER vultrino

WORKDIR /data
VOLUME ["/data"]

EXPOSE 7879

ENTRYPOINT ["vultrino"]
CMD ["web"]

Docker Compose

Create docker-compose.yml:

version: '3.8'

services:
  vultrino-web:
    image: ghcr.io/feirai/vultrino:latest
    command: web --bind 0.0.0.0:7879
    ports:
      - "7879:7879"
    environment:
      - VULTRINO_PASSWORD=${VULTRINO_PASSWORD}
    volumes:
      - vultrino-data:/data
      - ./config.toml:/etc/vultrino/config.toml:ro
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:7879/login"]
      interval: 30s
      timeout: 10s
      retries: 3

volumes:
  vultrino-data:

Create .env:

VULTRINO_PASSWORD=your-secure-password

Run:

docker-compose up -d

With Traefik (Reverse Proxy)

version: '3.8'

services:
  traefik:
    image: traefik:v2.10
    command:
      - "--api.insecure=true"
      - "--providers.docker=true"
      - "--entrypoints.web.address=:80"
      - "--entrypoints.websecure.address=:443"
      - "--certificatesresolvers.letsencrypt.acme.httpchallenge=true"
      - "--certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=web"
      - "[email protected]"
      - "--certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json"
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - letsencrypt:/letsencrypt

  vultrino:
    image: ghcr.io/feirai/vultrino:latest
    command: web --bind 0.0.0.0:7879
    environment:
      - VULTRINO_PASSWORD=${VULTRINO_PASSWORD}
    volumes:
      - vultrino-data:/data
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.vultrino.rule=Host(`vultrino.yourdomain.com`)"
      - "traefik.http.routers.vultrino.entrypoints=websecure"
      - "traefik.http.routers.vultrino.tls.certresolver=letsencrypt"
      - "traefik.http.services.vultrino.loadbalancer.server.port=7879"

volumes:
  vultrino-data:
  letsencrypt:

Kubernetes

Deployment

apiVersion: apps/v1
kind: Deployment
metadata:
  name: vultrino
spec:
  replicas: 1
  selector:
    matchLabels:
      app: vultrino
  template:
    metadata:
      labels:
        app: vultrino
    spec:
      containers:
        - name: vultrino-web
          image: ghcr.io/feirai/vultrino:latest
          args: ["web", "--bind", "0.0.0.0:7879"]
          ports:
            - containerPort: 7879
          env:
            - name: VULTRINO_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: vultrino-secrets
                  key: password
          volumeMounts:
            - name: data
              mountPath: /data
      volumes:
        - name: data
          persistentVolumeClaim:
            claimName: vultrino-pvc
---
apiVersion: v1
kind: Service
metadata:
  name: vultrino
spec:
  selector:
    app: vultrino
  ports:
    - port: 80
      targetPort: 7879
---
apiVersion: v1
kind: Secret
metadata:
  name: vultrino-secrets
type: Opaque
stringData:
  password: your-secure-password

Building the Image

# Clone repository
git clone https://github.com/FeirAI/vultrino.git
cd vultrino

# Build image
docker build -t vultrino:local .

# Run
docker run -d \
  --name vultrino \
  -p 7879:7879 \
  -e VULTRINO_PASSWORD=test \
  -v vultrino-data:/data \
  vultrino:local web

Environment Variables

VariableDescriptionRequired
VULTRINO_PASSWORDStorage encryption passwordYes
VULTRINO_CONFIGPath to config fileNo
RUST_LOGLog levelNo

Volumes

PathDescription
/dataCredential storage and state
/etc/vultrino/config.tomlConfiguration file (optional)

Initializing in Docker

# Initialize (creates admin credentials)
docker run -it --rm \
  -v vultrino-data:/data \
  ghcr.io/feirai/vultrino:latest \
  init

# Add a credential
docker run -it --rm \
  -e VULTRINO_PASSWORD=your-password \
  -v vultrino-data:/data \
  ghcr.io/feirai/vultrino:latest \
  add --alias github-api --key ghp_xxx

Health Checks

# Check the web UI
curl -f http://localhost:7879/login

# Check the JSON API (no auth required)
curl -f http://localhost:7879/api/v1/health

CLI Reference

The Vultrino CLI provides complete control over credential management, server operations, and administration.

Global Options

vultrino [OPTIONS] <COMMAND>

Options:
  -c, --config <FILE>    Path to config file
  -v, --verbose          Enable verbose output
  -h, --help             Print help
  -V, --version          Print version

Environment Variables

VariableDescription
VULTRINO_PASSWORDStorage encryption password (required)
VULTRINO_CONFIGPath to config file
RUST_LOGLog level (trace, debug, info, warn, error)

Commands

init

Initialize a new Vultrino instance.

vultrino init [OPTIONS]

Options:
  --force    Overwrite existing configuration

This command:

  1. Creates the credentials storage file
  2. Prompts for admin username and password
  3. Sets up initial configuration

Example:

export VULTRINO_PASSWORD="your-secure-password"
vultrino init
# Enter admin username: admin
# Enter admin password: ********

add

Add a new credential to storage.

vultrino add [OPTIONS]

Options:
  -a, --alias <ALIAS>         Human-readable name (required)
  -t, --type <TYPE>           Credential type [default: api_key]
  -k, --key <KEY>             API key or secret value
  -u, --username <USERNAME>   Username (for basic auth)
  -p, --password <PASSWORD>   Password (for basic auth)
  --description <DESC>        Optional description

Credential Types:

  • api_key — API key or token
  • basic_auth — Username and password
  • oauth2 — OAuth2 credentials (client ID, secret, tokens)
  • private_key — SSH or signing key

Examples:

# Add an API key
vultrino add --alias github-api --key ghp_xxx...

# Add basic auth credentials
vultrino add --alias jira-api --type basic_auth \
  --username [email protected] --password secret123

# Add with description
vultrino add --alias stripe-api --key sk_live_xxx \
  --description "Production Stripe key"

list

List all stored credentials.

vultrino list [OPTIONS]

Options:
  --json    Output as JSON

Example:

vultrino list
# ID                                    Alias         Type      Created
# 550e8400-e29b-41d4-a716-446655440000  github-api    api_key   2024-01-15
# 6ba7b810-9dad-11d1-80b4-00c04fd430c8  stripe-api    api_key   2024-01-16

info

Show details about a specific credential — metadata only, never the secret.

vultrino info <ALIAS>

Accepts a credential alias or id.

Example:

vultrino info github-api
# Alias: github-api
# Type: api_key
# Created: 2024-01-15T10:30:00Z
# Description: GitHub personal access token

remove

Remove a credential from storage.

vultrino remove <ALIAS>

Accepts a credential alias or id.

Example:

vultrino remove old-api-key
# Deleted credential: old-api-key

serve

Run the MCP server (with --mcp). serve on its own no longer starts an API server — it prints a message directing you to vultrino web and exits without binding. Use vultrino web for the HTTP JSON API and admin UI, and vultrino mcp (or vultrino serve --mcp) for the MCP stdio server.

vultrino serve [OPTIONS]

Options:
  -b, --bind <ADDR>    Bind address for the legacy stub [default: 127.0.0.1:7878]
  --mcp                Start as MCP server (stdio transport) — same as `vultrino mcp`

Examples:

# Start MCP server for AI agents
vultrino serve --mcp   # equivalently: vultrino mcp

# `vultrino serve` alone does NOT serve the API — use `vultrino web` instead

web

Start the HTTP JSON API and web administration UI (the same process serves both, plus the /mcp and /llm connector routes).

vultrino web [OPTIONS]

Options:
  -b, --bind <ADDR>    Bind address [default: 127.0.0.1:7879]

Example:

vultrino web
# API + Web UI available at http://127.0.0.1:7879

request

Make an authenticated HTTP request.

vultrino request [OPTIONS] <CREDENTIAL> <URL>

Arguments:
  <CREDENTIAL>                Credential alias to use for authentication
  <URL>                       Target URL

Options:
  -X, --method <METHOD>       HTTP method [default: GET]
  -H, --header <HEADER>       Additional headers (can be repeated)
  -d, --data <DATA>           Request body (JSON string, or @filename to read from a file)
  -q, --quiet                 Output only the response body (no status info)

Examples:

# Simple GET request (credential alias is the first positional argument)
vultrino request github-api https://api.github.com/user

# POST with JSON body
vultrino request stripe-api \
  -X POST \
  -H "Content-Type: application/json" \
  -d '{"amount": 1000, "currency": "usd"}' \
  https://api.stripe.com/v1/charges

# Body only
vultrino request api-key https://api.example.com/data --quiet

role

Manage RBAC roles.

vultrino role <SUBCOMMAND>

Subcommands:
  create    Create a new role
  list      List all roles
  get       Get role details
  delete    Delete a role

Create a role:

vultrino role create <NAME> [OPTIONS]

Options:
  -p, --permissions <PERMS>    Comma-separated: read,write,update,delete,execute
  -s, --scopes <SCOPES>        Credential patterns (glob): "github-*,stripe-*"
  -d, --description <DESC>     Role description

Examples:

# Create read-only role
vultrino role create readonly --permissions read

# Create role for GitHub credentials only
vultrino role create github-executor \
  --permissions read,execute \
  --scopes "github-*" \
  --description "Execute requests with GitHub credentials"

# List roles
vultrino role list

# Delete role
vultrino role delete old-role

key

Manage API keys for programmatic access.

vultrino key <SUBCOMMAND>

Subcommands:
  create    Create a new API key
  list      List all API keys
  revoke    Revoke an API key

Create an API key:

vultrino key create <NAME> [OPTIONS]

Options:
  -r, --role <ROLE>           Role to assign (required)
  -e, --expires <DURATION>    Expiration (e.g., "30d", "1y")

Examples:

# Create key with role
vultrino key create my-app --role github-executor
# Created API key: vk_a1b2c3d4e5f6...
# (Save this key - it won't be shown again)

# Create key with expiration
vultrino key create temp-key --role readonly --expires 7d

# List keys
vultrino key list

# Revoke key
vultrino key revoke vk_a1b2c3d4

completions

Generate shell completions.

vultrino completions <SHELL>

Shells:
  bash, zsh, fish, powershell

Examples:

# Bash
vultrino completions bash > /etc/bash_completion.d/vultrino

# Zsh
vultrino completions zsh > ~/.zfunc/_vultrino

# Fish
vultrino completions fish > ~/.config/fish/completions/vultrino.fish

Exit Codes

CodeMeaning
0Success
1General error
2Invalid arguments
3Credential not found
4Permission denied
5Storage error

Tips

Use Aliases

Add to your shell profile:

alias vreq='vultrino request'
alias vcred='vultrino list'

Store Password Securely

On macOS, use Keychain:

security add-generic-password -a vultrino -s vultrino -w "your-password"
export VULTRINO_PASSWORD=$(security find-generic-password -a vultrino -s vultrino -w)

On Linux, use a secrets manager or environment file:

# ~/.vultrino-env (chmod 600)
export VULTRINO_PASSWORD="your-password"

# In shell profile
source ~/.vultrino-env

Web UI

The Vultrino Web UI provides a browser-based interface for managing credentials, roles, and API keys.

Overview

The web interface offers:

  • Dashboard with usage statistics
  • Credential management (add, view, delete)
  • Role-based access control configuration
  • API key generation and management
  • Audit log viewing

Starting the Web UI

export VULTRINO_PASSWORD="your-password"
vultrino web
# Web UI available at http://127.0.0.1:7879

Custom bind address:

vultrino web --bind 0.0.0.0:8080

Authentication

The web UI requires authentication with the admin credentials set during vultrino init.

Login

Navigate to http://127.0.0.1:7879 and enter:

  • Username: The admin username set during init
  • Password: The admin password set during init

Sessions expire after 24 hours of inactivity.

Changing Admin Password

Currently, to change the admin password:

  1. Delete the admin configuration:

    rm ~/.vultrino/admin.json
    
  2. Reinitialize:

    vultrino init
    

Pages

Dashboard

The main dashboard displays:

  • Total credentials stored
  • Number of roles configured
  • Active API keys
  • Recent audit activity

Credentials

List View (/credentials)

  • Shows all stored credentials
  • Displays alias, type, and creation date
  • Credentials are never shown in the UI

Add Credential (/credentials/new)

  • Form to add new credentials
  • Supported types: API Key, Basic Auth
  • Optional description field

Delete Credential

  • Click delete button on credential row
  • Confirmation required

Roles

List View (/roles)

  • Shows all configured roles
  • Displays permissions and credential scopes

Create Role (/roles/new)

  • Name and description
  • Permission checkboxes:
    • Read — List credentials
    • Write — Create credentials
    • Update — Modify credentials
    • Delete — Remove credentials
    • Execute — Use credentials for requests
  • Credential scopes (glob patterns)

API Keys

List View (/keys)

  • Shows all API keys (prefix only)
  • Displays assigned role and expiration
  • Shows last used timestamp

Create Key (/keys/new)

  • Key name for identification
  • Role selection dropdown
  • Optional expiration date

Revoke Key

  • Click revoke button on key row
  • Immediate revocation, no confirmation

Audit Log

View (/audit)

  • Recent credential usage events
  • Shows timestamp, action, credential used
  • IP address and user agent when available

Configuration

Session Settings

Configure in config.toml:

[web]
session_timeout = 86400  # 24 hours in seconds
cookie_secure = true     # Require HTTPS for cookies

Binding

For production, always bind to localhost and use a reverse proxy:

[web]
bind = "127.0.0.1:7879"

Security Considerations

HTTPS

The web UI should always be accessed over HTTPS in production. Use a reverse proxy like nginx or Caddy for TLS termination.

Session Security

  • Sessions are stored server-side
  • Session IDs are cryptographically random
  • Cookies are HTTP-only and secure (when behind HTTPS)

CSRF Protection

Forms include CSRF tokens to prevent cross-site request forgery.

Rate Limiting

Login attempts are rate-limited to prevent brute force attacks.

Screenshots

Login Page

┌─────────────────────────────────────────┐
│           Vultrino                      │
│                                         │
│  ┌─────────────────────────────────┐    │
│  │ Username                        │    │
│  └─────────────────────────────────┘    │
│  ┌─────────────────────────────────┐    │
│  │ Password                        │    │
│  └─────────────────────────────────┘    │
│                                         │
│  [ Sign in ]                            │
│                                         │
└─────────────────────────────────────────┘

Dashboard

┌─────────────────────────────────────────────────────────────┐
│  Vultrino    Credentials  Roles  API Keys  Audit   [Logout] │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  ┌─────────┐  ┌─────────┐  ┌─────────┐  ┌─────────┐        │
│  │    5    │  │    3    │  │    2    │  │   127   │        │
│  │ Creds   │  │ Roles   │  │ Keys    │  │ Requests│        │
│  └─────────┘  └─────────┘  └─────────┘  └─────────┘        │
│                                                             │
│  Recent Activity                                            │
│  ───────────────────────────────────────────────────        │
│  10:30  github-api     GET /user                           │
│  10:28  stripe-api     POST /v1/charges                    │
│  10:25  github-api     GET /repos                          │
│                                                             │
└─────────────────────────────────────────────────────────────┘

Credentials List

┌─────────────────────────────────────────────────────────────┐
│  Credentials                              [ + New Credential]│
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  Alias          Type        Created         Actions         │
│  ─────────────────────────────────────────────────────      │
│  github-api     api_key     Jan 15, 2024    [Delete]        │
│  stripe-api     api_key     Jan 16, 2024    [Delete]        │
│  jira-api       basic_auth  Jan 17, 2024    [Delete]        │
│                                                             │
└─────────────────────────────────────────────────────────────┘

Troubleshooting

"Invalid credentials" error

  • Verify username and password match those set during vultrino init
  • Check that admin.json exists in the data directory

Session expires immediately

  • Ensure cookies are enabled in your browser
  • If using HTTPS, verify cookie_secure matches your setup

Cannot access from external machine

  • By default, the web UI binds to localhost only
  • Use a reverse proxy to expose it securely
  • Never bind directly to 0.0.0.0 in production without TLS

Blank page or errors

  • Check browser console for JavaScript errors
  • Verify the server is running: curl http://127.0.0.1:7879/login
  • Check server logs for errors

HTTP API (Credential Broker)

Vultrino runs actions with a credential on the caller's behalf and injects the secret in-path, so the client makes authenticated calls without ever seeing the credential. There is no transparent forwarding proxy and no credential header — a caller names a credential alias and an action, and Vultrino executes it through the enforced Policy Enforcement Point.

This page is a task-oriented overview. The complete, code-verified route table (every path, header, request/response shape, and error code) lives in docs/dev/API.md. Where this page and the dev reference differ, the dev reference wins.

The server

The JSON API is served by vultrino web, the same process that serves the HTML admin panel:

export VULTRINO_PASSWORD="your-password"
vultrino web
# Listening on http://127.0.0.1:7879

Custom bind address:

vultrino web --bind 0.0.0.0:8080

The server speaks plaintext HTTP; terminate TLS at a reverse proxy for network exposure. vultrino serve does not serve this API — see the CLI reference.

Authentication

Every JSON API route under /api/v1/ (except /api/v1/health) authenticates a bearer token:

Authorization: Bearer vk_your_api_key      # an API key
Authorization: Bearer vut_your_use_token   # a single-/multi-use scoped token

A vut_ prefix is recognized as a use token (scoped to one credential/action with optional use limits); anything else is validated as an API key. Admin routes additionally require an API key whose role holds the admin permission (use tokens are rejected).

Running an action — POST /api/v1/execute

The credential broker. Vultrino resolves the credential by alias, injects the secret, runs the action, scrubs the response, and returns it — the secret never leaves the vault.

curl -sX POST http://127.0.0.1:7879/api/v1/execute \
  -H "Authorization: Bearer vk_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
        "credential": "github-api",
        "method": "GET",
        "url": "https://api.github.com/user"
      }'

The request body is flat (not nested under params):

FieldRequiredNotes
credentialyesCredential alias.
methodyesHTTP method.
urlyesTarget URL (must be a public host — SSRF guard).
actionnoCanonical plugin.action or a govder action label. Omitted → http.request.
headersnoExtra request headers.
bodynoRequest body (JSON).
querynoQuery params.

Success (200) returns the upstream status, headers, and body (a string, after egress scrub):

{ "status": 200, "headers": { "content-type": "application/json" }, "body": "…" }

Approval required (202) — the action did not run; poll the returned approval_id:

{
  "outcome": "pending_approval",
  "approval_id": "appr_…",
  "message": "This action requires human approval before it runs. … Poll GET /api/v1/approvals/{id} …",
  "summary": "http.request on deploy-hook",
  "expires_at": "2026-06-20T11:30:00Z"
}

Poll GET /api/v1/approvals/{id} with the same bearer; the action runs at most once, on the first poll after a human approves. If the serving process crashes mid-execution, recovery is fail-closed — the action is not silently re-run; the approval is finalized as outcome unknown and must be re-approved to retry.

Injection by credential type

Vultrino formats the injected auth from the stored credential type — for example api_key becomes Authorization: Bearer <key> (or a custom header per the credential's header_name/header_prefix), basic_auth becomes Authorization: Basic <base64(user:pass)>, and oauth2 injects (and refreshes) the access token. The client sends none of these; it only names the alias.

Listing credentials — GET /api/v1/credentials

Metadata only — secrets are never returned. Requires the read permission and is filtered to the caller's role scope.

{ "credentials": [ { "alias": "github-api", "credential_type": "api_key", "description": "…" } ] }

There is no GET /api/v1/credentials/{alias} route. Credential writes (POST /api/v1/credentials, DELETE /api/v1/credentials/{id}) are admin-only — see Admin API.

Other surfaces on the same server

The web process also serves, on the same port:

  • POST /mcp — the networked MCP transport (JSON-RPC), for remote agent harnesses. See MCP Server.
  • POST /llm and /llm/channels/{channel}/… — the metered LLM proxy: point a harness's model base_url here so the provider key stays in the vault and token spend is metered. Providers are default-deny (VULTRINO_PROVIDER_*_ENABLED). See the LLM proxy reference.
  • POST /api/v1/workload/exchange and GET /api/v1/runtime/control — the workload-identity token exchange (a signed vwa_ assertion is traded for short-lived use tokens) and its non-consuming liveness lease.

Error responses

JSON API errors are { "error": "message", "code": "machine_code" }. Common: 400 execute_error (policy denied, credential not found, SSRF block, plugin error), 401 missing_api_key / invalid_api_key / invalid_token, 403 permission_denied / not_admin / token_unusable, 404 *_not_found.

Security

  • Bind to localhost and put a TLS-terminating reverse proxy (nginx, Caddy) in front for any network exposure.
  • Default-deny policy. A credential matching no policy is denied unless [enforcement] default_action = "allow". See Policy Configuration.
  • Egress scrub. Responses are scanned and the credential's own reflected secret is redacted before return; [[egress]] rules can block or redact further. See Configuration.

MCP Server

The Model Context Protocol (MCP) server allows AI agents to make authenticated API requests without accessing the actual credentials.

Overview

MCP is a protocol for AI assistants to interact with external tools and services. Vultrino's MCP server provides tools for:

  • Listing available credentials (by alias only)
  • Making authenticated HTTP requests
  • Managing credentials (with proper permissions)
┌──────────────────┐     ┌──────────────────┐     ┌──────────────────┐
│   AI Agent       │────▶│   Vultrino MCP   │────▶│   External       │
│   (Claude, etc.) │     │   Server         │     │   APIs           │
└──────────────────┘     └──────────────────┘     └──────────────────┘
        │                        │
        │  "Use github-api       │  Credential never
        │   to fetch user"       │  exposed to agent
        │                        │

Starting the MCP Server

export VULTRINO_PASSWORD="your-password"
vultrino mcp

The MCP server uses stdio transport, communicating via stdin/stdout. A remote harness can instead reach the networked MCP transport at POST /mcp on vultrino web (JSON-RPC, authenticated by a Bearer vk_/vut_ token).

API Key Authentication

There is no authenticate tool and no session step. Every tool call carries an api_key argument — a regular API key (vk_…) or a use token (vut_…) — which Vultrino validates per call and scopes to that principal's role. The argument is consumed by Vultrino and never forwarded to the target API.

{
  "tool": "list_credentials",
  "arguments": { "api_key": "vk_your_api_key_here" }
}

Setup:

  1. Admin creates an API key: vultrino key create ai-agent --role executor
  2. The agent includes that key as api_key in every tool call.

A tool call with a missing or invalid api_key is rejected. A use-token (vut_) agent sees only the named capabilities its token grants (plus check_approval), not the generic built-in tools below — those are for a direct operator holding a vk_ key.

Configuring AI Clients

Claude Desktop

Add to ~/Library/Application Support/Claude/claude_desktop_config.json:

{
  "mcpServers": {
    "vultrino": {
      "command": "/path/to/vultrino",
      "args": ["mcp"],
      "env": {
        "VULTRINO_PASSWORD": "your-password"
      }
    }
  }
}

Claude Code (CLI)

Add to your MCP configuration:

{
  "mcpServers": {
    "vultrino": {
      "command": "vultrino",
      "args": ["mcp"],
      "env": {
        "VULTRINO_PASSWORD": "your-password"
      }
    }
  }
}

Important: The agent must include its api_key (a vk_ key or vut_ use token) in every tool call; there is no separate authenticate step.

Generic MCP Client

import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";

const transport = new StdioClientTransport({
  command: "vultrino",
  args: ["mcp"],
  env: {
    VULTRINO_PASSWORD: process.env.VULTRINO_PASSWORD
  }
});

const client = new Client({
  name: "my-ai-app",
  version: "1.0.0"
});

await client.connect(transport);

// Every tool call carries the api_key (vk_ or vut_) — no separate authenticate step
await client.callTool("list_credentials", {
  api_key: process.env.VULTRINO_API_KEY
});

Available Tools

list_credentials

List all available credential aliases.

Input: None

Output:

{
  "credentials": [
    {
      "alias": "github-api",
      "type": "api_key",
      "description": "GitHub personal access token"
    },
    {
      "alias": "stripe-api",
      "type": "api_key",
      "description": "Stripe API key"
    }
  ]
}

Example prompt:

"What credentials are available?"

http_request

Make an authenticated HTTP request.

Input:

{
  "credential": "github-api",
  "method": "GET",
  "url": "https://api.github.com/user",
  "headers": {
    "Accept": "application/json"
  },
  "body": null
}

Output:

{
  "status": 200,
  "headers": {
    "content-type": "application/json"
  },
  "body": "{\"login\": \"username\", ...}"
}

Example prompts:

"Use github-api to get my user profile" "Make a POST request to Stripe to create a customer using stripe-api"

get_credential_info

Return metadata (type, description) for one credential — never the secret.

Input:

{
  "api_key": "vk_...",
  "credential": "github-api"
}

check_approval

Poll an action that was gated for human approval. Once approved, this tool runs the original action and returns its result; while pending it reports the status and tells the agent to keep polling. An agent may only poll approvals it created (same api_key/use token).

Input:

{
  "api_key": "vk_...",
  "approval_id": "appr_..."
}

Note: Adding or deleting credentials is not exposed as an MCP tool. Use the CLI (vultrino add / vultrino remove) or the admin JSON API. The built-in MCP tools are list_credentials, http_request, get_credential_info, and check_approval, plus any tools contributed by installed plugins or granted capabilities.

Security Model

Credential Isolation

The MCP server never exposes actual credential values to the AI agent. The agent only sees:

  • Credential aliases
  • Credential types
  • Descriptions

Permission Checks

If RBAC is enabled, the MCP server checks:

  1. API key validity (from session or configuration)
  2. Role permissions (read, execute, write, delete)
  3. Credential scope restrictions

Audit Trail

All MCP tool calls are logged:

2024-01-15T10:30:00Z MCP http_request credential=github-api url=https://api.github.com/user

Tool Descriptions

The MCP server provides rich tool descriptions to help AI agents understand capabilities:

{
  "name": "http_request",
  "description": "Make an authenticated HTTP request using a stored credential alias; authentication is injected inside the trusted connector.",
  "inputSchema": {
    "type": "object",
    "properties": {
      "credential": {
        "type": "string",
        "description": "Alias of the credential to use for authentication"
      },
      "method": {
        "type": "string",
        "enum": ["GET", "POST", "PUT", "PATCH", "DELETE"],
        "description": "HTTP method"
      },
      "url": {
        "type": "string",
        "description": "Target URL"
      },
      "headers": {
        "type": "object",
        "description": "Additional headers to include"
      },
      "body": {
        "type": "string",
        "description": "Request body (for POST, PUT, PATCH)"
      }
    },
    "required": ["credential", "method", "url"]
  }
}

Example Conversations

Listing and Using Credentials

User: "What API credentials do I have available?"

AI Agent: calls list_credentials tool

AI Agent: "You have the following credentials available:

  • github-api - GitHub personal access token
  • stripe-api - Stripe API key"

User: "Get my GitHub profile"

AI Agent: calls http_request with credential=github-api

AI Agent: "Your GitHub profile shows you're logged in as 'username' with 50 public repos..."

Making Authenticated Requests

User: "Create a new Stripe customer with email [email protected]"

AI Agent: calls http_request tool

{
  "credential": "stripe-api",
  "method": "POST",
  "url": "https://api.stripe.com/v1/customers",
  "headers": {
    "Content-Type": "application/x-www-form-urlencoded"
  },
  "body": "[email protected]"
}

AI Agent: "I've created a new Stripe customer. The customer ID is cus_xxx..."

Troubleshooting

MCP server not starting

  • Verify VULTRINO_PASSWORD is set
  • Check credentials file exists (vultrino list should work)
  • Look for error messages in stderr

Tool not found

  • Ensure you're using the latest Vultrino version
  • Verify MCP server started successfully
  • Check client configuration

Permission denied

  • Verify the API key (if RBAC enabled) has execute permission
  • Check credential scope restrictions
  • Review audit logs for denial reasons

Connection timeout

  • MCP uses stdio transport; ensure no other process is consuming stdin
  • Check that Vultrino binary path is correct
  • Verify environment variables are passed correctly

Best Practices

For AI Developers

  1. Don't ask for credentials — Always use aliases, never actual secrets
  2. Use descriptive aliases — Help the AI understand what each credential is for
  3. Set up RBAC — Create restricted roles for AI agent access
  4. Review audit logs — Monitor what requests agents are making

For System Administrators

  1. Use short-lived credentials — Rotate frequently
  2. Scope credentials narrowly — Each credential should do one thing
  3. Enable audit logging — Track all credential usage
  4. Review agent behavior — Periodically check what the AI is doing

Managing Credentials

This guide covers how to securely store, organize, and use credentials with Vultrino.

Credential Types

Vultrino supports several credential types:

TypeUse CaseAuth Header Format
api_keyAPI tokens, bearer tokensAuthorization: Bearer <key>
basic_authUsername/password authAuthorization: Basic <base64>
oauth2OAuth2 flowsAuthorization: Bearer <access_token>
private_keySSH keys, signing keys(used for signing, not HTTP)

Adding Credentials

API Keys

Most common type for SaaS APIs:

# GitHub Personal Access Token
vultrino add --alias github-api --key ghp_xxxxxxxxxxxx

# Stripe API Key
vultrino add --alias stripe-api --key sk_live_xxxxxxxxxxxx

# OpenAI API Key
vultrino add --alias openai --key sk-xxxxxxxxxxxx

# With description
vultrino add --alias anthropic --key sk-ant-xxxx \
  --description "Claude API key for production"

Basic Auth

For APIs using username/password:

vultrino add --alias jira-api --type basic_auth \
  --username [email protected] \
  --password api_token_here

vultrino add --alias jenkins --type basic_auth \
  --username admin \
  --password jenkins_token

OAuth2 Credentials

For OAuth2 flows with refresh tokens:

vultrino add --alias google-api --type oauth2 \
  --client-id "xxx.apps.googleusercontent.com" \
  --client-secret "GOCSPX-xxx" \
  --refresh-token "1//xxx"

Organizing Credentials

Naming Conventions

Use a consistent naming scheme:

<provider>-<environment>-<scope>

Examples:

  • github-prod-readonly
  • stripe-test-charges
  • aws-staging-s3

Using Prefixes

Group related credentials with prefixes:

# All GitHub credentials
vultrino add --alias github-api-readonly --key ghp_read...
vultrino add --alias github-api-admin --key ghp_admin...

# All Stripe credentials
vultrino add --alias stripe-live --key sk_live...
vultrino add --alias stripe-test --key sk_test...

Then restrict access with role scopes:

vultrino role create github-readonly --scopes "github-*" --permissions read,execute

Listing Credentials

Basic List

vultrino list

# Output:
# ID                                    Alias           Type        Created
# 550e8400-e29b-41d4-a716-446655440000  github-api      api_key     2024-01-15
# 6ba7b810-9dad-11d1-80b4-00c04fd430c8  stripe-live     api_key     2024-01-16

JSON Output

vultrino list --json | jq '.[] | select(.type == "api_key")'

Via Web UI

Navigate to /credentials in the web interface for a visual list.

Using Credentials

CLI Request

vultrino request github-api https://api.github.com/user

HTTP API

curl -sX POST http://localhost:7879/api/v1/execute \
     -H "Authorization: Bearer vk_your_api_key" \
     -H "Content-Type: application/json" \
     -d '{"credential": "github-api", "method": "GET", "url": "https://api.github.com/user"}'

MCP (AI Agents)

The AI agent uses the credential alias:

AI: "Using github-api to fetch your profile..."
*Makes request without seeing actual token*

Updating Credentials

Currently, to update a credential:

  1. Remove the old credential:

    vultrino remove github-api
    
  2. Add the new one:

    vultrino add --alias github-api --key ghp_newtoken
    

Future versions will support in-place updates.

Deleting Credentials

CLI

vultrino remove old-api-key
# Deleted credential: old-api-key

Web UI

Click the delete button on the credentials list, then confirm.

Security Best Practices

1. Use Descriptive Names

Bad:

vultrino add --alias key1 --key xxx

Good:

vultrino add --alias github-ci-readonly --key xxx \
  --description "Read-only token for CI pipeline"

2. Scope Credentials Narrowly

Instead of one admin key:

vultrino add --alias github-admin --key ghp_admin_all_perms

Create scoped credentials:

vultrino add --alias github-repos-read --key ghp_repos_read
vultrino add --alias github-actions-write --key ghp_actions_write

3. Set Expiration Reminders

Track when credentials expire:

vultrino add --alias stripe-api --key sk_live_xxx \
  --description "Expires: 2024-12-31"

4. Rotate Regularly

Set up a rotation schedule:

  1. Generate new credential at source (GitHub, Stripe, etc.)
  2. Add to Vultrino with new alias
  3. Test the new credential
  4. Update applications to use new alias
  5. Delete old credential

5. Audit Usage

Check what's using each credential:

# View audit logs
tail -f /var/log/vultrino/audit.log | grep github-api

Backup and Recovery

Backup

The credentials file is encrypted. Back it up securely:

cp ~/.vultrino/credentials.enc ~/backup/vultrino-$(date +%Y%m%d).enc

Recovery

To restore:

cp ~/backup/vultrino-20240115.enc ~/.vultrino/credentials.enc

You'll need the same VULTRINO_PASSWORD used when the backup was created.

Vultrino intentionally doesn't support exporting credentials in plaintext. This is a security feature, not a limitation.

Troubleshooting

"Credential not found"

  • Check the alias is spelled correctly
  • Verify with vultrino list
  • Credential may have been deleted

"Permission denied"

  • Your API key may not have access to this credential
  • Check role scopes: vultrino role list

"Decryption error"

  • Wrong VULTRINO_PASSWORD
  • Corrupted credentials file
  • Restore from backup if needed

"Invalid credential format"

  • Check credential type matches the data
  • API keys shouldn't have usernames
  • Basic auth requires both username and password

Roles & API Keys

Vultrino's Role-Based Access Control (RBAC) system lets you create scoped API keys for different applications, each with specific permissions.

Overview

┌─────────────┐     ┌─────────────┐     ┌─────────────────────┐
│   API Key   │────▶│    Role     │────▶│  Permissions        │
│   vk_xxx    │     │  executor   │     │  + Credential Scopes│
└─────────────┘     └─────────────┘     └─────────────────────┘
  • API Keys — Authenticate applications
  • Roles — Define permissions and scopes
  • Permissions — What actions are allowed
  • Scopes — Which credentials are accessible

Permissions

PermissionDescription
readList credentials (metadata only, never secrets)
writeCreate new credentials
updateModify existing credentials
deleteRemove credentials
executeUse credentials for authenticated requests

Predefined Roles

Vultrino includes three predefined roles that are always available:

RolePermissionsUse Case
adminread, write, update, delete, executeFull administrative access
executorread, executeAI agents and applications (recommended)
read-onlyreadMonitoring and listing credentials only

These roles cannot be deleted and are available immediately after init.

Creating Custom Roles

Basic Role

# Read-only role (can only list credentials)
vultrino role create readonly --permissions read

# Execute-only role (can use credentials but not manage them)
vultrino role create executor --permissions execute

# Full management role
vultrino role create admin --permissions read,write,update,delete,execute

Scoped Roles

Limit which credentials a role can access using glob patterns:

# Only GitHub credentials
vultrino role create github-user \
  --permissions read,execute \
  --scopes "github-*"

# Only test credentials (no production)
vultrino role create test-executor \
  --permissions execute \
  --scopes "*-test,*-staging"

# Multiple specific patterns
vultrino role create payment-processor \
  --permissions execute \
  --scopes "stripe-*,paypal-*,braintree-*"

With Description

vultrino role create ci-pipeline \
  --permissions read,execute \
  --scopes "github-ci-*" \
  --description "Used by CI/CD pipeline for deployments"

Managing Roles

List Roles

vultrino role list

# Output:
# Name            Permissions                    Scopes
# readonly        read                           (all)
# executor        execute                        (all)
# github-user     read,execute                   github-*

View Role Details

vultrino role get github-user

# Output:
# Name: github-user
# Permissions: read, execute
# Scopes: github-*
# Created: 2024-01-15T10:30:00Z

Delete Role

vultrino role delete old-role

Note: Deleting a role doesn't delete associated API keys, but those keys will no longer work.

Creating API Keys

Basic Key

vultrino key create my-app --role executor
# Output:
# Created API key: vk_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6
#
# IMPORTANT: Save this key now. It won't be shown again.

With Expiration

# Expires in 30 days
vultrino key create temp-access --role readonly --expires 30d

# Expires in 1 year
vultrino key create annual-key --role executor --expires 1y

# Specific date (ISO format)
vultrino key create project-key --role github-user --expires 2024-12-31

Managing API Keys

List Keys

vultrino key list

# Output:
# Prefix      Name         Role          Expires        Last Used
# vk_a1b2... my-app       executor      never          2024-01-15
# vk_x9y8... temp-access  readonly      2024-02-14     2024-01-16

Revoke Key

vultrino key revoke vk_a1b2c3d4
# Revoked API key: vk_a1b2c3d4

Revocation is immediate. Any requests using the key will fail.

Using API Keys

CLI with API Key (No Password Required)

Once vultrino web is running, use CLI commands with just an API key:

# Make request using API key (no VULTRINO_PASSWORD needed)
vultrino --key vk_a1b2c3d4... request github-api https://api.github.com/user

This connects to the running web server's API at http://127.0.0.1:7879.

MCP Server (Per-Request Auth)

Every MCP tool call requires the API key:

{
  "tool": "list_credentials",
  "arguments": {
    "api_key": "vk_a1b2c3d4..."
  }
}
{
  "tool": "http_request",
  "arguments": {
    "api_key": "vk_a1b2c3d4...",
    "credential": "github-api",
    "method": "GET",
    "url": "https://api.github.com/user"
  }
}

This allows multiple AI agents to use the same MCP server with different scoped keys.

HTTP API

Include the API key in the Authorization header:

# List credentials
curl -H "Authorization: Bearer vk_a1b2c3d4..." \
     http://localhost:7879/api/v1/credentials

# Execute request
curl -X POST http://localhost:7879/api/v1/execute \
     -H "Authorization: Bearer vk_a1b2c3d4..." \
     -H "Content-Type: application/json" \
     -d '{"credential": "github-api", "method": "GET", "url": "https://api.github.com/user"}'

Application Configuration

Store the API key in your application's environment:

# .env
VULTRINO_API_KEY=vk_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6
import os
import requests

api_key = os.environ["VULTRINO_API_KEY"]
response = requests.post(
    "http://vultrino:7879/api/v1/execute",
    headers={
        "Authorization": f"Bearer {api_key}",
        "Content-Type": "application/json"
    },
    json={
        "credential": "github-api",
        "method": "GET",
        "url": "https://api.github.com/user"
    }
)

Web UI

Creating Roles

  1. Navigate to Roles in the sidebar
  2. Click New Role
  3. Fill in:
    • Name
    • Description (optional)
    • Select permissions
    • Enter credential scopes (comma-separated)
  4. Click Create Role

Creating API Keys

  1. Navigate to API Keys in the sidebar
  2. Click New API Key
  3. Fill in:
    • Name (for identification)
    • Select a role
    • Expiration date (optional)
  4. Click Create Key
  5. Copy the key immediately — it's only shown once

Common Patterns

Multi-Environment Setup

# Production role (limited credentials)
vultrino role create prod-app \
  --permissions execute \
  --scopes "*-prod"

# Staging role (more credentials)
vultrino role create staging-app \
  --permissions read,execute \
  --scopes "*-staging,*-test"

# Development role (everything except prod)
vultrino role create dev-app \
  --permissions read,write,execute \
  --scopes "*-dev,*-staging,*-test"

Per-Service Keys

# Role for payment processing
vultrino role create payment-service \
  --permissions execute \
  --scopes "stripe-*,paypal-*"

# Create key for the payment service
vultrino key create payment-service-prod --role payment-service

# Role for email service
vultrino role create email-service \
  --permissions execute \
  --scopes "sendgrid-*,mailgun-*"

# Create key for the email service
vultrino key create email-service-prod --role email-service

CI/CD Pipeline

# Read-only for listing credentials in CI
vultrino role create ci-readonly \
  --permissions read \
  --scopes "*"

# Execute for deployments
vultrino role create ci-deploy \
  --permissions execute \
  --scopes "aws-deploy-*,github-ci-*"

# Short-lived keys for CI
vultrino key create ci-read-key --role ci-readonly --expires 7d
vultrino key create ci-deploy-key --role ci-deploy --expires 7d

AI Agent Access

# Limited role for AI agents
vultrino role create ai-agent \
  --permissions read,execute \
  --scopes "github-api,stripe-test"  # Only specific credentials

# Create key for the AI
vultrino key create claude-agent --role ai-agent

Security Best Practices

1. Principle of Least Privilege

Only grant the minimum permissions needed:

# Bad: Full admin access
vultrino role create my-app --permissions read,write,update,delete,execute

# Good: Only what's needed
vultrino role create my-app --permissions execute --scopes "api-needed-*"

2. Use Scopes

Always scope credentials when possible:

# Bad: Access to all credentials
vultrino role create service-role --permissions execute

# Good: Scoped to specific credentials
vultrino role create service-role --permissions execute --scopes "service-*"

3. Set Expiration

Use expiring keys for temporary access:

# Contractor access
vultrino key create contractor-key --role readonly --expires 90d

# CI pipeline (rotate weekly)
vultrino key create ci-key --role ci-deploy --expires 7d

4. Audit Key Usage

Check the audit log for unusual activity:

grep "vk_a1b2" /var/log/vultrino/audit.log

5. Rotate Keys Regularly

Even for long-lived applications, rotate keys periodically:

  1. Create new key
  2. Update application configuration
  3. Verify new key works
  4. Revoke old key

Troubleshooting

"Permission denied"

  • Check the role has the required permission
  • Verify the credential matches the role's scopes
  • Ensure the API key hasn't expired

"API key not found"

  • The key may have been revoked
  • Check for typos in the key
  • Verify the key was created successfully

"Role not found"

  • The role may have been deleted
  • Keys without valid roles won't work
  • Recreate the role or assign a new role to a new key

Use Tokens

A use token is a narrow, ephemeral grant — the opposite of a durable API key. Where an API key is a long-lived identity scoped by a role, a use token authorizes one kind of action against one credential (or glob), optionally capped to a number of uses and/or a time window. It is designed to be handed to an agent for a single task and then forgotten.

Use tokens are recognized by their vut_ prefix and are presented in exactly the same place as an API key — the api_key tool argument (MCP) or an Authorization: Bearer <token> header (HTTP). The underlying credential secret is not part of that agent-facing schema.

Creating a token

# "POST to the deploy webhook once, in the next 10 minutes"
vultrino token create deploy-once \
  --credential deploy-hook --action http.request --uses 1 --expires 10m
# Output: vut_xxxxxxxx...  (shown once — copy it now)

The plaintext token is shown once at creation. Vultrino stores only a hash, so it can never be recovered or re-displayed.

Scoping options

FlagMeaning
--credential <glob>Credential alias or glob the token may use (e.g. deploy-hook, github-*). Required.
--action <glob>Restrict to a single action (http.request) or a plugin glob (postgres.*). Omit for any action.
--uses <N>Cap total executions. --uses 1 is single-use; omit for unlimited.
--expires <dur>Time window: 30m, 24h, 7d. Omit for no expiry.
--require-approvalGate every use behind a human decision — see Action Approvals.

Both scopes are enforced authoritatively in the server at execution time, not just at the edge — a token narrowed to github-* / http.request is rejected for any other credential or action even if a caller tries to use it directly.

The --action scope may also be a govder action label (see Action Labels); it is satisfied by either the label or the canonical action it resolves to.

Strictness (V8)

When minting a token via the admin API, a strictness compiles to enforced settings, so direct and approve-at-checkpoint differ on the wire:

StrictnessCompiles to
directsingle-use (max_uses = 1) + require_approval + dual_control — the gated action requires two distinct approvers (M-of-N, M configurable) before it runs; see Action Approvals
checkpointrequire_approval; does not force single-use — the caller's --uses is preserved (unlimited unless given), so one approval is required per gated action

direct overrides any max_uses/require_approval the caller passed.

Guarantees

  • Fail-closed counting. A use is spent the moment the action runs — before the side effect, and even if the downstream call then errors. A single-use token can never drive two executions.
  • Cross-process atomic. The check-and-increment happens under a cross-process file lock on the vault, so the count holds even when the web UI and the MCP server run as separate processes sharing one encrypted store.
  • Preflight is free. A not-yet-loaded plugin or invalid parameters are caught before a use is consumed, so a misconfigured call doesn't burn the token.

Managing tokens

vultrino token list             # show tokens, their scopes, uses, and status
vultrino token revoke <id>      # immediately disable a token

Tokens are also listed and revocable in the Use Tokens page of the web admin UI.

Relationship to approvals

Add --require-approval to make a token's every use pause for a human. The use is not consumed when the approval is opened — only when the approved action actually runs. A use token's pending approvals are bounded so it can never open more outstanding approvals than it has remaining capacity (uses + pending ≤ max_uses). See Action Approvals.

Action Approvals

Some actions are too consequential to let an agent run unsupervised — a production refund, a DROP TABLE, a deploy. Action approvals put a human in the loop: Vultrino pauses before the action executes, and the agent never sees a result until someone signs off. The decision can be made in the admin panel, from a Telegram button, or via a link delivered by webhook/email.

What triggers an approval

An action is gated if any of these match:

  • The credential is flagged: vultrino meta set <alias> require_approval true
  • The request is authorized by a use token created with --require-approval
  • A policy rule matches with action = "prompt"

What the agent experiences

The flow is designed so the agent clearly understands it is waiting, not failing, and knows how to check back:

  1. The agent calls a tool. Instead of a result it receives an "APPROVAL REQUIRED" message containing an approval_id. The action has not run.
  2. The agent polls with that id — the check_approval MCP tool, GET /api/v1/approvals/{id}, or vultrino approval status <id> --wait.
  3. A human approves or denies it.
  4. On the next poll after approval, Vultrino runs the action and returns the real result. If denied or expired, the agent is told to stop.

Execution happens lazily on that poll, so no background worker is required and the result is delivered the moment the agent next checks.

Action detail — what the approver sees (approval_preview)

A human should approve on the substance of an action, not just its verb: who the money goes to and why, or the actual message and its recipient — not merely "send a payout" or "send a message". Each capability declares which of its call params are worth showing, with an approval_preview spec. Vultrino extracts those field values at approval-open (from the exact params that will execute) and surfaces them on the approval; the operator console and the feir-os Approvals inbox render them under "Requested action details."

Declare it per capability (in the capability upsert, or in feir-os deploy/connectors/capabilities.yaml, which govder carries here verbatim):

approval_preview:
  title: Payout                                 # heading for the detail block
  fields:
    - { label: "To",     path: body.to }        # inline value (default)
    - { label: "Amount", path: body.amount }
    - { label: "Memo",   path: body.memo, format: text }   # wrapped block (a body)
  • path is a dot-path into the tool's call params — body.to reads params["body"]["to"]. Only scalar leaves (string / number / bool) are shown; objects, arrays, and missing keys are skipped, and arrays are never indexed into.
  • format is text for a wrapped block (a message body, a reason) or omitted / inline for a one-line value. Fields render in the order listed.
  • Omit the block entirely and the approval falls back to its one-line summary — no change from today.

What is and isn't exposed. The preview carries only the declared field values — never the raw params blob, and never the credential (the machine GET /api/v1/approvals JSON deliberately withholds params; only the operator HTML console dumps the full blob). Field values are agent-authored and untrusted — an agent controls what ends up in a message body — so every consumer escapes them and keeps them visually distinct from the trusted risk / spend / identity facts. Only name paths to fields that are safe to show a human; never point a path at a secret, token, or api_key. When in doubt, show less — the operator can always open the full record.

No code changes are needed to give a new action type its detail view: declaring the fields is the whole job. See PreviewFieldSpec / extract_preview in src/capability/mod.rs.

Configuration

Enable approvals and configure out-of-band notifiers under [approvals] in config.toml:

[approvals]
enabled = true
ttl_secs = 3600                                   # default Medium-class total window
public_base_url = "https://vultrino.example.com"  # base for approve/deny links
oob_approver_identity = "[email protected]"      # REQUIRED with a notifier (V5): identity OOB links are bound to
reauth_interval_secs = 900                         # optional continuous re-auth (V5)
enforce_separation_of_duty = false                 # hard-reject self-approvals (V5)
dual_control_approvers = 2                          # distinct approvers for dual control (V12)

[approvals.telegram]                              # inline Approve / Deny buttons
bot_token = "123456:ABC-DEF..."
chat_id = "987654321"

[approvals.webhook]                               # POST to any URL (email / Slack / ...)
url = "https://hooks.example.com/vultrino-approvals"
auth_header = "Bearer your-webhook-secret"

# Per-criticality SLA windows (V5): window 1 = Pending→Escalated, window 2 =
# Escalated→Expired. Omitted classes use built-in defaults.
[[approvals.sla]]
class = "critical"
escalate_after_secs = 300
escalate_window_secs = 300

# Assign a criticality class to a (credential, action). First match wins;
# unmatched actions are "medium".
[[approvals.criticality_rules]]
credential_pattern = "pay-*"
action_pattern = "*"
class = "critical"

If approvals are enabled but no notifier is configured, decisions can still be made from the admin panel; Vultrino logs a warning that out-of-band approval is unavailable.

SLA, escalation, and continuous re-authorization (V5)

Every request is assigned a criticality class (low | medium | high | critical) from the first matching [[approvals.criticality_rules]] rule, defaulting to medium. The class drives a two-phase SLA:

  1. First window — while undecided, the request is pending. When the first window elapses it moves to escalated and the configured notifiers are re-pinged (with a panel link; the original one-time decision token is not re-issued).
  2. Second window — an escalated request that is still undecided when the final deadline passes auto-expires (a fail-closed deny). A high/critical request therefore escalates fast and then denies, rather than lingering open indefinitely.

Higher criticality uses shorter windows (built-in defaults: critical 5m+5m, high 15m+15m, low 4h+4h; medium splits the legacy ttl_secs across both phases). Override any class with [[approvals.sla]]. Lifecycle advancement happens both on each agent poll and via a background sweep, so a request nobody is polling still escalates and expires on time. From the agent's side escalated behaves exactly like pending — keep polling.

The credential can shorten the window, and it wins. Whatever the class SLA says, an approval's final deadline is clamped to the remaining life of the use token that will execute the action — an approval must never be offerable past the point where the credential can still honour it. Both phases scale proportionally, so a clamped request still escalates before it expires. If a request arrives with a credential that has under a second left, the approval is refused rather than opened (nothing runs, and nobody is asked to authorize an impossible action). So: to give approvers more time, lengthen the credential, not ttl_secs.

Set reauth_interval_secs to require continuous re-authorization: an approved grant that has not yet run within that window is treated as lapsed and must be re-approved before it can execute, rather than running on a stale decision.

Approver identity and separation of duty (V5)

Every human decision records an authenticated approver identity, not just the channel:

  • Admin panel — the logged-in session user.
  • Out-of-band link — the named oob_approver_identity the link is bound to (rather than an anonymous capability token). This is required when a notifier is configured (enforced at config load); an OOB verdict can never be recorded as the anonymous literal out-of-band — a link with no named identity bound is refused and the action must be decided in the admin panel.
  • CLI — the local OS user (cli:<user>).

A decision with a blank identity is rejected. Because both the requester's owner and the approver are recorded, separation of duty ("the approver must not be the requesting agent") is computed and recorded on every decision (and logged when violated) — an agent self-approving its own request is flagged. Set enforce_separation_of_duty = true to hard-reject a self-approval rather than only recording it (a self-denial is always allowed). The CLI decides as a trusted local admin, so its OS-user identity is advisory.

Dual control (M-of-N) (V12)

A high-risk action can require more than one distinct approver before it runs. A use token minted with strictness: direct (or any token flagged dual_control) opens an approval that needs [approvals] dual_control_approvers distinct sign-offs (default 2) — the action does not execute until the threshold is met:

  • Each approval records a distinct approver sign-off; the same identity can't satisfy two of the required slots (rejected as a duplicate).
  • The request stays pending (the poll response carries approvals_received / approvals_remaining) until enough distinct approvers sign off, then flips to approved and runs on the next poll.
  • A single denial vetoes the whole request regardless of how many approvals were gathered.
  • Separation of duty composes: with enforce_separation_of_duty, a self-approval by the requester is rejected and does not count toward the M-of-N threshold.
[approvals]
dual_control_approvers = 2   # distinct approvers a dual-control request needs (default 2)

Metrics read-back (V12)

GET /api/v1/metrics (admin) returns a structured point-in-time read-back: unauthorized_attempts (tool-call attempts denied by the policy engine or by cross-tenant isolation — counted whether the denial was enforced or, for an observe-mode tenant, merely observed; a per-process in-memory counter, like the rate-limit counters, that resets on restart and counts only this process — not partitioned by tenant), approval counts by state (approvals.by_status, plus dual_control_awaiting), and approval-decision latency percentiles (approval_latency_secs.{count,avg,p50,p95,max}). The approval counts are scoped to the calling key's tenant (approval partitioning) and the response echoes the tenant_scope applied (null for a global admin). The durable, cross-process event history is the signed event outbox.

Telegram/webhook/email links carry a single-use capability token and open a confirmation page rather than deciding on load — so a link prefetch or scanner can't silently approve an action, and the admin session is never required to act on a notification.

Set public_base_url to an HTTPS address so these links stay confidential, and avoid running the web server at DEBUG log level in production: request URIs (which carry the link's capability token) are logged at DEBUG.

Guarantees

  • At most once. An approved action's execution is claimed atomically and fenced by a monotonic execution epoch, so two racing polls can't both run it. A claim left behind by a process that crashed mid-execution is recovered after a timeout fail-closed: because the crashed attempt's side effect may already have fired, the action is not re-run — the approval is finalized terminally with outcome unknown — original worker lost mid-execution; re-approve to retry, so retrying is an explicit human decision rather than a silent double-fire. A transient pre-execution failure (e.g. a plugin not yet loaded), where nothing ran, is still retried rather than marked done.
  • Ownership. An agent may only poll approvals created by the same principal (API key or use token) that made the original request — checked before any execution.
  • Bounded pending approvals. A use token's uses + outstanding pending approvals can never exceed max_uses, enforced atomically under the vault lock — so a single-use token can't flood the approval queue (or the notifier) with requests it could never run.
  • Policy still applies at run time. Policy is re-evaluated when the action finally executes, so an explicit deny rule (URL / method / time-window) blocks even a human-approved action — a human approval is not a policy bypass. Rate limits are charged once, at request time; the deferred re-check never re-charges or re-denies an approved action against the rate limiter. When a deny does fire on resume, the use token is left unconsumed.

Managing approvals

vultrino approval list                  # pending and recent decisions
vultrino approval status <id> [--wait]  # poll one approval (optionally block)
vultrino approval approve <id>
vultrino approval deny <id>

The Approvals page of the web UI shows pending requests with their requester, credential, action, and parameters, and offers Approve / Deny actions.

Kill Switches & Halt (V6)

When an agent misbehaves you need to stop it now, not at the end of its run. Vultrino's halt is a layered kill switch with a deliberately-documented set of achievable semantics — it does not pretend to preempt work a harness gives it no way to preempt.

Halting an agent

# Admin API (Permission::Admin). Convergent: a repeat under the same
# Idempotency-Key RE-ASSERTS the halt rather than replaying it.
curl -XPOST https://vultrino.example.com/api/v1/agents/bot-7/halt \
  -H "Authorization: Bearer vk_admin_..."

The response summarizes the three legs:

{
  "agent_label": "bot-7",
  "revoked_tokens": ["vut_..."],
  "deny_policy_id": "halt:bot-7",
  "policy_active": true,
  "in_flight": [ { "session_id": "...", "credential": "...", "action": "...", "started_at": "..." } ],
  "callbacks_fired": 1
}

Lift a halt with DELETE /api/v1/agents/{label}/halt (the kill policy is removed; already-revoked tokens stay revoked — revocation is permanent, so mint fresh tokens to resume the agent).

What halt actually does — the three legs

  1. Revoke the agent's use tokens. Storage-authoritative and re-checked under the vault lock on every gated call, so it takes effect immediately and across processes (web + MCP).
  2. Install an authoritative per-agent kill policy (principal_pattern = the halt target). The target is matched against the principal's agent label or its key/token id, so it covers both label-bound use-token agents (/api/v1/agents/refund-bot/halt) and an API-key agent that carries no label (halt it by its key id, /api/v1/agents/vk_<id>/halt). It is a kill policy: unlike an ordinary per-agent Deny, it is evaluated before every other policy, so an allow rule that happens to be ordered first can never let a halted agent slip through. It is persisted and propagates to other processes via the policy refresh. (The halt target must be a literal identifier — *?[] globs are rejected — so a halt can't accidentally deny a whole fleet. The kill policy is a normal stored policy: it is visible in, and removable via, the policy admin API, in addition to DELETE …/halt.)

If the immediate in-process engine reload fails (rare), the halt still returns 200 with "policy_active": false: any of the agent's tokens have already been revoked (immediate, cross-process) and the kill policy is persisted, so it activates within the refresh window. The halt is effective within the kill-SLA regardless. (Token revocation matches the target against a token's agent label or its id, so a halt-by-id revokes that token too; an agent that authenticates only by API key has no token to revoke and relies on the kill policy.)

  1. Fire registered abort callbacks for the agent's in-flight sessions.

Achievable semantics (read this before relying on it)

  • Deny-the-next-gated-call is the baseline guarantee, and it is solid: within the kill-SLA, the halted agent's next attempt to use any credential is denied — on every path (MCP and HTTP), on every process. On the process that serves the admin API the kill policy is live immediately; other processes pick it up within the policy-refresh window (a few seconds), while token revocation is immediate everywhere.
  • True preemption of in-flight work is only possible where the harness exposes an abort/pause primitive. Vultrino can't reach into an agent runtime it doesn't control. Register a HaltCallback for harnesses that do expose one; without it, an action already mid-flight runs to completion (its result is still subject to egress controls), and the halt takes hold on the next gated call.

The session registry

Vultrino records in-flight gated executions so a halt can report — and a callback can act on — what an agent is doing right now:

curl https://vultrino.example.com/api/v1/sessions -H "Authorization: Bearer vk_admin_..."

The registry is in-memory and per-process (the same model as the rate-limit counters): it resets on restart, and in a web+MCP deployment each process only sees the executions it is running. The cross-process kill legs (token revoke + kill policy) are not subject to this limitation.

Abort callbacks

A harness integration registers a HaltCallback on the server; on halt it is fired with the agent's in-flight sessions, so an integration that can signal the agent runtime (cancel a task, close a session) can preempt rather than wait for the next gated call. With no callback registered, halt is purely deny-next-gated-call plus token revocation.

Workload Identity & Owner Binding (V10)

Vultrino's principal — the thing a policy's principal_pattern matches and an approval's separation-of-duty is computed against — can be a workload identity resolved from an external identity document, and a vk_/vut_ can carry an IdP-resolvable owner so a non-human identity (NHI) maps to a directory identity.

Trust boundary. The SPIFFE and OIDC resolvers parse and validate an already-verified document. The cloud-IAM resolvers are claim adapters — they map a verified token's claims to a principal but do not perform the cloud-specific cryptographic verification (JWKS fetch / cloud SDK), which is wired at deployment. Signature/issuer verification must happen before resolution — these resolvers trust the document they are handed, so the deployment must terminate mTLS / verify the token at the edge and pass the verified document inbound. The cloud-IAM adapters stay integration-time and are not auto-wired inbound.

Wiring it inbound (R6)

Enable inbound resolution with [identity]: a request carrying the configured header (the already transport-verified SVID or OIDC claims) has its principal resolved from that document before policy evaluationsubject becomes the Principal.id a principal_pattern matches, and owner the SoD owner.

[identity]
kind = "spiffe"                 # spiffe | oidc (the two wireable resolvers)
header = "x-spiffe-verified"    # inbound header carrying the verified document
allowed = ["example.org"]       # SPIFFE trust domains (or OIDC issuers); empty = any

So a principal_pattern Deny on spiffe://example.org/* blocks any request whose presented SVID is in that trust domain, regardless of which vk_/vut_ carried it. A malformed or untrusted document is logged and ignored (the request falls back to its static vk_/vut_ principal) — a bad document can only fail to refine the principal, never elevate it.

The resolved subject is an additional match dimension, never a replacement. The principal's stable vk_/vut_ id remains the halt / ownership anchor, so a halt keyed on the credential always holds even when a workload identity is presented (it can't be escaped by waving an SVID). To halt by a resolved workload identity itself, push a kill/Deny policy with principal_pattern = <the SVID/OIDC subject> through the admin write APIvultrino agent halt targets agent labels / credential ids (which exclude the :// in SVID strings), whereas a policy principal_pattern matches the resolved subject directly.

Resolving a workload identity

The vultrino::identity module turns an identity document into a WorkloadIdentity { kind, subject, trust_domain, owner }:

SourceResolversubjecttrust_domain
SPIFFE/SPIRE SVIDSpiffeResolverthe full spiffe://… IDthe trust domain (optionally allowlisted)
Generic OIDCOidcResolverthe sub claimthe iss (optionally allowlisted); owner from email/preferred_username
AWS IAM (Roles Anywhere)resolve_cloud_iam(AwsIam, …)the assumed-role arnaws
GCP workload identityresolve_cloud_iam(GcpWorkload, …)the service-account emailthe iss
Entra workload identityresolve_cloud_iam(EntraWorkload, …)the oidthe tid (tenant)

The resolved subject is what a policy principal_pattern matches — so a policy (or a halt) can target a SPIFFE ID, an IAM role ARN, or an OIDC subject, not just a static vk_/vut_ id.

Owner binding

A use token can be bound to a human/directory owner — the OIDC sub / SCIM id of the person accountable for the NHI. (The same owner_identity field exists on API keys for when the key-mint path is extended; today it is settable on use tokens via the admin token-mint.)

# Mint a use token bound to a directory owner (admin API).
curl -XPOST .../api/v1/tokens -H "Authorization: Bearer vk_admin_..." -d '{
  "name": "refund-bot", "credential_scope": "pay-*",
  "owner_identity": "[email protected]"
}'

The owner flows into the resolved principal and the approval record, and — most importantly — into separation of duty: when an owner is bound, approver ≠ requester's owner is computed against the directory owner (the precise human), not just the agent label. So a human approving an action requested by an NHI they own is flagged (or rejected, with enforce_separation_of_duty). See Action Approvals.

Multi-Tenancy & Per-Team Partition (V11)

A single vultrino can serve multiple teams/tenants, each with its own enforcement posture and credential isolation — so a federated enterprise can run one team in enforce mode while another is observe-only, on the same instance.

Tagging a principal with a tenant

An API key or use token carries an optional tenant. For a use token, set it at the admin mint:

curl -XPOST .../api/v1/tokens -H "Authorization: Bearer vk_admin_..." -d '{
  "name": "team-b-bot", "credential_scope": "*", "tenant": "team-b"
}'

The principal's tenant is what selects its enforcement mode and scopes its credential access.

Per-tenant enforcement mode

Configure each tenant's mode under [[tenants]]:

[[tenants]]
id = "team-a"
mode = "enforce"   # the default — a policy Deny blocks the action

[[tenants]]
id = "team-b"
mode = "observe"   # a policy Deny is recorded + emitted but NOT blocked
  • enforce (the default, and the mode for any untenanted or unlisted principal — fail-closed): a policy Deny blocks the action as usual.
  • observe: a policy Deny is downgraded to allow — the action runs, a warning is logged, and a policy.observed_denial event is emitted to the signed outbox (carrying the credential, action, and what would have happened). This lets a team onboard and watch what would be blocked before flipping to enforce, while other teams enforce on the same vultrino. Note this also downgrades the engine's fail-closed no_policy default-deny, so in an observe tenant a credential lacking an explicit allow policy is usable (the point of observe-only onboarding) — size the blast radius accordingly.

Observe mode downgrades an authorization-posture denial only. The following are security/financial/abuse boundaries and are not observable-away — they hold even in an observe tenant: cross-tenant isolation (below), use-token scope, RBAC, the dual-control gate, SpendCap / RateLimit resource guards (a credential under a per-action spend cap or a rate cap is never downgraded — those are financial/abuse boundaries, not authorization posture; conservatively, if any policy matching a credential carries a spend/rate rule, observe mode enforces all of that credential's denials), and — critically — a halt / kill switch (a halted agent stays blocked).

Credential isolation

A credential can be tagged to a tenant via its tenant metadata:

vultrino meta set team-a-secret tenant team-a

A principal may only use credentials in its own tenant; an untenanted credential is shared (usable by any tenant). A principal in team-b attempting to use a team-a-tagged credential is denied — regardless of team-b's enforce/observe mode (isolation is a hard boundary, not a policy that observe mode can downgrade).

Approval partitioning

When a gated action opens an approval, the approval is tagged with the opening principal's tenant — so it can be partitioned the same way credentials are. The partition rule is the visible_to_tenant predicate:

  • A global view (no acting tenant) sees every tenant's approval.
  • An untenanted (shared) approval is visible to every tenant (like an untenanted credential).
  • Otherwise a tenant only sees its own approvals — team-a can never see a team-b approval.

Where this is wired today:

  • Admin metrics (GET /api/v1/metrics) scopes its approval counts to the calling admin key's tenant (and echoes the tenant_scope it applied) — so a tenant-scoped admin key sees only its own (+ shared) approvals in the read-back.
  • The web admin panel is a global console (the session admin carries no tenant): it lists and decides across all tenants. Per-tenant decision scoping is therefore an API concern — the visible_to_tenant primitive is the gate a tenant-scoped decision endpoint uses; the panel itself is intentionally global.

Using with AI Agents

Vultrino enables AI agents to make authenticated API calls without exposing credentials. This guide covers integration patterns and best practices.

Why Vultrino for AI Agents?

AI agents (Claude, GPT, etc.) need to interact with APIs, but:

  • Credentials shouldn't be in prompts or context
  • Agents shouldn't see actual secrets
  • Usage should be auditable
  • Access should be restricted and revocable

Vultrino solves this by:

  1. Storing credentials securely (encrypted at rest)
  2. Exposing only aliases to agents
  3. Injecting auth automatically
  4. Logging all usage

Integration Options

The Model Context Protocol provides native AI integration:

vultrino mcp

Pros:

  • Native protocol for AI tools
  • Rich tool descriptions
  • Scoped access via API key authentication
  • Best security isolation

Setup: See MCP Server documentation for configuration.

2. HTTP API

For agents that can make HTTP requests:

vultrino web  # Start the JSON API + admin UI on 127.0.0.1:7879

The agent POSTs the credential alias and target to the execute endpoint (there is no credential header and no transparent proxy):

POST http://vultrino:7879/api/v1/execute
Authorization: Bearer vk_your_api_key
Content-Type: application/json

{"credential": "github-api", "method": "GET", "url": "https://api.github.com/user"}

Pros:

  • Works with any HTTP-capable agent
  • Simple integration
  • Language agnostic

3. CLI Tool Calls

For agents that can execute shell commands:

vultrino request github-api https://api.github.com/user

Pros:

  • Simplest integration
  • Works with function calling
  • Easy to audit

Setting Up for AI Agents

Step 1: Store Credentials

# Add credentials the AI will need
vultrino add --alias github-api --key ghp_xxx \
  --description "GitHub API for repo access"

vultrino add --alias stripe-test --key sk_test_xxx \
  --description "Stripe test mode API"

Step 2: Create Restricted Role

# Create a role with limited access
vultrino role create ai-agent \
  --permissions read,execute \
  --scopes "github-api,stripe-test" \
  --description "Limited access for AI agent"

Step 3: Create API Key

vultrino key create claude-key --role ai-agent --expires 30d
# Save the generated key

Step 4: Configure AI Client

For Claude Desktop MCP:

{
  "mcpServers": {
    "vultrino": {
      "command": "/path/to/vultrino",
      "args": ["mcp"],
      "env": {
        "VULTRINO_PASSWORD": "your-password"
      }
    }
  }
}

Step 5: Configure Agent with API Key

Add to your AI agent's system prompt:

When using Vultrino tools, always include your API key in every tool call:

For listing credentials:
{"tool": "list_credentials", "arguments": {"api_key": "vk_your_generated_key_here"}}

For making requests:
{"tool": "http_request", "arguments": {"api_key": "vk_your_generated_key_here", "credential": "...", ...}}

Important: Every tool call requires the api_key parameter. This enables multiple agents to share the same MCP server with different scoped access.

Example Interactions

Listing Available Credentials

User: "What APIs can you access?"

AI: calls list_credentials with api_key

{
  "tool": "list_credentials",
  "arguments": {"api_key": "vk_..."}
}

AI: "I have access to:

  • github-api — GitHub API for repo access
  • stripe-test — Stripe test mode API"

Making Requests

User: "Check my GitHub notifications"

AI: calls http_request with api_key and credential

{
  "tool": "http_request",
  "arguments": {
    "api_key": "vk_...",
    "credential": "github-api",
    "method": "GET",
    "url": "https://api.github.com/notifications"
  }
}

AI: "You have 3 unread notifications..."

Handling Errors

User: "Access my AWS account"

AI: calls http_request with credential=aws-prod

AI: "I don't have access to AWS credentials. I can only access:

  • github-api
  • stripe-test

Would you like to add AWS credentials?"

Security Best Practices

1. Scope Credentials Narrowly

Don't give AI access to admin credentials:

# Bad: Full admin access
vultrino add --alias github-admin --key ghp_admin_everything

# Good: Read-only access
vultrino add --alias github-readonly --key ghp_read_only_token

2. Use Test/Sandbox Credentials

For AI experimentation, use test mode:

vultrino add --alias stripe-test --key sk_test_xxx
# Not: stripe-live with sk_live_xxx

3. Set Key Expiration

Short-lived keys limit damage if compromised:

vultrino key create ai-key --role ai-agent --expires 7d

4. Monitor Usage

Check what the AI is doing:

# Watch audit log
tail -f /var/log/vultrino/audit.log

# Filter by credential
grep "github-api" /var/log/vultrino/audit.log

5. Restrict Scopes

Only allow access to specific credentials:

vultrino role create ai-readonly \
  --permissions read,execute \
  --scopes "github-readonly,stripe-test"

Common Patterns

Read-Only Research Agent

# Create read-only credential
vultrino add --alias github-public --key ghp_public_readonly

# Create restricted role
vultrino role create research-agent \
  --permissions read,execute \
  --scopes "github-public"

# Create key
vultrino key create research-key --role research-agent

Multi-Service Agent

# Add multiple credentials
vultrino add --alias github-api --key ghp_xxx
vultrino add --alias linear-api --key lin_xxx
vultrino add --alias notion-api --key secret_xxx

# Role with access to all
vultrino role create project-agent \
  --permissions read,execute \
  --scopes "github-api,linear-api,notion-api"

Development vs Production

# Dev credentials
vultrino add --alias stripe-dev --key sk_test_xxx
vultrino add --alias github-dev --key ghp_dev_xxx

# Dev-only role
vultrino role create ai-dev \
  --permissions read,execute \
  --scopes "*-dev,*-test"

# This role CANNOT access production credentials

Prompt Engineering Tips

Be Explicit About Available Credentials

System prompt:

You have access to these credentials through Vultrino:
- github-api: Read/write access to company repositories
- jira-api: Read access to project issues
- slack-api: Can post to #engineering channel

Always use these aliases when making API requests.
Never ask for or accept raw API keys.

Guide Credential Usage

When the user asks about GitHub:
1. Use github-api credential
2. Check available endpoints first
3. Prefer read operations over write

When uncertain, list available credentials first.

Handle Errors Gracefully

If a credential is not available or access is denied:
1. Inform the user what credentials you DO have access to
2. Suggest alternatives if possible
3. Never attempt to bypass Vultrino

Troubleshooting

Agent Can't Find Credentials

  • Verify credentials exist: vultrino list
  • Check role scopes include the credential
  • Ensure API key has read permission

Request Fails with 403

  • Check role has execute permission
  • Verify credential scope matches
  • The underlying API may also be denying access

MCP Server Not Responding

  • Ensure VULTRINO_PASSWORD is set
  • Check Vultrino binary path is correct
  • Review stderr for error messages

Audit Log Not Showing Requests

  • Audit logging may be disabled
  • Check config: logging.audit_file
  • Verify file permissions

Monitoring AI Usage

Real-time Monitoring

# Watch all AI requests
tail -f /var/log/vultrino/audit.log | grep ai-agent-key

# Count requests per credential
awk '{print $4}' /var/log/vultrino/audit.log | sort | uniq -c

Usage Reports

Generate daily summaries:

# Requests per credential today
grep $(date +%Y-%m-%d) /var/log/vultrino/audit.log | \
  awk '{print $4}' | sort | uniq -c | sort -rn

Alerting

Set up alerts for unusual activity:

  • Sudden spike in requests
  • Access to unexpected credentials
  • Failed authentication attempts

Policy Configuration

Policies add fine-grained control over how credentials can be used, including URL restrictions, method limits, and rate limiting.

Overview

Policies are evaluated for every credential use:

Request → RBAC Check → Policy Check → Credential Injection → Forward
                            │
                            └─ Deny if policy fails

Default posture for un-policied credentials

A policy's default_action only governs credentials its credential_pattern matches. What happens to a credential that matches no policy is set engine-wide by [enforcement] default_action (see Configuration):

  • deny (default): a credential matched by no policy is denied with a distinct no_policy reason — fail-closed. Grant access by adding a policy whose credential_pattern matches it.
  • allow: a credential matched by no policy is permitted — the legacy fail-open behavior.

Policy Structure

Policies are defined in the configuration file:

[[policies]]
name = "github-readonly"
credential_pattern = "github-*"
default_action = "deny"

[[policies.rules]]
condition = { url_match = "https://api.github.com/*" }
action = "allow"

[[policies.rules]]
condition = { method_match = ["GET", "HEAD"] }
action = "allow"

Configuration Fields

Policy Definition

FieldTypeDescription
namestringUnique policy name
credential_patternstringGlob pattern for credentials this policy applies to
principal_patternstring (optional)Glob over the presenting principal (key/token id or its agent_label). When set, the policy applies only to matching principals (V4).
default_actionstringAction when no rules match: allow, deny
rulesarrayList of policy rules

Per-agent policies (principal_pattern)

A policy with principal_pattern applies only to requests from a matching principal — the presenting key/token id, or an agent_label bound to the token (via the admin API). This makes a per-agent kill expressible: push a Deny scoped to one agent without affecting other agents sharing the same credential.

[[policies]]
name = "kill-refund-bot"
credential_pattern = "payments-*"
principal_pattern = "refund-bot"   # only this agent
default_action = "deny"

A request that carries no principal never matches a policy that sets principal_pattern.

Rule Definition

FieldTypeDescription
conditionobjectCondition to evaluate
actionstringAction if condition matches: allow, deny

Conditions

URL Match

Restrict to specific URLs or patterns:

# Exact match
condition = { url_match = "https://api.github.com/user" }

# Wildcard pattern
condition = { url_match = "https://api.github.com/repos/*" }

# Multiple paths
condition = { url_match = "https://api.github.com/{user,repos,gists}/*" }

Method Match

Restrict to specific HTTP methods:

# Single method
condition = { method_match = ["GET"] }

# Multiple methods
condition = { method_match = ["GET", "HEAD", "OPTIONS"] }

# All read operations
condition = { method_match = ["GET", "HEAD"] }

# Write operations
condition = { method_match = ["POST", "PUT", "PATCH", "DELETE"] }

Time Window

Restrict to specific hours:

# Business hours only (9 AM - 5 PM)
condition = { time_window = { start = "09:00", end = "17:00" } }

# Night shift (11 PM - 7 AM)
condition = { time_window = { start = "23:00", end = "07:00" } }

Rate Limit

Limit request frequency:

# 100 requests per minute
condition = { rate_limit = { max = 100, window_secs = 60 } }

# 1000 requests per hour
condition = { rate_limit = { max = 1000, window_secs = 3600 } }

# 10 requests per second (burst protection)
condition = { rate_limit = { max = 10, window_secs = 1 } }

Spend Cap

Cap the value an agent can spend in a single call, in minor units (e.g. cents) (V3). The amount is read from the request body by a spend extractor; a missing/unparseable amount fails closed (deny).

# Refunds: at most $50.00 per call, in USD.
condition = { spend_cap = { asset = "usd", per_action_max = 5000 } }

Use it as the condition of an allow rule (with default_action = "deny"): the call is allowed only while within the per-call cap. A SpendCap must be a rule's top-level condition (not nested in and/or/not), and its policy must be fail-closed (default_action = "deny") — both are enforced at load.

Per-action only — the spend check is stateless. Vultrino does not keep a cumulative/windowed ledger: a single call is the unit it bounds. Cumulative or budget enforcement is the book-of-record's plane (e.g. a spend ledger service) — it arrives as a per-agent/credential Deny policy pushed through the admin write API when a budget is exhausted, not as engine state inside vultrino. This keeps the proxy's decision stateless and the same across the web and MCP processes.

Notes:

  • Approval-gated spends are re-checked when the approval finally executes via the read-only resume path, which treats the already-checked per-action amount as admitted (it does not re-deny on resume).
  • Multiple assets: put per-asset caps as multiple rules in a single policy (first match wins). Two separate per-asset policies would deny each other's asset, because a non-matching asset falls through to that policy's deny default.

Combined Conditions

Use and and or for complex logic:

# URL AND method match
condition = { and = [
  { url_match = "https://api.github.com/repos/*" },
  { method_match = ["GET"] }
]}

# Allow GET to anything OR POST to specific endpoint
condition = { or = [
  { method_match = ["GET"] },
  { and = [
    { method_match = ["POST"] },
    { url_match = "https://api.github.com/repos/*/issues" }
  ]}
]}

Complete Examples

Read-Only API Access

[[policies]]
name = "github-readonly"
credential_pattern = "github-readonly-*"
default_action = "deny"

[[policies.rules]]
condition = { url_match = "https://api.github.com/*" }
action = "allow"

[[policies.rules]]
condition = { method_match = ["POST", "PUT", "PATCH", "DELETE"] }
action = "deny"

Rate-Limited Production Access

[[policies]]
name = "stripe-production"
credential_pattern = "stripe-live-*"
default_action = "deny"

# Allow only Stripe API
[[policies.rules]]
condition = { url_match = "https://api.stripe.com/*" }
action = "allow"

# Rate limit to prevent abuse
[[policies.rules]]
condition = { rate_limit = { max = 100, window_secs = 60 } }
action = "allow"

Business Hours Only

[[policies]]
name = "sensitive-data-access"
credential_pattern = "database-*"
default_action = "deny"

# Only during business hours
[[policies.rules]]
condition = { time_window = { start = "09:00", end = "18:00" } }
action = "allow"

Multi-Service Policy

[[policies]]
name = "payment-processing"
credential_pattern = "payment-*"
default_action = "deny"

# Allow Stripe
[[policies.rules]]
condition = { url_match = "https://api.stripe.com/*" }
action = "allow"

# Allow PayPal
[[policies.rules]]
condition = { url_match = "https://api.paypal.com/*" }
action = "allow"

# Allow Braintree
[[policies.rules]]
condition = { url_match = "https://api.braintreegateway.com/*" }
action = "allow"

# Block everything else by default

AI Agent Restrictions

[[policies]]
name = "ai-agent-safety"
credential_pattern = "ai-*"
default_action = "deny"

# Only read operations
[[policies.rules]]
condition = { method_match = ["GET", "HEAD"] }
action = "allow"

# Allow POST only to specific safe endpoints
[[policies.rules]]
condition = { and = [
  { method_match = ["POST"] },
  { or = [
    { url_match = "https://api.github.com/repos/*/issues" },
    { url_match = "https://api.github.com/repos/*/comments" }
  ]}
]}
action = "allow"

# Rate limit all requests
[[policies.rules]]
condition = { rate_limit = { max = 60, window_secs = 60 } }
action = "allow"

# Block dangerous operations
[[policies.rules]]
condition = { url_match = "https://api.github.com/repos/*/delete" }
action = "deny"

Policy Evaluation Order

  1. RBAC check — Does the API key have permission?
  2. Credential scope — Is the credential in scope for this role?
  3. Policy match — Find policies matching the credential alias
  4. Rule evaluation — Evaluate rules in order
  5. Default action — Apply if no rules matched

Rules are evaluated in order. First matching rule determines the action.

Debugging Policies

Verbose Logging

Enable debug logging to see policy evaluation:

RUST_LOG=vultrino=debug vultrino web

Output:

DEBUG vultrino::policy: Evaluating policy "github-readonly" for credential "github-api"
DEBUG vultrino::policy: Rule 1 url_match: matched
DEBUG vultrino::policy: Rule 2 method_match: GET in [GET, HEAD] = true
DEBUG vultrino::policy: Result: allow

Audit Log

Check why requests were denied:

grep "policy_denied" /var/log/vultrino/audit.log

Common Patterns

Deny by Default

Start restrictive, add specific allows:

[[policies]]
name = "strict-access"
credential_pattern = "*"
default_action = "deny"

[[policies.rules]]
condition = { url_match = "https://api.company.com/*" }
action = "allow"

Allow by Default with Blocklist

Allow most things, block specific patterns:

[[policies]]
name = "open-access"
credential_pattern = "dev-*"
default_action = "allow"

# Block production endpoints
[[policies.rules]]
condition = { url_match = "https://api.company.com/admin/*" }
action = "deny"

[[policies.rules]]
condition = { url_match = "https://api.company.com/billing/*" }
action = "deny"

Environment Separation

Different policies per environment:

# Production: strict
[[policies]]
name = "production"
credential_pattern = "*-prod"
default_action = "deny"

[[policies.rules]]
condition = { url_match = "https://api.production.com/*" }
action = "allow"

# Development: permissive
[[policies]]
name = "development"
credential_pattern = "*-dev"
default_action = "allow"

Best Practices

1. Start Restrictive

Default to deny and add specific allows:

default_action = "deny"

2. Use Specific URL Patterns

# Bad: too broad
condition = { url_match = "*" }

# Good: specific
condition = { url_match = "https://api.github.com/repos/myorg/*" }

3. Combine with RBAC

Policies complement RBAC, not replace it:

  • RBAC: Who can access which credentials
  • Policies: How credentials can be used

4. Document Policies

Use clear names and comments:

[[policies]]
# SECURITY: Prevents AI agents from deleting repositories
name = "ai-no-destructive"
credential_pattern = "ai-*"

5. Verify Before Deploying

Confirm a policy behaves as intended before relying on it in production: enable debug logging (RUST_LOG=vultrino=debug) and exercise the credential, then check the evaluation trace and the policy_denied events in the audit trail.

Plugin System

Vultrino supports trusted built-in Rust connectors and sandboxed WASM extensions. They occupy different credential trust boundaries.

PluginRegistry
├── built-in Rust plugin ── receives CredentialData as a trusted injector
└── WASM plugin
    ├── PluginManifest
    └── Wasmtime ABI v2 ─── receives alias + credential type only

WASM ABI v2 boundary

Installed WASM is untrusted. The host sends public action parameters and a non-secret credential handle. It never serializes a vault credential, metadata, or a general-purpose secret map into guest memory. ABI v1 modules fail installation and loading.

WASM plugins can provide public transformations, validation, actions, and MCP tools. They cannot currently perform an operation that needs private credential bytes because no secret-using host capability exists. Such functionality must be implemented as a reviewed built-in connector or wait for a narrow host operation specific to the credential type.

Installed plugins live under the platform data directory's vultrino/plugins/ folder and contain plugin.toml, the declared .wasm module, and .installed.json. The web server loads them at startup.

The old PGP signing module is an archived ABI v1 rejection fixture, not an available plugin.

Next steps

Installing Plugins

Vultrino plugins can be installed from local paths, git repositories, or archive URLs.

Installation Sources

From Local Path

Install a plugin from a local directory:

vultrino plugin install ./my-plugin
vultrino plugin install /absolute/path/to/plugin
vultrino plugin install ~/plugins/my-plugin

From Git Repository

Install directly from GitHub, GitLab, or other git hosts:

# Latest commit
vultrino plugin install https://github.com/user/vultrino-plugin

# Specific tag or branch
vultrino plugin install https://github.com/user/vultrino-plugin#v1.0.0
vultrino plugin install https://github.com/user/vultrino-plugin#main

From Archive URL

Install from a .tar.gz archive:

vultrino plugin install https://example.com/plugin-1.0.0.tar.gz

Build Process

When installing a plugin with a Cargo.toml, Vultrino automatically:

  1. Checks for the wasm32-wasip1 target (installs if needed)
  2. Runs cargo build --release --target wasm32-wasip1
  3. Copies the built WASM module to the plugin directory

Requirements:

  • Rust toolchain installed
  • rustup available in PATH

Managing Plugins

List Installed Plugins

vultrino plugin list

Example output:

Installed plugins:

  public-transform v2.0.0
    Source: https://github.com/example/public-transform#v2.0.0
    Installed: 2024-01-15
    MCP tools: normalize_payload

View Plugin Details

vultrino plugin info public-transform

Remove a Plugin

vultrino plugin remove public-transform

Reload a Plugin

Reload a plugin's WASM module without restarting:

vultrino plugin reload public-transform

Plugin Discovery

Plugins can be discovered in the Vultrino plugin registry (coming soon) or by searching GitHub for repositories tagged with vultrino-plugin.

Troubleshooting

Build Fails

If the WASM build fails:

  1. Ensure Rust is installed: rustup --version
  2. Check the target: rustup target list --installed
  3. Install the target manually: rustup target add wasm32-wasip1

Plugin Not Loading

Vultrino requires the credential-confining WASM ABI v2. ABI v1 modules are rejected because they accepted plaintext credential data. Installation validates the module and ABI before it is copied into the plugin directory.

Check the plugin manifest is valid:

cd ~/.vultrino/plugins/my-plugin
cat plugin.toml

Verify the WASM module exists:

ls -la *.wasm

Hot Reload Not Working

Make sure the Vultrino server has write access to the plugins directory and that the new WASM module compiles successfully.

Developing WASM Plugins

Vultrino loads WASI Preview 1 modules through a credential-confining ABI. A guest is untrusted: it receives public action parameters plus a non-secret credential handle, never vault material.

Current capability boundary

ABI v2 intentionally does not expose a generic “read credential” function. No secret-using host operations exist yet. Therefore a WASM plugin can:

  • validate and transform public action parameters;
  • use the selected credential's alias and type as non-secret routing context;
  • return a public result; and
  • define actions and MCP tools in its manifest.

It cannot sign, authenticate, or otherwise operate on private credential bytes. Those features require a narrow host-side capability for the particular operation. Do not work around this by placing secrets in action parameters.

Plugin structure

my-plugin/
├── Cargo.toml
├── plugin.toml
└── src/
    └── lib.rs

A manifest may define actions and tools normally. Credential fields can still describe the selected credential, but their values are held only by Vultrino; the guest receives the handle below.

ABI v2

The module must export:

  • vultrino_plugin_version() -> u32, returning 2;
  • vultrino_alloc(size: u32) -> u32;
  • vultrino_free(ptr: u32, len: u32);
  • vultrino_execute(ptr: u32, len: u32) -> u64; and
  • optionally vultrino_validate_params(action_ptr, action_len, params_ptr, params_len) -> i32.

The execute request is JSON:

{
  "action": "do_something",
  "credential_handle": {
    "alias": "selected-alias",
    "credential_type": "plugin:my-plugin:profile"
  },
  "parameters": {
    "input": "public data"
  }
}

There is deliberately no credential, secret map, metadata map, or credential id. ABI v1 modules that expect a plaintext credential object are rejected at installation and loading.

The response is JSON:

{
  "code": 0,
  "data": "public result",
  "error": null
}

Nonzero result codes are failures. Guest error text is treated as potentially secret-bearing after dispatch and is replaced with a constant message at the public execution boundary.

Build and test

cargo build --release --target wasm32-wasip1
vultrino plugin install ./my-plugin

Installation compiles source when necessary, verifies that the declared module exists, instantiates it, and checks ABI v2 before copying it into the installed plugin directory. The server loads installed plugins at startup.

Keep plugin results public, validate all parameters, bound memory use, and pin dependencies. A future secret-using plugin API will expose operation-specific host capabilities rather than plaintext credentials.

PGP Signing Plugin (archived)

The repository's PGP module targets the former WASM ABI v1 and is not a deployable plugin. ABI v1 serialized private credential material into guest memory; Vultrino now rejects it during installation and loading.

WASM ABI v2 gives an untrusted guest only the selected credential's alias and type. It has no generic credential-read operation and currently exposes no host-side PGP signing capability. Consequently, PGP signing through an external WASM plugin is unavailable.

The source and binary under plugins/pgp-signing/ are retained only as a negative compatibility fixture. Do not install them. A future PGP integration must keep the private key in Vultrino and expose only narrow host operations such as sign and public_key; restoring plaintext key transfer to WASM is not an acceptable migration.

SSH Plugin

The ssh plugin lets Vultrino hold an SSH password and drive remote deployments and commands against a host — without the calling agent ever seeing the password. It ships as a built-in plugin, so no separate installation step is needed.

Two actions are exposed:

ActionWhat it does
deployrsync a local directory to the remote host over SSH
runExecute a sequence of shell commands over SSH

Both read their inputs from credential metadata by default, so a credential alias can hold the full recipe for a specific server (source path, destination, excludes, command list). Agents then invoke the action against the alias and supply no parameters — which is also how the override-lock security model works.

System requirements

sshpass, ssh, and rsync must be on PATH.

| macOS (Homebrew) | brew install hudochenkov/sshpass/sshpass (and rsync is preinstalled) | | Debian / Ubuntu | apt install sshpass rsync openssh-client | | RHEL / Fedora | dnf install sshpass rsync openssh-clients |

The plugin will return a clear "binary not found" error if any of these are missing; it won't silently fall back.

Credential type: ssh_password

Holds the connection info and the password that will be supplied to sshpass at invocation time.

Fields

FieldTypeRequiredDescription
hosttextYesHostname or IP of the SSH server
porttextNoSSH port (default 22)
usertextYesSSH username
passwordpasswordYesSSH password (stored encrypted)

Adding via CLI

vultrino add \
  --alias prod-api \
  --type ssh_password \
  --ssh-host deploy.example.com \
  --ssh-user deploy
# prompts for SSH password

Add as many credentials as you have targets — each alias is an independent "instance" with its own host, user, password, and metadata defaults.

Configuring per-credential defaults (metadata)

Metadata is free-form key-value on the credential. The plugin looks up specific keys to populate action inputs. Set them via the vultrino meta subcommand:

vultrino meta set prod-api deploy.source_dir /path/to/local/dir/
vultrino meta set prod-api deploy.dest_dir   /opt/app/
vultrino meta set prod-api deploy.excludes   '[".git",".env","node_modules","dist"]'

vultrino meta list prod-api
vultrino meta unset prod-api deploy.excludes

Deploy keys

KeyDefaultDescription
deploy.source_dirLocal directory. Trailing / is significant to rsync — see man rsync.
deploy.dest_dirRemote directory.
deploy.excludes[]JSON array of rsync --exclude patterns.
deploy.flags-avzRsync flags as a single string.
deploy.timeout_secs1800 (30min)Kill local rsync if it exceeds this.
deploy.allow_overridefalseIf true, callers can override source_dir / dest_dir / excludes / flags in params.

Run keys

KeyDefaultDescription
run.commandsJSON array of commands. Each runs in its own SSH invocation.
run.stop_on_errorfalseIf true, halt the sequence on the first non-zero exit.
run.interval_ms0Milliseconds to sleep between commands.
run.timeout_secs300 (5min)Per-command timeout. Local ssh is killed on expiry.
run.allow_overridefalseIf true, callers can pass a custom commands array in params.

Shared keys

KeyDefaultDescription
ssh.strict_host_key_checkingaccept-newForwarded to ssh -o StrictHostKeyChecking=…. Sensible choices are accept-new, yes, no.

Actions

ssh.deploy — rsync a directory

Invoked with zero params, uses metadata defaults:

vultrino action prod-api ssh.deploy

Params (all optional):

ParamLocked by override?Description
dry_runAlways allowedRun rsync --dry-run. Useful for agents to preview.
source_dirLockedRequires deploy.allow_override=true.
dest_dirLockedRequires deploy.allow_override=true.
excludesLockedRequires deploy.allow_override=true.
flagsLockedRequires deploy.allow_override=true.
timeout_secsAlways allowedOverride the configured timeout.

Response body:

{
  "ok": true,
  "exit_code": 0,
  "stdout": "sending incremental file list\n...",
  "stderr": "",
  "duration_ms": 4213,
  "timed_out": false,
  "dry_run": false,
  "command_display": "sshpass -e rsync -avz --exclude=.git -e \"ssh -p 22 -o StrictHostKeyChecking=accept-new -o ConnectTimeout=30\" /src/ user@host:/dest/"
}

ssh.run — execute a command sequence

vultrino action prod-api ssh.run

Params (all optional):

ParamLocked by override?Description
commandsLockedJSON array. Requires run.allow_override=true.
stop_on_errorAlways allowedHalt on first non-zero exit.
interval_msAlways allowedSleep between commands.
timeout_secsAlways allowedPer-command timeout.

Response body:

{
  "ok": true,
  "results": [
    {
      "index": 0,
      "command": "uptime",
      "ok": true,
      "exit_code": 0,
      "stdout": " 13:42:17 up 18 days, ...\n",
      "stderr": "",
      "duration_ms": 742,
      "timed_out": false
    }
  ]
}

ok at the top level is true only if every command returned 0 and none timed out.

Security model

  • Password stays out of the agent response. Agents present a credential alias; the trusted plugin resolves it, decrypts the password, and passes it to sshpass via the SSHPASS environment variable — never visible in ps output, never on disk.
  • Override-locked by default. An agent cannot pass a custom command list or target directory unless the credential's metadata explicitly opts in with run.allow_override=true / deploy.allow_override=true. This is intentional: a credential is a fixed recipe, and a prompt- injected agent can't turn it into "rm -rf /" without the credential owner's opt-in.
  • Host key verification. StrictHostKeyChecking=accept-new is the default — trust on first use, reject on key change. Change it via the ssh.strict_host_key_checking metadata key if you need stricter or looser behavior.
  • Timeouts actually kill. Commands that exceed their timeout have the local ssh/rsync process sent SIGKILL (tokio::Command::kill_on_drop(true)). The remote side is best-effort — SIGHUP from the closed SSH channel should reach the remote command, but commands that explicitly detach from stdio (nohup, disown, backgrounded processes) can outlive the channel. Response carries a timed_out: bool so callers don't mistake a timeout for a clean exit.

MCP exposure

Both actions are exposed as MCP tools automatically:

MCP tool nameAction
ssh_deployssh.deploy
ssh_runssh.run

Schemas include all params above plus credential and api_key.

Common gotchas

bash: command not found when you know the binary is installed

ssh host "mycommand" gives you a non-interactive, non-login shell. It doesn't source ~/.bashrc, ~/.zshrc, or (usually) ~/.profile, so PATH additions installers wrote there aren't present. Workarounds in rough order of cleanest-first:

  • Reference the absolute path: /root/.nvm/versions/node/v20/bin/node server.js.
  • Set PATH inline: PATH=$HOME/.cargo/bin:$PATH my-tool .
  • Shift the command into an existing session that has the right environment: tmux send-keys -t mysession 'my-tool' Enter — this is fire-and-forget (you won't see the command's exit code), so pair with a health probe at the end if correctness matters.

pkill -f kills itself

pkill -f pattern matches against every process's full argv. The pkill you just ran also contains pattern in its argv, so it'll match — and the shell running it gets killed, causing your SSH connection to drop with exit status 255.

Use pkill <name> without -f when matching by process name. If you really need -f, pick a pattern that can't match pkill itself (e.g. pkill -f '^/usr/local/bin/myapp').

tmux send-keys doesn't report command results

tmux send-keys returns 0 as soon as the keystrokes are delivered — not when the command inside tmux finishes. If you need to know the deployment actually started, append an explicit probe after the send-keys command, running over ssh proper:

[
  "tmux send-keys -t app 'bun install && bun run start' Enter",
  "sleep 8 && curl -fsS http://127.0.0.1:3000/health"
]

The final command's exit code gives you real status.

Example: typical deploy + restart flow

For a service running under tmux on a VPS, with the backend process in a tmux pane named app:

vultrino add \
  --alias prod-api \
  --type ssh_password \
  --ssh-host deploy.example.com \
  --ssh-user deploy

vultrino meta set prod-api deploy.source_dir /path/to/local/backend/
vultrino meta set prod-api deploy.dest_dir   /srv/app/
vultrino meta set prod-api deploy.excludes \
  '[".git",".env","node_modules","dist","tests"]'

vultrino meta set prod-api run.commands '[
  "tmux send-keys -t app C-c",
  "tmux send-keys -t app C-c",
  "sleep 1",
  "pkill -9 mybinary || true",
  "sleep 1",
  "tmux send-keys -t app '\''cd /srv/app && myinstall && myrun'\'' Enter"
]'

# Sanity-check once before relying on it
vultrino action prod-api ssh.deploy -p '{"dry_run": true}'

# Real deploy + restart
vultrino action prod-api ssh.deploy
vultrino action prod-api ssh.run

Multiple targets? Just create more aliases with their own metadata — staging-api, dr-api, etc. Same plugin, different credentials.

Postgres Plugin

The postgres plugin holds PostgreSQL connection credentials and drives the two most common "run it from an automation system" operations: SQL execution (migrations, maintenance) and pg_dump-based backups. The stored password is absent from the calling agent's schema — Vultrino passes it to the trusted psql / pg_dump child via the PGPASSWORD environment variable, then drops its exposed copy. It ships as a built-in plugin; no separate installation step.

Two actions are exposed:

ActionWhat it does
run_sqlExecute SQL (a raw string or a local .sql file) against the DB
backupRun pg_dump and write the output to a local file

Both read their inputs from credential metadata by default, so a credential alias can hold the full recipe (which script to run, where to put backups). Agents then invoke the action against the alias and supply no parameters — which is how the override-lock security model works.

System requirements

psql and pg_dump must be on PATH. These ship together in the postgresql-client package.

| macOS (Homebrew) | brew install libpq && brew link --force libpq (or brew install postgresql@16 for the full server) | | Debian / Ubuntu | apt install postgresql-client | | RHEL / Fedora | dnf install postgresql |

Pick a client version that is greater than or equal to your server's major version — older clients can fail on dumps from newer servers.

Credential type: postgres

Holds the connection info and password used by both actions.

Fields

FieldTypeRequiredDescription
hosttextYesHostname or IP of the Postgres server
porttextNoPostgres port (default 5432)
databasetextYesDatabase name
usertextYesPostgres role / username
passwordpasswordYesPassword (stored encrypted; passed via PGPASSWORD)
sslmodetextNolibpq sslmode: disable, allow, prefer (default), require, verify-ca, verify-full

The default sslmode is prefer to match libpq's out-of-the-box behavior and to avoid hard-failing if the server doesn't advertise TLS. For production remote databases, set it explicitly to require or stronger on the credential.

Adding via CLI

vultrino add \
  --alias prod-db \
  --type postgres \
  --pg-host db.example.com \
  --pg-database app_production \
  --pg-user deploy \
  --pg-sslmode require
# prompts for the Postgres password

Add as many credentials as you have targets — each alias is an independent "instance" with its own host, user, password, and metadata defaults.

Configuring per-credential defaults (metadata)

Metadata is free-form key-value on the credential. The plugin looks up specific keys to populate action inputs. Set them via the vultrino meta subcommand:

vultrino meta set prod-db run_sql.file   /path/to/repo/migrations/apply.sql
vultrino meta set prod-db backup.output_dir /var/backups/postgres/
vultrino meta set prod-db backup.filename_template "{alias}-{date}.sql"

vultrino meta list prod-db
vultrino meta unset prod-db backup.format

run_sql keys

KeyDefaultDescription
run_sql.sqlDefault raw SQL string (mutually exclusive with run_sql.file).
run_sql.fileDefault path to a .sql file on the local machine.
run_sql.transactiontrueWrap the execution in a single transaction (--single-transaction + ON_ERROR_STOP).
run_sql.statement_timeout_ms0 (off)Sets Postgres statement_timeout at session start.
run_sql.timeout_secs600 (10min)Wall-clock timeout for the psql process. Local process is killed on expiry.
run_sql.allow_overridefalseIf true, callers can pass a custom sql or file in params.

backup keys

KeyDefaultDescription
backup.output_dirLocal directory to write dumps into. Must exist.
backup.filename_template{alias}-{timestamp}.sqlTokens: {alias}, {date} (YYYY-MM-DD), {time} (HH-MM-SS), {timestamp} (unix seconds).
backup.formatplainpg_dump format: plain, custom, directory, tar.
backup.timeout_secs1800 (30min)Wall-clock timeout for pg_dump.
backup.allow_overridefalseIf true, callers can pass output_path / format in params.

Actions

postgres.run_sql — execute SQL

Invoked with zero params, uses metadata defaults:

vultrino action prod-db postgres.run_sql

Params (all optional):

ParamLocked by override?Description
sqlLockedRaw SQL string. Requires run_sql.allow_override=true.
fileLockedPath to a local .sql file. Requires run_sql.allow_override=true.
transactionAlways allowedWrap in a single transaction (default: true).
timeout_secsAlways allowedWall-clock timeout override.

sql and file are mutually exclusive. Raw SQL is fed to psql via stdin (not -c), so it handles multi-statement scripts, comments, and \ meta-commands correctly.

Response body:

{
  "ok": true,
  "exit_code": 0,
  "stdout": "BEGIN\nCREATE TABLE\n...",
  "stderr": "",
  "duration_ms": 213,
  "timed_out": false,
  "source": "file:/path/to/migrations/apply.sql",
  "transaction": true
}

postgres.backup — pg_dump to a local file

vultrino action prod-db postgres.backup

Params (all optional):

ParamLocked by override?Description
output_pathLockedFull output file path. Requires backup.allow_override=true.
formatLockedOverride the format. Requires backup.allow_override=true.
timeout_secsAlways allowedWall-clock timeout override.

The destination directory must exist before the call — the plugin refuses to create it, so a mistyped path fails loudly instead of dumping into / unexpectedly.

Response body:

{
  "ok": true,
  "exit_code": 0,
  "stdout": "",
  "stderr": "",
  "duration_ms": 4210,
  "timed_out": false,
  "output_path": "/var/backups/postgres/prod-db-2026-04-24.sql",
  "format": "plain",
  "bytes_written": 12345678
}

Security model

  • Password stays out of the agent response. Agents present a credential alias; the trusted plugin decrypts the password and hands it to psql / pg_dump via the PGPASSWORD env var of the child process — not visible in ps, not on disk.
  • SQL overrides are locked by default. Raw SQL is code. An agent cannot pass a custom sql string or file path unless the credential's metadata explicitly opts in with run_sql.allow_override=true. With the lock on, the credential is a sealed "run our migration script" operation, and a prompt-injected agent can't turn it into DROP TABLE users or COPY … TO PROGRAM ….
  • Backup destinations are locked by default. Without the lock, an agent couldn't redirect a dump to e.g. /tmp/world-readable/ and exfiltrate the DB. The credential controls where dumps go; the agent controls only whether to run one.
  • TLS by credential, not by caller. sslmode lives on the credential, not in params. Agents can't downgrade it.
  • Timeouts actually kill. Both psql and pg_dump run under kill_on_drop(true) + stdin(null), so an expired timeout sends SIGKILL to the local process. On psql disconnect, any in-flight transaction is rolled back server-side. Partial pg_dump files on timeout should be treated as garbage and deleted; the response carries timed_out: bool so callers can branch cleanly.

MCP exposure

Both actions are exposed as MCP tools automatically:

MCP tool nameAction
postgres_run_sqlpostgres.run_sql
postgres_backuppostgres.backup

Schemas include the params above plus credential and api_key.

Common gotchas

Backup succeeded but file is tiny

If the pg_dump file is only a few hundred bytes of SQL, you almost certainly got "just the DDL / just the role" — check stderr in the response for a role permission error. pg_dump silently continues past many auth problems and returns 0. Run the same user manually to verify they have SELECT on everything.

"FATAL: no pg_hba.conf entry for host"

The Postgres server doesn't permit connections from your client IP. This is not a Vultrino problem — fix pg_hba.conf on the server (add a matching host line) and SELECT pg_reload_conf().

Client / server major version mismatch

pg_dump errors with server version X.Y; pg_dump version W.Z when the client is older than the server. Install a client matching your server's major version (e.g. postgresql-client-16 for a 16.x server).

run_sql returns 0 but "nothing happened"

By default the plugin passes --single-transaction and ON_ERROR_STOP=1, so a failing statement will exit non-zero. But if you explicitly set run_sql.transaction=false, a failing statement in the middle of a batch can be skipped and you'll still get exit_code: 0. Leave transactions on unless you really mean to opt out.

Raw SQL mode doesn't support psql client-side meta-commands with args

\i /path/to/other.sql works (it's handled client-side), but things like \copy TABLE FROM 'file.csv' depend on the client's filesystem. For file-based migrations, prefer run_sql.file with a path the Vultrino-side user can read.

Example: migration + nightly backup for a project

# 1. Store the credential once
vultrino add \
  --alias prod-db \
  --type postgres \
  --pg-host db.example.com \
  --pg-database app_production \
  --pg-user deploy \
  --pg-sslmode require

# 2. Configure per-credential defaults
vultrino meta set prod-db run_sql.file      /srv/app/migrations/apply.sql
vultrino meta set prod-db backup.output_dir /var/backups/postgres
vultrino meta set prod-db backup.filename_template "{alias}-{date}.sql"
vultrino meta set prod-db backup.format     custom

# 3. Apply migrations (agent- or cron-invokable)
vultrino action prod-db postgres.run_sql

# 4. Nightly backup (put this in cron / a scheduler)
vultrino action prod-db postgres.backup

Multiple targets? Just add more aliases — staging-db, analytics-db, prod-db-reader-only. Same plugin, independent recipes.

A common pattern is to pair this plugin with the SSH plugin: postgres.backup writes a dump locally, then ssh.deploy rsyncs the backup directory off-site. Each credential keeps its own blast radius and its own override-lock posture.

HTTP API Reference

The JSON API served by vultrino web. This page is the task-oriented route map; the exhaustive, code-verified wire reference (every field, object, enum, and error code) is docs/dev/API.md — it wins where the two differ.

Base URL & transport

  • Default base: http://127.0.0.1:7879 (override with vultrino web --bind).
  • All JSON routes are under /api/v1/.
  • Plaintext HTTP — terminate TLS at a reverse proxy for network exposure.

There is no transparent forwarding proxy and no X-Vultrino-Credential header: a client names a credential alias in the request body and Vultrino runs the action for it (see POST /api/v1/execute). vultrino serve does not serve this API.

Authentication

Authorization: Bearer vk_your_api_key      # an API key
Authorization: Bearer vut_your_use_token   # a scoped use token

A vut_ prefix is a use token; anything else is validated as an API key. Admin routes require an API key with the admin permission (use tokens rejected). The workload-exchange route instead takes a signed vwa_ assertion.

Error bodies are { "error": "message", "code": "machine_code" }. Codes by status: 400 invalid_* / execute_error; 401 missing_api_key / invalid_api_key / invalid_token; 403 permission_denied / not_admin / not_authorized / token_unusable; 404 *_not_found; 409 *_exists / idempotency_*; 500 storage_error.

Public / authenticated routes

GET /api/v1/health — no auth

{ "status": "ok", "version": "0.1.0" }

POST /api/v1/execute — run an action with a credential (API key or use token)

Vultrino injects the secret, runs the action, scrubs the response, returns it. The body is flat (not nested under params):

{
  "credential": "github-api",
  "method": "GET",
  "url": "https://api.github.com/user",
  "action": "http.request",
  "headers": { "Accept": "application/json" },
  "body": null,
  "query": {}
}
FieldRequiredNotes
credentialyesCredential alias.
methodyesHTTP method.
urlyesTarget URL (public host — SSRF guard).
actionnoCanonical plugin.action or a govder action label. Omitted → http.request.
headers / body / querynoRequest headers / JSON body / query params.

200{ "status", "headers", "body" } (body is a string, post-scrub). 202{ "outcome": "pending_approval", "approval_id": "appr_…", … } — the action did not run; poll the approval. Errors: 401 (bad bearer); 403 token_unusable (revoked/expired/exhausted token); 400 execute_error (policy denied, credential not found, SSRF block).

GET /api/v1/approvals/{id} — poll & lazily run an approved action

Authenticate with the same bearer that opened the approval. On the first poll after a human approves, the action runs at most once and the result is returned. status is one of Pending / Escalated / Approved / Denied / Expired. Errors: 401; 403 not_authorized / token_revoked; 404 approval_not_found.

GET /api/v1/credentials — list (API key, read)

{ "credentials": [ { "alias": "github-api", "credential_type": "api_key", "description": "…" } ] }

Metadata only — secrets are never returned; filtered to the caller's role scope. There is no GET /api/v1/credentials/{alias} route.

Admin routes (API key with admin only)

Use tokens are rejected. Mutating routes honor an optional Idempotency-Key. See Admin API for bodies and semantics.

MethodPathPurpose
POST / PUT / DELETE/api/v1/policies[/{id}]Manage stored policies (hot-reload).
POST / DELETE/api/v1/credentials[/{id}]Create (write-only secret) / delete by id.
POST/api/v1/tokens, /api/v1/tokens/{id}/revokeMint / revoke use tokens.
POST / DELETE/api/v1/roles[/{id}]Manage roles.
POST / DELETE/api/v1/agents/{label}/haltKill / un-kill an agent principal (V6).
GET/api/v1/sessions, /api/v1/metricsIn-flight sessions; per-process metrics (V12).
GET / POST/api/v1/events[?after=N], /api/v1/events/{seq}/replaySigned outbox replay + DLQ (V9).
PUT / DELETE/api/v1/workload-grants/{agent}Author / remove exchange grant templates.

Connector surfaces (same server)

MethodPathAuthPurpose
POST/mcpvk_ / vut_Networked MCP transport (JSON-RPC).
POST/llm, /llm/{*path}, /llm/channels/{channel}[/{*path}]vk_ / vut_Metered LLM proxy (provider gate default-deny; SSE streaming).
POST/api/v1/workload/exchangevwa_Trade a signed workload assertion for short-lived use tokens (gated by VULTRINO_WORKLOAD_EXCHANGE_ENABLED).
GET/api/v1/runtime/controlvut_Non-consuming liveness lease; 409 runtime_cancelled once revoked/expired/halted.

HTML admin panel (session auth)

Served alongside the API for human operators: /login, /dashboard (/), /credentials, /roles, /keys, /tokens, /approvals, /audit. Login is rate-limited; write forms require a CSRF token.

Admin API (runtime config-write)

The admin API lets a control plane (e.g. govder) configure the enforcement plane at runtime — push policies, mint/revoke tokens, manage roles, and register credentials — without restarting vultrino. It is served by vultrino web under /api/v1/ alongside the existing read/execute endpoints.

Authentication

Every admin endpoint requires an API key (vk_…) whose role holds the admin permission. The predefined admin role has it; grant it to a custom role with "permissions": ["admin", …]. Use tokens (vut_…) are rejected outright — admin is an API-key-only capability.

Authorization: Bearer vk_your_admin_key

Responses: 401 (missing/invalid key), 403 (valid key without admin, or a use token), 400 (invalid body), 404 (no such resource), 409 (duplicate or an in-flight idempotency key), 201/200 on success.

Idempotency

Mutating endpoints accept an optional Idempotency-Key header. While the first request is still in flight, a second with the same key gets 409. Keys are remembered for 24h.

Idempotency-Key: 5f3c…unique-per-logical-request

The key is bound to a hash of the request body: reusing a key with a different body returns 409 rather than replaying the original response.

A repeat with the same key and body is handled per route class:

  • Creates and mints (POST /policies, POST /capabilities, POST /tokens, POST /approval-tokens, POST /roles, POST /credentials) replay the original response instead of acting again, so a retried token mint never creates a second token. Minted token plaintext is not retained in the idempotency record — a replayed mint returns metadata plus a note (revoke and re-mint if you lost the original response).
  • Convergent writes (PUT /policies/{id}, PUT /capabilities/{id}, PUT /roles/{name}, POST /agents/{label}/halt) re-apply the body and return the fresh result. These address a caller-supplied deterministic id, so a second application only converges — and short-circuiting them would let a content-derived key silently leave an in-between (possibly wider) version enforced while reporting success. Each applied repeat emits its own policy.changed/capability.changed outbox event.

At-least-once on crash. Reserve → operate → record-completion are three separate atomic storage writes, not one transaction. If the process crashes after the operation persists but before completion is recorded, a retry (after the ~60s stale-reservation window) re-runs the operation. Idempotency is exactly-once only absent a mid-operation crash.

Endpoints

Policies

Policies pushed here are merged with the static [[policies]] from config.toml into the live engine (config policies stay declarative; the API manages dynamic ones by id). Each write hot-reloads the engine on the web process synchronously.

Cross-process propagation. Other long-running processes that share the same vault (notably the MCP server) reload policies on a periodic refresh (default 5s), so an admin push reaches them within that window — not instantly. For an immediate kill, revoke the use token instead: token revocation is storage-authoritative and re-checked under the lock on every gated call, so it takes effect on the very next call in every process.

MethodPathBodyResult
POST/api/v1/policies{name, credential_pattern, rules?, default_action, id?}201 canonical policy (id generated if omitted)
PUT/api/v1/policies/{id}same200 canonical policy (create-or-replace)
DELETE/api/v1/policies/{id}200 {deleted} / 404

rules and default_action use the same shape as the config file (allow / deny / prompt). An invalid credential_pattern glob is rejected with 400 rather than silently never matching.

Use tokens

MethodPathBodyResult
POST/api/v1/tokens{name, credential_scope, action_scope?, max_uses?, require_approval?, expires_in_secs?}201 {token, metadata} — plaintext shown once
POST/api/v1/tokens/{id}/revoke200 {revoked, metadata} / 404

Roles

MethodPathBodyResult
POST/api/v1/roles{name, permissions[], credential_scopes?, description?}201 role / 409 if the name exists
DELETE/api/v1/roles/{id}200 {deleted} / 404

Credentials

Secret material is write-only: it is stored encrypted and never returned by any endpoint (the create response carries metadata only).

MethodPathBodyResult
POST/api/v1/credentials{alias, metadata?, data}201 credential metadata / 409 duplicate alias
DELETE/api/v1/credentials/{id}200 {deleted} / 404

data is the tagged credential payload, e.g. {"type":"api_key","key":"…","header_name":"Authorization","header_prefix":"Bearer "}.

Webhooks

PUT /api/v1/config/webhooks (govder approval-callback target + signing key) is delivered as part of the signed webhook outbox (see the events/outbox guide), which owns webhook configuration and ordered, replayable delivery.

Deployment note (vault format)

The admin API's stored policies and idempotency records live in the encrypted vault, whose on-disk format is versioned (STORAGE_VERSION, currently 7). A newer binary reads an older vault fine, but the first write upgrades the on-disk format, after which any still-running older binary (a not-yet-upgraded MCP or CLI process sharing the same vault) is refused the vault entirely. Upgrade all vultrino processes before issuing admin writes to avoid breaking the un-upgraded enforcement plane.

Example

# Push an allow policy for github credentials (takes effect immediately).
curl -sX POST http://127.0.0.1:7879/api/v1/policies \
  -H "Authorization: Bearer $VULTRINO_ADMIN_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{"name":"gh-allow","credential_pattern":"github-*","default_action":"allow"}'

MCP Tools Reference

Complete reference for Vultrino's Model Context Protocol (MCP) tools.

Overview

Vultrino exposes tools through MCP that allow AI agents to:

  • List available credentials (list_credentials) and inspect one (get_credential_info)
  • Make authenticated HTTP requests (http_request)
  • Poll for human approval of gated actions (check_approval)

Installed plugins and granted capabilities can contribute additional named tools. Credential writes are not MCP tools (see the note below get_credential_info).

Authentication. Every tool takes an api_key argument. It accepts a regular API key (vk_…) or a use token (vut_…). A use token additionally constrains which credential and action the call may use, and how many times. The api_key argument is consumed by Vultrino and is never forwarded to the target API or plugin.

Tool Definitions

list_credentials

List all credentials available to the current session.

Schema:

{
  "name": "list_credentials",
  "description": "List all available credential aliases. Returns metadata only, never actual secrets.",
  "inputSchema": {
    "type": "object",
    "properties": {
      "api_key": { "type": "string", "description": "API key (vk_) or use token (vut_)" },
      "pattern": { "type": "string", "description": "Optional glob to filter aliases (e.g. 'github-*')" }
    },
    "required": ["api_key"]
  }
}

Input: None

Output:

{
  "credentials": [
    {
      "alias": "github-api",
      "type": "api_key",
      "description": "GitHub personal access token"
    },
    {
      "alias": "stripe-test",
      "type": "api_key",
      "description": "Stripe test mode API key"
    }
  ]
}

Required Permission: read

Example Usage:

User: "What APIs can you access?"
Agent: [calls list_credentials]
Agent: "I have access to 2 credentials: github-api and stripe-test"

http_request

Make an authenticated HTTP request using a stored credential.

Schema:

{
  "name": "http_request",
  "description": "Make an authenticated HTTP request. Agent input carries only the credential alias; Vultrino injects authentication inside the trusted connector.",
  "inputSchema": {
    "type": "object",
    "properties": {
      "api_key": {
        "type": "string",
        "description": "API key (vk_) or use token (vut_)"
      },
      "credential": {
        "type": "string",
        "description": "Alias of the credential to use for authentication"
      },
      "method": {
        "type": "string",
        "enum": ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"],
        "description": "HTTP method"
      },
      "url": {
        "type": "string",
        "description": "Target URL"
      },
      "headers": {
        "type": "object",
        "description": "Additional headers to include in the request",
        "additionalProperties": { "type": "string" }
      },
      "body": {
        "description": "Request body (for POST, PUT, PATCH requests)"
      },
      "query": {
        "type": "object",
        "description": "Query parameters to append to the URL",
        "additionalProperties": { "type": "string" }
      }
    },
    "required": ["api_key", "credential", "method", "url"]
  }
}

Input:

{
  "credential": "github-api",
  "method": "GET",
  "url": "https://api.github.com/user",
  "headers": {
    "Accept": "application/vnd.github.v3+json"
  }
}

Output:

{
  "status": 200,
  "headers": {
    "content-type": "application/json; charset=utf-8",
    "x-ratelimit-limit": "5000",
    "x-ratelimit-remaining": "4999"
  },
  "body": "{\"login\":\"username\",\"id\":12345,...}"
}

Required Permission: execute

Error Responses:

ErrorDescription
credential_not_foundThe specified credential alias doesn't exist
permission_deniedNo permission to use this credential
policy_deniedRequest blocked by policy rules
upstream_errorFailed to connect to target server

Example Usage:

User: "Get my GitHub profile"
Agent: [calls http_request with credential=github-api, url=https://api.github.com/user]
Agent: "Your GitHub profile shows you're logged in as 'username' with 42 public repos"

Approval-gated response:

If the credential, use token, or a policy requires human approval, http_request returns an "APPROVAL REQUIRED" message with an approval_id instead of a result — the action has not run. The agent should then poll check_approval (below) with that id.


check_approval

Poll a previously-gated action. Once a human approves it, this tool runs the action and returns the real result. Until then it reports the current status and tells the agent to keep polling. An agent may only check approvals it originally requested (same api_key/use token).

Schema:

{
  "name": "check_approval",
  "description": "Check the status of an action that required human approval, and retrieve its result once approved.",
  "inputSchema": {
    "type": "object",
    "properties": {
      "api_key": { "type": "string", "description": "API key or use token (same one that made the original request)" },
      "approval_id": { "type": "string", "description": "The approval id returned by the gated tool call" }
    },
    "required": ["api_key", "approval_id"]
  }
}

Output (still pending):

{
  "approval_id": "appr_xxxxxxxx",
  "status": "Pending",
  "executed": false,
  "message": "Awaiting human approval. The action has NOT run. Poll again every ~10-30 seconds."
}

Output (approved and executed):

{
  "approval_id": "appr_xxxxxxxx",
  "status": "Approved",
  "executed": true,
  "message": "Approved and executed.",
  "result": { "status": 200, "body": "..." }
}

A Denied or Expired status returns a message instructing the agent to stop and not retry. The action runs at most once no matter how many times it is polled.

Required Permission: execute (same as the original request)


get_credential_info

Return metadata about one credential — its type and description. Never exposes the secret value.

Schema:

{
  "name": "get_credential_info",
  "description": "Get metadata (type, description) for a specific credential. Does not expose the actual secret value.",
  "inputSchema": {
    "type": "object",
    "properties": {
      "api_key": { "type": "string", "description": "API key or use token" },
      "credential": { "type": "string", "description": "The credential alias or id" }
    },
    "required": ["api_key", "credential"]
  }
}

Required Permission: read


Credential writes are not MCP tools. The stdio/networked MCP server exposes exactly four built-in tools — list_credentials, http_request, get_credential_info, check_approval — plus any tools contributed by installed plugins or granted capabilities. To add or delete credentials, use the CLI (vultrino add / vultrino remove) or the admin JSON API (POST /api/v1/credentials, DELETE /api/v1/credentials/{id}). There is no add_credential or delete_credential tool.

Permission Requirements

ToolRequired Permission
list_credentialsread
get_credential_inforead
http_requestexecute
check_approvalexecute

Scope Restrictions

If the API key's role has credential scopes, tools are further restricted:

  • list_credentials — Only shows credentials matching scope patterns
  • get_credential_info — Only resolves credentials matching scope patterns
  • http_request — Only works with credentials matching scope patterns

Error Format

All MCP tool errors follow this format:

{
  "error": {
    "code": "error_code",
    "message": "Human-readable error message"
  }
}

Usage Patterns

Basic API Call

{
  "tool": "http_request",
  "arguments": {
    "credential": "github-api",
    "method": "GET",
    "url": "https://api.github.com/user"
  }
}

POST with JSON Body

{
  "tool": "http_request",
  "arguments": {
    "credential": "stripe-api",
    "method": "POST",
    "url": "https://api.stripe.com/v1/customers",
    "headers": {
      "Content-Type": "application/x-www-form-urlencoded"
    },
    "body": "[email protected]&name=Test+User"
  }
}

Check Available Credentials First

// Step 1: List what's available
{
  "tool": "list_credentials",
  "arguments": {}
}

// Step 2: Use a credential
{
  "tool": "http_request",
  "arguments": {
    "credential": "github-api",
    "method": "GET",
    "url": "https://api.github.com/repos/owner/repo"
  }
}

Best Practices for AI Agents

1. List First, Then Use

Always check available credentials before attempting to use one:

1. Call list_credentials
2. Verify the needed credential exists
3. Call http_request with the credential

2. Handle Errors Gracefully

When a credential isn't available:

Agent: "I don't have access to AWS credentials. The credentials I can use are:
- github-api (GitHub API)
- stripe-test (Stripe test mode)

Would you like to add AWS credentials?"

3. Use Appropriate Methods

  • GET — Fetch data
  • POST — Create resources
  • PUT — Replace resources
  • PATCH — Update resources
  • DELETE — Remove resources

4. Include Necessary Headers

Many APIs require specific headers:

{
  "headers": {
    "Accept": "application/json",
    "Content-Type": "application/json"
  }
}

5. Parse Response Bodies

The body field is a string. Parse it as appropriate:

  • JSON APIs: JSON.parse(response.body)
  • XML APIs: Parse as XML
  • Plain text: Use directly

Security Notes

  1. Alias-only schema — Raw credential fields are not present in MCP tool input/output
  2. All requests are logged — Audit trail of all tool usage
  3. Policies are enforced — URL and method restrictions apply
  4. Rate limits apply — Prevent abuse
  5. Scopes restrict access — Roles limit which credentials are visible

LLM Reference

This section provides documentation optimized for Large Language Models (LLMs) to understand and use Vultrino effectively.

Quick Reference

What is Vultrino?

Vultrino is a credential proxy that allows applications (including AI agents) to make authenticated API requests without seeing the actual credentials.

Key Concept: You use credential aliases (like "github-api"), not actual secrets.

Available Paths

For programmatic access to documentation, these raw markdown paths are available:

ContentPath
Full reference/llm/full-reference.md
Quick start/getting-started/quickstart.md
CLI commands/components/cli.md
HTTP API/api/http.md
MCP tools/api/mcp-tools.md

Condensed Reference

Making Authenticated Requests

Via HTTP API (vultrino web, default port 7879):

curl -sX POST http://localhost:7879/api/v1/execute \
     -H "Authorization: Bearer vk_your_api_key_here" \
     -H "Content-Type: application/json" \
     -d '{"credential": "<alias>", "method": "GET", "url": "https://target-api.com/endpoint"}'

Via MCP (AI Agents):

{
  "tool": "http_request",
  "arguments": {
    "api_key": "vk_your_api_key_here",
    "credential": "<alias>",
    "method": "GET",
    "url": "https://target-api.com/endpoint"
  }
}

Via CLI (requires running Vultrino web server):

vultrino --key vk_your_api_key request <alias> https://target-api.com/endpoint

MCP Tools Summary

ToolPurposeRequired Permission
list_credentialsList available credentialsread
http_requestMake authenticated requestexecute
check_approvalRetrieve the result of an action that required human approvalexecute
get_credential_infoGet credential metadataread

Important: Every tool call requires an api_key parameter for authentication. This value may be a regular API key (vk_…) or a use token (vut_…) — a single-use or time-scoped grant restricted to one credential/action.

Approval gating: If a request requires human approval, http_request returns an "APPROVAL REQUIRED" message with an approval_id and the action does not run. Poll check_approval with that id (re-sending your api_key) every ~10–30 seconds; once a human approves, it returns the real result. A denied/expired approval tells you to stop — do not retry.

Common Credential Aliases

Typical naming patterns:

  • github-api — GitHub API token
  • stripe-live / stripe-test — Stripe API keys
  • openai — OpenAI API key
  • anthropic — Anthropic API key
  • aws-prod / aws-staging — AWS credentials

For AI Agents

Authentication Model: Every tool call requires your API key. This enables multiple agents to use the same MCP server with different scoped keys.

Step 1: Check Available Credentials

{
  "tool": "list_credentials",
  "arguments": {
    "api_key": "vk_your_api_key_here"
  }
}

Response:

{
  "credentials": [
    {"alias": "github-api", "type": "api_key", "description": "..."},
    {"alias": "stripe-test", "type": "api_key", "description": "..."}
  ]
}

Step 2: Make Request

{
  "tool": "http_request",
  "arguments": {
    "api_key": "vk_your_api_key_here",
    "credential": "github-api",
    "method": "GET",
    "url": "https://api.github.com/user"
  }
}

Response:

{
  "status": 200,
  "headers": {"content-type": "application/json"},
  "body": "{\"login\":\"username\",...}"
}

Step 3: Parse Response

The body field is a JSON string. Parse it to access the data.

Error Handling

If a credential isn't available:

  1. List what IS available
  2. Explain to user
  3. Offer alternatives

Example response to user:

"I don't have access to AWS credentials. I can access: github-api, stripe-test. Would you like to add AWS credentials?"

HTTP API Quick Reference

Base URL http://127.0.0.1:7879 (served by vultrino web). All routes are under /api/v1/. Authenticate every call with Authorization: Bearer vk_… (API key) or vut_… (use token). There is no credential header and no transparent proxy.

Execute an action

POST /api/v1/execute
Authorization: Bearer <vk_ or vut_ token>
Content-Type: application/json

{"credential": "<alias>", "method": "GET", "url": "https://api.example.com/endpoint"}

The body is flat; action is optional (defaults to http.request). Optional headers, body, and query fields are also accepted.

List Credentials

GET /api/v1/credentials
Authorization: Bearer <vultrino-api-key>

Configuration Summary

Environment Variables

  • VULTRINO_PASSWORD — Storage encryption password (required)
  • VULTRINO_CONFIG — Config file path
  • RUST_LOG — Log level

Default Ports

  • 7879vultrino web: HTTP JSON API (/api/v1/…, /mcp, /llm) and the HTML admin UI
  • stdio — vultrino mcp: MCP server for local AI agents (no port)

File Locations

  • ~/.local/share/vultrino/credentials.enc — Encrypted vault (default; ~/ path is configurable)
  • Config: --config <path>, else the OS config dir (~/.config/vultrino/config.toml on Linux)

Security Model

  1. Credentials encrypted at rest — AES-256-GCM
  2. Aliases only — Raw credential fields are absent from the LLM-facing schema
  3. RBAC — Role-based access control via API keys
  4. Policies — URL/method restrictions
  5. Audit logging — Track all usage

API Key Authentication

Vultrino uses per-request API key authentication. Include your API key in every tool call:

{
  "tool": "list_credentials",
  "arguments": {
    "api_key": "vk_your_api_key_here"
  }
}

This design enables:

  • Multiple agents using the same MCP server with different keys
  • Scoped access - each key has its own permissions and credential access
  • No session state - stateless, secure by default

The API key determines what credentials you can access based on your assigned role:

RolePermissionsUse Case
executorread, executeAI agents (recommended)
read-onlyreadListing credentials only
adminallFull administrative access

Every tool call requires the api_key parameter.

Common Tasks

"List my credentials"

{"tool": "list_credentials", "arguments": {"api_key": "vk_..."}}

"Get my GitHub user info"

{
  "tool": "http_request",
  "arguments": {
    "api_key": "vk_...",
    "credential": "github-api",
    "method": "GET",
    "url": "https://api.github.com/user"
  }
}

"Create a Stripe customer"

{
  "tool": "http_request",
  "arguments": {
    "api_key": "vk_...",
    "credential": "stripe-api",
    "method": "POST",
    "url": "https://api.stripe.com/v1/customers",
    "headers": {"Content-Type": "application/x-www-form-urlencoded"},
    "body": "[email protected]"
  }
}

"List GitHub repos"

{
  "tool": "http_request",
  "arguments": {
    "api_key": "vk_...",
    "credential": "github-api",
    "method": "GET",
    "url": "https://api.github.com/user/repos"
  }
}

Vultrino Complete LLM Reference

This document contains everything an LLM needs to know to use Vultrino effectively.


What is Vultrino?

Vultrino is a credential proxy for the AI era. It allows AI agents and applications to make authenticated API requests without ever seeing the actual credentials.

Core Concept: You reference credentials by alias (e.g., "github-api"), and Vultrino automatically injects the real credential into your request.


MCP Tools

list_credentials

Lists all credentials you have access to.

Input: None required

Output:

{
  "credentials": [
    {
      "alias": "github-api",
      "type": "api_key",
      "description": "GitHub personal access token"
    }
  ]
}

Permission Required: read


http_request

Makes an authenticated HTTP request.

Input:

{
  "credential": "string (required) - credential alias",
  "method": "string (required) - GET|POST|PUT|PATCH|DELETE",
  "url": "string (required) - target URL",
  "headers": "object (optional) - additional headers",
  "body": "string (optional) - request body"
}

Output:

{
  "status": 200,
  "headers": {"content-type": "application/json"},
  "body": "string - response body"
}

Permission Required: execute

Example:

{
  "credential": "github-api",
  "method": "GET",
  "url": "https://api.github.com/user"
}

get_credential_info

Returns metadata (type, description) for one credential — never the secret.

Input:

{
  "credential": "string (required) - alias or id"
}

Permission Required: read


check_approval

Polls an action that was gated for human approval. Once approved, this tool runs the original action and returns its result; while pending it says to keep polling. You may only poll approvals created by the same api_key/use token.

Input:

{
  "approval_id": "string (required) - the appr_… id from a gated call"
}

Permission Required: execute

Credential writes are not MCP tools. The stdio MCP server exposes only list_credentials, http_request, get_credential_info, and check_approval (plus any plugin/capability tools). Adding or deleting credentials is done with the CLI (vultrino add / vultrino remove) or the admin JSON API — there is no add_credential / delete_credential MCP tool.

Every tool call also takes an api_key argument — a vk_ API key or a vut_ use token — which Vultrino consumes and never forwards to the target.


HTTP API Endpoints

Served by vultrino web at http://127.0.0.1:7879. All routes are under /api/v1/ and take Authorization: Bearer vk_… or vut_…. There is no credential header and no transparent proxy. Full wire reference: docs/dev/API.md.

Execute an action

POST /api/v1/execute
Authorization: Bearer {vk_ or vut_ token}
Content-Type: application/json

{
  "credential": "alias",
  "method": "GET",
  "url": "https://...",
  "action": "http.request",   // optional; defaults to http.request
  "headers": {},              // optional
  "body": null,               // optional
  "query": {}                 // optional
}

Returns { "status", "headers", "body" } on 200, or a 202 pending-approval envelope with an approval_id to poll.

List Credentials (metadata only)

GET /api/v1/credentials
Authorization: Bearer {api_key}    # requires the `read` permission

Create / Delete Credential (admin API key only)

POST   /api/v1/credentials         # body: { alias, metadata?, data }
DELETE /api/v1/credentials/{id}    # by id, not alias

There is no GET /api/v1/credentials/{alias} route. See Admin API.


CLI Commands

# Initialize
vultrino init

# Add credential
vultrino add --alias NAME --key SECRET

# List credentials
vultrino list

# Make request (credential alias is the first positional argument)
vultrino request ALIAS URL

# Start the HTTP API + web UI (default 127.0.0.1:7879)
vultrino web

# Start MCP server (stdio) for AI agents
vultrino mcp   # or: vultrino serve --mcp

# Manage roles
vultrino role create NAME --permissions read,execute
vultrino role list
vultrino role delete NAME

# Manage API keys
vultrino key create NAME --role ROLE
vultrino key list
vultrino key revoke KEY_PREFIX

Credential Types

api_key

  • For API tokens, bearer tokens
  • Injected as: Authorization: Bearer {key}

basic_auth

  • For username/password
  • Injected as: Authorization: Basic {base64(user:pass)}

oauth2

  • For OAuth2 with refresh tokens
  • Handles token refresh automatically

Permissions

PermissionDescription
readList credentials (metadata only)
writeCreate new credentials
updateModify existing credentials
deleteRemove credentials
executeUse credentials for requests

Error Codes

CodeMeaning
invalid_api_key / invalid_tokenMissing or invalid bearer token
permission_deniedRole lacks the required permission
token_unusableUse token revoked, expired, or exhausted
execute_errorPolicy denied, credential not found, SSRF block, or plugin error

Common Patterns

List then use

1. list_credentials → see what's available
2. http_request → use the appropriate credential

Handle missing credentials

When a credential isn't available:

  1. List available credentials
  2. Tell user what's available
  3. Suggest adding the needed credential

Parse response body

The body field in http_request response is always a string. Parse it according to the content-type:

  • application/json → JSON.parse()
  • text/plain → use directly
  • text/html → use directly

Security Notes

  1. Never ask for actual secrets - only use aliases
  2. Credential payload fields are absent - listing endpoints return metadata
  3. All requests are logged - audit trail exists
  4. Policies may restrict access - some URLs/methods may be blocked
  5. Scopes limit visibility - you may not see all credentials

Environment

VariablePurpose
VULTRINO_PASSWORDDecryption password (required)
VULTRINO_CONFIGConfig file path
RUST_LOGLog level
PortService
7879vultrino web — HTTP JSON API (/api/v1/…, /mcp, /llm) + admin UI
stdiovultrino mcp — MCP server for local AI agents (no port)

Quick Examples

Get GitHub user:

{"tool": "http_request", "arguments": {"credential": "github-api", "method": "GET", "url": "https://api.github.com/user"}}

Create Stripe customer:

{"tool": "http_request", "arguments": {"credential": "stripe-api", "method": "POST", "url": "https://api.stripe.com/v1/customers", "headers": {"Content-Type": "application/x-www-form-urlencoded"}, "body": "[email protected]"}}

List repos:

{"tool": "http_request", "arguments": {"credential": "github-api", "method": "GET", "url": "https://api.github.com/user/repos"}}

Post to Slack:

{"tool": "http_request", "arguments": {"credential": "slack-webhook", "method": "POST", "url": "https://hooks.slack.com/services/xxx", "headers": {"Content-Type": "application/json"}, "body": "{\"text\":\"Hello!\"}"}}