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.
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.
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_QUARANTINE_PROVIDER_API_KEY (see below)
bin/alfred-setup.sh # macOS/Linux; on Windows, run inside WSL
docker compose up -d
alfred user add --authorization operator --name "Your Name" # one-time
alfred chat # start a TUI conversationA provider key is required before the first
docker compose up -d.ALFRED_QUARANTINE_PROVIDER_API_KEYin.envis the credential for the quarantined half of the dual-LLM split, which now makes real provider calls. With it unset the core exits 2 (quarantine_provider_key_unset) and crash-loops underrestart: unless-stopped. This is deliberate — a real client on a placeholder key would be a silently dead LLM — but it means a keyless first run does not start.bin/alfred-setup.shwarns when the key is missing; it cannot seed one for you.Precisely: the refuse-boot is gated on comms being enabled (
settings.comms_enabled_adapters). With no adapters enabled there is no quarantine path, so no key is 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. If you run the core outside compose withALFRED_COMMS_ENABLED_ADAPTERSunset, the 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.
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.
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.
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 discord
This 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.
~/.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.
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).
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. See 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.
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.
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
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.
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.
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.