[DAN] BRIDGE
A real, standalone, zero-dependency multi-channel message bus for agents — post, read, and list shared channels from a plain local log.
⚡ Zero runtime dependencies. Pure Python standard library (≥ 3.9) — no packages to resolve, no build step.
python -m unittesttests it. Full breakdown under Dependencies.
🔴 Per-agent identity, with an honest caveat. Each agent registers a local secret key; every post is signed with an HMAC over its
(channel, agent, text, ts), and a read flags any message that doesn't verify asUNVERIFIED. This stops one agent from spoofing another's name. It is not a defense against a same-user attacker: the keys live in a local0600file, so any process running as the same user that can read that file can still forge a signature. See Trust model andSECURITY.md.
A local coordination log for cooperating agents: any number of agents register a signing key, then post to and read from any number of named channels, all backed by one plain, append-only file. No server, no broker, no network — just a file, a keyring, and four commands.
Install
Not on PyPI yet — install from source:
pip install git+https://github.com/STRATO-DAN/dan-oss-bridge-cli.git
or clone and install:
git clone https://github.com/STRATO-DAN/dan-oss-bridge-cli.git
cd dan-oss-bridge-cli
pip install .
Pure standard library, so there is no dependency tree to resolve. Once it's published,
pip install dan-oss-bridge will work directly.
Use
dan-oss-bridge register <agent> [--rotate] # create (or rotate) an agent's signing key
dan-oss-bridge post <channel> <agent> "<text>" # append a signed message to a channel
dan-oss-bridge read <channel> [--limit N] # read a channel (default: 50 most recent)
dan-oss-bridge channels # list every channel that has a message
An agent must be registered before it can post — registration mints a random secret key so its
messages can be signed. Messages persist to ~/.dan-oss-bridge/bus.jsonl by default (override with
--bus or DAN_OSS_BRIDGE_BUS), and keys to ~/.dan-oss-bridge/agents.json (override with
--keyring or DAN_OSS_BRIDGE_KEYRING), so both survive across processes and restarts.
Worked example
Real output from a live run — two agents register, post to a standup channel, then read it back:
$ dan-oss-bridge register agent-a
registered agent 'agent-a'; key stored in '/home/you/.dan-oss-bridge/agents.json'
$ dan-oss-bridge register agent-b
registered agent 'agent-b'; key stored in '/home/you/.dan-oss-bridge/agents.json'
$ dan-oss-bridge post standup agent-a "Starting on the cache refactor"
posted to 'standup'
$ dan-oss-bridge post standup agent-b "Reviewing agent-a's change now"
posted to 'standup'
$ dan-oss-bridge read standup
[standup] agent-a: Starting on the cache refactor
[standup] agent-b: Reviewing agent-a's change now
$ dan-oss-bridge channels
standup
$ dan-oss-bridge read standup --limit 1
[standup] agent-b: Reviewing agent-a's change now
Both posts verify against each agent's key, so they read back plainly. A message that does not verify — an unknown agent, a tampered record, or an older unsigned line — reads back flagged so a reader can tell:
$ dan-oss-bridge read standup
[standup] agent-a: Starting on the cache refactor
[standup] stranger (UNVERIFIED): I promise I am agent-a
Messages are returned oldest-first, and --limit N reads only the N most recent (the read seeks
the tail of the file, so it stays fast on a large log — it's bounded by N, not the whole file).
Turning identity off (local-trust mode)
Set DAN_OSS_BRIDGE_NO_AUTH=1 to restore the original unauthenticated behaviour — posts need no
registration and are unsigned, and reads print every message plainly without the UNVERIFIED flag.
Use it only where every writer of the bus file is already trusted.
$ DAN_OSS_BRIDGE_NO_AUTH=1 dan-oss-bridge post standup anyone "no key needed here"
posted to 'standup'
Python API
from dan_oss_bridge import MessageBus, Keyring
# With a keyring: identity is on — posts are signed, reads are verified.
keyring = Keyring("~/.dan-oss-bridge/agents.json")
keyring.register("agent-a") # idempotent; register(..., rotate=True) replaces
bus = MessageBus("~/.dan-oss-bridge/bus.jsonl", keyring=keyring)
bus.post("standup", "agent-a", "Starting on the cache refactor")
msgs = bus.read("standup") # -> [Message(..., hmac="…", verified=True)]
msgs[0].verified # -> True (False = UNVERIFIED, None = not checked)
# Without a keyring: the original unauthenticated behaviour.
plain = MessageBus("~/.dan-oss-bridge/bus.jsonl")
plain.post("standup", "anyone", "no key needed") # unsigned; read leaves verified = None
Message is a small dataclass — channel, agent, text, ts (a POSIX timestamp), plus hmac
(the stored signature, empty if unsigned) and verified (a read-time verdict: True, False for
UNVERIFIED, or None when read without a keyring). post raises ValueError on an empty
channel/agent or a text larger than 1 MiB, and UnregisteredAgentError (a ValueError subclass)
when the bus has a keyring but the agent has no key.
Configuration
| Setting | Default | What it does |
|---|---|---|
--bus <path> |
~/.dan-oss-bridge/bus.jsonl |
Which log file to use (CLI flag) |
DAN_OSS_BRIDGE_BUS |
(unset) | Same as --bus, via environment (the flag wins if both are set) |
--keyring <path> |
~/.dan-oss-bridge/agents.json |
Which agent keyring file to use (CLI flag) |
DAN_OSS_BRIDGE_KEYRING |
(unset) | Same as --keyring, via environment (the flag wins if both are set) |
DAN_OSS_BRIDGE_NO_AUTH |
(unset) | 1 disables identity: unsigned posts, unflagged reads, no registration required |
--rotate |
(off) | On register: replace an already-registered agent's key with a fresh one |
--limit <N> |
50 |
On read: how many of the most-recent messages to return |
What it never does
- Never opens a network socket or a server — it is a file and a CLI, nothing listens.
- Never lets one bad line deny reads to everyone — a corrupt record (invalid UTF-8, non-JSON, a valid-JSON non-object, or a bad timestamp) is skipped, not fatal.
- Never loads the whole log to answer
read --limit N— it seeks backward from the end, so read cost is bounded byN, not the file size. - Never crashes on a bad invocation — an unwritable or directory
--buspath is a clear one-line error and exit code, not a traceback. - Never drops a message it can't verify — an unverifiable message is flagged
UNVERIFIED, never silently discarded (deny-by-default on trust, not on delivery). - Never prints an agent's secret key —
registerreports only the keyring location, and the key file is written0600. - Never claims more than it delivers — identity authenticates across agents that don't share a key, not against a same-user attacker who can read the local key file (see the trust model).
Trust model
The bus is a local, append-only log with per-agent identity. Be explicit about exactly what that buys you — and what it does not — before you deploy it:
- The
agentfield is authenticated across agents. Each agent registers a local secret key, every post is signed with an HMAC over its(channel, agent, text, ts), and a read verifies that signature against the sending agent's key. A message that reads back verified could only have been produced by something holding that agent's key — one agent can no longer post under another agent's name just by typing it. - Unverifiable messages are flagged, not trusted and not dropped. A message with a missing or
invalid signature, from an agent the reader has no key for, or an older unsigned line, reads back
as
UNVERIFIED. The reader still sees it; it just isn't trusted. - The honest caveat: this is not a defense against a same-user attacker. The keys live in a
local file (
0600). Any process running as the same user that can read that file can sign as any agent whose key is in it. Identity here separates agents that don't share a key — it does not protect against an attacker who already has local read access to your keyring. Pair it with OS file permissions and process isolation for the boundary you actually need. - No cross-message tamper-evidence. There is no message-id or hash chain, so a writer with file access can still drop or reorder whole lines; per-message signatures detect edits to a signed message's content, not deletion or replay of entire records.
DAN_OSS_BRIDGE_NO_AUTH=1turns identity off entirely (unsigned posts, unflagged reads) for the original local-trust mode, where every writer of the file is already trusted.
Corrupt or hostile content still can't deny service — the read path tolerates bad lines even
while verifying signatures. See SECURITY.md for the full statement.
When to use this
- Best fit: coordinating several cooperating local agents/processes on one machine — a shared scratchpad they can post status to and read each other's, with zero infrastructure.
- Best fit: a dead-simple, dependency-free message log for a script or tool that just needs to leave and read notes on named channels.
Honest flip side: this is not a networked message broker and not a full security boundary. The per-agent signing authenticates senders across agents that don't share a key, but it is not a defense against a same-user attacker who can read the local key file, and there are no delivery guarantees across machines and no Slack/Discord/Telegram integration (v1 is a generic local bus; external platform bridges are each their own separately-scoped undertaking). If you need cross-machine transport or protection against a local attacker who can read your keyring, this isn't it.
Dependencies
| Runtime dependencies | 0 — Python standard library only (json, os, time, pathlib, …) |
| Install to run | the package itself; no dependency tree |
| Install to test | none — tests run on the standard-library unittest runner |
| Python | ≥ 3.9 |
Nothing is added to your environment beyond the package, and nothing phones home.
Project contents
| Path | What it is |
|---|---|
dan_oss_bridge/cli.py |
The CLI entry point — register / post / read / channels. |
dan_oss_bridge/bus.py |
MessageBus + Message — the append-only log, tail-bounded reads, corrupt-line-tolerant parsing, sign-on-post / verify-on-read. |
dan_oss_bridge/keyring.py |
Keyring — the local 0600 per-agent key store, plus the HMAC sign/verify helpers. |
dan_oss_bridge/__init__.py |
Public exports (MessageBus, Message, Keyring, UnregisteredAgentError). |
tests/ |
Real unit tests (python -m unittest discover -s tests). |
FAQ
Can two agents post at the same time? Yes — writes are append-only single lines, so concurrent posts from separate processes interleave cleanly without corrupting each other.
Can I read across all channels at once? read takes a channel; omit the channel argument to
read across all of them. channels lists every channel that has received a post.
What happens to a huge log over time? Reads stay fast (tail-bounded), but the file only grows — there's no built-in rotation yet. Rotate or truncate it yourself if it gets large.
Is the agent field trustworthy? Across agents, yes — a verified message was signed with
that agent's key, so a different agent can't post under the name. But it is not trustworthy
against a same-user attacker who can read the local key file, and an UNVERIFIED message hasn't
been authenticated at all. See Trust model.
Tests
python -m unittest discover -s tests
Runs the unit suite on the standard-library unittest runner — no dependencies to install. As of
this release that's 39 tests, all passing, covering the post/read/channels round-trip, channel
isolation and oldest-first ordering, the --limit tail read, and the full corrupt-input class
(invalid UTF-8, non-JSON, valid-JSON non-object, bad timestamp) proving one bad line can't deny
reads to the whole bus, plus oversized-text rejection and friendly CLI errors on a bad bus path —
and the identity layer: registration writes a 0600 keyring, a signed post verifies on read, a
forged/tampered/unsigned message reads UNVERIFIED, an unregistered agent can't post by default,
and DAN_OSS_BRIDGE_NO_AUTH=1 restores the unauthenticated post.
Contributing
See CONTRIBUTING.md for how to file an issue or submit a PR. Maintainers may use AI tools to help review contributions — please don't include personal information in an issue, PR, or commit beyond what's needed to describe the change.
Releasing
See RELEASING.md — the same version-bump/tag/publish process applies to every DAN-OSS tool, this one included.
License
MIT (code) — see LICENSE. The "DAN" name and logo are trademarked and not covered by
the MIT grant — see TRADEMARK.md.
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 dan_oss_bridge-0.2.0.tar.gz.
File metadata
- Download URL: dan_oss_bridge-0.2.0.tar.gz
- Upload date:
- Size: 24.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
73e97ef911c9fa9846f2ca0cdc3e4bf8bcea1b606c8234d7168c509d6c7f1aca
|
|
| MD5 |
8323c1ac6498d0565939c4b291bc76df
|
|
| BLAKE2b-256 |
0451fc3a3a5ea81da27f4e00ee1013c672555987b09181bc1bc6ff37d6f8e0b9
|
Provenance
The following attestation bundles were made for dan_oss_bridge-0.2.0.tar.gz:
Publisher:
publish.yml on STRATO-DAN/dan-oss-bridge-cli
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
dan_oss_bridge-0.2.0.tar.gz -
Subject digest:
73e97ef911c9fa9846f2ca0cdc3e4bf8bcea1b606c8234d7168c509d6c7f1aca - Sigstore transparency entry: 2879191450
- Sigstore integration time:
-
Permalink:
STRATO-DAN/dan-oss-bridge-cli@2e0b45abe66a085016e685823b415bfb96b29882 -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/STRATO-DAN
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@2e0b45abe66a085016e685823b415bfb96b29882 -
Trigger Event:
release
-
Statement type:
File details
Details for the file dan_oss_bridge-0.2.0-py3-none-any.whl.
File metadata
- Download URL: dan_oss_bridge-0.2.0-py3-none-any.whl
- Upload date:
- Size: 17.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c371fd97c311acf74a8674df1a194f1e2d7c7ea99e83b7a493cd7942fc043628
|
|
| MD5 |
4e857946d535c22cada62f1b05607c1c
|
|
| BLAKE2b-256 |
3ed14d95afc08cdf59545bbc00b4793feac4e3c7c624f6474a97df8f3fa09664
|
Provenance
The following attestation bundles were made for dan_oss_bridge-0.2.0-py3-none-any.whl:
Publisher:
publish.yml on STRATO-DAN/dan-oss-bridge-cli
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
dan_oss_bridge-0.2.0-py3-none-any.whl -
Subject digest:
c371fd97c311acf74a8674df1a194f1e2d7c7ea99e83b7a493cd7942fc043628 - Sigstore transparency entry: 2879191493
- Sigstore integration time:
-
Permalink:
STRATO-DAN/dan-oss-bridge-cli@2e0b45abe66a085016e685823b415bfb96b29882 -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/STRATO-DAN
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@2e0b45abe66a085016e685823b415bfb96b29882 -
Trigger Event:
release
-
Statement type: