mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-16 08:17:11 +00:00
fix(browser): gate response fulfillment opt-in
Squash merge PR #1295 into dev.
This commit is contained in:
@@ -127,12 +127,18 @@ CCS still supports environment-variable overrides for backward compatibility.
|
||||
| `CCS_BROWSER_USER_DATA_DIR` | Preferred override for Claude Browser Attach user-data dir |
|
||||
| `CCS_BROWSER_PROFILE_DIR` | Legacy alias for the same attach directory |
|
||||
| `CCS_BROWSER_DEVTOOLS_PORT` | Explicit DevTools port override |
|
||||
| `CCS_BROWSER_INTERCEPT_FULFILL_MODE=enabled` | Dangerous local-testing opt-in for Browser MCP response fulfillment; disabled by default |
|
||||
| `CCS_BROWSER_UPLOAD_ROOTS` | Optional `path.delimiter`-separated allowlist for local files that browser upload tools may read |
|
||||
| `CCS_BROWSER_DOWNLOAD_ROOTS` | Optional `path.delimiter`-separated allowlist for caller-provided browser download directories |
|
||||
|
||||
If an override is active, Browser status surfaces should report that the current session is being
|
||||
managed externally by environment variables.
|
||||
|
||||
Browser MCP request interception can continue or fail matched requests by default. Synthetic response
|
||||
fulfillment (`action: fulfill`) is more sensitive because it can serve caller-supplied response
|
||||
content inside the attached browser's target origin. CCS therefore hides and blocks fulfillment unless
|
||||
`CCS_BROWSER_INTERCEPT_FULFILL_MODE=enabled` is set for a trusted local test session.
|
||||
|
||||
The saved browser policy still controls default exposure. Env overrides change the effective attach
|
||||
path/port for the current shell; they do not bypass `policy: manual`.
|
||||
|
||||
|
||||
@@ -639,7 +639,7 @@ function getTools() {
|
||||
},
|
||||
},
|
||||
priority: { type: 'integer' },
|
||||
action: { type: 'string', enum: ['continue', 'fail', 'fulfill'] },
|
||||
action: { type: 'string', enum: getInterceptActionEnum() },
|
||||
statusCode: { type: 'integer', minimum: 100, maximum: 599 },
|
||||
responseHeaders: {
|
||||
type: 'array',
|
||||
@@ -1292,9 +1292,30 @@ function parseOptionalPageId(toolArgs) {
|
||||
: '';
|
||||
}
|
||||
|
||||
function getBrowserInterceptFulfillMode() {
|
||||
return String(process.env.CCS_BROWSER_INTERCEPT_FULFILL_MODE || 'disabled').trim() === 'enabled'
|
||||
? 'enabled'
|
||||
: 'disabled';
|
||||
}
|
||||
|
||||
function isBrowserInterceptFulfillEnabled() {
|
||||
return getBrowserInterceptFulfillMode() === 'enabled';
|
||||
}
|
||||
|
||||
function getInterceptActionEnum() {
|
||||
return isBrowserInterceptFulfillEnabled()
|
||||
? ['continue', 'fail', 'fulfill']
|
||||
: ['continue', 'fail'];
|
||||
}
|
||||
|
||||
function parseInterceptAction(value) {
|
||||
if (value === 'fulfill' && !isBrowserInterceptFulfillEnabled()) {
|
||||
throw new Error(
|
||||
'action fulfill is disabled by CCS_BROWSER_INTERCEPT_FULFILL_MODE=disabled; set it to enabled only for trusted local testing'
|
||||
);
|
||||
}
|
||||
if (value !== 'continue' && value !== 'fail' && value !== 'fulfill') {
|
||||
throw new Error('action must be one of: continue, fail, fulfill');
|
||||
throw new Error(`action must be one of: ${getInterceptActionEnum().join(', ')}`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@ import { runMcpRequests, getResponseText } from './browser-mcp-test-harness';
|
||||
import type { MockPageState } from './browser-mcp-test-harness';
|
||||
|
||||
describe('ccs-browser MCP server - session and interception', () => {
|
||||
const fulfillEnabledEnv = { CCS_BROWSER_INTERCEPT_FULFILL_MODE: 'enabled' };
|
||||
|
||||
it('lists browser tools including navigate, click, type, and screenshot', async () => {
|
||||
const responses = await runMcpRequests(
|
||||
[{ id: 'page-1', title: 'Example Page', currentUrl: 'https://example.com/' }],
|
||||
@@ -14,7 +16,9 @@ describe('ccs-browser MCP server - session and interception', () => {
|
||||
tools: Array<{
|
||||
name: string;
|
||||
description?: string;
|
||||
inputSchema?: { properties?: Record<string, { type?: string; minimum?: number }> };
|
||||
inputSchema?: {
|
||||
properties?: Record<string, { type?: string; minimum?: number; enum?: string[] }>;
|
||||
};
|
||||
}>;
|
||||
}
|
||||
).tools;
|
||||
@@ -106,7 +110,10 @@ describe('ccs-browser MCP server - session and interception', () => {
|
||||
expect(addRuleTool?.inputSchema?.properties?.urlRegex).toMatchObject({ type: 'string' });
|
||||
expect(addRuleTool?.inputSchema?.properties?.headerMatchers).toMatchObject({ type: 'array' });
|
||||
expect(addRuleTool?.inputSchema?.properties?.priority).toMatchObject({ type: 'integer' });
|
||||
expect(addRuleTool?.inputSchema?.properties?.action).toMatchObject({ type: 'string' });
|
||||
expect(addRuleTool?.inputSchema?.properties?.action).toMatchObject({
|
||||
type: 'string',
|
||||
enum: ['continue', 'fail'],
|
||||
});
|
||||
expect(addRuleTool?.inputSchema?.properties?.statusCode).toMatchObject({ type: 'integer' });
|
||||
expect(addRuleTool?.inputSchema?.properties?.responseHeaders).toMatchObject({ type: 'array' });
|
||||
expect(addRuleTool?.inputSchema?.properties?.headers).toBeUndefined();
|
||||
@@ -1148,6 +1155,7 @@ describe('ccs-browser MCP server - session and interception', () => {
|
||||
},
|
||||
],
|
||||
{
|
||||
childEnv: fulfillEnabledEnv,
|
||||
responseTimeoutMs: 12000,
|
||||
}
|
||||
);
|
||||
@@ -1217,6 +1225,7 @@ describe('ccs-browser MCP server - session and interception', () => {
|
||||
},
|
||||
],
|
||||
{
|
||||
childEnv: fulfillEnabledEnv,
|
||||
responseTimeoutMs: 12000,
|
||||
}
|
||||
);
|
||||
@@ -1372,6 +1381,7 @@ describe('ccs-browser MCP server - session and interception', () => {
|
||||
},
|
||||
],
|
||||
{
|
||||
childEnv: fulfillEnabledEnv,
|
||||
responseTimeoutMs: 12000,
|
||||
}
|
||||
);
|
||||
@@ -1435,6 +1445,46 @@ describe('ccs-browser MCP server - session and interception', () => {
|
||||
expect(pages[0]?.intercept?.continuedRequestIds).toContain('req-header-regex');
|
||||
});
|
||||
|
||||
it('exposes fulfill interception only with the explicit dangerous opt-in', async () => {
|
||||
const responses = await runMcpRequests(
|
||||
[{ id: 'page-1', title: 'Home', currentUrl: 'https://example.com/' }],
|
||||
[
|
||||
{ jsonrpc: '2.0', id: 958, method: 'tools/list' },
|
||||
{
|
||||
jsonrpc: '2.0',
|
||||
id: 959,
|
||||
method: 'tools/call',
|
||||
params: {
|
||||
name: 'browser_add_intercept_rule',
|
||||
arguments: {
|
||||
urlIncludes: '/api/mock',
|
||||
action: 'fulfill',
|
||||
statusCode: 200,
|
||||
body: 'blocked',
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
);
|
||||
|
||||
const tools = (
|
||||
responses.find((message) => message.id === 958)?.result as {
|
||||
tools: Array<{
|
||||
name: string;
|
||||
inputSchema?: { properties?: Record<string, { enum?: string[] }> };
|
||||
}>;
|
||||
}
|
||||
).tools;
|
||||
const addRuleTool = tools.find((tool) => tool.name === 'browser_add_intercept_rule');
|
||||
expect(addRuleTool?.inputSchema?.properties?.action?.enum).toEqual(['continue', 'fail']);
|
||||
|
||||
const response = responses.find((message) => message.id === 959);
|
||||
expect((response?.result as { isError?: boolean }).isError).toBe(true);
|
||||
expect(getResponseText(response)).toContain(
|
||||
'Browser MCP failed: action fulfill is disabled by CCS_BROWSER_INTERCEPT_FULFILL_MODE=disabled'
|
||||
);
|
||||
});
|
||||
|
||||
it('adds a fulfill interception rule and lists its response summary', async () => {
|
||||
const responses = await runMcpRequests(
|
||||
[{ id: 'page-1', title: 'Home', currentUrl: 'https://example.com/' }],
|
||||
@@ -1461,7 +1511,8 @@ describe('ccs-browser MCP server - session and interception', () => {
|
||||
method: 'tools/call',
|
||||
params: { name: 'browser_list_intercept_rules', arguments: {} },
|
||||
},
|
||||
]
|
||||
],
|
||||
{ childEnv: fulfillEnabledEnv }
|
||||
);
|
||||
|
||||
const listText = getResponseText(responses.find((message) => message.id === 962));
|
||||
@@ -1515,6 +1566,7 @@ describe('ccs-browser MCP server - session and interception', () => {
|
||||
},
|
||||
],
|
||||
{
|
||||
childEnv: fulfillEnabledEnv,
|
||||
responseTimeoutMs: 12000,
|
||||
}
|
||||
);
|
||||
@@ -1577,6 +1629,7 @@ describe('ccs-browser MCP server - session and interception', () => {
|
||||
},
|
||||
],
|
||||
{
|
||||
childEnv: fulfillEnabledEnv,
|
||||
responseTimeoutMs: 12000,
|
||||
}
|
||||
);
|
||||
@@ -1610,7 +1663,8 @@ describe('ccs-browser MCP server - session and interception', () => {
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
],
|
||||
{ childEnv: fulfillEnabledEnv }
|
||||
);
|
||||
|
||||
const addText = getResponseText(responses.find((message) => message.id === 967));
|
||||
@@ -1636,7 +1690,8 @@ describe('ccs-browser MCP server - session and interception', () => {
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
],
|
||||
{ childEnv: fulfillEnabledEnv }
|
||||
);
|
||||
|
||||
const response = responses.find((message) => message.id === 968);
|
||||
@@ -1664,7 +1719,8 @@ describe('ccs-browser MCP server - session and interception', () => {
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
],
|
||||
{ childEnv: fulfillEnabledEnv }
|
||||
);
|
||||
|
||||
const response = responses.find((message) => message.id === 969);
|
||||
@@ -1692,7 +1748,8 @@ describe('ccs-browser MCP server - session and interception', () => {
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
],
|
||||
{ childEnv: fulfillEnabledEnv }
|
||||
);
|
||||
|
||||
const response = responses.find((message) => message.id === 970);
|
||||
@@ -1719,7 +1776,8 @@ describe('ccs-browser MCP server - session and interception', () => {
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
],
|
||||
{ childEnv: fulfillEnabledEnv }
|
||||
);
|
||||
|
||||
const response = responses.find((message) => message.id === 971);
|
||||
@@ -1885,6 +1943,7 @@ describe('ccs-browser MCP server - session and interception', () => {
|
||||
},
|
||||
],
|
||||
{
|
||||
childEnv: fulfillEnabledEnv,
|
||||
responseTimeoutMs: 12000,
|
||||
}
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user