feat(agora): channel identity, authority, and delivery hardening - #7151
Draft
forkwright wants to merge 45 commits into
Draft
feat(agora): channel identity, authority, and delivery hardening#7151forkwright wants to merge 45 commits into
forkwright wants to merge 45 commits into
Conversation
InboundMessage had no receiving-account identity, so multi-account
Signal/Matrix deployments collapsed identical senders and rooms across
accounts onto one route, and replies left from the provider's default
account instead of the account that received the message.
- Add account_id to InboundMessage; both providers stamp it during
extraction.
- Signal's poll loop now passes the account to signal-cli receive
instead of None, so multi-account daemons are polled per account.
- ChannelBinding gains an optional account leg: a scoped binding
matches only messages that account received; unscoped bindings keep
matching any account. Session-key templates gain an {account}
placeholder.
- Replies send with the inbound account_id, and command audit records
and dispatch spans carry it.
Part of #5195
Group routing matched on channel + group_id alone, so every participant of a configured group could drive the agent and whatever command surface the route permitted, even when the operator intended one trusted participant. ChannelBinding gains a participants allowlist: when non-empty, only the listed senders may activate the binding; other senders — including fellow group members — fall through to lower-priority routes. The leg applies uniformly to group, source, and channel-wildcard bindings, so a wildcard with participants doubles as a per-sender channel allowlist. Empty preserves the previous any-participant behavior. Part of #5194
Any inbound message that parses as a ! command was intercepted after route resolution and could enumerate agents, channel health, models, skills, blackboard entries, and sessions. Wildcard and default route bindings — the documented quickstart posture — exposed that surface to every sender. Add MessagingConfig.commands (InboundCommandPolicy), fail-closed by default: only public_commands (help, ping) are reachable, and the operator surface requires an explicit operators entry naming the channel+sender (channel:source, per-segment wildcard). default_allow restores the old behavior as an explicit opt-out. Denied commands get a refusal reply, a warning, and an aletheia_command_denied_total metric; !help shows non-operator senders only the public subset. Unknown commands stay public because their reply leaks nothing beyond the filtered !help hint. The allowlist contents are operator policy; this commit ships only the enforcement substrate and the deny default. Part of #5193
The channel ingress model had no stable provider message IDs and no durable checkpoints: restarts and retry windows could replay or duplicate messages, Matrix parsed event_id but dropped it, and every outbound send used a fresh UUID so retried replies could double-post. - InboundMessage gains message_id (Matrix event_id; signal-cli exposes none) and a dedupe_key() that prefers the provider ID and falls back to a SHA-256 content hash over the routing identity. - A bounded DedupeFilter (agora::dedupe, 4096-key FIFO window) runs in the dispatcher before any routing or agent work; repeats are dropped and counted in aletheia_ingress_duplicates_total. - SendParams gains an idempotency_key; Matrix maps it to the room-send transaction ID, and dispatch replies derive it from the inbound message identity, so a replayed inbound event cannot double-post. - Matrix sync cursors persist per account through a CursorStore (JSON-file implementation under the instance data dir, hashed file names so account IDs stay out of paths); a persisted cursor overrides initial_since on startup, and each processed batch checkpoints (aletheia_cursor_checkpoints_total). Signal needs no cursor — signal-cli receive consumes destructively. The full ingress journal from the issue is deliberately not in this commit; the persisted cursor plus bounded dedupe window cover the restart/retry replay paths without a second storage engine. Part of #5196
messaging.maxConcurrentHandlers was enforced only inside ChannelListener::run, but the runtime consumed the receiver via into_receiver and spawned one dispatch task per message — the cap was dead config, and a message flood could create unbounded dispatch and LLM work. into_receiver also orphaned the provider poll handles at the end of the setup block. spawn_dispatcher now owns the ChannelListener and dispatches through listener.run(), so the configured cap bounds in-flight work on the live path and the provider handles drain with the listener at shutdown. run() gains saturation instrumentation: an in-flight gauge (aletheia_inbound_handlers_in_flight) and a counter (aletheia_inbound_handler_saturation_total) incremented whenever the receive loop has to wait for a free slot, which is the backpressure signal — upstream mpsc buffers fill and provider poll loops suspend. A flood test drives four messages at a cap of two and pins that handler concurrency reaches but never exceeds the limit. Per-channel rate limits from the issue are not in this commit; the bounded channels already exert backpressure and the saturation metric makes pressure visible. Part of #5197
Channel payloads and sender identifiers were inconsistently redacted: raw Signal envelopes and Matrix events were captured unconditionally, dispatch spans and warnings logged full senders, command audit records persisted raw sender/group IDs, Signal health payloads keyed on raw phone numbers, and the only redaction helper was a Signal-specific redact_phone used in exactly one span. - agora::redact centralizes channel-identity redaction (suffix-4 form) and is now used by the listener span, the dispatch span and no-route warning, the Signal poll span, command audit origin records, and the Signal probe/connection-health payloads. - messaging.retainRawPayloads (default false) gates raw payload capture in both providers' extraction paths; the raw envelope/event is attached only on explicit operator opt-in. - InboundMessage loses its derived Debug for a manual impl that redacts identities, reports text/attachments by size only, and shows a captured raw payload as present-or-absent. Tests cover phone/Matrix-ID/short-value redaction, the Debug output contract, raw-payload opt-in and opt-out on both extractors, redacted health keys, and raw identifiers staying out of command audit history. Part of #5198
ChannelListener set the active-subscriptions gauge at construction but cleared it only on a drop path that into_receiver disabled, and the runtime called into_receiver immediately — so the gauge pinned at the startup count for the life of the process regardless of actual provider task state. Gauge ownership now lives in a SubscriptionGuard: construction adds the handle count, guard drop subtracts it. run() holds the guard until every handler and provider task has drained; into_receiver returns the guard with the receiver and handles so manual-control callers keep the metric honest. The set-to-zero drop heuristic is gone, so two overlapping listeners no longer stomp each other's count. Tests cover the transfer (gauge survives into_receiver, clears on guard drop) and the existing drop/run shutdown paths. Part of #5204
…hema The CONFIGURATION.md field reference is generated from the taxis schema doc comments (aletheia#5774); the cursor-precedence note on initialSince, the saturation-metrics note on maxConcurrentHandlers, and the messaging.commands posture prose belong in the source doc comments so the generated table carries them. Part of #5196, part of #5197
Two CI-gate hazards in the new modules: the dedupe test module carried an expect(clippy::expect_used) attribute with no expect call to fulfill (unfulfilled_lint_expectations is denied), and types::hex_lower indexed a byte string, which the workspace's indexing_slicing warn-deny rejects. hex_digit now matches on the nibble like the existing dispatch helper. Part of #5196
The agora crate gained a sha2 workspace dependency for the dedupe content hash; the lockfile edge travels with it since cargo cannot run in the authoring environment. Part of #5196
WHY: the redaction commit passed the loop-shadowed owned account_id String to redact::identifier(&str) (E0308), and the concurrency-cap flood test moved the max_seen/completed Arcs into the run() closure and then asserted on them after the move (E0382). Local cargo check is unavailable in this lane, so both surfaced only in the CI gate: borrow the account id, and clone the two asserted Arcs before the closure so the originals remain for the post-run assertions.
Move active-subscription ownership into each Matrix account future so normal exit, cancellation, and task failure release the gauge through RAII. This follows the listener lifecycle change that removes listener-owned subscription accounting.
`toml::map::Map` has no `values_mut`. It carries `iter_mut` and `get_mut` (toml 0.8.23, `src/map.rs:107,214`), which is what the pre-existing `encrypt_recursive` in this same file already uses. That single unresolved method produced all five gate errors: once `entry` binds to an error type, `as_table_mut`, `auth.get_mut(field)`, and the `*secret = encrypt_value(...)?` assignment each fail in turn, reported as E0308 x3 and E0277 on lines that are themselves correct and identical in shape to the working call site at `encrypt.rs:422`. Verified this was the only `values_mut` the branch introduced; the other call sites in the workspace are `HashMap`/`serde_json::Map`, which do have it.
added 15 commits
August 28, 2026 11:51
…ss borrow clippy -D warnings failed gate-coverage-compile-checks on three lints in fjall_store.rs introduced by the command_lifecycle feature: delete_session and put_command_lifecycle_record_in_tx exceeded too_many_lines (135/100 and 107/100), and validate_command_lifecycle_record took a needless reference to an already-&str field. Extracted the command_lifecycle key-collection/index-check block out of delete_session into command_lifecycle_delete_keys_for_session_in_tx (mirroring the existing tool_audit_keys_for_session_in_tx / note_gid_delete_keys_for_session_in_tx helpers), and factored the repeated prefix-range scans (messages/usage/distillations/notes) into prefixed_child_keys_in_tx. Split put_command_lifecycle_record_in_tx into a session/id/record-building half (build_command_lifecycle_record_in_tx) and the collision-check/insert half. Dropped the redundant & on spec.delivery_key in is_digest.
…me errors crates/aletheia/src/dispatch.rs: call get_history with session_id.as_str() instead of &StoredSessionId -- StoredSessionId no longer derefs to &str since 3d6b28e typed command lifecycle owner ids, and get_history expects &str. crates/agora/src/listener.rs: three test ChannelProvider impls (PartiallyFailedProvider, CooperativeCancellationProvider, UnexpectedCancellationProvider) had name() delegate to self.id(). Calling a trait method through self ties the return lifetime to the trait's declared (elided, &self-bound) signature rather than the impl's narrower &'static str override, so the borrow checker cannot prove the result outlives 'static. Match the repo's own idiom (TestProvider, PanicProvider) and return the literal directly instead of delegating.
…b baseline - crates/pylon/src/tests/error.rs: removed four deep_merge_* tests whose target (handlers::config::deep_merge) was deleted when config.rs moved patch-merge logic into taxis::redact::apply_section_patch_with_marker_authority; equivalent coverage already exists in crates/taxis/src/redact.rs. - crates/taxis/src/redact.rs: fixed E0308 in marker_literal_at_non_sensitive_path_remains_data -- EmbeddingSettings.model is Option<String>, compared via .as_deref(). - crates/graphe/src/store/fjall_store.rs: removed unused fjall::Readable import in delete_session (no trait method of it is called in that fn). - scripts/stub-baseline.toml: regenerated with --write-baseline -- this branch's agora/semeion/client.rs changes paid down the id/jsonrpc stub sites the baseline still claimed (count 1, tree has 0).
full-gate-build failed with 4 dead_code errors under -D warnings: MatrixEvent::unsigned/extra, MatrixEventContent::msgtype, and EnvelopeOutcome::UnsupportedContentLost/MalformedLost's kind/reason fields, all deserialized/constructed but never read. The Signal envelope fields were the real defect: normalize_batch discarded kind and reason via '..' after only reading lost_parts, throwing away the exact diagnostic content this hardening effort exists to preserve. It now logs them at debug level before folding lost_parts into the loss counter. The Matrix fields (unsigned, top-level extra, msgtype) are genuinely unused today -- unsigned/extra are held for the deferred raw-payload capture path already noted at the 'raw: None' call site (#5198), and msgtype is deserialized for shape parity but not yet used to filter message kinds. Marked #[expect(dead_code)] with that reasoning rather than deleted, since removing them would silently drop data the governed capture path (#5198) still needs to deserialize.
Adding `command_lifecycle_part` put `import_bundle_children_in_tx` at 8
parameters against clippy's ceiling of 7:
error: this function has too many arguments (8/7)
--> crates/graphe/src/store/fjall_store.rs:4748:5
A `#[expect(clippy::too_many_arguments)]` would have cleared it, but this file
already has the better answer and graphe carries no such suppression anywhere.
`NoteTxParts` and `CommandLifecycleTxParts` group transaction keyspaces exactly
this way -- and this function was already building both of them inline out of its
own loose parameters, which is the tell that the grouping belonged one level up.
`ImportBundleTxParts` carries the six partitions an imported bundle's child rows
are written to. The signature goes 8 -> 3, the two inner structs are now built
from its fields rather than from separate arguments, and the single call site
passes one value.
Verified: `cargo clippy -p graphe --all-targets --keep-going` exit 0, no warnings.
…dening-local # Conflicts: # crates/taxis/src/registry.rs # crates/taxis/src/reload.rs
…ption `crates/agora/clippy.toml` disallows `std::fs::read` and `std::fs::File::open`, offering two ways out: use `tokio::fs`, or "abstract behind a trait for testability". The second is what this file already does -- `CursorStore` is the trait, `FileCursorStore` is one implementation, and every consumer holds it as `Arc<dyn CursorStore>`. The trait is synchronous, so moving to `tokio::fs` would make every caller async for no behavioural gain, and the directory fsync in `sync_directory` has no `tokio::fs` equivalent at all. Both suppressions name which of the policy's two conditions they satisfy, rather than asserting the lint is wrong. What this deliberately does NOT settle: the crate header says all persistence belongs in mneme, which argues a cursor store should not live in agora at all. That is a placement decision rather than a lint one, and it is raised on the PR instead of being decided by an attribute.
- taxis/validate.rs:918 — validate_binding_source_kind's first let-else
(binding.get("sourceKind") else { return None }) returns None unchanged,
so it rewrites faithfully to the ? operator per the lint's own suggestion.
The second let-else in the same function pushes an error before returning
and is untouched — not what clippy flagged.
- graphe/portability.rs:408 — sample_agent_file() (test fixture builder, not
production code) was 117 lines. Extracted sample_workspace(),
sample_command_lifecycle_record(), and sample_session() as sub-builders;
sample_agent_file() now composes them. Same AgentFile is produced, so no
assertion in any consumer of the fixture changes.
- graphe/store/fjall_store_tests.rs:1191 —
delete_session_removes_usage_distillation_and_note_rows is one linear
scenario (populate every child partition, delete, assert all partitions
empty). Suppressed with #[expect(clippy::too_many_lines, reason = ...)]
per the repo's own idiom (crates/aletheia/src/commands/agent_io.rs);
splitting it would obscure the single guarantee under test.
- graphe/store/fjall_store_tests_portability.rs:624 —
import_session_bundle_writes_everything_atomically is the same shape (one
atomic bundle import, assert every partition landed). Same #[expect]
treatment, same rationale.
No test assertions changed.
…ailures Four independent CI failures on run 33210347451 (head 97cea15), all fixed in this pass. crates/agora/src/matrix/mod.rs:389 -- unnecessary_sort_by. accounts.sort_unstable_by compared keys by reference (`left.cmp(right)`); the account id key is `String` (not Copy), so clippy's own `*left` suggestion does not type-check. Rewritten as `sort_unstable_by_key(|(left, _)| (*left).clone())` -- pays one clone per account (a probe path, not hot) rather than keeping a manual comparator clippy flags as equivalent to the key-extraction form. crates/agora/src/matrix/mod.rs:421 -- missing_fields_in_debug. The manual `Debug for MatrixProvider` omitted `cursor_store` entirely. It is `Option<Arc<dyn CursorStore>>` and `CursorStore` carries no `Debug` bound, so the field itself cannot be printed. Added a derived `cursor_store_set` field (present/absent, matching the existing `default_account_set` pattern) and switched `.finish()` to `.finish_non_exhaustive()` to mark the repr as deliberately partial. crates/agora/src/matrix/mod.rs:108 -- MatrixEventContent::msgtype. Carried `reason="deserialized for shape parity; not yet used to filter message kinds"` with no tracking issue, so check-stub-accountability.py rejected it as new unaccounted debt (aletheia#7082 was filed for this site). Nothing reads `.msgtype` anywhere in the crate -- removed the field; the raw JSON key still round-trips through the adjacent `#[serde(flatten)] extra` map, so no deserialization behavior changes. crates/agora/src/router.rs:297 -- manual_contains. `participant_allowed` compared `participants.iter().any(|p| *p == msg.sender)`; `participants` is `Vec<String>`, so the equality-search closure is exactly what `.contains()` already does. Rewritten as `binding.participants.contains(&msg.sender)`, same truth table, fewer lines. crates/agora/src/semeion/client.rs:431 -- nonminimal_bool on a negated `is_some_and`. `!response.get("timestamp").and_then(as_u64).is_some_and(|t| t > 0)` is true exactly when the timestamp is absent, unparseable, or zero; `is_none_or(|t| t == 0)` covers the same two cases (None, or Some via the predicate) without the leading negation. u64 can't be negative so `t == 0` is equivalent to `!(t > 0)`. crates/agora/src/types.rs:217 -- single_match/single_match_else. `InboundMessage::dedupe_key`'s two-arm `match self.group_id.as_deref() { Some(group) => .., None => .. }` destructures only the `Some` arm; rewritten as `if let Some(group) = .. { .. } else { .. }`, same hashing order and same two branches. crates/graphe/src/store/fjall_store.rs:761 -- dead_code under the ML-feature build (gliner, nuextract). `ImportBundleTxParts` has exactly one construction site (`import_bundle_children_in_tx`'s caller, `import_session_bundle`), and both live inside the `#[cfg(feature = "portability")] impl SessionStore { .. }` block spanning lines 4295-4964. The ML-feature build config does not enable `portability`, so that whole impl compiles out and the struct's only constructor disappears -- while the struct definition, carrying no cfg gate of its own, still compiles in and trips `-D warnings` on zero constructions. Gated the struct definition with the matching `#[cfg(feature = "portability")]` so it exists exactly when its constructor does; compared against every sibling struct in the same file (`ImportSessionOutcome`, `ImportSessionBundle`, `ImportSessionNote`, `ImportCommandLifecycleRecord`, `ImportSessionWorkingState`, `ImportSessionBundleResult`), which already carry the identical gate for the identical reason. `NoteTxParts` and `CommandLifecycleTxParts`, which sit next to `ImportBundleTxParts` in the default build, are untouched: both have other construction sites outside the portability block, so they stay live in the default build. The fourth reported failure -- "ERROR: diaporeia MCP inventory ... is out of date" in gate-coverage-scripts -- is not a real check failure. It is the `test-diaporeia-mcp-inventory.py` unit suite's own `test_check_mode` deliberately exercising the --check failure path against a synthetic stale fixture; the step prints that line and then "OK: all MCP inventory generator tests passed" and exits 0, confirmed against the actual job log for run 33210347451. `generate-diaporeia-mcp-inventory.py --check` against the real crates/diaporeia/CLAUDE.md passes with no diaporeia files touched in this branch or in origin/main ahead of it; the job's only real failure was check-stub-accountability.py, fixed above.
…ailures - cursor.rs: scope std::fs::write in a test fixture with an #[expect(clippy::disallowed_methods)], matching the repo's own precedent (integration-tests/tests/r722_substrate_canary.rs) for test setup that deliberately writes fixture bytes synchronously. - listener.rs: move the NotifyOnDrop struct+impl before the channel setup in listener_drop_aborts_owned_tasks_promptly, clearing items_after_statements without changing behavior. - matrix/client.rs: rewrite two match-with-panic-arm blocks as let...else per clippy's manual_let_else, in sync_rejects_missing_null_and_empty_next_batch and sync_does_not_follow_redirects. - metrics.rs: add #[expect(clippy::expect_used, reason = "test assertions")] on the tests module, matching the same pattern already used in cursor.rs, listener.rs, and router.rs — this test module holds only assertion .expect()/.expect_err() calls. - semeion/envelope.rs: drop the now-unfulfilled #[expect(clippy::indexing_slicing)] on the tests module; the tests use serde_json::json! + .get(), not raw indexing, so nothing in the module triggers the lint anymore. gate-coverage-sovereign-hnsw is the known intermittent (#6952); this branch touches only crates/krites/CAPABILITY_MATRIX.toml call-site counts, no HNSW code.
clippy::doc_markdown flagged both doc lines: "Closed OpenAPI shape..." reads as prose containing a term that looks like code and must be marked as such. The crate already carries this exact convention throughout openapi.rs and health.rs (`OpenAPI`), so this follows existing house style rather than inventing a new form. Before: /// Closed OpenAPI shape for a binding's source selector. After: /// Closed `OpenAPI` shape for a binding's source selector. (and the CommandTier variant's doc line, same fix)
…heir tests Four clippy failures in taxis's own test modules, none touching production code: - loader.rs:969 used std::fs::write directly to corrupt a test fixture key, tripping the crate's disallowed_methods policy (async/testability). Scoped an #[expect(clippy::disallowed_methods, ...)] to the single statement, matching the identical precedent already in oikos.rs::check_writable and validate_tests.rs. - redact.rs's mod tests block suppressed indexing_slicing but never expect_used, so four expect_err() calls added to that module (clippy's expect_used lint also fires on Result::expect_err, not only expect) were bare errors under -D warnings. Added the crate's standard #[expect(clippy::expect_used, reason = "test assertions")] alongside the existing indexing_slicing suppression. - validate_tests.rs's file-level #![expect] list carried unwrap_used and indexing_slicing but not expect_used, so two expect_err() calls in tests added there were likewise bare. Added the matching file-level suppression. - reload.rs's mod tests block had no indexing_slicing suppression at all; two new assertions index vec[0] immediately after asserting bindings.len(), the exact house pattern already suppressed in validate_tests.rs. Added the same #[expect] with the same reasoning. No assertion, comparison, or expected value changed in any of the four files — only lint scope.
…ring Three pedantic clippy failures in crates/aletheia, none behavioral: - dispatch_one had grown to 122 lines (threshold 100). Split the turn-send tail (resolve the nous handle, log, build the ingress marker, send the turn, reply) into a new dispatch_turn helper, leaving dispatch_one as the routing/dedupe/command-interception path it already reads as. No control flow changed; the split point is the existing boundary between 'decide what to do with this message' and 'do the turn'. - command_conversation_id matched msg.group_id.as_deref() with two non-trivial arms (Some/None) purely to pick which two hash_field calls to make; clippy flagged it as destructuring a single pattern. Rewrote as if let ... else, per the lint's own suggestion — same three hashed fields, same order, same values. - start_inbound_dispatch took 8 arguments against this crate's local clippy.toml threshold of 7 (unset there, so it falls back to clippy's default rather than the workspace root's raised threshold of 10). Bundled the seven wiring inputs (config, nous_manager, session_store, ready_rx, the two optional channel providers, shutdown_token) into an InboundDispatchDeps struct, matching the DispatcherParts pattern already used two lines later in the same function for the same reason. Updated the sole caller in runtime/mod.rs accordingly. No timing, ordering, or error handling changed in any of the three sites.
…ction claim The nextest run this PR's compile-blocking clippy fixes finally let through surfaced a genuine (pre-existing, not lint-related) test bug: after capacity 2 evicts 'a' on the third insert, the test re-inserted 'a' and then asserted 'b' was STILL a duplicate. That is not reachable under a strict capacity-2 FIFO: re-inserting 'a' is itself a fourth distinct entry, and it evicts the then-oldest surviving key -- 'b', not 'a' again -- leaving only 'c' and the re-added 'a' in the window. The implementation's eviction is a plain bounded FIFO exactly matching its own doc comment; the test's premise that two keys survive a third eviction under capacity 2 was arithmetically impossible, not a defect in DedupeFilter. Corrected the assertion to check 'c' (the key that is actually still resident) for the expected duplicate, and added a check that 'b' now reads as new -- since it was evicted -- rather than silently leaving that half of the scenario unverified. No production code changed; DedupeFilter's eviction logic is untouched.
The nextest run finally reaching this suite (after this PR's other
compile-blocking fixes) surfaced another arithmetic mistake in a test's
literal expectation, not a defect in identifier().
identifier() keeps exactly the last 4 raw characters of its input and
prefixes them with '...' -- verified elsewhere in this same file by
keeps_only_last_four_chars("12345") => "...2345". For a Matrix id like
"@alice:example.org", the last 4 characters are '.', 'o', 'r', 'g' (the
domain's leading dot falls inside that window), so the correct output is
"....org" (four dots). The test asserted "...org" (three dots), which
silently assumed "org" alone occupied the 4-character window and forgot
the preceding dot -- an assumption the function was never written to honor
and that no other test in this file supports.
Corrected both assertions to the byte-for-byte actual output and documented
why the count is four, not three, so the next reader does not repeat the
same miscount. No production code changed; identifier()'s last-4-characters
contract is untouched and remains uniform across phone numbers and Matrix
ids.
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.



Recovered from a local-only branch on metis during a box lean-down: 45 commits, 71 files, +12,764/-1,993, never pushed and never PR'd. Opening as a draft so the work is discoverable and has a lifecycle rather than living as a bare ref.
What it is
Agora messaging-channel identity and authority hardening:
The commit tail is CI cleanup (clippy pedantic, dead-code, disallowed-method, two miscounted-test fixes), so it was being driven toward a clean gate rather than abandoned mid-thought.
Provenance — this supersedes several dead refs
It is the superset of the whole #7003 effort. Verified by patch-id and, where patch-ids disagreed, by direct file diff (the only deltas were an
.iter().any()→.contains()refactor and doc-comment reordering — later polish on identical logic):fix/agora-channel-hardening— the original PR fix(agora): make channel identity, authority, ingress, and lifecycle fail closed #7003 branch, closed unmerged; a git ancestor of this branchcodex/pr7003-{command-audit,config,integration,listener,matrix,router,signal}— seven per-area splits of fix(agora): make channel identity, authority, ingress, and lifecycle fail closed #7003, none ever PR'd, all content-subsumed hereOnce this lands, those eight refs can be deleted with no loss.
State — read before reviewing
The tip is from 2026-08-28 and main has moved a long way since (PRs #7037–#7106 landed in that window). This needs a rebase and a fresh gate before it is mergeable; it is draft for exactly that reason. Nothing here has been verified against current main — the claim being made is only that the work exists and is unlanded, not that it is ready.