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
6 changes: 3 additions & 3 deletions agentex-ui/DOCKER.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,13 +91,13 @@ The application runs with the following default environment variables:
- `PORT=3000`
- `HOSTNAME=0.0.0.0`

To configure public runtime variables, pass them when running the container:
To configure runtime variables, pass them when running the container:

```bash
# Explicit env flags
docker run --rm -p 3000:3000 \
-e NEXT_PUBLIC_AGENTEX_API_BASE_URL=http://localhost:5003 \
-e NEXT_PUBLIC_SGP_APP_URL=https://egp.dashboard.scale.com \
-e AGENTEX_API_URL=http://localhost:5003 \
-e NEXT_PUBLIC_SGP_APP_URL=https://app.example.com \
agentex-ui:latest

# Or via an env file
Expand Down
4 changes: 2 additions & 2 deletions agentex-ui/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,8 +96,8 @@ cp example.env.development .env.development
Edit `.env.development` with your configuration:

```bash
# Backend API endpoint
NEXT_PUBLIC_AGENTEX_API_BASE_URL=http://localhost:5003
# Backend API endpoint — server-only upstream for the /api/agentex BFF proxy
AGENTEX_API_URL=http://localhost:5003
```

### 3. Install Dependencies
Expand Down
42 changes: 42 additions & 0 deletions agentex-ui/app/api/_lib/bff.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/** Server-only platform API base for the BFF routes. Prefers SGP_API_URL, else the app URL. */
export const SGP_BASE_URL =
process.env.SGP_API_URL ??
(process.env.NEXT_PUBLIC_SGP_APP_URL
? `${process.env.NEXT_PUBLIC_SGP_APP_URL}/api`
: undefined);

/** Return the Cookie header with the named cookie removed (null if nothing remains). */
function stripCookie(header: string | null, name: string): string | null {
if (!header) return null;
const kept = header
.split(';')
.map(c => c.trim())
.filter(c => {
const eq = c.indexOf('=');
return (eq === -1 ? c : c.slice(0, eq)) !== name;
});
return kept.length ? kept.join('; ') : null;
}

/**
* Attach credentials to an upstream request's `headers` in place, so none reach client JS:
* forward `x-selected-account-id` (the upstream authorizes the account), drop any
* client-sent `authorization`, and forward cookies for the upstream's own auth — minus the
* account-scoped `_jwt` (access-profile) cookie. Cookie-auth backends prioritize `_jwt` over
* `x-selected-account-id`, so leaving it would pin the account to the one the user linked in
* with and ignore the selected account; identity still comes from `_identityJwt`.
*/
export async function applyBffCredentials(
req: Request,
headers: Headers
): Promise<void> {
const accountId = req.headers.get('x-selected-account-id');
if (accountId) headers.set('x-selected-account-id', accountId);
else headers.delete('x-selected-account-id');

headers.delete('authorization');

const cookie = stripCookie(req.headers.get('cookie'), '_jwt');
if (cookie) headers.set('cookie', cookie);
else headers.delete('cookie');
}
67 changes: 67 additions & 0 deletions agentex-ui/app/api/agentex/[...path]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { applyBffCredentials } from '@/app/api/_lib/bff';

/**
* Same-origin BFF proxy for the Agentex API, so the upstream URL and credentials never
* reach client JS. applyBffCredentials attaches credentials server-side.
*/
export const dynamic = 'force-dynamic';

const UPSTREAM = (
process.env.AGENTEX_API_URL ?? 'http://localhost:5003'
).replace(/\/$/, '');

// Hop-by-hop headers to drop. Credential headers (cookie/authorization) are handled by
// applyBffCredentials, not here.
const STRIP_REQ = ['host', 'connection', 'content-length'];
const STRIP_RES = [
'content-encoding',
'content-length',
'transfer-encoding',
'connection',
];

async function proxy(
req: Request,
ctx: { params: Promise<{ path?: string[] }> }
): Promise<Response> {
const { path = [] } = await ctx.params;
const search = new URL(req.url).search;
const target = `${UPSTREAM}/${path.join('/')}${search}`;

const headers = new Headers(req.headers);
for (const h of STRIP_REQ) headers.delete(h);
await applyBffCredentials(req, headers);

const method = req.method.toUpperCase();
const hasBody = method !== 'GET' && method !== 'HEAD';
const upstream = await fetch(target, {
method,
headers,
body: hasBody ? req.body : undefined,
redirect: 'manual',
// @ts-expect-error `duplex` is required to stream a request body (undici)
duplex: 'half',
});

// Pass the upstream body through unbuffered so SSE / streaming responses work.
const resHeaders = new Headers(upstream.headers);
for (const h of STRIP_RES) resHeaders.delete(h);
// Don't leak an upstream (internal) redirect target to the browser.
if (upstream.status >= 300 && upstream.status < 400) {
resHeaders.delete('location');
}
return new Response(upstream.body, {
status: upstream.status,
headers: resHeaders,
});
Comment thread
greptile-apps[bot] marked this conversation as resolved.
}

export {
proxy as DELETE,
proxy as GET,
proxy as HEAD,
proxy as OPTIONS,
proxy as PATCH,
proxy as POST,
proxy as PUT,
};
34 changes: 7 additions & 27 deletions agentex-ui/app/api/feedback/route.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { NextResponse } from 'next/server';

import { applyBffCredentials, SGP_BASE_URL } from '@/app/api/_lib/bff';

type FeedbackRequestBody = {
traceId: string;
messageId: string;
Expand All @@ -13,33 +15,10 @@ type FeedbackRequestBody = {
agentAcpType?: string;
};

const SGP_BASE_URL =
process.env.NEXT_PUBLIC_SGP_API_URL ??
(process.env.NEXT_PUBLIC_SGP_APP_URL
? `${process.env.NEXT_PUBLIC_SGP_APP_URL}/api`
: undefined);

function getSGPHeaders(request: Request): Record<string, string> {
const headers: Record<string, string> = {
'Content-Type': 'application/json',
};
const forwarded = [
'cookie',
'authorization',
'x-api-key',
'x-selected-account-id',
];
for (const key of forwarded) {
const value = request.headers.get(key);
if (value) headers[key] = value;
}
return headers;
}

async function sgpPost<T>(
path: string,
body: unknown,
headers: Record<string, string>
headers: Headers
): Promise<T> {
const res = await fetch(`${SGP_BASE_URL}${path}`, {
method: 'POST',
Expand All @@ -57,8 +36,7 @@ export async function POST(request: Request) {
if (!SGP_BASE_URL) {
return NextResponse.json(
{
error:
'SGP feedback is not configured. Set NEXT_PUBLIC_SGP_API_URL or NEXT_PUBLIC_SGP_APP_URL.',
error: 'SGP feedback is not configured. Set SGP_API_URL.',
},
{ status: 503 }
);
Expand Down Expand Up @@ -100,7 +78,9 @@ export async function POST(request: Request) {
);
}

const sgpHeaders = getSGPHeaders(request);
// Credentials attached server-side (see applyBffCredentials).
const sgpHeaders = new Headers({ 'Content-Type': 'application/json' });
await applyBffCredentials(request, sgpHeaders);
const now = new Date().toISOString();

try {
Expand Down
27 changes: 27 additions & 0 deletions agentex-ui/app/api/user-info/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { NextResponse } from 'next/server';

import { applyBffCredentials, SGP_BASE_URL } from '@/app/api/_lib/bff';

/**
* Scoped BFF proxy for the caller's accounts (access_profiles), used to bootstrap/switch
* the selected account. Only this path is exposed — not a catch-all — so the browser can't
* reach arbitrary platform endpoints with the server-attached credentials.
*/
export const dynamic = 'force-dynamic';

export async function GET(request: Request): Promise<Response> {
if (!SGP_BASE_URL) {
return NextResponse.json(
{ error: 'SGP is not configured. Set SGP_API_URL.' },
Comment thread
declan-scale marked this conversation as resolved.
{ status: 503 }
);
}

const headers = new Headers({ accept: 'application/json' });
await applyBffCredentials(request, headers);
const upstream = await fetch(`${SGP_BASE_URL}/user-info`, { headers });
return new Response(upstream.body, {
status: upstream.status,
headers: { 'content-type': 'application/json' },
});
}
20 changes: 5 additions & 15 deletions agentex-ui/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,23 +7,13 @@ export default async function RootPage() {
await connection();

const sgpAppURL = process.env.NEXT_PUBLIC_SGP_APP_URL ?? '';
const agentexAPIBaseURL =
process.env.NEXT_PUBLIC_AGENTEX_API_BASE_URL ?? 'http://localhost:5003';

if (!agentexAPIBaseURL) {
return (
<div role="alert">
<p>Missing some configs</p>
<pre>{JSON.stringify({ sgpAppURL, agentexAPIBaseURL }, null, 2)}</pre>
</div>
);
}
// The account picker needs the platform API (accounts come from /api/user-info).
const accountsEnabled = !!(
process.env.SGP_API_URL ?? process.env.NEXT_PUBLIC_SGP_APP_URL
);

return (
<AgentexProvider
sgpAppURL={sgpAppURL}
agentexAPIBaseURL={agentexAPIBaseURL}
>
<AgentexProvider accountsEnabled={accountsEnabled} sgpAppURL={sgpAppURL}>
<AgentexUIRoot />
</AgentexProvider>
);
Expand Down
Loading
Loading