Skip to Content
文档📐 ADR 决策记录ADR-0015: Agent Configuration Lives in Graph Nodes, Not External Files

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

PropertyGraph nodeExternal 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 usersclunky (must use runex agent add)great (any text editor)
Startup barrierrequires engine running0 — 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 cat and 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_aliases change → no field-set trigger fires → no engine sees it)
  • Bypasses tx_log audit 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_log records every alias change with full audit
  • Obsidian mirror picks it up automatically (no schema change needed — binary_aliases is 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_alias column 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 pull their vault and agents, sources, sessions, capabilities all follow. With external YAML, each machine would drift independently.
  • Onboarding: runex init is the only first-run command. runexfab doctor --sync can 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 add or 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_alias to 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)
Last updated on