Skip to content

ai-assistant: docs/proposals: Add consent-poc adversarial testing harness#890

Open
illume wants to merge 1 commit into
headlamp-k8s:mainfrom
illume:docs/ai-assistant-consent-poc
Open

ai-assistant: docs/proposals: Add consent-poc adversarial testing harness#890
illume wants to merge 1 commit into
headlamp-k8s:mainfrom
illume:docs/ai-assistant-consent-poc

Conversation

@illume

@illume illume commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Adds docs/proposals/consent-poc/ — a Playwright-based proof-of-concept adversarial test harness for validating the AI assistant's consent and approval flows, including a vulnerable test server for adversarial scenario reproduction.

Assisted by Copilot.

Part of the ai-assistant design proposals.

@illume illume requested review from ashu8912 and Copilot July 8, 2026 11:48
@illume illume force-pushed the docs/ai-assistant-consent-poc branch from e214bd1 to 05b9c14 Compare July 8, 2026 11:52

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a consent/approval-flow proof-of-concept under ai-assistant/docs/proposals/consent-poc/, including a protected Node.js server, a deliberately vulnerable twin server used as a negative control, and a Playwright E2E/adversarial test harness to validate the security properties (Sec-Fetch filtering, COOP isolation, nonce validation, SW registration blocking).

Changes:

  • Add a dependency-free Node HTTP PoC server (server.cjs) implementing the consent popup flow and server-side protections.
  • Add a vulnerable “twin” server (vulnerable-server.cjs) to validate that attacks truly succeed when protections are removed.
  • Add Playwright config + two test suites (positive + adversarial/negative-control), plus documentation and local npm scripts.

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 10 comments.

Show a summary per file
File Description
ai-assistant/docs/proposals/consent-poc/server.cjs Protected PoC server implementing consent flow, COOP header, Sec-Fetch gating, SW header blocking, and nonce approval.
ai-assistant/docs/proposals/consent-poc/vulnerable-server.cjs Vulnerable twin server used as a negative control for adversarial tests.
ai-assistant/docs/proposals/consent-poc/tests/consent-poc.pw-spec.ts Core Playwright suite validating HTTP protections and end-to-end popup behavior.
ai-assistant/docs/proposals/consent-poc/tests/consent-poc-adversarial.pw-spec.ts Adversarial/negative-control suite (raw-socket checks, fuzzing, twin validation, popup isolation assertions).
ai-assistant/docs/proposals/consent-poc/playwright.config.ts Playwright configuration and dual webServer startup (protected + twin).
ai-assistant/docs/proposals/consent-poc/README.md Documentation for the PoC model, threat vectors, and test harness usage.
ai-assistant/docs/proposals/consent-poc/package.json Local scripts and Playwright dev dependency for running the E2E suite.
ai-assistant/docs/proposals/consent-poc/.gitignore Local ignores for node/test artifacts.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Minimal proof of concept for the same-port consent popup security model described in
[`backend-and-mcp.md`](../backend-and-mcp.md).

**No third-party dependencies.** Pure Node.js + minimal HTML/JS.
## Quick start

```bash
cd docs/proposals/consent-poc
## What it proves

The PoC demonstrates that a **same-port popup** can be securely isolated from the
page that opens it, using three coordinated browser mechanisms:
Comment on lines +87 to +90
- `tests/consent-poc.spec.ts` — 19 tests covering the attack table above
(HTTP-level Sec-Fetch filtering, COOP header, nonce validation, nonce
replay, Service Worker blocking) and browser-level attack buttons.
- `tests/consent-poc-adversarial.spec.ts` — 34 additional tests that attack

// Adversarial / negative-control tests for the consent-poc.
//
// Rationale: the positive tests in consent-poc.spec.ts could theoretically
Comment on lines +32 to +45
function rawHttp(host: string, port: number, request: string): Promise<string> {
return new Promise((resolve, reject) => {
const sock = net.createConnection({ host, port }, () => sock.write(request));
const chunks: Buffer[] = [];
sock.on('data', c => chunks.push(c));
sock.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')));
sock.on('error', reject);
// Safety timeout.
setTimeout(() => {
sock.destroy();
resolve(Buffer.concat(chunks).toString('utf8'));
}, 3000);
});
}
// Poller on main page should detect approval within 30s (checks every 1s).
await expect(page.locator('#log')).toContainText(
'Command approved by user',
{ timeout: 10_000 }
<div class="card">
<h1>🔒 Command Approval Required</h1>
<p>A plugin wants to run this command:</p>
<div class="command">${command}</div>
<input type="hidden" name="nonce" value="${nonce}">
<button type="submit" autofocus>Approve</button>
</form>
<p>Command: ${command}</p>
Comment on lines +1 to +4
node_modules/
test-results/
playwright-report/
package-lock.json
@illume illume force-pushed the docs/ai-assistant-consent-poc branch from 05b9c14 to 3f207f9 Compare July 8, 2026 11:52
@illume illume requested a review from Copilot July 8, 2026 11:55

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 8 out of 8 changed files in this pull request and generated 12 comments.

<div class="card">
<h1>🔒 Command Approval Required</h1>
<p>A plugin wants to run this command:</p>
<div class="command">${command}</div>
<input type="hidden" name="nonce" value="${nonce}">
<button type="submit" autofocus>Approve</button>
</form>
<p>Command: ${command}</p>
4466,
`GET /consent/${consentId} HTTP/1.1\r\nHost: localhost:4466\r\nSec-Fetch-Dest: document\r\nSec-Fetch-Mode: navigate\r\nConnection: close\r\n\r\n`
);
const [headerBlock] = raw.split('\r\n\r\n', 1);
4467,
`GET /consent/${consentId} HTTP/1.1\r\nHost: localhost:4467\r\nConnection: close\r\n\r\n`
);
const [headerBlock] = raw.split('\r\n\r\n', 1);
Comment on lines +351 to +359
if (url.pathname === '/api/request-consent' && method === 'POST') {
const body = await parseBody(req);
const { command } = JSON.parse(body);
const consentId = randomId();
const nonce = randomId();
consents.set(consentId, { nonce, command, approved: false, used: false });
json(res, 200, { consentId });
return;
}
("Approve Command") or the real URL (which contains the `consentId`), so the
security property holds. `window.opener === null` inside the popup is the
cross-browser-portable primitive and is asserted by
`consent-poc-adversarial.spec.ts`.
Comment on lines +136 to +138
- `tests/consent-poc.spec.ts` — 19 e2e tests covering the attack table above.
- `tests/consent-poc-adversarial.spec.ts` — 34 adversarial / negative-control
tests.

// Adversarial / negative-control tests for the consent-poc.
//
// Rationale: the positive tests in consent-poc.spec.ts could theoretically
Comment on lines +12 to +18
// Vulnerable TWIN of server.cjs used as a NEGATIVE CONTROL for the
// consent-poc tests. It exposes the same API and HTML, but
// deliberately removes the 3 server-side protections:
//
// 1. No Sec-Fetch-Dest/Mode filtering on /consent/:id
// 2. No `Cross-Origin-Opener-Policy` header on the consent page
// 3. No `Service-Worker: script` block
Comment on lines +32 to +45
function rawHttp(host: string, port: number, request: string): Promise<string> {
return new Promise((resolve, reject) => {
const sock = net.createConnection({ host, port }, () => sock.write(request));
const chunks: Buffer[] = [];
sock.on('data', c => chunks.push(c));
sock.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')));
sock.on('error', reject);
// Safety timeout.
setTimeout(() => {
sock.destroy();
resolve(Buffer.concat(chunks).toString('utf8'));
}, 3000);
});
}
… testing harness

Playwright-based proof-of-concept for validating the AI Assistant consent
and approval flows under both normal and adversarial conditions.

Contents:
- `server.cjs`: minimal Express server that simulates a legitimate MCP tool
  endpoint returning expected responses.
- `vulnerable-server.cjs`: intentionally insecure server that omits input
  validation, for reproducing attack scenarios.
- `consent-poc.pw-spec.ts`: happy-path Playwright suite — user sees the approval
  dialog, confirms, and the tool executes correctly.
- `consent-poc-adversarial.pw-spec.ts`: attack scenario suite — verifies that
  malformed or injected tool arguments are blocked by the approval flow before
  reaching the server.

Use these tests to verify that changes to `InlineToolApprovalManager` or the
consent UI do not regress the security boundary.

Signed-off-by: René Dudfield <renedudfield@microsoft.com>
Co-authored-by: Ashu Ghildiyal <aghildiyal@microsoft.com>
Signed-off-by: Ashu Ghildiyal <aghildiyal@microsoft.com>
@illume illume force-pushed the docs/ai-assistant-consent-poc branch from 3f207f9 to afee1f5 Compare July 8, 2026 11:59
@illume illume requested a review from Copilot July 8, 2026 15:29

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 8 out of 8 changed files in this pull request and generated 5 comments.

Comment on lines +318 to +321
<h1>🔒 Command Approval Required</h1>
<p>A plugin wants to run this command:</p>
<div class="command">${command}</div>
<form method="POST" action="/consent/${consentId}/approve" style="margin-top: 1.5rem;">
Comment on lines +56 to +61
<form method="POST" action="/consent/${consentId}/approve">
<input type="hidden" name="nonce" value="${nonce}">
<button type="submit" autofocus>Approve</button>
</form>
<p>Command: ${command}</p>
</body></html>`;
Comment on lines +56 to +58
For older browsers without `Sec-Fetch-*` headers, the server allows the request
(headers are absent), but COOP + nonce still provide protection. The consent page
would be visible to `fetch()` but the nonce can't be replayed after use.
Comment on lines +32 to +45
function rawHttp(host: string, port: number, request: string): Promise<string> {
return new Promise((resolve, reject) => {
const sock = net.createConnection({ host, port }, () => sock.write(request));
const chunks: Buffer[] = [];
sock.on('data', c => chunks.push(c));
sock.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')));
sock.on('error', reject);
// Safety timeout.
setTimeout(() => {
sock.destroy();
resolve(Buffer.concat(chunks).toString('utf8'));
}, 3000);
});
}
Comment on lines +187 to +191
// Poller on main page should detect approval within 30s (checks every 1s).
await expect(page.locator('#log')).toContainText(
'Command approved by user',
{ timeout: 10_000 }
);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants