positif-ai 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- positif_ai-0.1.0/LICENSE +109 -0
- positif_ai-0.1.0/LICENSE-MIT +21 -0
- positif_ai-0.1.0/NOTICE +8 -0
- positif_ai-0.1.0/PKG-INFO +244 -0
- positif_ai-0.1.0/README.md +215 -0
- positif_ai-0.1.0/cli/__init__.py +1 -0
- positif_ai-0.1.0/cli/main.py +583 -0
- positif_ai-0.1.0/core/__init__.py +3 -0
- positif_ai-0.1.0/core/acp_client.py +369 -0
- positif_ai-0.1.0/core/agent_registry.py +92 -0
- positif_ai-0.1.0/core/audit_report.py +127 -0
- positif_ai-0.1.0/core/boardroom.py +177 -0
- positif_ai-0.1.0/core/cards.py +121 -0
- positif_ai-0.1.0/core/classifier.py +92 -0
- positif_ai-0.1.0/core/db.py +92 -0
- positif_ai-0.1.0/core/default_data/cards/coder.card.json +30 -0
- positif_ai-0.1.0/core/default_data/cards/codex.card.json +34 -0
- positif_ai-0.1.0/core/default_data/cards/gemini.card.json +43 -0
- positif_ai-0.1.0/core/default_data/cards/hermes.card.json +35 -0
- positif_ai-0.1.0/core/default_data/cards/research.card.json +28 -0
- positif_ai-0.1.0/core/default_data/policy.seed.json +155 -0
- positif_ai-0.1.0/core/dispatch.py +522 -0
- positif_ai-0.1.0/core/doctor.py +229 -0
- positif_ai-0.1.0/core/explain.py +338 -0
- positif_ai-0.1.0/core/handoff.py +338 -0
- positif_ai-0.1.0/core/idempotency.py +110 -0
- positif_ai-0.1.0/core/metrics.py +238 -0
- positif_ai-0.1.0/core/planner.py +332 -0
- positif_ai-0.1.0/core/policy.py +119 -0
- positif_ai-0.1.0/core/processes.py +86 -0
- positif_ai-0.1.0/core/provenance.py +51 -0
- positif_ai-0.1.0/core/ratelimit.py +75 -0
- positif_ai-0.1.0/core/redaction.py +54 -0
- positif_ai-0.1.0/core/release_check.py +169 -0
- positif_ai-0.1.0/core/render_specs.py +343 -0
- positif_ai-0.1.0/core/retention.py +119 -0
- positif_ai-0.1.0/core/routing.py +56 -0
- positif_ai-0.1.0/core/sandbox_manager.py +114 -0
- positif_ai-0.1.0/core/schema.sql +125 -0
- positif_ai-0.1.0/core/server.py +481 -0
- positif_ai-0.1.0/core/sse.py +45 -0
- positif_ai-0.1.0/core/stats.py +64 -0
- positif_ai-0.1.0/core/transcript.py +151 -0
- positif_ai-0.1.0/core/whois.py +93 -0
- positif_ai-0.1.0/positif_ai.egg-info/PKG-INFO +244 -0
- positif_ai-0.1.0/positif_ai.egg-info/SOURCES.txt +116 -0
- positif_ai-0.1.0/positif_ai.egg-info/dependency_links.txt +1 -0
- positif_ai-0.1.0/positif_ai.egg-info/entry_points.txt +2 -0
- positif_ai-0.1.0/positif_ai.egg-info/requires.txt +7 -0
- positif_ai-0.1.0/positif_ai.egg-info/top_level.txt +3 -0
- positif_ai-0.1.0/pyproject.toml +71 -0
- positif_ai-0.1.0/setup.cfg +4 -0
- positif_ai-0.1.0/switchboard_cli.py +45 -0
- positif_ai-0.1.0/tests/test_agent_registry.py +164 -0
- positif_ai-0.1.0/tests/test_audit_report.py +131 -0
- positif_ai-0.1.0/tests/test_boardroom.py +166 -0
- positif_ai-0.1.0/tests/test_db_ro.py +27 -0
- positif_ai-0.1.0/tests/test_dispatch.py +971 -0
- positif_ai-0.1.0/tests/test_doctor.py +154 -0
- positif_ai-0.1.0/tests/test_explain.py +276 -0
- positif_ai-0.1.0/tests/test_handoff.py +154 -0
- positif_ai-0.1.0/tests/test_idempotency.py +54 -0
- positif_ai-0.1.0/tests/test_init.py +164 -0
- positif_ai-0.1.0/tests/test_metrics.py +128 -0
- positif_ai-0.1.0/tests/test_planner.py +228 -0
- positif_ai-0.1.0/tests/test_processes.py +76 -0
- positif_ai-0.1.0/tests/test_provenance.py +146 -0
- positif_ai-0.1.0/tests/test_release_check.py +121 -0
- positif_ai-0.1.0/tests/test_release_readiness.py +175 -0
- positif_ai-0.1.0/tests/test_render_specs.py +246 -0
- positif_ai-0.1.0/tests/test_retention.py +208 -0
- positif_ai-0.1.0/tests/test_sandbox_manager.py +129 -0
- positif_ai-0.1.0/tests/test_security.py +208 -0
- positif_ai-0.1.0/tests/test_server.py +51 -0
- positif_ai-0.1.0/tests/test_smoke_dispatch.py +113 -0
- positif_ai-0.1.0/tests/test_sse.py +46 -0
- positif_ai-0.1.0/tests/test_transcript.py +238 -0
- positif_ai-0.1.0/tests/test_worker_template.py +84 -0
positif_ai-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
Positif is licensed under TWO licenses:
|
|
2
|
+
|
|
3
|
+
* The `sbd` CLI client (switchboard_cli.py) is MIT (full text in LICENSE-MIT).
|
|
4
|
+
It covers what a third party needs to talk to a Positif switchboard from
|
|
5
|
+
their own tooling. The file carries an `SPDX-License-Identifier: MIT` header.
|
|
6
|
+
|
|
7
|
+
* Everything else -- the switchboard engine (core/, cli/, deploy/, workers/) --
|
|
8
|
+
is licensed under the Business Source License 1.1 (BUSL-1.1), reproduced
|
|
9
|
+
below. It converts to the Change License (Apache-2.0) on the Change Date,
|
|
10
|
+
per-version.
|
|
11
|
+
|
|
12
|
+
This mirrors the Bourdon (sibling product) licensing structure. Free for solo
|
|
13
|
+
developers, research, and non-competing commercial use per the Additional Use
|
|
14
|
+
Grant below.
|
|
15
|
+
|
|
16
|
+
===============================================================================
|
|
17
|
+
|
|
18
|
+
License text copyright (c) 2020 MariaDB Corporation Ab, All Rights Reserved.
|
|
19
|
+
"Business Source License" is a trademark of MariaDB Corporation Ab.
|
|
20
|
+
|
|
21
|
+
Parameters
|
|
22
|
+
|
|
23
|
+
Licensor: RADLAB LLC
|
|
24
|
+
Licensed Work: Positif Version 0.1.0 or later. The Licensed Work is (c) 2026
|
|
25
|
+
RADLAB LLC.
|
|
26
|
+
Additional Use Grant: You may make production use of the Licensed Work, provided
|
|
27
|
+
Your use does not include offering the Licensed Work to third
|
|
28
|
+
parties on a hosted or embedded basis in order to compete with
|
|
29
|
+
RADLAB LLC's paid version(s) of the Licensed Work. For purposes
|
|
30
|
+
of this license:
|
|
31
|
+
|
|
32
|
+
A "competitive offering" is a Product that is offered to third
|
|
33
|
+
parties on a paid basis, including through paid support
|
|
34
|
+
arrangements, that significantly overlaps with the capabilities
|
|
35
|
+
of RADLAB LLC's paid version(s) of the Licensed Work. If Your
|
|
36
|
+
Product is not a competitive offering when You first make it
|
|
37
|
+
generally available, it will not become a competitive offering
|
|
38
|
+
later due to RADLAB LLC releasing a new version of the Licensed
|
|
39
|
+
Work with additional capabilities. In addition, Products that
|
|
40
|
+
are not provided on a paid basis are not competitive.
|
|
41
|
+
|
|
42
|
+
"Product" means software that is offered to end users to manage
|
|
43
|
+
in their own environments or offered as a service on a hosted
|
|
44
|
+
basis.
|
|
45
|
+
|
|
46
|
+
"Embedded" means including the source code or executable code
|
|
47
|
+
from the Licensed Work in a competitive offering. "Embedded"
|
|
48
|
+
also means packaging the competitive offering in such a way
|
|
49
|
+
that the Licensed Work must be accessed or downloaded for the
|
|
50
|
+
competitive offering to operate.
|
|
51
|
+
|
|
52
|
+
Hosting or using the Licensed Work(s) for internal purposes
|
|
53
|
+
within an organization is not considered a competitive
|
|
54
|
+
offering. RADLAB LLC considers your organization to include all
|
|
55
|
+
of your affiliates under common control.
|
|
56
|
+
|
|
57
|
+
For binding interpretive guidance on using RADLAB LLC products
|
|
58
|
+
under the Business Source License, please see the LICENSE_FAQ
|
|
59
|
+
at https://github.com/getbourdon/bourdon/blob/main/LICENSE_FAQ.md
|
|
60
|
+
Change Date: Four years from the date the Licensed Work is published.
|
|
61
|
+
Change License: Apache License, Version 2.0
|
|
62
|
+
|
|
63
|
+
For information about alternative licensing arrangements for the Licensed Work,
|
|
64
|
+
please contact positif@radlab.online.
|
|
65
|
+
|
|
66
|
+
Notice
|
|
67
|
+
|
|
68
|
+
Business Source License 1.1
|
|
69
|
+
|
|
70
|
+
Terms
|
|
71
|
+
|
|
72
|
+
The Licensor hereby grants you the right to copy, modify, create derivative
|
|
73
|
+
works, redistribute, and make non-production use of the Licensed Work. The
|
|
74
|
+
Licensor may make an Additional Use Grant, above, permitting limited production use.
|
|
75
|
+
|
|
76
|
+
Effective on the Change Date, or the fourth anniversary of the first publicly
|
|
77
|
+
available distribution of a specific version of the Licensed Work under this
|
|
78
|
+
License, whichever comes first, the Licensor hereby grants you rights under
|
|
79
|
+
the terms of the Change License, and the rights granted in the paragraph
|
|
80
|
+
above terminate.
|
|
81
|
+
|
|
82
|
+
If your use of the Licensed Work does not comply with the requirements
|
|
83
|
+
currently in effect as described in this License, you must purchase a
|
|
84
|
+
commercial license from the Licensor, its affiliated entities, or authorized
|
|
85
|
+
resellers, or you must refrain from using the Licensed Work.
|
|
86
|
+
|
|
87
|
+
All copies of the original and modified Licensed Work, and derivative works
|
|
88
|
+
of the Licensed Work, are subject to this License. This License applies
|
|
89
|
+
separately for each version of the Licensed Work and the Change Date may vary
|
|
90
|
+
for each version of the Licensed Work released by Licensor.
|
|
91
|
+
|
|
92
|
+
You must conspicuously display this License on each original or modified copy
|
|
93
|
+
of the Licensed Work. If you receive the Licensed Work in original or
|
|
94
|
+
modified form from a third party, the terms and conditions set forth in this
|
|
95
|
+
License apply to your use of that work.
|
|
96
|
+
|
|
97
|
+
Any use of the Licensed Work in violation of this License will automatically
|
|
98
|
+
terminate your rights under this License for the current and all other
|
|
99
|
+
versions of the Licensed Work.
|
|
100
|
+
|
|
101
|
+
This License does not grant you any right in any trademark or logo of
|
|
102
|
+
Licensor or its affiliates (provided that you may use a trademark or logo of
|
|
103
|
+
Licensor as expressly required by this License).
|
|
104
|
+
|
|
105
|
+
TO THE EXTENT PERMITTED BY APPLICABLE LAW, THE LICENSED WORK IS PROVIDED ON
|
|
106
|
+
AN "AS IS" BASIS. LICENSOR HEREBY DISCLAIMS ALL WARRANTIES AND CONDITIONS,
|
|
107
|
+
EXPRESS OR IMPLIED, INCLUDING (WITHOUT LIMITATION) WARRANTIES OF
|
|
108
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT, AND
|
|
109
|
+
TITLE.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 RADLAB LLC
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
positif_ai-0.1.0/NOTICE
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
Positif (Agent Switchboard)
|
|
2
|
+
Copyright (c) 2026 RADLAB LLC, a Wyoming limited liability company.
|
|
3
|
+
|
|
4
|
+
Licensing: MIT for the `sbd` CLI client (switchboard_cli.py, see LICENSE-MIT);
|
|
5
|
+
Business Source License 1.1 for the switchboard engine (see LICENSE), which
|
|
6
|
+
converts to Apache-2.0 four years after each version's publication.
|
|
7
|
+
|
|
8
|
+
https://positif.ai - Contact: positif@radlab.online
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: positif-ai
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Positif — Agent Switchboard: security-first inter-agent control plane (sbd) for the RADLAB fleet
|
|
5
|
+
Author-email: Ryan Davis <ryandavispro1@gmail.com>
|
|
6
|
+
Maintainer: RADLAB LLC
|
|
7
|
+
License-Expression: BUSL-1.1 AND MIT
|
|
8
|
+
Project-URL: Homepage, https://positif.ai
|
|
9
|
+
Project-URL: Repository, https://gitlab.com/radlab1/positif/positif
|
|
10
|
+
Project-URL: Documentation, https://positif.ai/docs/
|
|
11
|
+
Keywords: agent,router,switchboard,mcp,a2a,acp,tailnet
|
|
12
|
+
Classifier: Development Status :: 2 - Pre-Alpha
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
17
|
+
Requires-Python: >=3.10
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
License-File: LICENSE
|
|
20
|
+
License-File: LICENSE-MIT
|
|
21
|
+
License-File: NOTICE
|
|
22
|
+
Requires-Dist: fastmcp>=3.0
|
|
23
|
+
Requires-Dist: httpx>=0.27
|
|
24
|
+
Provides-Extra: dev
|
|
25
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
26
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
|
|
27
|
+
Requires-Dist: ruff>=0.4; extra == "dev"
|
|
28
|
+
Dynamic: license-file
|
|
29
|
+
|
|
30
|
+
# Positif — Agent Switchboard (`sbd`)
|
|
31
|
+
|
|
32
|
+
<!-- mcp-name: ai.positif/positif -->
|
|
33
|
+
|
|
34
|
+
> **Positif** (`positif.ai`) is the canonical product name. `sbd` is the CLI, and
|
|
35
|
+
> `switchboard` / `POSITIF_*` remain the stable **technical namespace** (package, env
|
|
36
|
+
> vars, `pos1.` token prefix, `/opt/positif-*` paths, the `positif.service`
|
|
37
|
+
> unit). Those identifiers stay put — renaming them is a separate, box-coordinated
|
|
38
|
+
> migration — but the product you're reading about is **Positif**.
|
|
39
|
+
>
|
|
40
|
+
> **Status:** `0.1.0` pre-alpha. This repository is release-preparation work,
|
|
41
|
+
> not a stable v1 release. Public distribution, deployment outside RADLAB, or
|
|
42
|
+
> release publication still requires human release authorization under RADLAB
|
|
43
|
+
> governance.
|
|
44
|
+
|
|
45
|
+
**The security-first agent control plane** — the governance + routing layer for a
|
|
46
|
+
multi-agent fleet. Not "another orchestrator": it authenticates the caller
|
|
47
|
+
(tailnet whois), enforces a metadata-only policy, dedupes, **routes** (explicit /
|
|
48
|
+
by capability), **brokers loop-guarded handoff** between agents, rate-limits, and
|
|
49
|
+
**audits every decision** — then gets out of the data path. It does not own memory
|
|
50
|
+
(that is **Bourdon**) and does not execute work (that is the agents); it governs
|
|
51
|
+
*who can reach whom, with what, and proves it happened.*
|
|
52
|
+
|
|
53
|
+
Sibling to **Bourdon** in one ecosystem — Bourdon = what the fleet *knows*,
|
|
54
|
+
Positif = who's *online* + what's *in flight*. They meet at exactly one MCP
|
|
55
|
+
boundary. (Positioning + productization path: `claude-brain SWITCHBOARD_PRODUCTIZATION.md`.)
|
|
56
|
+
|
|
57
|
+
Lives on the always-on `radlab-cloud` box (`/opt/positif-{src,venv,data}`),
|
|
58
|
+
mirroring Bourdon's layout. Port **7600** (Bourdon = 7500).
|
|
59
|
+
|
|
60
|
+
## 0.1.0 Pre-Alpha: What's Built
|
|
61
|
+
|
|
62
|
+
`sbd` is a FastMCP server exposing one secure tool, `dispatch(target, payload)`,
|
|
63
|
+
over streamable-HTTP bound to the tailnet IP. The HTTP transport is deliberate:
|
|
64
|
+
it's what makes `whois`-gated auth real — the caller's peer IP comes from the
|
|
65
|
+
live request and tailscaled turns it into an authoritative node identity.
|
|
66
|
+
|
|
67
|
+
**Dispatch state machine** (`core/dispatch.py`):
|
|
68
|
+
|
|
69
|
+
1. **whois** the caller's peer IP → node + tags + user (unknown ⇒ default-deny)
|
|
70
|
+
2. **provenance** tag — `trusted | tainted` (caller may self-declare tainted)
|
|
71
|
+
3. **target card** lookup (A2A Agent Card + reachability), unknown ⇒ deny
|
|
72
|
+
4. **policy** check — static `(caller, target)` table, **metadata only, never the
|
|
73
|
+
payload**; `tainted` is forbidden from sensitive targets
|
|
74
|
+
(`write/spend/prod/privileged`)
|
|
75
|
+
5. **idempotency** — dedup-hit returns the cached result, never re-dispatches
|
|
76
|
+
6. **shadow classifier** — predicts + logs the `(intent → target)` decision but
|
|
77
|
+
**does not route** (the event log is the training set)
|
|
78
|
+
7. **forward** — over the card's transport (`acp` → drives `hermes acp`; `http` →
|
|
79
|
+
POSTs to an HTTP worker, ready for Clyde / a `*-automations` worker)
|
|
80
|
+
8. **log + cache** the decision
|
|
81
|
+
|
|
82
|
+
A per-caller **token-bucket rate limit** runs before forwarding (caps cost/DoS
|
|
83
|
+
from a runaway or compromised caller). Observability: the `health` MCP tool and
|
|
84
|
+
`sbd stats` aggregate the event log over a default 7-day window (per-target
|
|
85
|
+
counts, deny rate, latency p50/p95). Use `--all-time` only when an operator
|
|
86
|
+
intentionally needs the full retained dataset.
|
|
87
|
+
|
|
88
|
+
Routing is **explicit-target** today. The classifier is scaffolded and learning
|
|
89
|
+
from day one.
|
|
90
|
+
|
|
91
|
+
### Reaching Hermes (ACP)
|
|
92
|
+
|
|
93
|
+
Hermes is delegated to over the Zed Agent Client Protocol (newline-delimited
|
|
94
|
+
JSON-RPC over stdio) — the Bourdon-aware path. `core/acp_client.py` spawns
|
|
95
|
+
`hermes acp --accept-hooks`, runs `initialize → session/new → session/prompt`,
|
|
96
|
+
accumulates `agent_message_chunk` text, auto-answers any `session/request_permission`
|
|
97
|
+
(else the turn deadlocks), and returns on `stopReason: end_turn`.
|
|
98
|
+
|
|
99
|
+
## Layout
|
|
100
|
+
|
|
101
|
+
```
|
|
102
|
+
core/ db schema.sql whois policy idempotency classifier cards acp_client dispatch server
|
|
103
|
+
default_data/ packaged policy + card seeds used by installed `sbd init`
|
|
104
|
+
cli/ main.py (sbd serve | init | whois | events | cards | stats | metrics | training | prune)
|
|
105
|
+
data/ editable mirror/override seeds for source-tree operators
|
|
106
|
+
deploy/ positif.service
|
|
107
|
+
scripts/ smoke_dispatch.py tests/ test_dispatch.py
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## Operator Feature Notes
|
|
111
|
+
|
|
112
|
+
Draft feature notes live at:
|
|
113
|
+
|
|
114
|
+
- `docs/features/README.md`
|
|
115
|
+
- `docs/features/policy-simulator.md`
|
|
116
|
+
- `docs/features/dry-run-dispatch.md`
|
|
117
|
+
- `docs/features/agent-registry.md`
|
|
118
|
+
- `docs/features/audit-reporting.md`
|
|
119
|
+
- `docs/features/sandbox-manager.md`
|
|
120
|
+
- `docs/features/release-readiness.md`
|
|
121
|
+
- `docs/features/render-surfaces.md`
|
|
122
|
+
|
|
123
|
+
`sbd render` emits JSON Render flat specs for those operator surfaces. This is
|
|
124
|
+
the renderer-ready contract for operator clients, not a release authorization
|
|
125
|
+
path:
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
sbd render command-center --db /opt/positif-data/positif.db
|
|
129
|
+
sbd render audit --db /opt/positif-data/positif.db --window-days 7
|
|
130
|
+
sbd render release --root /opt/positif-src
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
An optional local Ink renderer lives in `tui/` for the same surfaces:
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
npm --prefix tui ci
|
|
137
|
+
npm --prefix tui run build
|
|
138
|
+
node tui/dist/index.js --surface command-center --db /opt/positif-data/positif.db
|
|
139
|
+
node tui/dist/index.js --surface release --root /opt/positif-src --once
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## Run
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
# on the box
|
|
146
|
+
/opt/positif-venv/bin/python -m pip install -e /opt/positif-src
|
|
147
|
+
openssl rand -base64 32 > /opt/positif-data/handoff.secret
|
|
148
|
+
chown switchboard:switchboard /opt/positif-data/handoff.secret
|
|
149
|
+
chmod 600 /opt/positif-data/handoff.secret
|
|
150
|
+
sbd init --db /opt/positif-data/positif.db # schema + seed policy + cards
|
|
151
|
+
sbd serve --transport http --host 100.113.192.59 --port 7600 --db /opt/positif-data/positif.db
|
|
152
|
+
# in another shell (on the box, so whois resolves a real tailnet node):
|
|
153
|
+
/opt/positif-venv/bin/python scripts/smoke_dispatch.py
|
|
154
|
+
# add --yes-live only when you intentionally want to spawn the target agent:
|
|
155
|
+
/opt/positif-venv/bin/python scripts/smoke_dispatch.py --yes-live
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
Wire a coding agent as an MCP client by pointing it at
|
|
159
|
+
`http://100.113.192.59:7600/mcp/`.
|
|
160
|
+
|
|
161
|
+
## Audit retention
|
|
162
|
+
|
|
163
|
+
Positif bounds and redacts operator-facing metadata before storage or export.
|
|
164
|
+
`event_log.intent` and `event_log.error` are stored as short redacted strings, and
|
|
165
|
+
`sbd events` / `sbd training` re-apply the same bounds when reading older rows.
|
|
166
|
+
Stats, metrics, classifier accuracy, and training export default to the last 7
|
|
167
|
+
days; pass `--all-time` for an explicit full retained read.
|
|
168
|
+
|
|
169
|
+
Retention is manual, not automatic during dispatch:
|
|
170
|
+
|
|
171
|
+
```bash
|
|
172
|
+
sbd prune --db /opt/positif-data/positif.db --events-days 30 --idempotency-hours 24 --batch-size 1000
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
The default policy keeps audit events for 30 days and idempotency cache rows for
|
|
176
|
+
24 hours. Prune deletes in bounded batches so operators can run it from cron or
|
|
177
|
+
systemd timers without putting cleanup work on the request path.
|
|
178
|
+
|
|
179
|
+
## Security Stance (0.1.0 Pre-Alpha)
|
|
180
|
+
|
|
181
|
+
* Routes on **metadata, never payload**; **no token minting or forwarding** for
|
|
182
|
+
caller identity (Hermes uses its own key). Positif mints only short-lived
|
|
183
|
+
signed handoff bearers for trace/depth/path continuity.
|
|
184
|
+
* **Caller identity is asserted on the tailnet** — the peer IP must be in the
|
|
185
|
+
Tailscale CGNAT range (`100.64.0.0/10`) before whois is even consulted, so a
|
|
186
|
+
loopback/proxy/public peer can't be mistaken for a fleet node. Default-deny on
|
|
187
|
+
unknown caller / unknown target / no policy row (a targeted `deny` overrides a
|
|
188
|
+
broader `allow`).
|
|
189
|
+
* `tainted` (untrusted-origin) dispatches are denied from privileged targets at
|
|
190
|
+
ingress, and are additionally bounded to non-destructive tools.
|
|
191
|
+
* **Fully unprivileged: no root, no sudo.** `sbd` runs as the
|
|
192
|
+
`switchboard` user, and the Hermes it drives runs as that *same* unprivileged
|
|
193
|
+
user with its own private `HERMES_HOME` (`/opt/positif-data/hermes-home` —
|
|
194
|
+
config + Anthropic key, **no Slack tokens**). A compromise of `sbd` or its Hermes
|
|
195
|
+
yields only the `switchboard` user: it **cannot read `/root`, touch system files,
|
|
196
|
+
or escalate**. The gateway's separate root Hermes instance is untouched.
|
|
197
|
+
`deploy/preflight.sh` refuses to start unless the `HERMES_HOME` is present,
|
|
198
|
+
private, and switchboard-owned (and `hermes` is root-owned); an in-code argv
|
|
199
|
+
allowlist in `acp_client` is the second barrier on the spawn.
|
|
200
|
+
* **Per-dispatch tool bound + audit (allowlist, fail-closed).** Every tool the
|
|
201
|
+
target routes through the ACP permission gate is logged (`event_log.tool_audit`,
|
|
202
|
+
preserved even on error/timeout turns). `compute` (the **default** for Hermes;
|
|
203
|
+
forced for `tainted`; can be narrowed-to via `max_capability`) is an *allowlist*
|
|
204
|
+
— only `read/search/fetch/think` pass; `edit/delete/move/execute` and any
|
|
205
|
+
absent/unknown kind are **denied**. **Verified:** under `compute`, Hermes was
|
|
206
|
+
denied a file edit *and* a dangerous `rm` at the gate, while Bourdon MCP tools +
|
|
207
|
+
Q&A still work. Truly benign commands (e.g. `whoami`) auto-run in Hermes and skip
|
|
208
|
+
the gate — but since Hermes now runs **unprivileged**, even those can't read
|
|
209
|
+
`/root`, touch system files, or escalate, so the residual is contained.
|
|
210
|
+
* **Security selectors fail closed** — `provenance` and `max_capability` normalize
|
|
211
|
+
and resolve any typo/casing to the *more restrictive* branch, never the privileged
|
|
212
|
+
one. A cap can only narrow.
|
|
213
|
+
* **Idempotency keys are namespaced** by caller+target — a caller-supplied key
|
|
214
|
+
can't read another caller's or another target's cached result.
|
|
215
|
+
* **Spawn cleanup is process-group aware.** ACP and CLI agent launches create a
|
|
216
|
+
subprocess group/session where the platform supports it. On timeout,
|
|
217
|
+
Positif terminates the group, then kills it if it does not exit; non-POSIX
|
|
218
|
+
platforms fall back to best-effort direct child cleanup.
|
|
219
|
+
* **CLI review runners must not hide write capability.** The Codex card runs
|
|
220
|
+
`codex exec --skip-git-repo-check` without
|
|
221
|
+
`--dangerously-bypass-approvals-and-sandbox`, is marked `compute`, and is a
|
|
222
|
+
review/reasoning node rather than an autonomous writer. A future write-capable
|
|
223
|
+
Codex runner must be a separate, explicitly named card.
|
|
224
|
+
* **Per-agent Unix users are a deploy design, not a hidden runtime shortcut.**
|
|
225
|
+
Future `sbd-hermes`, `sbd-coder`, and `sbd-codex` execution must go through
|
|
226
|
+
exact root-owned wrapper commands before code enables it. Broad sudo is not
|
|
227
|
+
acceptable. Until that runner exists, preflight enforces private
|
|
228
|
+
switchboard-owned homes/workspaces for the seeded agents.
|
|
229
|
+
* **Signed handoff metadata.** Brokered handoff trace IDs, depths, and paths are
|
|
230
|
+
carried as `Authorization: Bearer pos1.<payload>.<hmac>` tokens signed with
|
|
231
|
+
`POSITIF_HANDOFF_SECRET_FILE` (default deploy path:
|
|
232
|
+
`/opt/positif-data/handoff.secret`). Unsigned `X-SBD-*` headers are ignored
|
|
233
|
+
by default; set `POSITIF_ALLOW_LEGACY_HANDOFF=1` only for a temporary compatibility
|
|
234
|
+
window. Handoff tokens authenticate metadata only; payload bodies are never used
|
|
235
|
+
to derive or validate handoff identity.
|
|
236
|
+
|
|
237
|
+
## Roadmap (Not Yet Built)
|
|
238
|
+
|
|
239
|
+
Litestream/B2 durability, presence/heartbeat, capability-match + semantic routing
|
|
240
|
+
as primary, NATS, brokered agent↔agent handoff, a second `sbd` to kill the SPOF.
|
|
241
|
+
See `claude-brain/PROJECTS/RADLAB-CLOUD/SWITCHBOARD.md`.
|
|
242
|
+
|
|
243
|
+
---
|
|
244
|
+
RADLAB LLC, a Wyoming limited liability company · BUSL-1.1
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
# Positif — Agent Switchboard (`sbd`)
|
|
2
|
+
|
|
3
|
+
<!-- mcp-name: ai.positif/positif -->
|
|
4
|
+
|
|
5
|
+
> **Positif** (`positif.ai`) is the canonical product name. `sbd` is the CLI, and
|
|
6
|
+
> `switchboard` / `POSITIF_*` remain the stable **technical namespace** (package, env
|
|
7
|
+
> vars, `pos1.` token prefix, `/opt/positif-*` paths, the `positif.service`
|
|
8
|
+
> unit). Those identifiers stay put — renaming them is a separate, box-coordinated
|
|
9
|
+
> migration — but the product you're reading about is **Positif**.
|
|
10
|
+
>
|
|
11
|
+
> **Status:** `0.1.0` pre-alpha. This repository is release-preparation work,
|
|
12
|
+
> not a stable v1 release. Public distribution, deployment outside RADLAB, or
|
|
13
|
+
> release publication still requires human release authorization under RADLAB
|
|
14
|
+
> governance.
|
|
15
|
+
|
|
16
|
+
**The security-first agent control plane** — the governance + routing layer for a
|
|
17
|
+
multi-agent fleet. Not "another orchestrator": it authenticates the caller
|
|
18
|
+
(tailnet whois), enforces a metadata-only policy, dedupes, **routes** (explicit /
|
|
19
|
+
by capability), **brokers loop-guarded handoff** between agents, rate-limits, and
|
|
20
|
+
**audits every decision** — then gets out of the data path. It does not own memory
|
|
21
|
+
(that is **Bourdon**) and does not execute work (that is the agents); it governs
|
|
22
|
+
*who can reach whom, with what, and proves it happened.*
|
|
23
|
+
|
|
24
|
+
Sibling to **Bourdon** in one ecosystem — Bourdon = what the fleet *knows*,
|
|
25
|
+
Positif = who's *online* + what's *in flight*. They meet at exactly one MCP
|
|
26
|
+
boundary. (Positioning + productization path: `claude-brain SWITCHBOARD_PRODUCTIZATION.md`.)
|
|
27
|
+
|
|
28
|
+
Lives on the always-on `radlab-cloud` box (`/opt/positif-{src,venv,data}`),
|
|
29
|
+
mirroring Bourdon's layout. Port **7600** (Bourdon = 7500).
|
|
30
|
+
|
|
31
|
+
## 0.1.0 Pre-Alpha: What's Built
|
|
32
|
+
|
|
33
|
+
`sbd` is a FastMCP server exposing one secure tool, `dispatch(target, payload)`,
|
|
34
|
+
over streamable-HTTP bound to the tailnet IP. The HTTP transport is deliberate:
|
|
35
|
+
it's what makes `whois`-gated auth real — the caller's peer IP comes from the
|
|
36
|
+
live request and tailscaled turns it into an authoritative node identity.
|
|
37
|
+
|
|
38
|
+
**Dispatch state machine** (`core/dispatch.py`):
|
|
39
|
+
|
|
40
|
+
1. **whois** the caller's peer IP → node + tags + user (unknown ⇒ default-deny)
|
|
41
|
+
2. **provenance** tag — `trusted | tainted` (caller may self-declare tainted)
|
|
42
|
+
3. **target card** lookup (A2A Agent Card + reachability), unknown ⇒ deny
|
|
43
|
+
4. **policy** check — static `(caller, target)` table, **metadata only, never the
|
|
44
|
+
payload**; `tainted` is forbidden from sensitive targets
|
|
45
|
+
(`write/spend/prod/privileged`)
|
|
46
|
+
5. **idempotency** — dedup-hit returns the cached result, never re-dispatches
|
|
47
|
+
6. **shadow classifier** — predicts + logs the `(intent → target)` decision but
|
|
48
|
+
**does not route** (the event log is the training set)
|
|
49
|
+
7. **forward** — over the card's transport (`acp` → drives `hermes acp`; `http` →
|
|
50
|
+
POSTs to an HTTP worker, ready for Clyde / a `*-automations` worker)
|
|
51
|
+
8. **log + cache** the decision
|
|
52
|
+
|
|
53
|
+
A per-caller **token-bucket rate limit** runs before forwarding (caps cost/DoS
|
|
54
|
+
from a runaway or compromised caller). Observability: the `health` MCP tool and
|
|
55
|
+
`sbd stats` aggregate the event log over a default 7-day window (per-target
|
|
56
|
+
counts, deny rate, latency p50/p95). Use `--all-time` only when an operator
|
|
57
|
+
intentionally needs the full retained dataset.
|
|
58
|
+
|
|
59
|
+
Routing is **explicit-target** today. The classifier is scaffolded and learning
|
|
60
|
+
from day one.
|
|
61
|
+
|
|
62
|
+
### Reaching Hermes (ACP)
|
|
63
|
+
|
|
64
|
+
Hermes is delegated to over the Zed Agent Client Protocol (newline-delimited
|
|
65
|
+
JSON-RPC over stdio) — the Bourdon-aware path. `core/acp_client.py` spawns
|
|
66
|
+
`hermes acp --accept-hooks`, runs `initialize → session/new → session/prompt`,
|
|
67
|
+
accumulates `agent_message_chunk` text, auto-answers any `session/request_permission`
|
|
68
|
+
(else the turn deadlocks), and returns on `stopReason: end_turn`.
|
|
69
|
+
|
|
70
|
+
## Layout
|
|
71
|
+
|
|
72
|
+
```
|
|
73
|
+
core/ db schema.sql whois policy idempotency classifier cards acp_client dispatch server
|
|
74
|
+
default_data/ packaged policy + card seeds used by installed `sbd init`
|
|
75
|
+
cli/ main.py (sbd serve | init | whois | events | cards | stats | metrics | training | prune)
|
|
76
|
+
data/ editable mirror/override seeds for source-tree operators
|
|
77
|
+
deploy/ positif.service
|
|
78
|
+
scripts/ smoke_dispatch.py tests/ test_dispatch.py
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Operator Feature Notes
|
|
82
|
+
|
|
83
|
+
Draft feature notes live at:
|
|
84
|
+
|
|
85
|
+
- `docs/features/README.md`
|
|
86
|
+
- `docs/features/policy-simulator.md`
|
|
87
|
+
- `docs/features/dry-run-dispatch.md`
|
|
88
|
+
- `docs/features/agent-registry.md`
|
|
89
|
+
- `docs/features/audit-reporting.md`
|
|
90
|
+
- `docs/features/sandbox-manager.md`
|
|
91
|
+
- `docs/features/release-readiness.md`
|
|
92
|
+
- `docs/features/render-surfaces.md`
|
|
93
|
+
|
|
94
|
+
`sbd render` emits JSON Render flat specs for those operator surfaces. This is
|
|
95
|
+
the renderer-ready contract for operator clients, not a release authorization
|
|
96
|
+
path:
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
sbd render command-center --db /opt/positif-data/positif.db
|
|
100
|
+
sbd render audit --db /opt/positif-data/positif.db --window-days 7
|
|
101
|
+
sbd render release --root /opt/positif-src
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
An optional local Ink renderer lives in `tui/` for the same surfaces:
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
npm --prefix tui ci
|
|
108
|
+
npm --prefix tui run build
|
|
109
|
+
node tui/dist/index.js --surface command-center --db /opt/positif-data/positif.db
|
|
110
|
+
node tui/dist/index.js --surface release --root /opt/positif-src --once
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## Run
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
# on the box
|
|
117
|
+
/opt/positif-venv/bin/python -m pip install -e /opt/positif-src
|
|
118
|
+
openssl rand -base64 32 > /opt/positif-data/handoff.secret
|
|
119
|
+
chown switchboard:switchboard /opt/positif-data/handoff.secret
|
|
120
|
+
chmod 600 /opt/positif-data/handoff.secret
|
|
121
|
+
sbd init --db /opt/positif-data/positif.db # schema + seed policy + cards
|
|
122
|
+
sbd serve --transport http --host 100.113.192.59 --port 7600 --db /opt/positif-data/positif.db
|
|
123
|
+
# in another shell (on the box, so whois resolves a real tailnet node):
|
|
124
|
+
/opt/positif-venv/bin/python scripts/smoke_dispatch.py
|
|
125
|
+
# add --yes-live only when you intentionally want to spawn the target agent:
|
|
126
|
+
/opt/positif-venv/bin/python scripts/smoke_dispatch.py --yes-live
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Wire a coding agent as an MCP client by pointing it at
|
|
130
|
+
`http://100.113.192.59:7600/mcp/`.
|
|
131
|
+
|
|
132
|
+
## Audit retention
|
|
133
|
+
|
|
134
|
+
Positif bounds and redacts operator-facing metadata before storage or export.
|
|
135
|
+
`event_log.intent` and `event_log.error` are stored as short redacted strings, and
|
|
136
|
+
`sbd events` / `sbd training` re-apply the same bounds when reading older rows.
|
|
137
|
+
Stats, metrics, classifier accuracy, and training export default to the last 7
|
|
138
|
+
days; pass `--all-time` for an explicit full retained read.
|
|
139
|
+
|
|
140
|
+
Retention is manual, not automatic during dispatch:
|
|
141
|
+
|
|
142
|
+
```bash
|
|
143
|
+
sbd prune --db /opt/positif-data/positif.db --events-days 30 --idempotency-hours 24 --batch-size 1000
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
The default policy keeps audit events for 30 days and idempotency cache rows for
|
|
147
|
+
24 hours. Prune deletes in bounded batches so operators can run it from cron or
|
|
148
|
+
systemd timers without putting cleanup work on the request path.
|
|
149
|
+
|
|
150
|
+
## Security Stance (0.1.0 Pre-Alpha)
|
|
151
|
+
|
|
152
|
+
* Routes on **metadata, never payload**; **no token minting or forwarding** for
|
|
153
|
+
caller identity (Hermes uses its own key). Positif mints only short-lived
|
|
154
|
+
signed handoff bearers for trace/depth/path continuity.
|
|
155
|
+
* **Caller identity is asserted on the tailnet** — the peer IP must be in the
|
|
156
|
+
Tailscale CGNAT range (`100.64.0.0/10`) before whois is even consulted, so a
|
|
157
|
+
loopback/proxy/public peer can't be mistaken for a fleet node. Default-deny on
|
|
158
|
+
unknown caller / unknown target / no policy row (a targeted `deny` overrides a
|
|
159
|
+
broader `allow`).
|
|
160
|
+
* `tainted` (untrusted-origin) dispatches are denied from privileged targets at
|
|
161
|
+
ingress, and are additionally bounded to non-destructive tools.
|
|
162
|
+
* **Fully unprivileged: no root, no sudo.** `sbd` runs as the
|
|
163
|
+
`switchboard` user, and the Hermes it drives runs as that *same* unprivileged
|
|
164
|
+
user with its own private `HERMES_HOME` (`/opt/positif-data/hermes-home` —
|
|
165
|
+
config + Anthropic key, **no Slack tokens**). A compromise of `sbd` or its Hermes
|
|
166
|
+
yields only the `switchboard` user: it **cannot read `/root`, touch system files,
|
|
167
|
+
or escalate**. The gateway's separate root Hermes instance is untouched.
|
|
168
|
+
`deploy/preflight.sh` refuses to start unless the `HERMES_HOME` is present,
|
|
169
|
+
private, and switchboard-owned (and `hermes` is root-owned); an in-code argv
|
|
170
|
+
allowlist in `acp_client` is the second barrier on the spawn.
|
|
171
|
+
* **Per-dispatch tool bound + audit (allowlist, fail-closed).** Every tool the
|
|
172
|
+
target routes through the ACP permission gate is logged (`event_log.tool_audit`,
|
|
173
|
+
preserved even on error/timeout turns). `compute` (the **default** for Hermes;
|
|
174
|
+
forced for `tainted`; can be narrowed-to via `max_capability`) is an *allowlist*
|
|
175
|
+
— only `read/search/fetch/think` pass; `edit/delete/move/execute` and any
|
|
176
|
+
absent/unknown kind are **denied**. **Verified:** under `compute`, Hermes was
|
|
177
|
+
denied a file edit *and* a dangerous `rm` at the gate, while Bourdon MCP tools +
|
|
178
|
+
Q&A still work. Truly benign commands (e.g. `whoami`) auto-run in Hermes and skip
|
|
179
|
+
the gate — but since Hermes now runs **unprivileged**, even those can't read
|
|
180
|
+
`/root`, touch system files, or escalate, so the residual is contained.
|
|
181
|
+
* **Security selectors fail closed** — `provenance` and `max_capability` normalize
|
|
182
|
+
and resolve any typo/casing to the *more restrictive* branch, never the privileged
|
|
183
|
+
one. A cap can only narrow.
|
|
184
|
+
* **Idempotency keys are namespaced** by caller+target — a caller-supplied key
|
|
185
|
+
can't read another caller's or another target's cached result.
|
|
186
|
+
* **Spawn cleanup is process-group aware.** ACP and CLI agent launches create a
|
|
187
|
+
subprocess group/session where the platform supports it. On timeout,
|
|
188
|
+
Positif terminates the group, then kills it if it does not exit; non-POSIX
|
|
189
|
+
platforms fall back to best-effort direct child cleanup.
|
|
190
|
+
* **CLI review runners must not hide write capability.** The Codex card runs
|
|
191
|
+
`codex exec --skip-git-repo-check` without
|
|
192
|
+
`--dangerously-bypass-approvals-and-sandbox`, is marked `compute`, and is a
|
|
193
|
+
review/reasoning node rather than an autonomous writer. A future write-capable
|
|
194
|
+
Codex runner must be a separate, explicitly named card.
|
|
195
|
+
* **Per-agent Unix users are a deploy design, not a hidden runtime shortcut.**
|
|
196
|
+
Future `sbd-hermes`, `sbd-coder`, and `sbd-codex` execution must go through
|
|
197
|
+
exact root-owned wrapper commands before code enables it. Broad sudo is not
|
|
198
|
+
acceptable. Until that runner exists, preflight enforces private
|
|
199
|
+
switchboard-owned homes/workspaces for the seeded agents.
|
|
200
|
+
* **Signed handoff metadata.** Brokered handoff trace IDs, depths, and paths are
|
|
201
|
+
carried as `Authorization: Bearer pos1.<payload>.<hmac>` tokens signed with
|
|
202
|
+
`POSITIF_HANDOFF_SECRET_FILE` (default deploy path:
|
|
203
|
+
`/opt/positif-data/handoff.secret`). Unsigned `X-SBD-*` headers are ignored
|
|
204
|
+
by default; set `POSITIF_ALLOW_LEGACY_HANDOFF=1` only for a temporary compatibility
|
|
205
|
+
window. Handoff tokens authenticate metadata only; payload bodies are never used
|
|
206
|
+
to derive or validate handoff identity.
|
|
207
|
+
|
|
208
|
+
## Roadmap (Not Yet Built)
|
|
209
|
+
|
|
210
|
+
Litestream/B2 durability, presence/heartbeat, capability-match + semantic routing
|
|
211
|
+
as primary, NATS, brokered agent↔agent handoff, a second `sbd` to kill the SPOF.
|
|
212
|
+
See `claude-brain/PROJECTS/RADLAB-CLOUD/SWITCHBOARD.md`.
|
|
213
|
+
|
|
214
|
+
---
|
|
215
|
+
RADLAB LLC, a Wyoming limited liability company · BUSL-1.1
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""sbd command-line interface."""
|