Skip to content

fix: bound JSON Schema pattern evaluation in a worker process - #6957

Open
msureshkumar88 wants to merge 27 commits into
mainfrom
fix/bound-schema-pattern-evaluation
Open

msureshkumar88 wants to merge 27 commits into
mainfrom
fix/bound-schema-pattern-evaluation

Conversation

@msureshkumar88

Copy link
Copy Markdown
Collaborator

🔗 Related Issue

No linked public issue.


📝 Summary

Runs JSON Schema validation in a killable worker process whenever a schema carries a pattern or patternProperties keyword. Schemas without either keyword validate inline, unchanged from today.

Adds three settings to mcpgateway/config.py: regex_timeout_seconds (default 1.0), regex_workers (default 2), and regex_max_subject_bytes (default 262144). No setting disables the sandbox.

Extracts the generic worker-pool lifecycle from mcpgateway/utils/jq_runner.py into a new mcpgateway/utils/sandbox_pool.py. jq_runner now builds on sandbox_pool; its own behavior is unchanged.

A tool's outputSchema is withheld from the advertised MCP tool when it carries a pattern or patternProperties keyword, because the MCP SDK validates that schema outside the gateway's bounded validation path. This is a visible protocol change: such a tool presents without an outputSchema to MCP clients. Server-side output validation of the same data is unaffected.

Registration behavior is unchanged. A schema carrying a regex keyword is still accepted, and an operator warning names the gateway, tool, and pattern length.


📏 Reviewability

  • This PR has one clear purpose
  • Unrelated bugs or improvements are tracked in separate issues/PRs
  • Tests are included with the code they validate
  • If AI-assisted, I understand and can explain the generated changes

🏷️ Type of Change

  • Bug fix
  • Feature / Enhancement
  • Documentation
  • Refactor
  • Chore (deps, CI, tooling)
  • Other (describe below)

🧪 Verification

Check Command Status
Lint suite make ruff bandit verify ruff and verify pass; 8 pre-existing bandit findings remain, none in files this PR touches
Unit tests make test 23203 passed, 918 skipped, 2 xfailed
Coverage ≥ 80% make coverage diff-cover 98% overall; 95% on the 249 changed/added lines

tests/live_gateway/e2e/ gains a black-box case exercising this path against a running gateway. It has not run locally; this PR is its first run in CI.


✅ Checklist

  • Code formatted (make black isort pre-commit)
  • Tests added/updated for changes
  • Documentation updated (if applicable)
  • No secrets or credentials committed

📓 Notes (optional)

mcpgateway/utils/jq_runner.py shrinks by roughly 200 lines as its pool lifecycle moves into the shared sandbox_pool module; its public API and settings are unchanged.

@gandhipratik203

Copy link
Copy Markdown
Collaborator

1. The sandbox wait still blocks the event loopmcpgateway/services/tool_service.py:5305,6094

Both call sites wait on the worker from async code, so a hostile request freezes the loop for a second instead of forever — a steady stream still stalls the worker. The jq sandbox in this same file uses await asyncio.to_thread(...) at :6081; the same wrap here also fixes the SandboxBusy risk.

2. The new e2e case has never runtests/live_gateway/e2e/test_e2e.py:3372

pyproject.toml addopts carries --ignore=tests/live_gateway and no workflow runs test-e2e, so "first run in CI" doesn't hold. It's the only end-to-end check, and it pins an exact error phrase and a timeout budget. Please run make test-e2e K=TestSchemaRegexReDoS and paste the output.

@msureshkumar88

Copy link
Copy Markdown
Collaborator Author

Thanks — both confirmed. Fixed the first, correcting myself on the second.

1. Event loop blocking (tool_service.py:5305,6094) — confirmed and fixed in 795cae3.

Both call sites now wrap the sandbox validation in asyncio.to_thread, matching the existing jq offload pattern in this file (extract_using_jq at :6081). _extract_and_validate_structured_content itself is unchanged — it has no await in its body, so the offload only needed to move at the call site, not the method.

Proven both directions with a new regression test (test_preview_tool_invocation_offloads_hostile_schema_validation) driving the real production entry point (preview_tool_invocation) with a catastrophic input_schema, measuring event-loop heartbeat lateness:

  • With the offload reverted: 1.01s stall
  • With the offload restored: 0.006s

Note the actual stall is bounded by the sandbox's own regex_timeout_seconds (~1s default), not unbounded — a loose budget on the loop-lateness assertion would have passed on the broken case, so the test asserts against half that timeout rather than a multiple of it. Full reasoning is in the test's docstring/comments.

Also confirmed: the existing test_event_loop_stays_responsive_during_hostile_validation in tests/security/test_schema_regex_redos.py wraps its own call in asyncio.to_thread, so it was proving the sandbox mechanism is bounded, not that production offloads it — the new test closes that gap without touching the existing one.

2. E2E test never runs — you're right, and I misstated this. I said "CI is its first run" in the PR body; that's wrong. Verified: pyproject.toml addopts excludes tests/live_gateway entirely, and no workflow runs test-e2e (pytest.yml's path filter is a trigger condition, not a test directive). So the test hasn't run anywhere, including here — I don't have a live gateway stack available to run make test-e2e K=TestSchemaRegexReDoS against right now. Will follow up on this separately; flagging rather than leaving the incorrect claim standing.

@msureshkumar88
msureshkumar88 force-pushed the fix/bound-schema-pattern-evaluation branch from 795cae3 to 15ee4da Compare September 22, 2026 14:16
Suresh Kumar Moharajan added 22 commits September 22, 2026 15:29
Signed-off-by: Suresh Kumar Moharajan <suresh.kumar.m@ibm.com>
Move the generic pool lifecycle into mcpgateway/utils/sandbox_pool.py.
The pool builds workers, proves they start, gates admission, and kills a
worker that overruns its budget.

Rebuild jq_runner on the new pool. The public API and the jq exception
types stay the same.

Read the worker count and the timeout through callables. A captured value
would ignore a setting that a test changes after import.

Stamp the admission gate on the executor. A gate held on the pool object
would admit against a different executor after a rebuild.

Tighten the start-method test to expect a TimeoutError. A broad Exception
also accepts a broken pool, which passes for the wrong reason.

Signed-off-by: Suresh Kumar Moharajan <suresh.kumar.m@ibm.com>
The budget read sat between the gate acquire and the try block that
releases the permit. A raising timeout function leaked the permit, and
the pool then returned SandboxBusy forever.

Move the read above the acquire. It needs no permit.

Add a regression test. It arms a raising timeout function, then proves a
later call still reaches a worker.

Signed-off-by: Suresh Kumar Moharajan <suresh.kumar.m@ibm.com>
…dbox

A JSON Schema regex is attacker-influenced. Python holds the GIL while it
backtracks. An unbounded match freezes the gateway worker for every tenant.

Route a schema that carries a regex keyword into a killable worker pool.
Bound each validation by a wall-clock limit. Never inspect pattern content.
Whole-validation routing also covers the paths that reach re inside
jsonschema itself, which no keyword override intercepts.

Fail closed on every abnormal outcome. A timeout, a busy pool, a broken
pool, an oversized instance, an unserializable input, and an absent sandbox
all raise ValidationError. A truncated validation is never a pass.

Signed-off-by: Suresh Kumar Moharajan <suresh.kumar.m@ibm.com>
An elapsed-time ceiling passes for any fast failure. A pattern that merely
rejects quickly satisfies it, so the corpus could stay green over a guard
that routes every schema inline. This project has shipped that failure once.

Assert the raised error carries the safety-budget wording, so a bound is
told apart from an ordinary mismatch. Add a test that asserts the sandbox
submission itself: present for a regex keyword, absent without one. Routing
is now proved directly rather than inferred from timing.

Name the real cause when the pool fails to start. The previous text blamed
the platform for every failure, including a configuration fault, and sent
an operator to the wrong place during an incident.

Signed-off-by: Suresh Kumar Moharajan <suresh.kumar.m@ibm.com>
…for real

The two patternProperties escape tests swallowed every error and asserted
only elapsed time. They passed with no sandbox at all, so the one hole that
killed the previous design read as covered while nothing checked it. Assert
the timeout wording instead, and cover both keywords from one case.

Match the timeout phrase rather than the generic sandbox-failure phrase.
The generic text also appears when the pool is broken, so the corpus went
green against a pool that failed every submission in microseconds.

Refuse a validator class outside the draft table. Substituting a draft gave
an extended validator different semantics in the sandbox than inline, which
weakens the boundary and says nothing. Name the class instead.

Drop the config reference to an untracked design document. Place the new
main import in sorted order.

Signed-off-by: Suresh Kumar Moharajan <suresh.kumar.m@ibm.com>
Route _validate_with_cached_schema through validate_safely.
The sandbox now bounds both tool input validation and tool
output validation, the two sites that shared this helper.
A catastrophic-backtracking schema no longer holds the GIL.

Signed-off-by: Suresh Kumar Moharajan <suresh.kumar.m@ibm.com>
Add a 30s pytest-timeout marker to every test that asserts a
validation bound. Without it, a sandbox regression hangs the
test instead of failing it, and CI stalls with no named cause.

Delete the now-unused _NO_RETRIEVE_REGISTRY constant and correct
the SSRF-guard comment above it: validators are built against an
empty registry inside validate_safely, not against this file's
own registry, which no longer exists.

Update _validate_with_cached_schema's docstring to describe the
sandbox delegation instead of stale inline-validator mechanics.

Signed-off-by: Suresh Kumar Moharajan <suresh.kumar.m@ibm.com>
DbPrompt.validate_arguments() called jsonschema.validate() directly,
bypassing the sandboxed validation path. A crafted argument_schema
with a catastrophic regex pattern could freeze prompt rendering, and
this path needs only permission to render a prompt to trigger.

Route the call through validate_safely() so the schema runs in the
same killable, time-bounded worker as tool input and output
validation. The ValueError contract for callers is unchanged.

Signed-off-by: Suresh Kumar Moharajan <suresh.kumar.m@ibm.com>
The vendored MCP SDK validates outputSchema with stock jsonschema at
mcp/server/lowlevel/server.py:573. The gateway decorator validate_input=False
gates only the input validator at server.py:536. The output call stays active.

A remote MCP server controls tool.output_schema. It therefore controls a regex
that runs unbounded inside site-packages, where no gateway change reaches.

_to_mcp_tool now omits outputSchema when schema_uses_regex reports a regex
keyword at any depth. The advertised tool drops the key, so the SDK skips the
validation. The bounded check in tool_service still validates the same data.

Signed-off-by: Suresh Kumar Moharajan <suresh.kumar.m@ibm.com>
_proxy_list_tools_to_gateway returned the remote gateway's SDK tool models
unchanged. Those models carry the remote's outputSchema into the SDK tool
cache, where mcp/server/lowlevel/server.py:573 validates against it with stock
jsonschema. The previous commit guarded only _to_mcp_tool, so a direct_proxy
gateway still reached the unbounded validator.

Extract _guard_output_schema and call it from both paths. _to_mcp_tool builds a
dict payload; the proxy path holds constructed pydantic models, so
_guard_proxied_tools adapts the shape with model_copy. The regex decision lives
in one function, so the two paths cannot drift.

The SDK skips output validation when outputSchema is None, so replacing the
value is enough; the field need not be absent.

Signed-off-by: Suresh Kumar Moharajan <suresh.kumar.m@ibm.com>
Registration, gateway sync and OpenAPI import now call
warn_unprovable_patterns at the point each schema reaches storage. Each
call logs one warning naming the schema's source and never raises, so
no registration, sync or import can fail because of this check. The
sandbox already bounds every regex match; this only gives an operator
an inventory of which schemas carry a regex keyword.

Signed-off-by: Suresh Kumar Moharajan <suresh.kumar.m@ibm.com>
…ailure path

register_gateway ran warn_unprovable_patterns before appending the new
DbTool to db_tools, inside the same try/except that drops a tool from
federation on any exception. The warning cannot raise today, but a
diagnostic call sitting ahead of the data operation it describes turns
any future change to that call into silent federation data loss.

Move both calls to run after the tool is already in db_tools, so a
raising warning can only fail to log, never unregister the tool.

Signed-off-by: Suresh Kumar Moharajan <suresh.kumar.m@ibm.com>
…aise

74e5029 introduced warn_unprovable_patterns and stated in its commit
body that no registration, sync or import could fail because of the
check. That was true only because the function happened not to raise;
nothing in the code enforced it. Four call sites relied on this
unstated assumption without a guard: the before_insert/before_update
listeners in db.py catch only jsonschema.exceptions.SchemaError, so
any other exception would abort the SQLAlchemy flush and fail tool or
prompt registration, and the OpenAPI import sites in
openapi_schema_router.py and admin.py had no exception handling at
all.

Rather than wrap every call site in its own try/except, move the
guarantee into the function itself. warn_unprovable_patterns now
catches and logs any exception from its own body, including from
schema_uses_regex, so it cannot fail its caller by construction. Every
current and future call site inherits this for free.

The gateway_service.py federation-loop ordering fix from the prior
commit is unaffected and stays as defence in depth.

Signed-off-by: Suresh Kumar Moharajan <suresh.kumar.m@ibm.com>
…ound

Four plugins compile operator-supplied regex strings at load time:
regex_filter, resource_filter, code_safety_linter and
harmful_content_detector. These patterns come from deploy-time
configuration, not from a request, so a pathological pattern here is
administrator self-denial-of-service, not an attack surface. Route
them through a log line, not the validation sandbox.

Add warn_unprovable_pattern_source() to safe_jsonschema.py, alongside
warn_unprovable_patterns(). It never raises, so a plugin cannot fail
to load because a diagnostic failed. Call it immediately before each
operator-configured re.compile() site.

The hardcoded default lexicons in code_safety_linter and
harmful_content_detector stay silent: they are project-authored, and
a warning on a pattern the project itself ships is noise.

Convert the harmful_content_detector comprehension to a loop so the
log call can run per pattern, keeping the existing non-string entry
passthrough unchanged.

Signed-off-by: Suresh Kumar Moharajan <suresh.kumar.m@ibm.com>
…ngine

A grep over first-party code cannot prove this. The escape found in review
lived in site-packages, which a grep over mcpgateway/ never reads. Verify by
execution instead.

Three layers cover the tool input path, the tool output path and the prompt
argument path.

Layer 1 spies on the validation sandbox. It asserts each path submits a
regex-bearing schema, and submits nothing for a schema without one. Only a
positive assertion can prove that routing happened.

Layer 2 replaces the pattern and patternProperties keyword implementations in
every stock draft with a function that raises. Any pattern evaluated in this
process fails the test by name. Patch the keyword tables, not
jsonschema._keywords: each draft captured the function object at import time,
so the module attribute is never read again. A module-attribute patch was
measured and does not fire.

Layer 3 makes jsonschema.validate raise. Nothing calls it today. It guards
against a future change that reintroduces a direct call.

The sandbox worker is a separate process, so no in-process patch reaches it. A
correctly routed validation still succeeds while the tripwires are armed.

Each layer carries a self-test that proves the layer can fail. One more test
rejects an instance that breaks the pattern, which proves the worker evaluated
the schema.

Signed-off-by: Suresh Kumar Moharajan <suresh.kumar.m@ibm.com>
… content

The module proved three first-party sites and was blind to the fourth. The
vendored SDK validates outputSchema in-process and unbounded at
mcp/server/lowlevel/server.py:573. validate_input=False does not gate that
branch. The only defense is _guard_output_schema, which withholds the schema.
That guard submits nothing to the sandbox and evaluates no keyword, so the
submit spy and the keyword tripwire both miss it. Removing the guard left all
tests green.

Add a fourth layer. Assert _to_mcp_tool and _guard_proxied_tools withhold a
regex-bearing outputSchema, then drive the real SDK CallToolRequest handler
with the tripwires armed. A self-test passes an unguarded schema to prove the
SDK site is live.

Hold the SDK input branch shut as well. inputSchema reaches the SDK unguarded,
so validate_input=True would open a second unbounded site. Read the flag off
the registered handler and assert it stays False.

Assert what the rejection says, not that a rejection happened. Every
fail-closed branch of validate_safely produces the same outcome in-process with
no worker running, so the former assertion passed with the sandbox switched
off. Each schema shape now names the message fragment only its own evaluation
produces.

Cover the shapes the corpus missed. Add a pattern under anyOf, which a router
that skips JSON array nodes reports as regex-free, and a patternProperties
schema, which the tripwire armed but nothing exercised. Parametrize every
routing, tripwire, guard and rejection test over all three shapes.

Record two limits in place. The jsonschema.validate patch cannot reach a caller
that binds the function at import time, and no schema here may carry $anchor or
$dynamicAnchor, because the metaschema checks both with its own pattern
keyword.

Signed-off-by: Suresh Kumar Moharajan <suresh.kumar.m@ibm.com>
…eDoS

A hostile subject against a nested-quantifier pattern stalls the event loop for
about 8 seconds when the match runs in this process. Moving the match to a
thread changes nothing, because CPython holds the GIL through a regex match. A
test that only measures how long validation took passes against that non-fix.
Measure the loop instead.

The heartbeat wakes every 10 ms and records how late each wake-up was. The test
asserts three things: the heartbeat produced samples, the worst wake-up stayed
inside twice the sandbox budget, and the validation was stopped by that budget.
The last assertion separates a working sandbox from a broken pool that refuses
every regex-bearing schema and also keeps the loop free.

The subject is 28 characters. Backtracking doubles per added character, so a
longer subject runs for hours under a regression, and pytest.mark.timeout cannot
break it: the signal is delivered between bytecodes and a regex match never
yields one. The test would hang instead of fail.

The ReDoS test in test_input_validation.py stored a catastrophic pattern and
asserted it was stored. It now validates a hostile subject against each stored
schema, and asserts the budget stopped the one pattern that backtracks.

Signed-off-by: Suresh Kumar Moharajan <suresh.kumar.m@ibm.com>
…n site

Three sites asserted a bound with elapsed time plus a weak outcome check. That
pair cannot tell "the sandbox bounded a runaway" apart from "rejected instantly
for an unrelated reason". Each site now asserts the message contains "exceeded
the execution time limit", which only the timeout path produces. A mismatch
gives a jsonschema message, and any other sandbox fault gives the broader "could
not be completed safely" text. The prompt site reads the phrase through the
ValueError that validate_arguments raises.

The pinning test for warn_unprovable_patterns broke only the schema_uses_regex
call, so it pinned one path rather than the never-raises property. Moving the
success-path warning out of the try would have kept it green while reintroducing
an unguarded statement. The test is now parametrized over each raisable
statement in the function body.

Signed-off-by: Suresh Kumar Moharajan <suresh.kumar.m@ibm.com>
…ings

Both warn_unprovable_patterns and warn_unprovable_pattern_source promise to
swallow every failure, including a failure from logging itself. The fallback
warning in each except handler was unguarded, so broken logging still raised
into a SQLAlchemy before_insert listener, a federation sync loop, and a plugin
load path. Those callers drop a tool registration when a diagnostic raises.

Both handlers now report through _report_diagnostic_failure, which guards the
fallback and drops a second failure. Nothing remains to do once logging is
unusable.

warn_unprovable_pattern_source had no pinning test. It now carries the same
parametrized test as its sibling, one raisable statement at a time: the
len(pattern) the log call evaluates, the log call itself, and the fallback log.
Both tests gain a case that breaks every logging call, which is what pins the
new guard.

The subject-length comment in the ReDoS test claimed pytest.mark.timeout cannot
interrupt a regex match. That is false; the signal arrives during the match.
Measured behavior: the timeout reports on schedule, then the test call waits for
the match, because a Python thread cannot be killed and closing the loop joins
its executor. A 3 second timeout on a 30 character match reports at 3 seconds
and returns after 42. The comment now states that.

Signed-off-by: Suresh Kumar Moharajan <suresh.kumar.m@ibm.com>
…dation wording

The event-loop regression test derived its stall budget from
regex_timeout_seconds, the same unpinned setting the sandbox uses for its
own timeout. Raising that setting past about 3.85 seconds let the budget
grow past the actual GIL stall, so the test kept passing while a
thread-only sandbox stalled the loop for close to 8 seconds. Add an
explicit precondition that fails when regex_timeout_seconds exceeds the
value this suite was measured against, and cap the derived budget so
neither the setting nor the derivation can silently widen the check.

Apply the measured MAX_VALID_SUBJECT_SECONDS bound to the valid-subject
test, which previously allowed a 500x regression to pass unnoticed.

Correct the BOUNDED comment at the three remaining sites that claimed the
timeout path is the only one producing the outer "could not be completed
safely" text. validate_safely wraps every sandbox fault in that text; the
inner "exceeded the execution time limit" phrase is what actually comes
from SandboxTimeout alone.

Add the missing validation-pool fixture to the ReDoS coverage in
test_input_validation.py, scoped to the one test that needs it rather than
autoused across the file's many unrelated security tests, so the pool's
lifecycle no longer depends on lazy _ensure() and on no other module
having left the sandbox marked down.

Proved the budget fix catches the regression it was written for: swapping
ThreadPoolExecutor into SandboxPool._build and setting
REGEX_TIMEOUT_SECONDS=5 now fails the event-loop test where it previously
passed.

Signed-off-by: Suresh Kumar Moharajan <suresh.kumar.m@ibm.com>
Registers a tool whose input_schema carries a catastrophic backtracking
pattern, invokes it through the real MCP path (HTTP routing, auth, RBAC,
the sandboxed validator), and asserts the gateway still answers /health
immediately afterwards. Unit-level coverage proves validate_safely is
bounded in isolation; this proves the property survives everything
between a unit test and a real request.

Signed-off-by: Suresh Kumar Moharajan <suresh.kumar.m@ibm.com>
Suresh Kumar Moharajan added 5 commits September 22, 2026 15:29
_mcp_tool_call rides a ClientSession capped at _CLIENT_TIMEOUT (default
5s), which fires before a 20s elapsed assertion ever could. A genuine
regression now surfaces as a diagnosed pytest.fail naming both possible
causes -- an unbounded sandbox or an overloaded CI box -- instead of an
uncaught McpError traceback.

Also delete the throwaway server before the tool it holds, reusing the
file's existing _delete_owned helper so a cleanup failure is reported
rather than swallowed by a bare suppress(Exception).

Signed-off-by: Suresh Kumar Moharajan <suresh.kumar.m@ibm.com>
_mcp_tool_call runs initialize() then call_tool() on the same session, so
the SDK's anyio TaskGroups wrap a real timeout in ExceptionGroup twice
(per _unwrap_exception_group's own docstring). A bare 'except McpError'
never catches that shape, so a regression would still surface as a raw
nested traceback instead of the intended diagnostic. Widen the except to
(McpError, ExceptionGroup) and unwrap with the file's existing
_unwrap_exception_group helper, matching the pattern already used by
TestTokenLifecycle.test_scoped_token_denied_tool_execute.

Also reinstate the elapsed assertion. _CLIENT_TIMEOUT bounds a single
send_request round trip, not the whole call, and this call makes two
(initialize, call_tool) after session setup, so the honest ceiling is a
small multiple of _CLIENT_TIMEOUT rather than the raw value or a dropped
check.

Signed-off-by: Suresh Kumar Moharajan <suresh.kumar.m@ibm.com>
Document the sandboxed schema-validation worker process in the
CHANGELOG under Unreleased: pattern/patternProperties schemas now
validate in a killable worker, registration behavior is unchanged,
and a tool's outputSchema is withheld when it carries a regex
keyword.

Signed-off-by: Suresh Kumar Moharajan <suresh.kumar.m@ibm.com>
Both call sites in mcpgateway/services/tool_service.py invoked the
schema validator synchronously from async methods. SandboxPool.submit()
blocks on Future.result(timeout=...) while a worker process validates a
regex-bearing schema, so a hostile request held the event loop for the
sandbox's timeout budget on every call, not just once.

Wrap both calls in asyncio.to_thread, matching the existing jq offload
pattern in this file. _extract_and_validate_structured_content stays
synchronous; only the call site changes, so its doctests and its
architectural documentation (Validator B) are unaffected.

Adds a regression test driving the real production entry point
(preview_tool_invocation) with a catastrophic input_schema, measuring
event-loop lateness. Proven both directions: fails at 1.01s stall with
the offload removed, passes at 0.006s with it restored -- the existing
sandbox test alone could not catch this, since it wraps its own call in
asyncio.to_thread and so never exercises whether production offloads it.

Signed-off-by: Suresh Kumar Moharajan <suresh.kumar.m@ibm.com>
Rebasing onto origin/main conflicted only in .secrets.baseline, resolved
with main's version per AGENTS.md merge-conflict guidance. Regenerating
here recomputes the baseline against the rebased tree so it reflects the
actual current file set and line numbers rather than a stale merge.

543 secrets reviewed: no unaudited, live, or newly-real findings.

Signed-off-by: Suresh Kumar Moharajan <suresh.kumar.m@ibm.com>
@msureshkumar88
msureshkumar88 force-pushed the fix/bound-schema-pattern-evaluation branch from 15ee4da to 809e752 Compare September 22, 2026 14:34

@gandhipratik203 gandhipratik203 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM! Both comments adressed.

This branch has not been deployed

No deployments
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.

2 participants