diff --git a/webapp/plugins/vouch-proxy.test.ts b/webapp/plugins/vouch-proxy.test.ts index 3b1e0f0c..c58472c5 100644 --- a/webapp/plugins/vouch-proxy.test.ts +++ b/webapp/plugins/vouch-proxy.test.ts @@ -21,6 +21,7 @@ beforeAll(async () => { method: req.method, path: req.url, auth: req.headers.authorization ?? null, + agent: req.headers['x-vouch-agent'] ?? null, body, }), ) @@ -63,6 +64,21 @@ test('forwards POST body and Authorization to the target, rewriting /proxy prefi expect(JSON.parse(echoed.body).method).toBe('kb.status') }) +test('forwards the X-Vouch-Agent reviewer identity to the target', async () => { + const res = await fetch(`${proxyUrl}/proxy/rpc`, { + method: 'POST', + headers: { + 'content-type': 'application/json', + 'x-vouch-target': upstreamUrl, + 'x-vouch-agent': 'alice', + }, + body: JSON.stringify({ id: '1', method: 'kb.approve', params: {} }), + }) + expect(res.status).toBe(200) + const echoed = await res.json() + expect(echoed.agent).toBe('alice') +}) + test('forwards GET /proxy/health to target /health', async () => { const res = await fetch(`${proxyUrl}/proxy/health`, { headers: { 'x-vouch-target': upstreamUrl }, diff --git a/webapp/plugins/vouch-proxy.ts b/webapp/plugins/vouch-proxy.ts index 8e2b9442..c3dfdee6 100644 --- a/webapp/plugins/vouch-proxy.ts +++ b/webapp/plugins/vouch-proxy.ts @@ -59,6 +59,10 @@ export function proxyMiddleware(): (req: IncomingMessage, res: ServerResponse, n const headers: Record = {} if (req.headers['content-type']) headers['content-type'] = String(req.headers['content-type']) if (req.headers.authorization) headers.authorization = String(req.headers.authorization) + // Reviewer identity: a human approving in the console must be attributed to + // themselves, not left as the tokenless `unknown-agent` default (which + // collides with the proposing agent and trips the self-approval gate). + if (req.headers['x-vouch-agent']) headers['x-vouch-agent'] = String(req.headers['x-vouch-agent']) const upstream = mod.request( { diff --git a/webapp/src/components/Shell.tsx b/webapp/src/components/Shell.tsx index f0d53b37..81a8341a 100644 --- a/webapp/src/components/Shell.tsx +++ b/webapp/src/components/Shell.tsx @@ -4,6 +4,7 @@ import { NavLink, Outlet, useLocation } from 'react-router-dom' import { ConnectDialog } from '../connection/ConnectDialog' import { ALL_SCOPE, useConnection } from '../connection/ConnectionContext' import { useFanout } from '../lib/fanout' +import { reviewerId, setReviewerId } from '../lib/rpc' import type { Proposal, SessionEntry } from '../lib/types' const THEME_KEY = 'vouch-ui.theme' @@ -33,6 +34,7 @@ export function Shell() { const location = useLocation() const [theme, setTheme] = useState(() => localStorage.getItem(THEME_KEY) ?? 'dark') const [manageOpen, setManageOpen] = useState(false) + const [reviewer, setReviewer] = useState(reviewerId) useEffect(() => { document.documentElement.dataset.theme = theme @@ -127,6 +129,17 @@ export function Shell() { ))} )} + { + setReviewer(e.target.value) + setReviewerId(e.target.value) + }} + placeholder="reviewer" + title="Approvals and proposals from this console are attributed to this name (empty falls back to 'console')" + className="w-28 rounded-lg border border-rule bg-paper-2 px-2 py-1.5 text-xs text-ink-2 outline-none focus:border-accent" + />