Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions daemonapi/connection.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// SPDX-License-Identifier: AGPL-3.0-or-later

package daemonapi

// Connection is an opaque handle to a daemon stream connection.
// Plugins receive Connection values from DialConnection / Accept and
// pass them back to SendData / CloseConnection / NewConnReadWriter.
// The concrete *daemon.Connection in web4/pkg/daemon satisfies this
// via Go's structural typing — there are no methods on the interface
// itself; it's a marker token.
//
// Keeping Connection an opaque marker is deliberate: plugins like
// runtime treat connections as values they hand back to the daemon
// for any operation. They never inspect connection internals.
// Anything that ever does (writing bytes, reading bytes, closing)
// goes through ConnReadWriter — see below.
type Connection interface{}

// ConnReadWriter is the read/write adapter plugins use when they
// need net.Conn-style I/O on a Connection. Construct one via
// Daemon.NewConnReadWriter(conn).
type ConnReadWriter interface {
Read(p []byte) (int, error)
Write(p []byte) (int, error)
Close() error
}

// PortAllocator is an opaque handle to the daemon's port table.
// Plugins receive it via Daemon.Ports() and typically hand it to
// other plugins that need to bind well-known ports. Like Connection,
// it has no methods on this interface — concrete daemon types
// satisfy it via structural typing.
type PortAllocator interface{}

// TunnelRegistry is an opaque handle to the daemon's tunnel table.
// Same opaque-marker shape as PortAllocator: plugins pass it
// around, the daemon owns the implementation.
type TunnelRegistry interface{}
181 changes: 181 additions & 0 deletions daemonapi/daemon.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
// SPDX-License-Identifier: AGPL-3.0-or-later

package daemonapi

import (
"context"

"github.com/pilot-protocol/common/crypto"
"github.com/pilot-protocol/common/protocol"
registry "github.com/pilot-protocol/common/registry/client"
)

// Daemon is the dependency-free contract a daemon engine exposes to
// plugins. The concrete *daemon.Daemon in web4/pkg/daemon satisfies
// this interface via Go's structural typing — neither side needs to
// import the other.
//
// Every method here exists because at least one plugin (handshake,
// runtime, libpilot) calls it. Adding a method later is a backwards-
// compatible change as long as concrete daemon implementations grow
// the corresponding method first.
//
// Return types deliberately use:
//
// - Common-package concrete types (*crypto.Identity, *registry.Client,
// protocol.Addr) — those types already live in the dependency-free
// common module, so referencing them here doesn't create cycles.
//
// - daemonapi-local interfaces (Connection, PortAllocator, EventBus,
// etc.) — where the concrete return type lives inside the daemon
// engine. The opaque-marker interfaces keep plugins from poking
// at engine internals while still letting the daemon hand the
// value back to plugins as a typed token.
type Daemon interface {
// --- Lifecycle ---------------------------------------------------

// Start brings the daemon online. Returns when the listeners are
// bound and the daemon is ready to accept traffic, or with an
// error if any step of bootstrap fails.
Start() error

// Stop drains in-flight work and tears down the daemon. Idempotent;
// safe to call from a signal handler.
Stop() error

// --- Identity ---------------------------------------------------

// NodeID returns this daemon's stable 32-bit node ID. 0 when the
// identity has not been loaded yet.
NodeID() uint32

// Identity returns the daemon's Ed25519 keypair holder. Returns
// nil when the daemon was started without an identity file
// (in-memory tests).
Identity() *crypto.Identity

// IdentityPath returns the on-disk path to the identity file.
// Empty when running in-memory.
IdentityPath() string

// Sign signs the message with the local Ed25519 private key.
// Returns nil when no identity is loaded.
Sign(msg []byte) []byte

// --- Configuration ----------------------------------------------

// AdminToken returns the local admin token used to authenticate
// privileged registry RPCs. Empty when not configured.
AdminToken() string

// TrustAutoApprove reports whether the daemon was started with
// the auto-approve flag set. Plugins gating user-visible decisions
// on this flag (handshake auto-accept) read it once at Init.
TrustAutoApprove() bool

// --- Network plumbing -------------------------------------------

// Addr returns the daemon's pilot-network address. Stable for
// the life of the daemon process.
Addr() protocol.Addr

// DialConnection opens an outbound stream to (dstAddr, dstPort)
// and returns an opaque Connection handle. The handle is passed
// back to SendData, CloseConnection, NewConnReadWriter, etc.
DialConnection(dstAddr protocol.Addr, dstPort uint16) (Connection, error)

// DialConnectionContext is DialConnection with a deadline. The
// context's Done channel cancels the dial.
DialConnectionContext(ctx context.Context, dstAddr protocol.Addr, dstPort uint16) (Connection, error)

// SendData writes the byte slice to the stream connection.
// Blocks until the data is queued for transmission.
SendData(conn Connection, data []byte) error

// SendDatagram sends an unconnected (UDP-shaped) payload to
// (dstAddr, dstPort). Best-effort, no retransmission.
SendDatagram(dstAddr protocol.Addr, dstPort uint16, data []byte) error

// CloseConnection tears down the connection. Idempotent.
CloseConnection(conn Connection)

// NewConnReadWriter wraps a stream Connection as a net.Conn-style
// adapter. Plugins that need read/write semantics use this; the
// daemon retains ownership of the underlying connection state.
NewConnReadWriter(conn Connection) ConnReadWriter

// Ports returns the daemon's port allocator. Opaque to most
// plugins; usually handed off to other plugins (e.g. handshake)
// that bind well-known ports through it.
Ports() PortAllocator

// Tunnels returns the daemon's tunnel registry. Opaque to most
// plugins.
Tunnels() TunnelRegistry

// --- Registry ---------------------------------------------------

// RegistryClient returns the L8 registry-side-channel client.
// nil when the daemon is running without a registry connection.
RegistryClient() *registry.Client

// RegConnListNodes is the privileged list_nodes RPC, used by the
// policy plugin to enumerate per-network members.
RegConnListNodes(netID uint16, token string) (map[string]any, error)

// SetMemberTags updates the local node's per-network tag list
// via the registry.
SetMemberTags(netID uint16, tags []string)

// --- Events -----------------------------------------------------

// PublishEvent is the bus.Publish wrapper, exposed at top level
// because plugins commonly publish without holding a Bus reference.
PublishEvent(topic string, payload map[string]any)

// Bus returns the in-process event bus for plugins that subscribe.
Bus() EventBus

// --- Trust + handshake plugin coordination ----------------------

// GetTrustChecker returns the currently-registered trust checker
// (typically the trustedagents plugin). Returns nil when no
// checker is wired.
GetTrustChecker() TrustChecker

// RegisterTrustChecker installs the given checker as the daemon's
// trust authority. Called once at startup by the trustedagents
// plugin via the runtime adapter.
RegisterTrustChecker(tc TrustChecker)

// HandshakeService returns the currently-registered handshake
// service. Returns nil when no handshake plugin is wired (tests
// that bypass plugins).
HandshakeService() HandshakeService

// RegisterHandshakeService installs the handshake plugin's
// service. Called once at startup via the runtime adapter.
RegisterHandshakeService(svc HandshakeService)

// TrustedPeers proxies through to HandshakeService().TrustedPeers().
// Returns nil when no handshake plugin is wired.
TrustedPeers() []HandshakeTrustRecord

// HandshakeRevokeTrust proxies through to HandshakeService().RevokeTrust.
HandshakeRevokeTrust(nodeID uint32) error

// HandshakeSendRequest proxies through to HandshakeService().SendRequest.
HandshakeSendRequest(nodeID uint32, reason string) error

// --- Policy + webhook plugin coordination -----------------------

// RegisterPolicyManager installs the policy plugin's manager.
RegisterPolicyManager(pm PolicyManager)

// SetWebhookURL hot-swaps the active webhook URL on the registered
// webhook plugin. No-op when no plugin is registered.
SetWebhookURL(url string)

// RegisterWebhookManager installs the webhook plugin's manager.
RegisterWebhookManager(wm WebhookManager)
}
55 changes: 55 additions & 0 deletions daemonapi/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// SPDX-License-Identifier: AGPL-3.0-or-later

// Package daemonapi is the dependency-free contract layer between the
// daemon engine and its plugins.
//
// The cycle problem: handshake / runtime / libpilot are daemon plugins
// that historically imported web4/pkg/daemon for the concrete Daemon
// struct, Config, Connection, Listener, and assorted services. The
// daemon binary in turn imported the plugins to compose itself. This
// is a real cycle (web4 ↔ plugins) that survives only because of
// `replace` directives during local dev.
//
// daemonapi breaks the cycle by hosting:
//
// 1. Pure-interface contracts (Daemon, Connection, Listener,
// TrustChecker, HandshakeService, PolicyManager, PolicyRunner,
// WebhookManager, ...). The concrete *daemon.Daemon and its
// members satisfy these via Go's structural typing — the daemon
// engine never imports daemonapi to register, and plugins never
// import the daemon engine.
//
// 2. A runtime plugin lifecycle (Plugin interface) and a process-
// global registry (RegisterPlugin, LoadAll). Plugins register
// themselves from an init() block in their own package; the
// daemon engine, with no compile-time knowledge of which plugins
// exist, iterates whatever the registry contains.
//
// "Not static" wiring means:
//
// - The daemon engine has zero hardcoded list of plugins. Adding or
// removing a plugin from a binary is a single blank-import line
// in cmd/daemon/main.go; the daemon, plugin, and other plugin
// packages all stay unchanged.
//
// - The Plugin contract is interface-based, so a plugin's source
// code is interchangeable: two `handshake` implementations satisfy
// the same Plugin interface, the daemon doesn't know the difference.
//
// - The registration mechanism is identical for in-process plugins
// (Go packages linked into the binary) and for true runtime
// plugins (Go plugin.Open of .so files). The .so's init() block
// calls RegisterPlugin the same way; the daemon engine then
// iterates the registry.
//
// What this package does NOT do:
//
// - It does not own daemon implementation code. Concrete types stay
// in web4/pkg/daemon (and plugins' own implementations stay in
// their own repos). Interfaces only.
//
// - It does not specify how the daemon engine starts or shuts down.
// That's the daemon's job. The Plugin lifecycle methods (Init,
// Shutdown) tell plugins when the daemon is ready and when it's
// stopping; the daemon decides the broader sequencing.
package daemonapi
32 changes: 32 additions & 0 deletions daemonapi/event.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// SPDX-License-Identifier: AGPL-3.0-or-later

package daemonapi

import "time"

// Event is the payload structure carried over the daemon event bus.
// Mirrors the layout of the concrete event type in web4/pkg/daemon
// so subscriber channels receive identical-shaped values across the
// interface boundary.
type Event struct {
Topic string
NodeID uint32
Time time.Time
Payload map[string]any
}

// EventBus is the in-process pub/sub surface plugins consume. The
// concrete *inProcessBus in web4/pkg/daemon satisfies this; plugins
// retrieve it via Daemon.Bus() and Subscribe / Publish without
// knowing the underlying implementation.
type EventBus interface {
// Publish emits the topic + payload to every subscriber whose
// pattern matches the topic. Non-blocking; events may be dropped
// on per-subscriber backpressure.
Publish(topic string, payload map[string]any)

// Subscribe registers a pattern-matching subscriber and returns
// the receive channel plus a cancellation closure. Calling the
// closure stops delivery and drains the buffer.
Subscribe(pattern string) (<-chan Event, func())
}
43 changes: 43 additions & 0 deletions daemonapi/plugin.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// SPDX-License-Identifier: AGPL-3.0-or-later

package daemonapi

import "context"

// Plugin is the lifecycle contract every daemon plugin implements.
// The daemon calls Init once after the engine is running and Shutdown
// once when the engine is stopping. Name returns a stable identifier
// used for registration, log lines, and metrics labels.
//
// A plugin holds the Daemon it was initialized with for the rest of
// its lifetime; the daemon engine will not be replaced under it. If
// the daemon shuts down, plugins receive Shutdown before the engine
// finishes its own teardown — they are guaranteed to be able to use
// the Daemon during Shutdown for any final cleanup (closing pending
// streams, dispatching final webhook deliveries, etc.).
type Plugin interface {
// Name returns the registration key of this plugin. Must be
// stable across releases of the plugin; the daemon engine may
// use it for keyed lookups, persistence, and operator-facing
// status output.
Name() string

// Init wires the plugin to a running daemon engine. The plugin
// retains the Daemon for the rest of its lifetime. Init returns
// an error if the plugin cannot bootstrap; the daemon aborts
// startup on any plugin Init failure.
Init(d Daemon) error

// Shutdown stops the plugin's background work and drains any
// in-flight requests. The Daemon passed to Init is still valid
// during Shutdown — plugins may use it for last-mile work — but
// the daemon engine will not accept new traffic. Shutdown should
// honor the context's deadline; the daemon will not wait
// indefinitely on a stuck plugin.
Shutdown(ctx context.Context) error
}

// Factory builds a new instance of a plugin. Registered factories
// run when the daemon calls LoadAll; each call produces a fresh
// Plugin value, so plugins do not share state across daemon restarts.
type Factory func() Plugin
Loading
Loading