feat(routing): toAiGateway() terminator for BoxLang AI Gateways - #694
Conversation
|
CI status on Fixed by this push: Still red in one run: I do not believe that failure belongs to this PR:
I could not name the individual failing spec: the job log's readable tail is the test harness's own server-log dump, and the annotations on the TestBox Report check are not reachable from here. Worth noting the harness contains a deliberately random-failing scheduled task ( I also could not re-run the failed job to confirm: the API returns Generated by Claude Code |
|
Follow-up with the actual failing spec, now that I can read the full job log rather than just its tail. Failing spec: This confirms it is not this PR's:
I still don't have the means to re-run just that job ( Generated by Claude Code |
Exposes a BoxLang AI Gateway over HTTP natively, alongside toAi() and toMCP().
Until now an application had to hand-write the routes and a passthrough handler
for the gateway surface, which is exactly what BX Agents generates per project.
route( "/gateways" ).toAiGateway( session: "SupportAgentSession" ) registers:
GET/POST {pattern}[/:gateway]/events platform handshake + inbound
GET {pattern}/interactions/:requestID poll a pending approval
POST {pattern}/interactions/:requestID/decisions submit a human decision
GET {pattern}/info what this mount serves
GET and POST share the /events path because a platform is given ONE URL and
verifies it with a GET before it ever POSTs to it. Pin a mount to one gateway
with toAiGateway( "slack" ), or leave the name out and the terminator inserts a
:gateway placeholder so one mount serves every registered gateway. A pinned name
always wins over the placeholder, so a request cannot redirect a pinned mount.
Pass a session (a WireBox ID resolved per request, or a live GatewaySession) and
every inbound message is dispatched as an agent turn and acked 202 immediately,
without waiting on the turn: a platform webhook times out in seconds while an
agent turn does not. Without one, events are verified and parsed only.
Also fixes a related routing bug: a response closure that rendered the response
itself had its status code and content type flattened back onto the route's
static statusCode by the router's own renderData() call. Render data set during
the closure is now respected, which is what lets a gateway route answer 401 on a
bad signature and text/plain on a handshake. Render data an interceptor set
before the route ran is left alone.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015nRppywTeuVpXgVG24Wm9W
…ormat
Two CI failures on the previous commit:
- Adobe 2023/2025 could not compile Router.cfc: a member call on a
parenthesized expression, `( arguments.result.headers ?: {} ).each( ... )`,
is not valid CFML there. The struct goes into a local first. The router test
had the same class of problem in an arrow function whose body was an
assignment, so that is now a block body.
- cfformat rejected all three files. Formatted with cfformat 0.22.1 against the
repo's own .cfformat.json: the over-long handshake call is split, the info
payload's struct keys align, and the test drops method chains that the
formatter re-wraps.
Also narrows the RoutingService change: the render-data tracking now lives
inside the closure branch that uses it, so it no longer joins (and re-aligns)
an assignment group it has no business touching.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015nRppywTeuVpXgVG24Wm9W
6eb213e to
e0216e3
Compare
|
Rebased onto The same pre-existing failure recurred post-rebase — this time on both I dug into why it's flaky rather than just noting the coincidence again. The spec ( var modules = getController().getSetting( "modules" ) // the REAL app's live, shared struct
modules[ moduleName ] = { resources: [], routes: [] } // mutated in place
router.addModuleRoutes( ... ) // re-reads getSetting("modules") independently
// ...
router.removeModuleRoutes( moduleName )
structDelete( modules, moduleName ) // cleanup, same captured reference
Not fixing it here — it's a pre-existing isolation bug in an unrelated spec, and patching it would widen this PR's scope. Flagging the mechanism in case it's worth its own issue. Every other check remains green (BoxLang, BoxLang-CFML, Lucee 5/6, formatting), and I'll keep watching. Generated by Claude Code |
route().toAiGateway() (ColdBox/coldbox-platform#694) just merged to development but hasn't shipped in a release yet, so the test harness's pinned coldbox-8.1.0.zip predates it entirely. Point tests/box.json at ColdBox's "be" bleeding-edge build instead of the versioned release zip, matching how other modules in this ecosystem track unreleased core features (bx-ai's own devDependencies pin testbox the same way). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015nRppywTeuVpXgVG24Wm9W
Description
Exposes a BoxLang AI Gateway over HTTP natively from the routing DSL, alongside
toAi()andtoMCP(). Until now an application had to hand-write the routes plus a passthrough handler for the gateway surface: that is exactly what BX Agents generates into every project it builds, with a comment saying ColdBox has notoAiGateway()terminator.Registers, inheriting any modifiers already set (
withCondition,withDomain,withSSL,meta, module/namespace):{pattern}[/:gateway]/events{base}.gateway.events{pattern}/interactions/:requestID{base}.gateway.interaction{pattern}/interactions/:requestID/decisions{base}.gateway.decision{pattern}/info{base}.gateway.infoDesign notes
/eventsbecause a platform is given ONE URL to store and verifies it with a GET before it ever POSTs to it (Meta'shub.challengeecho being the canonical case). Two routes sharing a pattern would merge into one inaddRoute()anyway, losing the second closure, so the verb branch lives inside the closure.:gatewayplaceholder or a straygatewayvalue in the request collection, so a request can never redirect a pinned mount at another gateway.sessionis resolved per request, so registering a route never forces the session to be constructed. Passing it means every inbound message is dispatched as an agent turn and acked202immediately, without waiting on the turn: a platform webhook times out in seconds while an agent turn does not. Omitting it parses without dispatching.gateway/gatewayName/gatewaySessionmetadata, andRoutingServicelogs them the same way it logsaiandmcproutes.Related bug fix. A route
responseclosure that rendered the response itself (event.renderData(...)) had its status code and content type flattened back onto the route's staticstatusCodeby the router's ownrenderData()call at the end ofrenderResponse(). Render data set during the closure is now respected, which is what lets a gateway route answer401on a bad signature andtext/plainon a handshake. Render data an interceptor set before the route ran is deliberately left alone.Dependency. BoxLang only, and requires the
bxaimodule, same guardtoAi()/toMCP()use. The processing itself lives in bx-ai: this terminator calls the transport-agnostic statics from ortus-boxlang/bx-ai#286, now merged.Jira Issues
Not filed yet: the enabling bx-ai half is ortus-boxlang/bx-ai#286, and this is the framework side. Happy to open a COLDBOX issue and link it if you want one tracked before merge.
Type of change
Checklist
On that last box, precisely what was and wasn't verified locally. The TestBox suite itself could not run here (no engine or server available in this environment), so CI is the first real execution of
RouterGatewayTest.cfc. Everything else was checked against the real tools:.cfformat.json, and the three changed files are formatter-stable under it. One caveat worth flagging: that local copy also wants to change lines this PR never touched ({ GET : "edit" }→{ GET : "edit" }inresources(), and similar inline structs elsewhere) which CI's copy evidently accepts as-is, so its output was applied only inside this PR's own code and reverted everywhere else.( struct ?: {} ).each( ... )), while BoxLang, BoxLang-CFML and Lucee 5/6 all accepted it. Fixed in the second commit, along with the same class of problem in the test (an arrow function whose body was an assignment).bxaiguard stubbed, since the module is not installed here), before and after formatting. Every value the new spec asserts came back from that run: patterns (gateways/:gateway/events/,gateways/interactions/:requestID/,gateways/interactions/:requestID/decisions/,gateways/info/), verbs (GET,POST/GET/POST/GET), route names,patternParams,sslandconditioninheritance, thegateway/gatewayName/gatewaySessionmetadata, and bothInvalidArgumentExceptionguards.tests/specs/web/routing/RouterGatewayTest.cfcmirrorsRouterAITest.cfc(sameskip="notBoxlang"guard and mocked controller) and covers registration, verbs, naming, modifier inheritance, argument validation, and the private per-request resolvers.🤖 Generated with Claude Code
https://claude.ai/code/session_015nRppywTeuVpXgVG24Wm9W