Skip to content
🛡️ adminadmin@example.com · Domain admin in Acme Corp

Chapter 14 — The real agent

Hand it to the software written to do it

Chapter 13 made you speak the node contract by hand.

This chapter gives that same contract to the released plexd agent — one container, on your host, outside the cluster — and you stay in the operator's chair and watch.

bash
TOKEN=$(plexctl bootstrap-token issue \
  --project "$PROJECT_ID" --kind node --env-prefix dev --ttl 1h \
  --output json | jq -r '.token')

Same token type. Same seam. Different hands.

Give it a config file

bash
mkdir -p agent-logs
cat > plexd-config.yaml <<'EOF'
actions:
  enabled: false

log_fwd:
  file_patterns:
    - /var/log/agent/*.log
EOF

log_fwd.file_patterns gives the agent a log source — every line appended to a matching file becomes a candidate for the ingest pipeline. That is the seam this chapter closes at the end.

actions.enabled: false keeps the remote-execution kill switch off. With a config file present, an unset value means on.

One command

bash
docker run --detach --name plexd-agent \
  --add-host=host.docker.internal:host-gateway \
  --user 0:0 \
  --cap-add NET_ADMIN \
  --volume "$PWD/plexd-config.yaml:/etc/plexd/config.yaml:ro" \
  --volume "$PWD/agent-logs:/var/log/agent" \
  --env PLEXD_API=http://host.docker.internal:8080 \
  --env PLEXD_PROJECT_ID="$PROJECT_ID" \
  --env PLEXD_RESOURCE_HANDLE=edge-agent-01 \
  --env PLEXD_REQUESTED_RESOURCE_ID=edge-agent-01 \
  --env PLEXD_BOOTSTRAP_TOKEN="$TOKEN" \
  --env PLEXD_POLICY_ENABLED=false \
  --env PLEXD_LOG_LEVEL=debug \
  ghcr.io/plexsphere/plexd:latest up

--user 0:0 --cap-add NET_ADMIN is what makes the data plane real. The two travel together: the image runs as 65534 by default, and a capability only reaches a non-root process if the binary carries file capabilities — which the distroless build does not.

No /dev/net/tun. No --privileged.

Under a second

bash
docker logs plexd-agent 2>&1 | head -10
text
time=2026-08-03T19:19:19.010Z level=INFO msg="starting plexd" version=v0.6.0 mode=node actions_enabled=false
time=2026-08-03T19:19:19.663Z level=INFO msg="registration successful" component=registration node_id=019fc910-f565-7d55-9258-61595da2c8ab mesh_ip=10.50.0.5
time=2026-08-03T19:19:19.663Z level=INFO msg=registered node_id=019fc910-f565-7d55-9258-61595da2c8ab mesh_ip=10.50.0.5
time=2026-08-03T19:19:19.663Z level=DEBUG msg="netlink interface created" component=wireguard interface=plexd0
time=2026-08-03T19:19:19.664Z level=INFO msg="wireguard interface created" component=wireguard interface=plexd0 listen_port=51820
time=2026-08-03T19:19:19.664Z level=DEBUG msg="address configured" component=wireguard interface=plexd0 address=10.50.0.5/24
time=2026-08-03T19:19:19.664Z level=INFO msg="wireguard interface configured" component=wireguard interface=plexd0 listen_port=51820 mesh_ip=10.50.0.5

Every single thing chapter 13 made you do by hand — keypair, register body, token redemption, keeping the node secret key — happened on its own, in about a second.

And then one millisecond later it did something you could not do by hand: it programmed a kernel WireGuard interface.

Capture the Node id

bash
NODE_ID=$(docker logs plexd-agent 2>&1 | grep -m1 'msg=registered' \
  | grep -o 'node_id=[^ ]*' | cut -d= -f2)
echo "$NODE_ID"

Read the mesh as the operator

bash
plexctl peer list --domain "$DOMAIN_ID"
text
NODE_ID                               MESH_IP    REACHABILITY
019fc90c-89a7-7530-b993-aa941d12c480  10.50.0.1  healthy
019fc90c-fcab-7b79-854b-cd39aa624d6e  10.50.0.2  healthy
019fc90f-e597-76bc-b11f-8f0508dc944f  10.50.0.3  never_reported
019fc90f-e9e5-7b45-acb7-28f5f0e30918  10.50.0.4  never_reported
019fc910-f565-7d55-9258-61595da2c8ab  10.50.0.5  healthy

.1 and .2 are the dev stack's own in-cluster agents — they enrolled before you ran a single command. .3 and .4 are chapter 13's. .5 is the one you just started.

The rows that deserve a pause

Three read healthy. Two read never_reported: chapter 13's Nodes, which have never sent a heartbeat.

Reachability is heartbeat history, not a liveness probe.

Your agent's healthy is earned: it heartbeats every 30 seconds and the platform admits each one.

The two verdicts ask for different work. never_reported says the agent was never started. stale and unreachable say a working Node went quiet.

healthystale after 1m30s → unreachable after 5m, per Domain configuration. A Node reaches either only after it has reported once.

The agent's own words

bash
plexctl state get --node "$NODE_ID"
text
REPORTS
KEY                  VALUE
status.mesh          {"interface":"plexd0","peer_count":4,"listen_port":51820}
status.bridge        {"enabled":false,...}
status.ingress       {"enabled":false,"rule_count":0,...}

Metadata and data are empty — those are the operator's buckets and you wrote nothing.

Reports is the agent's outbound channel. Every status.* key was authored and pushed by the agent; no operator wrote a byte.

peer_count: 4

The four other mesh members from the previous slide, each programmed onto the kernel interface with its public key and its mesh address.

Chapter 13's curl calls put those keys into the inventory. The agent's reconcile loop pulled them back out and wrote them into the kernel.

Be precise about what this is: programmed peers, not live tunnels. A WireGuard handshake also needs a reachable endpoint per peer, and this single-host topology offers none.

What you are reading is everything the control plane can program from inventory alone — key material, addressing, topology — delivered to a kernel interface within a second of registration.

Now the last loop

bash
echo "line one from the operator's shell" >> agent-logs/app.log

A line, in a file, on your laptop.

bash
docker logs plexd-agent 2>&1 | grep 'logs reported'
text
time=2026-08-03T19:19:49.705Z level=DEBUG msg="logs reported to platform" component=logfwd accepted_at=2026-08-03T19:19:49.704Z

A receipt, not an attempt. The agent logs this line only after the platform accepted the batch, and accepted_at is the platform's own timestamp.

And back out again

bash
FROM=$(date -u -v-15M +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || date -u -d '-15 min' +%Y-%m-%dT%H:%M:%SZ)

plexctl logs query --domain "$DOMAIN_ID" \
  --query '{node="'"$NODE_ID"'"}' \
  --from "$FROM" --to "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
  --output json | jq -r '.body | fromjson | .data.result[].values[][1]'
text
{"severity":"info","unit":"/var/log/agent/app.log","hostname":"38f770594dc1","message":"line one from the operator's shell","timestamp":"2026-08-03T19:19:49.691926298Z"}

There it is.

The source file as unit, the container id as hostname, your text as message — selected by the Node label the routing layer stamped on it, through the same mediated query chapter 12 introduced.

The window is the last fifteen minutes up to now, computed on the spot. Give the pipeline up to a minute end to end.

The other pipelines

bash
docker logs plexd-agent 2>&1 | grep 'metrics reported'
text
time=2026-08-03T19:20:19.897Z level=DEBUG msg="metrics reported to platform" component=metrics accepted_at=2026-08-03T19:20:19.896Z

Metrics posts on its own 60-second timer, same accepted-only receipts.

Audit has one candidate and drops it: the ingest contract admits only auditd and k8s, and the agent's own process-start entry is neither — so it is discarded client-side rather than posted for the platform to refuse.

An agent that declines to send what would be rejected is an agent that knows the contract.

What you just watched

A container with one credential became a Node with an identity, an address, a programmed data plane, and a telemetry pipeline the platform accepts — in about a second, with no human in the loop after docker run.

Every step of it was something you did by hand two chapters ago.

And every step of it — the token issuance, the registration, the session grants, the credential approval — is sitting in the hash-chained audit log you can recompute.