Skip to content

Commit 99aad28

Browse files
committed
mothership: the embedded CLI answers v2 in-process; grep memoizes both platform catalogs
The embedded CLI and the agent-cli engines were typed v2 clients pointed at the server's own URL: every tool call was a network round trip through the proxy, API-key auth, the abuse rate limits, and the proxy body ceiling. A grep over one block definition cost 8-34s and tripped the per-key limit (dev 2026-09-03). - sim-cli: ResolvedProfile / EmbeddedCliIdentity take an optional transport; the client sends through it instead of fetch. The installed CLI never sets it. - sim: an in-process transport resolves a v2 path against a generated route table (scripts/generate-v2-route-table.ts, check:v2-route-table in check:audits) and invokes the route handler directly. The request is marked internal through a WeakSet — not a header — so admission still authenticates it but skips the pre-auth IP bucket and the per-key rate limits, which exist for callers on the wire. Contracts, use cases, presenters, and error envelopes are the ones the network path runs. Anything outside the v2 table falls through to fetch. - grep engine: blocks and tools (5,000 built-ins at 100 a page) are memoized per workspace; an exact block or tool id (--in file_v5) resolves from that corpus without listing any workspace world; name fragments still index every world and fetch only the matches.
1 parent 5617a3b commit 99aad28

16 files changed

Lines changed: 985 additions & 21 deletions
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
/**
2+
* @vitest-environment node
3+
*/
4+
import { NextResponse } from 'next/server'
5+
import { afterEach, describe, expect, it, vi } from 'vitest'
6+
7+
const { handlers } = vi.hoisted(() => ({
8+
handlers: {
9+
listBlocks: vi.fn(),
10+
getBlock: vi.fn(),
11+
latestVersion: vi.fn(),
12+
},
13+
}))
14+
15+
vi.mock('@/lib/api/server/routes/v2-route-table.generated', () => ({
16+
V2_ROUTES: [
17+
{ pattern: '/api/v2/blocks', load: async () => ({ GET: handlers.listBlocks }) },
18+
{ pattern: '/api/v2/blocks/{blockId}', load: async () => ({ GET: handlers.getBlock }) },
19+
{
20+
pattern: '/api/v2/blocks/latest',
21+
load: async () => ({ GET: handlers.latestVersion }),
22+
},
23+
],
24+
}))
25+
26+
import {
27+
createInProcessTransport,
28+
matchV2Route,
29+
} from '@/lib/api/server/routes/in-process-transport'
30+
import { isInternalRequest } from '@/lib/api/server/routes/internal-request'
31+
32+
describe('in-process transport', () => {
33+
afterEach(() => {
34+
vi.restoreAllMocks()
35+
vi.clearAllMocks()
36+
})
37+
38+
it('prefers the more literal pattern and decodes dynamic segments', async () => {
39+
expect(matchV2Route('/api/v2/blocks/latest')?.params).toEqual({})
40+
const dynamic = matchV2Route('/api/v2/blocks/slack%20v2')
41+
expect(dynamic?.params).toEqual({ blockId: 'slack v2' })
42+
expect(matchV2Route('/api/v2/nowhere')).toBeNull()
43+
})
44+
45+
it('dispatches a v2 request to its route handler in-process, marked internal', async () => {
46+
handlers.getBlock.mockImplementation(
47+
async (request: Request, context: { params: Promise<Record<string, string>> }) =>
48+
NextResponse.json({
49+
internal: isInternalRequest(request),
50+
key: request.headers.get('x-api-key'),
51+
query: new URL(request.url).searchParams.get('workspaceId'),
52+
params: await context.params,
53+
})
54+
)
55+
const transport = createInProcessTransport()
56+
57+
const response = await transport('http://internal/api/v2/blocks/agent?workspaceId=ws-1', {
58+
method: 'GET',
59+
headers: { 'x-api-key': 'secret' },
60+
})
61+
62+
expect(response.status).toBe(200)
63+
expect(await response.json()).toEqual({
64+
internal: true,
65+
key: 'secret',
66+
query: 'ws-1',
67+
params: { blockId: 'agent' },
68+
})
69+
expect(handlers.listBlocks).not.toHaveBeenCalled()
70+
})
71+
72+
it('falls through to fetch for anything outside the v2 table', async () => {
73+
const network = vi.spyOn(globalThis, 'fetch').mockResolvedValue(new Response('elsewhere'))
74+
const transport = createInProcessTransport()
75+
76+
const response = await transport('http://internal/api/files/serve/abc', { method: 'GET' })
77+
78+
expect(await response.text()).toBe('elsewhere')
79+
expect(network).toHaveBeenCalledTimes(1)
80+
expect(handlers.getBlock).not.toHaveBeenCalled()
81+
})
82+
})
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import { NextRequest } from 'next/server'
2+
import { markInternalRequest } from '@/lib/api/server/routes/internal-request'
3+
import { V2_ROUTES } from '@/lib/api/server/routes/v2-route-table.generated'
4+
5+
/**
6+
* A `fetch` that answers the server's own v2 requests in-process.
7+
*
8+
* The embedded CLI and the agent-cli engines are typed v2 clients. Pointing them at
9+
* the server's URL made every tool call a network round trip through the proxy,
10+
* API-key authentication, the abuse rate limits, and the proxy body ceiling — a
11+
* grep over one block definition cost seconds and tripped the per-key limit. This
12+
* transport resolves the request's path against the generated route table and
13+
* invokes the route handler directly, with the request marked internal so
14+
* admission authenticates it but does not rate-limit it. Contracts, use cases,
15+
* presenters, and error envelopes are untouched: the handler that runs is the one
16+
* the network path would run.
17+
*
18+
* Anything outside the v2 table falls through to real `fetch`.
19+
*/
20+
21+
type RouteHandler = (
22+
request: NextRequest,
23+
context: { params: Promise<Record<string, string>> }
24+
) => Promise<Response>
25+
26+
interface CompiledRoute {
27+
regex: RegExp
28+
params: string[]
29+
/** Literal segments — a more specific pattern wins over a parameterized one. */
30+
literals: number
31+
load: () => Promise<object>
32+
}
33+
34+
interface MatchedRoute {
35+
params: Record<string, string>
36+
literals: number
37+
load: () => Promise<object>
38+
}
39+
40+
const COMPILED: CompiledRoute[] = V2_ROUTES.map((route) => {
41+
const params: string[] = []
42+
let literals = 0
43+
const source = route.pattern
44+
.split('/')
45+
.map((segment) => {
46+
const param = /^\{(.+)\}$/.exec(segment)
47+
if (!param?.[1]) {
48+
literals += 1
49+
return segment.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
50+
}
51+
params.push(param[1])
52+
return '([^/]+)'
53+
})
54+
.join('/')
55+
return { regex: new RegExp(`^${source}$`), params, literals, load: route.load }
56+
})
57+
58+
export function matchV2Route(pathname: string): MatchedRoute | null {
59+
let best: MatchedRoute | null = null
60+
for (const route of COMPILED) {
61+
const match = route.regex.exec(pathname)
62+
if (!match) continue
63+
if (best && best.literals >= route.literals) continue
64+
const params: Record<string, string> = {}
65+
route.params.forEach((name, index) => {
66+
params[name] = decodeURIComponent(match[index + 1] ?? '')
67+
})
68+
best = { params, literals: route.literals, load: route.load }
69+
}
70+
return best
71+
}
72+
73+
function requestUrl(input: RequestInfo | URL): URL {
74+
if (typeof input === 'string') return new URL(input)
75+
if (input instanceof URL) return input
76+
return new URL(input.url)
77+
}
78+
79+
export function createInProcessTransport(): typeof fetch {
80+
return async (input, init) => {
81+
const url = requestUrl(input)
82+
const method = (init?.method ?? 'GET').toUpperCase()
83+
const matched = url.pathname.startsWith('/api/v2/') ? matchV2Route(url.pathname) : null
84+
if (!matched) return fetch(input, init)
85+
const handler = Reflect.get(await matched.load(), method)
86+
if (typeof handler !== 'function') return fetch(input, init)
87+
const request = new NextRequest(url, {
88+
method,
89+
headers: init?.headers,
90+
body: init?.body ?? null,
91+
signal: init?.signal ?? undefined,
92+
})
93+
markInternalRequest(request)
94+
return (handler as RouteHandler)(request, { params: Promise.resolve(matched.params) })
95+
}
96+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/**
2+
* Requests the server makes to itself.
3+
*
4+
* The embedded CLI and the agent-cli engines dispatch to the v2 route handlers
5+
* in-process (see `in-process-transport.ts`). Those requests are marked here so
6+
* admission can skip the abuse controls that exist for callers arriving over the
7+
* network: the pre-auth IP bucket and the per-key rate limits. Authentication is
8+
* not skipped — an internal request still carries the caller's key and resolves
9+
* to the same principal the network path would.
10+
*
11+
* A WeakSet keyed by the Request object, not a header: any client on the wire
12+
* can set a header; nothing outside this process can reach the set.
13+
*/
14+
const INTERNAL_REQUESTS = new WeakSet<Request>()
15+
16+
export function markInternalRequest(request: Request): void {
17+
INTERNAL_REQUESTS.add(request)
18+
}
19+
20+
export function isInternalRequest(request: Request): boolean {
21+
return INTERNAL_REQUESTS.has(request)
22+
}

apps/sim/lib/api/server/routes/v2-json-route.test.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ class TestLockedError extends HttpError {
2828
vi.mock('@/lib/api/server/routes/v2-api-key-auth', () => v2ApiKeyAuthModuleMock)
2929
vi.mock('@/lib/core/rate-limiter', () => v2RateLimiterModuleMock)
3030

31+
import { markInternalRequest } from '@/lib/api/server/routes/internal-request'
3132
import type { V2ApiKeyAuthContext } from '@/lib/api/server/routes/v2-api-key-auth'
3233
import {
3334
defineV2JsonRoute,
@@ -204,6 +205,21 @@ describe('defineV2JsonRoute', () => {
204205
expect(response.headers.get('Cache-Control')).toBe('private, no-store')
205206
})
206207

208+
it('authenticates but never rate-limits a request the server marked as its own', async () => {
209+
// The embedded CLI and the agent-cli engines dispatch to these handlers in-process;
210+
// the IP bucket and the per-key limits exist for callers on the wire, and a chat
211+
// turn's tool calls all land on one key (dev 2026-09-03: grep hit the limit).
212+
const internal = request()
213+
markInternalRequest(internal)
214+
215+
const response = await createHandler()(internal)
216+
217+
expect(response.status).toBe(201)
218+
expect(v2RouteMocks.authenticate).toHaveBeenCalledTimes(1)
219+
expect(v2RouteMocks.preauthRate).not.toHaveBeenCalled()
220+
expect(v2RouteMocks.operationRate).not.toHaveBeenCalled()
221+
})
222+
207223
it('fails closed before authentication when the IP bucket cannot admit the request', async () => {
208224
v2RouteMocks.preauthRate.mockResolvedValueOnce({
209225
allowed: false,

apps/sim/lib/api/server/routes/v2-json-route.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
methodMatchesContract,
66
requireJsonRouteDefinition,
77
} from '@/lib/api/server/routes/definition'
8+
import { isInternalRequest } from '@/lib/api/server/routes/internal-request'
89
import type {
910
JsonApiRouteContract,
1011
JsonNextRouteHandler,
@@ -318,7 +319,12 @@ async function admitAuthenticatedV2Request(
318319
throw new V2RouteInfrastructureError('authentication', error)
319320
}
320321

321-
const limited = await rateLimitPolicy.enforce(request, auth, operation)
322+
// The server's own requests (embedded CLI, agent-cli engines) are authenticated like
323+
// any other but never rate limited: the buckets exist for callers on the wire, and a
324+
// chat turn's tool calls all land on one key. See `internal-request.ts`.
325+
const limited = isInternalRequest(request)
326+
? null
327+
: await rateLimitPolicy.enforce(request, auth, operation)
322328
return limited ? { success: false, response: limited } : { success: true, auth }
323329
}
324330

@@ -330,7 +336,7 @@ async function admitRateLimitedV2Request(
330336
): Promise<
331337
{ success: true; auth: V2ApiKeyAuthContext } | { success: false; response: NextResponse }
332338
> {
333-
const preAuthResponse = await enforceV2PreAuthIpLimit(request)
339+
const preAuthResponse = isInternalRequest(request) ? null : await enforceV2PreAuthIpLimit(request)
334340
if (preAuthResponse) return { success: false, response: preAuthResponse }
335341
return admitAuthenticatedV2Request(request, operation, authPolicy, rateLimitPolicy)
336342
}
@@ -355,7 +361,7 @@ export async function admitOptionalV2Request(
355361
): Promise<
356362
{ success: true; auth?: V2ApiKeyAuthContext } | { success: false; response: NextResponse }
357363
> {
358-
const preAuthResponse = await enforceV2PreAuthIpLimit(request)
364+
const preAuthResponse = isInternalRequest(request) ? null : await enforceV2PreAuthIpLimit(request)
359365
if (preAuthResponse) return { success: false, response: preAuthResponse }
360366
if (!request.headers.has('x-api-key')) return { success: true }
361367
return admitAuthenticatedV2Request(request, operation, authPolicy, rateLimitPolicy)

0 commit comments

Comments
 (0)