Skip to content

fix(dm-result): honor tierMap + strip file markers from REST DM body - #985

Merged
sonichi merged 2 commits into
sonichi:mainfrom
VasiliyRad:fix/dm-result-tier-map-and-file-markers
May 22, 2026
Merged

sonichi merged 2 commits into
sonichi:mainfrom
VasiliyRad:fix/dm-result-tier-map-and-file-markers

Conversation

@VasiliyRad

Copy link
Copy Markdown
Contributor

Why

Three real bugs in src/dm-result.py — the REST-only fallback delivery path used by poll_dm_fallback when voice / the live bridge can't reach the user:

Bug A: _resolve_owner_id ignored tierMap

src/discord-bridge.py:poll_proactive honors the documented priority chain $SUTANDO_DM_OWNER_ID → tierMap[uid] == "owner" → bot-filter fallback. dm-result.py only honored env override + bot-filter — drift between the live bridge and the fallback path.

Visible symptom: admin tier-tagged an owner in access.json; live bridge routes proactive DMs correctly, but the fallback path could pick a different non-bot (or a bot, in the degraded-network case where /users/{id} lookups all fail and the function falls back to allow[0]).

Fix: insert tierMap branch between env override and bot-filter. Defensive: only accept tier-tagged owner if still in allowFrom.

Bug D: send_dm delivered [file:|send:|attach:] markers as literal text

poll_dm_fallback shells out to dm-result.py --file results/task-X.txt to deliver result files. Result files often embed file markers:

Here's the screenshot you asked about: [file: /tmp/sutando-x.png]

The live bridge attaches the file via discord.File(path) on the WS-connected client. dm-result is REST-only and has no multipart upload path — so the marker landed verbatim in the DM and the file was silently dropped.

Fix: mirror discord-bridge's _split_file_markers helper. send_dm strips markers before chunking and logs the dropped file list. Actual REST multipart upload is a follow-up — explicitly called out in the inline comment.

Bug C (secondary): empty-body-after-strip posted "" → Discord 400

Pre-fix: chunks = list(_chunk_for_discord(text)) or [text]. For a whitespace-only or all-marker body, the chunker yields nothing → empty list → falsy → falls back to [""] and POSTs empty content. Discord returns HTTP 400 / code 50006.

Fix: detect empty-after-strip and skip the /messages call entirely. Reports no-op, returns True (not an error).

Tests

tests/dm-result-send-dm.test.py (5 cases). Mocks urllib.request.urlopen with a recording fake (_FakeTransport) and probes the full REST flow. The fake raises AssertionError on any unmocked request, so future regressions fail loudly.

Case Pins
test_tier_map_resolution_skips_bot_lookup Bug A regression guard. tierMap-tagged owner resolved directly; NO /users/{id} calls.
test_bot_filter_fallback_still_works_without_tier_map Existing behavior preserved.
test_file_markers_stripped_from_body Bug D regression guard. Sent body has no [file: literal; surrounding text preserved.
test_empty_body_after_marker_strip_does_not_post_messages Bug C regression guard. All-marker body → zero /messages POSTs.
test_env_override_skips_access_json_entirely Priority-order pin.

Test plan

  • python3 tests/dm-result-send-dm.test.py — 5/5 pass
  • Syntax check
  • Manual: with tierMap set in access.json, verify dm-result picks the tier-tagged owner

🤖 Generated with Claude Code

Three real bugs in the REST-only fallback delivery path used by
`poll_dm_fallback` when voice / the live bridge can't reach the user:

## Bug A: `_resolve_owner_id` ignored `tierMap`

`src/discord-bridge.py:poll_proactive` honors the documented priority
chain `$SUTANDO_DM_OWNER_ID` -> `tierMap[uid] == "owner"` -> bot-filter
fallback. `dm-result.py` only honored env override + bot-filter — drift
between the live bridge and the fallback path. An admin who tier-tagged
the owner in access.json saw the live bridge route correctly, but the
fallback could pick a different non-bot from allowFrom (or a bot, in
the degraded-network case where /users/{id} lookups all fail).

Fix: insert the tierMap branch between env override and bot-filter.
Defensive: only accept tier-tagged owner if still present in allowFrom
— stale tags from removed users must not resolve a delisted owner.

## Bug D: `send_dm` delivered `[file:|send:|attach:]` markers as
## literal text

`poll_dm_fallback` shells out to `dm-result.py --file results/task-X.txt`
to deliver result files when the live bridge can't. Result files often
embed file markers:

    Here's the screenshot you asked about: [file: /tmp/sutando-x.png]

The live bridge attaches the file via `discord.File(path)` on the
WS-connected client. dm-result is REST-only and has no multipart
upload path — so the marker landed verbatim in the DM and the file
was silently dropped.

Fix: mirror discord-bridge's `_split_file_markers` helper.  `send_dm`
now strips markers from the body before chunking and logs the dropped
file list. Actual REST multipart upload is a follow-up — flagged in
the inline comment.

## Bug C (secondary): empty-body-after-strip posted `""` -> Discord 400

Pre-fix: `chunks = list(_chunk_for_discord(text)) or [text]`. For a
whitespace-only or all-marker body, the chunker yields nothing ->
empty list -> falsy -> falls back to `[""]` and POSTs empty content.
Discord returns HTTP 400 / code 50006.

Fix: detect empty-after-strip and skip the /messages call entirely.

## Tests

`tests/dm-result-send-dm.test.py` (5 cases). Mocks
`urllib.request.urlopen` with a recording fake (`_FakeTransport`) and
probes the full REST flow. The fake raises `AssertionError` on any
unmocked request, so future regressions fail loudly.

  - `test_tier_map_resolution_skips_bot_lookup` (Bug A regression
    guard + perf assertion that the wasted /users/{id} call is gone)
  - `test_bot_filter_fallback_still_works_without_tier_map`
    (existing behavior preserved)
  - `test_file_markers_stripped_from_body` (Bug D regression guard)
  - `test_empty_body_after_marker_strip_does_not_post_messages`
    (Bug C regression guard — the fake transport raises
    AssertionError on an unmocked POST, so a future regression here
    fails loudly)
  - `test_env_override_skips_access_json_entirely` (priority-order
    pin)

All 5 pass locally.

Co-Authored-By: Vasiliy's Sutando <noreply@anthropic.com>
@CLAassistant

CLAassistant commented May 21, 2026 •

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@sonichi

sonichi commented May 21, 2026

Copy link
Copy Markdown
Owner

LGTM. Three real bugs, three independent regression tests — the right granularity.

Bug A is the most consequential — drift between discord-bridge.py:poll_proactive's documented priority chain ($SUTANDO_DM_OWNER_ID → tierMap[uid]=="owner" → bot-filter) and dm-result.py's implementation which was missing the tierMap step. The defensive allowFrom recheck before accepting a tier-tagged owner matters — a stale tierMap entry for a removed user shouldn't grant access.

Bug D (file markers landing as literal text) is the easiest to silently miss in normal use. dm-result.py is REST-only with no multipart upload path; mirroring _split_file_markers is the safe interim. The PR comment explicitly flagging "REST multipart is the proper follow-up" is the right call.

Bug C (empty-after-strip → Discord 400) is the kind of post-fix-of-fix that's easy to skip but matters — all-marker bodies hit this. Returning True (no-op success) rather than False (retry-able failure) is the correct semantics.

If #990 lands first, this PR should rebase to import _split_file_markers from the consolidated location rather than maintain a parallel copy.

@sonichi
sonichi merged commit 23a8374 into sonichi:main May 22, 2026
2 checks passed
sonichi added a commit that referenced this pull request May 25, 2026
…achments (#1029)

* feat(dm-result): REST multipart upload — actually deliver [file:] attachments

## Why — closes the follow-up flagged in PR #985

PR #985 stripped `[file:|send:|attach:]` markers from the dm-result
body before sending so attackers couldn't deliver the literal text
`[file: /path]` to the user's DM. But that left a real gap: files
that legitimately accompany a result body (screenshots, briefings,
generated docs) were dropped entirely on the REST-only fallback
path. The TODO comment on the strip site explicitly called out:

  "REST multipart upload for actual file delivery is a follow-up"

This PR implements that follow-up. The WS-connected live bridge
calls `discord.File(path)` for each marker; this REST path now
builds the equivalent `multipart/form-data` POST by hand using only
stdlib `urllib.request` (consistent with the rest of dm-result.py's
zero-dependency posture).

## What ships

### 1. Path allowlist (mirrors discord-bridge)

`_SEND_ALLOWED_ROOTS` + `_SEND_ALLOWED_PREFIXES` + `_is_path_sendable`
— byte-identical policy to the canonical helper in
`src/discord-bridge.py`. Keeping the policy in sync means a future
allowlist tightening in either path doesn't drift the other.

### 2. Multipart upload helper

`_send_message_with_files(channel_id, token, content, file_paths)`
constructs the multipart envelope:

  - random boundary via uuid4 (won't collide with payload)
  - `payload_json` part for the message body (`{"content": "..."}`)
  - `files[N]` parts with `filename=` and binary content
  - 30s timeout (multipart uploads can be slower than JSON; bound the
    invocation time)

Filename header sanitization mirrors PR #1022's
`_safe_attachment_basename` shape — CR/LF/`"` in basenames are
replaced with `_`, length capped at 80 chars. Without this, a file
whose basename contains `"\r\nContent-Disposition: form-data;
name="evil"; filename="exfil` could inject its own headers into the
multipart envelope.

### 3. send_dm() updated

Splits the file markers into `sendable` (passes allowlist) vs
`rejected` (logged to stderr, not uploaded). Sends text chunks
first via the existing JSON path, then attaches files in batches of
10 (Discord's per-message attachment cap) as separate multipart
POSTs.

Backwards-compatible: a body with no markers goes through unchanged.

## Tests

`tests/dm-result-multipart-upload.test.py` (5 cases) — replaces
`urllib.request.urlopen` with `_FakeTransport` that captures the
full request body bytes so the test can assert on the actual
multipart envelope shape:

  - allowlisted file uploads via multipart (file bytes present)
  - non-allowlisted file rejected (zero multipart calls)
  - file-only message (empty content but valid multipart)
  - 11 files split into 10+1 batches
  - filename header sanitization (CR/LF/`"` stripped — defends
    against multipart-envelope splicing via attacker-supplied
    filename, same class as PR #1022)

All 5 pass locally.

Co-Authored-By: Vasiliy's Sutando <noreply@anthropic.com>

* refactor(send-allowlist): extract shared module per @liususan091219 review

Lucy's PR #1029 review flagged the file-attachment allowlist as a
copy between discord-bridge.py and dm-result.py, with a "keep in sync"
comment that wouldn't prevent drift — and noted that copies "will
drift" regardless. Per her recommendation: extract to a tiny shared
module that both consumers import.

## Pre-existing drift this also fixes

The two copies were already out of sync:

  - `src/discord-bridge.py` had the richer set: `_REPO / "results"`,
    `_REPO / "notes"`, `shared_personal_path("notes", _REPO)`,
    `_REPO / "docs"`, `~/Desktop/iclr-backups`,
    `~/Documents/sutando-launch-assets`, plus the four `/tmp/*`
    prefixes.
  - `src/dm-result.py` (from PR #1029) had the narrower set: only
    `_REPO / "results"`, `_REPO / "notes"`, `_REPO / "docs"`, and
    the same four prefixes — missing the personal-notes / Desktop /
    Documents roots.

So a file like `~/Desktop/iclr-backups/asset.png` would deliver via
the live WS bridge but get rejected by the REST fallback. Same shape
as the kinds of drift Lucy was guarding against. The extract makes
the policy a single set; both paths now respect the same allowlist.

## What ships

### `src/send_allowlist.py` (new)

  - `SEND_ALLOWED_ROOTS` — the canonical superset (all 6 roots).
  - `SEND_ALLOWED_PREFIXES` — the four `/tmp/*` prefixes.
  - `is_path_sendable(fpath) -> bool` — the canonical predicate.

Imports `resolve_workspace` and `shared_personal_path` at module-
load so the per-user paths resolve correctly. Stays dep-light
(stdlib only beyond those two existing helpers).

### `src/discord-bridge.py`

Replaces 21-line inline definition + 22-line function with an import
+ a one-line alias (`_is_path_sendable = _is_path_sendable_shared`).
The 7 existing call sites in this file don't need touching because
the local name `_is_path_sendable` is preserved.

### `src/dm-result.py`

Replaces the PR #1029 inline copy with the same import. Local names
`_SEND_ALLOWED_ROOTS` / `_SEND_ALLOWED_PREFIXES` preserved via
import-as alias so other tests that reference them keep working.

## Tests

`tests/send-allowlist-shared.test.py` — 6 architectural assertions:

  - shared module exists
  - discord-bridge.py imports from it
  - dm-result.py imports from it
  - no inline (re-)definitions of `SEND_ALLOWED_ROOTS` /
    `SEND_ALLOWED_PREFIXES` in either consumer
  - no inline `def [_]is_path_sendable(` in either consumer
  - the shared module contains the documented set (drops are
    deliberate, pinned by this test)

The architectural pin is the load-bearing piece: it makes future
"oh I'll just add it inline real quick" regressions fail loudly.

All 6 tests pass locally. Existing
`tests/dm-result-multipart-upload.test.py` (5 cases) and
`tests/dm-result-send-dm.test.py` (5 cases) still pass — no
behavior change for the live paths, just plumbing.

Co-Authored-By: liususan091219 (review feedback)
Co-Authored-By: Vasiliy's Sutando <noreply@anthropic.com>

* test(discord-bridge-allowlist): accept send_allowlist.py as helper location

Per @qingyun-wu PR #1029 review — CI red because the structural test
in `tests/discord-bridge-allowlist.test.py` greps for
`def _is_path_sendable(...)` in `src/discord-bridge.py`. The PR moves
the canonical implementation into `src/send_allowlist.py` (per
@liususan091219's drift-extraction review on #1029) and leaves only
a runtime alias in discord-bridge.py, so the original regex misses.

Fix: scan BOTH source files. Helper is found in either location:

  - Legacy inline form: `def _is_path_sendable(...)` in discord-bridge.py
  - Post-refactor form: `def is_path_sendable(...)` in send_allowlist.py

If the helper lives in send_allowlist.py, the test additionally
verifies discord-bridge.py imports it — without the import, the gate
sites (`discord.File(fpath)`) would reference an undefined name.
DOTALL match so multi-line `from send_allowlist import (… as …, ...)`
parenthesized imports work.

The contract this test guards (helper uses os.path.realpath for
CodeQL sanitizer, checks both ROOTS and PREFIXES, fail-closed
default, all sinks gated) is unchanged — only the location-discovery
logic was updated.

Co-Authored-By: qingyun-wu (review feedback)
Co-Authored-By: Vasiliy's Sutando <noreply@anthropic.com>

---------

Co-authored-by: Vasiliy's Sutando <noreply@anthropic.com>
Co-authored-by: Chi Wang <4250911+sonichi@users.noreply.github.com>
liususan091219 pushed a commit to liususan091219/sutando-fork that referenced this pull request May 28, 2026
…onichi#985)

Three real bugs in the REST-only fallback delivery path used by
`poll_dm_fallback` when voice / the live bridge can't reach the user:

## Bug A: `_resolve_owner_id` ignored `tierMap`

`src/discord-bridge.py:poll_proactive` honors the documented priority
chain `$SUTANDO_DM_OWNER_ID` -> `tierMap[uid] == "owner"` -> bot-filter
fallback. `dm-result.py` only honored env override + bot-filter — drift
between the live bridge and the fallback path. An admin who tier-tagged
the owner in access.json saw the live bridge route correctly, but the
fallback could pick a different non-bot from allowFrom (or a bot, in
the degraded-network case where /users/{id} lookups all fail).

Fix: insert the tierMap branch between env override and bot-filter.
Defensive: only accept tier-tagged owner if still present in allowFrom
— stale tags from removed users must not resolve a delisted owner.

## Bug D: `send_dm` delivered `[file:|send:|attach:]` markers as
## literal text

`poll_dm_fallback` shells out to `dm-result.py --file results/task-X.txt`
to deliver result files when the live bridge can't. Result files often
embed file markers:

    Here's the screenshot you asked about: [file: /tmp/sutando-x.png]

The live bridge attaches the file via `discord.File(path)` on the
WS-connected client. dm-result is REST-only and has no multipart
upload path — so the marker landed verbatim in the DM and the file
was silently dropped.

Fix: mirror discord-bridge's `_split_file_markers` helper.  `send_dm`
now strips markers from the body before chunking and logs the dropped
file list. Actual REST multipart upload is a follow-up — flagged in
the inline comment.

## Bug C (secondary): empty-body-after-strip posted `""` -> Discord 400

Pre-fix: `chunks = list(_chunk_for_discord(text)) or [text]`. For a
whitespace-only or all-marker body, the chunker yields nothing ->
empty list -> falsy -> falls back to `[""]` and POSTs empty content.
Discord returns HTTP 400 / code 50006.

Fix: detect empty-after-strip and skip the /messages call entirely.

## Tests

`tests/dm-result-send-dm.test.py` (5 cases). Mocks
`urllib.request.urlopen` with a recording fake (`_FakeTransport`) and
probes the full REST flow. The fake raises `AssertionError` on any
unmocked request, so future regressions fail loudly.

  - `test_tier_map_resolution_skips_bot_lookup` (Bug A regression
    guard + perf assertion that the wasted /users/{id} call is gone)
  - `test_bot_filter_fallback_still_works_without_tier_map`
    (existing behavior preserved)
  - `test_file_markers_stripped_from_body` (Bug D regression guard)
  - `test_empty_body_after_marker_strip_does_not_post_messages`
    (Bug C regression guard — the fake transport raises
    AssertionError on an unmocked POST, so a future regression here
    fails loudly)
  - `test_env_override_skips_access_json_entirely` (priority-order
    pin)

All 5 pass locally.

Co-authored-by: Vasiliy's Sutando <noreply@anthropic.com>
Co-authored-by: Chi Wang <4250911+sonichi@users.noreply.github.com>
liususan091219 pushed a commit to liususan091219/sutando-fork that referenced this pull request May 28, 2026
…achments (sonichi#1029)

* feat(dm-result): REST multipart upload — actually deliver [file:] attachments

## Why — closes the follow-up flagged in PR sonichi#985

PR sonichi#985 stripped `[file:|send:|attach:]` markers from the dm-result
body before sending so attackers couldn't deliver the literal text
`[file: /path]` to the user's DM. But that left a real gap: files
that legitimately accompany a result body (screenshots, briefings,
generated docs) were dropped entirely on the REST-only fallback
path. The TODO comment on the strip site explicitly called out:

  "REST multipart upload for actual file delivery is a follow-up"

This PR implements that follow-up. The WS-connected live bridge
calls `discord.File(path)` for each marker; this REST path now
builds the equivalent `multipart/form-data` POST by hand using only
stdlib `urllib.request` (consistent with the rest of dm-result.py's
zero-dependency posture).

## What ships

### 1. Path allowlist (mirrors discord-bridge)

`_SEND_ALLOWED_ROOTS` + `_SEND_ALLOWED_PREFIXES` + `_is_path_sendable`
— byte-identical policy to the canonical helper in
`src/discord-bridge.py`. Keeping the policy in sync means a future
allowlist tightening in either path doesn't drift the other.

### 2. Multipart upload helper

`_send_message_with_files(channel_id, token, content, file_paths)`
constructs the multipart envelope:

  - random boundary via uuid4 (won't collide with payload)
  - `payload_json` part for the message body (`{"content": "..."}`)
  - `files[N]` parts with `filename=` and binary content
  - 30s timeout (multipart uploads can be slower than JSON; bound the
    invocation time)

Filename header sanitization mirrors PR sonichi#1022's
`_safe_attachment_basename` shape — CR/LF/`"` in basenames are
replaced with `_`, length capped at 80 chars. Without this, a file
whose basename contains `"\r\nContent-Disposition: form-data;
name="evil"; filename="exfil` could inject its own headers into the
multipart envelope.

### 3. send_dm() updated

Splits the file markers into `sendable` (passes allowlist) vs
`rejected` (logged to stderr, not uploaded). Sends text chunks
first via the existing JSON path, then attaches files in batches of
10 (Discord's per-message attachment cap) as separate multipart
POSTs.

Backwards-compatible: a body with no markers goes through unchanged.

## Tests

`tests/dm-result-multipart-upload.test.py` (5 cases) — replaces
`urllib.request.urlopen` with `_FakeTransport` that captures the
full request body bytes so the test can assert on the actual
multipart envelope shape:

  - allowlisted file uploads via multipart (file bytes present)
  - non-allowlisted file rejected (zero multipart calls)
  - file-only message (empty content but valid multipart)
  - 11 files split into 10+1 batches
  - filename header sanitization (CR/LF/`"` stripped — defends
    against multipart-envelope splicing via attacker-supplied
    filename, same class as PR sonichi#1022)

All 5 pass locally.

Co-Authored-By: Vasiliy's Sutando <noreply@anthropic.com>

* refactor(send-allowlist): extract shared module per @liususan091219 review

Lucy's PR sonichi#1029 review flagged the file-attachment allowlist as a
copy between discord-bridge.py and dm-result.py, with a "keep in sync"
comment that wouldn't prevent drift — and noted that copies "will
drift" regardless. Per her recommendation: extract to a tiny shared
module that both consumers import.

## Pre-existing drift this also fixes

The two copies were already out of sync:

  - `src/discord-bridge.py` had the richer set: `_REPO / "results"`,
    `_REPO / "notes"`, `shared_personal_path("notes", _REPO)`,
    `_REPO / "docs"`, `~/Desktop/iclr-backups`,
    `~/Documents/sutando-launch-assets`, plus the four `/tmp/*`
    prefixes.
  - `src/dm-result.py` (from PR sonichi#1029) had the narrower set: only
    `_REPO / "results"`, `_REPO / "notes"`, `_REPO / "docs"`, and
    the same four prefixes — missing the personal-notes / Desktop /
    Documents roots.

So a file like `~/Desktop/iclr-backups/asset.png` would deliver via
the live WS bridge but get rejected by the REST fallback. Same shape
as the kinds of drift Lucy was guarding against. The extract makes
the policy a single set; both paths now respect the same allowlist.

## What ships

### `src/send_allowlist.py` (new)

  - `SEND_ALLOWED_ROOTS` — the canonical superset (all 6 roots).
  - `SEND_ALLOWED_PREFIXES` — the four `/tmp/*` prefixes.
  - `is_path_sendable(fpath) -> bool` — the canonical predicate.

Imports `resolve_workspace` and `shared_personal_path` at module-
load so the per-user paths resolve correctly. Stays dep-light
(stdlib only beyond those two existing helpers).

### `src/discord-bridge.py`

Replaces 21-line inline definition + 22-line function with an import
+ a one-line alias (`_is_path_sendable = _is_path_sendable_shared`).
The 7 existing call sites in this file don't need touching because
the local name `_is_path_sendable` is preserved.

### `src/dm-result.py`

Replaces the PR sonichi#1029 inline copy with the same import. Local names
`_SEND_ALLOWED_ROOTS` / `_SEND_ALLOWED_PREFIXES` preserved via
import-as alias so other tests that reference them keep working.

## Tests

`tests/send-allowlist-shared.test.py` — 6 architectural assertions:

  - shared module exists
  - discord-bridge.py imports from it
  - dm-result.py imports from it
  - no inline (re-)definitions of `SEND_ALLOWED_ROOTS` /
    `SEND_ALLOWED_PREFIXES` in either consumer
  - no inline `def [_]is_path_sendable(` in either consumer
  - the shared module contains the documented set (drops are
    deliberate, pinned by this test)

The architectural pin is the load-bearing piece: it makes future
"oh I'll just add it inline real quick" regressions fail loudly.

All 6 tests pass locally. Existing
`tests/dm-result-multipart-upload.test.py` (5 cases) and
`tests/dm-result-send-dm.test.py` (5 cases) still pass — no
behavior change for the live paths, just plumbing.

Co-Authored-By: liususan091219 (review feedback)
Co-Authored-By: Vasiliy's Sutando <noreply@anthropic.com>

* test(discord-bridge-allowlist): accept send_allowlist.py as helper location

Per @qingyun-wu PR sonichi#1029 review — CI red because the structural test
in `tests/discord-bridge-allowlist.test.py` greps for
`def _is_path_sendable(...)` in `src/discord-bridge.py`. The PR moves
the canonical implementation into `src/send_allowlist.py` (per
@liususan091219's drift-extraction review on sonichi#1029) and leaves only
a runtime alias in discord-bridge.py, so the original regex misses.

Fix: scan BOTH source files. Helper is found in either location:

  - Legacy inline form: `def _is_path_sendable(...)` in discord-bridge.py
  - Post-refactor form: `def is_path_sendable(...)` in send_allowlist.py

If the helper lives in send_allowlist.py, the test additionally
verifies discord-bridge.py imports it — without the import, the gate
sites (`discord.File(fpath)`) would reference an undefined name.
DOTALL match so multi-line `from send_allowlist import (… as …, ...)`
parenthesized imports work.

The contract this test guards (helper uses os.path.realpath for
CodeQL sanitizer, checks both ROOTS and PREFIXES, fail-closed
default, all sinks gated) is unchanged — only the location-discovery
logic was updated.

Co-Authored-By: qingyun-wu (review feedback)
Co-Authored-By: Vasiliy's Sutando <noreply@anthropic.com>

---------

Co-authored-by: Vasiliy's Sutando <noreply@anthropic.com>
Co-authored-by: Chi Wang <4250911+sonichi@users.noreply.github.com>
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.

3 participants