mcp server
AlfredOS
An open-source, self-hostable, multi-user, multi-persona, security-hardened agentic OS.
Set up
Runs on your computer. Pick your agent and operating system.
Before you start
- uv (`curl -LsSf https://astral.sh/uv/install.sh | sh`)
{
"mcpServers": {
"alfredos": {
"command": "uvx",
"args": [
"alfred"
]
}
}
}same format Cursor, GitHub Copilot CLI, Junie, JetBrains AI Assistant, Augment (Auggie CLI), Factory Droid, Kiro, Devin CLI +12
Values in angle brackets are placeholders. Restart Claude Desktop after editing its config.Claude Desktop docs
README
synced Oct 21, 2026AlfredOS
An open-source, self-hostable, multi-user, multi-persona, security-hardened agentic OS.
Status: Pre-implementation. The design is in PRD.md; the operating manual for AI agents working in this repo is in .rulesync/rules/CLAUDE.md.
Community: Join the AlfredOS Discord to ask questions, share builds, or follow development.
What is AlfredOS?
AlfredOS is a long-lived agentic runtime that hosts AI personas — specialized agents with their own purposes — and lets them:
- Converse with users across pluggable platforms (Discord + Telegram + TUI for MVP).
- Share multi-layered memory (working, episodic, semantic, vector, knowledge graph) per user, with auto-save and auto-recall.
- Coordinate with each other, with explicit safety rails (loop detection, budget caps, audit visualization).
- Extend themselves with new skills under a reviewer-gated change process — never validating their own work.
- Run continuously as a bounded autonomous OODA loop, with full audit trail and one-command rollback.
AlfredOS 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.
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.
Quickstart
Not yet implemented. Target experience for v0.1:
git clone https://github.com/alfred-os/AlfredOS
cd AlfredOS
cp .env.example .env # then set ALFRED_DEEPSEEK_API_KEY + ALFRED_QUARANTINE_PROVIDER_API_KEY (see below)
bin/alfred-setup.sh # macOS/Linux; on Windows, run inside WSL
docker compose up -d
alfred chat # start a TUI conversation
The operator account is created for you by
bin/alfred-setup.sh(migration0004seeds it; display name fromALFRED_OPERATOR_NAMEin.env, defaultoperator). There is no manualalfred user addstep — running one after setup fails withOperatorAlreadyExists(exit 2). SetALFRED_OPERATOR_NAMEin.envbeforebin/alfred-setup.shfor a custom display name.
Two provider keys are required before the first
docker compose up -d.bin/alfred-setup.shvalidates both up front and refuses to proceed (exit 1), listing every problem at once, if either is missing (or, for the DeepSeek key, still thesk-...placeholder):
ALFRED_DEEPSEEK_API_KEY— the privileged (primary) LLM credential. It is a required setting with no default (src/alfred/config/settings.py): the core cannot even construct its settings without it, and it rejects the literalsk-...placeholder shipped in.env.example. This key is required regardless of which comms adapters are enabled. Get one from https://platform.deepseek.com.ALFRED_QUARANTINE_PROVIDER_API_KEY— the credential for the quarantined half of the dual-LLM split, which now makes real provider calls. The quarantined provider must differ from the privileged one (config/routing.yaml), so with the default DeepSeek-privileged setup this is an Anthropic key — get one from https://console.anthropic.com. With it unset the core exits 2 (quarantine_provider_key_unset) and crash-loops underrestart: unless-stopped. This is deliberate — a keyless first run does not start.bin/alfred-setup.shreports the missing key and exits 1; it cannot seed one for you.Precisely: the quarantine-key refuse-boot is gated on comms being enabled (
settings.comms_enabled_adapters). With no adapters enabled there is no quarantine path, so that key is not needed and the core boots fine. That is not the quickstart above:docker-compose.yamldefaultsALFRED_COMMS_ENABLED_ADAPTERSto["alfred_tui"], so the compose stack — the path this README documents — does enable comms and does require the key. (ALFRED_DEEPSEEK_API_KEYis required either way.) If you run the core outside compose withALFRED_COMMS_ENABLED_ADAPTERSunset, only the quarantine key is genuinely optional.
docker compose up -d now starts alfred-core as a long-running daemon
(alfred daemon start, restart: unless-stopped) — earlier releases ran it as a
one-shot command runner. One-off subcommands still work via
docker compose run --rm alfred-core <cmd> (migrate, user add, chat, …) because
run overrides the service command. Run bin/alfred-setup.sh before
docker compose up -d: it seeds the audit.hash_pepper and provisions secrets the
daemon requires to boot. Skip it and the daemon refuse-boots and, under
restart: unless-stopped, crash-loops. The script seeds what it can and warns about
what it cannot — ALFRED_QUARANTINE_PROVIDER_API_KEY has to come from you.
docker compose up -d also starts alfred-gateway — the always-up resumable front
door that holds an alfred chat session across a core restart. As of this release the
gateway links to the core: the daemon binds comms-tui.sock on the shared
alfred_run volume and the gateway dials it (its compose-internal
alfred-gateway:9464/metrics gateway_core_link_up gauge reads 1 once both are up;
see ops/prometheus/prometheus.yml).
alfred-core serves its own Prometheus endpoint at alfred-core:9465/metrics — the
core-owned series (quarantine capability revocations, comms dispatch, supervisor action
durations, plugin/DLP scan timings). Like the gateway's, it is compose-internal and never
host-published: there is no host port to curl. The bind port comes from
ALFRED_CORE_METRICS_PORT (default 9465), which exists so the daemon and
alfred daemon healthcheck resolve one port from one place — treat it as fixed, since the
bundled scrape config targets alfred-core:9465 literally (Prometheus cannot expand env vars
in a scrape target). To check the endpoint from the host: docker compose exec alfred-core alfred daemon healthcheck (also wired as the service's container healthcheck, so
docker compose ps shows unhealthy if the metrics bind failed). Note: the alfred_run
volume inherits ownership
from the image on first creation; if you are upgrading an older deployment that
already has an alfred_run volume with the wrong owner, run
docker compose down && docker volume rm <project>_alfred_run before up -d so it is
re-created owned by the alfred user.
docker compose up -d also starts alfred-prometheus and alfred-grafana —
default-on, internal-only services. alfred-prometheus scrapes alfred-core:9465 and
alfred-gateway:9464; alfred-grafana queries Prometheus through its server-side datasource
proxy and gives you a dashboard, so the QuarantineCapabilityRevoked alert (and every other
core/gateway series) is queryable, not just defined. Reach Grafana at
http://127.0.0.1:3000 (Linux) or see the per-platform notes in the
observability stack runbook for OrbStack/Docker
Desktop. bin/alfred-setup.sh seeds a strong GF_SECURITY_ADMIN_PASSWORD into .env for
you — Grafana refuses to start on the default admin:admin (exit 78; the runbook covers
this if you hit it).
AppArmor hosts (Ubuntu 23.10+ and other userns-restricted Linux): the dual-LLM quarantine child runs under bubblewrap, which builds an unprivileged user namespace. On hosts with
kernel.apparmor_restrict_unprivileged_userns=1(the modern Ubuntu default) the kernel refuses that namespace unless the container runs under an AppArmor profile carryinguserns,.bin/alfred-setup.shloads the bundleddocker/apparmor/alfred-bwrapprofile for you. If you rundocker composedirectly (skipping the setup script), load it first oralfred-corecrash-loops withbwrap: No permissions to create new namespace:sudo apparmor_parser -r docker/apparmor/alfred-bwrapRun all
docker composecommands from the repository root: the composesecurity_opt: seccomp=docker/seccomp/alfred-bwrap.jsonpath resolves relative to the compose-invocation directory, not the compose file. macOS and non-AppArmor Linux hosts need none of this (thesecurity_optlines are runtime no-ops there). The bundled PBS interpreter adds roughly +110 MB to thealfred-coreimage.
macOS host access to Postgres (G7-3 connectivity-free core)
alfred_internal is internal: true, so on Docker-Desktop/OrbStack (macOS) the
alfred-postgres host-published port 5432 is not forwarded — psql -h localhost from a
Mac host will not connect. The compose-internal core reaches Postgres over alfred_internal,
and the dev test loop uses testcontainers, so neither is affected. For a one-off host query,
exec into the network: docker compose exec alfred-postgres psql -U alfred -d alfred. On
Linux, published ports NAT independently of the internal network, so host access still works.
Mandatory egress proxy
ALFRED_EGRESS_PROXY_URL is mandatory — the core has no direct-egress fallback.
DeepSeek: if you override ALFRED_DEEPSEEK_BASE_URL from its default, set the same value on
both alfred-core and alfred-gateway. The gateway derives its destination allowlist from
that variable; a mismatch means the core dials a host the gateway denies.
Anthropic: the gateway allowlist is hardcoded to api.anthropic.com. A custom Anthropic
endpoint is not supported — it would be denied by the allowlist.
Enable Discord (Developer Mode walkthrough)
AlfredOS ships a DM-only Discord adapter hosted by the gateway. It is
off by default — a stock docker compose up -d hosts no comms
adapter at all, so the gateway boots healthy with zero configuration
(ADR-0054).
Enabling Discord is opt-in and requires both a bot token and the
hosted-adapter flag below — the token alone does not turn Discord on.
Operator workflow for a fresh deploy:
-
Create a bot in the Discord developer portal. Visit https://discord.com/developers/applications, create a new application, then create a Bot user under it.
-
Enable the Message Content gateway intent. Bot settings → Privileged Gateway Intents → toggle Message Content on. Without this, the adapter sees every DM as empty content and never reaches the orchestrator.
-
Copy the bot token. Bot settings → Reset Token → copy.
-
Set the token and opt in, in
.env. Open your.envfile (copy from.env.exampleif you have not already) and set both lines:ALFRED_DISCORD_BOT_TOKEN=YOUR-TOKEN-HERE ALFRED_GATEWAY_HOSTED_ADAPTERS=["alfred_discord"]Re-running
bin/alfred-setup.shwrites the second line for you: it seedsALFRED_GATEWAY_HOSTED_ADAPTERSwhenever it finds a token already in.env. Skip the second line and the gateway still hosts nothing —--wait-ready discordin step 8 below then exits3(the adapter isn't in the hosted set, so it can't be resolved), rather than timing out. The token itself is read byalfred-coreon boot and delivered to the gateway-hosted Discord child over fd-3 at spawn time; the gateway and child never hold the token in their environment. -
Invite the bot to a server with the
botscope. AlfredOS only reads DMs; you do not need any guild-message permissions yet. -
Bind your Discord user to the operator identity. In Discord: Settings → Advanced → Developer Mode → right-click your user → Copy ID. Then on the host:
alfred user bind --slug <your-operator-slug> --platform discord --platform-id <snowflake>The setup script offers an interactive prompt for this in its final step.
-
Start the gateway.
docker compose up -d alfred-gateway -
Verify the adapter is ready. Once the gateway is up, run:
alfred gateway adapters --wait-ready discordThis polls until the Discord adapter reports ready or the timeout expires. Exit
0means the adapter reachedon_readyand is accepting DMs. Then send the bot a DM from your Discord account; the round-trip lands through the orchestrator with audit + budget + episodic memory + DLP all in place.
Secrets file — permission propagation matrix
~/.config/alfred/secrets.toml is plaintext for Slices 2 and 3.
ADR-0012 documents this as
a known risk; secrets management hardening (containerised secret broker)
ships in Slice 4 per
ADR-0015.
The broker reads this host-default file when ALFRED_SECRETS_FILE is unset (completing
ADR-0012). If you already keep secrets there — or your ~/.config is a git repo — read the
upgrade note first.
In the meantime:
- macOS: Docker Desktop maps the host file's uid/gid to the
container uid/gid directly;
chmod 600on the host applies inside the container too. The setup script runsexport UID GIDbecause macOS bash 3.2 does not exportUIDby default. - Linux:
user: "${UID:-1000}:${GID:-1000}"indocker-compose.yamlresolves to the operator's real uid/gid; the bind-mount'schmod 600is enforced by the kernel exactly as on the host. - WSL2: same as Linux, with the caveat that running
docker composefrom PowerShell (vswsl) sees a different uid namespace. Run the setup script from inside WSL to keep the perms consistent.
Backup-vector reminder: if you back up your ~/.config with
restic, borg, or similar, exclude ~/.config/alfred/secrets.toml
or the backup will contain plaintext API keys. The ALFRED_DISCORD_BOT_TOKEN
lives in .env (not in secrets.toml); exclude .env from any backup
that should not retain plaintext credentials. The containerised secret
broker lands in Slice 4; until then, the operator owns both exclusions.
Configuration
Operator-facing environment variables live in .env.example;
copy it to .env and edit. The Slice-3 trust-boundary section documents the
plugin-launcher, capability-gate, and supervisor knobs (sandbox policy
directory, plugin UID, perf-gate force-run, redis maxmemory, state-git path).
Environment resolution (ALFRED_ENVIRONMENT)
ALFRED_ENVIRONMENT is a mandatory, closed-vocabulary setting
(development / production / test) — alfred daemon start refuses to
boot without a recognised value. It resolves via a three-layer precedence,
highest wins:
- The
ALFRED_ENVIRONMENTprocess environment variable (exported in your shell, or set bydocker compose/systemd). /etc/alfred/environment— a root-owned system file holding a bare value (noKEY=prefix).- Your
.envfile — lowest precedence, gap-fill only: it supplies a value only when neither of the above is set, and it can never unlock the gateway's dev/test-only launch-target-override escape hatch even when its value is otherwise valid.
.env.example ships ALFRED_ENVIRONMENT=production uncommented — copy it to
.env as-is, or export the env var, or write /etc/alfred/environment, to
satisfy this.
The three are not fully equivalent when the process reads .env itself. The
plugin sandbox launcher fully trusts the env var and the root-owned
/etc/alfred/environment; from a .env it reads directly it accepts production
and nothing else, because .env is writable by anything with project-directory
access and honouring a laxer value there would let a sandbox escape make itself
permanent.
- Compose (the default deployment): unaffected.
docker-compose.yamlsubstitutesALFRED_ENVIRONMENTfrom your host shell or host.envinto a container environment variable before the container starts, so inside the container it arrives as the fully-trusted env-var source.developmentandtestwork normally. - Bare host, configured only through
.env: the shippedproductiondefault works, but to rundevelopmentortestyou must export the variable or write/etc/alfred/environment— otherwise the daemon starts and every sandboxed plugin spawn refuses withenvironment_untrusted_source.
See ADR-0057
for that rule, and ADR-0053
for the full precedence chain, the fail-closed behaviour on an unreadable
/etc, and why a .env-sourced value can never unlock that escape hatch.
Distinct from ALFRED_ENV below. The two names look alike but gate
unrelated decisions: ALFRED_ENVIRONMENT (this section) is the closed triple
gating boot and the launch-target override; ALFRED_ENV (next section) is a
free-form dev/production selector gating which capability-gate implementation
gets constructed. Setting one does not set the other — .env.example comments
both to keep them from being confused for one another.
Gate selection
Distinct from ALFRED_ENVIRONMENT above — this section's ALFRED_ENV gates
the capability-gate implementation, not boot or the launch-target override.
The capability gate has two implementations: RealGate (Postgres-backed)
and DevGate (fail-open stubs, development-only). Selection is effectively
opt-out of DevGate: only ALFRED_ENV=development (or unset/empty/whitespace,
which short-circuits the bootstrap to DevGate) selects the stub. Anything else
— including typos — falls through to RealGate:
ALFRED_ENV value | Gate constructed |
|---|---|
development | DevGate (fail-open stubs) |
| Unset, empty, or whitespace-only | DevGate |
Anything else (production, staging, prdouction typo, ...) | RealGate |
This means a typo in a production deployment safely falls through to
RealGate. The matching startup log event (bootstrap.gate_selected,
INFO-level) carries the exact env value the bootstrap read so an operator
who set ALFRED_ENV=prod (instead of production) can confirm which gate
they ended up on. The plugin runbook
(docs/runbooks/slice-3-plugins.md)
walks through the full Slice-3 deployment, including the launcher and the
supervisor.
Design
See PRD.md for the full design, including:
- Architecture overview
- The 7 capability pillars + persona system
- Security model and prompt-injection defenses
- Memory model
- Reviewer-gated self-improvement
- Token caching and cost control
- Deployment and self-healing
- MVP scope vs. roadmap
Contributing
Contributions welcome. Read CONTRIBUTING.md and our CODE_OF_CONDUCT.md. Contributions are licensed under the project's Apache-2.0 license.
For Python work specifically: docs/python-conventions.md is the canonical reference (tooling, types, errors, async, testing, security, i18n). AI agents should dispatch the alfred-python-developer subagent, which applies it without being asked. The 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 — records the Slice 2.5 hooks subsystem.
If you (or an AI agent) are contributing to this repository, also read .rulesync/rules/CLAUDE.md for repo conventions, security rules, and the self-improvement process.
Security
If you have found a security vulnerability, do not open a public issue. Use GitHub Security Advisories to report privately. See SECURITY.md for details.
License
AlfredOS is licensed under the Apache License, Version 2.0. See the LICENSE and NOTICE files for the full terms.
Plugins 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.