Skip to content

feat(channels): add Matrix as a first-class chat channel (with E2EE)#293

Open
anconina wants to merge 220 commits into
mainfrom
feature/matrix-channel
Open

feat(channels): add Matrix as a first-class chat channel (with E2EE)#293
anconina wants to merge 220 commits into
mainfrom
feature/matrix-channel

Conversation

@anconina

@anconina anconina commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds Matrix as a first-class chat channel, bringing the supported-channel count to 11 (Discord, Telegram, Slack, WhatsApp, iMessage, Signal, IRC, LINE, Email, MS Teams, Matrix). The adapter follows the hexagonal ports+adapters pattern used by every other channel and ships with end-to-end encryption (E2EE) support, media handling, credential validation, a comis doctor health check, and full docs.

Built test-first per the repo protocol — the diff is 96 test / 62 feat / 33 fix commits, with a per-phase deep code review that caught and fixed three correctness blockers the mocks missed (send watermark, dead E2EE-receive path, reconcile double-send).

What's included

  • Channel adapter (packages/channels/src/matrix/**) — client/auth, message mapper, formatter, mentions, reactions, media handler, invite policy, activity/state tracking, resolver, and the plugin registration.
  • E2EE — crypto store (with concurrency handling), decrypt-degrade path for undecryptable events, and encryption reconciliation guard.
  • CLI — install-wizard Matrix enumeration, --matrix-e2ee flag (incl. headless false), and a comis doctor Matrix health/credentials check.
  • Observabilitychannel:decrypt_failed bridged into the trajectory mapping.
  • Docs — new docs/channels/matrix.mdx, plus config-yaml / env-vars / CLI / data-directory / threat-model / channel-index updates, and the website channel tile + facts (count → 11).

Merge with main

This branch was just merged up to origin/main (release 1.0.47). Conflicts resolved:

  • packages/{channels,comis}/package.json — kept matrix-js-sdk 41.8.0 alongside origin's mailparser 3.9.12 bump; versions land on 1.0.47.
  • event-bus-bridge.test.ts bridge count guard reconciled to 120 (base 117 + channel:decrypt_failed here + mcp:server:connected/connect_failed from main).
  • pnpm-lock.yaml — reconciled; pnpm --lockfile-only confirms it is a no-op (fully consistent).

Test plan

  • pnpm validate (clean build + cycles + lint:security + coverage) — enforced by the pre-push hook.
  • Live-homeserver E2EE smoke against a real Matrix server (owner follow-up; not runnable in CI).

Notes / follow-ups

  • Tracked fast-follows (not blocking): matrix_decrypt explain verdict, token+E2EE requiring an explicit deviceId, ME-03 client-transport hardening.
  • Release sequencing: main has already shipped 1.0.47, so the Matrix work will fold into a subsequent tagged release — confirm the target version before tagging.

comis-agent added 30 commits July 5, 2026 14:26
- Add matrix-js-sdk@41.8.0 (exact pin, no caret) to @comis/channels,
  the workspace root, and the umbrella comis package.
- The npm latest dist-tag currently points at a 41.9.0 release
  candidate; the stable 41.8.0 line is pinned instead.
- Regenerate pnpm-lock.yaml; engines (node >=22) are compatible.
- Assert MatrixChannelEntrySchema defaults (disabled, e2ee on,
  allowlist mode, autoJoinOnInvite, private-homeserver off, empty
  allowFrom), full-block parsing, SecretRef credential fields, and
  strict-object rejection of unknown keys.
- Assert the matrix entry is registered on ChannelConfigSchema with
  the same defaults and nested strict rejection.
- Fails on pre-patch code: the schema and its registration do not
  yet exist.
- Add MatrixChannelEntrySchema (strict object) with token/password/
  recovery-key credential fields as SecretRef-or-string, MXID-keyed
  allowFrom trust, allowMode, autoJoinOnInvite, allowPrivateHomeserver,
  e2ee, stateDir and per-channel media processing.
- Register it on ChannelConfigSchema and export the inferred type.
- Regenerate the section-registry parity snapshot (adds the
  channels.matrix.* rows; additive only).
- Assert a stranded MATRIX_ACCESS_TOKEN env var yields a single
  KNOWN_PROVIDER_ENV finding named with the matrix provider, not the
  generic unknown fallthrough.
- Fails on pre-patch code: MATRIX_ACCESS_TOKEN matches only the
  generic _TOKEN catch-all today.
- Add a MATRIX_ACCESS_TOKEN provider pattern to the secrets-audit env
  scanner, placed before the generic _TOKEN catch-all so first-match
  wins and a stranded token is flagged with the matrix provider and an
  actionable finding.
- classifyMatrixError row-by-row: M_UNKNOWN_TOKEN→auth, M_LIMIT_EXCEEDED/429→platform+backoff, M_FORBIDDEN/403→auth, 5xx→platform, absent→internal
- errcode arm precedence over HTTP status
- secret in cause never leaks into hint
- classifyMatrixError maps M_* errcodes + HTTP status to the closed errorKind union, a retry disposition, and an operator-actionable hint
- errcode arm evaluated before status; M_UNKNOWN_TOKEN→auth (token-expiry recovery keys on this), M_LIMIT_EXCEEDED/429→platform retryable, M_FORBIDDEN/403→auth, 5xx→platform, no-signal→internal
- M_UNKNOWN_TOKEN hint names channels.matrix.accessToken
- void cause guard: a secret-bearing cause is never rendered into the hint
- MatrixErrorKind typed via Extract<ErrorKind, ...> for compiler-checked membership in the closed union
- Row-by-row cover of the invite decision: auto-join off never joins
  (any mode); allowlist joins iff the inviter MXID is listed; an empty
  allowlist joins nothing (default-closed); open mode joins any invite.
- Assert the decision keys on the full inviter MXID, never a
  display-name-like value or a same-localpart different-homeserver MXID.
- decideInvite({ autoJoinOnInvite, allowMode, allowFrom, inviterMxid })
  -> 'join' | 'ignore'; pure, no SDK import, deterministic.
- Default-closed: auto-join off never joins; allowlist joins iff the
  inviter's full MXID is listed (empty list joins nothing); open joins any.
- Trust keys on the exact full inviter MXID, never a display name.
- Row-by-row cover of the three gates: drop pre-sync-ready backlog, drop
  toStartOfTimeline backfill, drop non-message types, and drop any event
  whose timestamp is not strictly past the persisted watermark.
- Assert a live post-watermark message is delivered, plus an explicit
  forced-re-initial-sync case: replayed old timestamps stay guarded while
  a genuinely newer event passes.
- shouldDeliverTimelineEvent({ syncReady, toStartOfTimeline, eventType,
  eventTs, watermark }) -> boolean; pure, no SDK import, deterministic.
- Delivers only when all three gates pass: sync-ready, live (not
  toStartOfTimeline), an m.room.message, and strictly newer than the
  persisted watermark — so backlog, backfill, and replayed stale events
  are dropped and a forced re-initial-sync stays guarded.
…matrix

- validateHomeserverUrl: public delegates ok, private/loopback blocked by default, opt-in permits private with a loud config-posture warn, cloud-metadata ALWAYS blocked even with the opt-in on, non-http(s) rejected
- validateMatrixCredentials: names missing homeserverUrl/credential/userId fields and never echoes a secret value
- fails pre-patch: the credential-validator module does not yet exist (RED before GREEN)
- validateHomeserverUrl delegates to the audited guard by default; the allowPrivateHomeserver opt-in relaxes ONLY the private/loopback range, still requires http(s), and STILL denies cloud-metadata by reusing the exported blocklist (no metadata SSRF hole on a self-hosted opt-in)
- relaxing the range check logs a loud config-posture warning naming the knob
- validateMatrixCredentials names any missing homeserverUrl/credential/userId field and never echoes a token or password value
- renderMarkdownToMatrixHtml: bold/italic/code/fenced/link/list/quote/heading
  plus plaintext-fallback body and outbound HTML-escape + dangerous-scheme guard
- sanitizeInboundHtml: strips script/style/iframe with contents, on* handlers,
  non-http(s)/mxc schemes, and non-allowlisted tag markup; preserves the safe subset
- renderMarkdownToMatrixHtml renders bold/italic/inline+fenced code, safe-scheme
  links, ordered/unordered lists, blockquotes and headings to
  org.matrix.custom.html; body carries the plaintext markdown fallback; text
  nodes are HTML-escaped and non-http(s)/mxc link schemes degrade to plain text
- sanitizeInboundHtml removes script/style/iframe/object/embed with contents,
  strips on* handlers and non-http(s)/mxc href/src schemes, and drops markup for
  tags outside a safe allowlist (keeping their text); defense-in-depth ahead of
  the central external-content wrapper
- non-m.room.message maps to null; a text event maps to a schema-valid
  NormalizedMessage (UUID id, matrix channelType, room id, plaintext body)
- senderId is the full MXID from getSender(), spoof-proof against a
  display name set in content
- chatType from the direct-room flag; a fresh UUID per call
- inbound formatted_body is sanitized before it reaches metadata;
  a message with no verifiable sender maps to null
- mapMatrixEventToNormalized returns a NormalizedMessage for m.room.message
  events and null otherwise; senderId is the full MXID from getSender()
  (never a display name), and an event with no verifiable sender maps to null
- generated UUID id, matrix channelType, room id as channelId, plaintext body
  as text, systemNowMs timestamp, chatType from the direct-room flag
- Matrix event id in metadata.matrixEventId; an inbound formatted_body is run
  through sanitizeInboundHtml before it is carried into metadata
- /sync resumes from the persisted sync token with an enabled filter
- three-gate watermark guard precedes delivery (sync-ready, live, past-watermark)
- delivered events advance and persist the watermark
- invite gate joins allowlisted inviters and ignores the rest (default-closed)
- fails pre-patch: matrix-client module does not exist yet
…ite gate

- createMatrixClient composes the pure guards into matrix-js-sdk subscriptions
  behind an injected client seam (no homeserver needed to test the wiring)
- ClientEvent.Sync tracks readiness and persists the advanced batch token
- RoomEvent.Timeline runs the three-gate watermark guard before mapping and
  delivering; delivered events advance and persist the watermark
- RoomEvent.MyMembership gates invites on the inviter MXID via decideInvite
- /sync resumes from the persisted token via client.store.setSyncToken (41.8.0
  has no startClient since-option) plus an enabled lazy-members sync filter
- secret-free INFO/DEBUG/ERROR observability on every sync/invite boundary
- mid-run M_UNKNOWN_TOKEN with a re-login seam re-authenticates, persists the
  fresh token + device id, and resumes syncing
- mid-run M_UNKNOWN_TOKEN with no seam emits a loud health signal whose hint
  names channels.matrix.accessToken and never silently stops
- a server-rejected stale since clears the persisted token but retains the
  watermark so re-entry stays guarded
- fails pre-patch: the sync-error recovery branch does not exist yet
…nc token

- a sync ERROR carrying M_UNKNOWN_TOKEN re-authenticates via the injected seam
  when a password login is available, persists the fresh token + device id, and
  resumes syncing
- with no re-login seam it emits a loud health signal and ERROR whose hint names
  channels.matrix.accessToken, and never silently stops the client
- a server-rejected stale since (M_UNKNOWN) clears the persisted sync token but
  retains the watermark, so the forced re-initial-sync stays backlog-guarded
- a reauth-in-flight guard prevents parallel re-logins on repeated sync errors
- the token/secret never reaches a log line or the emitted health signal
- re-login yielding no device id still resumes
- a failed re-login falls through to the loud dark-token signal
- a failed resume after a successful refresh logs an error
- an unclassified sync error warns without stopping or signalling
…uilder

- matrix-adapter-outbound.test.ts pins the m.text content shape (body +
  org.matrix.custom.html formatted_body) the pure builder must return
- matrix-adapter.test.ts drives a fake matrix-js-sdk client through the
  ChannelPort lifecycle: homeserver guard before connect, credential
  precondition, whoami-authenticated start, the MXID speaker gate
  (default-open when allowFrom is empty; drops a non-allowlisted sender when
  populated), the m.room.message send shape, and connectionMode polling
Compose the credential/homeserver validation, token/password auth, and the
/sync transport into a pull-model ChannelPort:

- start() runs the credential precondition and the homeserver SSRF guard
  before any connection is opened; a blocked homeserver errs without building
  a client. It then authenticates (whoami-validated) and starts /sync.
- inbound: the MXID speaker-trust gate runs on each mapped message before
  fan-out — default-open when allowFrom is empty (the invite gate is the
  default-closed boundary), dropping a non-allowlisted sender when the list is
  populated; both trust decisions key on the full MXID. The request context +
  traceId are minted here at the ingress boundary.
- outbound: sendMessage renders markdown into an m.room.message with a
  plaintext body and an org.matrix.custom.html formatted_body.
- getStatus reports connectionMode polling (stale-exempt).

matrix-adapter-outbound.ts is a pure builder over the shared markdown renderer.
Pins channelType matrix, id channel-matrix, the honest capability literals
(reactions/edit/delete/fetchHistory/attachments/typing/threads all false,
buttons none, maxMessageChars 32768, replyToMetaKey matrixEventId), and that
activate()/deactivate() delegate to adapter.start()/stop().
Wrap the Matrix adapter as a ChannelPluginPort. Every capability flag is honest
for the plaintext scope — reactions/edit/delete/history/attachments/typing/
threads all false, buttons none — with maxMessageChars 32768 and replyToMetaKey
matrixEventId. activate()/deactivate() delegate to the adapter's start()/stop().

Grows the explicit-capabilities roster to 12: the source-walking arch test now
finds the matrix own-property literals. The roster edit ships with the plugin it
locks so no intermediate commit points the source-walk at a missing file.
comis-agent added 12 commits July 6, 2026 23:53
…ld count

- ChannelConfig is a flat bag whose optional fields are the union of every
  supported channel's credentials; adding Matrix took it past the audit threshold
- acknowledge the width at the declaration, matching the accumulator-bag convention
- the codec and parse helpers throw on absent or malformed media-encryption info
  and on audited WASM codec failures
- every such throw is caught and translated to a Result at the send-path
  fromPromise boundary, so it never escapes as an unhandled rejection
An encrypted room's raw /messages history yields m.room.encrypted events
with no plaintext body, so the body digest never matches — a fully-covered
scan then wrongly reports not_sent, which would replay a message that DID
land. Assert the reconcile is unresolved (never a false not_sent) for an
encrypted room. Fails on the current code (returns not_sent).
A raw /messages read of an encrypted room returns m.room.encrypted events
with no plaintext body, so the reconcile body digest can never match and a
covered scan wrongly reported not_sent — replaying a message that had
already landed and duplicating it. Detect the encrypted room via the
authoritative isEncryptionEnabledInRoom (the check the attachment path
uses) and return unresolved (park+escalate) before the scan, upholding the
port contract that a channel which cannot tell never reports not_sent.
The outward-send ledger digests the agent's raw markdown, but the adapter
rewrites mention markup into matrix.to pill links and splits oversized text
into multiple events before sending — so a landed send's wire body no longer
matches the ledger digest. Assert both a mention-rewritten body and a
multi-chunk send that WERE sent reconcile to unresolved, never a false
not_sent. Both fail on the current code (return not_sent).
…ruct the sent body

The reconcile digest is over the agent's raw markdown, but the wire body is
rendered in-adapter (mention markup rewritten to pill links, oversized text
split into chunks), so a landed send's body may legitimately fail to match.
Reserve not_sent for the reliable case — a covered scan with NO bot-authored
in-window event, where the send provably did not land. When the bot authored
any in-window event but none matches, return unresolved: one could be that
rewritten or chunked send, and a false not_sent would replay it into a
duplicate. Updates the two reconcile tests whose in-window-bot-non-match
scenario is now unresolved, and adds an explicit no-in-window-bot not_sent
case.
…atrix doctor creds check

The creds probe flags an unresolved ${VAR} only on accessToken, so a
password-login or recovery-key ref that fails to resolve slips past the
offline check. Assert both are named as a fail. Fails on the current code
(reports pass).
… in the doctor creds check

The offline creds probe matched an unresolved ${VAR} only on accessToken, so
a password-login or recovery-key ref that failed to resolve passed silently
and only surfaced later at the live reachability probe. Match any of the three
Matrix credential paths (accessToken/password/recoveryKey); the credential
value is still never read — only the ${VAR} name and path appear.
The value-less --matrix-e2ee flag could only be true or absent, so a headless
operator had no way to DISABLE the default-on encryption the interactive
wizard exposes as a Yes/No toggle. Accept --matrix-e2ee <value> (true or
false), coerce and validate the raw Commander string, and reject a non-boolean
value early. An omitted flag stays undefined so the daemon default applies.

The tests exercise the string-valued flag the pre-patch boolean-only type
could not express, so the failing state and the type widening land together.
The Matrix tile requested the logo at fill 000000 (black), which is near
invisible against the section's dark background where every other icon uses a
light or brand color. Request a white fill. Also drop the hardcoded channel
count from the wizard channel-step doc comment so it cannot drift again.
…probe-failure branches

Pin that a crypto-enabled install still scans plaintext rooms normally, and
that a room-encryption probe which throws yields unresolved (never a false
not_sent). Completes branch coverage of the encrypted-room guard.
Conflicts resolved:
- packages/channels/package.json, packages/comis/package.json: keep
  matrix-js-sdk 41.8.0 (this branch) alongside origin's mailparser 3.9.12
  patch bump; versions land on origin's 1.0.47 release.
- packages/observability/src/trajectory/event-bus-bridge.test.ts: bridge
  entry-count guard reconciled to 120 (base 117 + channel:decrypt_failed
  from this branch + mcp:server:connected/connect_failed from origin).
- pnpm-lock.yaml: importers reconciled to match; pnpm --lockfile-only
  confirms the merged lockfile is a no-op (fully consistent).
@mintlify

mintlify Bot commented Jul 7, 2026

Copy link
Copy Markdown

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated (UTC)
comis 🟢 Ready View Preview Jul 7, 2026, 6:55 AM

💡 Tip: Enable Workflows to automatically generate PRs for you.

@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown

PR description incomplete

Please fill in all required sections before this PR can be reviewed.

Required sections: Description, Related Issue, Type of Change, Checklist, RED Test Proof.

For code changes in packages/*/src/**, paste the failing test output (test name + assertion error) from before the production patch in the RED Test Proof section, or write EXEMPT: <reason> for docs/CI/config-only PRs.

See CONTRIBUTING.md for the full contribution bar.

@socket-security

socket-security Bot commented Jul 7, 2026

Copy link
Copy Markdown

All alerts resolved. Learn more about Socket for GitHub.

This PR previously contained dependency changes with security issues that have been resolved, removed, or ignored.

View full report

}

// Heading.
const heading = line.match(/^(#{1,6})\s+(.*)$/);

// Remove active/embedding elements together with their contents, then any
// orphan open/close/void tag of the same name that a nested structure left.
out = out.replace(DANGEROUS_PAIRED, "");
// Remove active/embedding elements together with their contents, then any
// orphan open/close/void tag of the same name that a nested structure left.
out = out.replace(DANGEROUS_PAIRED, "");
out = out.replace(DANGEROUS_ORPHAN, "");
out = out.replace(DANGEROUS_ORPHAN, "");

// Strip `on*` event-handler attributes (quoted and unquoted forms).
out = out.replace(/\son\w+\s*=\s*"[^"]*"/gi, "");

// Strip `on*` event-handler attributes (quoted and unquoted forms).
out = out.replace(/\son\w+\s*=\s*"[^"]*"/gi, "");
out = out.replace(/\son\w+\s*=\s*'[^']*'/gi, "");
// Strip `on*` event-handler attributes (quoted and unquoted forms).
out = out.replace(/\son\w+\s*=\s*"[^"]*"/gi, "");
out = out.replace(/\son\w+\s*=\s*'[^']*'/gi, "");
out = out.replace(/\son\w+\s*=\s*[^\s>]+/gi, "");
Comment on lines +62 to +72
const rewrittenMarkdown = markdown.replace(
MENTION_MARKUP_RE,
(whole, name: string, target: string) => {
if (!MXID_RE.test(target)) return whole; // not a real MXID — leave literal
if (!seen.has(target)) {
seen.add(target);
userIds.push(target);
}
return `[${name}](${MATRIX_TO_BASE}${target})`;
},
);
@anconina

anconina commented Jul 7, 2026

Copy link
Copy Markdown
Contributor Author

Code review

Found 5 issues:

  1. Self-hosted homeserver media downloads are SSRF-blocked despite allowPrivateHomeserver: true (bug due to setup-media.ts building trustedFetchOrigins solely from channels.*.apiRootObject.values(channelsCfg).map((c) => c?.apiRoot) — while the Matrix schema has homeserverUrl and no apiRoot). A private/loopback homeserver origin never reaches the allowlist, so every mxc:// media fetch takes the strict validateUrl private-range branch and is rejected. The comment this PR adds in ssrf-fetcher.ts claims "a self-hosted homeserver … is reachable" via the trusted-origin branch, but only the live-test harness hand-builds that array; no production wiring does.

// the strict SSRF firewall still gates every OTHER URL (e.g. the daemon's own 127.0.0.1:4766 stays blocked).
const channelsCfg = (container.config.channels ?? {}) as Record<string, { apiRoot?: string } | undefined>;
const trustedFetchOrigins = Object.values(channelsCfg)
.map((c) => c?.apiRoot)
.filter((r): r is string => typeof r === "string" && r.length > 0)
.map((r) => {

// Per-hop SSRF firewall: DNS-resolve + IP-range/metadata classification.
// A hop whose ORIGIN exactly matches a configured trusted origin (a
// self-hosted homeserver / the test emulator on loopback) is validated
// leniently via validateLocalServerUrl (loopback permitted) — mirroring the
// single-shot path — so an authed download from a private/loopback media
// host is reachable; EVERY other hop stays on strict validateUrl. This
// allowance is INDEPENDENT of the token-drop below: the bearer still rides
// only an authAllowHosts hop, so a trusted-but-off-allowlist redirect target
// (a CDN on another host) is reached WITHOUT the token.
let hopOrigin: string | undefined;

  1. MATRIX_RECOVERY_KEY is invisible to the .env secrets audit (bug due to KNOWN_PROVIDER_PATTERNS in secrets-audit.ts registering only /^MATRIX_ACCESS_TOKEN$/). Matrix has three secret fields (accessToken, password, recoveryKey); MATRIX_PASSWORD falls to the generic _PASSWORD "unknown" bucket, and MATRIX_RECOVERY_KEY matches no pattern at all, so a MATRIX_RECOVERY_KEY=… line in .env yields zero audit findings — even though docs/channels/matrix.mdx tells users to set it. The doctor check covers all three fields (MATRIX_CREDENTIAL_PATHS in matrix-health.ts), but the audit scanner was not given the same treatment (compare the msteams entry one line above).

{ pattern: /^SLACK_SIGNING_SECRET$/, provider: "slack" },
{ pattern: /^MSTEAMS_APP_PASSWORD$/, provider: "msteams" },
{ pattern: /^MATRIX_ACCESS_TOKEN$/, provider: "matrix" },
{ pattern: /^GROQ_API_KEY$/, provider: "groq" },
{ pattern: /^DEEPGRAM_API_KEY$/, provider: "deepgram" },

  1. The four new comis init flags (--matrix-homeserver, --matrix-user-id, --matrix-access-token, --matrix-e2ee) are absent from the comis init Channels flag table in docs/reference/cli.mdx, and --matrix-e2ee appears nowhere in docs/ at all — despite docs/channels/matrix.mdx having a dedicated E2EE section (CLAUDE.md says "Keep docs/**/*.mdx up to date in the same change that alters anything they describe — user-facing behavior, config keys/defaults, CLI commands/flags")

.option("--msteams-auth-mode <mode>", "Microsoft Teams auth mode: secret|certificate|managedIdentity")
.option("--matrix-homeserver <url>", "Matrix homeserver base URL")
.option("--matrix-user-id <id>", "Matrix bot user ID (MXID, @bot:host)")
.option("--matrix-access-token <tok>", "Matrix bot access token")
.option("--matrix-e2ee <value>", "Enable Matrix end-to-end encryption: true or false (defaults to true)")
// Media generation

| `--channels <list>` | `string` | - | Comma-separated channel list |
| `--telegram-token <tok>` | `string` | - | Telegram bot token |
| `--discord-token <tok>` | `string` | - | Discord bot token |
| `--slack-bot-token <tok>` | `string` | - | Slack bot token |
| `--slack-app-token <tok>` | `string` | - | Slack app token |
| `--line-token <tok>` | `string` | - | LINE channel token |
| `--line-secret <sec>` | `string` | - | LINE channel secret |

  1. The getSecret("MATRIX_RECOVERY_KEY") fallback added in setup-channels-adapters.ts has no row in the new "Matrix" section of docs/reference/environment-variables.mdx — only MATRIX_ACCESS_TOKEN is documented, breaking the convention that every channel secret with a getSecret fallback gets a row (CLAUDE.md says docs must stay current with "env vars (docs/reference/environment-variables.mdx)")

const { homeserverUrl, userId, deviceId, allowFrom, allowMode, autoJoinOnInvite, allowPrivateHomeserver, e2ee } = channelConfig.matrix;
const accessToken = (channelConfig.matrix.accessToken as string | undefined) || getSecret("MATRIX_ACCESS_TOKEN");
const recoveryKey = (channelConfig.matrix.recoveryKey as string | undefined) || getSecret("MATRIX_RECOVERY_KEY");
const password = channelConfig.matrix.password as string | undefined;

### Matrix
| Variable | Purpose |
|----------|---------|
| `MATRIX_ACCESS_TOKEN` | Bot access token — the fallback source for `channels.matrix.accessToken` (also settable inline in config or as a SecretRef) |
The homeserver URL (`channels.matrix.homeserverUrl`) and the bot MXID (`channels.matrix.userId`) are plain configuration values, not secrets, and are set in `config.yaml` rather than as environment variables.

  1. Stale "11 platform adapters" counts left un-bumped for the 12th (Matrix) adapter: docs/developer-guide/packages.mdx lines 23 (enumeration also omits Matrix), 39, and 187, plus docs/operations/observability.mdx line 28 ("All 11 channel adapters") — the msteams PR bumped these same lines 10→11, and the channel-count-consistency architecture test covers facts.ts/Channels.astro/docs.json but not these two files (CLAUDE.md "Docs-Current")

| agent | `@comis/agent` | Executor, budget, circuit breaker, RAG, sessions, context engine | `PiExecutor`, session manager, budget tracker |
| channels | `@comis/channels` | 11 platform adapters (Discord, Telegram, Slack, WhatsApp, Signal, iMessage, LINE, IRC, Echo, Email, Microsoft Teams) | Channel adapters, message mappers, media resolvers |
| cli | `@comis/cli` | Commander.js CLI, JSON-RPC client | CLI commands, RPC client |

The on-wire field is `normalized.metadata.traceId`; the helper `getMessageTraceId()` reads it. All 11 channel adapters wrap their dispatch loops in `runWithContext`, and the orchestrator's `adapter.onMessage` handler applies a defense-in-depth second wrap so no adapter can silently skip propagation.

🤖 Generated with Claude Code

- If this code review was useful, please react with 👍. Otherwise, react with 👎.

comis-agent added 9 commits July 7, 2026 12:33
Resolved conflicts:
- packages/comis/package.json: keep @matrix-org/matrix-sdk-crypto-wasm; take
  the newer @earendil-works/pi-* 0.80.6 pins to match the other packages.
- packages/web/src/api/contracts.generated.*: regenerated via
  `pnpm contracts:generate` against the merged @comis/core contract registry
  (218 contracts, 136510B min / 13977B gz) — the drift gate is green.
- init.test.ts: init exposes 45 options after adding the matrix init flags.
- event-bus-bridge.test.ts: TRAJECTORY_BRIDGE_MAPPING has 123 entries after
  bridging the matrix channel events.

pnpm validate green.
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.

2 participants