| title | Security Model |
|---|---|
| description | Unified security architecture — guardrails, audit trail, credential vault, and messaging security |
| icon | shield |
⚠️ Partially superseded by Agent UI v2 (agents run as out-of-process sidecars; see the Agent UI server spec). This doc's localhost-trust framing (an unauthenticated loopback port is safe because only local processes reach it) no longer holds under v2: agents run as out-of-process sidecars that must authenticate on every leg — see §0.11 (per-spawn secret + per-agent-scoped callback token) and §0.24 (third-party trust root + egress containment). Read v2 §0 before relying on the localhost-only assumptions below.
Date: 2026-04-01
Status: Planning
Milestones: v0.17.2, v0.18.2, v0.21.0, v0.23.0
Related issues: #94, #438, #447, #459, #461, #559
Prerequisites: None (security is cross-cutting)
This document unifies all security concerns scattered across the GAIA roadmap into a single plan. GAIA enforces a defense-in-depth model: localhost-only communication, sandboxed tool execution, confirmation gates for destructive operations, and a complete audit trail.
The plan is organized into ten security domains, each mapped to a specific milestone and GitHub issue. Implementation is phased: foundational guardrails ship in v0.18.2, browser and desktop controls in v0.21.0, and autonomous execution safety in v0.23.0.
| Surface | Threat | Current State | Target State |
|---|---|---|---|
| Network ports | Remote exploitation | MCP bridge on :8765, API on :8080 (localhost only) |
Remain localhost-only; firewall rules enforced |
| MCP tool execution | Malicious or unverified tools | No classification; all tools execute freely | Tiered classification: auto-approve, confirm, deny |
| Shell commands | Arbitrary code execution | Whitelist in ShellToolsMixin (ALLOWED_COMMANDS) |
Retain whitelist; extend to all agent types |
| RAG cache | Pickle deserialization (RCE) | pickle.load() on cached data guarded by a GAIA_CACHE_V1 magic header and 500 MB size cap (src/gaia/rag/sdk.py) — shipped in v0.17.2 |
Migrate to fully safe serialization / HMAC (#447 follow-up) |
| Credentials | Secret leakage via logs/env | Env vars (ATLASSIAN_SITE_URL, GITHUB_TOKEN, etc.) |
Encrypted credential vault |
| Browser automation | Navigation to malicious URLs | No URL restrictions on Playwright MCP | URL allowlist + domain restrictions (#459) |
| Desktop control | Unauthorized system modification | Not yet implemented | Opt-in model with screenshot permissions (#461) |
| Messaging adapters | Prompt injection from untrusted input | Not yet implemented | Restricted tool set, input sanitization (#635) |
| Skill marketplace | Malicious third-party skills | No marketplace yet | Code signing + declared permissions |
+-----------------------------------------------------------+
| TRUSTED: Local User Session |
| - Desktop UI (Electron IPC) |
| - CLI (gaia chat, gaia-code) |
| - Local filesystem |
| |
| +-----------------------------------------------------+ |
| | SEMI-TRUSTED: MCP Servers (localhost) | |
| | - Playwright, Brave Search, Fetch | |
| | - User-configured servers from ~/.gaia/mcp.json | |
| | - npm packages (verified via checksum) | |
| +-----------------------------------------------------+ |
| |
| +-----------------------------------------------------+ |
| | UNTRUSTED: External Inputs | |
| | - Messaging adapters (Discord, Slack, Telegram) | |
| | - Web content fetched via Fetch/Playwright | |
| | - Third-party skills from marketplace | |
| | - User-uploaded documents (PDF, DOCX) | |
| +-----------------------------------------------------+ |
+-----------------------------------------------------------+
All GAIA services bind exclusively to 127.0.0.1. No public ports are opened.
| Service | Default Bind | Port | Configurable |
|---|---|---|---|
| Lemonade Server | 127.0.0.1 |
8000 | Yes (host/port) |
| MCP Bridge | 127.0.0.1 |
8765 | Yes (env: GAIA_MCP_HOST, GAIA_MCP_PORT) |
| API Server | 127.0.0.1 |
8080 | Yes |
| Electron UI | IPC (no network) | N/A | No |
Enforcement:
- The MCP bridge (
src/gaia/mcp/mcp_bridge.py) and API server (src/gaia/api/) must reject non-loopback bind addresses unless--dangerous-allow-remoteis explicitly passed (v0.23.0). - The Windows installer and setup wizard should configure Windows Firewall to block inbound connections to GAIA ports.
Layer 1: Network isolation (localhost-only, no public ports)
|
Layer 2: Tool classification (auto-approve / confirm / deny)
|
Layer 3: Input validation (command whitelist, URL allowlist, sanitization)
|
Layer 4: Execution sandboxing (subprocess isolation, timeout, resource limits)
|
Layer 5: Audit trail (all tool executions logged to SQLite)
|
Layer 6: Credential isolation (encrypted vault, never logged)
GitHub issues: #438 (v0.18.2), #559 (v0.23.0)
Every tool registered in _TOOL_REGISTRY (see src/gaia/agents/base/tools.py) is
assigned a risk tier:
| Tier | Behavior | Examples |
|---|---|---|
| READ | Auto-approve, no confirmation | search_web, read_file, run_shell_command (read-only whitelist), git status |
| WRITE | Confirm-first popup in UI, y/n in CLI | write_file, run_cli_command (unrestricted), browser_click |
| DESTRUCTIVE | Confirm-first with warning banner | delete_file, git reset, shell rm |
| DENIED | Always blocked, cannot be overridden | os.system, eval, exec, subprocess.Popen (direct) |
MCP tools from external servers are classified by name. The whitelists below apply to the three pre-configured MCP servers (Playwright, Brave Search, Fetch).
# Proposed location: src/gaia/agents/base/security.py
MCP_AUTO_APPROVE = {
# Playwright -- read-only browser operations
"browser_navigate",
"browser_snapshot",
"browser_console_messages",
"browser_network_requests",
"browser_tabs",
"browser_wait_for",
"browser_evaluate",
"browser_navigate_back",
"browser_close",
# Brave Search -- all operations are read-only
"brave_web_search",
"brave_local_search",
# Fetch -- read-only HTTP
"fetch",
}
MCP_ALWAYS_CONFIRM = {
# Playwright -- write operations (mutate page state)
"browser_click",
"browser_fill_form",
"browser_file_upload",
"browser_type",
"browser_select_option",
"browser_press_key",
"browser_drag",
"browser_hover",
}
# Any MCP tool not in either set --> CONFIRM by default.
# Unknown tools from newly connected MCP servers always require confirmation
# until the user explicitly promotes them to auto-approve via config.Desktop UI (Electron):
- Agent requests tool execution via SSE event.
- UI displays a modal: tool name, arguments (truncated), risk tier.
- User clicks "Allow" or "Deny".
- Result sent back via MCP bridge POST.
- If denied, agent receives
{"status": "denied", "reason": "user_rejected"}and must replan without that tool.
CLI:
- Agent prints tool name and arguments to console.
- Prompt:
Execute [tool_name]? (y/n/always): - "always" adds the tool to session-level auto-approve (not persisted).
# Extension to src/gaia/agents/base/tools.py
@tool(risk_tier="write")
def write_file(path: str, content: str) -> dict:
"""Write content to a file."""
...
@tool(risk_tier="read")
def read_file(path: str) -> dict:
"""Read a file and return its contents."""
...The risk_tier parameter is stored in _TOOL_REGISTRY[tool_name]["risk_tier"].
The _execute_tool method in src/gaia/agents/base/agent.py checks the tier
before execution and gates on confirmation if required.
Status: Shipped. GitHub issues: #3373, #3374.
The flagship sidecar's --bypass-permissions (and the TUI's /bypass) is the
one sanctioned place session-wide trust lives — see
src/gaia/agents/base/tool_grants.py. It has always turned the confirmation
prompt off. It now also lifts the shell tool's own guardrails, because
turning off the prompt alone left the agent unable to do the work the user had
just consented to: there was no interpreter, test runner or package manager in
the allowlist, and nothing could write its output anywhere.
What comes off, together, and only under bypass:
| Gate | Default | Under bypass |
|---|---|---|
Shell operator block (>, >>, <, &, `, $(), newline) |
refused before parsing | parsed and executed |
| Confirmation prompts | asked every call | pre-approved (unchanged behaviour) |
| Per-binary read-only policy | ALLOWED_COMMANDS + the git / PowerShell / find / sort / uniq sub-guards |
replaced by DEVELOPER_COMMANDS |
| Shell rate limit (10/min, 3/10s) | paced — a command over the cap waits its turn | lifted |
Pipes and chaining (\|, &&, \|\|, ;) were never blocked, or have since
stopped being: every step of a chain runs by default, judged on its own binary.
Bypass changes nothing there.
Redirection has one exception. A command that is only a skill-granted CLI —
gh issue list > out.txt with shell:execute:gh loaded — is handed its
arguments directly instead of to a shell, so the > would arrive as one more
word for gh to read and the file would never be written. That case is refused
with an explanation rather than run. Chain it with anything else and the shell
performs the redirect as usual.
Blast radius: arbitrary code execution. The developer set adds node,
npm, make, cmake, go, cargo, sed, awk, curl, sleep, timeout,
export, cp and mv. It also names python, python3, pytest, gh and
git, which are consolidation rather than new reach — those already have
paths today (execute_python_file for the generic_file_ops profiles, a
shell:execute:<binary> skill grant for pytest, gh and git). Listing them
here means bypass has one answer to "may this binary run" instead of three. The
per-skill grant path is untouched and still works with bypass off.
Naming git means its policy's outright refusals — push, reset, rebase, the
ones a grant cannot approve — come off under bypass with everything else. Same
reasoning as rm below, inverted: that refusal is a tripwire rather than a
boundary, since python and make in this same set reach git by shelling out.
Omitting it would have left bypass stricter than the default tier, where git's
read-only floor still answers git status.
Except installs outside the project. Naming python and npm let
python -m pip install x and npm install -g x change the user's system
Python or global packages unasked — an agent did exactly that to work around a
broken run_python. A command that installs into an environment outside a
virtual environment or the project (gaia.agents.tools.package_installs) is
refused under bypass, with guidance to tell the user the command or use a
project venv, and is a one-time approval (never "always") otherwise.
rm is deliberately excluded. This is not a security boundary — anything in the
set can delete a file — it is a tripwire against an accidental recursive delete.
cp and mv are in, because a build or test run reasonably needs them.
ALLOWED_COMMANDS is unchanged. The developer set is a separate frozenset,
so the read-only tier keeps claiming exactly what it claims and #2768's
hardening of it never has to reason about these entries.
Stdio transport only. The shell-guardrail lift exists on the sidecar's stdio
transport, which is one local parent process on a private pipe. It is not
reachable on the HTTP transport
(hub/agents/gaia/python/gaia_agent/server.py): that is a bound socket, and an
unguarded shell reachable over it is remote code execution rather than a relaxed
permission model. The HTTP handler pins full_access = False, and the
request model forbids unknown fields so a client cannot ask for it.
HTTP does have POST /v1/gaia/sessions/{id}/bypass, but it grants only the
other half of bypass — running gated tools unasked. Both halves ride one
PermissionState, so that class takes lifts_shell_gates, false for every HTTP
session: without it, attach would write the session's bypass onto each turn's
fresh handler and silently undo the pin above. Token auth and
HostOriginMiddleware (caller_auth.py) continue to guard that surface for
everything else.
It still audits. Skipping the confirmation prompt does not skip the
record. Every command executed under bypass is written to
~/.gaia/cache/file_audit.log with its full arguments and its per-segment
breakdown, so cd build && make is two auditable invocations rather than one
opaque string. Arguments are recorded verbatim, so a secret passed on a command
line lands in that file — treat it as sensitive.
Command substitution is the one thing bypass cannot audit precisely: the body of
a $(...) is not a segment and is not individually validated. It runs because
the shell runs it.
Bypass is toggleable mid-session over the control channel, so the shell gates
read it live rather than caching it: /bypass off puts the guardrails back on
the very next command.
GitHub issue: #94 (v0.18.2)
MCP servers run as child processes (subprocess.run in src/gaia/mcp/external_services.py).
The following hardening applies:
| Control | Implementation | Milestone |
|---|---|---|
| Process isolation | Each MCP server runs in its own subprocess | Existing |
| Timeout enforcement | subprocess.run(..., timeout=30) |
Existing |
| Resource limits | CPU and memory cgroup limits (Linux); Job Objects (Windows) | v0.18.2 |
| Filesystem sandbox | MCP servers can only access ~/.gaia/mcp/<server>/ |
v0.18.2 |
| Network restriction | MCP servers inherit localhost-only binding | Existing |
| Stderr capture | All stderr logged for audit | v0.18.2 |
When a user connects a new MCP server via the MCP Settings UI or ~/.gaia/mcp.json:
- GAIA calls
tools/liston the server to discover available tools. - All discovered tools are classified as CONFIRM by default.
- The user can promote individual tools to AUTO-APPROVE via the Settings UI.
- Promoted tools are stored in
~/.gaia/config.jsonundermcp.trusted_tools.
{
"mcp": {
"trusted_tools": {
"github": ["list_repos", "get_issue", "search_code"],
"context7": ["resolve_library_id", "get_library_docs"]
}
}
}MCP servers installed via npx must be verified:
- Package name must be scoped (
@modelcontextprotocol/server-*or@anthropic/*). - Package integrity is checked via npm's
--prefer-offlineand lockfile hashes. - Unscoped packages display a warning: "This MCP server is from an unverified publisher. Proceed?"
- A future AMD Verified badge (v0.24.0) will indicate code-signed packages.
Cross-cutting concern; no single GitHub issue.
Every tool execution produces an audit record:
| Field | Type | Description |
|---|---|---|
timestamp |
ISO 8601 | When the tool was invoked |
session_id |
UUID | Conversation session identifier |
tool_name |
string | Registry name of the tool |
tool_source |
enum | native, mcp:<server>, skill:<name> |
risk_tier |
enum | read, write, destructive, denied |
arguments |
JSON | Tool arguments (secrets redacted) |
result_status |
enum | success, error, denied, timeout |
result_summary |
string | Truncated result (max 500 chars) |
user_confirmed |
bool | Whether user confirmation was required and granted |
duration_ms |
int | Execution wall-clock time |
input_channel |
enum | cli, desktop_ui, api, messaging:<platform> |
Audit logs are stored in SQLite using the existing DatabaseMixin pattern
(src/gaia/database/mixin.py):
# Location: ~/.gaia/audit.db
CREATE TABLE tool_executions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
timestamp TEXT NOT NULL DEFAULT (datetime('now')),
session_id TEXT NOT NULL,
tool_name TEXT NOT NULL,
tool_source TEXT NOT NULL,
risk_tier TEXT NOT NULL,
arguments TEXT, -- JSON, secrets redacted
result_status TEXT NOT NULL,
result_summary TEXT,
user_confirmed INTEGER DEFAULT 0,
duration_ms INTEGER,
input_channel TEXT NOT NULL DEFAULT 'cli'
);
CREATE INDEX idx_tool_executions_session ON tool_executions(session_id);
CREATE INDEX idx_tool_executions_timestamp ON tool_executions(timestamp);
CREATE INDEX idx_tool_executions_tool ON tool_executions(tool_name);The Electron UI displays the audit trail in a dedicated "Activity" panel:
- Filterable by session, tool, risk tier, date range.
- Exportable as CSV or JSON.
- Destructive/denied actions are highlighted.
CLI access:
gaia audit list # Last 50 entries
gaia audit list --tool write_file # Filter by tool
gaia audit list --risk destructive # Filter by risk tier
gaia audit export --format json # Export full audit logBefore writing to the audit log, arguments are scrubbed:
- Any value matching a known credential pattern (API keys, tokens, passwords) is
replaced with
[REDACTED]. - Fields named
password,token,secret,api_key,auth,credentialare always redacted regardless of value. - The redaction function lives in
src/gaia/agents/base/security.py.
Strategy doc requirement; no dedicated GitHub issue yet.
Credentials are stored as environment variables or in plaintext config files:
ATLASSIAN_SITE_URL,ATLASSIAN_EMAIL,ATLASSIAN_API_TOKEN(Jira)GITHUB_TOKEN(GitHub MCP)BRAVE_API_KEY(Brave Search MCP)PERPLEXITY_API_KEY(web search fallback)DISCORD_BOT_TOKEN,SLACK_BOT_TOKEN,TELEGRAM_BOT_TOKEN(messaging)
Problems: env vars are visible in /proc/<pid>/environ (Linux), Get-Process
(Windows), and can leak into logs, crash reports, or child process environments.
Location: ~/.gaia/credentials.db (SQLite with encrypted values).
# Location: src/gaia/security/credential_vault.py
class CredentialVault:
"""Encrypted credential storage for GAIA."""
def __init__(self, path: str = "~/.gaia/credentials.db"):
...
def store(self, service: str, key: str, value: str) -> None:
"""Encrypt and store a credential."""
...
def retrieve(self, service: str, key: str) -> str:
"""Retrieve and decrypt a credential."""
...
def delete(self, service: str, key: str) -> None:
"""Remove a credential."""
...
def list_services(self) -> List[str]:
"""List services with stored credentials (not the values)."""
...Encryption: AES-256-GCM. Key derivation: PBKDF2 from machine-specific entropy (Windows DPAPI on Windows, Keychain on macOS, Secret Service on Linux).
- v0.18.2: Introduce
CredentialVaultclass, support both env vars and vault. - v0.21.0: UI for managing credentials (add/remove/rotate).
- v0.23.0: Deprecate env var credentials with warning; vault is primary.
- v0.24.0: Env var credentials removed from documentation.
- Credentials MUST NEVER appear in log output, audit trail, error messages, or crash reports.
- The
format_execution_tracefunction insrc/gaia/agents/base/errors.pymust scrub tool arguments before formatting. - MCP server configs with
envkeys containingTOKEN,KEY,SECRET,PASSWORD, orCREDENTIALmust mask values in all debug output.
GitHub issue: #459 (v0.21.0)
Playwright MCP browser operations are restricted to an allowlist of domains:
{
"browser_security": {
"mode": "allowlist",
"allowed_domains": [
"*.google.com",
"*.github.com",
"*.stackoverflow.com",
"*.wikipedia.org",
"*.python.org",
"*.npmjs.com",
"*.docs.rs",
"*.developer.mozilla.org"
],
"blocked_domains": [
"*.onion",
"localhost",
"127.0.0.1",
"0.0.0.0",
"*.local"
],
"max_concurrent_tabs": 3,
"navigation_timeout_ms": 30000,
"allow_downloads": false,
"allow_file_protocol": false
}
}The Playwright MCP tool wrapper intercepts browser_navigate calls:
- Parse the target URL.
- Check domain against
allowed_domains(glob matching). - Check domain against
blocked_domains(always takes precedence). - If domain is not in either list, prompt user for confirmation.
file://,javascript:,data:protocols are always blocked.
- Pages that attempt to open new windows or popups are blocked.
- JavaScript
alert(),confirm(),prompt()dialogs are auto-dismissed. - Downloaded files are quarantined to
~/.gaia/downloads/and never auto-executed. - Cookie and localStorage data is isolated per session (no persistence across agent restarts).
GitHub issue: #461 (v0.21.0)
Desktop control (CUA) capabilities are disabled by default. Users must explicitly enable them:
# CLI opt-in
gaia config set desktop_control.enabled true
# Or via Electron UI: Settings > Security > Desktop Control > Enable| Permission | Description | Default | Requires Confirmation |
|---|---|---|---|
screenshot |
Capture screen regions | Off | Yes (first time) |
mouse_move |
Move mouse cursor | Off | Yes (per session) |
mouse_click |
Click at coordinates | Off | Yes (per action) |
keyboard_type |
Type text | Off | Yes (per action) |
keyboard_shortcut |
Press key combinations | Off | Yes (per action) |
window_manage |
Resize, move, close windows | Off | Yes (per action) |
Screenshots capture potentially sensitive information. Controls:
- Screenshots are stored in
~/.gaia/screenshots/with session-scoped filenames. - Screenshots are auto-deleted after session ends (configurable retention).
- Screenshots are never sent to external services (processed locally via VLM).
- The Electron UI displays a persistent indicator when screenshot capture is active.
- Desktop control actions execute with a minimum 500ms delay between actions (prevents runaway automation).
- A global kill switch: pressing
Escapethree times rapidly cancels all pending desktop control actions. - Desktop control is not available over messaging adapters (CLI and desktop UI only).
- CUA sessions are time-bounded (default: 5 minutes, configurable).
GitHub issue: #559 (v0.23.0)
Dangerous mode is an explicit opt-in that bypasses tool confirmation gates for autonomous execution. It is designed for advanced users running unattended workflows (scheduled tasks, CI pipelines, batch processing).
Dangerous mode requires a deliberate multi-step activation:
# CLI activation -- requires typing the full flag
gaia chat --dangerous-mode
# Confirmation prompt (cannot be suppressed)
# WARNING: Dangerous mode bypasses all tool confirmation gates.
# All tool executions will proceed without asking for approval.
# Audit logging remains active.
#
# Type "I understand the risks" to continue:Dangerous mode CANNOT be:
- Enabled via config file (must be per-session, explicit).
- Activated over messaging adapters (Discord, Slack, Telegram).
- Activated via the API server (only CLI and desktop UI).
- Combined with desktop control permissions (CUA always requires confirmation).
| Behavior | Normal Mode | Dangerous Mode |
|---|---|---|
| READ tools | Auto-approve | Auto-approve (no change) |
| WRITE tools | Confirm-first | Auto-approve |
| DESTRUCTIVE tools | Confirm-first + warning | Auto-approve + warning logged |
| DENIED tools | Always blocked | Still blocked |
| Audit logging | Active | Active (cannot be disabled) |
| Desktop control | Per-action confirmation | Still per-action (no bypass) |
| MCP unknown tools | Confirm | Auto-approve (with audit) |
- CLI: Red banner
[DANGEROUS MODE]in prompt. - Desktop UI: Red border on chat window, persistent warning badge.
- Audit log: All entries during dangerous mode are tagged
dangerous_mode: true.
Prerequisites for #647 (skill marketplace).
The SKILL.md format specification, permission model, security tiers, and sandboxing architecture are defined in skill-format.mdx — the canonical reference for all skill/plugin security. Key security properties:
- Skills declare permissions in YAML frontmatter using domain-scoped syntax
(
filesystem:read,network:write, etc.) - Three security tiers: AMD Verified, Community Reviewed, Experimental
- Skills run in isolated context with restricted
_TOOL_REGISTRYaccess - Dedicated working directory per skill:
~/.gaia/skills/<skill_name>/ - Code signing infrastructure planned for v0.24.0 (#462-#465)
Related issues: #635 (messaging adapters), #559 (dangerous mode exclusion)
Messaging platforms (Discord, Slack, Telegram) introduce untrusted external input from potentially hostile users. This is fundamentally different from the desktop UI where the local user is trusted.
Messaging adapters operate with a restricted tool set by default:
MESSAGING_ALLOWED_TOOLS = {
# Read-only tools only
"search_web",
"brave_web_search",
"brave_local_search",
"fetch",
"read_file", # Read-only, within allowed paths
"search_documents", # RAG search
"list_documents", # RAG listing
}
MESSAGING_DENIED_TOOLS = {
# Never available over messaging
"write_file",
"delete_file",
"run_cli_command",
"browser_click",
"browser_fill_form",
"browser_type",
# All desktop control tools
"screenshot",
"mouse_click",
"keyboard_type",
}All messages from external platforms are sanitized before reaching the agent:
- Length limit: Messages exceeding 4,000 characters are truncated.
- Injection filtering: Known prompt injection patterns are detected and blocked (e.g., "ignore previous instructions", "system prompt:", role-switching attempts).
- PII filtering: Outbound responses are scanned for potential PII leakage
(email addresses, phone numbers, SSNs). Detected PII is replaced with
[PII REDACTED]in responses sent back to the messaging platform. - Rate limiting: Per-user, per-channel, and global rate limits (see
messaging-integrations-plan.mdxSection 7 for configuration).
Each messaging platform user maps to an isolated GAIA session:
- Sessions are keyed by
(platform, user_id, channel_id). - No cross-session data leakage.
- Sessions have a configurable TTL (default: 24 hours).
- Session history is stored in
~/.gaia/messaging/sessions.db, separate from the main audit log.
GitHub issue: #447 — shipped in v0.17.2 via PR #722.
The RAG SDK (src/gaia/rag/sdk.py) uses pickle.load() to deserialize cached
document indexes. Pickle deserialization of untrusted data can lead to
arbitrary code execution.
Attack vector: A malicious PDF is indexed, the resulting cache file is tampered with
(or a crafted .pkl file is placed in ~/.gaia/cache/), and the next time the RAG
SDK loads the cache, arbitrary code executes.
The RAG cache now prepends a fixed magic header (GAIA_CACHE_V1\n) and enforces
a MAX_CACHE_SIZE (500 MB) before any deserialization attempt. See
src/gaia/rag/sdk.py:44. This closes the drive-by-write attack on
~/.gaia/cache/ without forcing a JSON migration. Future hardening — full
JSON serialization or HMAC-authenticated pickle — remains tracked on #447.
-
Immediate (v0.17.2): Replace
pickle.dump/pickle.loadwithjsonfor the cache data structure. The cached data (chunks,full_text,metadata) is JSON-serializable. -
If JSON is insufficient (e.g., numpy arrays in embeddings): Use
numpy.save/numpy.loadwithallow_pickle=Falsefor embedding vectors, and JSON for metadata. -
Cache integrity: Add HMAC-SHA256 verification to cache files. The HMAC key is derived from the file content hash, ensuring tampered caches are rejected.
# Before (vulnerable):
with open(cache_path, "rb") as f:
cached_data = pickle.load(f)
# After (safe):
with open(cache_path, "r") as f:
cached_data = json.load(f)
# Verify HMAC before using cached_data- Migration: Existing
.pklcache files are invalidated on upgrade. Users will see a one-time re-indexing of their documents.
| Item | Issue | Priority | Effort |
|---|---|---|---|
| Fix pickle deserialization in RAG cache | #447 | P0 | 1 day |
| Cache integrity verification (HMAC) | #447 | P0 | 0.5 day |
| Item | Issue | Priority | Effort |
|---|---|---|---|
| Tool risk tier classification system | #438 | P0 | 2 days |
@tool(risk_tier=...) decorator extension |
#438 | P0 | 1 day |
| MCP tool classification (auto-approve/confirm/deny) | #94, #438 | P0 | 2 days |
| Confirmation flow (CLI + desktop UI) | #438 | P0 | 3 days |
| Audit trail SQLite schema + logging | -- | P1 | 2 days |
gaia audit CLI commands |
-- | P1 | 1 day |
| Secret redaction in logs and audit trail | -- | P1 | 1 day |
| MCP server resource limits (timeout, filesystem sandbox) | #94 | P1 | 2 days |
| npm package verification for MCP servers | #94 | P2 | 1 day |
| Credential vault (initial, alongside env vars) | -- | P2 | 3 days |
| Item | Issue | Priority | Effort |
|---|---|---|---|
| Browser URL allowlist | #459 | P0 | 2 days |
| Domain restriction enforcement for Playwright | #459 | P0 | 1 day |
| Protocol blocking (file://, javascript:, data:) | #459 | P0 | 0.5 day |
| Desktop control opt-in model | #461 | P0 | 2 days |
| Screenshot permission system | #461 | P1 | 1 day |
| Desktop control safety constraints (delay, kill switch) | #461 | P1 | 1 day |
| Credential vault UI (add/remove/rotate) | -- | P1 | 2 days |
| Content security (popup blocking, download quarantine) | #459 | P2 | 1 day |
| Item | Issue | Priority | Effort |
|---|---|---|---|
| Dangerous mode (CLI + desktop UI) | #559 | P0 | 2 days |
| Dangerous mode exclusions (messaging, API, CUA) | #559 | P0 | 1 day |
| Messaging adapter restricted tool set | #635 | P0 | 1 day |
| Input sanitization for messaging | #635 | P1 | 2 days |
| PII filtering for outbound messaging responses | #635 | P1 | 2 days |
| Prompt injection detection | #635 | P1 | 2 days |
| Deprecate env var credentials (vault primary) | -- | P2 | 1 day |
--dangerous-allow-remote bind flag |
-- | P2 | 0.5 day |
| Item | Issue | Priority | Effort |
|---|---|---|---|
| SKILL.md manifest specification | -- | P0 | 2 days |
| Permission enforcement for skills | -- | P0 | 3 days |
| Code signing infrastructure (AMD Verified) | -- | P1 | 5 days |
| Skill sandbox (filesystem isolation, registry validation) | -- | P1 | 3 days |
| AMD Verified badge in Agent Hub UI | #462-#465 | P2 | 1 day |
| Issue | Title | Milestone | Security Domain |
|---|---|---|---|
| #94 | MCP security vulnerabilities | v0.18.2 | MCP sandboxing, tool classification |
| #438 | Tool execution guardrails | v0.18.2 | Risk tiers, confirmation flow |
| #447 | Pickle deserialization vulnerability in RAG cache | v0.17.2 | Serialization safety |
| #459 | Browser use security policy and URL allowlisting | v0.21.0 | Browser domain restrictions |
| #461 | Desktop control security policy and opt-in model | v0.21.0 | CUA permissions |
| #559 | Dangerous mode -- opt-in guardrail bypass | v0.23.0 | Autonomous execution |
| #612 | Agent registry | v0.18.2 | Tool discovery trust |
| #635 | Messaging adapters | v0.23.0 | Input sanitization, restricted tools |
| #647 | Skill marketplace | v0.24.0 | Code signing, sandboxed skills |
The following security measures are already implemented in the codebase:
| Measure | Location | Description |
|---|---|---|
| Shell command whitelist | src/gaia/agents/tools/shell_tools.py |
ALLOWED_COMMANDS set restricts CLI tool to read-only commands |
| Git command whitelist | src/gaia/agents/tools/shell_tools.py |
Only read-only git subcommands (status, log, diff, etc.) |
| Localhost-only MCP bridge | src/gaia/mcp/mcp.json |
GAIA_MCP_HOST defaults to localhost |
| Subprocess timeout | src/gaia/mcp/external_services.py |
timeout=30 on MCP subprocess calls |
| Tool registry validation | src/gaia/agents/base/agent.py |
Rejects unregistered tool names |
| Required argument checking | src/gaia/agents/base/agent.py |
Validates tool arguments before execution |
These provide a baseline. This plan builds on them systematically.
-
Credential vault key management: Should the vault encryption key be hardware-backed (TPM/fTPM on AMD platforms) or software-derived (DPAPI/Keychain)? TPM provides stronger security but adds platform-specific complexity.
-
Prompt injection detection: What detection model should be used? Options range from regex heuristics to a dedicated classifier model. The classifier approach is more robust but adds latency and model dependency.
-
Skill sandboxing depth: Should skills run in a separate Python subprocess (strong isolation, high overhead) or in-process with restricted globals (weaker isolation, low overhead)?
-
Audit log retention: What is the default retention period? Options: 30 days, 90 days, unlimited. Unlimited is safest for compliance but grows the database.
-
Browser allowlist management: Should the default allowlist be permissive (allow all, block known-bad) or restrictive (block all, allow known-good)? The current proposal is restrictive, which is safer but may frustrate users who browse diverse sites.