Skip to content

Add direct messages (open_dm): a private 1:1 channel, auth-native#41

Merged
Jacobobber merged 1 commit into
mainfrom
feat/direct-messages
Jul 8, 2026
Merged

Add direct messages (open_dm): a private 1:1 channel, auth-native#41
Jacobobber merged 1 commit into
mainfrom
feat/direct-messages

Conversation

@Jacobobber

Copy link
Copy Markdown
Owner

Direct messages (open_dm)

A private 1:1 channel between two agents — built as an auth-native construct, not a new transport. A DM is one A2ABus channel whose policy is a single scope that only the two parties hold, so privacy is the same scope gate that guards every other channel, with no separate access list to keep in sync.

  • direct_channel(a, b) — pure, deterministic derivation of an order-independent channel name and pair scope (truncated SHA-256 over the NUL-separated sorted subjects). direct_channel(a, b) == direct_channel(b, a), distinct pairs don't collide, and a self-DM is rejected.
  • open_dm(bus, a, b) — declares that channel on the bus with a policy requiring the pair scope to both post and read, and returns the DirectChannel descriptor. Idempotent to re-open (existing messages untouched).
  • The caller mints each party a token carrying descriptor.scope. That scope is the entire access control: a third party who learns the channel name still can't post or read it (it lacks the scope, and the bus denies it opaquely — indistinguishable from an unknown channel).

Once opened it's an ordinary channel, so post / catch_up / ack and the v1.7 live roster all work and stay gated to the pair.

dm = open_dm(bus, "alice", "bob")
alice = Principal("alice", frozenset({dm.scope}))
bus.post(alice, dm.name, "bob, can you take batch-7?")

Verification

  • tests/test_dm.py — 9 tests: order-independent derivation, no collision between distinct pairs (incl. the NUL-separator case), self-DM rejected, the two parties can talk, a third party can't read even knowing the name, a third party can't post, open_dm idempotent, different pairs isolated, roster works on a DM.
  • examples/direct_messages.py runs and prints the exchange + the opaque denial.
  • Full suite 310 passed (+9). mypy --strict, ruff, catalog freshness all green.

Additive.

A DM is not a new transport -- it is one A2A channel whose policy is a single
scope only the two parties hold, so privacy is the same scope gate as every
other channel:

- direct_channel(a, b) purely derives an order-independent channel name + pair
  scope (truncated SHA-256 of the NUL-separated sorted subjects; (a,b)==(b,a),
  distinct pairs don't collide, self-DM rejected).
- open_dm(bus, a, b) declares that channel gated to the pair scope for post and
  read, and returns the descriptor; the caller mints each party a token
  carrying descriptor.scope. That scope is the whole access control -- a third
  party who learns the name is denied opaquely, like any other channel.
- Ordinary channel once opened: post/catch_up/ack and the live roster apply and
  stay gated to the pair. Demo: examples/direct_messages.py.

310 tests (+9), mypy --strict, ruff, catalog freshness green. v1.8.0.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@Jacobobber Jacobobber merged commit 811b20e into main Jul 8, 2026
12 checks passed
@Jacobobber Jacobobber deleted the feat/direct-messages branch July 8, 2026 20:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant