Positif — Agent Switchboard: security-first inter-agent control plane (sbd) for the RADLAB fleet
Project description
Positif — Agent Switchboard (sbd)
Positif (
positif.ai) is the canonical product name.sbdis the CLI, andswitchboard/POSITIF_*remain the stable technical namespace (package, env vars,pos1.token prefix,/opt/positif-*paths, thepositif.serviceunit). Those identifiers stay put — renaming them is a separate, box-coordinated migration — but the product you're reading about is Positif.Status:
0.2.0pre-alpha. This repository is release-preparation work, not a stable v1 release. Public distribution, deployment outside RADLAB, or release publication still requires human release authorization under RADLAB governance.
The security-first agent control plane — the governance + routing layer for a multi-agent fleet. Not "another orchestrator": it authenticates the caller (tailnet whois), enforces a metadata-only policy, dedupes, routes (explicit / by capability), brokers loop-guarded handoff between agents, rate-limits, and audits every decision — then gets out of the data path. It does not own memory (that is Bourdon) and does not execute work (that is the agents); it governs who can reach whom, with what, and proves it happened.
Sibling to Bourdon in one ecosystem — Bourdon = what the fleet knows,
Positif = who's online + what's in flight. They meet at exactly one MCP
boundary. (Positioning + productization path: claude-brain SWITCHBOARD_PRODUCTIZATION.md.)
Lives on the always-on fleet-box box (/opt/positif-{src,venv,data}),
mirroring Bourdon's layout. Port 7600 (Bourdon = 7500).
0.2.0 Pre-Alpha: What's Built
sbd is a FastMCP server exposing one secure tool, dispatch(target, payload),
over streamable-HTTP bound to the tailnet IP. The HTTP transport is deliberate:
it's what makes whois-gated auth real — the caller's peer IP comes from the
live request and tailscaled turns it into an authoritative node identity.
Dispatch state machine (core/dispatch.py):
- whois the caller's peer IP → node + tags + user (unknown ⇒ default-deny)
- provenance tag —
trusted | tainted(caller may self-declare tainted) - target card lookup (A2A Agent Card + reachability), unknown ⇒ deny
- policy check — static
(caller, target)table, metadata only, never the payload;taintedis forbidden from sensitive targets (write/spend/prod/privileged) - idempotency — dedup-hit returns the cached result, never re-dispatches
- shadow classifier — predicts + logs the
(intent → target)decision but does not route (the event log is the training set) - forward — over the card's transport (
acp→ driveshermes acp;http→ POSTs to an HTTP worker, ready for Clyde / a*-automationsworker) - log + cache the decision
A per-caller token-bucket rate limit runs before forwarding (caps cost/DoS
from a runaway or compromised caller). Observability: the health MCP tool and
sbd stats aggregate the event log over a default 7-day window (per-target
counts, deny rate, latency p50/p95). Use --all-time only when an operator
intentionally needs the full retained dataset.
Routing is explicit-target today. The classifier is scaffolded and learning from day one.
Reaching Hermes (ACP)
Hermes is delegated to over the Zed Agent Client Protocol (newline-delimited
JSON-RPC over stdio) — the Bourdon-aware path. core/acp_client.py spawns
hermes acp --accept-hooks, runs initialize → session/new → session/prompt,
accumulates agent_message_chunk text, auto-answers any session/request_permission
(else the turn deadlocks), and returns on stopReason: end_turn.
Layout
core/ db schema.sql whois policy policy_studio idempotency classifier classifier_pipeline cards acp_client dispatch server
default_data/ packaged policy + card seeds used by installed `sbd init`
cli/ main.py (sbd serve | init | policy-lint | policy-diff | policy-simulate | ...)
data/ editable mirror/override seeds for source-tree operators
deploy/ positif.service
scripts/ smoke_dispatch.py tests/ test_dispatch.py
Operator Feature Notes
Draft feature notes live at:
docs/features/README.mddocs/features/policy-simulator.mddocs/features/policy-change-studio.mddocs/features/dry-run-dispatch.mddocs/features/agent-registry.mddocs/features/audit-reporting.mddocs/features/classifier-promotion.mddocs/features/sandbox-manager.mddocs/features/retention-automation.mddocs/features/presence-heartbeat.mddocs/features/release-readiness.mddocs/features/release-evidence.mddocs/features/ha-durability.mddocs/features/render-surfaces.mddocs/features/operator-tui.md
sbd render emits JSON Render flat specs for those operator surfaces. This is
the renderer-ready contract for operator clients, not a release authorization
path:
sbd render command-center --db /opt/positif-data/positif.db
sbd render agent-detail --db /opt/positif-data/positif.db
sbd render audit --db /opt/positif-data/positif.db --window-days 7
sbd render audit-detail --db /opt/positif-data/positif.db --window-days 7
sbd render retention --db /opt/positif-data/positif.db
sbd render release --root /opt/positif-src
Every database-backed render surface requires explicit --db; release is the
only render surface that may run from --root without a database path.
Heartbeat probes update bounded agent presence metadata without dispatching work:
sbd heartbeat --db /opt/positif-data/positif.db --target hermes --timeout-ms 1500
sbd heartbeat --db /opt/positif-data/positif.db --all --timeout-ms 1500
sbd agents --db /opt/positif-data/positif.db --stale-after 300 --json
Release evidence packs collect local review artifacts without authorizing,
tagging, publishing, or deploying anything. The manifest always records
release_authorized: false:
sbd release-evidence --root /opt/positif-src --output /opt/positif-data/release-evidence
sbd release-evidence --root /opt/positif-src --output /opt/positif-data/release-evidence --run-checks --force
HA durability checks start with local SQLite backup and read-only restore validation only. Phase A is local backup/restore; Phase B is a future second-node dry run; Phase C is Litestream/B2/NATS or other external replication only after separate third-party review. This feature adds no B2, NATS, Litestream config, credentials, bucket names, endpoints, or external replication calls:
sbd backup --db /opt/positif-data/positif.db --output /opt/positif-data/backups
sbd restore-check --backup /opt/positif-data/backups/positif-20260630T000000Z.sqlite --root /opt/positif-src --json
sbd durability-report --db /opt/positif-data/positif.db --backup-dir /opt/positif-data/backups --json
Policy Change Studio reviews candidate policy JSON before seeding. Lint, diff, and simulation are review-only: they do not mutate the live database, do not log events, do not claim idempotency rows, and do not call target transports.
sbd policy-lint --policy ./policy.candidate.json
sbd policy-diff --db /opt/positif-data/positif.db --policy ./policy.candidate.json --json
sbd policy-simulate --db /opt/positif-data/positif.db --policy ./policy.candidate.json --caller-node pc.example.ts.net --target hermes --payload "diagnose" --provenance trusted --json
Classifier promotion reporting and assist-mode recommendations stay advisory.
classifier-report reads shadow prediction metadata, while classifier-assist
returns a target only when the caller is already policy-authorized for that
target. Primary classifier routing remains out of scope.
sbd classifier-report --db /opt/positif-data/positif.db --window-days 7 --json
sbd classifier-assist --db /opt/positif-data/positif.db --intent "review this patch" --caller-node pc.example.ts.net --provenance trusted --json
An optional local Ink renderer lives in tui/ for the same surfaces:
npm --prefix tui ci
npm --prefix tui test
npm --prefix tui run build
node tui/dist/index.js --surface command-center --db /opt/positif-data/positif.db
node tui/dist/index.js --surface agents --db /opt/positif-data/positif.db --status error
node tui/dist/index.js --surface release --root /opt/positif-src --once
The TUI supports command-center, agents, agent-detail, audit,
audit-detail, metrics, retention, and release JSON Render specs. It is
read-only: it does not dispatch, prune, mutate policy, generate release
evidence, authorize release, call external services, or emit analytics.
Run
# on the box
/opt/positif-venv/bin/python -m pip install -e /opt/positif-src
openssl rand -base64 32 > /opt/positif-data/handoff.secret
chown switchboard:switchboard /opt/positif-data/handoff.secret
chmod 600 /opt/positif-data/handoff.secret
sbd init --db /opt/positif-data/positif.db # schema + seed policy + cards
sbd serve --transport http --host 127.0.0.1 --port 7600 --db /opt/positif-data/positif.db
# in another shell (on the box, so whois resolves a real tailnet node):
/opt/positif-venv/bin/python scripts/smoke_dispatch.py
# add --yes-live only when you intentionally want to spawn the target agent:
/opt/positif-venv/bin/python scripts/smoke_dispatch.py --yes-live
Wire a coding agent as an MCP client by pointing it at
http://127.0.0.1:7600/mcp/.
Audit retention
Positif bounds and redacts operator-facing metadata before storage or export.
event_log.intent and event_log.error are stored as short redacted strings, and
sbd events / sbd training re-apply the same bounds when reading older rows.
Stats, metrics, classifier accuracy, and training export default to the last 7
days; pass --all-time for an explicit full retained read.
Retention is manual, not automatic during dispatch:
sbd retention-report --db /opt/positif-data/positif.db --events-days 30 --idempotency-hours 24 --json
sbd prune --db /opt/positif-data/positif.db --events-days 30 --idempotency-hours 24 --batch-size 1000
The default policy keeps audit events for 30 days and idempotency cache rows for 24 hours. Prune deletes in bounded batches so operators can run it from cron or systemd timers without putting cleanup work on the request path.
Reviewed systemd templates are included at deploy/positif-prune.service and
deploy/positif-prune.timer. A human operator must review and install them on
the target host before enabling the timer.
Security Stance (0.2.0 Pre-Alpha)
- Routes on metadata, never payload; no token minting or forwarding for caller identity (Hermes uses its own key). Positif mints only short-lived signed handoff bearers for trace/depth/path continuity.
- Caller identity is asserted on the tailnet — the peer IP must be in the
Tailscale CGNAT range (
100.64.0.0/10) before whois is even consulted, so a loopback/proxy/public peer can't be mistaken for a fleet node. Default-deny on unknown caller / unknown target / no policy row (a targeteddenyoverrides a broaderallow). tainted(untrusted-origin) dispatches are denied from privileged targets at ingress, and are additionally bounded to non-destructive tools.- Fully unprivileged: no root, no sudo.
sbdruns as theswitchboarduser, and the Hermes it drives runs as that same unprivileged user with its own privateHERMES_HOME(/opt/positif-data/hermes-home— config + Anthropic key, no Slack tokens). A compromise ofsbdor its Hermes yields only theswitchboarduser: it cannot read/root, touch system files, or escalate. The gateway's separate root Hermes instance is untouched.deploy/preflight.shrefuses to start unless theHERMES_HOMEis present, private, and switchboard-owned (andhermesis root-owned); an in-code argv allowlist inacp_clientis the second barrier on the spawn. - Per-dispatch tool bound + audit (allowlist, fail-closed). Every tool the
target routes through the ACP permission gate is logged (
event_log.tool_audit, preserved even on error/timeout turns).compute(the default for Hermes; forced fortainted; can be narrowed-to viamax_capability) is an allowlist — onlyread/search/fetch/thinkpass;edit/delete/move/executeand any absent/unknown kind are denied. Verified: undercompute, Hermes was denied a file edit and a dangerousrmat the gate, while Bourdon MCP tools + Q&A still work. Truly benign commands (e.g.whoami) auto-run in Hermes and skip the gate — but since Hermes now runs unprivileged, even those can't read/root, touch system files, or escalate, so the residual is contained. - Security selectors fail closed —
provenanceandmax_capabilitynormalize and resolve any typo/casing to the more restrictive branch, never the privileged one. A cap can only narrow. - Idempotency keys are namespaced by caller+target — a caller-supplied key can't read another caller's or another target's cached result.
- Spawn cleanup is process-group aware. ACP and CLI agent launches create a subprocess group/session where the platform supports it. On timeout, Positif terminates the group, then kills it if it does not exit; non-POSIX platforms fall back to best-effort direct child cleanup.
- CLI review runners must not hide write capability. The Codex card runs
codex exec --skip-git-repo-checkwithout--dangerously-bypass-approvals-and-sandbox, is markedcompute, and is a review/reasoning node rather than an autonomous writer. A future write-capable Codex runner must be a separate, explicitly named card. - Per-agent Unix users are a deploy design, not a hidden runtime shortcut.
Future
sbd-hermes,sbd-coder, andsbd-codexexecution must go through exact root-owned wrapper commands before code enables it. Broad sudo is not acceptable. Until that runner exists, preflight enforces private switchboard-owned homes/workspaces for the seeded agents. - Heartbeat probes are metadata-only.
sbd heartbeatnever dispatches payloads, never runs prompts, and never creates agent work sessions. CLI probes check only configured filesystem paths, ACP probes stay session-free, and HTTP probes use only explicit card health URLs with enforced timeouts. Probe errors are bounded and redacted before storage. - Signed handoff metadata. Brokered handoff trace IDs, depths, and paths are
carried as
Authorization: Bearer pos1.<payload>.<hmac>tokens signed withPOSITIF_HANDOFF_SECRET_FILE(default deploy path:/opt/positif-data/handoff.secret). UnsignedX-SBD-*headers are ignored by default; setPOSITIF_ALLOW_LEGACY_HANDOFF=1only for a temporary compatibility window. Handoff tokens authenticate metadata only; payload bodies are never used to derive or validate handoff identity.
Roadmap (Not Yet Built)
Second-node dry runs, Litestream/B2 durability, capability-match + semantic
routing as primary, NATS, brokered agent-to-agent handoff, a second sbd to
kill the SPOF. Litestream, B2, NATS, and other external replication services
require separate third-party review before implementation.
See claude-brain/PROJECTS/RADLAB-CLOUD/SWITCHBOARD.md.
RADLAB LLC, a Wyoming limited liability company · BUSL-1.1
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file positif_ai-0.2.1.tar.gz.
File metadata
- Download URL: positif_ai-0.2.1.tar.gz
- Upload date:
- Size: 307.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.9
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f7525aca52f4488201f0f50d48af7a22ea47eb8710349c5b996b47091faadcc9
|
|
| MD5 |
1863539d61ba409ac4d5f5a77bf66a19
|
|
| BLAKE2b-256 |
c51dd2358a91fb91c61da2c4a495ba25e1d51c4ee86af542e53a71308713dd80
|
File details
Details for the file positif_ai-0.2.1-py3-none-any.whl.
File metadata
- Download URL: positif_ai-0.2.1-py3-none-any.whl
- Upload date:
- Size: 221.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.9
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b5b2af63c87832a0b0f5275b0b2335beac7e28e5e6543757a47cde43f966b758
|
|
| MD5 |
223ff0a24ede53ea23141aa20c577f47
|
|
| BLAKE2b-256 |
9b89331b754906d9238c782228a420368e9112a81bef26e4e2c85f80dc66e520
|