Skip to content

Viper

Verifiable Identities for Provenance Enriched Runtimes — a CLI sidecar that wraps AI coding agent invocations with cryptographic identity and provenance. It runs a localhost reverse proxy that signs requests (RFC 9421, Ed25519) and injects attestation data before forwarding to the gateway.

Schema Version: viper/data/v0.1
Purpose:
Defines the structure for capturing agent state, which includes environmental context, configuration, and capabilities.

Please use struct definitions as the canonical schema definition.

  • schema: (string)
    The version identifier for the schema format.
  • timestamp: (string, ISO8601)
    The time the data was generated.
  • nonce: (string, UUID)
    A unique identifier for the data instance.
  • agent: (object)
    Metadata about the agent and its runtime environment. The schema for this section may differ drastically between different runtime environments and agent application types (e.g. ClaudeCode vs. Codex).
  • derived: (object)
    Derived or calculated properties pertaining to environment configuration and request context. This section has normalized properties across different agent application types.
  • environment: (object)
    Captured environment variables from the host process at attestation time. See Captured environment variables below.

Describes the device environment where the agent runs.

  • type: Type of host (e.g., EmployeeDevice)
  • hostName: Device hostname
  • hostArch: Architecture (e.g., x86_64)
  • hostKernelName: Kernel type (e.g., Linux)
  • hostKernelRelease: Kernel release version
  • hostKernelVersion: Detailed kernel version string

Viper proxy service information.

  • viperVersion: Viper proxy version
  • viperCid: Content identifier (CID) for the proxy binary

Describes the current agent application session.

  • sessionId: Unique session identifier (UUID)
  • user: Username or session owner
  • parentProcess: Parent shell/process (e.g., bash)
  • cwd: Current working directory
  • git: (object) Git details:
    • branch: Branch name
    • commit: Commit SHA
    • remote: Git remote URL
    • dirty: Boolean, indicates if there are uncommitted changes

Agent application-related metadata.

  • type: Application type (e.g., ClaudeCode)
  • claudeVersion: Application version
  • claudeCid: CID representing the specific app version
  • claudeConfigFilesCid: CID for the config files used by the app

Configuration artifacts and permissions.

  • skillsCids: (object)
    Map of skill names to their respective directory CIDs
    • e.g., "tmux": "<CID>"
  • permissions: (array of strings)
    • List of granted permissions (example: WebFetch)

Describes the agent context from the request.

  • provider: Name of the model provider (e.g., Anthropic)
  • model: Model name (e.g., opus-4.6)
  • systemPromptCid: CID of the system prompt used
  • tools: (array of strings)
    • List of tool names available (e.g., bash)
  • vars: (object)
    Map of environment variable name → value, captured from the host process at the time the attestation was built.

Only variables whose names appear in the configured env_vars list (see Configuration) are read. Names that are not set in the environment are silently omitted, so the map only ever contains names that were actually present. Values are recorded verbatim — Viper does not redact, hash, or transform them — so operators should be deliberate about which names they enable.

Default capture list targets sandbox provenance:

VariableSource
DAYTONA_SANDBOX_SNAPSHOT, DAYTONA_SANDBOX_ID, DAYTONA_ORGANIZATION_IDDaytona sandboxes
E2B_SANDBOX, E2B_TEMPLATE_ID, E2B_SANDBOX_IDE2B sandboxes

Because the captured values are part of the content-addressed state attestation that Viper signs and the gateway persists, gateway-side policies (OPA/Rego, etc.) can read them out of the attestation and gate requests on sandbox identity or any other captured signal.

Via Nix:

Terminal window
nix profile install 'git+ssh://git@github.com/eqtylab/gateway.git#viper'
# installs at ~/.nix-profile/bin/viper

Via Cargo:

Terminal window
cargo install --git ssh://git@github.com/eqtylab/gateway.git viper
# installs at ~/.cargo/bin/viper
Terminal window
viper --version
Terminal window
# create a local DID key (on first run, viper will prompt for a gateway URL)
viper did add
# create local did key and assign it to an app in one step
viper did add --assign claude
# assign a key to an app (interactive selector)
viper did assign
# assign non-interactively
viper did assign --app claude --did did:key:z6Mk...
# register an app-specific did with gateway using an existing token
viper did register --app claude --provisioning-token ggv1....
# or omit the token to run browser login + automatic local handoff
# (opens Guardian UI, mints a token there, and sends it back to viper via loopback callback)
# requires `guardian_url` in ~/.config/viper/config.toml
viper did register --app claude

Viper wraps your coding agent, proxying all LLM API calls through the gateway with signed requests:

Terminal window
# claude code
viper claude
# opencode
viper opencode
# codex
viper codex
# openclaw (run standalone proxy and point OpenClaw at the printed URL)
viper proxy --app openclaw --gateway-endpoint https://gateway.example.com/v1/openai_api

Viper stores its configuration at ~/.config/viper/:

PathDescription
config.tomlMain config (gateway URL, default DID)
keys/Ed25519 keyring
blobs/Local content-addressed blobs
logs/Session logs

The config file path can be overridden with the VIPER_CONFIG environment variable. You can also override the gateway endpoints with:

  • VIPER_GATEWAY_URL
  • VIPER_GUARDIAN_URL

If no config file exists, setting VIPER_GATEWAY_URL lets Viper create one without prompting.

Trusting the MITM root in the macOS keychain

Section titled “Trusting the MITM root in the macOS keychain”

Inside a wrapped session, trust for the egress MITM root is injected via env vars (SSL_CERT_FILE, CURL_CA_BUNDLE, NODE_EXTRA_CA_CERTS, …), which covers Node, Python, curl, and git. Go programs on macOS — notably gh — validate TLS against the system keychain and ignore those vars, so they fail with x509: certificate is not trusted. To fix that, run viper ca trust once: it adds the Viper root to your login keychain with SSL-only trust (never the admin/system domain; expect the macOS password dialog). viper ca trust --check reports status for scripting, and viper ca untrust removes the trust again. viper ca regenerate automatically removes stale keychain trust and offers to trust the new root, so the keychain never drifts from the on-disk CA.

For product-facing documentation of current Daytona/E2B sandbox detection support and trust boundaries, see sandbox-support.md.

default_did = "did:key:z6Mk..."
llm_gateway_api_url = "https://gateway.example.com/v1"
guardian_url = "https://guardian.example.com"
# Optional. Environment variable names to capture into state attestations.
# Only names that are actually present in the process environment are recorded.
# If omitted, defaults to the sandbox-provenance list (see
# "Captured environment variables" above).
env_vars = [
"DAYTONA_SANDBOX_SNAPSHOT",
"DAYTONA_SANDBOX_ID",
"DAYTONA_ORGANIZATION_ID",
"E2B_SANDBOX",
"E2B_TEMPLATE_ID",
"E2B_SANDBOX_ID",
]
# Optional project-presentation.v1 configuration for the Codex DID.
# Credential JSON remains in separate local files; relative paths resolve
# from ~/.config/viper/. CIDs must use the JSON_JCS multicodec.
[project_presentations.codex]
credential_files = ["memberships/project-a.json"]
credential_cids = ["baga6yaq..."]
project_urn = "urn:uuid:7f0e6f3a-0000-4000-8000-000000000000"

Values captured via env_vars are stored verbatim in the state attestation blob (which is content-addressed and persisted by the gateway). Do not list secrets you don’t want appearing in audit data.

Project presentation is app-scoped (claude, codex, opencode, openclaw, or proxy) because each app has its own signer DID.

  • credential_files are local ProjectMembershipV1 credential JSON files. Viper checks that each subject matches the app DID, JCS-canonicalizes the JSON, stores it as a JSON_JCS blob, and uploads it through the signed blob endpoint before first use.
  • credential_cids are durable references for membership credentials whose JSON_JCS CIDs are already known. If the matching blob is local, Viper validates and uploads it. If it is not local, Viper sends the CID and relies on the Gateway already having the blob or indexed membership evidence.
  • project_urn selects one active project when multiple memberships could match. It does not prove membership by itself.

Viper automatically merges the registered agent VC CID from its DID state into the signed X-Viper-Credential-CID list; do not add that CID here. The list is sorted and deduplicated and may contain at most eight total CIDs, including the automatic agent VC. Use a selector for multi-project or multiple CID-only configurations. Keep config.toml and credential files readable only by the local user.

Terminal window
# from the repo root
docker build -f viper/Dockerfile -t viper .
# or from the viper/ directory
docker build -f Dockerfile -t viper ..

Mount the config directory so keys and config persist across runs:

Terminal window
# initial setup (interactive)
docker run -it -v ~/.config/viper:/home/viper/.config/viper viper:latest did add
docker run -it -v ~/.config/viper:/home/viper/.config/viper viper:latest did register --provisioning-token ggv1....
# run with an agent
docker run -it -v ~/.config/viper:/home/viper/.config/viper viper:latest claude

The provisioning token can also be passed via environment variable:

Terminal window
docker run -it \
-v ~/.config/viper:/home/viper/.config/viper \
-e VIPER_PROVISIONING_TOKEN=ggv1.... \
viper:latest did register

Viper images are automatically built and pushed to GHCR on merges to main and version tags:

ghcr.io/eqtylab/guardian-viper:latest
ghcr.io/eqtylab/guardian-viper:<tag>
ghcr.io/eqtylab/guardian-viper:sha-<commit>

YubiKey Setup for Viper This guide explains how to prepare a YubiKey for use as a Viper DID signer.

Why this setup is needed viper did add —signer-type yubikey expects to find an existing PIV key in a YubiKey slot and to be able to read that slot’s public key metadata.

A brand new or reset YubiKey often has:

the PIV app enabled, but no key in the signing slot default PIN / PUK / management key values no certificate stored for the slot In that state, Viper cannot derive a DID from the YubiKey slot, so setup must happen first.

Prerequisites Use the repo dev shell so the required tools are available:

nix develop

The shell includes:

ykman yubico-piv-tool pcsc_scan openssl

  1. Confirm the YubiKey is visible Check that the smartcard stack can see the device:

pcsc_scan

What this does:

verifies that the OS PC/SC stack can talk to the YubiKey over CCID confirms the device is inserted and readable If this fails, Viper will not be able to use the YubiKey either.

  1. Inspect the device and PIV app ykman info ykman piv info yubico-piv-tool -a status

What these do:

ykman info shows the model, serial, firmware, and enabled interfaces ykman piv info shows the PIV state, including remaining PIN retries yubico-piv-tool -a status shows which slots are populated What to look for:

the serial number, if you want to target a specific YubiKey whether slot 9c is empty whether the PIN / PUK / management key are still defaults 3. Change the default PIN Do this before provisioning if the key is fresh and still using the default PIN.

yubico-piv-tool -a change-pin -P 123456 -N ''

What this does:

authenticates with the default PIV PIN 123456 replaces it with your chosen PIN Why it matters:

Viper needs the PIN to access the YubiKey-backed signer leaving the default PIN in place is not acceptable outside of throwaway testing Important:

repeated bad PIN attempts reduce the retry counter if retries are exhausted, recovery becomes more disruptive 4. Generate a P-256 key in slot 9c Viper currently expects a P-256 key for YubiKey-backed DIDs.

yubico-piv-tool -s 9c -a generate -A ECCP256 -o /tmp/yubi-9c-pubkey.pem

What this does:

creates a new ECC P-256 keypair on the YubiKey in slot 9c writes the public key to /tmp/yubi-9c-pubkey.pem Why it matters:

the private key stays on the YubiKey Viper uses this slot as the hardware-backed signing key without a key in the slot, viper did add —signer-type yubikey cannot succeed 5. Create a certificate for the slot yubico-piv-tool -s 9c
-a verify-pin -P ''
-S ‘/CN=Viper YubiKey DID/’
-a selfsign-certificate
-i /tmp/yubi-9c-pubkey.pem
-o /tmp/yubi-9c-cert.pem

What this does:

verifies your PIN uses the slot’s public key creates a self-signed certificate for that key writes the certificate to /tmp/yubi-9c-cert.pem Why it matters:

the current Viper YubiKey path expects the slot to expose usable public key material having a certificate in the slot makes the slot easier to inspect and verify 6. Import the certificate into slot 9c yubico-piv-tool -s 9c -a import-certificate -i /tmp/yubi-9c-cert.pem

What this does:

stores the generated certificate in the same PIV slot Why it matters:

this makes the slot readable by certificate inspection tools it also gives you a direct way to confirm the slot is provisioned correctly 7. Verify the slot is provisioned yubico-piv-tool -s 9c -a read-certificate -o /tmp/yubi-9c-cert.pem openssl x509 -in /tmp/yubi-9c-cert.pem -text -noout

What this does:

reads back the certificate from slot 9c prints the certificate contents Why it matters:

if this fails, the slot is still not ready for Viper if it succeeds, the slot is provisioned and readable 8. Create the Viper DID from the YubiKey Use the YubiKey PIN in the environment when running Viper:

VIPER_YUBIKEY_PIN='' viper did add —signer-type yubikey —assign claude —yubikey-serial

Replace:

claude with codex, opencode, openclaw, or proxy as needed with the YubiKey serial from ykman info What this does:

opens the specific YubiKey verifies the PIN reads the slot metadata and public key derives a did:key from the YubiKey-backed P-256 key stores the signer in Viper’s keyring optionally assigns it to a specific app 9. Register the DID with the gateway After the DID is added locally, register it with your target gateway:

viper did register —app claude

Or with a provisioning token:

viper did register —app claude —provisioning-token ''

Why it matters:

adding the DID locally is not enough the gateway must trust and register the signing key before requests will be accepted Security notes At minimum, do not leave these values at their defaults:

PIN PUK management key For local experimentation, changing just the PIN may be enough to get started. For any shared or longer-lived use, change all three.

Common failure modes wrong pin Meaning:

the YubiKey was found the PIV app was reached the provided PIN was incorrect Fix:

verify you are using the right YubiKey verify the right PIN use —yubikey-serial if multiple keys are attached failed to read YubiKey slot metadata Meaning:

Viper reached the device, but could not read usable metadata for the selected slot Common causes:

slot 9c is empty the slot is not provisioned with a P-256 key the device/firmware does not support the metadata command Viper currently uses Fix:

provision slot 9c as shown above confirm yubico-piv-tool -s 9c -a read-certificate … works Failed fetching certificate Meaning:

the slot does not currently contain a readable certificate Fix:

generate the key create the certificate import the certificate Recommended local verification flow If you are debugging YubiKey support locally, use this order:

pcsc_scan ykman info ykman piv info yubico-piv-tool -a status provision slot 9c verify read-certificate run viper did add —signer-type yubikey run viper did register That sequence separates transport problems, PIV state problems, slot provisioning problems, and Viper-specific problems.