Metadata-Version: 2.4
Name: positif-ai
Version: 0.2.1
Summary: Positif — Agent Switchboard: security-first inter-agent control plane (sbd) for the RADLAB fleet
Author-email: Ryan Davis <ryandavispro1@gmail.com>
Maintainer: RADLAB LLC
License-Expression: BUSL-1.1 AND MIT
Project-URL: Homepage, https://positif.ai
Project-URL: Repository, https://gitlab.com/radlab1/positif/positif
Project-URL: Documentation, https://positif.ai/docs/
Keywords: agent,router,switchboard,mcp,a2a,acp,tailnet
Classifier: Development Status :: 2 - Pre-Alpha
Classifier: Intended Audience :: Developers
Classifier: Operating System :: POSIX :: Linux
Classifier: Programming Language :: Python :: 3
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
License-File: LICENSE-MIT
License-File: NOTICE
Requires-Dist: fastmcp>=3.0
Requires-Dist: httpx>=0.27
Provides-Extra: dev
Requires-Dist: pytest>=8.0; extra == "dev"
Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
Requires-Dist: ruff>=0.4; extra == "dev"
Dynamic: license-file

# Positif — Agent Switchboard (`sbd`)

<!-- mcp-name: ai.positif/positif -->

> **Positif** (`positif.ai`) is the canonical product name. `sbd` is the CLI, and
> `switchboard` / `POSITIF_*` remain the stable **technical namespace** (package, env
> vars, `pos1.` token prefix, `/opt/positif-*` paths, the `positif.service`
> unit). Those identifiers stay put — renaming them is a separate, box-coordinated
> migration — but the product you're reading about is **Positif**.
>
> **Status:** `0.2.0` pre-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`):

1. **whois** the caller's peer IP → node + tags + user (unknown ⇒ default-deny)
2. **provenance** tag — `trusted | tainted` (caller may self-declare tainted)
3. **target card** lookup (A2A Agent Card + reachability), unknown ⇒ deny
4. **policy** check — static `(caller, target)` table, **metadata only, never the
   payload**; `tainted` is forbidden from sensitive targets
   (`write/spend/prod/privileged`)
5. **idempotency** — dedup-hit returns the cached result, never re-dispatches
6. **shadow classifier** — predicts + logs the `(intent → target)` decision but
   **does not route** (the event log is the training set)
7. **forward** — over the card's transport (`acp` → drives `hermes acp`; `http` →
   POSTs to an HTTP worker, ready for Clyde / a `*-automations` worker)
8. **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.md`
- `docs/features/policy-simulator.md`
- `docs/features/policy-change-studio.md`
- `docs/features/dry-run-dispatch.md`
- `docs/features/agent-registry.md`
- `docs/features/audit-reporting.md`
- `docs/features/classifier-promotion.md`
- `docs/features/sandbox-manager.md`
- `docs/features/retention-automation.md`
- `docs/features/presence-heartbeat.md`
- `docs/features/release-readiness.md`
- `docs/features/release-evidence.md`
- `docs/features/ha-durability.md`
- `docs/features/render-surfaces.md`
- `docs/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:

```bash
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:

```bash
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`:

```bash
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:

```bash
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.

```bash
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.

```bash
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:

```bash
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

```bash
# 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:

```bash
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 targeted `deny` overrides a
  broader `allow`).
* `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.** `sbd` runs as the
  `switchboard` user, and the Hermes it drives runs as that *same* unprivileged
  user with its own private `HERMES_HOME` (`/opt/positif-data/hermes-home` —
  config + Anthropic key, **no Slack tokens**). A compromise of `sbd` or its Hermes
  yields only the `switchboard` user: it **cannot read `/root`, touch system files,
  or escalate**. The gateway's separate root Hermes instance is untouched.
  `deploy/preflight.sh` refuses to start unless the `HERMES_HOME` is present,
  private, and switchboard-owned (and `hermes` is root-owned); an in-code argv
  allowlist in `acp_client` is 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 for `tainted`; can be narrowed-to via `max_capability`) is an *allowlist*
  — only `read/search/fetch/think` pass; `edit/delete/move/execute` and any
  absent/unknown kind are **denied**. **Verified:** under `compute`, Hermes was
  denied a file edit *and* a dangerous `rm` at 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** — `provenance` and `max_capability` normalize
  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-check` without
  `--dangerously-bypass-approvals-and-sandbox`, is marked `compute`, 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`, and `sbd-codex` execution 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 heartbeat` never 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 with
  `POSITIF_HANDOFF_SECRET_FILE` (default deploy path:
  `/opt/positif-data/handoff.secret`). Unsigned `X-SBD-*` headers are ignored
  by default; set `POSITIF_ALLOW_LEGACY_HANDOFF=1` only 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
