Extensions¶
Shed provides a namespaced message bus for communication between processes inside VMs and external processes on the host. This enables building plugins that extend shed's functionality without modifying shed itself.
Overview¶
The extension system works by routing messages between guest and host:
- A process inside the VM publishes a message to a namespace (e.g.,
opfor 1Password) - The shed agent forwards it over vsock to the shed server
- The server delivers it to a registered host-side listener via SSE
- The listener processes the request and sends a response back
sequenceDiagram
participant G as Guest Process
participant A as Agent (VM)
participant S as Shed Server
participant L as Host Listener
G->>A: POST /v1/publish
A->>S: vsock message
S->>L: SSE event
L->>S: POST /respond
S->>A: vsock message
A->>G: HTTP response
Message Envelope¶
All messages use a common JSON envelope:
{
"id": "019abc12-3456-7def-8901-234567890abc",
"namespace": "op",
"type": "request",
"in_reply_to": "",
"final": true,
"timestamp": "2026-03-29T12:00:00Z",
"payload": {},
"shed": {
"name": "my-dev",
"backend": "vz",
"server": "mini"
}
}
| Field | Type | Description |
|---|---|---|
id |
string | UUIDv7 identifier, unique per message |
namespace |
string | Routing key (e.g., op, proxy). system:* is reserved. |
type |
string | request, response, or event |
in_reply_to |
string | For responses: the id of the original request |
final |
boolean | Whether this is the last response for a request |
timestamp |
string | RFC 3339 timestamp |
payload |
object | Namespace-specific data (opaque to shed) |
shed |
object | Shed metadata, added by the server |
Publishing from Inside a VM¶
The shed agent runs an HTTP API on 127.0.0.1:498 inside every VZ and Firecracker VM.
Send a request (wait for response)¶
curl -s http://127.0.0.1:498/v1/publish -d '{
"namespace": "op",
"payload": {"action": "read", "item": "github-token"}
}'
The agent blocks until the host listener responds (30s timeout). The response envelope is returned as the HTTP body.
Send an event (fire-and-forget)¶
curl -s http://127.0.0.1:498/v1/publish -d '{
"namespace": "proxy",
"type": "event",
"payload": {"action": "register", "port": 8080}
}'
Returns 202 Accepted immediately.
Error responses¶
| Status | Code | Meaning |
|---|---|---|
| 400 | INVALID_BODY |
Malformed JSON |
| 400 | MISSING_NAMESPACE |
Namespace field is empty |
| 503 | NO_CONNECTION |
Agent has no active vsock connection to host |
| 504 | TIMEOUT |
Host listener did not respond within 30s |
Building a Host-Side Listener¶
A host-side listener subscribes to a namespace via SSE and receives messages from all running sheds.
Subscribe to a namespace¶
Messages arrive as SSE events:
event: message
data: {"id":"...","namespace":"op","type":"request","payload":{...},"shed":{"name":"my-dev","backend":"vz","server":"mini"}}
Send a response¶
When you receive a request, respond by POSTing back to the server. Include the shed name for routing:
curl -X POST http://localhost:8080/api/plugins/listeners/op/respond \
-H "Content-Type: application/json" \
-d '{
"namespace": "op",
"type": "response",
"in_reply_to": "019abc12-...",
"final": true,
"shed": {"name": "my-dev"},
"payload": {"token": "ghp_..."}
}'
Go client example¶
package main
import (
"bufio"
"encoding/json"
"fmt"
"net/http"
"strings"
)
func main() {
req, _ := http.NewRequest("GET",
"http://localhost:8080/api/plugins/listeners/op/messages", nil)
req.Header.Set("Accept", "text/event-stream")
resp, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer resp.Body.Close()
scanner := bufio.NewScanner(resp.Body)
for scanner.Scan() {
line := scanner.Text()
if strings.HasPrefix(line, "data: ") {
data := strings.TrimPrefix(line, "data: ")
var env map[string]any
json.Unmarshal([]byte(data), &env)
fmt.Printf("Message from shed %s: %v\n",
env["shed"].(map[string]any)["name"], env["payload"])
}
}
}
Server API Reference¶
All endpoints are under the shed server's HTTP API (default port 8080).
List active listeners¶
Subscribe to namespace (SSE)¶
Registers as the listener for the namespace and streams messages as SSE events. Only one listener per namespace is allowed.
| Status | Code | Meaning |
|---|---|---|
| 403 | NAMESPACE_RESERVED |
system:* namespaces cannot be registered |
| 409 | NAMESPACE_ALREADY_REGISTERED |
Another listener is already subscribed |
Respond to a message¶
Sends a response envelope back to the originating shed. The envelope body must include shed.name to route the response.
| Status | Code | Meaning |
|---|---|---|
| 204 | Response delivered | |
| 400 | MISSING_SHED |
Envelope missing shed.name |
| 404 | SHED_NOT_CONNECTED |
Target shed has no active message channel |
List connected sheds¶
Extension Management¶
Extensions can be selectively enabled via server configuration. The agent reads extension manifests from the VM image and activates only the configured ones.
Server Configuration¶
When extensions is omitted from the config, no extensions are activated.
Extension Manifests¶
Each extension provides a manifest file in /etc/shed-extensions.d/:
# /etc/shed-extensions.d/ssh-agent.yaml
namespace: ssh-agent
systemd_unit: shed-ext-ssh-agent.service
description: SSH agent forwarding via shed message bus
| Field | Required | Description |
|---|---|---|
namespace |
Yes | Plugin namespace (must match server config) |
systemd_unit |
Yes | Systemd unit to enable |
description |
No | Human-readable description |
Activation Flow¶
- Server sends the enabled extensions list in the health handshake
- Agent reads manifests from
/etc/shed-extensions.d/ - Agent runs
systemctl enable --nowfor each enabled extension - Extensions not in the enabled list remain inactive
Extensions are enable-only — they persist until VM restart. Runtime disabling is not supported.
Extension Health¶
The agent periodically checks each enabled extension at two levels:
- guest: Is the systemd service running? (
systemctl is-active) - host: Can the extension reach the host agent end-to-end? (bus ping round-trip)
Health status is included in heartbeats and visible via shed list -vv:
Naming Convention¶
| Component | Pattern | Example |
|---|---|---|
| Guest binary | shed-ext-<namespace> |
shed-ext-ssh-agent |
| Systemd unit | shed-ext-<namespace>.service |
shed-ext-ssh-agent.service |
| Manifest | <namespace>.yaml |
ssh-agent.yaml |
| Host binary | shed-ext-<purpose>-host |
shed-ext-credentials-host |
SDK¶
The github.com/charliek/shed/sdk Go module provides shared types and clients for building extensions:
sdk.Envelope— Universal message formatsdk.BusClient— Guest-side HTTP publisher to shed-agentsdk.HostClient— Host-side SSE subscriber to shed-server
import "github.com/charliek/shed/sdk"
// Guest-side: publish a request
bus := sdk.NewBusClient(sdk.DefaultPublishURL, sdk.DefaultBusTimeout)
resp, err := bus.Publish(ctx, "my-namespace", payload)
// Host-side: subscribe to messages
host := sdk.NewHostClient(sdk.WithServerURL("http://localhost:8080"))
ch := host.Subscribe(ctx, "my-namespace")
for env := range ch {
// handle message, send response
host.Respond(ctx, "my-namespace", responseEnv)
}
shed-extensions¶
The experimental image variant comes with shed-extensions pre-installed, providing SSH agent forwarding and AWS credential proxying. Create a shed with --image experimental, enable extensions in your server config, and run the host agent to enable credential brokering.
shed-extensions is a concrete implementation built on this plugin bus. See the shed-extensions documentation for credential brokering setup and usage.
Use Cases¶
1Password CLI proxy¶
Run op commands from inside a VM using host credentials:
- Host runs a listener subscribed to
opnamespace - VM has an
opshim that publishes requests toopnamespace - Host listener runs the real
opCLI and returns the result
Dynamic proxy registration¶
A reverse proxy on the host listens to a proxy namespace. Sheds send event messages to register routes:
{
"namespace": "proxy",
"type": "event",
"payload": {"action": "register", "hostname": "myapp.dev", "port": 8080}
}
The proxy extension uses the Connect API to route traffic into VMs via DialService. See the proxy integration design for the full architecture.
Activity monitoring¶
A monitoring process subscribes to a monitor namespace. Sheds publish activity events, and the monitor triggers notifications (desktop alerts, Slack messages) based on configurable rules.
Reserved Namespaces¶
Namespaces prefixed with system: are reserved for shed internal use and cannot be registered by external listeners. Currently used:
| Namespace | Purpose |
|---|---|
system:health |
Agent health checks during VM startup |
Limitations¶
- One listener per namespace (additional registrations are rejected)
- Guest-side subscribe (receiving host-initiated messages) is not yet supported
- The agent HTTP endpoint uses plain HTTP (HTTPS support planned)