ADR-0015: Agent Configuration Lives in Graph Nodes, Not External Files
- Status: Accepted
- Date: 2026-07-06
Context
M1 (runexfab agent discover) shipped a built-in catalog mapping
catalog keys (claude, opencode, codex, …) to canonical PATH
binaries, and shutil.which(catalog_binary) did the discover scan.
That works for the standard install — but a user with shell-aliased
agents (alias claude='… claude-minimax' in .zshrc) was invisible to
discover: shell aliases are not on PATH, and shutil.which() does not
resolve shell aliases.
The user request that triggered this ADR was: “let me add
~/.runex/agent_aliases.yaml so I can declare claude → [claude-minimax, claude-deepseek] and discover can find them.”
This is the first time anyone in the codebase has proposed putting config outside the graph. Every previous config (sources, agents, sessions, capabilities, approval policy) lives as graph nodes. The question is: does this new requirement justify breaking that pattern?
Why this matters more than it looks
If we add ONE external config file, the door opens:
~/.runex/agent_aliases.yaml— today~/.runex/source_overrides.yaml— next month (some user wants Obsidian notes to sync from a different folder than the vault root)~/.runex/personal_profiles.yaml— soon after (a few users want to override the catalog’s approval policy defaults locally)~/.runex/routing_rules.yaml— when M6 capability routing is no longer enough and someone wants “always route X to Y”
Each individual file is justifiable on its own. Together, they
re-introduce the exact problem the graph was designed to avoid:
two parallel sources of truth that drift, that don’t get backed up
together, that don’t fire reactive rules, that don’t show up in
Obsidian sync, that don’t get audited via tx_log.
Properties the graph gives us, that YAML doesn’t
| Property | Graph node | External YAML |
|---|---|---|
| Single source of truth for config | ✓ (graph is THE store) | ✗ (one more place) |
| Reactive cascade on config change | ✓ (field-set trigger fires) | ✗ (file change is invisible to bus) |
| Audit trail | ✓ (tx_log records every set_field) | ✗ (file changes don’t log) |
| Obsidian mirror sync | ✓ (scm fields → frontmatter on writeback) | ✗ (YAML stays local) |
| Field validation | ✓ (scm schema declares types) | ✗ (manual schema file, can drift) |
| Cross-platform (path hell) | ✓ (no XDG vs HOME, no Windows path) | ✗ (must pick one convention) |
| Backup = vault backup | ✓ | ✗ (YAML must be backed up separately) |
| Discover can read it (M1.5 fallback) | ✓ (query Agents by binary_aliases field) | ✗ (must implement YAML parser in engine) |
| Editor UX for users | clunky (must use runex agent add) | great (any text editor) |
| Startup barrier | requires engine running | 0 — reads directly |
The editor UX is the only row where YAML wins. And that one row has
counter-arguments: runex agent add --binary-aliases ... already exists
after M1.5; the user never needs to hand-edit YAML.
Decision
Agent configuration — including the new binary_aliases field — lives
on the Agent supertag, not in ~/.runex/agent_aliases.yaml. No
external config files for this domain. The “configure your agents” CLI
is the only sanctioned interface.
This is consistent with ADR-0007 (Python-as-extension: don’t ship first-class config files; route everything through nodes). It re-affirms the architecture’s core stance: the graph is the configuration.
Alternatives
方案 A — External YAML (rejected)
# ~/.runex/agent_aliases.yaml
claude: [claude-minimax, claude-deepseek]
opencode: [opencode-gemini]discover reads this file first, then falls back to PATH scan. Pros:
- Editor UX is great (users can
catand edit) - Zero startup barrier
- Familiar to ops engineers (yaml is conventional)
Cons (the reason rejected):
- Breaks “everything is a node” mental model
- Bypasses reactive cascade (
binary_aliaseschange → nofield-settrigger fires → no engine sees it) - Bypasses
tx_logaudit trail - Bypasses Obsidian sync (yaml stays local)
- First YAML invites more YAMLs (slippery slope, see above)
- Loses engine-level schema validation
方案 B — Hybrid (rejected)
External YAML for “static personal preferences” (e.g. user’s personal catalog extensions), graph nodes for “system-managed state” (e.g. agents, runs). Discover reads YAML first, then graph.
Cons:
- Most complex of the three
- Same drift problem as A (yaml changes don’t get reactive cascade to invalidate the graph cache)
- The “what counts as static” boundary moves every time someone adds a feature
- Rejected on YAGNI grounds — we don’t yet have evidence the graph path can’t handle every requirement
方案 C — Chosen: Agent supertag binary_aliases field (ADR-0015)
Added in agent.scm:
(field "binary_aliases" (type "text")
(description "catalog 标准 binary 之外的别名 binary(逗号分隔,…
); discover 找不到 catalog 标准名时回退到这里"))agent_catalog.detect_available(store) walks registered Agents; when
the canonical binary is missing on PATH, it probes each alias in
binary_aliases and surfaces it with via_alias=<name>. Users add
agents via runex agent add --binary-aliases claude-minimax,claude-deepseek
or through runex agent discover --register-all.
Pros:
- Zero new infrastructure
- All 8 properties in the table above preserved
- Reactive cascade fires on alias change (
field-set→ noop today, but ready for future rules that want to react) tx_logrecords every alias change with full audit- Obsidian mirror picks it up automatically (no schema change needed
—
binary_aliasesis just another text field)
Cons:
- Users can’t hand-edit config in their favorite text editor (must
use
runex agent add/set_field) - Discover’s
via_aliascolumn is unfamiliar UX (first time we surface “I found this through a workaround”)
The cons are real but small. The editor UX gap is bounded (one
subcommand with a clear help string); the unfamiliar UX is documented
in runex agent discover --help and in docs/CLI-CONTRACT.md.
Consequences
What becomes easier
- Backup:
runex-vault/is now the only thing to back up. One rsync / git / restic target covers everything. - Sync: A user with two machines (work + home) can
git pulltheir vault and agents, sources, sessions, capabilities all follow. With external YAML, each machine would drift independently. - Onboarding:
runex initis the only first-run command.runexfab doctor --synccan diff personal config against published defaults. - Audit: “who set binary_aliases=claude-minimax on this Agent and
when” is one SQL query against
tx_log.
What becomes harder
- Hand-editing agent config in a text editor is no longer possible.
Users must run
runex agent addor use an Obsidian frontmatter edit (which mirrors back to the graph via the Obsidian connector). - Adding a new alias requires re-running
register(or via the discover —register-all flow). One CLI invocation, no more complicated than that. - The discover UX must explain
via_aliasto users who don’t know their install has aliased binaries. The column header does the work.
What this decision does NOT cover
- Other config-file temptations (source overrides, routing rules, personal profiles). Each future request re-runs this decision. The bar is now: “is the graph fundamentally incapable of representing this?” If no, graph. If yes, escalate.
- Multi-user / multi-machine config sharing via git. This was an implicit assumption in some YAML-favouring arguments; the graph approach supports it BETTER (whole vault in git), so this ADR actually strengthens the position.
When we would revisit
Documented trigger conditions — if any of these becomes true, this ADR is reopened:
- Users start asking for config they can edit with their editor, not
because it’s faster but because they want version-controlled diff
visibility across many config items — if
git diff runex-vault/isn’t enough, this ADR might be wrong. - A new category of config is genuinely orthogonal to the ontology (e.g. external HTTP timeouts, retry policies, not related to any node) — that’s a different domain, the graph isn’t the right home.
- The graph starts measurably slowing down reads of config (it doesn’t — config queries are O(nodes), and Obsidian mirror already caches). Not a concern in practice.
See also
- ADR-0007 (Python-as-extension: same stance for extension code, not external config)
- ADR-0009 (Relationship authoring: graph is the only authoring surface for relationships)
- ADR-0011 (Obsidian mirror: the sync mechanism that makes “graph-only config” actually viable for Obsidian users)