You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The #855 intake validation guards native_meeting_id against length (>255) and NUL/control bytes → typed 422. But a value carrying URL-reserved characters (e.g. a Teams passcode accidentally left on the id: 397421056486982?p=X8hcQVTnGNpGelJLSv) passes both guards (short, no control bytes) and is accepted. Then three things break downstream, none typed:
Malformed join URL.construct_meeting_url string-interpolates the raw id into the template (teams: https://teams.microsoft.com/l/meetup-join/{native_meeting_id}), yielding …/l/meetup-join/397421056486982?p=X8hcQVTnGNpGelJLSv — the bot fails to join (join_failure).
Unfindable record.platform_specific_id is stored as the full 397421056486982?p=… string, so the dashboard (which looks up by the clean numeric id 397421056486982) returns 'Meeting not found for platform teams and ID 397421056486982'.
The user sees a bot that 'fails to join' with no indication that the input was malformed.
Reproduced (staging, 2026-07-21)
POST /bots {"platform":"teams","native_meeting_id":"397421056486982?p=X8hcQVTnGNpGelJLSv"} → meeting 13563 created, constructed_meeting_url = https://teams.microsoft.com/l/meetup-join/397421056486982?p=X8hcQVTnGNpGelJLSv, status → failed/join_failure; the same URL passed with a clean native_meeting_id: 397421056486982 + full meeting_url joined fine (meeting 13564, active).
Fix (extend #855 intake validation, at the point of introduction)
At bot-spawn intake, for platforms where native_meeting_id is interpolated as a URL path segment, reject (typed 422) — or parse/normalize — an id containing URL-reserved characters (? & = / # space). Ideally: if the id looks like it carries a query string (?p=…), either extract the passcode into the passcode field or 422 with a message naming the problem ('native_meeting_id must be the bare meeting id; pass the passcode separately or supply meeting_url'). Mirror the #855 pattern: refuse at the door, name the problem, before any record is written.
Acceptance
native_meeting_id containing ?/=/&/space → typed 422 at intake (negative control), not a 500/silent join_failure. Delivered — row A2 of the live table in the 2026-08-17 20:42Z comment: meeting_password → 422 naming passcode, against the pre-fix control on the same stack returning 201 and silently dropping it (the reported bug, reproduced). The fix(meeting-api): reject native_meeting_id carrying URL/query chars at intake (#892) #910 intake guard at bot_spawn/router.py:343 covers ? # & = / and whitespace. ✅
A bare id + separate passcode (or a full meeting_url) still works (positive control — meeting 13564 pattern). Delivered — rows A1 and A3, same live table: the bot receives …/meet/397421056486982?p=X8hcQVTnGNpGelJLSv where the pre-fix control gave …/l/meetup-join/555666777888 with no passcode at all; a full meeting_url carrying ?p= is green both ways in. ✅
Not an acceptance leg — a scope note, moved out of the list so it is read as one: the dashboard's own
parser handles this correctly today, which is why the defect only bites raw API and MCP callers.
Found while spawning a Teams witness via the raw API — the dashboard's own parser avoids this, so it only bites API/MCP integrators.
Gap (adjacent to #843/#855, not covered by it)
The #855 intake validation guards
native_meeting_idagainst length (>255) and NUL/control bytes → typed 422. But a value carrying URL-reserved characters (e.g. a Teams passcode accidentally left on the id:397421056486982?p=X8hcQVTnGNpGelJLSv) passes both guards (short, no control bytes) and is accepted. Then three things break downstream, none typed:construct_meeting_urlstring-interpolates the raw id into the template (teams: https://teams.microsoft.com/l/meetup-join/{native_meeting_id}), yielding…/l/meetup-join/397421056486982?p=X8hcQVTnGNpGelJLSv— the bot fails to join (join_failure).platform_specific_idis stored as the full397421056486982?p=…string, so the dashboard (which looks up by the clean numeric id397421056486982) returns 'Meeting not found for platform teams and ID 397421056486982'.Reproduced (staging, 2026-07-21)
POST /bots {"platform":"teams","native_meeting_id":"397421056486982?p=X8hcQVTnGNpGelJLSv"}→ meeting 13563 created,constructed_meeting_url = https://teams.microsoft.com/l/meetup-join/397421056486982?p=X8hcQVTnGNpGelJLSv, status →failed/join_failure; the same URL passed with a cleannative_meeting_id: 397421056486982+ fullmeeting_urljoined fine (meeting 13564, active).Fix (extend #855 intake validation, at the point of introduction)
At bot-spawn intake, for platforms where
native_meeting_idis interpolated as a URL path segment, reject (typed 422) — or parse/normalize — an id containing URL-reserved characters (? & = / # space). Ideally: if the id looks like it carries a query string (?p=…), either extract the passcode into the passcode field or 422 with a message naming the problem ('native_meeting_id must be the bare meeting id; pass the passcode separately or supply meeting_url'). Mirror the #855 pattern: refuse at the door, name the problem, before any record is written.Acceptance
native_meeting_idcontaining?/=/&/space → typed 422 at intake (negative control), not a 500/silent join_failure. Delivered — row A2 of the live table in the 2026-08-17 20:42Z comment:meeting_password→ 422 namingpasscode, against the pre-fix control on the same stack returning 201 and silently dropping it (the reported bug, reproduced). The fix(meeting-api): reject native_meeting_id carrying URL/query chars at intake (#892) #910 intake guard atbot_spawn/router.py:343covers? # & = /and whitespace. ✅…/meet/397421056486982?p=X8hcQVTnGNpGelJLSvwhere the pre-fix control gave…/l/meetup-join/555666777888with no passcode at all; a fullmeeting_urlcarrying?p=is green both ways in. ✅Not an acceptance leg — a scope note, moved out of the list so it is read as one: the dashboard's own
parser handles this correctly today, which is why the defect only bites raw API and MCP callers.
Found while spawning a Teams witness via the raw API — the dashboard's own parser avoids this, so it only bites API/MCP integrators.