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
Section titled “Schema”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.
Top-level Fields
Section titled “Top-level Fields”- 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.
agent (object)
Section titled “agent (object)”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
session
Section titled “session”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
derived (object)
Section titled “derived (object)”config
Section titled “config”Configuration artifacts and permissions.
- skillsCids: (object)
Map of skill names to their respective directory CIDs- e.g.,
"tmux": "<CID>"
- e.g.,
- permissions: (array of strings)
- List of granted permissions (example:
WebFetch)
- List of granted permissions (example:
request
Section titled “request”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)
- List of tool names available (e.g.,
environment (object)
Section titled “environment (object)”Captured environment variables
Section titled “Captured environment variables”- 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:
| Variable | Source |
|---|---|
DAYTONA_SANDBOX_SNAPSHOT, DAYTONA_SANDBOX_ID, DAYTONA_ORGANIZATION_ID | Daytona sandboxes |
E2B_SANDBOX, E2B_TEMPLATE_ID, E2B_SANDBOX_ID | E2B 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.
Quickstart
Section titled “Quickstart”Install
Section titled “Install”Via Nix:
nix profile install 'git+ssh://git@github.com/eqtylab/gateway.git#viper'# installs at ~/.nix-profile/bin/viperVia Cargo:
cargo install --git ssh://git@github.com/eqtylab/gateway.git viper# installs at ~/.cargo/bin/viperVerify install
Section titled “Verify install”viper --version# 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 stepviper did add --assign claude
# assign a key to an app (interactive selector)viper did assign
# assign non-interactivelyviper did assign --app claude --did did:key:z6Mk...
# register an app-specific did with gateway using an existing tokenviper 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.tomlviper did register --app claudeViper wraps your coding agent, proxying all LLM API calls through the gateway with signed requests:
# claude codeviper claude
# opencodeviper opencode
# codexviper codex
# openclaw (run standalone proxy and point OpenClaw at the printed URL)viper proxy --app openclaw --gateway-endpoint https://gateway.example.com/v1/openai_apiConfiguration
Section titled “Configuration”Viper stores its configuration at ~/.config/viper/:
| Path | Description |
|---|---|
config.toml | Main 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_URLVIPER_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.
Sandbox support documentation
Section titled “Sandbox support documentation”For product-facing documentation of current Daytona/E2B sandbox detection support and trust boundaries, see sandbox-support.md.
config.toml
Section titled “config.toml”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_varsare 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_filesare localProjectMembershipV1credential 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_cidsare 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_urnselects 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.
Docker
Section titled “Docker”# from the repo rootdocker build -f viper/Dockerfile -t viper .
# or from the viper/ directorydocker build -f Dockerfile -t viper ..Mount the config directory so keys and config persist across runs:
# initial setup (interactive)docker run -it -v ~/.config/viper:/home/viper/.config/viper viper:latest did adddocker run -it -v ~/.config/viper:/home/viper/.config/viper viper:latest did register --provisioning-token ggv1....
# run with an agentdocker run -it -v ~/.config/viper:/home/viper/.config/viper viper:latest claudeThe provisioning token can also be passed via environment variable:
docker run -it \ -v ~/.config/viper:/home/viper/.config/viper \ -e VIPER_PROVISIONING_TOKEN=ggv1.... \ viper:latest did registerCI / Container Images
Section titled “CI / Container Images”Viper images are automatically built and pushed to GHCR on merges to main and version tags:
ghcr.io/eqtylab/guardian-viper:latestghcr.io/eqtylab/guardian-viper:<tag>ghcr.io/eqtylab/guardian-viper:sha-<commit>YubiKey Setup
Section titled “YubiKey Setup”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
- 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.
- 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='
Replace:
claude with codex, opencode, openclaw, or proxy as needed
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
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.