ALF Architecture
April 21, 2026 · View on GitHub
This is the canonical reference for ALF's internal structure. Every refactor, every new feature, every PR review points back to this document. Golden rule: if a new package fits none of the 5 blocks nor the periphery, it's a red flag — open a discussion before coding.
1. One-sentence description
ALF is:
A Capability (tool / skill / app) is executed by the AI, with graded access to Memory, inside a Sandbox that enforces a Policy. The Runtime orchestrates the four.
Anything that does not directly participate in that sentence is periphery (plumbing, transport, UI, ops).
2. The 5 blocks
internal/
├── capability/ ← what ALF can execute (tools + skills + apps)
├── memory/ ← what ALF knows / remembers (conv + embeddings + preferences)
├── ai/ ← the brain that decides (provider + strategy + ResolveModel)
├── sandbox/ ← the guards that enforce (firewall + vault + filesystem + integrity)
└── runtime/ ← the conductor (orchestrates the four)
2.1 capability/
Role. Everything ALF can execute on the AI's behalf.
Absorbs. tooling/ + skills/ + marketplace/ (as Capability producers; the adapters live in those source packages and register into capability.Registry).
Minimum contract.
type Capability interface {
Manifest() Manifest // name, version, description, kind
Permissions() PermissionSet // what it declares it needs
Execute(ctx context.Context, in Input) (Output, error)
}
type Kind int
const (
KindTool Kind = iota // native short-lived execution (read_file, bash, grep...)
KindSkill // prompt + orchestrated tools (commit-push, doc-writer...)
KindApp // UI iframe + backend (xpost, contacthive...)
)
Hard rules.
- A Capability never calls another Capability directly. It returns a result; the Runtime composes.
- A Capability does not know about Memory nor AI. Inputs arrive via
Input. - Every Capability has a versioned Manifest and an explicit PermissionSet.
- The Marketplace is a Capability registry (index + installation).
2.2 memory/
Role. Unified persistence for everything ALF knows.
Sub-packages.
memory/dedup/— near-duplicate blocking at index time.memory/embed/— concrete ONNX implementation ofmemory.Embedder(tokenizer included). Used bycmd/embed-server.memory/curation/— fact extraction from LLM output (Extractor) + periodic consolidation/dedup (Consolidator). Also hostsConsolidatePreferences, which was moved out of the memory root so the root stays free ofai/providerimports. These services consumememory.Store; they do not redefine the contract.memory/memtest/— contract tests run against everyStoreimplementation.memory/socketsrv/— IPC server for thememory-toolsbinary.
Minimum contract.
type Store interface {
// Conversations
AppendMessage(ctx, convID ConvID, msg Message) error
ListMessages(ctx, convID ConvID, opts ListOpts) ([]Message, error)
Summarize(ctx, convID ConvID) (Summary, error)
// Embeddings
Index(ctx, scope Scope, doc Document) error
Search(ctx, scope Scope, query string, k int) ([]Hit, error)
// Preferences
GetPref(ctx, key string) (Value, error)
SetPref(ctx, key string, val Value) error
}
Hard rules.
- Every function that touches a conversation takes
convIDas an explicit parameter. No "current conv" hidden in a singleton. - One SQLite schema.
CurrentConvID/ActiveConvIDis a user preference, not a memory-state field. - Conv scoping is checked at the function signature, not in the body.
- Embeddings and messages share the same
convIDwhen they are tied to a conv.
2.3 ai/
Role. Everything that turns an intent into tokens.
Internal organisation. internal/ai/ at the root holds the public contract (Engine, Strategy, Request, Event, ResolveModel, Message, ToolCall, ToolSpec). Implementations live in private sub-packages: ai/provider/ holds the concrete drivers (API / CLI / Codex / Registry).
Minimum contract.
type Engine interface {
Run(ctx context.Context, req Request) (<-chan Event, error)
}
type Request struct {
Model ModelID // resolved by ResolveModel, never hard-coded
Messages []Message // supplied by Memory, not re-read by AI
Tools []Capability // supplied by Runtime, not picked by AI
MaxTokens int
Stream bool
}
// Strategy describes how to chain multiple Engine.Run calls for a single
// turn (retry, chain-of-thought, parallel, single-shot, ...). Runtime picks
// the Strategy; ai/ owns the contract, not the orchestration machinery
// (sessions, task store, spawning — those live in runtime/).
type Strategy interface {
Run(ctx context.Context, engine Engine, req Request) (<-chan Event, error)
}
Hard rules.
- One
ResolveModel, everywhere. A CI test (TestHardcodedModelFallback) fails when a hard-coded fallback sneaks into any other package. - AI does not read Memory directly. Runtime prepares the Request.
- AI does not execute Capabilities. It emits
ToolCallevents; Runtime runs them. ai/owns theStrategycontract. Concrete orchestrators (agents, classifier router) live inruntime/.
2.4 sandbox/
Role. Enforce per-Capability access policies.
Sub-packages.
sandbox/exec/— chroot + setpriv + per-app namespaces for executing Capabilities.sandbox/integrity/— hash-based tool/script integrity (TOCTOU-safe via exec-time hash).sandbox/network/— firewall proxy + outbound allow/deny rules.sandbox/secrets/— vault + per-app proxy socket. Note: this is distinct frominternal/envsecrets/(the env-var / Docker-secret reader at boot).
Minimum contract.
type Policy struct {
FileAccess FileRules // read/write paths authorised
Network NetworkRules // domains authorised
Secrets SecretRules // vault keys accessible
Tier Tier // aggregated capabilities
}
type Sandbox interface {
// Returns a ctx that cap.Execute() must run inside.
Apply(ctx context.Context, cap Capability, policy Policy) (context.Context, error)
}
Hard rules.
- One Policy applies to one Capability. No implicit accumulation.
- The Policy is derived from the Capability's Manifest plus the user tier. Never ad hoc.
- Firewall (network), Vault (secrets), Filesystem (exec) and Integrity are four facets of the same Sandbox.
- Integrity scanning runs inside the Sandbox, not alongside it.
- This is the central promise of ALF. Enforcement is staged — integrity is wired today; facet wire-in (network/secrets/filesystem through
PolicyFrom(ctx)) is the v0.8.0 follow-up.
2.5 runtime/
Role. Orchestrate the four other blocks. The only package that knows them all.
Sub-packages.
runtime/agents/— multi-agent orchestrator (absorbed from the oldagents/package).runtime/classifier/— tier classifier (absorbed from the oldrouter/classifier).
Minimum contract.
type Runtime interface {
// Process one user chat turn.
Chat(ctx context.Context, convID memory.ConvID, userInput string) (<-chan Event, error)
// Execute a one-shot Capability (UI button, scheduler, ...).
Invoke(ctx context.Context, capID CapabilityID, args Args) (Output, error)
// Streaming variants (Converse / ConverseStream) for chat pipelines.
}
What Runtime does.
- Resolves the Capability from the registry.
- Loads history from Memory (scoped by convID).
- Derives the Policy from the Manifest + tier.
- Calls
Sandbox.Applyto prepare the ctx. - Runs
AI.Runwith the prepared Request. - Loops on
ToolCallevents: executes each Capability via Sandbox, re-injects results. - Persists the turn into Memory.
Hard rules.
- Runtime is the only package that imports
capability + memory + ai + sandboxtogether. - None of the four blocks imports Runtime.
- Consumers (controlcenter, telegram, scheduler) import only Runtime.
3. Periphery
These packages do not participate in the core sentence. They are consumers or plumbing.
User-facing consumers
cli/— host CLIcontrolcenter/— web UItelegram/— Telegram botvoice/— voice transcription bridgescheduler/— cron-based invoker (consumer ofRuntime.Invoke)
Ops / plateform (platform/)
Grouped under internal/platform/ to make the block-vs-periphery separation visible:
platform/eventlog/— append-only event logplatform/gittrack/— git repo watcher (extraction signal)platform/media/— image/video/PDF processingplatform/mood/— daily personality layerplatform/session/— CLI session bookkeepingplatform/signal/— Unix-socket server for in-sandbox Telegram/reaction callsplatform/supervisor/— background service manager (restart policies)platform/tlsgen/— self-signed cert generator for local HTTPSplatform/trace/— chain/task tracingplatform/updater/— image-update notifierplatform/vulncheck/—govulncheckwrapper (CI guard)
Ancillary (still at internal/ root)
envsecrets/— Docker / Kubernetes env-var secret reader at boot. Distinct fromsandbox/secrets/.marketplace/— app installer + permission gate + lifecycle manager. Registers apps as Capabilities viacapability_adapter.go.skills/— skills FS store + prompt catalog + trigger matching. Registers skills as Capabilities viacapability_adapter.go.tooling/— tool discovery (scantools.d/*.json), native-tool host, integrity integration. Registers tools as Capabilities viacapability_adapter.go.comms/— legacy chat pipeline. Its absorption intoruntime/is tracked in issue #377 (v0.8.0).archtest/— CI-only package that enforces dependency rules.
4. Dependency rules
consumers (controlcenter, telegram, scheduler, cli, ...)
│
▼
runtime
│
├──► capability
├──► memory
├──► ai
└──► sandbox
Hard forbidden imports:
capabilitymust not import memory, ai, sandbox, runtime.memorymust not import capability, ai, sandbox, runtime.aimust not import capability, memory, sandbox, runtime.sandboxmust not import capability (it takes aManifestas parameter), memory, ai, runtime.- Only runtime may import the four.
- No consumer imports one of the four blocks directly (apart from public types re-exposed via runtime).
A CI test enforces these rules (internal/archtest/deps_test.go — TestFoundationDependencyRules, TestConsumerDependencyRules). It runs in enforcing mode.
A second CI test (TestHardcodedModelFallback) fails when any file outside ai/resolve.go introduces a model fallback string.
5. Decision tree: where does my new file belong?
Use this when adding a new file or package:
-
Is it executable by the AI (a tool, skill, or app)? →
capability/contract + register through the adapter intooling/,skills/, ormarketplace/. -
Does it persist or recall data (conversations, embeddings, preferences)? →
memory/(or a sub-package undermemory/). -
Does it turn an intent into tokens (provider driver, strategy, model resolution)? →
ai/(contract) orai/provider/(concrete driver). -
Does it enforce a policy (file access, network, secrets, integrity)? →
sandbox/(or a facet sub-package). -
Does it orchestrate the four (pipelines, agents, classifier)? →
runtime/. -
Is it a user-facing surface (CLI, web UI, Telegram, voice, scheduler)? → root-level consumer package (
cli/,controlcenter/,telegram/,voice/,scheduler/). -
Is it ops plumbing (cron, tracing, supervision, updater, cert gen, event log, …)? →
platform/<name>/. -
None of the above? → 🚨 Red flag. Open a discussion issue before you code.
6. Acceptance criteria
The architecture is honoured when:
- The 5 packages (
capability,memory,ai,sandbox,runtime) exist and respect the dependency rules (CI-verified). - Old fused packages (
chatdb,conversation,memstore,firewall,vault,router,provider,agentsstandalone) no longer exist. - No user-facing behaviour change from the refactor (regression tests green).
- Average PR diff on a bug fix is under 3 files (measured on the next fix cycle).
ResolveModelis unique and protected by a CI test.CurrentConvIDis a preference read from Memory, not a memory-state field.- A new contributor can point to one of the 5 blocks for 80 % of "where do I touch for X?" questions.
7. What this architecture is NOT
- ❌ A rewrite. It is a reorganisation — the business code stays, it changes address.
- ❌ A place to add new features without going through the decision tree.
- ❌ A static document — when reality diverges from this text, we change whichever is wrong (usually the text).
8. Why it exists
The 0.7.8 cycle produced ~30 fixes, five of them alone on chat / multi-tab / conv scoping. The common root: conversational state scattered across four packages (chatdb, conversation, memstore, memory) and convID scoping absent from function signatures.
Without this rework each cycle repeats the same pattern. With it, the class of bugs disappears by construction.