{
  "markdown": "# Positif — Agent Switchboard (`sbd`)\n\n<!-- mcp-name: ai.positif/positif -->\n\n> **Positif** (`positif.ai`) is the canonical product name. `sbd` is the CLI, and\n> `switchboard` / `POSITIF_*` remain the stable **technical namespace** (package, env\n> vars, `pos1.` token prefix, `/opt/positif-*` paths, the `positif.service`\n> unit). Those identifiers stay put — renaming them is a separate, box-coordinated\n> migration — but the product you're reading about is **Positif**.\n>\n> **Status:** `0.6.0` pre-alpha. This repository is release-preparation work,\n> not a stable v1 release. Public distribution and release publication still\n> require human release authorization.\n\n**The security-first agent control plane** — the governance + routing layer for a\nmulti-agent fleet. Not \"another orchestrator\": it authenticates the caller\n(tailnet whois), enforces a metadata-only policy, dedupes, **routes** (explicit /\nby capability), **brokers loop-guarded handoff** between agents, rate-limits, and\n**audits every decision** — then gets out of the data path. It does not own memory\n(that is **Bourdon**) and does not execute work (that is the agents); it governs\n*who can reach whom, with what, and proves it happened.*\n\nSibling to **Bourdon** in one ecosystem — Bourdon = what the fleet *knows*,\nPositif = who's *online* + what's *in flight*. They meet at exactly one MCP\nboundary.\n\nLives on the always-on switchboard host (service data under `/var/lib/positif`),\nmirroring Bourdon's layout. Port **7600** (Bourdon = 7500).\n\n## 0.2.0 Pre-Alpha: What's Built\n\n`sbd` is a FastMCP server exposing one secure tool, `dispatch(target, payload)`,\nover streamable-HTTP bound to the tailnet IP. The HTTP transport is deliberate:\nit's what makes `whois`-gated auth real — the caller's peer IP comes from the\nlive request and tailscaled turns it into an authoritative node identity.\n\n**Dispatch state machine** (`core/dispatch.py`):\n\n1. **whois** the caller's peer IP → node + tags + user (unknown ⇒ default-deny)\n2. **provenance** tag — `trusted | tainted` (caller may self-declare tainted)\n3. **target card** lookup (A2A Agent Card + reachability), unknown ⇒ deny\n4. **policy** check — static `(caller, target)` table, **metadata only, never the\n   payload**; `tainted` is forbidden from sensitive targets\n   (`write/spend/prod/privileged`)\n5. **idempotency** — dedup-hit returns the cached result, never re-dispatches\n6. **shadow classifier** — predicts + logs the `(intent → target)` decision but\n   **does not route** (the event log is the training set)\n7. **forward** — over the card's transport (`acp` → drives `hermes acp`; `http` →\n   POSTs to an HTTP worker, ready for a local assistant / `*-automations` worker)\n8. **log + cache** the decision\n\nA per-caller **token-bucket rate limit** runs before forwarding (caps cost/DoS\nfrom a runaway or compromised caller). Observability: the `health` MCP tool and\n`sbd stats` aggregate the event log over a default 7-day window (per-target\ncounts, deny rate, latency p50/p95). Use `--all-time` only when an operator\nintentionally needs the full retained dataset.\n\nRouting is **explicit-target** today. The classifier is scaffolded and learning\nfrom day one.\n\n### Reaching Hermes (ACP)\n\nHermes is delegated to over the Zed Agent Client Protocol (newline-delimited\nJSON-RPC over stdio) — the Bourdon-aware path. `core/acp_client.py` spawns\n`hermes acp --accept-hooks`, runs `initialize → session/new → session/prompt`,\naccumulates `agent_message_chunk` text, auto-answers any `session/request_permission`\n(else the turn deadlocks), and returns on `stopReason: end_turn`.\n\n## Layout\n\n```\ncore/  db schema.sql whois policy policy_studio idempotency classifier classifier_pipeline cards acp_client dispatch server\n      default_data/ packaged policy + card seeds used by installed `sbd init`\ncli/   main.py            (sbd serve | init | policy-lint | policy-diff | policy-simulate | ...)\ndata/  editable mirror/override seeds for source-tree operators\ndeploy/ positif.service\nscripts/ smoke_dispatch.py     tests/ test_dispatch.py\n```\n\n## Operator Feature Notes\n\nDraft feature notes live at:\n\n- `docs/features/README.md`\n- `docs/features/policy-simulator.md`\n- `docs/features/policy-change-studio.md`\n- `docs/features/dry-run-dispatch.md`\n- `docs/features/agent-registry.md`\n- `docs/features/audit-reporting.md`\n- `docs/features/classifier-promotion.md`\n- `docs/features/sandbox-manager.md`\n- `docs/features/retention-automation.md`\n- `docs/features/presence-heartbeat.md`\n- `docs/features/release-readiness.md`\n- `docs/features/release-evidence.md`\n- `docs/features/ha-durability.md`\n- `docs/features/render-surfaces.md`\n- `docs/features/operator-tui.md`\n\n`sbd render` emits JSON Render flat specs for those operator surfaces. This is\nthe renderer-ready contract for operator clients, not a release authorization\npath:\n\n```bash\nsbd render command-center --db /var/lib/positif/positif.db\nsbd render agent-detail --db /var/lib/positif/positif.db\nsbd render audit --db /var/lib/positif/positif.db --window-days 7\nsbd render audit-detail --db /var/lib/positif/positif.db --window-days 7\nsbd render retention --db /var/lib/positif/positif.db\nsbd render release --root /opt/positif-src\n```\n\nEvery database-backed render surface requires explicit `--db`; `release` is the\nonly render surface that may run from `--root` without a database path.\n\nHeartbeat probes update bounded agent presence metadata without dispatching\nwork:\n\n```bash\nsbd heartbeat --db /var/lib/positif/positif.db --target hermes --timeout-ms 1500\nsbd heartbeat --db /var/lib/positif/positif.db --all --timeout-ms 1500\nsbd agents --db /var/lib/positif/positif.db --stale-after 300 --json\n```\n\nRelease evidence packs collect local review artifacts without authorizing,\ntagging, publishing, or deploying anything. The manifest always records\n`release_authorized: false`:\n\n```bash\nsbd release-evidence --root /opt/positif-src --output /var/lib/positif/release-evidence\nsbd release-evidence --root /opt/positif-src --output /var/lib/positif/release-evidence --run-checks --force\n```\n\nHA durability checks start with local SQLite backup and read-only restore\nvalidation only. Phase A is local backup/restore; Phase B is a future\nsecond-node dry run; Phase C is Litestream/B2/NATS or other external replication\nonly after separate third-party review. This feature adds no B2, NATS,\nLitestream config, credentials, bucket names, endpoints, or external replication\ncalls:\n\n```bash\nsbd backup --db /var/lib/positif/positif.db --output /var/lib/positif/backups\nsbd restore-check --backup /var/lib/positif/backups/positif-20260630T000000Z.sqlite --root /opt/positif-src --json\nsbd durability-report --db /var/lib/positif/positif.db --backup-dir /var/lib/positif/backups --json\n```\n\nPolicy Change Studio reviews candidate policy JSON before seeding. Lint, diff,\nand simulation are review-only: they do not mutate the live database, do not log\nevents, do not claim idempotency rows, and do not call target transports.\n\n```bash\nsbd policy-lint --policy ./policy.candidate.json\nsbd policy-diff --db /var/lib/positif/positif.db --policy ./policy.candidate.json --json\nsbd policy-simulate --db /var/lib/positif/positif.db --policy ./policy.candidate.json --caller-node pc.example.ts.net --target hermes --payload \"diagnose\" --provenance trusted --json\n```\n\nClassifier promotion reporting and assist-mode recommendations stay advisory.\n`classifier-report` reads shadow prediction metadata, while `classifier-assist`\nreturns a target only when the caller is already policy-authorized for that\ntarget. Primary classifier routing remains out of scope.\n\n```bash\nsbd classifier-report --db /var/lib/positif/positif.db --window-days 7 --json\nsbd classifier-assist --db /var/lib/positif/positif.db --intent \"review this patch\" --caller-node pc.example.ts.net --provenance trusted --json\n```\n\nAn optional local Ink renderer lives in `tui/` for the same surfaces:\n\n```bash\nnpm --prefix tui ci\nnpm --prefix tui test\nnpm --prefix tui run build\nnode tui/dist/index.js --surface command-center --db /var/lib/positif/positif.db\nnode tui/dist/index.js --surface agents --db /var/lib/positif/positif.db --status error\nnode tui/dist/index.js --surface release --root /opt/positif-src --once\n```\n\nThe TUI supports `command-center`, `agents`, `agent-detail`, `audit`,\n`audit-detail`, `metrics`, `retention`, and `release` JSON Render specs. It is\nread-only: it does not dispatch, prune, mutate policy, generate release\nevidence, authorize release, call external services, or emit analytics.\n\n## Run\n\n```bash\n# on the box\n/opt/positif-venv/bin/python -m pip install -e /opt/positif-src\nopenssl rand -base64 32 > /var/lib/positif/handoff.secret\nchown switchboard:switchboard /var/lib/positif/handoff.secret\nchmod 600 /var/lib/positif/handoff.secret\nsbd init --db /var/lib/positif/positif.db          # schema + seed policy + cards\nsbd serve --transport http --host 127.0.0.1 --port 7600 --db /var/lib/positif/positif.db\n# in another shell (on the box, so whois resolves a real tailnet node):\n/opt/positif-venv/bin/python scripts/smoke_dispatch.py\n# add --yes-live only when you intentionally want to spawn the target agent:\n/opt/positif-venv/bin/python scripts/smoke_dispatch.py --yes-live\n```\n\nWire a coding agent as an MCP client by pointing it at\n`http://127.0.0.1:7600/mcp/`.\n\n## Audit retention\n\nPositif bounds and redacts operator-facing metadata before storage or export.\n`event_log.intent` and `event_log.error` are stored as short redacted strings, and\n`sbd events` / `sbd training` re-apply the same bounds when reading older rows.\nStats, metrics, classifier accuracy, and training export default to the last 7\ndays; pass `--all-time` for an explicit full retained read.\n\nRetention is manual, not automatic during dispatch:\n\n```bash\nsbd retention-report --db /var/lib/positif/positif.db --events-days 30 --idempotency-hours 24 --json\nsbd prune --db /var/lib/positif/positif.db --events-days 30 --idempotency-hours 24 --batch-size 1000\n```\n\nThe default policy keeps audit events for 30 days and idempotency cache rows for\n24 hours. Prune deletes in bounded batches so operators can run it from cron or\nsystemd timers without putting cleanup work on the request path.\n\nReviewed systemd templates are included at `deploy/positif-prune.service` and\n`deploy/positif-prune.timer`. A human operator must review and install them on\nthe target host before enabling the timer.\n\n## Security Stance (0.2.0 Pre-Alpha)\n\n* Routes on **metadata, never payload**; **no token minting or forwarding** for\n  caller identity (Hermes uses its own key). Positif mints only short-lived\n  signed handoff bearers for trace/depth/path continuity.\n* **Caller identity is asserted on the tailnet** — the peer IP must be in the\n  Tailscale CGNAT range (`100.64.0.0/10`) before whois is even consulted, so a\n  loopback/proxy/public peer can't be mistaken for a fleet node. Default-deny on\n  unknown caller / unknown target / no policy row (a targeted `deny` overrides a\n  broader `allow`).\n* `tainted` (untrusted-origin) dispatches are denied from privileged targets at\n  ingress, and are additionally bounded to non-destructive tools.\n* **Fully unprivileged: no root, no sudo.** `sbd` runs as the\n  `switchboard` user, and the Hermes it drives runs as that *same* unprivileged\n  user with its own private `HERMES_HOME` (`/var/lib/positif/hermes-home` —\n  config + Anthropic key, **no Slack tokens**). A compromise of `sbd` or its Hermes\n  yields only the `switchboard` user: it **cannot read `/root`, touch system files,\n  or escalate**. The gateway's separate root Hermes instance is untouched.\n  `deploy/preflight.sh` refuses to start unless the `HERMES_HOME` is present,\n  private, and switchboard-owned (and `hermes` is root-owned); an in-code argv\n  allowlist in `acp_client` is the second barrier on the spawn.\n* **Per-dispatch tool bound + audit (allowlist, fail-closed).** Every tool the\n  target routes through the ACP permission gate is logged (`event_log.tool_audit`,\n  preserved even on error/timeout turns). `compute` (the **default** for Hermes;\n  forced for `tainted`; can be narrowed-to via `max_capability`) is an *allowlist*\n  — only `read/search/fetch/think` pass; `edit/delete/move/execute` and any\n  absent/unknown kind are **denied**. **Verified:** under `compute`, Hermes was\n  denied a file edit *and* a dangerous `rm` at the gate, while Bourdon MCP tools +\n  Q&A still work. Truly benign commands (e.g. `whoami`) auto-run in Hermes and skip\n  the gate — but since Hermes now runs **unprivileged**, even those can't read\n  `/root`, touch system files, or escalate, so the residual is contained.\n* **Security selectors fail closed** — `provenance` and `max_capability` normalize\n  and resolve any typo/casing to the *more restrictive* branch, never the privileged\n  one. A cap can only narrow.\n* **Idempotency keys are namespaced** by caller+target — a caller-supplied key\n  can't read another caller's or another target's cached result.\n* **Spawn cleanup is process-group aware.** ACP and CLI agent launches create a\n  subprocess group/session where the platform supports it. On timeout,\n  Positif terminates the group, then kills it if it does not exit; non-POSIX\n  platforms fall back to best-effort direct child cleanup.\n* **CLI review runners must not hide write capability.** The Codex card runs\n  `codex exec --skip-git-repo-check` without\n  `--dangerously-bypass-approvals-and-sandbox`, is marked `compute`, and is a\n  review/reasoning node rather than an autonomous writer. A future write-capable\n  Codex runner must be a separate, explicitly named card.\n* **Per-agent Unix users are a deploy design, not a hidden runtime shortcut.**\n  Future `sbd-hermes`, `sbd-coder`, and `sbd-codex` execution must go through\n  exact root-owned wrapper commands before code enables it. Broad sudo is not\n  acceptable. Until that runner exists, preflight enforces private\n  switchboard-owned homes/workspaces for the seeded agents.\n* **Heartbeat probes are metadata-only.** `sbd heartbeat` never dispatches\n  payloads, never runs prompts, and never creates agent work sessions. CLI probes\n  check only configured filesystem paths, ACP probes stay session-free, and HTTP\n  probes use only explicit card health URLs with enforced timeouts. Probe errors\n  are bounded and redacted before storage.\n* **Signed handoff metadata.** Brokered handoff trace IDs, depths, and paths are\n  carried as `Authorization: Bearer pos1.<payload>.<hmac>` tokens signed with\n  `POSITIF_HANDOFF_SECRET_FILE` (default deploy path:\n  `/var/lib/positif/handoff.secret`). Unsigned `X-SBD-*` headers are ignored\n  by default; set `POSITIF_ALLOW_LEGACY_HANDOFF=1` only for a temporary compatibility\n  window. Handoff tokens authenticate metadata only; payload bodies are never used\n  to derive or validate handoff identity.\n\n## Roadmap (Not Yet Built)\n\nSecond-node dry runs, Litestream/B2 durability, capability-match + semantic\nrouting as primary, NATS, brokered agent-to-agent handoff, a second `sbd` to\nkill the SPOF. Litestream, B2, NATS, and other external replication services\nrequire separate third-party review before implementation.\n\n---\nRADLAB LLC, a Wyoming limited liability company · BUSL-1.1",
  "bytes": 15225,
  "sha": "a029826fcb466697941b761341d8c2ecfe97c6736a1f89700c7149f24e594642",
  "repo_slug": "",
  "fonte": "pypi",
  "truncated": false,
  "api": "https://agentalog.com/api/listings/mcp_ai_positif_positif_1370be9c/readme"
}