Shared protocol definitions, generated code, and client libraries for Power Manage. Used by the Control Server, Gateway, Agent, and Web UI.
sdk/
├── proto/pm/v1/ Protocol Buffer source definitions
│ ├── common.proto Base types, enums, error codes
│ ├── actions.proto Action types, parameters, scheduling
│ ├── agent.proto Bidirectional streaming (Agent ↔ Gateway)
│ ├── control.proto Control API (164 RPCs)
│ ├── device_auth.proto Agent enrollment via local unix socket
│ └── internal.proto Gateway-to-control proxy for credential operations
│
├── gen/go/pm/v1/ Generated Go code (protobuf + Connect RPC)
│ ├── *.pb.go Message types with injected validation tags
│ └── pmv1connect/ Connect RPC client and server interfaces
│
├── go/
│ ├── client.go Agent streaming client (mTLS, heartbeat, action dispatch)
│ ├── crypto/ CSR generation and certificate utilities
│ ├── logging/ Structured logging setup (slog)
│ ├── pkg/ Package manager abstraction library
│ │ ├── apt.go APT (Debian/Ubuntu)
│ │ ├── dnf.go DNF (Fedora/RHEL)
│ │ ├── pacman.go Pacman (Arch Linux)
│ │ ├── zypper.go Zypper (openSUSE)
│ │ └── flatpak.go Flatpak (cross-distro)
│ ├── sys/ Linux system management libraries
│ │ ├── exec/ Command execution with pluggable PrivilegeBackend (sudo/doas)
│ │ ├── fs/ Filesystem operations (read, write, atomic, permissions)
│ │ ├── service/ Service manager — pluggable ServiceBackend (systemd/openrc/runit/s6)
│ │ ├── encryption/ Disk encryption — pluggable Backend (luks/geli/cgd)
│ │ ├── network/ WiFi connection profiles — pluggable WifiBackend (nm/connman/wpa/iwd)
│ │ ├── firewall/ Packet filter — pluggable Backend (nftables/iptables/firewalld/ufw/pf)
│ │ ├── dns/ Resolver config — pluggable Backend (resolved/resolvconf/dnsmasq/nm)
│ │ ├── netconfig/ IP / routing / DHCP — pluggable Backend (nm/networkd/netplan/dhcpcd/ifupdown)
│ │ ├── notify/ Desktop notification utilities
│ │ ├── osquery/ osquery integration (lazy-init, system queries)
│ │ ├── reboot/ Reboot scheduling
│ │ ├── terminal/ PTY session management
│ │ └── user/ User & group management, password generation
│ ├── validate/ Input validation (struct tag validator + ULID rule)
│ └── verify/ Action payload signature verification
│
├── ts/ TypeScript SDK (framework-agnostic browser utilities)
│ ├── client.ts Connect-RPC client with all API methods
│ ├── auth.ts JWT token management, persistent auth storage
│ ├── errors.ts Error code extraction from Connect-RPC errors
│ ├── action-types.ts Action type constants and display helpers
│ ├── config.ts Configuration utilities
│ ├── offline.ts Offline support utilities
│ └── index.ts Package exports
│
├── test/ Integration test infrastructure
│ ├── Dockerfile.integration Test container (systemd + sudo)
│ └── run-tests.sh Test runner script
│
└── Makefile Proto generation commands
Six proto files define the entire API surface:
| File | Purpose |
|---|---|
common.proto |
ULID identifiers, execution status, assignment modes, error detail codes |
actions.proto |
Action types (package, update, repository, app_image, deb, rpm, flatpak, shell, service, file, directory, user, group, ssh, sshd, admin_policy, lps, encryption, wifi, agent_update), parameters, scheduling. Several capability areas are modelled with a backend enum so the same action type can target multiple implementations (e.g. `AdminPolicyParams.backend = sudo |
agent.proto |
AgentService — bidirectional streaming RPC + action sync, heartbeat, output streaming, OS queries, log queries. Hello includes arch for platform detection. |
control.proto |
ControlService — full RPC surface (~164 methods) covering users, devices, groups, actions, sets, definitions, assignments, tokens, executions, roles, user groups, identity providers, SCIM, TOTP, audit, compliance policies, certificate renewal, search, server settings, and more |
device_auth.proto |
DeviceAuthService — agent enrollment via local unix socket |
internal.proto |
InternalService — gateway-to-control proxy for credential-bearing operations (LUKS keys, LPS passwords) and agent auto-update info |
go/client.go provides the agent-side streaming client:
import sdk "github.com/manchtools/power-manage-sdk"
client, _ := sdk.NewClient(gatewayURL,
sdk.WithMTLS(certFile, keyFile, caFile),
)
client.Run(ctx, handler)Features: mTLS authentication, automatic heartbeat, action result reporting, live output streaming, security alerts.
The receive loop is hardened against a compromised or buggy gateway:
- Inbound size bound.
NewClientwiresconnect.WithReadMaxBytes(maxInboundMessageBytes)(16 MiB). An over-largeServerMessageis refused with a resource-exhausted error andRunreturns cleanly; the caller's reconnect loop (the agent connection loop) re-establishes the stream, instead of allocating the frame — closing an OOM/DoS vector. - Per-message panic isolation.
dispatchServerMessageruns each message under a scopedrecover(): a panic inside any handler method is caught, logged, and turned into a non-fatal dropped frame so one bad handler invocation cannot crash-loop the whole agent (fleet DoS). Genuine fatal stream send/receive errors still propagate (they return fromRun, leaving reconnection to the caller). - Bounded, panic-safe goroutine fan-out. The server-originated
RequestInventoryandRevokeLuksDeviceKeylegs (and the inventory ticker) run throughsafeGo— a spawned goroutine with its own deferredrecover()— and are gated behind bounded semaphores so a flood of frames cannot spawn unbounded goroutines or crash the process from a goroutine panic. - Malformed-oneof nil-guards. A
ServerMessagewhose inner oneof payload is nil (e.g. aServerMessage_Actionwith a nilActionDispatch) is logged and dropped, never dereferenced. - Context-honoring sends. Every
Send*method observes itsctxfor both the send-lock acquisition and the underlyingstream.Send. A stalled peer (a full HTTP/2 flow-control window) can no longer wedge a sender — or every sender queued behind it — past its deadline; a deadline surfaces as actxerror. At most onestream.Sendis ever in flight, so on-wire ordering is preserved.
The sys/exec wrappers SIGKILL-escalate a cancelled child's process group after a bounded grace (then read the final status under a second bounded grace), so a SIGTERM-ignoring child cannot pin the reaping goroutine past the action's deadline.
go/client.go also provides a standalone RenewCertificate function for certificate rotation:
result, _ := sdk.RenewCertificate(ctx, controlURL, csrPEM, currentCertPEM)
// result.Certificate — new signed certificate (PEM)
// result.NotAfter — certificate expiry time
// result.CACert — active CA certificate (PEM), for CA rotationThe agent presents its current (still valid) certificate and a new CSR. The Control Server verifies the certificate, checks the fingerprint against the database, signs the new CSR, and returns the active CA certificate. If the CA has been rotated, the agent should update its stored CA certificate.
Bootstrap transport hardening. RegisterAgent and RenewCertificate are the unauthenticated bootstrap calls; their default HTTP client is bounded (request timeout + TLS 1.3 floor) so a hung or malicious control endpoint cannot wedge enrollment/renewal. Proxy support is retained for enterprise deployments (the channel is TLS-authenticated and the optional enrollment CA-pin catches a wrong-CA outcome). A ClientOption (the mTLS variants) overrides the client entirely.
Enrollment trust helpers (crypto):
fp, _ := crypto.CAFingerprintFromPEM(caPEM) // lowercase-hex SHA-256 of the CA DER
err := crypto.VerifyCAContinuity(oldCAPEM, newCAPEM) // accept only an identical or cross-signed CACAFingerprintFromPEM is byte-identical to the control server's ca.FingerprintFromPEM, so an operator-supplied out-of-band pin (e.g. openssl x509 -in ca.crt -outform DER | sha256sum) matches what the agent derives from the registration-returned CA. VerifyCAContinuity guards certificate rotation: a returned CA is adopted only when it is byte-identical to, or cross-signed by, the enrolled CA — refusing an unrelated trust-anchor swap.
pkg/ abstracts five Linux package managers behind a unified interface with a builder API:
import "github.com/manchtools/power-manage-sdk/pkg"
pm, _ := pkg.New() // auto-detect
pm.Install("nginx").Version("1.24.0").Run() // fluent builder
updates, _ := pm.ListUpgradable() // query methodsSee the package manager README for the full API.
sys/ provides opinionated Linux system management utilities. All privileged operations run through the configured privilege backend (sudo or doas, see exec.SetPrivilegeBackend), so the calling process does not need to be root.
import "github.com/manchtools/power-manage-sdk/sys/exec"
result, err := exec.Run(ctx, "ls", "-la") // basic command
result, err := exec.Privileged(ctx, "systemctl", "restart", "nginx") // through sudo/doas
stdout, err := exec.Query("hostname") // quick query
ok := exec.Check("which", "nginx") // boolean checkKey features:
- Streaming output via
RunStreamingwith per-line callbacks - Automatic path resolution for the privileged commands
- Output truncation at 1 MiB to prevent memory issues
import "github.com/manchtools/power-manage-sdk/sys/fs"
content, err := fs.ReadFile(ctx, "/etc/hostname")
err := fs.WriteFileAtomic(ctx, "/etc/nginx/nginx.conf", content, "0644", "root", "root")
exists := fs.FileExists(ctx, "/etc/motd")
err := fs.MkdirWithPermissions(ctx, "/opt/app", "0755", "app", "app", true)All operations escalate via the configured privilege backend (sudo or doas). WriteFileAtomic uses a write-then-rename sequence so concurrent readers see either the old or new file, never a half-written one. For fsync-level durability + an unguessable temp suffix, use AtomicWriteFile in atomic_write.go.
import "github.com/manchtools/power-manage-sdk/sys/user"
info, err := user.Get("deploy") // get user info (UID, GID, shell, groups, locked)
err := user.Create(ctx, "deploy", "-m", "-s", "/bin/bash")
err := user.GroupCreate(ctx, "developers")
err := user.GroupAddUser(ctx, "deploy", "developers")
password, err := user.GeneratePassword(24, true) // 24 chars, complex
err := user.SetPassword(ctx, "deploy", password)import "github.com/manchtools/power-manage-sdk/sys/osquery"
oq := osquery.New() // lazy-init, detects installs without restart
rows, err := oq.Query(ctx, "os_version", nil, 0) // query a tableThe convenience table path (Query/QueryTable with a table name) refuses a
curated deny-list of credential-bearing tables — shadow, process_envs,
crontab, shell_history, sudoers — before any SQL is built or run, so a
compromised control server cannot exfiltrate them through the agent's privileged
osquery. The deny-list is defense-in-depth on top of the CA-signed osquery
RPC; the signed RawSql path is the operator's explicit escape hatch and is
intentionally not gated.
import "github.com/manchtools/power-manage-sdk/sys/encryption"
err := encryption.AddKey(ctx, devicePath, existingKey, newKey)
err := encryption.AddKeyToSlot(ctx, devicePath, slot, existingKey, newKey)encryption exposes a pluggable Backend (LUKS today; geli/cgd planned). Call encryption.SetBackend(encryption.BackendLUKS) once at startup.
import "github.com/manchtools/power-manage-sdk/sys/service"
status, err := service.Status("nginx.service") // {Enabled, Active, Masked, Static}
err := service.EnableNow(ctx, "nginx.service")
err := service.WriteUnit(ctx, "myapp.service", unitContent)
err := service.DaemonReload(ctx)service selects between systemd / openrc / runit / s6 via service.SetServiceBackend(...).
verify/ provides action payload signature verification for agents:
import "github.com/manchtools/power-manage-sdk/verify"
signer := verify.NewActionSigner(caKey) // server-side signing
verifier := verify.NewActionVerifier(caCert) // agent-side verification
err := verifier.Verify(action) // verify action signatureBeyond actions, the same CA key signs the four root stream-RPCs (osquery, log query, LUKS revoke, inventory) under per-surface disjoint domains so a signature for one surface can never be replayed against another:
// Control server signs the canonical bytes of each message (signature field
// cleared) under that surface's domain; the agent verifies before any root work.
canonical, _ := verify.OSQueryCanonical(q) // also LogQueryCanonical / RevokeLuksDeviceKeyCanonical / RequestInventoryCanonical
sig, _ := signer.SignDomain(verify.OSQuerySignatureDomain, canonical)
err := verifier.VerifyDomain(verify.OSQuerySignatureDomain, canonical, sig)validate/ wraps the go-playground/validator library with the project's custom rules (today: a ulid tag for ULID identifiers). RPC handlers and other server code use it via:
import "github.com/manchtools/power-manage-sdk/validate"
v := validate.NewValidator() // returns a *validator.Validate with rules registered
msg, ok := validate.Struct(v, request) // returns a formatted error message
text := validate.FormatFieldError(fieldErr) // single-field formatterField rules are declared via validate:"..." struct tags injected into the generated proto types (@gotags: validate:"required,ulid" etc.).
The sys/ packages include integration tests that run inside a systemd-enabled container:
./sdk/test/run-tests.shThis builds a test image, boots systemd, and runs all tests as a non-root user with sudo access.
The ts/ directory contains framework-agnostic browser utilities used by the web frontend:
| File | Purpose |
|---|---|
client.ts |
Connect-RPC client wrapping the full ControlService surface (~164 RPCs) |
auth.ts |
JWT token management with persistent auth storage ("keep me signed in") |
errors.ts |
Error code extraction from ConnectError details (getErrorCode()) |
action-types.ts |
Action type constants, display names, and icon mappings |
config.ts |
Configuration utilities |
offline.ts |
Offline support utilities |
Generated TypeScript types (protobuf messages) are produced separately via Buf:
cd ../web && npx buf generate # outputs to web/src/lib/gen/pm/v1/Or download the ts-sdk.tar.gz from the latest release and extract into your project.
# Go (protobuf + Connect RPC + validation tags)
make generate
# TypeScript (from the web directory)
cd ../web && npx buf generate
# Install Go proto tools
make install-toolsDownstream repos (agent, server) pin the SDK to a specific commit via a Go pseudo-version. SDK main can change without breaking downstream builds; bumps are explicit PRs. For the full release flow — pinning, cross-cutting development with go.work, tagging conventions, and the separation between human-readable GitHub Release tags and Go module pins — see docs/release-coordination.md.
MIT — see LICENSE.