Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .changeset/next-same-origin-proxy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
'@agent-devtools/next': patch
---

fix(next): proxy the agent server same-origin so the App Router widget isn't CORS-blocked

The agent server is loopback-only with no `Access-Control-*` surface (by design — same as the Vite path). The Next adapter previously injected the raw `http://127.0.0.1:<port>` agent URL as the widget's base URL, so the in-page widget fetched `/v1/agent/commands` / `/v1/agent/stream` cross-origin from the dev page (`localhost:<port>`) and the browser blocked it: `No 'Access-Control-Allow-Origin' header`. The widget mounted but could never talk to its backend.

`withAgentDevtools` now mirrors the Vite plugin's proxy: it injects a same-origin base path (`/__agent_devtools`) and installs a Next `rewrites()` rule that forwards `/__agent_devtools/:path*` to the agent server. Browser requests stay same-origin (no CORS), the agent server keeps its loopback-only, no-CORS posture, and existing user `rewrites` are composed (proxy rule in `beforeFiles`). When no `baseUrl` is supplied the wrapper adds no rewrite and stays a clean no-op.
37 changes: 37 additions & 0 deletions .changeset/pre.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
{
"mode": "pre",
"tag": "beta",
"initialVersions": {
"@agent-devtools/docs": "0.0.7",
"@agent-devtools/example-angular-vite": "0.0.0",
"@agent-devtools/example-html": "0.0.0",
"@agent-devtools/example-next": "0.0.0",
"@agent-devtools/example-next-pages": "0.0.0",
"@agent-devtools/example-nuxt": "0.0.0",
"@agent-devtools/example-nuxt2": "0.0.0",
"@agent-devtools/example-react-vite": "0.0.0",
"@agent-devtools/example-svelte-vite": "0.0.0",
"@agent-devtools/example-sveltekit": "0.0.0",
"@agent-devtools/example-vue-vite": "0.0.0",
"@agent-devtools/example-vue2-vite": "0.0.0",
"@agent-devtools/angular": "1.3.1",
"@agent-devtools/core": "1.3.1",
"@agent-devtools/e2e": "0.0.0",
"@agent-devtools/harness-core": "1.3.1",
"@agent-devtools/html": "1.3.1",
"@agent-devtools/next": "1.3.1",
"@agent-devtools/next-pages": "1.3.1",
"@agent-devtools/nuxt": "1.3.1",
"@agent-devtools/nuxt2": "1.3.1",
"@agent-devtools/react": "1.3.1",
"@agent-devtools/svelte": "1.3.1",
"@agent-devtools/sveltekit": "1.3.1",
"@agent-devtools/vite": "1.3.1",
"@agent-devtools/vue": "1.3.1",
"@agent-devtools/vue2": "1.3.1",
"@agent-devtools/widget-core": "1.3.1"
},
"changesets": [
"next-same-origin-proxy"
]
}
8 changes: 8 additions & 0 deletions packages/angular/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
# @agent-devtools/angular

## 1.3.2-beta.0

### Patch Changes

- Updated dependencies []:
- @agent-devtools/core@1.3.2-beta.0
- @agent-devtools/widget-core@1.3.2-beta.0

## 1.3.1

### Patch Changes
Expand Down
2 changes: 1 addition & 1 deletion packages/angular/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@agent-devtools/angular",
"version": "1.3.1",
"version": "1.3.2-beta.0",
"description": "Angular adapter for agent-devtools — Ivy component walker + DOM picker + closed Shadow DOM widget",
"keywords": [
"agent-devtools",
Expand Down
2 changes: 2 additions & 0 deletions packages/core/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# Changelog

## 1.3.2-beta.0

## 1.3.1

## 1.3.1-beta.0
Expand Down
2 changes: 1 addition & 1 deletion packages/core/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@agent-devtools/core",
"version": "1.3.1",
"version": "1.3.2-beta.0",
"description": "Framework-agnostic core for agent-devtools — server, agent engine, widget shell",
"keywords": [
"agent-devtools",
Expand Down
2 changes: 2 additions & 0 deletions packages/harness-core/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# Changelog

## 1.3.2-beta.0

## 1.3.1

## 1.3.1-beta.0
Expand Down
2 changes: 1 addition & 1 deletion packages/harness-core/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@agent-devtools/harness-core",
"version": "1.3.1",
"version": "1.3.2-beta.0",
"description": "Generic agent harness — domain-agnostic loop strategies + LLM provider abstraction. Source for both @agent-devtools and external SaaS consumers.",
"license": "MIT",
"type": "module",
Expand Down
8 changes: 8 additions & 0 deletions packages/html/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
# @agent-devtools/html

## 1.3.2-beta.0

### Patch Changes

- Updated dependencies []:
- @agent-devtools/vite@1.3.2-beta.0
- @agent-devtools/widget-core@1.3.2-beta.0

## 1.3.1

### Patch Changes
Expand Down
2 changes: 1 addition & 1 deletion packages/html/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@agent-devtools/html",
"version": "1.3.1",
"version": "1.3.2-beta.0",
"description": "npx runner for agent-devtools — serve a plain HTML folder with the dev-only widget injected, no framework, no config",
"keywords": [
"agent-devtools",
Expand Down
8 changes: 8 additions & 0 deletions packages/next-pages/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
# @agent-devtools/next-pages

## 1.3.2-beta.0

### Patch Changes

- Updated dependencies []:
- @agent-devtools/core@1.3.2-beta.0
- @agent-devtools/react@1.3.2-beta.0

## 1.3.1

### Patch Changes
Expand Down
2 changes: 1 addition & 1 deletion packages/next-pages/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@agent-devtools/next-pages",
"version": "1.3.1",
"version": "1.3.2-beta.0",
"description": "Next.js Pages Router adapter for agent-devtools — wraps the React widget for legacy pages/_app.tsx hosts",
"keywords": [
"agent-devtools",
Expand Down
14 changes: 14 additions & 0 deletions packages/next/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,19 @@
# @agent-devtools/next

## 1.3.2-beta.0

### Patch Changes

- [#17](https://github.com/Seungwoo321/agent-devtools/pull/17) [`6c4101e`](https://github.com/Seungwoo321/agent-devtools/commit/6c4101e1eaca6a3bbcda4b0ef31428fda7fcac0a) Thanks [@Seungwoo321](https://github.com/Seungwoo321)! - fix(next): proxy the agent server same-origin so the App Router widget isn't CORS-blocked

The agent server is loopback-only with no `Access-Control-*` surface (by design — same as the Vite path). The Next adapter previously injected the raw `http://127.0.0.1:<port>` agent URL as the widget's base URL, so the in-page widget fetched `/v1/agent/commands` / `/v1/agent/stream` cross-origin from the dev page (`localhost:<port>`) and the browser blocked it: `No 'Access-Control-Allow-Origin' header`. The widget mounted but could never talk to its backend.

`withAgentDevtools` now mirrors the Vite plugin's proxy: it injects a same-origin base path (`/__agent_devtools`) and installs a Next `rewrites()` rule that forwards `/__agent_devtools/:path*` to the agent server. Browser requests stay same-origin (no CORS), the agent server keeps its loopback-only, no-CORS posture, and existing user `rewrites` are composed (proxy rule in `beforeFiles`). When no `baseUrl` is supplied the wrapper adds no rewrite and stays a clean no-op.

- Updated dependencies []:
- @agent-devtools/core@1.3.2-beta.0
- @agent-devtools/react@1.3.2-beta.0

## 1.3.1

### Patch Changes
Expand Down
2 changes: 1 addition & 1 deletion packages/next/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@agent-devtools/next",
"version": "1.3.1",
"version": "1.3.2-beta.0",
"description": "Next.js 15 adapter for agent-devtools — re-exports the React widget + adds App / Pages router dev-only injection",
"keywords": [
"agent-devtools",
Expand Down
42 changes: 40 additions & 2 deletions packages/next/src/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,19 +54,57 @@ describe('withAgentDevtools', () => {
expect(result.reactStrictMode).toBe(true);
});

it('propagates baseUrl and pairingToken into env', () => {
it('injects the same-origin proxy path as base URL (not the raw agent URL) and the pairing token', () => {
vi.stubEnv('NODE_ENV', 'development');
const result = withAgentDevtools(
{},
{ baseUrl: 'http://127.0.0.1:4317', pairingToken: 'tok-abc' },
) as Record<string, unknown>;
expect(result.env).toMatchObject({
AGENT_DEVTOOLS_NEXT_ENABLED: 'true',
AGENT_DEVTOOLS_NEXT_BASE_URL: 'http://127.0.0.1:4317',
// The browser must reach the agent server same-origin via the rewrite —
// never the raw cross-origin loopback URL (no CORS surface there).
AGENT_DEVTOOLS_NEXT_BASE_URL: '/__agent_devtools',
AGENT_DEVTOOLS_NEXT_PAIRING_TOKEN: 'tok-abc',
});
});

it('adds a same-origin proxy rewrite to the agent server when baseUrl is set', async () => {
vi.stubEnv('NODE_ENV', 'development');
const result = withAgentDevtools(
{},
{ baseUrl: 'http://127.0.0.1:4317/', pairingToken: 'tok-abc' },
) as { rewrites?: () => Promise<{ beforeFiles: { source: string; destination: string }[] }> };
expect(typeof result.rewrites).toBe('function');
const set = await result.rewrites!();
expect(set.beforeFiles).toContainEqual({
source: '/__agent_devtools/:path*',
destination: 'http://127.0.0.1:4317/:path*',
});
});

it('composes the proxy rewrite ahead of pre-existing rewrites', async () => {
vi.stubEnv('NODE_ENV', 'development');
const userRule = { source: '/old/:path*', destination: '/new/:path*' };
const result = withAgentDevtools(
{ rewrites: () => Promise.resolve([userRule]) },
{ baseUrl: 'http://127.0.0.1:4317', pairingToken: 'tok' },
) as unknown as { rewrites: () => Promise<{ beforeFiles: unknown[]; afterFiles: unknown[] }> };
const set = await result.rewrites();
expect(set.beforeFiles).toContainEqual({
source: '/__agent_devtools/:path*',
destination: 'http://127.0.0.1:4317/:path*',
});
expect(set.afterFiles).toContainEqual(userRule);
});

it('does not add a rewrite when no baseUrl is provided', () => {
vi.stubEnv('NODE_ENV', 'development');
const result = withAgentDevtools({}, { pairingToken: 'tok' }) as Record<string, unknown>;
expect(result.rewrites).toBeUndefined();
expect((result.env as Record<string, string>).AGENT_DEVTOOLS_NEXT_BASE_URL).toBeUndefined();
});

it('preserves pre-existing env entries when merging', () => {
vi.stubEnv('NODE_ENV', 'development');
const result = withAgentDevtools({ env: { EXISTING_KEY: 'keep-me' } }) as Record<
Expand Down
65 changes: 61 additions & 4 deletions packages/next/src/config.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,19 @@
/**
* `next.config.{js,mjs,ts}` wrapper. Two responsibilities:
* `next.config.{js,mjs,ts}` wrapper. Three responsibilities:
*
* 1. (Dev) Inject AGENT_DEVTOOLS_NEXT_* env entries so the client-side
* bootstrap module knows the base URL and pairing token without the
* caller having to import them through a separate file.
*
* 2. (Prod) Install a webpack alias that maps the widget chain to
* 2. (Dev) Install a same-origin proxy rewrite. The in-page widget fetches
* `${PROXY_PATH}/v1/agent/*` on the Next dev origin and Next forwards to
* the loopback agent server. The agent server exposes no `Access-Control-*`
* surface (it stays loopback-only), so a direct cross-origin fetch from
* the page to `127.0.0.1:<port>` would be CORS-blocked. The injected base
* URL is therefore the proxy path, not the raw agent URL — mirroring the
* Vite plugin's proxy middleware.
*
* 3. (Prod) Install a webpack alias that maps the widget chain to
* `false` (empty module). This is Layer 1 of the dev-only guard for
* the Next adapter: a user-side `'use client'` component that does
* `import { bootstrapAgentDevtools } from '@agent-devtools/next/bootstrap'`
Expand Down Expand Up @@ -68,6 +76,20 @@ const ENABLED_ENV = 'AGENT_DEVTOOLS_NEXT_ENABLED';
const BASE_URL_ENV = 'AGENT_DEVTOOLS_NEXT_BASE_URL';
const PAIRING_TOKEN_ENV = 'AGENT_DEVTOOLS_NEXT_PAIRING_TOKEN';

// Same-origin mount the widget fetches through (mirrors the Vite plugin's
// PROXY_PATH). Next rewrites forward `${PROXY_PATH}/v1/agent/*` to the loopback
// agent server, so browser requests never cross origins and the agent server
// keeps its no-CORS, loopback-only posture.
const PROXY_PATH = '/__agent_devtools';

type RewriteRule = { source: string; destination: string };
type RewriteSet = {
beforeFiles: RewriteRule[];
afterFiles: RewriteRule[];
fallback: RewriteRule[];
};
type RewriteResult = RewriteRule[] | Partial<RewriteSet>;

// Aliased to `false` (empty module) in production webpack builds. We
// deliberately KEEP `@agent-devtools/next/bootstrap` in the graph because
// it is the tiny shim whose body early-returns on
Expand Down Expand Up @@ -108,10 +130,45 @@ export function withAgentDevtools<TConfig extends NextConfigLike>(

const env = collectEnv(nextConfig);
env[ENABLED_ENV] = 'true';
if (options.baseUrl) env[BASE_URL_ENV] = options.baseUrl;
if (options.pairingToken) env[PAIRING_TOKEN_ENV] = options.pairingToken;

return { ...nextConfig, env, webpack };
// No agent server URL → nothing to proxy or wire. Leave the enabled flag
// only; the bootstrap no-ops when it finds no base URL.
if (!options.baseUrl) {
return { ...nextConfig, env, webpack };
}

// Inject the SAME-ORIGIN proxy path as the base URL, not the raw agent URL.
// The agent server has no `Access-Control-*` surface, so the in-page widget
// must reach it same-origin via the rewrite below — a direct cross-origin
// fetch to `127.0.0.1:<port>` would be CORS-blocked.
env[BASE_URL_ENV] = PROXY_PATH;

const agentBaseUrl = options.baseUrl.replace(/\/+$/, '');
const proxyRule: RewriteRule = {
source: `${PROXY_PATH}/:path*`,
destination: `${agentBaseUrl}/:path*`,
};
const previousRewrites = nextConfig.rewrites;
const rewrites = async (): Promise<RewriteSet> => {
const prev =
typeof previousRewrites === 'function'
? await (previousRewrites as () => RewriteResult | Promise<RewriteResult>)()
: undefined;
if (Array.isArray(prev)) {
return { beforeFiles: [proxyRule], afterFiles: prev, fallback: [] };
}
if (prev && typeof prev === 'object') {
return {
beforeFiles: [proxyRule, ...(prev.beforeFiles ?? [])],
afterFiles: prev.afterFiles ?? [],
fallback: prev.fallback ?? [],
};
}
return { beforeFiles: [proxyRule], afterFiles: [], fallback: [] };
};

return { ...nextConfig, env, webpack, rewrites };
}

function collectEnv(config: NextConfigLike): Record<string, string> {
Expand Down
9 changes: 9 additions & 0 deletions packages/nuxt/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
# @agent-devtools/nuxt

## 1.3.2-beta.0

### Patch Changes

- Updated dependencies []:
- @agent-devtools/core@1.3.2-beta.0
- @agent-devtools/vue@1.3.2-beta.0
- @agent-devtools/widget-core@1.3.2-beta.0

## 1.3.1

### Patch Changes
Expand Down
2 changes: 1 addition & 1 deletion packages/nuxt/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@agent-devtools/nuxt",
"version": "1.3.1",
"version": "1.3.2-beta.0",
"description": "Nuxt 3 module for agent-devtools — re-exports the Vue widget + adds dev-only injection through defineNuxtModule",
"keywords": [
"agent-devtools",
Expand Down
9 changes: 9 additions & 0 deletions packages/nuxt2/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
# @agent-devtools/nuxt2

## 1.3.2-beta.0

### Patch Changes

- Updated dependencies []:
- @agent-devtools/core@1.3.2-beta.0
- @agent-devtools/vue2@1.3.2-beta.0
- @agent-devtools/widget-core@1.3.2-beta.0

## 1.3.1

### Patch Changes
Expand Down
2 changes: 1 addition & 1 deletion packages/nuxt2/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@agent-devtools/nuxt2",
"version": "1.3.1",
"version": "1.3.2-beta.0",
"description": "Nuxt 2 module for agent-devtools — re-exports the Vue 2 widget + adds dev-only injection through the Nuxt 2 module API",
"keywords": [
"agent-devtools",
Expand Down
8 changes: 8 additions & 0 deletions packages/react/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
# Changelog

## 1.3.2-beta.0

### Patch Changes

- Updated dependencies []:
- @agent-devtools/core@1.3.2-beta.0
- @agent-devtools/widget-core@1.3.2-beta.0

## 1.3.1

### Patch Changes
Expand Down
2 changes: 1 addition & 1 deletion packages/react/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@agent-devtools/react",
"version": "1.3.1",
"version": "1.3.2-beta.0",
"description": "React 19 adapter for agent-devtools — fiber walker + DOM picker + closed Shadow DOM widget",
"keywords": [
"agent-devtools",
Expand Down
Loading