Architecture

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:

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:

  1. Write ze-<name>-cmd.yang in the plugin's yang/ folder
  2. Run make generate
  3. Done: the codegen produces embed.go, register.go, and updates all.go

How Codegen Enables the Folder Test

The codegen (scripts/codegen/plugin_imports.go) scans the directory tree for:

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:

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:

  1. Add the banned token to the central guard test
  2. Add the presence assertion to the owner's YANG test
  3. 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