Command Ownership and Plugin Structure
The Folder Test
Every feature in Ze passes two folder tests:
Copy test: copy a plugin folder into the project, run codegen (make generate),
and the plugin's commands, YANG schema, and handlers are live. No manual wiring.
Delete test: delete a plugin folder, run codegen, and every one of its features disappears. Every other plugin and the core keep working. No dangling references, no broken builds.
These two tests are the load-bearing invariant of the architecture. Everything below exists to make them hold.
Three Directories, Three Roles
internal/
core/ # shared primitives, no subsystem knowledge
component/ # subsystem implementations + config YANG
plugins/ # user-facing command surfaces
core/ -- Infrastructure Primitives
Leaf packages that provide reusable services. They depend on nothing else
internal (or only on other core/ packages). No subsystem-specific knowledge.
Examples: slogutil (logging), crashlog (panic capture), env (environment
variables), family (address families), health (health registry), metrics
(prometheus), events (event bus), diagnostic (doctor codes), textbuf
(string building), paths (file locations).
component/ -- Subsystem Implementations
Subsystem logic that composes core/ packages. May depend on each other.
Owns configuration YANG (the data model parsed from config files).
Examples: bgp (BGP engine), config (config system), cli (SSH CLI editor),
iface (interface management), host (hardware detection), firewall,
doctor (readiness checks), web, l2tp.
Config YANG (ze-bgp-conf.yang, ze-iface-conf.yang) lives here because it
defines the subsystem's data model. It is not a user-facing command surface.
plugins/ -- User-Facing Command Surfaces
Everything the user interacts with through CLI commands. Each plugin folder contains:
- YANG command schemas -- the command tree definitions
- RPC handlers -- the
pluginserver.RegisterRPCs()handlers for online commands - CLI registration -- the
registry.RegisterRoot()/MustRegisterLocalMeta()wiring for offline commands
Plugins are thin. They import component/ or core/ for the real work.
A plugin translates a user command into a call to an implementation library.
Plugin Directory Layout
internal/plugins/<name>/
register.go # CLI registration (RegisterRoot, MustRegisterLocalMeta)
yang/
ze-<name>-cmd.yang # hand-written YANG command definitions
embed.go # GENERATED: //go:embed vars
register.go # GENERATED: yang.RegisterModule() calls
self_containment_test.go # presence test: owner's commands ARE declared
cmd/
register.go # pluginserver.RegisterRPCs() in init()
handler.go # RPC handler functions
YANG as Data, Not Code
YANG files are declarative definitions, not Go code. They live in a yang/
subfolder (not schema/) to make this clear. The Go glue files in yang/
(embed.go, register.go) are generated by codegen from the .yang files
present in the directory. The only hand-written file is the .yang itself.
This means adding a YANG command schema to a plugin is:
- Write
ze-<name>-cmd.yangin the plugin'syang/folder - Run
make generate - Done: the codegen produces
embed.go,register.go, and updatesall.go
How Codegen Enables the Folder Test
The codegen (scripts/codegen/plugin_imports.go) scans the directory tree for:
- Packages containing
yang.RegisterModulecalls -> adds toall.goschema imports - Packages containing
pluginserver.RegisterRPCscalls -> adds toall.goRPC imports yang/directories containing.yangfiles -> generatesembed.go+register.go
Because discovery is directory-based, copying a plugin folder in (or deleting it) and re-running codegen is all that's needed to wire (or unwire) the plugin.
YANG Container Merge
Owners declare their commands by re-stating the path from the verb root:
module ze-host-cmd {
namespace "urn:ze:host:cmd";
prefix hostcmd;
import ze-extensions { prefix ze; }
container show {
container host {
container cpu {
ze:command "ze-show:host-cpu";
}
}
}
}
The YANG loader unions same-named top-level containers across all registered
modules. The owner module needs no import or augment of the central verb
schema. Give each module a unique namespace and prefix.
Verb-Root Anchors
Multi-owner verbs (show, clear, monitor, delete, set, update) keep a
bare anchor in the central package internal/component/cmd/<verb>/. The anchor
declares only truly generic commands (cross-plugin aggregations, process-global
introspection). Each owner container-merges its subtree onto the verb root.
A central self-containment test bans every migrated token to prevent drift back.
What Stays Central
A command stays in the central verb package only when it has no single removable owner:
- Aggregates a cross-plugin registry (
show policy list,show metrics name) - Reads the generic core system (
show version,show health,show uptime) - Is process-global (
show system memory,show system goroutines)
Self-Containment Invariant
Two tests enforce the invariant per verb:
Central guard (e.g. cmd/show/yang/self_containment_test.go):
a banned-token map listing every owner-migrated command. If any banned token
appears in the central YANG, the test fails.
Owner presence (e.g. plugins/host/yang/self_containment_test.go):
asserts the owner's YANG declares its commands. If the owner is deleted, the
presence test vanishes with it (correct). If someone moves the command back to
central, the presence test fails (incorrect, caught).
When carving a new command out of a central schema:
- Add the banned token to the central guard test
- Add the presence assertion to the owner's YANG test
- Both halves must exist before the carve is complete
Handler Registration
Handlers register via pluginserver.RegisterRPCs() in an init() function
inside the plugin's cmd/ package:
func init() {
pluginserver.RegisterRPCs(
pluginserver.RPCRegistration{
WireMethod: "ze-show:host-cpu",
Handler: handleShowHostCPU,
},
)
}
The package must be blank-imported (directly or transitively) from
internal/component/plugin/all/all.go. The codegen scans for packages
containing pluginserver.RegisterRPCs or yang.RegisterModule calls and
generates the import list.
Summary: Where Things Go
| Artifact | Location | Hand-written? |
|---|---|---|
| Implementation library | component/<name>/ or core/<name>/ |
✓ |
| Config YANG (data model) | component/<name>/yang/ |
✓ |
| Command YANG (CLI tree) | plugins/<name>/yang/ |
✓ |
| YANG embed + register | plugins/<name>/yang/ |
Generated |
| RPC handlers | plugins/<name>/cmd/ |
✓ |
| Offline CLI registration | plugins/<name>/register.go |
✓ |
| Blank imports | all.go |
Generated |