Skip to content

Security: WingedGuardian/GENesis-AGI

SECURITY.md

Security Policy

Reporting a Vulnerability

If you discover a security vulnerability in Genesis, please report it responsibly:

  1. DO NOT open a public GitHub issue.
  2. Use GitHub Security Advisories to create a private report, or contact the project maintainers directly.
  3. Include:
    • Description of the vulnerability
    • Steps to reproduce
    • Potential impact
    • Suggested fix (if any)

We aim to acknowledge security reports within 48 hours and provide a substantive response within one week.

Supported Versions

Version Supported
v3.x Yes
< v3 No

Genesis v3 is a ground-up rebuild. Earlier versions (v1/v2) are not maintained and should not be used.


Security Architecture

Genesis is an autonomous agent system. The same principle that governs its autonomy also governs its security: trust is earned through verified behavior, not assumed.

An autonomous system that can act on your behalf must also be a system you can trust with increasing responsibility over time. The security model is not a lockdown bolted onto an agent -- it is the autonomy model itself. Every layer described below exists because Genesis takes seriously the question: "what has this system demonstrated it can be trusted to do?"

Earned Autonomy as Security Model

Genesis implements a graduated autonomy framework. The shipped ladder is four levels, L1-L4 (genesis.autonomy.types.AutonomyLevel); the design document describes an eventual L5-L7, which is not built. Higher levels are unlocked through demonstrated competence, verified by the system and approved by the operator.

Each autonomy level gates specific capabilities:

  • L1: Simple tool use — fully autonomous.
  • L2: Known-pattern tasks — mostly autonomous.
  • L3: Novel tasks — propose and execute with a checkpoint.
  • L4: Proactive outreach — threshold-gated and governed.

Autonomy permissions are stored per-category and can be revoked instantly. There are four categories (genesis.autonomy.types.AutonomyCategory): direct_session, background_cognitive, sub_agent, outreach. The operator always has final authority. See docs/architecture/genesis-v3-autonomous-behavior-design.md for the full framework, and read it as design intent rather than as a description of what currently ships.

The autonomous-CLI approval gate is the hard boundary underneath all of it. Every autonomous background Claude Code session must be rooted in an explicit operator approval: manual_approval_required (autonomy/cli_policy.py) defaults to True, and AutonomousCliApprovalGate refuses to dispatch without one. No autonomy level unlocks past it.

It is a SETTING, not a constant, and you should treat it as one worth checking. It is read from config/autonomous_cli_policy.yaml and can be overridden by the GENESIS_AUTONOMOUS_CLI_APPROVAL_ENABLED environment variable — so it can be turned off in an install without anyone forking anything. The project treats keeping it True as non-negotiable and ships it that way; what this document cannot tell you is whether it is still True in YOUR install. Verify it, and treat a False as the whole earned-autonomy model being switched off.

Container Isolation

Genesis is designed to run in an isolated environment (container or VM). This is not optional hardening -- it is the assumed deployment model. The container boundary limits blast radius: even if an autonomous action goes wrong, the damage is contained to the Genesis environment.

Recommendations:

  • Run Genesis in a dedicated container or VM, not on a shared workstation.
  • Use a non-root user account.
  • Restrict network egress to required endpoints (LLM APIs, Qdrant, Ollama).

Network Exposure & Management Ports

Genesis exposes a local dashboard over HTTP (and, optionally, a remote-desktop / noVNC console). So it can be reached through a reverse proxy or a private overlay network, the bundled service unit binds the dashboard to all interfaces (0.0.0.0) by default rather than to loopback. When a dashboard password is set, state-changing /api requests are gated: check_api_mutation_auth (dashboard/auth.py) runs as an app-level before_request and requires either the internal bearer token or an authenticated same-origin cookie, with CSRF checked from Sec-Fetch-Site/Origin/Referer and failing closed. /v1 enforces its own separate bearer.

Read the limits of that gate carefully, because they decide whether network isolation is still load-bearing for you — it is:

  • It covers mutations only. GET/HEAD/OPTIONS stay open by design, so health probes and dashboard polling keep working. Anything readable through the API is readable by anyone who can reach the port.

  • It is inert when no dashboard password is set, which is the default.

    A narrow exception to that last point. A few routes decide not whether to answer but how much to include, and those read has_verified_credential() instead — which, unlike is_authenticated(), is never true when no password is configured, because nothing can be proved without a credential to present. Today that covers the provider-key values (/api/genesis/secrets) and the backup target (/api/genesis/backup/status) and the filesystem paths and NAS username (/api/genesis/backup/config). Those are withheld from an unauthenticated caller on every install, including a passwordless one. The routes still answer, but a withheld value changes the response's shape: the secrets route returns each key with an empty value and values_withheld: true, /api/genesis/backup/status omits destinations.tier2.target, and /api/genesis/backup/config omits local_path, nas and nas_user.

    This is not the whole class, and it should not be read as one. The carve-out is a list of FIELDS — not a property of a route, a helper, or the shape of a response. Three shorter rules were tried here and all three were false, so the list is given explicitly:

    • the value of every key in the secrets registry (_KEY_REGISTRY, parsed from secrets.env.example), which is the whole file rather than a sensitive subset;
    • the backup tier-2 target;
    • the backup local path, share and username.

    Two properties of that list are easy to get wrong. It nests rather than partitions: the backup path, share and username are themselves registry keys, so they appear in the first set as well, reached by a second route. And the same underlying value can be treated differently by each route that serves it — the backup config route strips credentials out of the repository URL before returning it, while the secrets registry returns that key's raw value, so a URL with an embedded token is disclosed there and not here. Read the route, not the field name.

    For every field not in that list, assume the default posture above: reads are open, is_authenticated() opens up when no password is configured, and network isolation is the control. Do not infer that a route is covered because its response resembles one that is — a route elsewhere can mask values behind an explicit reveal step and still sit outside this entirely, and a route whose entire product IS the stored value has no middle state to offer, so gating it would remove the function rather than narrow it.

  • It exempts everything under the /api/genesis/auth/ prefix — a prefix match, not a fixed list, so any route added there in future is exempt by default. Today that prefix holds login, logout and an auth-status probe. None of them changes a credential — but login and logout do establish and tear down an authenticated session, and they run ungated by design, because a login that required prior authentication could never succeed.

  • It can be disabled outright with GENESIS_DASHBOARD_API_AUTH=off. That also reopens the file browser's write, create, rename, delete and upload routes to an anonymous caller, inside the browser's allowed roots, which include the Genesis source tree and ~/.claude. The file browser's reads stay behind login whatever the switch says.

  • It covers state-changing HTTP requests under the API prefixes, and nothing else. The interactive terminal runs over a WebSocket and the noVNC console over its own port; neither is behind this gate, so for those the control is still network isolation and the dashboard password on the UI.

This is safe only under the assumed deployment model: the host is not publicly exposed. The dashboard is meant to be reachable through one of:

  • a private overlay network (e.g., Tailscale / WireGuard), where only your own devices can reach the port; and/or
  • a host-side reverse proxy that forwards to the container.

The threat model is public exposure — not your LAN or private overlay. If you run Genesis on a host reachable from the public internet, you must restrict the management ports yourself; Genesis does not assume an authenticating gateway in front of them.

Operator checklist:

  • Do not port-forward the dashboard or console ports from a public router.
  • Bind or firewall the management ports to your private/overlay network — e.g. restrict the dashboard port to your overlay's address range (Tailscale uses the 100.64.0.0/10 CGNAT range) with nftables/ufw, or change the service unit to bind a specific private interface instead of 0.0.0.0.
  • Treat the built-in web terminal and the noVNC console as unauthenticated administrative access: anyone who can reach those ports can drive Genesis. For the dashboard API, assume the same for reads and for any install with no dashboard password set — with the credential-gated field values noted above as the one carve-out, which is a narrow one and does not change the posture. Network isolation remains the primary control; the mutation gate above is a second layer, not a replacement for it.

Security audits should verify this network restriction (firewall / overlay) rather than re-flagging the 0.0.0.0 bind, which is intentional for the proxy/overlay deployment model.

Tool-Level Guards (PreToolUse Hooks)

Genesis uses PreToolUse hooks to enforce tool-level security policies at runtime. These hooks fire on every tool invocation, including autonomous sessions, and cannot be bypassed by the agent.

Examples of enforced policies:

  • Blocking shell commands that match dangerous patterns (e.g. rm -rf /)
  • Blocking destructive git operations, pushes and merges that have not met the repository's review gates, and writes to protected paths
  • Blocking web fetches to known-problematic URLs
  • Blocking editable installs pointed at a worktree, which would redirect system-wide imports

Hooks are configured in .claude/settings.json, and which hook fires is decided by the tool matcher -- they are not one program:

  • Bash is matched by a family of dedicated guards under scripts/hooks/ (destructive_command_guard.py, git_discard_guard.py, protected_paths_guard.py, git_push_guard.py, worktree_cwd_guard.py, and others) plus a small inline matcher.
  • WebFetch/WebSearch is matched by scripts/hooks/web_tools_gate.py.
  • Write/Edit is matched by scripts/behavioral_linter.py, which lints file content against config/behavioral_rules/*.yaml. It never sees a shell command or a URL.

They are the inner guardrail -- the last line of defense when autonomy permissions have already been granted.

Not every safety mechanism is a hook, and the distinction is a real difference in guarantee. Process-group kill validation, for example, lives in genesis.util.proc_kill and hardens Genesis's own subprocess management at runtime; no hook inspects agent-authored code for it. A hook cannot be bypassed by the agent; a runtime helper only protects the call sites that use it.

Secrets Management

Genesis uses an environment-file approach for secrets:

  • All API keys and tokens live in secrets.env at the project root.
  • This file is gitignored and should be set to mode 0600 (owner read/write only).
  • The genesis.env module (src/genesis/env.py) resolves the secrets path at runtime, with support for SECRETS_PATH environment variable override.
  • A detect-secrets scan and a blocking gitleaks scan (.gitleaks.toml rules: API keys plus install-specific IP/hostname patterns) run in CI on every PR to verify no secrets leak into the public repo.
  • Scripts read secrets.env as data (scripts/lib/load_secrets.sh or the Python dotenv reader) — never source it; a sourced value containing $(...) would execute.

Rules:

  • Never commit API keys, tokens, or credentials to version control.
  • Never hardcode secrets in source files.
  • Rotate keys regularly. Use separate keys for development and production.

Data Protection (Qdrant Delete Guard)

Genesis stores episodic memory and knowledge in Qdrant vector collections. A delete guard in the collections module prevents accidental bulk deletion of production data. This was implemented after a real incident where test execution deleted production memory.

The guard:

  • Blocks collection-level delete operations unless explicitly overridden.
  • Ensures test fixtures use isolated collections that do not collide with production data.

Untrusted Content & Write-Path Isolation

Genesis ingests content it did not author -- fetched pages, mail, documents, third-party messages. The risk is not that this content is stored; it is that stored content is later auto-consumed into privileged state, where it becomes instruction rather than data.

Provenance stamping. Every stored memory and observation carries an origin_class -- owner, first_party, or an external/untrusted class.

Two privileged-write paths are gated on it, fail-closed on NULL or an unrecognised value (genesis.security.immunity.is_trusted_for_privileged_write):

  • the user model, which will only fold in deltas from trusted origins (memory/user_model.py), and
  • the autonomy dispatcher, which will not pick up a task_detected observation from an untrusted origin (autonomy/dispatcher.py).

Scope this claim precisely: those two paths are gated, not "all untrusted content is isolated." External-origin content can still reach a model's context through ordinary recall and summarisation surfaces. What the gate prevents is untrusted content auto-promoting itself into the user model or into an autonomous dispatch without an operator in the loop.

Irreversible memory operations require approval. Entity merges delete mentions and links and cannot be un-merged, so they are no longer applied on staleness alone: the applier consumes only approved proposals (memory/entity_adjudication.py), and a pre-delete snapshot is journaled so an applied merge can be reconstructed.

Session identifiers are validated before use as path components. Session ids arrive from outside the process and several hooks interpolate them into filesystem paths. A shared validator (is_safe_session_id in scripts/hooks/hook_input.py) rejects traversal shapes, separators, null bytes, the empty string, and over-long values. It exists because hooks had each hand-rolled their own check and disagreed about what was unsafe; adoption is widespread but not yet complete, so a hook that has not moved over still carries a narrower check of its own.

External Egress

Delivery to the operator -- Telegram, voice, mail addressed to you -- is never gated. You are the recipient; gating it would only obstruct you.

Autonomous egress to the outside world is a different matter, and the honest current state is mixed:

  • Enforcing: mail sending passes a real gate that can hold a send (autonomy/email_gate.py).
  • Observe-only: the capability gate at the Discord and GitHub-issue doors (autonomy/shadow_gate.py) records what it would decide and does not hold anything. Its own contract says so. Treat it as instrumentation ahead of an enforcement stage, not as a control that is protecting you today.
  • Build-time backstop: scripts/check_external_io.py runs in CI and fails the build when a new hardcoded external endpoint appears outside an allowlist. It reasons about literal endpoints in source, so it cannot see egress routed through a browser session or a third-party integration layer.

If you are deciding whether to trust this system with an outbound channel, the first two bullets are the ones that matter: one channel enforces, the rest are watched.

Dependency Security

Python dependencies are declared in pyproject.toml (the sole dependency source — there is no requirements.txt). Known-CVE scanning runs automatically:

  • CI — the dependency-audit job in .github/workflows/ci.yml runs pip-audit against the resolved runtime tree on every PR/push and weekly, reporting advisories outside its retained ignore baseline in the job log. Audit findings are advisory: step-level continue-on-error prevents them from failing the job. Dependency installation failures still fail the job. Existing ignored advisories are listed with rationale in that job.
  • Dependabot — GitHub's dependency-graph security alerts are enabled for the repo, and .github/dependabot.yml keeps the GitHub Actions current.

To scan locally:

pip install pip-audit && pip-audit

Incident Response

If you suspect a security issue:

  1. Revoke any compromised API keys immediately.
  2. Review logs for unauthorized actions or unexpected tool calls.
  3. Check for unexpected file modifications in the Genesis directory.
  4. Rotate all credentials.
  5. Delete ~/.genesis/flask_secret_key and restart genesis-server. This is what invalidates existing dashboard sessions — changing DASHBOARD_PASSWORD alone does not. Sessions are signed with that key rather than with the password, so a stolen cookie survives a password change and keeps whatever the session reaches, including the web terminal.
  6. Report the incident to project maintainers.

License

See LICENSE file for details.

There aren't any published security advisories