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/      # shared subsystem implementations and services
    plugins/        # self-contained command-only and full-subsystem owners

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/ -- Shared Subsystem Implementations

Subsystem logic that multiple owners use can compose core/ and other component/ packages. A component owns configuration YANG when its data model belongs to the shared subsystem rather than one removable plugin.

Examples include bgp (BGP engine), config (config system), cli (SSH CLI editor), iface (interface management), host (hardware detection), firewall, doctor (readiness checks), web, and l2tp.

plugins/ -- Self-Contained Feature Owners

Each folder under internal/plugins/ owns a removable feature surface. Two plugin shapes use this directory:

Both shapes can own user commands. Removing either folder removes all surfaces that belong to that owner.

Plugin Directory Layout

internal/plugins/<name>/
    register.go            # lifecycle or offline CLI registration, when needed
    *.go                   # runtime, config, and state for a full-subsystem plugin
    yang/
        ze-<name>-*.yang   # hand-written command or config definitions
        embed.go           # GENERATED: //go:embed vars
        register.go        # GENERATED: yang.RegisterModule() calls
    cmd/
        register.go        # pluginserver.RegisterRPCs() in init()
        handler.go         # RPC handler functions

Command-only plugins omit subsystem runtime files. Full-subsystem plugins keep their runtime with the command and configuration surfaces that they own.

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 Shared code: component/<name>/ or core/<name>/
Full-subsystem plugin: plugins/<name>/
Config YANG (data model) Shared subsystem: component/<name>/yang/
Full-subsystem plugin: plugins/<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