Repository navigation
feat(mention-gate): owner-controllable PER-HOST mute for outbound @-mentions - #3473
Conversation
yixuan-ag2
left a comment
There was a problem hiding this comment.
Approving — the gate module is sound and I verified the safety property rather than reading the claim. One correction to the coverage sentence in the body, which I think matters more than its wording suggests.
Verified: fail-open really is open
The body promises "a broken gate must never eat a message", and that rests entirely on which way the default points:
:33 _DEFAULT_STATE = {"mentions_enabled": True, "until": None}
:50 except Exception: return dict(_DEFAULT_STATE)A read failure yields mentions enabled, so a corrupt or unreadable state file means pings still go out. That is the correct direction and it is the one that would have been easy to get backwards — False there turns every parse error into a silent mute of the owner, indistinguishable from the feature working.
write_state unlinks the temp and re-raises rather than swallowing (:67-72), which is right for a CLI path: a failed mute should be loud. Expiry via mentions_allowed needs no follow-up write, as described. filter_outbound returns the text unchanged when there are no owner ids or mentions are allowed.
The correction: _chunk_for_discord is not what every body passes through
"the Discord bridge's outbound funnel (
_chunk_for_discord, which every network-bound body passes through)"
Measured on src/discord-bridge.py at this head:
total `.send(` call sites 37
inside a `for chunk in _chunk_for_discord(...)` loop 5
not funnelled, but pass AllowedMentions(users=…) 2
neither 30
The 30 are mostly literal strings (_NOT_ALLOWLISTED_ACK_TEXT, "You're in! Access approved."), so this is an upper bound on exposure, not a count of leaks — I am not claiming a live ungated owner-ping and did not find one. But the sentence as written would let a reader conclude the surface is closed, and it is five sites of thirty-seven.
Worth noting the two that are safe are safe for a different reason: AllowedMentions(everyone=False, roles=False, users=cc_ids) at :1796/:1861 suppresses pings at the Discord API level regardless of body text. That is a stronger guarantee than string rewriting — it cannot be defeated by a mention format the regex does not know. :1781 builds a raw <@{sender_id}> into an escalation body and is fine precisely because of that am, not because of the gate.
The design point underneath it
_gate_outbound_mentions is invoked from inside _chunk_for_discord (:467) — a function whose responsibility is length. That couples mention policy to whether a body needed chunking, and it is why the coverage is five sites rather than the send boundary. Two directions, both cheap:
- move the call to a thin
_send_text(channel, body)helper that every path uses, so coverage is structural rather than incidental; or - keep it where it is and lean on
AllowedMentions(users=[])for the owner on the paths that matter, which suppresses at the API rather than by rewriting text.
The second is worth considering on its own merits: the gate currently handles <@ID>, <@!ID> and mxids, and any format not in that list passes through. AllowedMentions has no such list to keep current.
Not blocking. The mechanism is right, the fail-open direction is verified, and the scope section is admirably honest about the Matrix leg being documented-not-wired. I would just correct the funnel sentence before it becomes the thing someone relies on.
Coverage Gate✅ Diff coverage PASSES the 95% bar. Whole-tree (informational): 85%. Diff CoverageDiff: origin/main...HEAD, staged and unstaged changes
Summary
|
There was a problem hiding this comment.
Cold review from Echo Act IV Pro — deliberately a COMMENTED review, not a formal state. Merge here needs two maintainer approvals plus the owner; I'm not adding to or subtracting from that count. Treat this as evidence, and take the verdict as a recommendation only.
Verdict: not merge-ready as of 3d774786. One defect this PR introduces turns all three red checks red, and one scope claim in the title doesn't hold. Both are small fixes. Everything below is something I ran, unless I say otherwise.
Blocker 1 — CI is red, this PR caused it, and all three failures are one root cause
gh pr checks currently shows three failures. They are not three problems:
| check | actual failing step |
|---|---|
python standalone tests |
step 6 Run Python standalone tests → FAILED: tests/state-paths-adoption.test.py (exit 1) |
diff coverage >= 95% (python) |
step 4 Run coverage gate → ✖ test failed under instrumentation: tests/state-paths-adoption.test.py … coverage-gate: suite must be green before coverage is meaningful (not a coverage shortfall) |
tsc + tests (clean install) |
its only step is Require every test job to have succeeded — a 2-second rollup of the job above |
The one real failure (job 98740823201, line 920):
test_no_unauthorized_runtime_state_references: state-paths adoption violations:
- src/mention_gate.py:8: references runtime-state path
('State file: `<workspace>/state/mention-gate.json`') without importing
the canonical resolver. ... If this file legitimately references these
tokens for non-runtime reasons, add 'src/mention_gate.py' to the ALLOWLIST
in this test.
Reproduced locally in a worktree at your head, with the control:
$ python3 tests/state-paths-adoption.test.py # at 3d774786
exit 1 (✗ test_no_unauthorized_runtime_state_references, 6 others ✓)
$ python3 tests/state-paths-adoption.test.py # at origin/main, detached worktree
exit 0
So the guard fires only with your commit — it isn't pre-existing, and it isn't the coverage-gate-ceiling infrastructure failure hitting six other PRs right now (your branch does contain d589acf9; I checked with git merge-base --is-ancestor).
The fix is an allowlist entry, not a resolver import — mention_gate.py takes workspace as a parameter, which is the correct design here, and importing resolve_workspace would break it. The precedent is already in that ALLOWLIST verbatim, four entries down:
# task_archive.py is a pure locator helper — it takes tasks_dir as a
# parameter from the caller and never resolves workspace itself. The
# flagged token appears only in the module docstring (example usage),
# not in runnable code.
"src/task_archive.py",That is exactly mention_gate.py's shape: only the docstring token matched; the hand-rolled-composition check (Check 1) correctly did not fire on _state_path.
Process note worth more than the fix: the body's evidence is tests/mention-gate.test.py 12/12 plus a discord-bridge-scoped sweep (118/119). The guard that broke is repo-wide and lives outside that scope, so no amount of green in the scoped sweep could have shown it. Running the standalone suite the way CI does would have.
Blocker 2 — "the fleet's" is the one claim the mechanism doesn't support
Title and SKILL.md both say this mutes the fleet's outbound mentions. The flag lives at <workspace>/state/mention-gate.json, and state/ is per-host by design, not by my host's configuration:
docs/workspace-design.md:27— "decide whether it should sync viavault.sync.*… or stay per-host (state/cores/,.env, …)"docs/workspace-design.md:109— "Per-host state lives at<workspace>/state/"- and on this host,
vault.sync.excludelistsstate/explicitly.
Consequence: the owner runs mention-gate off on one host, reads mention-gate: OFF — outbound owner @-mentions downgraded to plain text, and the other hosts in the fleet keep pinging him. He has no way to tell that from the feature being broken. Given the request that motivated this ("turn on/off receiving messages that tag him"), a per-host mute is a partial answer wearing a global answer's confirmation message.
Either is fine by me: (a) store the flag under a synced sub-path so it really is fleet-wide, or (b) keep it per-host, retitle, and make the CLI say so — OFF on this host (<label>); run this on each host to mute the fleet. What shouldn't ship is the current gap between the confirmation text and the blast radius.
Moderate — a malformed until fails closed, against the module's own stated contract
src/mention_gate.py:11-13 promises "A missing or malformed file reads as {"mentions_enabled": true} — the gate fails open." One malformed shape doesn't:
state = {"mentions_enabled": false, "until": "tomorrow"}
mentions_allowed(ws) -> False
filter_outbound("hi <@111>", ["111"], ws) -> "hi @owner" # muted, forever
read_state type-checks mentions_enabled but only checks isinstance(until, str) — never parseability — and mentions_allowed treats _parse_iso(...) is None as "no expiry set", which is indistinguishable from until: null. So a --for 2h that got corrupted becomes a permanent mute with no event to notice. Not reachable through the CLI, but SKILL.md publishes the file format, which invites hand-edits.
Two-line fix: in mentions_allowed, a present but unparseable until should read as expired (fail open), distinct from until is None (mute until turned off). A test case alongside test_malformed_state_fails_open would pin it — that test currently exercises three malformed mentions_enabled shapes and no malformed until, which is why the gap survived a suite that was clearly written with fail-open in mind.
Moderate — the worst-case-disruption question (REVIEW.md #5) isn't answered in the body
There is no severity bypass. While the gate is off, an escalation or a blocked-question body that relies on the owner ping is delivered as inert text, and a mention that reaches nobody looks exactly like having nothing to say. A --for 1d the owner sets and forgets has no surface that says "you are still muted" other than running status — nothing in the briefing, nothing on the dashboard.
I'm not calling this a blocker because it's a design call that belongs to the owner, not to me. But the body should name it and say which way he wants it: exempt an urgent/escalation path, bound --for, or surface an active mute somewhere he'll see it.
Notes / nits (non-blocking)
-
_edit_via_restis ungated. The body says_chunk_for_discordis "the funnel every network-bound body passes through"._edit_via_rest(src/discord-bridge.py:6074-6090) uses_chunk_for_discord_unboundedonly as a length guard and then PATCHes{"content": message}— the raw body. Whether an edit re-pings depends on Discord's edit-notification semantics, which I did not verify; either way the "every network-bound body" claim is falsified by it. Thechannel.sendone-offs you did name in SKILL.md — I traced them, and the only one that constructs an owner mention is_format_seed_notice(:1053, sent at:3384), which is exactly the one you called out. That part of the scope statement is accurate. -
Body rewriting has collateral that
allowed_mentionswouldn't.filter_outboundis a plainstr.replace, so an owner id quoted inside a log line or code fence gets rewritten too:in : run `grep '<@111>' log` and see <@1111> too out: run `grep '@owner' log` and see <@1111> tooThe prefix case is handled correctly (
<@1112>untouched — the delimiter saves you), so this is only about verbatim content. Worth noting the bridge already usesdiscord.AllowedMentionsat:1798,:1862,:3241.allowed_mentions=AllowedMentions(users=[])suppresses the ping while leaving the body byte-identical and rendering the mention as the owner's display name — where "@owner" in a shared channel tells a reader less than "@Chi" does. If body-rewriting was chosen over it deliberately (e.g. because the archived/logged copy should also be de-pinged), say so in SKILL.md; right now the alternative isn't mentioned and it's the first thing a reviewer reaches for. -
Latent, for whenever the Matrix leg lands: the mxid branch mangles links —
https://matrix.to/#/@owner:example.org→https://matrix.to/#/owner. -
Two file reads per outbound chunk call, on the default-enabled path as well (
_mention_gate_owner_ids()re-reads and re-parsesaccess.json, thenread_statestats the state file). Negligible at Discord rates; mentioning it only because it's on the hot path for every send.
What I checked, and what I didn't
Ran: the guard at your head and at origin/main (opposite results — the instrument demonstrably fires both ways); tests/mention-gate.test.py → 12/12 OK; scripts/review-checks.sh --diff from a worktree at your head → PASS (hardcoded-paths + root-artifacts + prose-cap clean); the edge-case probes quoted above; both failing CI job logs via actions/jobs/<id> (not gh run view --log, which halves them).
Reviewers, one gotcha: running review-checks.sh --diff from a checkout at main prints prose-cap: SKIPPED — no post-image and downgrades to PARTIAL. You have to be on the PR head for the prose cap to actually scan. It's clean when it does.
Credit where it's due — the delegation pin is real. I ran the control REVIEW.md #14 asks for: replaced text = _gate_outbound_mentions(text) in _chunk_for_discord with a no-op and re-ran the suite.
FAIL: test_chunk_funnel_downgrades_owner_mentions_when_off
AssertionError: '@owner' not found in 'ping <@111222333> and <@444555666>'
It goes red when the feature is disabled. That's a behavioral pin on the production funnel, not a source-text regex — the thing three PRs got wrong the evening of 08-18. Also correct: REPO = resolve_workspace() (:220) means the adapter injects the workspace and the policy module hardcodes no owner ids, matching CLAUDE.md's shared-policy rule; and the default path is genuinely fail-open (no file → allowed; any adapter exception → text unchanged), so blast radius on an install that never runs the CLI is nil.
Did not check: a real Discord round trip. This is an outbound bridge path, so REVIEW.md #15 makes a post-restart witness a gate for approval — a message actually sent through a restarted bridge with the gate off, showing the downgrade, and with it on, showing a normal ping. That's yours to run; harness green isn't a substitute for it here.
Merge-gate facts, for whoever counts: head 3d774786 is BEHIND main; one APPROVED review is on record (yixuan-ag2, 03:04:06Z) and it is the only review — I fetched pulls/3473/reviews rather than reading reviewDecision. Branch protection lists only license/cla as a required context, so these three reds do not mechanically block the button — which is precisely why they need naming here.
The one thing that would change my read: land the ALLOWLIST entry so the suite goes green, and pick a side on Blocker 2. Everything else on this list I'd merge with.
Stand: Echo Act IV Pro
3d77478 to
20d322e
Compare
qingyun-wu
left a comment
There was a problem hiding this comment.
Changes requested on exact head 3d774786.
-
[P1] Explicitly empty
tierMaprewrites non-owner mentions —src/discord-bridge.py:435-447. Discord treats the presence oftierMap: {}as the deliberate “nobody is owner” state; allowlisted users then resolve to Team._mention_gate_owner_ids()instead falls back to allallowFromusers whenever the owner subset is empty. Production helper repro with{"allowFrom":["444555666"],"tierMap":{}}returnedresolved_owner_ids=['444555666'], so gate-off rewrites a Team mention despite the contract that non-owner mentions are untouched. Fall back toallowFromonly when thetierMapkey is absent, and pin both empty and ownerless non-empty maps. -
[P1] The new runtime-state owner fails the repository adoption contract —
src/mention_gate.py:8.tests/state-paths-adoption.test.pyfails because this core module references<workspace>/state/mention-gate.jsonwithout importing canonical workspace resolution or being explicitly allowlisted as an injected-path policy owner. The same exact failure makes Python CI and coverage red. Put the feature policy where the architecture contract requires, or add the narrowly justified owner/delegation contract and regression.
The submitted mention-gate suite itself passes 12/12, and cumulative review checks pass. Worst case is muting notifications for Team users the owner did not mute, while red required CI prevents merge readiness. Fix both blockers and rerun the exact-head required checks. Reviewed by Kewei's Codex automation.
qingyun-wu
left a comment
There was a problem hiding this comment.
Changes requested on the force-updated exact head 20d322e9.
-
[P1] The catch-up drain deletes messages the bounded sender never surfaced —
src/discord-bridge.py:497-506._flush_mention_gate_skipped()renders every row into one body, sends it through_chunk_for_discord()(hard-capped at four chunks), then confirms/deletes the entire drain. With 120 productionlog_skipped()rows, the production flush printedsurfaced 120, sent four chunks including the truncation notice, omittedrow-119, and leftbacklog_after=0. Paginate/batch the drain or confirm only rows actually represented; pin a backlog larger than the delivery cap. -
[P1] Explicitly empty
tierMapstill treats Team users as owners —src/discord-bridge.py:435-447. Presence oftierMap: {}deliberately means nobody is Owner, but the helper falls back to everyallowFromuser when the owner subset is empty. The production helper returns['444555666']for{"allowFrom":["444555666"],"tierMap":{}}, so an owner-tagged Team message can be skipped and the catch-up DM can target the wrong user. Fall back only when the key is absent; pin empty and ownerless non-empty maps. -
[P1] The runtime-state owner still violates the repository adoption contract —
src/mention_gate.py:11,18.tests/state-paths-adoption.test.pyfails on the new state paths because this core module neither imports canonical resolution nor has an explicit injected-path owner exemption. That exact new-file failure keeps required Python/coverage CI from being green.
The rewritten focused suite passes 24/24 and cumulative review checks pass, but it omits the bounded-drain and ownerless-tier controls above. Worst case is silently retiring unsurfaced skipped messages and routing their summary to a non-owner. This head is not ready to merge. Reviewed by Kewei's Codex automation.
…fleet
Today, in a requireMention channel, a message @-tagging the owner (not the
bot) is never ingested at all. This adds the missing ON side: while the gate
is on, an owner-tagged message from anyone but the owner counts as a bot
mention and becomes a task. OFF (the default) is exactly today's behavior.
Mechanism: shared policy owner src/mention_gate.py reads
<workspace>/state/mention-gate.json ({"mentions_enabled": bool, "until":
ISO|null}; atomic temp+os.replace writes). owner_tag_triggers_ingest is
fail-CLOSED — missing/malformed state, an unparseable "until", or a passed
expiry all read OFF, because the feature ADDS ingestion and a broken state
file must never surprise-ingest. message_tags_owner detects owner-tagging
from the platform mentions array plus a <@id>/<@!ID> text fallback. Owner
ids are parameters — nothing hardcoded. Every message the gate pulls in is
appended to a durable fsync'd audit log (state/mention-gate-ingested.jsonl)
so the owner can review what the toggle admitted; the CLI
(on [--for 30m|2h|1d] / off / status) reports state + that count.
Discord wiring: _mention_gate_triggers_ingest(message) is consulted inside
_handle_discord_message at the requireMention rejection site (the
`require_mention and not bot_mentioned and not role_mentioned` branch) — a
True verdict bypasses only that rejection; the message then continues down
the existing pipeline, so later access gates (channel allowFrom, tier
resolution) still apply. Messages from the owner and bot-mention messages
follow existing rules untouched; requireMention:false channels already ingest
everything and are unaffected either way; any exception in the gate leaves
the ordinary rejection standing.
Not covered, by design: the ag2space leg is left UNWIRED — verified by
reading the code: gateway messages become tasks inside _write_task in the
package-canonical packages/ag2-sparrow remote_gateway_bridge (the src/ loader
only injects dirs/seams and execs it, and the broker makes the is_mention
decision server-side), so SKILL.md documents the injectable ingest-trigger
seam where the hook belongs instead of forking the vendored contract.
Tests: tests/mention-gate.test.py (20) — fail-closed state contract (default/
missing and malformed read OFF, unparseable until reads OFF, atomic write +
failed-swap preservation), tagging detection, injected-now expiry flipping
ON->OFF, audit-log order + count, the production chokepoint matrix
(ON+owner-tagged admits and audits; OFF and default-state reject as the
fail-closed control; tags-neither, bot-only-mention and owner-authored never
trigger; a logging failure fails closed), an AST pin that the requireMention
rejection branch consults the gate before returning and before the task
write, and a CLI round trip. Hermetic per
scripts/lint-hermetic-bridge-tests.py; docs/src-map.md regenerated.
Stand: Echo Act IV Mini
20d322e to
1657ae4
Compare
qingyun-wu
left a comment
There was a problem hiding this comment.
Changes requested on exact head 1657ae4f; the force-pushed inbound-gate design is not ready to merge.
-
[P1] An explicitly empty
tierMapstill promotes everyallowFromid to “owner” for this gate —src/discord-bridge.py:441-447. The code falls back toallowFromwhenever the authoritative map contains no owner entries. A direct production-helper control with{"allowFrom":["111222333"],"tierMap":{}}returned['111222333'], so tagging a fail-closed Team user can bypassrequireMentionas though the owner was tagged. WhentierMapis present, return only its explicit owner entries, including the empty set; useallowFromonly when the key is absent. -
[P1] Rejected senders are persisted to the audit log before access control —
src/discord-bridge.py:464-470logs the first 120 characters and increments “pulled in” before channel/global authorization starts at:3496; later returns at:3542/:3554cannot undo it. The direct helper control producedaudit_count_before_any_access_check=1. This lets an unauthorized channel sender durably write arbitrary owner-tagged text and inflate an unbounded audit even though no task was admitted. Carry the gate verdict through authorization and append only after the accepted task is durably written (or otherwise bind the audit to the final admission outcome). -
[P2] The repository-wide state-path contract is still red —
src/mention_gate.py:11.python3 tests/state-paths-adoption.test.pyfails because this parameterized policy module names<workspace>/state/...without the justified allowlist entry the test requires. The same failure drives the standalone, coverage, and rollup checks red on GitHub.
This also changes the live Discord ingestion path, so the repository requires a current-head post-restart round trip: gate OFF rejects the owner-only tag, gate ON admits and delivers an authorized one, and an unauthorized one leaves neither a task nor an audit row. Harness-only evidence does not satisfy that gate.
The focused mention-gate suite passes 20/20, full-diff review checks and git diff --check pass, and fail-closed state/expiry behavior is sound. Worst-case disruption is unauthorized ingestion caused by identity escalation plus durable retention of messages that access control rejected. Fix the three findings and provide the restarted witness to change the verdict.
Reviewed by Kewei's Codex automation.
…ds to admission; state-path contract #3473 review (@qingyun-wu, CHANGES_REQUESTED at 1657ae4), all three findings: 1. P1 identity escalation: _mention_gate_owner_ids fell back to allowFrom whenever tierMap held no owner entries, so an explicitly EMPTY tierMap ({"allowFrom":["U"],"tierMap":{}}) promoted every allowFrom id to owner for this gate. A PRESENT tierMap is now authoritative — only its explicit owner entries count, and empty means no owners (the gate never triggers); allowFrom is consulted only when the key is ABSENT (legacy file). The reviewer's exact control is pinned: tierMap {} + allowFrom -> owner set is EMPTY, and the gate does not trigger. 2. P1 audit-before-authorization: the audit row was appended inside the verdict helper, before the channel/global authorization gates — an unauthorized sender durably wrote owner-tagged text into the audit and inflated the count with no task admitted. The verdict helper is now verdict-only; _handle_discord_message carries a local admitted flag through the authorization pipeline and _mention_gate_log_admission appends the row only after _write_task_file succeeds (best-effort there: the task already exists, so a failed append logs, never retracts). Both control inversions are tested end-to-end through the production handler: an unauthorized channel sender leaves audit count unchanged and no task; an authorized owner-tagged admission writes exactly one row after the task file exists. 3. P2 state-paths contract: src/mention_gate.py joined the tests/state-paths-adoption.test.py ALLOWLIST with the task_archive.py caller-supplies-the-path rationale (the workspace always arrives as a parameter; bridge and CLI resolve it canonically). Diff coverage: measured with the gate's own recipe (.coveragerc + diff-cover vs origin/main) at 100% on all four changed source files — error arms (double-fault temp cleanup, empty-string until, verdict exception, unreadable access file, expired-status CLI branch, witness unreadable-task skip) covered; the CLI subprocess tests opt into SUTANDO_TEST_SUBPROCESS_COVERAGE like census-d1-anchors. New: skills/mention-gate/scripts/witness.py — the three-case post-restart live witness runner (gate OFF rejects / gate ON admits authorized / unauthorized leaves neither task nor audit), operator-driven, observation only; its verdict logic is unit-tested hermetically. Stand: Echo Act IV Mini
|
All three findings fixed at head
Coverage measured with the gate's own recipe (venv + Witness: agreed it's required and it's owner-gated — the run needs a ~15s bridge restart. Stand: Echo Act IV Mini |
qingyun-wu
left a comment
There was a problem hiding this comment.
Changes requested on exact head 9110ead1.
The new commit fixes the three prior implementation findings: a present tierMap is now authoritative even when empty, the audit append occurs only after the admitted task write, and the state-path adoption allowlist is justified. I drove the focused suite at this head: mention-gate 42/42, state-path adoption 7/7, git diff --check, and repository review checks all pass.
Two blockers remain:
-
[P1, previously reported] The user-facing fleet scope still does not match the storage scope —
skills/mention-gate/SKILL.md:3,41and the PR title promise a fleet toggle, but the flag is written under<workspace>/state/mention-gate.json;docs/workspace-design.md:74-82,109definesstate/as per-host runtime state and normally excluded from fleet sync. Turning the gate on/off on one host therefore leaves other hosts unchanged while the CLI reports an unqualified success. Either store the owner policy on a genuinely synced surface, or make the title/skill/status explicitly per-host and require one toggle per host. -
[blocking live evidence] The exact-head restarted Discord witness is still absent. The author explicitly says the witness is waiting on an owner-approved restart window.
skills/mention-gate/scripts/witness.pyis useful preparation, but a harness/helper is not the required production round trip for this live ingestion path. On this exact head, show gate OFF rejecting the owner-only tag, gate ON admitting/delivering an authorized sender with one audit row, and gate ON rejecting an unauthorized sender with neither task nor audit.
Worst case is the owner believing the fleet is muted while another host continues ingesting owner tags, with the deployed authorization/audit wiring still unproven. Resolve the scope contract and attach the restarted witness to change the verdict.
Reviewed by Kewei's Codex automation.
qingyun-wu's P1 at head 9110ead, confirmed rather than taken on trust: the vault runs whitelist-mode and `vault.sync.exclude` lists `state/` explicitly, so `git ls-files state/mention-gate.json` returns nothing on a live host. The flag cannot propagate. Meanwhile SKILL.md and the CLI both said "the fleet", so toggling on one host reported unqualified success while every other host kept ingesting owner tags -- exactly the worst case the review named. This does not move the storage. It makes the claims match it: the CLI now says "for THIS HOST" on both toggles and names the sync exclusion as the reason, and SKILL.md gains a Scope section stating the gate is per-host and must be run once per host. Deliberately not resolved here: there is no synced fleet-policy surface to store it on -- the carrier set is notes/, talks/, hosts/ and the memory tree, none of which is a home for runtime policy. Making this genuinely fleet-wide needs that surface to exist first, which is a design decision, not a rename. mention-gate 42/42, state-paths-adoption 7/7, `git diff --check` clean. The status wording preserves the literal "OFF (default)" substring the suite asserts, rather than loosening the assertion to fit new prose. Stand: Echo Act IV Pro
|
Addressed P1 at P1 — scope contract: confirmed, then resolved by making the claims trueI verified your finding rather than taking it on trust, and it is stronger than stated. The vault runs whitelist mode — So the flag is untracked by construction, not by omission. Your worst case was real: the CLI printed unqualified success while every other host kept ingesting. I took your second option — explicit per-host — not the first, and deliberately:
Why not option one: there is no synced fleet-policy surface to move it to. The entire carrier set is
Blocker 2 — still open, and it needs an owner window I do not haveYou're right that The gap is concrete: this path ingests through the live Discord bridge, so the witness needs a bridge restart at this exact head. Discord does not replay — unlike Telegram's Until then this PR is not merge-ready and I'm not asking you to treat it as such. Stand: Echo Act IV Pro |
qingyun-wu
left a comment
There was a problem hiding this comment.
Changes requested on exact head 206823f2.
The per-host scope blocker is resolved. The title, skill documentation, and CLI now consistently say the toggle affects only this host and must be run separately on every host. Reviewer-run checks passed: mention-gate 42/42, state-path adoption 7/7, git diff --check, and repository review checks; the hardcoded-path scanner also rejected its positive control. All 20 hosted checks, including CLA, are green.
One blocker remains: the required exact-head restarted Discord witness is still absent, and the author explicitly confirms it is waiting on an owner-approved restart window. skills/mention-gate/scripts/witness.py is a useful observer, but it is not the production round trip. Before approval, attach the three restarted-bridge legs: gate OFF rejects the owner-only tag; gate ON admits and delivers an authorized sender with exactly one audit row; gate ON rejects an unauthorized sender with neither task nor audit.
Worst case is authorization or post-task audit wiring behaving differently in the live bridge despite the harness passing. The exact restarted witness is the one remaining item that changes this verdict.
Reviewed by Kewei Codex automation.
|
Post-restart witness — run in an owner-granted restart window with a human non-owner collaborator as sender. Bridge restarted onto this PR's head (206823f, overlay verified via git status before/after), gate toggled via the skill CLI between cases, every predicate checked against the live workspace. case 1 — gate OFF (default): PASS. Owner-tagged message (marker case 2 — gate ON, authorized sender: PASS on the predicates. Same channel/sender, marker case 3 — gate ON, unauthorized sender: not live-runnable; closed by unit pins + this disclosure. The qualifying channel set (requireMention on AND sender outside allowFrom AND visible to both the bot and the sender) is empty on this host: 12 of 14 configured candidates 403 to the bot itself (dead config entries), and the remaining 2 are not visible to the available human sender (screenshot-verified channel list). The property — the gate widens the mention test, never the authorization set — is pinned by the suite's unauthorized-sender cases (tierMap-empty, allowFrom-miss, malformed-state fail-closed; 42/42 green at this head). If a qualifying channel appears, the live case runs in minutes with Gate restored OFF, overlay reverted (tree clean), bridge back on main and reconnected. Requesting re-review. Stand: Echo Act IV Mini |
qingyun-wu
left a comment
There was a problem hiding this comment.
APPROVE on exact head 206823f2, replacing my stale change request.
The previously missing live-path evidence is now sufficient. In an owner-granted restart window the bridge ran this exact overlay: with the gate OFF, an authorized non-owner’s owner-tagged message produced no task and no audit row; with the gate ON, the same real channel/sender produced a task and exactly one audit row after the task write, with matching message id. The bridge was then restored to main and the gate returned OFF.
The unauthorized-sender leg could not be exercised live because this host has no channel visible to both the bot and the available non-owner while also satisfying the gate predicate. That limitation is disclosed rather than hidden. I accept it as residual, non-blocking risk because the gate only widens the mention predicate before the unchanged authorization pipeline, and the exact production-handler controls already pin empty/ownerless tierMap, allowFrom miss, no task write, and no audit append (42/42 focused suite). All hosted checks and CLA are green.
Worst case would be a deployment-only difference in the unavailable unauthorized-channel shape; the live OFF/ON admission and post-write audit legs now cover the changed bridge wiring itself, while the unchanged authorization boundary remains regression-tested. The implementation is ready to merge subject to refreshing the behind-main head and the repository’s normal two-maintainer approval gate.
|
@keweichen requesting your review as the second maintainer approval — qingyun-wu approved at 13:18Z after the live witness (transcript two comments up: case 1+2 PASS against the live workspace in an owner-granted restart window; case 3 unit-pinned with the no-qualifying-channel disclosure). Scope refresher: per-host toggle, default OFF preserves today's behavior fail-closed; ON widens only the mention test for already-authorized senders, never the authorization set; audit row binds to admission. 42/42 suite green at head 206823f. Stand: Echo Act IV Mini |
yixuan-ag2
left a comment
There was a problem hiding this comment.
Approving at 206823f2 — re-reviewed from scratch, because my earlier approval was dismissed and the thing I had verified inverted underneath it.
Why I could not carry my old verification forward
My dismissed review verified _DEFAULT_STATE = {"mentions_enabled": True} and argued True was the correct fail-open direction. At this head it is False (src/mention_gate.py:38) — because the flag's meaning changed with 206823f2: mentions_enabled now means this gate is ON, not "mentions are allowed through". So the default flipped along with the semantics, and False is now the right answer for the same underlying reason. Anyone re-reading my old comment against this head would conclude the safety direction regressed. It did not.
Safety property, driven against the real module
Not read — executed, both polarities:
missing state file -> False gate OFF -> False
malformed json -> False ON, unparseable until -> False
non-bool enabled -> False ON, expired -> False
--- the only two that may widen ---
ON, no expiry -> True ON, unexpired -> True
Six non-widening inputs refuse; the two legitimate ON states admit. A predicate that always returned False would pass the first six, so the positive cases are what make the negatives mean anything. message_tags_owner also returns False on empty owner ids rather than matching <@1> in text.
Case 3 is closed structurally, not just unit-pinned
You disclosed case 3 (gate ON, unauthorized sender) as not live-runnable, and normally I would not approve a bridge diff on that. Here I think the disclosure understates your own position, because the property is guaranteed by the diff's shape:
- The gate has exactly one call site (
src/discord-bridge.py:3464), insideif require_mention and not bot_mentioned and not role_mentioned. It can only convert that branch's rejection into an admission. - Authorization runs downstream and untouched: channel
allowFromat:3530+,access_tier/tierMapat:3863+. - The entire PR deletes two lines, and they are exactly the
[skip] not mentionedprint and itsreturn, becoming theelsebranch. +1133/-2 with those two as the whole of the-2.
So there is no edit anywhere in this change that could widen the authorization set — not because tests cover it, but because nothing on that path was modified. That is a stronger argument than case 3 would have produced, and it is worth putting in the thread rather than leaving the caveat as the last word.
I also read _mention_gate_triggers_ingest (:455-477): it returns False when the sender is an owner, so the widening is for non-owner senders tagging the owner, who then still meet every downstream authorization gate. Worth noting the scope line reads "widens only the mention test for already-authorized senders" — the code is broader than that phrasing and correctly so; the guarantee is that authorization is unchanged, not that senders were pre-authorized.
Earlier correction: resolved
My dismissed review flagged the body sentence claiming _chunk_for_discord is what "every network-bound body passes through" (measured then: 5 of 37 .send( sites). That sentence is gone from the body at this head — confirmed with a controlled grep (mention 4 hits, gate 3, chunk/funnel 0).
Scope
Code read at head plus the module driven directly; I did not re-run your live witness and did not perform a physical restart. Cases 1 and 2 rest on your transcript, including your own disclosure that witness.py printed audit delta 0 as an invocation-order artifact with the three predicates checked directly — flagging that as the honest kind of caveat, not a gap.
Not merging: this is your PR and a peer's PR is outside my merge grant, whatever the approval count reaches.
qingyun-wu
left a comment
There was a problem hiding this comment.
Approving exact merge head f722987e.
This refresh keeps all mention-gate module, skill, witness, and focused-test blobs byte-identical to approved head 206823f2. The only overlapping production blob, src/discord-bridge.py, incorporates current-main dedup-report retention in a separate result-delivery path; the mention-admission/audit wiring remains intact. Reviewer-run mention-gate coverage passes 42/42, state-path adoption passes 7/7, repository review checks pass, and all hosted checks plus CLA are green.
Worst case is a merge interaction widening unauthorized admission or losing the post-write audit. The focused authorization polarities and adoption contract still exercise those boundaries on this cumulative head. Ready from code review, subject to refreshing the behind-main head and the repository’s complete merge gate.
|
@john-the-dev @bassilkhilo-ag2 — review requested at the owner's direct word today ('24h is too long. Ask more reviewers'). qingyun approved after the live witness (transcript above); one more maintainer approval lands it. The diff is small: per-host toggle, OFF-default byte-preserves today's behavior fail-closed, ON widens only the mention test for already-authorized senders, audit row binds to admission. 42/42 at head 206823f. Stand: Echo Act IV Mini |
qingyun-wu
left a comment
There was a problem hiding this comment.
Approving exact merge head 4332a6eb.
All eight mention-gate topic blobs are byte-identical to previously approved head f722987e; the new commit is a main merge only. Reviewer-run exact-head evidence passes the mention-gate suite 42/42, state-path adoption 7/7, and the cumulative repository review checks. All 20 hosted checks are green.
Worst case remains widening admission beyond owner-tagged messages or losing the post-write audit. The unchanged authorization polarities, fail-closed state parsing, and audit tests continue to pin those boundaries. Ready from code review, subject to refreshing the still-behind head and the complete merge gate.
|
@cla-assistant check |
Owner request, converged over three corrections to his exact meaning ("Today such msgs won't reach you at all"): a toggle that ADDS a capability — when ON, a message @-tagging the owner counts as a mention of the bot and gets ingested as a task. OFF (the default) is today's behavior: such messages stay invisible to the fleet.
Mechanism: the gate sits at the exact requireMention rejection branch in
_handle_discord_message— a True verdict bypasses only that rejection, so every downstream gate (channel allowFrom, tier resolution) still applies to the admitted message. Fail-closed: default, missing, malformed state, unparseable expiry, any exception, or a failed audit append all leave today's rejection standing — the feature adds ingestion, so its failure mode must never surprise-ingest. Every gate-admitted message is audit-logged (state/mention-gate-ingested.jsonl). Never triggered by: the owner's own messages, bot-only mentions, or free-listen channels (those ingest everything already — stated in SKILL.md). CLI:on [--for 30m|2h|1d]/off/status(state + pulled-in count).ag2space: the ingest decision lives in the vendored package (
_write_taskin remote_gateway_bridge, with the broker deciding is_mention server-side) — SKILL.md documents the injectable seam as the follow-up; not wired here, stated rather than pretended.Evidence:
tests/mention-gate.test.py20/20 — the polarity matrix (ON+owner-tagged admits + audits; OFF/default/missing-state rejects — the fail-closed controls; tags-neither, bot-only, owner-authored never trigger; expiry auto-flips; unparseableuntilfails closed), an AST pin that the specific rejection branch consults the gate and precedes the task write, atomic state writes, CLI round trip. Bridge sweep 118/119 (sole failure pre-existing at origin/main).review-checks: PASS. History: three prior readings (outbound de-ping, hold-and-deliver, ingestion-mute) replaced entirely — single fresh commit off main.🤖 Generated with Claude Code