Architecture¶
s-ai is a nix flake module that closes over its own inputs (agent versions,
skill sources) and exposes a single homeModules.s-ai. Consumers import it,
set providers/plugins/etc., and s-ai renders the canonical layer into each
agent's native format.
Repo layout¶
flake.nix # flake-parts: homeModules.s-ai, packages (omp/pi/opencode), test-home, checks
modules/default.nix # canonical options: agents, plugins, mcp, providers, theme, prompts
modules/agents/*.nix # per-agent translation (omp/pi/opencode)
modules/lib/skills.nix # plugins -> per-agent home.file skill mappings
modules/lib/prompts.nix # numbered markdown -> combined system prompt
modules/theme.nix # base16 palette -> omp theme (66 slots)
plugins/core/skills/ # the shared skill set
prompts/ # numbered system-prompt files (01-git, ...)
testhost/ # scratch consumer for testing changes
How the canonical layer becomes per-harness files¶
A plugin, MCP server, provider, palette, or prompt is declared once in the
canonical options. Each agent module (modules/agents/<name>.nix) reads the
canonical set and emits the harness's native config:
| Canonical | opencode | omp | pi |
|---|---|---|---|
| skills | ~/.config/opencode/skills/ |
~/.omp/agent/skills/ |
~/.pi/agent/skills/ |
| MCP | programs.opencode MCP integration |
~/.omp/agent/mcp.json |
~/.pi/agent/mcp.json |
| providers | provider attrset |
models.yml |
models.json |
| prompt | AGENTS.md |
AGENTS.md |
AGENTS.md |
| theme | — (opencode themes its own TUI) | derived s-ai-dark/light |
theme = "dark" |
Skill routing¶
Each plugin owns a skills.dir. skills.agents decides which agents get
the skills symlinked. A plugin loaded through an agent's native mechanism
(ponytail via opencode's .mjs / pi's package system) excludes that agent
so skills are not double-registered. skills.copyAllFiles marks authored
skills: every file ships (checklists, templates) and SKILL.md front matter
is validated at eval time; vendored inputs ship SKILL.md only and get the
duplicate-name check, so upstream breakage cannot wedge eval.
MCP schema translation¶
Each harness's MCP config schema differs:
- opencode accepts a
stdioserver withcommand/args/env, or a remote server withtype/url/headers. - omp is strict (
additionalProperties: false): stdio takescommand/args/env, http takestype/url/headers. Pi-only keys (lifecycle, directTools, autoApprove) are stripped and{env:VAR}values become env-var names. - pi validates: url servers take
type="http"/"streamable-http"or notypeat all; command servers take"stdio"or none. The canonicaltype = "remote"is translated away to{ url, headers? }.
Host-level programs.mcp.servers additions merge into the canonical set and
land in every agent, so a workstation-level bitbucket server reaches all
three.
Declarative / imperative posture¶
- Read-only data (skills, mcp.json, models, themes, AGENTS.md) renders as store symlinks - agents read them, they cannot rot.
- Agent-editable settings (omp's
config.yml, OMO'somo.jsonc) render as writable copies viahome.activation. The agents rewrite them at runtime; the next switch re-asserts the declared values.
Secrets boundary¶
Providers accept only env-var names (apiKeyEnv), never values. The
store contains no secrets; the consumer exports values via sops or
session env.
Sandboxing / isolation¶
- Each binary lives in its own store path; versions never clash with system packages.
- Renders are read-only symlinks; agent runtime state stays in its own home
dir (
~/.omp,~/.pi,~/.config/opencode). - Agents are NOT OS-sandboxed by design - a coding agent needs repo write,
git, network, and a TTY. Guardrails are the agents' own permission prompts
(tuned via
extraSettings) and per-invocation worktrees/VMs.
Systems¶
The flake lists x86_64-linux, aarch64-linux, aarch64-darwin manually
rather than nix-systems/default: that includes x86_64-darwin, which
llm-agents publishes no packages for. This is the actual support
intersection of the agent inputs.