Skip to content

feat(core): receiver-side DM gating with a user-issued capability - #174

Merged
Mearman merged 5 commits into
mainfrom
feat/dm-receiver-gating
Sep 17, 2026
Merged

Mearman merged 5 commits into
mainfrom
feat/dm-receiver-gating

Conversation

@Mearman

@Mearman Mearman commented Sep 17, 2026

Copy link
Copy Markdown
Member

Closes #162

Adds a dm:send capability, issued by the local user principal (#160), that a receiver checks before admitting an unsolicited DM contact -- the receiver-controlled admission list #162 describes, parallel to how a room owner admits room members via room:member grants.

What's here:

  • verifyDmSendToken: receiver-side verification rooted at the user principal, not the presenting device
  • MeshStoreIdentity carries the user-principal identity (userIdentity) alongside the existing per-slot device identity
  • handleRoomJoin's DM branch checks a presented token: valid -> auto-admit, invalid -> refuse outright (manage-error, no new wire mechanism), absent -> unchanged existing two-round human-consent flow
  • admitAgentForDm / revokeAgentDmAccess on MeshStore, mirroring room-lifecycle.ts's own invite/kick pattern, backed by persisted issued-grant bookkeeping on the user principal (user-identity.ts)
  • requestDmAccess grows an optional dmSendGrant parameter so a requester can present a grant it already holds
  • integration tests covering the full admit -> auto-admit -> revoke lifecycle, plus a wrong-bearer refusal case

@Mearman
Mearman force-pushed the feat/dm-receiver-gating branch from 00f971a to 4dcf613 Compare September 17, 2026 17:03
Adds verifyDmSendToken, mirroring room-token-verification.ts's own
verifyRoomToken but rooted at the local user principal's device-id
rather than a room owner. A dm:send token must carry the dm:send
capability, scope kind "user" with path equal to the user principal,
and a delegation chain that actually roots at that principal -- a
self-issued or third-party token naming the right scope by
coincidence still fails, since rootIssuer is checked independently
of scope.path.
Adds loadIssuedDmGrant/saveIssuedDmGrant/deleteIssuedDmGrant, keyed
by bearer device-id hex, mirroring identity-store.ts's own
issuedGrants bookkeeping for room:member grants. A user principal
needs to remember the token-id of each dm:send grant it mints so a
later revocation can name which one to revoke -- a token-id is never
presented back on the wire, so this is the only record of it.

Also fixes renewIfNeeded to preserve issuedDmGrants across a
near-expiry certificate renewal, which previously always wrote a
bare {privateKey, certificate, expiresAt} record.
MeshStoreIdentity now carries userIdentity (an IdentityPort for the
local user principal, agent-comms#160) and userIdentityOptions (the
on-disk location it was loaded from), alongside the existing
per-bridge-slot device identity. Every setIdentity() call site is
updated to load and wire it through, wiring it via
loadOrCreateUserIdentity the same way bridge-mesh.ts and
test-transport.ts already load the per-slot device identity.

This is plumbing only: nothing yet reads userIdentity from
MeshStoreIdentity. It lays the groundwork for verifying and minting
dm:send capability tokens (agent-comms#162) against the same
principal every bridge on this machine account shares.
Wires verifyDmSendToken into handleRoomJoin's DM branch: a room.join
request for a DM path that presents a token verifies it against this
node's own user principal and auto-admits on success, refuses
outright on a present-but-invalid token, and falls through to the
existing two-round human-consent flow when no token is presented at
all. This is additive -- every existing DM admission path is
unchanged when the requester holds no dm:send grant.

Adds the admission primitives a user needs to actually issue and
withdraw that grant, mirroring room-lifecycle.ts's own invite/kick
pattern: admitAgentForDm mints and persists a dm:send grant, and
revokeAgentDmAccess mints a revocation entry, records and gossips
it, and forgets the issued-grant record. requestDmAccess grows an
optional dmSendGrant parameter so a requester can present a grant it
was already admitted with.
Prettier wraps the multi-line condition onto its own lines; this was
left single-line after resolving a rebase conflict.
@Mearman
Mearman force-pushed the feat/dm-receiver-gating branch from 4dcf613 to 9186fe7 Compare September 17, 2026 17:07
@Mearman
Mearman marked this pull request as ready for review September 17, 2026 17:09
@Mearman
Mearman merged commit 1896ff4 into main Sep 17, 2026
6 checks passed
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 17, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
🔒 Security Review Completed 2026-09-17T17:18:45.803512Z 9186fe7 Draft marked ready
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@Mearman
Mearman deleted the feat/dm-receiver-gating branch September 17, 2026 17:09
@github-actions

Copy link
Copy Markdown

🎉 This PR is included in version 3.10.0 🎉

The release is available on:

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Receiver-side DM gating with user-issued capability

1 participant