Documentation
¶
Overview ¶
Package coreapi defines the L10 plugin runtime contract.
The interfaces in this package are the only surface a plugin (L11) ever sees of the daemon. Plugins import coreapi; the daemon implements coreapi; the bridge happens at lifecycle bootstrap (cmd/daemon/main.go registers concrete plugins against the daemon's coreapi implementations).
See docs/architecture/01-LAYERS.md §10 for the layer's role, docs/architecture/03-INVARIANTS.md for the principles this package enforces, and docs/architecture/06-CHANGES.md §2 for the rationale of each interface signature.
Stability contract: every exported identifier in this package is part of the daemon-plugin ABI. Removing or renaming any of them breaks every plugin. Additions are forward-compatible.
Index ¶
- Constants
- Variables
- func PluginRecoveredPanicCount() uint64
- func RecoverPlugin(plugin, op string, events EventBus, onPanic func(any))
- func ResetPluginRecoveredPanicCountForTest()
- type Addr
- type Deps
- type Event
- type EventBus
- type Identity
- type Listener
- type PeerInfo
- type PeerResolver
- type PolicyEventType
- type PolicyManager
- type PolicyRunner
- type Service
- type ServiceRegistry
- type Stream
- type Streams
- type TrustChecker
Constants ¶
const ( PolicyEventConnect = "connect" PolicyEventDial = "dial" PolicyEventDatagram = "datagram" PolicyEventJoin = "join" PolicyEventLeave = "leave" PolicyEventCycle = "cycle" )
Variables ¶
var ( // ErrRegistryStarted is returned by ServiceRegistry.Register and // ServiceRegistry.StartAll when StartAll has already been called. // Plugins must register before bootstrap. ErrRegistryStarted = errors.New("coreapi: service registry already started") // ErrServiceNotReady indicates a Service.Start call was made on a // dependency that itself hasn't completed Start. Surface only — // Service implementations shouldn't return this; the registry will. ErrServiceNotReady = errors.New("coreapi: dependency service not ready") // ErrPeerNotFound is the canonical "directory has no record" error // from PeerResolver. Plugins should match on errors.Is. ErrPeerNotFound = errors.New("coreapi: peer not found") )
Sentinel errors returned by the L10 surface.
Functions ¶
func PluginRecoveredPanicCount ¶
func PluginRecoveredPanicCount() uint64
PluginRecoveredPanicCount returns the total number of panics swallowed by RecoverPlugin since process start.
func RecoverPlugin ¶
RecoverPlugin is the L11 panic-recovery shim used at the top of every plugin entrypoint goroutine: Service.Start helper goroutines, acceptLoop, and per-connection handlers. Usage:
defer coreapi.RecoverPlugin("eventstream", "acceptLoop", events, nil)
On panic it:
- Recovers (caller goroutine continues / loop iteration is dropped)
- Logs at ERROR with structured plugin/op fields, panic value, and full goroutine stack trace
- Increments PluginRecoveredPanicCount
- Publishes a "plugin.<plugin>.panic" event on the bus (if events != nil) so observability subscribers see the recovery
- Calls onPanic(r) if non-nil — typical use is per-conn close, or signaling a future per-plugin supervisor for restart
TODO(03-INVARIANTS.md §8): per-plugin supervisor not yet implemented. Today the boundary just survives + logs. A future tier will signal a restart of the panicked plugin via the onPanic callback.
This must be the OUTERMOST defer in the goroutine: defers run LIFO, so other defers (conn.Close, mu.Unlock, removeSub) run first.
func ResetPluginRecoveredPanicCountForTest ¶
func ResetPluginRecoveredPanicCountForTest()
ResetPluginRecoveredPanicCountForTest is test-only.
Types ¶
type Addr ¶
Addr is the 48-bit virtual address used throughout the protocol. Re-exported here so plugins can stay free of pkg/protocol if they want.
type Deps ¶
type Deps struct {
Streams Streams
Identity Identity
Resolver PeerResolver
Events EventBus
Logger *slog.Logger
// Optional — nil if the plugin providing them isn't registered.
Trust TrustChecker
}
Deps is the bag of capabilities a plugin can use. Optional fields may be nil if the corresponding plugin isn't loaded; plugins that hard-depend on them should error in Start().
type Event ¶
Event is one item published to the EventBus. Topics are dot-namespaced (e.g., "tunnel.established", "security.nonce_replay"). Payload keys/values are plugin-defined; subscribers parse them.
type EventBus ¶
type EventBus interface {
Publish(topic string, payload map[string]any)
Subscribe(pattern string) (<-chan Event, func())
}
EventBus is the publish/subscribe channel that replaces inline webhook.Emit calls inside core layers. Core (L2-L7) publishes; the webhook plugin (and any other observability plugin) subscribes.
Publish is non-blocking. If the bus is over capacity, the event is dropped (and a metric counter is incremented inside the daemon implementation). This keeps L2 readLoop / L6 decrypt latency bounded.
Subscribe returns a buffered channel and an unsubscribe func. Pattern is a glob: "tunnel.*" matches "tunnel.established" but not "security.nonce_replay".
type Identity ¶
type Identity interface {
NodeID() uint32
Address() Addr
PublicKey() ed25519.PublicKey
Sign(msg []byte) ([]byte, error)
}
Identity is the daemon's own identity — its Ed25519 keypair, its stable nodeID, its 48-bit address. Plugins may sign arbitrary bytes (e.g., for plugin-level auth proofs) but cannot replace the identity.
type Listener ¶
Listener accepts inbound streams on a single well-known or ephemeral port. Returned by Streams.Listen.
type PeerInfo ¶
type PeerInfo struct {
NodeID uint32
Addr Addr
Endpoint *net.UDPAddr // best-known reachable endpoint, or nil
PubKey ed25519.PublicKey
Public bool
Hostname string
RelayOnly bool
}
PeerInfo is the directory record for a remote node. Returned by PeerResolver.Resolve and PeerResolver.ListByNetwork.
type PeerResolver ¶
type PeerResolver interface {
Resolve(ctx context.Context, nodeID uint32) (PeerInfo, error)
ResolveHostname(ctx context.Context, name string) (uint32, error)
ListByNetwork(ctx context.Context, networkID uint32) ([]PeerInfo, error)
}
PeerResolver is the L8 directory surface. The daemon's implementation talks to the registry over the bootstrap TCP side-channel (see 01-LAYERS §L8).
type PolicyEventType ¶
type PolicyEventType = string
PolicyEventType is the kind of protocol event a policy is evaluated against. Type alias to string so daemon-local primitive interfaces can satisfy plugin signatures via structural typing without importing this package (T7.1).
type PolicyManager ¶
type PolicyManager interface {
// Start compiles a policy JSON for the given network and registers
// a runner. Returns the runner handle; existing runners for the
// same network are stopped first.
Start(netID uint16, policyJSON []byte) (PolicyRunner, error)
// Stop stops the runner for netID (no-op if absent).
Stop(netID uint16)
// Get returns the runner for netID or nil.
Get(netID uint16) PolicyRunner
// All returns a snapshot of all running runners.
All() []PolicyRunner
// StopAll stops every runner. Called during daemon shutdown.
StopAll()
// LoadPersisted runs at daemon-Start to restore runners from disk.
LoadPersisted() error
}
PolicyManager owns the per-network registry of policy runners. The daemon holds it as an interface field; cmd/daemon (L12) constructs the concrete plugin and calls Daemon.RegisterPolicyManager.
type PolicyRunner ¶
type PolicyRunner interface {
NetworkID() uint16
// HasMember returns true if peerNodeID is in this runner's
// per-peer state. The daemon iterates all runners to consult
// every network the peer belongs to (deny wins across networks).
HasMember(peerNodeID uint32) bool
// EvaluatePortGate is the daemon-facing gate API for inbound SYN
// (Connect), outbound SYN (Dial), and datagram (in/out) events.
// The plugin builds the per-peer ctx internally (peer_age_s,
// peer_tags, members) using its peer state and the
// daemon-supplied localTags + nodeInfoTags. Returns the
// allow/deny verdict (default allow on no explicit deny).
EvaluatePortGate(eventType PolicyEventType, port uint16, peerNodeID uint32, payloadSize int, direction string, localTags, nodeInfoTags []string) bool
// EvaluateActions runs an action-event (cycle/join/leave) with a
// caller-built ctx. Side-effect-only: no return value.
EvaluateActions(eventType PolicyEventType, ctx map[string]any)
Status() map[string]any
PeerList() []map[string]any
ForceCycle() map[string]any
ReconcileNow()
// PolicyJSON returns the marshaled policy document. Used by IPC
// handlers that read the current policy back to admin tools.
PolicyJSON() ([]byte, error)
Stop()
}
PolicyRunner is the daemon-facing surface of a single network's running policy. The plugin's concrete *PolicyRunner type implements this. The daemon never holds the concrete type — only this interface.
type Service ¶
type Service interface {
Name() string
Order() int
Start(ctx context.Context, deps Deps) error
Stop(ctx context.Context) error
}
Service is the lifecycle contract every L11 plugin implements.
Order determines the start sequence. Lower numbers start first; higher numbers stop first. Suggested ranges:
10-49 Foundation (none today) 50-79 Trust / identity-adjacent (trustedagents) 80-99 Observability (webhook) 100-199 Application services (dataexchange, eventstream, tasks) 200-249 Sidecars (skillinject) 250+ Tooling-bound (updater)
Start receives Deps (the L10 surface). Implementations must NOT retain references to anything outside Deps — that's the whole extraction contract.
Stop should drain in-flight work, close listeners, and signal background goroutines to exit. It must return within 5 seconds or the daemon shutdown gate will fail.
type ServiceRegistry ¶
type ServiceRegistry struct {
// contains filtered or unexported fields
}
ServiceRegistry coordinates plugin lifecycle. cmd/daemon/main.go constructs one, registers each plugin, and hands it to the daemon. The daemon calls StartAll during bootstrap and StopAll during shutdown.
func (*ServiceRegistry) All ¶
func (sr *ServiceRegistry) All() []Service
All returns a snapshot of the registered services in start order.
func (*ServiceRegistry) Register ¶
func (sr *ServiceRegistry) Register(s Service) error
Register adds a service. Must be called before StartAll. After StartAll runs, Register is a no-op error.
func (*ServiceRegistry) StartAll ¶
func (sr *ServiceRegistry) StartAll(ctx context.Context, deps Deps) error
StartAll sorts by Order and starts every service in sequence. The first failing Start aborts and returns its error; previously- started services are NOT auto-stopped (the caller's job, via Stop() or by passing a context that cancels).
type Stream ¶
type Stream interface {
io.ReadWriteCloser
LocalAddr() Addr
LocalPort() uint16
RemoteAddr() Addr
RemotePort() uint16
SetDeadline(t time.Time) error
SetReadDeadline(t time.Time) error
SetWriteDeadline(t time.Time) error
}
Stream is one bidirectional ordered byte stream between two (Addr, port) endpoints. It mirrors net.Conn so plugins can wrap it behind any io-aware library.
type Streams ¶
type Streams interface {
Dial(ctx context.Context, dst Addr, port uint16) (Stream, error)
Listen(port uint16) (Listener, error)
SendDatagram(ctx context.Context, dst Addr, port uint16, data []byte) error
}
Streams is the L7 surface plugins consume. The daemon-side implementation routes through L7 → L6 → L5 → L4 → L2.
SendDatagram is the connectionless variant (one packet, no ACK, no retransmit). Used by plugins that don't need stream semantics.
type TrustChecker ¶
type TrustChecker interface {
// IsTrusted reports whether the peer is on the auto-approve allowlist.
// Returns the agent's display name when known. Both return values are
// zero on miss.
IsTrusted(nodeID uint32) (name string, ok bool)
}
TrustChecker is the trusted-agents gate consumed by L11/tasks (and any other plugin that gates on peer reputation).
IsTrusted: returns true if the peer is on the auto-approve allowlist (loaded from the trusted-agents JSON, refreshed hourly).