Skip to content

The security layer

The security layer is what makes Lupid more than an audit log. Every request your agent makes passes through it, and the layer can block, redact, defer, or allow each one based on rules you write.

Every tool call is evaluated against a declarative policy attached to the agent. The engine returns one of three outcomes:

  • allow — the call proceeds, the audit row notes the policy id and allow.
  • deny — the call never leaves Lupid; agent sees HTTP 403.
  • require_hitl — the call parks in the approval queue (see below) and only goes through once a human approver clicks through.

Policies live per-agent. The engine evaluates against (principal, action, resource) where principal is the agent, action is the tool name, and resource is whatever the tool is touching (a URL, a file path, a database).

A policy that allows the agent to call any tool but require_hitl on bash looks like:

permit(principal == Agentum::Agent::"<agent-uuid>", action, resource);
@advice("require_hitl:bash")
permit(
principal == Agentum::Agent::"<agent-uuid>",
action == Action::"call_tool",
resource is Agentum::Tool
) when { resource.name == "bash" };

See Writing a policy for the full walk-through.

DLP scans the request body before the request leaves Lupid, and the response body before it returns to the agent. The detector ships seventeen kinds:

CategoryDetectors
PIIEmail, PhoneNumber, SsnUs, CreditCard, IpAddress
API keysAwsAccessKeyId, AwsSecretAccessKey, GithubToken, SlackToken, OpenAiKey, AnthropicKey, GoogleApiKey
Auth tokensJwtToken, BearerToken
CryptographicPrivateKeyPem
HeuristicGenericHighEntropy

For each detector, you set a per-severity action. Lupid maps each detector to one of Low, Medium, High, Critical. The action options are:

  • observe — log the hit, don’t block, don’t redact.
  • warn — log the hit, emit an alert, but let the call through.
  • redact — replace the matched substring with a tag like [EMAIL] before the request leaves (or before the response reaches the agent).
  • block — drop the request entirely; agent gets a 403.

The defaults ship safe: block at High and Critical, warn at Medium, observe at Low. Override via the AGENTUM_DLP_ACTION_HIGH / AGENTUM_DLP_ACTION_CRITICAL env vars.

Agent credentials never live in the agent’s environment as long-lived static values. Operators store them once in the vault; agents request short-lived leases at the moment they need to use them.

The vault supports four backends, picked by AGENTUM__VAULT_BACKEND:

  • static (default) — encrypted at rest in the credentials Postgres column with AES-256-GCM, key derived from AGENTUM_ENCRYPTION_KEY_HEX.
  • hashicorp — HashiCorp Vault KV v2 mount.
  • aws — AWS Secrets Manager.
  • azure — Azure Key Vault.

A lease workflow:

  1. Operator stores the underlying credential: agentum vault store --agent-id <id> --service-name stripe --from-env STRIPE_KEY.
  2. Agent (or operator on its behalf) requests a lease: agentum vault issue --agent-id <id> --service-name stripe. The response includes the plaintext value and an expires_at timestamp.
  3. The agent uses the value directly, or — on the proxy path — the agent puts the placeholder Authorization: Bearer agentum://stripe-key in its request and the gateway substitutes the real value before the request leaves.
  4. The lease auto-expires. Revoke immediately with agentum vault revoke <lease-id>, or revoke every lease for an agent (e.g. on suspension) via POST /api/v1/vault/revoke-agent/<agent-id>.

See Credential vault for the full walk-through.

Some tool calls are too consequential to enforce purely by policy — buying inventory, sending production email, deleting customer data. Lupid lets you tag those with @advice("require_hitl:<reason>") in the policy. When a matching call comes through:

  1. The request parks in the approval_requests table with state pending.
  2. An HMAC-signed webhook fires to every configured webhook URL. The payload has the agent id, tool, resource, reason, and a deep link into the dashboard.
  3. The agent either blocks synchronously (long-poll) or gets a 202 with a poll URL.
  4. An operator with the right role clicks Approve or Deny on the HITL page.
  5. The request proceeds (or doesn’t) and an audit event is written either way.

You can require multiple approvers per request (set required_approvals on the policy advice). The queue dedupes pending requests on dedup_key.

Every agent has a state machine: Provisioning → Active → Suspended → Quarantined → Decommissioned. You can move an agent between states from the dashboard or via agentum agent {suspend,quarantine,kill,activate}.

Suspended adds the agent’s JWT to the revocation blocklist. The blocklist is a bloom-filtered list backed by Postgres, distributed across replicas via LISTEN/NOTIFY. Within ~100 ms the kill propagates everywhere. Quarantined goes further and tags every audit event from the agent for investigator review.