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:
- Private data — agents read memories, user records, and org secrets.
- Untrusted content — agents process emails, web scrapes, uploaded files, and tool responses that may contain adversarial payloads.
- 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
- curl
- Python
- JavaScript
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"
]
}'
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
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
The key field is returned once only. Store it in a secret manager — it cannot be recovered.
Revoke a key
curl -X DELETE https://api.ainative.studio/api/v1/auth/keys/{key_id} \
-H "Authorization: Bearer $ZERODB_API_KEY"
Key rotation workflow
POST /api/v1/auth/keys→ capture newkey- Update agent config to use new key
- Verify agent is healthy
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
- Python SDK
- curl
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)
# 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"}'
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) |
- curl
- Python
- JavaScript
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
}'
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"]
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;
}
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 |
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
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)
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
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:
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 |
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
curl "https://api.ainative.studio/api/v1/approvals" \
-H "Authorization: Bearer $ZERODB_API_KEY"
Submit a decision: POST /api/v1/approvals/{approval_id}
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 |
- curl
- CSV Export
# 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"
# 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
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 — setting up ZeroDB MCP servers
- Memory MCP Server — memory tools reference
- ZeroDB Full Server — complete data layer
- API Reference — full REST API docs