---
title: "Agent Security Guide"
description: "Build on AINative securely — scoped API keys, memory namespace isolation, content quarantine, heartbeat oversight, approval gates, and audit logs."
canonical: "https://docs.ainative.studio/docs/security/agent-security-guide"
last-updated: "2026-10-03T21:47:10.764Z"
---

# Agent Security Guide

Source: https://docs.ainative.studio/docs/security/agent-security-guide

> Build on AINative securely — scoped API keys, memory namespace isolation, content quarantine, heartbeat oversight, approval gates, and audit logs.

import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';

## Building on AINative Securely

This guide covers the security primitives shipped in the Agent Security Sprint. Every section maps to a real API — copy the examples directly.

---

## The Threat Model

AI agents sit at the intersection of three dangers — the **lethal trifecta**:

1. **Private data** — agents read memories, user records, and org secrets.
2. **Untrusted content** — agents process emails, web scrapes, uploaded files, and tool responses that may contain adversarial payloads.
3. **External communications** — agents call APIs, send emails, and write to databases.

When these converge without controls, a single malicious document can exfiltrate private data over external channels.

| Threat | Defense |
|--------|---------|
| Over-privileged credentials | Agent-scoped API keys with TTL |
| Cross-session memory contamination | Namespace isolation |
| Prompt injection via untrusted content | Quarantine API |
| Runaway long-running agents | Heartbeat + dead-man switch |
| Unauthorized high-risk actions | Approval gates |
| No visibility into what agents did | Audit log |

---

## 1. Credential Hygiene — Scoped API Keys

Your root `ZERODB_API_KEY` grants full access. Agent-scoped keys restrict each agent to only the permissions it needs, with an automatic expiry.

### Create a key

**`POST /api/v1/auth/keys`**

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `name` | string | yes | Human-readable label (1–128 chars) |
| `ttl_seconds` | integer | no | Seconds until expiry. Omit for non-expiring. |
| `scopes` | array | yes | Permissions: `<service>:<permission>[:<namespace>]` |

**Valid services:** `zerodb`, `inference`, `memory`, `file`, `agent`, `mcp`

<Tabs>
<TabItem value="curl" label="curl">

```bash
curl -X POST https://api.ainative.studio/api/v1/auth/keys \
  -H "Authorization: Bearer $ZERODB_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "scraper-agent-key",
    "ttl_seconds": 3600,
    "scopes": [
      "zerodb:read:project/my-project",
      "memory:write:session/run-42"
    ]
  }'
```

</TabItem>
<TabItem value="python" label="Python">

```python
import os, requests

resp = requests.post(
    "https://api.ainative.studio/api/v1/auth/keys",
    headers={"Authorization": f"Bearer {os.environ['ZERODB_API_KEY']}"},
    json={
        "name": "scraper-agent-key",
        "ttl_seconds": 3600,
        "scopes": [
            "zerodb:read:project/my-project",
            "memory:write:session/run-42",
        ],
    },
)
resp.raise_for_status()
agent_key = resp.json()["key"]  # store immediately — shown once only
```

</TabItem>
<TabItem value="js" label="JavaScript">

```js
const resp = await fetch("https://api.ainative.studio/api/v1/auth/keys", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.ZERODB_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    name: "scraper-agent-key",
    ttl_seconds: 3600,
    scopes: ["zerodb:read:project/my-project", "memory:write:session/run-42"],
  }),
});
const { key } = await resp.json(); // store immediately — shown once only
```

</TabItem>
</Tabs>

The `key` field is returned **once only**. Store it in a secret manager — it cannot be recovered.

### Revoke a key

```bash
curl -X DELETE https://api.ainative.studio/api/v1/auth/keys/{key_id} \
  -H "Authorization: Bearer $ZERODB_API_KEY"
```

### Key rotation workflow

1. `POST /api/v1/auth/keys` → capture new `key`
2. Update agent config to use new key
3. Verify agent is healthy
4. `DELETE /api/v1/auth/keys/{old_key_id}`

---

## 2. Memory Safety — Namespace Isolation

### Why the default namespace is dangerous

Without isolation, every memory write lands in the `global` namespace — shared across all workflows. A prompt injection in an email could poison future agent behavior.

### Namespace format

| Namespace | When to use |
|-----------|-------------|
| `global` | Trusted, long-lived facts about the user or org |
| `project:<project_id>` | Project-scoped isolation |
| `session:<session_id>` | Ephemeral — disposable after the agent run |

### Disposable memory pattern

<Tabs>
<TabItem value="python" label="Python SDK">

```python
import uuid
from app.services.memory.zeromemory import ZeroMemory

session_ns = f"session:{uuid.uuid4()}"
memory = ZeroMemory(db)

try:
    # Write to isolated session namespace
    await memory.remember(
        content=sanitized_document,
        entity_id="scrape-job-88",
        namespace=session_ns,
    )
    results = await agent.run(namespace=session_ns)

    # Promote verified facts to global
    for fact in results.verified_facts:
        await memory.remember(content=fact, namespace="global")
finally:
    # Always clean up — even if agent crashed
    await memory.forget(namespace=session_ns)
```

</TabItem>
<TabItem value="curl" label="curl">

```bash
## Store in session namespace
curl -X POST https://api.ainative.studio/api/v1/public/memory/v2/remember \
  -H "Authorization: Bearer $ZERODB_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "content": "Scraped product: Widget Pro — $49.99",
    "entity_id": "scrape-job-88",
    "namespace": "session:run-88"
  }'

## Recall within namespace only
curl -X POST https://api.ainative.studio/api/v1/public/memory/v2/recall \
  -H "Authorization: Bearer $ZERODB_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"query": "product prices", "namespace": "session:run-88"}'

## Bulk-clear session namespace when done
curl -X DELETE https://api.ainative.studio/api/v1/public/memory/v2/forget \
  -H "Authorization: Bearer $ZERODB_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"namespace": "session:run-88"}'
```

</TabItem>
</Tabs>

---

## 3. Content Sanitization — The Quarantine API

Quarantine any content arriving from outside your control boundary before passing it to a privileged agent.

**What gets stripped:**
- Zero-width and bidi Unicode (U+200B, U+202A–U+202E, etc.)
- HTML comment injection (`<!-- ... -->`)
- Script tags (`<script>...</script>`)
- Base64 blobs (60+ char heuristic)
- External links (when `strip_links: true`)

### `POST /api/v1/public/security/quarantine`

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `content` | string | yes | Raw text to sanitize (max 500,000 chars) |
| `content_type` | string | yes | `pdf_extract`, `html`, `ocr`, or `markdown` |
| `strip_links` | boolean | no | Strip external links (default `true`) |

<Tabs>
<TabItem value="curl" label="curl">

```bash
curl -X POST https://api.ainative.studio/api/v1/public/security/quarantine \
  -H "Authorization: Bearer $ZERODB_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "content": "<raw email or scraped HTML>",
    "content_type": "html",
    "strip_links": true
  }'
```

</TabItem>
<TabItem value="python" label="Python">

```python
import os, requests

def quarantine_and_ingest(raw: str, content_type: str) -> str:
    resp = requests.post(
        "https://api.ainative.studio/api/v1/public/security/quarantine",
        headers={"Authorization": f"Bearer {os.environ['ZERODB_API_KEY']}"},
        json={"content": raw, "content_type": content_type, "strip_links": True},
    )
    resp.raise_for_status()
    result = resp.json()

    if not result["safe_to_use"]:
        raise ValueError(
            f"Content failed quarantine: {result['threats_detected']}. "
            "Manual review required."
        )

    return result["sanitized"]
```

</TabItem>
<TabItem value="js" label="JavaScript">

```js
async function quarantineAndIngest(raw, contentType) {
  const resp = await fetch(
    "https://api.ainative.studio/api/v1/public/security/quarantine",
    {
      method: "POST",
      headers: {
        Authorization: `Bearer ${process.env.ZERODB_API_KEY}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        content: raw,
        content_type: contentType,
        strip_links: true,
      }),
    }
  );
  const result = await resp.json();
  if (!result.safe_to_use) {
    throw new Error(`Content failed quarantine: ${result.threats_detected}`);
  }
  return result.sanitized;
}
```

</TabItem>
</Tabs>

**Response fields:**

| Field | Description |
|-------|-------------|
| `sanitized` | Cleaned text — pass this to your agent |
| `threats_detected` | Array: `hidden_unicode`, `html_comment_injection`, `script_injection`, `base64_blob`, `external_links` |
| `stripped_count` | Total items removed |
| `safe_to_use` | `false` when `script_injection` or `base64_blob` found — human review recommended |

:::warning
Never pass content with `safe_to_use: false` to a privileged agent context. Flag it for human review.
:::

### Integration flow

```
Untrusted content
       │
       ▼
POST /security/quarantine
       │
       ├─ safe_to_use=false ──► Reject / escalate to human
       │
       └─ safe_to_use=true  ──► Pass sanitized to agent
                                (use session namespace, not global)
```

---

## 4. Agent Oversight — Heartbeat and Approval Gates

### Heartbeat: the dead-man switch

If an agent stops heartbeating within `heartbeat_interval_seconds`, the session transitions to `stalled` and supervisors are alerted.

**1. Create a session**

```bash
curl -X POST https://api.ainative.studio/api/v1/agent/sessions \
  -H "Authorization: Bearer $ZERODB_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "user_id": "u_xyz",
    "session_id": "run-88",
    "heartbeat_interval_seconds": 30
  }'
```

**2. Send heartbeats** (call at least once per `heartbeat_interval_seconds`)

```bash
curl -X POST https://api.ainative.studio/api/v1/agent/sessions/run-88/heartbeat \
  -H "Authorization: Bearer $ZERODB_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"user_id": "u_xyz"}'
```

Check the `status` field in the response — exit cleanly on `terminated` or `quarantined`.

**3. Terminate**

```bash
curl -X POST https://api.ainative.studio/api/v1/agent/sessions/run-88/terminate \
  -H "Authorization: Bearer $ZERODB_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"user_id": "u_xyz", "quarantine": false}'
```

Set `quarantine: true` to preserve state for forensics.

**Python heartbeat loop:**

```python
import threading, time, requests, os

def heartbeat_loop(session_id: str, user_id: str, interval: int = 25):
    while True:
        time.sleep(interval)
        resp = requests.post(
            f"https://api.ainative.studio/api/v1/agent/sessions/{session_id}/heartbeat",
            headers={"Authorization": f"Bearer {os.environ['ZERODB_API_KEY']}"},
            json={"user_id": user_id},
        )
        resp.raise_for_status()
        if resp.json().get("status") in ("terminated", "quarantined"):
            raise SystemExit(f"Session {session_id} killed by supervisor")

t = threading.Thread(target=heartbeat_loop, args=("run-88", "u_xyz"), daemon=True)
t.start()
```

### Approval gates for high-risk actions

Before any destructive or irreversible action, pause and request human approval.

**Request approval: `POST /api/v1/sessions/{session_id}/approval-requests`**

| Field | Type | Description |
|-------|------|-------------|
| `action` | string | Machine-readable ID, e.g. `bash_egress` |
| `description` | string | Human-readable: what will happen |
| `risk_level` | string | `low`, `medium`, `high`, or `critical` |

```bash
curl -X POST https://api.ainative.studio/api/v1/sessions/run-88/approval-requests \
  -H "Authorization: Bearer $ZERODB_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "action": "send_email",
    "description": "Send summary report to user@example.com",
    "risk_level": "high"
  }'
```

Requests start in `pending` status and auto-deny after 5 minutes.

**List pending approvals: `GET /api/v1/approvals`**

```bash
curl "https://api.ainative.studio/api/v1/approvals" \
  -H "Authorization: Bearer $ZERODB_API_KEY"
```

**Submit a decision: `POST /api/v1/approvals/{approval_id}`**

```bash
curl -X POST https://api.ainative.studio/api/v1/approvals/apr_abc123 \
  -H "Authorization: Bearer $ZERODB_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"decision": "approve"}'
```

`decision` must be `"approve"` or `"deny"`.

---

## 5. Observability — Audit Logs

Every MCP tool call is recorded in `mcp_request_logs` with `session_id`, `tool_name`, `risk_score`, `approval_decision`, and `input_hash`. Data is retained for 90 days.

### `GET /api/v1/audit/tool-calls`

| Parameter | Description |
|-----------|-------------|
| `session_id` | Filter by session |
| `from` | ISO-8601 start (inclusive) |
| `to` | ISO-8601 end (inclusive) |
| `min_risk_score` | 0.0–1.0 threshold |
| `tool_name` | Exact tool name |
| `limit` | 1–1000, default 100 |
| `offset` | Pagination |

<Tabs>
<TabItem value="curl" label="curl">

```bash
## High-risk calls in the last hour
curl "https://api.ainative.studio/api/v1/audit/tool-calls\
?from=2026-05-17T13:00:00Z&min_risk_score=0.7" \
  -H "Authorization: Bearer $ZERODB_API_KEY"
```

</TabItem>
<TabItem value="csv" label="CSV Export">

```bash
## Download full audit CSV for compliance
curl "https://api.ainative.studio/api/v1/audit/tool-calls/export\
?from=2026-05-01T00:00:00Z" \
  -H "Authorization: Bearer $ZERODB_API_KEY" \
  -o tool-call-audit.csv
```

</TabItem>
</Tabs>

**Response record fields:**

| Field | Description |
|-------|-------------|
| `timestamp` | ISO-8601 UTC |
| `session_id` | MCP session identifier |
| `tool` | Tool name invoked |
| `input_summary` | Endpoint path used |
| `outcome` | `success`, `client_error`, or `error` |
| `risk_score` | 0.0–1.0 (null if unscored) |
| `approval_decision` | Decision if an approval gate was used |

---

## 6. Defense in Depth Checklist

Before deploying any agent to production:

| Check | Done? |
|-------|-------|
| Use a scoped API key (not root key) — `POST /api/v1/auth/keys` | [ ] |
| Set TTL on all agent keys (`ttl_seconds`) | [ ] |
| Use `session:<id>` namespace for untrusted workflows | [ ] |
| Quarantine all external content before agent ingestion | [ ] |
| Never pass content with `safe_to_use: false` to a privileged agent | [ ] |
| Enable heartbeat for any long-running agent (>5 min) | [ ] |
| Set approval gates for destructive or external actions | [ ] |
| Monitor audit log for `risk_score` anomalies | [ ] |
| Rotate agent keys on a schedule (weekly CI, daily high-risk) | [ ] |
| Clean up session namespaces after agent runs | [ ] |

---

## Related

- [MCP Servers Overview](../mcp/overview) — setting up ZeroDB MCP servers
- [Memory MCP Server](../mcp/memory-server) — memory tools reference
- [ZeroDB Full Server](../mcp/full-server) — complete data layer
- [API Reference](../api/overview) — full REST API docs
