Skip to content

Commit 3bf9de7

Browse files
bloveclaude
andcommitted
fix(ag-ui): resolve the subagents agent URL against <base href>
cockpit/ag-ui/subagents was the only ag-ui app configuring a root-absolute `provideAgent({ url: '/agent' })`; the other six build the URL relative to document.baseURI. scripts/assemble-examples.ts rewrites each app's <base href> to /ag-ui/<topic>/, and the Vercel route table only proxies ^/ag-ui/([^/]+)/agent(/.*)?$ to the Railway runtime. The literal therefore requested https://examples.threadplane.ai/agent, which matches no route, fell through the filesystem handle and landed on the 404 catch-all. Local dev hid it: dev serves <base href="/">, so both forms resolve to /agent and proxy.conf.mjs forwards either one. Add a static guard covering every registered ag-ui capability. Production smoke cannot see this class of bug — the page still serves a healthy 200 index.html — so the assertion has to be made against the source. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent 73f7f71 commit 3bf9de7

2 files changed

Lines changed: 66 additions & 1 deletion

File tree

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import { readFileSync } from 'node:fs';
2+
import { dirname, join, resolve } from 'node:path';
3+
import { fileURLToPath } from 'node:url';
4+
import { capabilities } from './scripts/capability-registry';
5+
6+
/**
7+
* Guard for a failure mode production smoke cannot see.
8+
*
9+
* `scripts/assemble-examples.ts` rewrites each cockpit app's `<base href>` to
10+
* `/<product>/<topic>/`, and the Vercel route table only proxies
11+
* `^/ag-ui/([^/]+)/agent(/.*)?$` to the Railway runtime. An app that hardcodes
12+
* a root-absolute `'/agent'` therefore requests `https://<host>/agent`, which
13+
* matches no route, falls through the filesystem handle and lands on the 404
14+
* catch-all — while still serving a perfectly healthy 200 index.html, so every
15+
* page-reachability assertion keeps passing.
16+
*
17+
* Local dev is not a safety net either: dev serves with `<base href="/">`, so
18+
* the literal and the base-relative form resolve identically to `/agent` and
19+
* proxy.conf.mjs forwards both.
20+
*
21+
* The base-relative form is the only one that survives the base-href rewrite.
22+
*/
23+
const AGENT_URL_EXPR = "new URL('agent', document.baseURI).pathname";
24+
25+
const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), '../..');
26+
27+
/**
28+
* Both products are served by the same aggregated AG-UI runtime and both get
29+
* their <base href> rewritten at assemble time, so both are exposed to this.
30+
*/
31+
const agentBackedCapabilities = capabilities.filter(
32+
(c) => c.product === 'ag-ui' || c.product === 'runtimes'
33+
);
34+
35+
describe('ag-ui agent URL is resolved against <base href>', () => {
36+
it('covers every registered agent-backed capability', () => {
37+
expect(agentBackedCapabilities.length).toBeGreaterThan(0);
38+
});
39+
40+
for (const cap of agentBackedCapabilities) {
41+
it(`${cap.product}/${cap.topic} resolves its agent URL relative to the base href`, () => {
42+
const configPath = join(
43+
repoRoot,
44+
'cockpit',
45+
cap.product,
46+
cap.topic,
47+
'angular/src/app/app.config.ts'
48+
);
49+
const source = readFileSync(configPath, 'utf8');
50+
51+
expect(
52+
source,
53+
`${configPath} must build the agent URL with ${AGENT_URL_EXPR} so it ` +
54+
`resolves under the deployed <base href="/${cap.product}/${cap.topic}/">`
55+
).toContain(AGENT_URL_EXPR);
56+
57+
// A root-absolute literal silently 404s in production; see the note above.
58+
expect(
59+
source,
60+
`${configPath} hardcodes a root-absolute agent URL, which does not ` +
61+
`survive the <base href> rewrite in scripts/assemble-examples.ts`
62+
).not.toMatch(/url:\s*['"`]\/agent/);
63+
});
64+
}
65+
});

cockpit/ag-ui/subagents/angular/src/app/app.config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { provideChat } from '@threadplane/chat';
55

66
export const appConfig: ApplicationConfig = {
77
providers: [
8-
provideAgent({ url: '/agent' }),
8+
provideAgent({ url: new URL('agent', document.baseURI).pathname }),
99
provideChat({}),
1010
],
1111
};

0 commit comments

Comments
 (0)