{"id":"169d8306-5bd8-40e4-a50d-74cad7ee9dd9","slug":"alfredos","kind":"mcp","name":"AlfredOS","summary":"An open-source, self-hostable, multi-user, multi-persona, security-hardened agentic OS.","owner":"alfred-os","repoUrl":"https://github.com/alfred-os/AlfredOS","license":"Apache-2.0","licenseTier":"permissive","trust":"community","stars":6,"match":null,"fewStars":false,"format":"mcp","createdAt":"2026-05-24T15:02:40.000Z","addedAt":"2026-10-21T08:56:25.057Z","logoUrl":"https://avatars.githubusercontent.com/u/287465253?v=4","linkOnly":false,"collection":null,"skillPath":null,"categories":["communication","cloud-infra"],"tags":["agentic-os","agents","agpl","ai","discord","docker","llm","multi-agent"],"runtime":"python","local":true,"cloudCapable":false,"cloudCapability":"unknown","cloudReadyOnPlatform":false,"needsApiKey":false,"reviewed":false,"syncedAt":"2026-10-21T08:56:47.949Z","sample":false,"url":"https://conduits.dev/tools/alfredos","language":"Python","transports":["stdio"],"install":{"pypi":"alfred"},"requirements":{"env":[],"upstreamMayCharge":false},"tools":null,"readme":"# AlfredOS\n\n[![Discord](https://img.shields.io/discord/1508136189830369300?label=chat&logo=discord&logoColor=white&style=flat-square)](https://discord.gg/HeNwaBhJfU)\n[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/alfred-os/AlfredOS)\n\n> An open-source, self-hostable, multi-user, multi-persona, security-hardened agentic OS.\n\n**Status:** Pre-implementation. The design is in [`PRD.md`](./PRD.md); the operating manual for AI agents working in this repo is in [`.rulesync/rules/CLAUDE.md`](./.rulesync/rules/CLAUDE.md).\n\n**Community:** Join the [**AlfredOS Discord**](https://discord.gg/HeNwaBhJfU) to ask questions, share builds, or follow development.\n\n## What is AlfredOS?\n\nAlfredOS is a long-lived agentic runtime that hosts AI **personas** — specialized agents with their own purposes — and lets them:\n\n- Converse with users across pluggable platforms (Discord + Telegram + TUI for MVP).\n- Share multi-layered memory (working, episodic, semantic, vector, knowledge graph) per user, with auto-save and auto-recall.\n- Coordinate with each other, with explicit safety rails (loop detection, budget caps, audit visualization).\n- Extend themselves with new skills under a reviewer-gated change process — never validating their own work.\n- Run continuously as a bounded autonomous OODA loop, with full audit trail and one-command rollback.\n\nAlfredOS is hardened from day one against prompt injection, credential leakage, and PII exfiltration. Trust tiers, a dual-LLM split, a capability-gated tool layer, outbound DLP, secret brokering, canary tokens, and a cross-provider reviewer agent are all part of the MVP — not later additions.\n\n**Alfred** (no \"OS\") is the name of the default persona — the head butler — who ships enabled out of the box. Specialist personas (Lucius, Oracle, Diana) are bundled as examples; operators enable them as needed.\n\n## Quickstart\n\n> Not yet implemented. Target experience for v0.1:\n\n```sh\ngit clone https://github.com/alfred-os/AlfredOS\ncd AlfredOS\ncp .env.example .env       # then set ALFRED_DEEPSEEK_API_KEY + ALFRED_QUARANTINE_PROVIDER_API_KEY (see below)\nbin/alfred-setup.sh        # macOS/Linux; on Windows, run inside WSL\ndocker compose up -d\nalfred chat                 # start a TUI conversation\n```\n\n> The operator account is created for you by `bin/alfred-setup.sh` (migration `0004` seeds it;\n> display name from `ALFRED_OPERATOR_NAME` in `.env`, default `operator`). There is no manual\n> `alfred user add` step — running one after setup fails with `OperatorAlreadyExists` (exit 2).\n> Set `ALFRED_OPERATOR_NAME` in `.env` before `bin/alfred-setup.sh` for a custom display name.\n\n> **Two provider keys are required before the first `docker compose up -d`.**\n> `bin/alfred-setup.sh` validates both up front and refuses to proceed (exit 1), listing every\n> problem at once, if either is missing (or, for the DeepSeek key, still the `sk-...` placeholder):\n>\n> - **`ALFRED_DEEPSEEK_API_KEY`** — the privileged (primary) LLM credential. It is a required\n>   setting with no default (`src/alfred/config/settings.py`): the core cannot even construct\n>   its settings without it, and it rejects the literal `sk-...` placeholder shipped in\n>   `.env.example`. This key is required regardless of which comms adapters are enabled. Get one\n>   from <https://platform.deepseek.com>.\n> - **`ALFRED_QUARANTINE_PROVIDER_API_KEY`** — the credential for the quarantined half of the\n>   dual-LLM split, which now makes real provider calls. The quarantined provider **must differ**\n>   from the privileged one (`config/routing.yaml`), so with the default DeepSeek-privileged setup\n>   this is an **Anthropic** key — get one from <https://console.anthropic.com>. With it unset the\n>   core exits 2 (`quarantine_provider_key_unset`) and crash-loops under `restart: unless-stopped`.\n>   This is deliberate — a keyless first run does not start. `bin/alfred-setup.sh` reports the\n>   missing key and exits 1; it cannot seed one for you.\n>\n> **Precisely:** the _quarantine_-key refuse-boot is gated on comms being enabled\n> (`settings.comms_enabled_adapters`). With no adapters enabled there is no quarantine path, so\n> that key is not needed and the core boots fine. That is not the quickstart above:\n> `docker-compose.yaml` defaults `ALFRED_COMMS_ENABLED_ADAPTERS` to `[\"alfred_tui\"]`, so the\n> compose stack — the path this README documents — does enable comms and does require the key.\n> (`ALFRED_DEEPSEEK_API_KEY` is required either way.) If you run the core outside compose with\n> `ALFRED_COMMS_ENABLED_ADAPTERS` unset, only the quarantine key is genuinely optional.\n\n`docker compose up -d` now starts **`alfred-core`** as a **long-running daemon**\n(`alfred daemon start`, `restart: unless-stopped`) — earlier releases ran it as a\none-shot command runner. One-off subcommands still work via\n`docker compose run --rm alfred-core <cmd>` (`migrate`, `user add`, `chat`, …) because\n`run` overrides the service `command`. **Run `bin/alfred-setup.sh` _before_\n`docker compose up -d`**: it seeds the `audit.hash_pepper` and provisions secrets the\ndaemon requires to boot. Skip it and the daemon refuse-boots and, under\n`restart: unless-stopped`, crash-loops. The script seeds what it can and warns about\nwhat it cannot — `ALFRED_QUARANTINE_PROVIDER_API_KEY` has to come from you.\n\n`docker compose up -d` also starts **`alfred-gateway`** — the always-up resumable front\ndoor that holds an `alfred chat` session across a core restart. As of this release the\ngateway **links to the core**: the daemon binds `comms-tui.sock` on the shared\n`alfred_run` volume and the gateway dials it (its compose-internal\n`alfred-gateway:9464/metrics` `gateway_core_link_up` gauge reads `1` once both are up;\nsee `ops/prometheus/prometheus.yml`).\n\n`alfred-core` serves its own Prometheus endpoint at **`alfred-core:9465/metrics`** — the\ncore-owned series (quarantine capability revocations, comms dispatch, supervisor action\ndurations, plugin/DLP scan timings). Like the gateway's, it is **compose-internal and never\nhost-published**: there is no host port to curl. The bind port comes from\n`ALFRED_CORE_METRICS_PORT` (default `9465`), which exists so the daemon and\n`alfred daemon healthcheck` resolve one port from one place — treat it as fixed, since the\nbundled scrape config targets `alfred-core:9465` literally (Prometheus cannot expand env vars\nin a scrape target). To check the endpoint from the host: `docker compose exec alfred-core\nalfred daemon healthcheck` (also wired as the service's container healthcheck, so\n`docker compose ps` shows `unhealthy` if the metrics bind failed). Note: the `alfred_run`\nvolume inherits ownership\nfrom the image on **first** creation; if you are upgrading an older deployment that\nalready has an `alfred_run` volume with the wrong owner, run\n`docker compose down && docker volume rm <project>_alfred_run` before `up -d` so it is\nre-created owned by the `alfred` user.\n\n`docker compose up -d` also starts **`alfred-prometheus`** and **`alfred-grafana`** —\ndefault-on, internal-only services. `alfred-prometheus` scrapes `alfred-core:9465` and\n`alfred-gateway:9464`; `alfred-grafana` queries Prometheus through its server-side datasource\nproxy and gives you a dashboard, so the `QuarantineCapabilityRevoked` alert (and every other\ncore/gateway series) is queryable, not just defined. Reach Grafana at\n`http://127.0.0.1:3000` (Linux) or see the per-platform notes in the\n[observability stack runbook](docs/runbooks/observability-stack.md) for OrbStack/Docker\nDesktop. `bin/alfred-setup.sh` seeds a strong `GF_SECURITY_ADMIN_PASSWORD` into `.env` for\nyou — Grafana refuses to start on the default `admin:admin` (exit 78; the runbook covers\nthis if you hit it).\n\n> **AppArmor hosts (Ubuntu 23.10+ and other userns-restricted Linux):** the dual-LLM\n> quarantine child runs under bubblewrap, which builds an unprivileged user namespace.\n> On hosts with `kernel.apparmor_restrict_unprivileged_userns=1` (the modern Ubuntu\n> default) the kernel refuses that namespace unless the container runs under an AppArmor\n> profile carrying `userns,`. `bin/alfred-setup.sh` loads the bundled\n> `docker/apparmor/alfred-bwrap` profile for you. If you run `docker compose` directly\n> (skipping the setup script), load it first or `alfred-core` crash-loops with\n> `bwrap: No permissions to create new namespace`:\n>\n> ```sh\n> sudo apparmor_parser -r docker/apparmor/alfred-bwrap\n> ```\n>\n> Run all `docker compose` commands **from the repository root**: the compose\n> `security_opt: seccomp=docker/seccomp/alfred-bwrap.json` path resolves relative to the\n> compose-invocation directory, not the compose file. macOS and non-AppArmor Linux hosts\n> need none of this (the `security_opt` lines are runtime no-ops there). The bundled PBS\n> interpreter adds roughly +110 MB to the `alfred-core` image.\n\n### macOS host access to Postgres (G7-3 connectivity-free core)\n\n`alfred_internal` is `internal: true`, so on Docker-Desktop/OrbStack (macOS) the\n`alfred-postgres` host-published port `5432` is not forwarded — `psql -h localhost` from a\nMac host will not connect. The compose-internal core reaches Postgres over `alfred_internal`,\nand the dev test loop uses testcontainers, so neither is affected. For a one-off host query,\nexec into the network: `docker compose exec alfred-postgres psql -U alfred -d alfred`. On\nLinux, published ports NAT independently of the internal network, so host access still works.\n\n### Mandatory egress proxy\n\n`ALFRED_EGRESS_PROXY_URL` is mandatory — the core has no direct-egress fallback.\n\n**DeepSeek:** if you override `ALFRED_DEEPSEEK_BASE_URL` from its default, set the same value on\n**both** `alfred-core` and `alfred-gateway`. The gateway derives its destination allowlist from\nthat variable; a mismatch means the core dials a host the gateway denies.\n\n**Anthropic:** the gateway allowlist is hardcoded to `api.anthropic.com`. A custom Anthropic\nendpoint is not supported — it would be denied by the allowlist.\n\n### Enable Discord (Developer Mode walkthrough)\n\nAlfredOS ships a DM-only Discord adapter hosted by the gateway. It is\n**off by default** — a stock `docker compose up -d` hosts no comms\nadapter at all, so the gateway boots healthy with zero configuration\n([ADR-0054](docs/adr/0054-gateway-hosted-adapters-default-empty.md)).\nEnabling Discord is opt-in and requires **both** a bot token and the\nhosted-adapter flag below — the token alone does not turn Discord on.\nOperator workflow for a fresh deploy:\n\n1. **Create a bot in the Discord developer portal.** Visit\n   <https://discord.com/developers/applications>, create a new\n   application, then create a Bot user under it.\n2. **Enable the Message Content gateway intent.** Bot settings →\n   Privileged Gateway Intents → toggle **Message Content** on. Without\n   this, the adapter sees every DM as empty content and never reaches\n   the orchestrator.\n3. **Copy the bot token.** Bot settings → Reset Token → copy.\n4. **Set the token and opt in, in `.env`.** Open your `.env` file (copy\n   from `.env.example` if you have not already) and set **both** lines:\n\n   ```sh\n   ALFRED_DISCORD_BOT_TOKEN=YOUR-TOKEN-HERE\n   ALFRED_GATEWAY_HOSTED_ADAPTERS=[\"alfred_discord\"]\n   ```\n\n   Re-running `bin/alfred-setup.sh` writes the second line for you: it\n   seeds `ALFRED_GATEWAY_HOSTED_ADAPTERS` whenever it finds a token\n   already in `.env`. Skip the second line and the gateway still hosts\n   nothing — `--wait-ready discord` in step 8 below then exits `3` (the\n   adapter isn't in the hosted set, so it can't be resolved), rather than\n   timing out.\n   The token itself is read by `alfred-core` on boot and delivered to\n   the gateway-hosted Discord child over fd-3 at spawn time; the\n   gateway and child never hold the token in their environment.\n5. **Invite the bot to a server with the `bot` scope.** AlfredOS only\n   reads DMs; you do not need any guild-message permissions yet.\n6. **Bind your Discord user to the operator identity.** In Discord:\n   Settings → Advanced → Developer Mode → right-click your user → Copy\n   ID. Then on the host:\n\n   ```sh\n   alfred user bind --slug <your-operator-slug> --platform discord --platform-id <snowflake>\n   ```\n\n   The setup script offers an interactive prompt for this in its final\n   step.\n7. **Start the gateway.**\n\n   ```sh\n   docker compose up -d alfred-gateway\n   ```\n\n8. **Verify the adapter is ready.** Once the gateway is up, run:\n\n   ```sh\n   alfred gateway adapters --wait-ready discord\n   ```\n\n   This polls until the Discord adapter reports ready or the timeout\n   expires. Exit `0` means the adapter reached `on_ready` and is\n   accepting DMs. Then send the bot a DM from your Discord account; the\n   round-trip lands through the orchestrator with audit + budget +\n   episodic memory + DLP all in place.\n\n### Secrets file — permission propagation matrix\n\n`~/.config/alfred/secrets.toml` is plaintext for Slices 2 and 3.\n[ADR-0012](docs/adr/0012-file-backed-secret-broker.md) documents this as\na known risk; secrets management hardening (containerised secret broker)\nships in Slice 4 per\n[ADR-0015](docs/adr/0015-slice4-containerised-quarantined-llm.md).\n\nThe broker reads this host-default file when `ALFRED_SECRETS_FILE` is unset (completing\nADR-0012). If you already keep secrets there — or your `~/.config` is a git repo — read the\n[upgrade note](docs/runbooks/2026-07-03-secrets-file-host-default.md) first.\n\n**In the meantime:**\n\n- **macOS:** Docker Desktop maps the host file's uid/gid to the\n  container uid/gid directly; `chmod 600` on the host applies inside\n  the container too. The setup script runs `export UID GID` because\n  macOS bash 3.2 does not export `UID` by default.\n- **Linux:** `user: \"${UID:-1000}:${GID:-1000}\"` in\n  `docker-compose.yaml` resolves to the operator's real uid/gid; the\n  bind-mount's `chmod 600` is enforced by the kernel exactly as on the\n  host.\n- **WSL2:** same as Linux, with the caveat that running `docker compose`\n  from PowerShell (vs `wsl`) sees a different uid namespace. Run the\n  setup script from inside WSL to keep the perms consistent.\n\n**Backup-vector reminder:** if you back up your `~/.config` with\n`restic`, `borg`, or similar, **exclude `~/.config/alfred/secrets.toml`**\nor the backup will contain plaintext API keys. The `ALFRED_DISCORD_BOT_TOKEN`\nlives in `.env` (not in `secrets.toml`); exclude `.env` from any backup\nthat should not retain plaintext credentials. The containerised secret\nbroker lands in Slice 4; until then, the operator owns both exclusions.\n\n## Configuration\n\nOperator-facing environment variables live in [`.env.example`](./.env.example);\ncopy it to `.env` and edit. The Slice-3 trust-boundary section documents the\nplugin-launcher, capability-gate, and supervisor knobs (sandbox policy\ndirectory, plugin UID, perf-gate force-run, redis maxmemory, state-git path).\n\n### Environment resolution (`ALFRED_ENVIRONMENT`)\n\n`ALFRED_ENVIRONMENT` is a mandatory, closed-vocabulary setting\n(`development` / `production` / `test`) — `alfred daemon start` refuses to\nboot without a recognised value. It resolves via a three-layer precedence,\nhighest wins:\n\n1. The `ALFRED_ENVIRONMENT` process environment variable (exported in your\n   shell, or set by `docker compose` / `systemd`).\n2. `/etc/alfred/environment` — a root-owned system file holding a bare value\n   (no `KEY=` prefix).\n3. Your `.env` file — lowest precedence, gap-fill only: it supplies a value\n   only when neither of the above is set, and it can never unlock the\n   gateway's dev/test-only launch-target-override escape hatch even when its\n   value is otherwise valid.\n\n`.env.example` ships `ALFRED_ENVIRONMENT=production` uncommented — copy it to\n`.env` as-is, or export the env var, or write `/etc/alfred/environment`, to\nsatisfy this.\n\n**The three are not fully equivalent when the process reads `.env` itself.** The\nplugin sandbox launcher fully trusts the env var and the root-owned\n`/etc/alfred/environment`; from a `.env` it reads directly it accepts `production`\nand nothing else, because `.env` is writable by anything with project-directory\naccess and honouring a laxer value there would let a sandbox escape make itself\npermanent.\n\n- **Compose (the default deployment): unaffected.** `docker-compose.yaml`\n  substitutes `ALFRED_ENVIRONMENT` from your host shell or host `.env` into a\n  container _environment variable_ before the container starts, so inside the\n  container it arrives as the fully-trusted env-var source. `development` and\n  `test` work normally.\n- **Bare host, configured only through `.env`:** the shipped `production` default\n  works, but to run `development` or `test` you must export the variable or write\n  `/etc/alfred/environment` — otherwise the daemon starts and every sandboxed\n  plugin spawn refuses with `environment_untrusted_source`.\n\nSee [ADR-0057](docs/adr/0057-directional-trust-for-the-launcher-environment.md)\nfor that rule, and [ADR-0053](docs/adr/0053-three-layer-environment-precedence.md)\nfor the full precedence chain, the fail-closed behaviour on an unreadable\n`/etc`, and why a `.env`-sourced value can never unlock that escape hatch.\n\n**Distinct from `ALFRED_ENV` below.** The two names look alike but gate\nunrelated decisions: `ALFRED_ENVIRONMENT` (this section) is the closed triple\ngating boot and the launch-target override; `ALFRED_ENV` (next section) is a\nfree-form dev/production selector gating which capability-gate implementation\ngets constructed. Setting one does not set the other — `.env.example` comments\nboth to keep them from being confused for one another.\n\n### Gate selection\n\nDistinct from `ALFRED_ENVIRONMENT` above — this section's `ALFRED_ENV` gates\nthe capability-gate implementation, not boot or the launch-target override.\n\nThe capability gate has two implementations: `RealGate` (Postgres-backed)\nand `DevGate` (fail-open stubs, development-only). Selection is effectively\n**opt-out of DevGate**: only `ALFRED_ENV=development` (or unset/empty/whitespace,\nwhich short-circuits the bootstrap to DevGate) selects the stub. Anything else\n— including typos — falls through to `RealGate`:\n\n| `ALFRED_ENV` value | Gate constructed |\n| --- | --- |\n| `development` | `DevGate` (fail-open stubs) |\n| Unset, empty, or whitespace-only | `DevGate` |\n| Anything else (`production`, `staging`, `prdouction` typo, ...) | `RealGate` |\n\nThis means a typo in a production deployment safely falls through to\n`RealGate`. The matching startup log event (`bootstrap.gate_selected`,\nINFO-level) carries the exact env value the bootstrap read so an operator\nwho set `ALFRED_ENV=prod` (instead of `production`) can confirm which gate\nthey ended up on. The plugin runbook\n([`docs/runbooks/slice-3-plugins.md`](./docs/runbooks/slice-3-plugins.md))\nwalks through the full Slice-3 deployment, including the launcher and the\nsupervisor.\n\n## Design\n\nSee [`PRD.md`](./PRD.md) for the full design, including:\n\n- Architecture overview\n- The 7 capability pillars + persona system\n- Security model and prompt-injection defenses\n- Memory model\n- Reviewer-gated self-improvement\n- Token caching and cost control\n- Deployment and self-healing\n- MVP scope vs. roadmap\n\n## Contributing\n\nContributions welcome. Read [`CONTRIBUTING.md`](./CONTRIBUTING.md) and our [`CODE_OF_CONDUCT.md`](./CODE_OF_CONDUCT.md). Contributions are licensed under the project's [Apache-2.0 license](./LICENSE).\n\nFor Python work specifically: [`docs/python-conventions.md`](./docs/python-conventions.md) is the canonical reference (tooling, types, errors, async, testing, security, i18n). AI agents should dispatch the [`alfred-python-developer`](./.rulesync/subagents/alfred-python-developer.md) subagent, which applies it without being asked. The [`docs/adr/`](./docs/adr/) directory holds the Architecture Decision Records that explain _why_ the conventions look the way they do. The most recent — [ADR-0014: pluggable hooks for every action](./docs/adr/0014-pluggable-hooks-for-every-action.md) — records the Slice 2.5 hooks subsystem.\n\nIf you (or an AI agent) are contributing to this repository, also read [`.rulesync/rules/CLAUDE.md`](./.rulesync/rules/CLAUDE.md) for repo conventions, security rules, and the self-improvement process.\n\n## Security\n\nIf you have found a security vulnerability, **do not open a public issue**. Use [GitHub Security Advisories](https://github.com/alfred-os/AlfredOS/security/advisories/new) to report privately. See [`SECURITY.md`](./SECURITY.md) for details.\n\n## License\n\nAlfredOS is licensed under the [Apache License, Version 2.0](./LICENSE). See the [LICENSE](./LICENSE) and [NOTICE](./NOTICE) files for the full terms.\n\nPlugins communicate with the core via the MCP subprocess boundary (stdio / HTTP) and are not considered derivative works; plugin authors may license their work however they choose.\n"}