coreapi

package
v1.10.0-rc4 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 11, 2026 License: AGPL-3.0 Imports: 13 Imported by: 0

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

View Source
const (
	PolicyEventConnect  = "connect"
	PolicyEventDial     = "dial"
	PolicyEventDatagram = "datagram"
	PolicyEventJoin     = "join"
	PolicyEventLeave    = "leave"
	PolicyEventCycle    = "cycle"
)

Variables

View Source
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

func RecoverPlugin(plugin, op string, events EventBus, onPanic func(any))

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:

  1. Recovers (caller goroutine continues / loop iteration is dropped)
  2. Logs at ERROR with structured plugin/op fields, panic value, and full goroutine stack trace
  3. Increments PluginRecoveredPanicCount
  4. Publishes a "plugin.<plugin>.panic" event on the bus (if events != nil) so observability subscribers see the recovery
  5. 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

type Addr = protocol.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

type Event struct {
	Topic   string
	NodeID  uint32
	Time    time.Time
	Payload map[string]any
}

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

type Listener interface {
	Accept() (Stream, error)
	Close() error
	Addr() Addr
	Port() uint16
}

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).

func (*ServiceRegistry) StopAll

func (sr *ServiceRegistry) StopAll(ctx context.Context) error

StopAll stops every started service in reverse order. Errors from individual Stop calls are collected; the first one is returned but every service still gets its Stop call invoked.

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).

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL