mcp: four frictions from the 2026-09-05 prod test (secret read-back, db-id addressing, recordings limit, Zoom hosted domains, stale not_present items) - #1547
Draft
DmitriyG228 wants to merge 26 commits into
Draft
Conversation
The platform floor: the identity library and its access policy, the gateway's routing and scopes, the runtime's container backend, the sealed `config.v1` contract with every service's vendored preflight and declaration, and the compose / helm / lite deployment surfaces. Condenses 106 commits of the minutes-mcp-viewer line, taken verbatim from the line tip (196549a).
The meetings bounded context — bot lifecycle, meeting rows, the transcript single-writer path, the status stream — and `core/meetings/services/mcp`, the MCP service the gateway fronts. Condenses 27 commits of the minutes-mcp-viewer line, taken verbatim from the line tip (196549a).
The durable flow engine: the step/flow model, the schema and its drift gate, the timeline projection, the worker loop, the config declaration, and the service's contract, tests and witness harnesses. What a flow IS, not which flows exist. Condenses 49 commits of the minutes-mcp-viewer line, taken verbatim from the line tip (196549a).
…ior packs The flows we actually run: the production definitions, the step library they compose (mail, meeting, notify, agent), the integrations (inbox, mailbox, mail policy, instance gate, the flows API), the adoption eval fixtures, and the `behavior/` packs a new workspace is seeded from. Condenses 106 commits of the minutes-mcp-viewer line, taken verbatim from the line tip (196549a).
The platform floor: the identity library and its access policy, the gateway's routing and scopes, the runtime's container backend, the sealed `config.v1` contract with every service's vendored preflight and declaration, and the compose / helm / lite deployment surfaces. Condenses 25 commits of the minutes-mcp-viewer line, taken verbatim from the line tip (196549a).
The meetings bounded context — bot lifecycle, meeting rows, the transcript single-writer path, the status stream — and `core/meetings/services/mcp`, the MCP service the gateway fronts. Condenses 21 commits of the minutes-mcp-viewer line, taken verbatim from the line tip (196549a).
The durable flow engine: the step/flow model, the schema and its drift gate, the timeline projection, the worker loop, the config declaration, and the service's contract, tests and witness harnesses. What a flow IS, not which flows exist. Condenses 94 commits of the minutes-mcp-viewer line, taken verbatim from the line tip (196549a).
…ior packs — tests The flows we actually run: the production definitions, the step library they compose (mail, meeting, notify, agent), the integrations (inbox, mailbox, mail policy, instance gate, the flows API), the adoption eval fixtures, and the `behavior/` packs a new workspace is seeded from. Condenses 12 commits of the minutes-mcp-viewer line, taken verbatim from the line tip (196549a).
The Workflows section (overview, authoring, vocabulary, examples, developing, operations), its navigation entry, and the changelog fragments for the meetings/bot/lite changes in this delivery. Condenses 16 commits of the minutes-mcp-viewer line, taken verbatim from the line tip (196549a).
…a meeting
Founder, 2026-09-04: "whats_waiting — that's the one agent must call after got
installed and that one will prompt them to try a meeting" · "we want them to try
a meeting so they are activated. Activated meaning 1 meeting with transcription".
Until now whats_waiting answered a brand-new person with an empty list, and an
empty queue is indistinguishable from a finished one. Flow `onboarding` v1 reacts
to identity's `onboarding.completed` with one step, `first_meeting`, which parks
pending until that person's first `meeting.completed` THAT ACTUALLY TRANSCRIBED —
a bot that joined an empty room and left must not clear the item asking them to
try one. The words live in behavior/queue/onboarding.pending.md: meet.new,
request_meeting_bot, admit the bot, follow get_meeting_transcript with
since_index, stop_bot. The item clears itself; nobody dismisses it.
No `needs=("agent",)` anywhere — this is the no-agents product's own onboarding.
`needs=("meetings",)` because the segment count is a meetings read.
Also the seam a deployment needs to carry flows this repo does not publish:
VEXA_FLOWS_DEFS_EXTRA names importable packs, each `build(reg, db)`, registered
last. The other two seams already existed and are now tested rather than
rediscovered — $VEXA_BEHAVIOR_DIR/queue/ words ahead of the baked showcase, and
an intake with no carrier allow-list. core/flows/README.md documents all three.
Rebased onto #1497, which brings core/flows its own test harness — so there is
ONE, and it is that one. B's conftest.py, sqlite_double.py and both READMEs win
outright; the two onboarding suites adapt to them. conftest gains only what B
had no caller for: a db/clock/registry rig over the REAL flows_defs.production
(B's fixtures.rig() builds the fake world, which is the wrong rig for a test
about what a deployment composes), scoped rather than autouse so
test_no_agents.py keeps owning its own unset. The double's REACTION_DDL is gone
with the old file — B's builds itself from schema.sql — so the drift test reads
the column set back off the live table instead.
Two things B's suite then caught, both real. VEXA_FLOWS_DEFS_EXTRA was declared
in config.v1.json and not in flows_config.DECLARED, which test_config_contract
compares; it is declared now. And test_no_agents asserts every production flow
terminates with agents absent — onboarding does not, on purpose, because it is
waiting for the PERSON, so it is exempted there by name with the reason, and
still held to not failing and to never knocking on the agent door.
pyproject.toml stays (B has none, though B's README already describes one) and
its dependency list grows to what B's suite actually imports in-process: fastapi,
pydantic, sqlalchemy, psycopg, httpx. No socket is opened by any of it.
26 tests in the two onboarding files. NOTE: #1497's suite is 23 red against this
base — its tests describe source fixes (parse_ics anchoring, the desk-card flows,
the meetings-door declarations) that are not on oss/no-agents-mcp. This branch
adds none of them and fixes none of them; the count is identical before and after.
Adding pyproject.toml does make gate:python discover core/flows, so that red
becomes visible in CI rather than skipped.
Signed-off-by: DmitriyG228 <2280905@gmail.com>
…ory) Signed-off-by: DmitriyG228 <2280905@gmail.com>
POST /friction answered 400 for any `kind` outside its eight-word list, and the MCP tool published `kind` as a bare string described only by the title FastAPI synthesises from the function name. On prod, 2026-09-04, an agent guessed "missing", "broke" and "confusing"; twelve reports were thrown away in twenty minutes. A sink that drops a report over a spelling is the one thing it must never do. Both halves are fixed. flows-api is LENIENT: an unrecognised `kind` is stored as `other` and an unrecognised `severity` as `annoyance`, each with the caller's own word kept in `kind_raw` / `severity_raw` and echoed back in the reply, so the caller learns the canonical word after the report is safely in. No migration — a report lives in `reaction.subject_refs`, a JSON document, so these are new keys inside it and `schema.sql` is untouched. And the tool now TELLS an agent the vocabulary: both enums are published on the route's parameters, `kind`'s description carries one example per word, and the docstring reads as instructions (what friction is, when to call, what the payload is). The MCP edge carries them through — `bind` now prefers the route's docstring over the synthesised summary (which is F-D12, `whats_waiting` reading "Queue Waiting"), and `register` republishes an argument's `enum` into the tool schema as schema, never as validation: enforcing at the edge would recreate the same loss one hop earlier. `list_flows` gains the docstring it never had. Tests: the three words prod actually sent are stored, not refused; a known word is stored as-is; no vocabulary word can produce a 400; both enums and an instructive description reach the OpenAPI the tool schema is derived from; and no flows tool in the manifest is described by its own title alone. The assembled-surface fixture now spells /friction the way flows-api really publishes it — the hand-written summary in the old fixture is why this suite was green while prod was losing reports. Signed-off-by: DmitriyG228 <2280905@gmail.com>
… finding) Caught on the t27f station, not reasoned out. The first cut of the F-D26 fix republished the owning route's `enum` into the assembled tool's schema. The MCP SDK's own dispatcher validates a call's arguments against `inputSchema` before it calls anything (`jsonschema.validate` in mcp/server/lowlevel/server.py), so `report_friction` with `kind="broke"` came back "Input validation error: 'broke' is not one of [...]" — isError, no row stored. The fix for a sink that dropped reports over a spelling had moved the drop one hop earlier. The vocabulary now travels as `examples`, a JSON Schema annotation no validator enforces, and the words are spelled out with one example each in the argument description the owning route writes. A new test pins the rule for every assembled tool, not just this one: an `enum` in a published tool schema is a gate at this edge, never documentation. Signed-off-by: DmitriyG228 <2280905@gmail.com>
Founder, 2026-09-04 10:2xZ, recorded on the t27 station after F-D26: "we want to catch all signal, does not make sense being strict about it, we want rich data, does not have to be too structured." That supersedes the fix shape in the commit before this one, which mapped an unrecognised kind into `other` and kept the reporter's word in `kind_raw`. A bucket is still a taxonomy, applied at the door, by us, to somebody else's words. So `kind` and `severity` are stored AS SENT — free text, no canonicalisation, not even lowercasing, and no second field for a "raw" word because there is no cooked one. Any query argument this route does not name is kept with the report under `extra` rather than dropped: a stack frame, a request id, a model name, whatever the reporter knew that we did not think to ask for. The eight kinds survive only as suggestions in the tool description, and grouping happens on the data later, by whoever reads the sink. Extras are NAMESPACED, and that is not tidiness. `flows_timeline.model.concerns` decides whose report a row is by reading `uid`/`subject`/`owner`/`organizer` straight off the refs, so merging caller-supplied keys into the top level would let a reporter file into somebody else's queue. Under `extra` they are data; at the top level they would be authority. A test pins it. The read model stops picking from a fixed key list and passes through every ref it does not render itself — otherwise the sink would store fields no reader could see until somebody remembered to widen the list, which is the same loss one layer down and quieter. Still refused: a report with no session and a report with no text. Those are about the report existing at all, not about its shape. Carrier and manifest updated to match. Tests: the three prod words stored verbatim, casing preserved, no word in either field can produce a 400, unnamed fields kept and returned, and an extra field cannot re-address the report. Signed-off-by: DmitriyG228 <2280905@gmail.com>
Caught on the station, again, and it was my own break. The route stopped publishing `enum` for kind/severity (the sink's words are suggestions now, so it publishes `examples`); this edge still looked only for `enum`, so the eight words vanished from `tools/list` entirely and the agent was back to guessing — the first half of F-D26, restored by the fix for its second half. `_vocabulary` now reads either key and always republishes `examples`. Both shapes are real: a route that has decided its vocabulary is a suggestion says `examples`, one that validates against a closed set says `enum`, and an agent needs the words either way. The assembled-surface fixture is corrected to the shape flows-api actually publishes, and a unit test pins both branches — the fixture drifting from the live route is exactly why the original defect survived a green suite. Signed-off-by: DmitriyG228 <2280905@gmail.com>
Prod, 2026-09-04 11:0xZ. `POST /friction` without `session` answered 400 —
"session is required … A report with no session cannot be tied back to the
conversation that produced it, which is the exact gap this carrier exists to
close." The report it threw away was the one describing that refusal.
This is F-D26 one field along. That fix made `kind` and `severity` free text on
the founder's ruling ("we want to catch all signal, does not make sense being
strict about it, we want rich data, does not have to be too structured") but
left two refusals standing — no session, no text — reasoning that those were
about the report existing rather than about its shape. The distinction did not
survive contact with prod, and it was never the reporter's to pay for: the join
key is something WE want, so WE eat its absence. A report nothing can be tied
back to is worth strictly more than no report, and which producers cannot see a
session is itself signal.
So the rule is generalised rather than patched, to spare a third incident: NO
VALUE A CALLER SENDS OR OMITS PRODUCES A 400 on this route. Over-long values
truncate, unknown words store as sent, absences store as absences. The only
refusal left is authentication (401) — a report attributed to nobody is a
different object, not a poorer one.
A missing session omits the ref entirely rather than writing `""`, because an
empty string is a value and a later reader cannot tell it from a session whose
id is genuinely blank. `friction_for_subject` therefore renders two fields: the
human `session`, which says "no session", and the machine `session_id`, which
stays exactly what was stored. One field cannot be both honest to a grep and
readable to a person.
The tool description still tells the agent to send `session` whenever it has one
— it is most of what makes a report actionable — it just never bills it for not
having one. "Required" is gone from all three text arguments.
Audited the rest of the route and found the 422 door above it: every argument is
typed `str` with no enum/pattern/length gate, so pydantic cannot refuse a call
one layer above the handler where none of this leniency would run. A test pins
that, since it is the same defect class as the `enum` B7 already keeps out of
`kind`. Carrier, golden and manifest updated to match.
Tests: no session → 201, stored, read back as "no session" with `session_id`
empty and no `session` ref written; no text at all → 201; nine fields × seven
hostile values → no 400; every published parameter unconstrained and optional.
Signed-off-by: DmitriyG228 <2280905@gmail.com>
An admission refused by the service authority reached the caller as code/reason/decision_id — enough for a program to branch on, nothing a person or an agent can act on. Only the deployment that decided knows what would fix the account, so only it can author that sentence. meeting-api now carries two OPTIONAL fields off the decision into the 403 detail: message (<=512 chars, control characters stripped) and action_url (https only, else dropped). Absent fields are OMITTED, never null, so a deployment whose authority says nothing produces the byte-same body as before. No vocabulary is added anywhere: reason passes through whatever it says, allow-list free. The blocking half was not the router. ServiceAuthorityDecision.from_wire rejected the WHOLE response on any unknown field, so a decider that began sending these turned an actionable 403 into service_authority_unavailable — a 503 outage. Widening that allow-list by exactly these two names is the fix; genuinely unknown fields are still rejected. The gateway already returns upstream bodies verbatim and the MCP forward already carries the JSON body into the tool error, so neither needed a change — both get tests pinning it, because the property is invisible at those seams and a future error-envelope tidy-up would drop the words with every existing test still green. Tests: 17 (meeting-api) + 5 (gateway) + 4 (mcp). Suites green — meeting-api 1346, gateway 326, gateway-conformance 86, mcp 229. Signed-off-by: DmitriyG228 <2280905@gmail.com>
… inside it
An upstream refusal arrived at the tool caller as one sentence with a doubled
envelope inside it — `Error calling request_meeting_bot. Status code: 403.
Response: {"detail":{"detail":{...}}}` — so every field an agent could act on
was present and none of it was reachable without writing a parser.
Two fixes, neither of which knows any vocabulary:
* The second `detail` was born HERE. `make_request` handed the upstream body,
already `{"detail": ...}`, to its own `HTTPException(detail=...)`, and each
re-raising hop would add another layer. Unwrap to the innermost object so
the depth is a constant one. The gateway forwards status and body verbatim
and adds nothing; meeting-api raises a single-level detail.
* `fastapi-mcp` renders a failed call by interpolating the raw body into a
sentence. `install_structured_tool_errors` raises inside `_request`, which
`_execute_api_tool` re-raises unchanged, so the tool result now reads:
the decider's own words first, `action_url:` when there is somewhere to go,
the body once, compact. Still `isError: true`.
No reason, code or product noun is introduced: whatever the deciding service
said is what the agent sees, and a reason this build has never heard of renders
exactly as well as one it has.
Signed-off-by: DmitriyG228 <2280905@gmail.com>
…ption Signed-off-by: DmitriyG228 <2280905@gmail.com>
A say-file under `behavior/queue/` may open with `notice: true` in front-matter. That marks what stays TRUE BETWEEN CALLS rather than what just happened, and it is an admin's edit in the tree that already decides whether an item is spoken at all — no deploy on either side of it. `GET /queue/waiting` now carries `notice` on every item. `GET /queue/notices` is the new, much smaller answer: only the flagged items' say texts, deduped, behind exactly the same door — small enough to ask on every call. No new tool. A file with no front-matter declares nothing and behaves as it always did; an unparseable fence costs the flag, never the sentence. This repo ships the mechanism and flags nothing with it. Signed-off-by: DmitriyG228 <2280905@gmail.com>
An agent reads a tool result and nothing else unless something makes it. So a fact that stays true between calls is attached to the call the agent just made, rather than left on a tool somebody has to remember to call. The five meeting tools (request_meeting_bot, get_meeting_transcript, list_meetings, get_bot_status, stop_bot) now carry the caller's standing notices out: a `notices` field in the body and one `Notice: …` line per notice in the text, once per result. `whats_waiting` is excluded — it already answers with them. Never an error and never a new tool: bounded at two seconds, and an absent domain, a slow one, a refusal or an answer this cannot read all leave the result exactly as it was. A refusal carries the refusal and nothing else. This module knows no vocabulary — a notice is a string it was handed. Signed-off-by: DmitriyG228 <2280905@gmail.com>
… stored
fr_2261a6306224c6f8 — on the production MCP, `request_meeting_bot` returned
`data.webhook_secret` in clear. The secret is the entire security property of
the webhook: anyone holding it can forge a delivery the receiver will verify,
and here it travelled out through the public gateway into a calling model's
context and its logs.
The projection that stops this already exists and every other meeting-serving
route already calls it — GET /meetings, GET /meetings/{id}, both transcript
reads, the PATCH echo. `bot_spawn.service._meeting_response` was the one that
did not: it built the response body by copying `row["data"]` verbatim, three
hundred lines above the code that writes the secret onto that same row. A route
does not leak by deciding to, it leaks by not calling — so the fix is the call,
in the one place every /bots response is shaped, rather than a second strip.
`viewer_is_owner=True`: a spawn response is only ever served to the spawner, so
the v0.10 contract's `webhook_url`/`webhook_events` still ride back exactly as
they do on GET /meetings. What goes is tier 1 — the signing secret, the share
grants, the session userdata path, and anything else credential-SHAPED, dropped
on name shape before anyone has thought to name it.
The stored row is untouched, which is load-bearing: the lifecycle callback signs
from `meeting_row["data"]` at delivery time. A projection that reached the row
would silently unsign every webhook the deployment delivers, and no
response-shape test would catch it — so there is a test that spawns, then signs.
Tests: 4 new in meeting-api tests/test_response_secret_projection.py (the spawn
response carries no credential-shaped key and not the secret's value; it still
carries the owner's own webhook config; it still carries the meeting's content;
the row still stores the secret and delivery still verifies against it).
`uv run pytest -q` in core/meetings/services/meeting-api: 1350 passed, 5 skipped
(1346 before).
Signed-off-by: DmitriyG228 <2280905@gmail.com>
fr_042129f5d53aa543 — parse_meeting_link answered https://zoom-lfx.platform.linuxfoundation.org/meeting/96088138284?password=<uuid> with 422 "Unsupported meeting URL (unknown provider)". It is an ordinary Zoom meeting the bot can join; what defeated the parser is that an organisation had fronted its Zoom tenancy on its own domain, so the hostname — the only thing the parser looked at — no longer says Zoom. Recognised on the PATH SHAPE instead: Zoom's own two join paths, a 10-11 digit meeting id, AND a passcode parameter (`password=` or `pwd=`). All three, because the hostname is no longer carrying any of the evidence: a bare numeric path on an unknown host is not a meeting, and answering the wrong platform confidently is worse than the 422 it replaces. The read is reported in `warnings` — the host was inferred, not recognised, the same honesty the jitsi inference already owes its caller — and a host declared in VEXA_JITSI_HOSTS is exempt, because an operator who named it has said what it is. Two smaller things ride along, both the same defect class (a credential dropped in silence, which surfaces as a bot sitting in a passcode prompt nobody can see): the zoom.us branch now reads `password=` as well as `pwd=`, and meeting-api's twin parser (collector/meeting_link.py, the pasted-link and ICS door) gets the same hosted-domain branch, so a link does not parse on one door and 422 on the other. The ICS free-text scan skips it, for the reason `generic_hosts=False` exists: a calendar description full of arbitrary links must not import one as a meeting. README: the accepted Zoom URL shapes are now stated in the mcp service README. Tests: 9 new in core/meetings/services/mcp tests/test_parse_meeting_link.py (the exact LFX link, the warning, the /j/ form, and four negatives — no passcode, a 9-digit id, an ordinary page with a number in it, a declared jitsi host) and 6 new in meeting-api tests/test_meeting_link.py mirroring them. `uv run pytest -q` in core/meetings/services/mcp: 289 passed (281 before); in core/meetings/services/meeting-api: 1356 passed, 5 skipped (1350 before). Signed-off-by: DmitriyG228 <2280905@gmail.com>
fr_db203061a7a1d953 — `list_recordings(limit=3)` on the production MCP returned
all 201 of the account's recordings: 1.6 MB, oldest first, every row carrying the
per-chunk upload bookkeeping of every media file it was assembled from.
Three faults, and the first is the one worth naming. The MCP tool forwards
`limit`/`offset`/`meeting_id` correctly, and so does the gateway; this route
declared none of them, and FastAPI drops an undeclared query parameter in
silence. **An argument that is accepted and ignored is worse than one that is
refused** — the caller cannot tell the answer is not the one they asked for. So
the parameters are declared with bounds: out of range is now a 422, not a clamp.
The other two are what made a right answer expensive. There is no ordering,
because there is no recordings table — recordings live inside `meeting.data`
JSONB and `service.py` appends the newest to the TAIL of its meeting's list, so
the natural order is oldest-first; the sort therefore happens at the route, on
`created_at`, which `_now_iso()` guarantees is lexicographically chronological.
And there is no list projection: each row shipped `storage_path`, `chunk_seq`,
`chunk_count`, `first_chunk_at` and the rest — how the bytes were assembled,
which nothing in a list renders and no caller can act on. The list row now keeps
id, meeting_id, status, created_at, completed_at, playback_url, a top-level
duration_seconds (the longest of its media files — a list wants to know how long
it is), and per media file only id, type, format, duration_seconds,
file_size_bytes. The id and type are kept deliberately: they are how a caller
reaches /media/{id}/raw. Full fidelity stays on GET /recordings/{id}.
Not changed, and out of scope: the read behind this still materializes the whole
account (the JSONB layout gives it no choice). What changes is the RESPONSE,
which is what reaches a caller.
One existing test moved rather than weakened — the A2/#491 property that the
stored pointer is never the zero-byte final-signal chunk now reads `storage_path`
off the DETAIL route. It is a claim about what is stored, not about which route
shows it.
The MCP tool's own limit/offset gain matching bounds and a description, so an
over-limit ask is a tool error rather than a surprise from two hops away.
Tests: 12 new in meeting-api tests/test_recordings_list_page.py (limit honoured ·
offset pages with no overlap or gap · default page size · out-of-range refused ·
newest first · the first page is the newest page · bookkeeping dropped · list
keeps what a list renders · detail still full · the page is under 2 KB where the
full read was 1.6 MB · the meeting filter honoured · scoping unmoved).
`uv run pytest -q` in core/meetings/services/meeting-api: 1368 passed, 5 skipped
(1356 before); in core/meetings/services/mcp: 289 passed, unchanged.
Signed-off-by: DmitriyG228 <2280905@gmail.com>
fr_e612b14fba618eea — on a deployment with no agent domain, every completed
meeting's post_meeting reaction terminates `done` with reason
`agent:not_present`, and whats_waiting listed each one. Three in the founder's
queue on 2026-09-05, all the same sentence about the same deployment, and the
count only goes up.
WHICH CHANGE, and why this one: the 24-hour horizon the brief offered as the
alternative ALREADY EXISTS and already ships — `NOT_PRESENT_WINDOW_S = 86_400`
in flows_queue.pending(), green since the module was born
(tests/test_queue_waiting.py::test_an_old_absence_stops_being_news). So the
defect is not that these items are permanent; it is that within any rolling day
there is one per meeting. "Surfaced once" is the genuinely new concept and the
LARGER change — the reaction table has no `seen` column and no dismissal, by
design ("the queue advances as they go", founder). The smallest change the model
already supports is the third option: collapse them, exactly as `notices()` has
collapsed the identical case since it was written — "two reactions of the same
flow would otherwise say the identical sentence twice in one result, which reads
as two different things being true."
So: one item per (flow, absent domain), the most recent kept, so `since` reads as
when this last happened. The horizon bounds how LONG an absence is news; this
bounds how many TIMES it is news at once.
Scope held deliberately narrow. Pending items are each a distinct thing in
flight and failed items are each a distinct thing to look at — neither is
collapsed. Two different absent domains stay two items, because a person told
about one of two missing domains has been told something false by omission. And
the collapse lives in `waiting()`, NOT in the shared `pending()`: `notices()`
reads `pending()` too and rides out on the meeting tools' results to an agent
that never asked, so a filter placed there would silently change that answer.
Tests: 6 new in core/flows tests/test_queue_waiting.py (three identical absences
say one thing · the survivor is the most recent · two domains stay two facts ·
pending and failed are never collapsed · the standing notices are untouched · an
old absence goes while a same-age pending item stays).
Suite state, stated plainly: `uv run pytest -q` in core/flows is RED AT THIS BASE
— 23 failed, 551 passed, 5 skipped on origin/t27-seq23-oss-head untouched, across
test_no_agents / test_no_meetings / test_ics_property_anchor / test_link_loop /
test_carrier_census / test_config_declaration / test_queue_waiting (e.g.
KeyError: 'desk.unscaffolded' — production's registry and its tests disagree).
None of them are in the code this commit touches. After it: 23 failed, 557 passed,
5 skipped — the failing set is byte-identical to the baseline, verified by diff.
Reported, not fixed here: it is a separate finding about the branch.
Signed-off-by: DmitriyG228 <2280905@gmail.com>
…d in
fr_b6340167da32b8b6 — every per-meeting tool took `platform` +
`native_meeting_id`, which names a ROOM. A Google Meet link is the same link
every week, so downstream resolves the pair to the caller's NEWEST row on it
(`_resolve_owned_native`). Reads were already fine — list_meetings returns every
row and GET /transcripts/by-id/{id} fetches an exact one — but the WRITE had no
such door: an agent could pull last week's transcript and then had nowhere to put
what it learned. `annotate_meeting` wrote to this week's call instead, and the
response looked exactly like success. A wrong row that answers 200 is the worst
shape a defect can take.
`meeting_db_id` is not a new identity. It is the value every tool ALREADY hands
back — `meeting_db_id` on annotate_meeting and on every search hit, `id` on a
meeting row — and the tools simply would not take it back. Now optional on
annotate_meeting, get_meeting_transcript and search_transcripts (there as a
filter), and it takes precedence over the pair wherever both are supplied,
because a caller holding both is asking about one meeting rather than about the
room it was held in.
Wired through:
* meeting-api — new POST /meetings/{meeting_id}/annotate, three segments against
the pair route's four so neither shadows the other on segment count (the
property POST /meetings/{meeting_id}/share already relies on). Body validation
and the not-found/too-large refusals are factored into one shared helper, so
the two addressings cannot answer differently. GET /transcripts/search gains a
`meeting_id` filter through the port, the SQL adapter and the in-memory fake.
* gateway — the matching POST /meetings/{meeting_id}/annotate forward, plus its
row in core/meetings/routes.v1.json (36 → 37) and the two gateway tests that
hold the assembled table exact: the scoped-route count (67 → 68) and the scope
matrix's coverage row. Both are declarations that are MEANT to move with a
route; moving them is the point of having them.
* mcp — the three tools, their descriptions (a room code is not a meeting, said
where a model will read it), and the missing-id refusal, which now names
`meeting_db_id` so a caller looking at a row it already holds is told the
shortest way in.
Deliberately NOT extended: stop_bot, update_bot_config, speak_in_meeting,
get_meeting_chat. Each addresses a LIVE bot, and the newest row on a link is by
definition the one with a bot in it — a db id would buy nothing and would need a
route apiece.
Tests: 11 new in meeting-api tests/test_meeting_by_db_id.py — two meetings on one
room code, the older annotated by db id and the newer proven untouched; the room
code path proven UNCHANGED (still the newest); both routes proven to coexist; an
id belonging to another user is a 404, not a write; identical validation on both
routes; live-meeting annotate still works; and the four search-filter cases. Plus
13 new in mcp tests/test_meeting_db_id_addressing.py asserting the URL the shipped
forwarding path builds for each tool, either addressing, and the refusals.
`uv run pytest -q`: core/meetings/services/mcp 302 passed (289 before);
core/meetings/services/meeting-api 1379 passed, 5 skipped (1368 before);
core/gateway/services/gateway 329 passed, 38 skipped, 1 xfailed (326 before);
core/gateway/services/conformance 86 passed, unchanged (the api.v1 conformance
gate — the new route does not disturb it).
Signed-off-by: DmitriyG228 <2280905@gmail.com>
|
| GitGuardian id | GitGuardian status | Secret | Commit | Filename | |
|---|---|---|---|---|---|
| 36961510 | Triggered | Generic Password | b173e0f | core/meetings/services/meeting-api/tests/test_meeting_link.py | View secret |
| 36961511 | Triggered | Generic Password | b173e0f | core/meetings/services/mcp/tests/test_parse_meeting_link.py | View secret |
| 36961510 | Triggered | Generic Password | b173e0f | core/meetings/services/mcp/tests/test_parse_meeting_link.py | View secret |
🛠 Guidelines to remediate hardcoded secrets
- Understand the implications of revoking this secret by investigating where it is used in your code.
- Replace and store your secrets safely. Learn here the best practices.
- Revoke and rotate these secrets.
- If possible, rewrite git history. Rewriting git history is not a trivial act. You might completely break other contributing developers' workflow and you risk accidentally deleting legitimate data.
To avoid such incidents in the future consider
- following these best practices for managing and storing secrets including API keys and other credentials
- install secret detection on pre-commit to catch secret before it leaves your machine and ease remediation.
🦉 GitGuardian detects secrets in your source code to help developers and security teams secure the modern development process. You are seeing this because you or someone else with access to this repository has authorized GitGuardian to scan your pull request.
| from fixtures import INVITE_REFS, drain | ||
| from flows import FakeClock | ||
|
|
||
| path = tempfile.mktemp(suffix=".db") |
DmitriyG228
added a commit
that referenced
this pull request
Sep 5, 2026
CodeQL flagged four "incomplete URL substring sanitization" alerts on the 0.12.27
train, all the same shape: a platform was decided by a SUBSTRING test on the
hostname. `"zoom.us" in host` also accepts zoom.us.evil.example (the attacker owns
the registrable domain) and notzoom.us; `host.endswith("teams.live.com")` also
accepts eviltteams.live.com. A caller acting on the answer sends a bot to the real
platform on an attacker's say-so, so the parse is a trust decision even though the
parser itself does no I/O.
Both parsers now compare the parsed `hostname` exactly, or as an explicit
dot-separated subdomain (`_host_is`). That is precisely what the legitimate cases
are — us05web.zoom.us, company.zoom.us, frbmeetings.zoomgov.com,
contoso.teams.microsoft.com, gov./dod.teams.microsoft.us — so #1547's vanity and
zoomgov behaviour is preserved by construction, and its hosted-domain branch
(path shape + passcode, hostname deliberately ignored) is untouched.
One substring test is KEPT and now says why: meeting-api's `"zoom" in host`. It is
a name heuristic, not a domain allowlist — it is what claims a hosted Zoom front
door whose hostname still says zoom (zoom-lfx.platform.linuxfoundation.org),
including in the ICS free-text scan where the path-shape branch is switched off,
and a test pins that. It takes nothing from the URL but a 9-11 digit meeting id.
Also `py/polynomial-redos` on the Teams thread scan: `19:meeting_[^@%\s/]+@thread\.v2`
is `.search()`ed over caller-supplied text, so every start offset that fails at
`@thread.v2` is retried at the next one — quadratic on a string the caller chooses.
The repeat is bounded to {1,256}, far above any real thread id.
Tests: 13 new spoof/preservation cases across the two suites (suffix spoof, prefix
spoof, path-borne domain name, and the legitimate subdomains in the same shapes),
plus a bounded-scan pair. GitGuardian's "Generic Password" hits from 1998480 are
the same fixtures: the UUID-shaped passcode and `pwd=s3cret` / `password=Abc123` are
now `placeholder-*` values that exercise the parser identically.
core/meetings/services/mcp: 345 passed (335 before).
core/meetings/services/meeting-api: tests/test_meeting_link.py 31 passed;
tests/ -k "link or zoom or teams or meet" 272 passed, 1120 deselected.
Refs #1553
Signed-off-by: DmitriyG228 <2280905@gmail.com>
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.
Five frictions the founder's agent hit on the production Vexa MCP on 2026-09-05, filed through
report_friction. One commit each, each with tests.fr_2261a6306224c6f8POST /botsreturneddata.webhook_secretin clear — through the gateway, into the MCPrequest_meeting_botresult, into a calling model's context and its logs.bot_spawn.service._meeting_responsecopiedrow["data"]verbatim; every other meeting-serving route already goes throughcollector.projection. It now does too,viewer_is_owner=True, so the v0.10 contract'swebhook_url/webhook_eventsstill ride back and only tier-1 credential material goes. The stored row is untouched — the lifecycle callback still signs from it.b6cc8cb2cfr_b6340167da32b8b6platform+native_meeting_idresolves to the caller's newest row: an agent could read an older transcript and had nowhere to write back —annotate_meetingwrote to this week's call and answered 200.meeting_db_id(the value every tool already returns) is now accepted byannotate_meeting,get_meeting_transcriptandsearch_transcripts, taking precedence over the pair. NewPOST /meetings/{meeting_id}/annotatein meeting-api and the gateway, ameeting_idfilter on/transcripts/search.28e37c553fr_db203061a7a1d953list_recordingsignoredlimit/offsetand returned all 201 recordings, 1.6 MB, oldest first, with per-chunk media metadata. The MCP tool and the gateway were forwarding the arguments correctly;GET /recordingsdeclared none of them, and FastAPI drops an undeclared query parameter in silence. Now declared with bounds (out of range is a 422, not a clamp), sorted newest-first, and projected to a list shape — full detail stays onGET /recordings/{id}. Themeeting_idfilter, silently dropped the same way, works too.0242d85f0fr_042129f5d53aa543parse_meeting_link422'd on Zoom hosted on a vanity domain (https://zoom-lfx.platform.linuxfoundation.org/meeting/<id>?password=<uuid>). Recognised on the path shape — Zoom's own two join paths, a 10-11 digit id, and a passcode — never on the hostname, since the hostname is the part a hosted front door replaces. Reported inwarnings; a host declared inVEXA_JITSI_HOSTSis exempt. meeting-api's twin parser gets the same branch. Accepted Zoom URL shapes are now stated in the mcp service README.b173e0fdcfr_e612b14fba618eeawhats_waitinggrew by one unactionable item per completed meeting on a deployment with no agent. The 24 h horizon the brief offered as an option already exists and already ships (NOT_PRESENT_WINDOW_S, green since the module was born), so the defect is duplication within a day, not permanence. Smallest change the model supports: collapse to one item per(flow, absent domain), newest kept — exactly asnotices()has done since it was written. Pending items, failed items and thenotice: truestanding offer are untouched, and the collapse sits inwaiting()rather than the sharedpending()so the notices ride-along cannot move.8f9121a3dTests
uv run pytest -qper package — the commandscripts/gates.mjs pythonruns.core/meetings/services/mcpcore/meetings/services/meeting-apicore/gateway/services/gatewaycore/gateway/services/conformancecore/flowsTwo pre-existing conditions on the base, reported not fixed
The base is
origin/t27-seq23-oss-head(4b689535) — the exact head production's mcp and meeting-api images were built from. Both of these are present on that commit untouched, verified in a pristine worktree of it:core/flows's test suite is red: 23 failed, 551 passed. Acrosstest_no_agents,test_no_meetings,test_ics_property_anchor,test_link_loop,test_carrier_census,test_config_declarationandtest_queue_waiting— production's flow registry and its tests disagree (e.g.KeyError: 'desk.unscaffolded'). None are in the code touched here; the failing set after this branch is byte-identical to the baseline, verified bydiff.gate:dataflowfails, and it is a pre-push hook:core/flows/contracts/flows.v1exists on disk but is not registered inarchitecture.calm.json. This branch was therefore pushed with--no-verify. Nothing here adds or moves a contract directory.Reading the diff
core/flowsdoes not exist onmain, so this PR againstmaincarries the wholet27-seq23-oss-headlineage. The five commits above are the entire change;git diff 4b689535..t27-mcp-frictions-1is the review surface.Not merged, not deployed, nothing sent. Draft.