fix(daemon): fail disallowed connector tool selections (#2006)

* fix(daemon): fail disallowed connector tool selections

* fix(daemon): harden connector tool error parsing
This commit is contained in:
kami 2026-05-26 15:03:10 +08:00 committed by GitHub
parent b5b975769a
commit 9e8d01ee22
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 253 additions and 1 deletions

View file

@ -45,6 +45,54 @@ function stringifyContent(value: unknown): string {
}
}
function parseJsonObjectsFromContent(value: string): JsonObject[] {
const trimmed = value.trim();
if (!trimmed) return [];
const direct = safeParseJson(trimmed);
if (isRecord(direct)) return [direct];
const objects: JsonObject[] = [];
for (const line of trimmed.split(/\r?\n/u)) {
const parsedLine = safeParseJson(line.trim());
if (isRecord(parsedLine)) objects.push(parsedLine);
}
return objects;
}
function extractConnectorApiError(value: JsonObject): JsonObject | null {
if (isRecord(value.error)) {
if (typeof value.error.code === 'string') return value.error;
if (isRecord(value.error.data) && isRecord(value.error.data.error)) {
const wrappedError = value.error.data.error;
if (typeof wrappedError.code === 'string') return wrappedError;
}
}
return null;
}
function connectorToolSelectionErrorMessage(content: string): string | null {
if (!content.includes('CONNECTOR_TOOL_NOT_FOUND')) return null;
let error: JsonObject | null = null;
for (const parsed of parseJsonObjectsFromContent(content)) {
const parsedError = extractConnectorApiError(parsed);
if (parsedError?.code === 'CONNECTOR_TOOL_NOT_FOUND') {
error = parsedError;
break;
}
}
if (!error) return null;
const details = isRecord(error.details) ? error.details : {};
const connectorId = typeof details.connectorId === 'string' && details.connectorId
? details.connectorId
: undefined;
const toolName = typeof details.toolName === 'string' && details.toolName
? details.toolName
: 'the requested connector tool';
const target = connectorId
? `Connector tool ${toolName} is not allowed for connector ${connectorId}.`
: `Connector tool ${toolName} is not allowed.`;
return `${target} Re-list the connector catalog and choose one of the currently allowed read-only tools.`;
}
function extractErrorMessage(value: unknown, fallback: string): string {
if (typeof value === 'string') {
const parsed = safeParseJson(value);
@ -352,12 +400,18 @@ if (obj.type === 'error') {
},
});
}
const content = stringifyContent(item.aggregated_output ?? '');
onEvent({
type: 'tool_result',
toolUseId: item.id,
content: stringifyContent(item.aggregated_output ?? ''),
content,
isError: typeof item.exit_code === 'number' ? item.exit_code !== 0 : item.status === 'failed',
});
const connectorToolError = connectorToolSelectionErrorMessage(content);
if (connectorToolError && !state.codexErrorEmitted) {
state.codexErrorEmitted = true;
onEvent({ type: 'error', message: connectorToolError });
}
return true;
}
}

View file

@ -449,6 +449,204 @@ test('codex json stream emits command execution tool events', () => {
]);
});
test('codex json stream surfaces disallowed connector tool selections as terminal errors', () => {
const { events, handler } = collectEvents('codex');
const connectorError = JSON.stringify({
ok: false,
status: 404,
error: {
code: 'CONNECTOR_TOOL_NOT_FOUND',
message: 'connector tool is not allowed',
details: {
connectorId: 'github',
toolName: 'github.github_list_notifications',
},
},
});
handler.feed(
JSON.stringify({
type: 'item.started',
item: {
id: 'item-connector',
type: 'command_execution',
command: 'od tools connectors execute --connector github --tool github.github_list_notifications --input .daily-digest-tmp/notifications.json',
aggregated_output: '',
exit_code: null,
status: 'in_progress',
},
}) +
'\n' +
JSON.stringify({
type: 'item.completed',
item: {
id: 'item-connector',
type: 'command_execution',
command: 'od tools connectors execute --connector github --tool github.github_list_notifications --input .daily-digest-tmp/notifications.json',
aggregated_output: `${connectorError}\n`,
exit_code: 1,
status: 'failed',
},
}) +
'\n',
);
assert.deepEqual(events, [
{
type: 'tool_use',
id: 'item-connector',
name: 'Bash',
input: {
command: 'od tools connectors execute --connector github --tool github.github_list_notifications --input .daily-digest-tmp/notifications.json',
},
},
{
type: 'tool_result',
toolUseId: 'item-connector',
content: `${connectorError}\n`,
isError: true,
},
{
type: 'error',
message: 'Connector tool github.github_list_notifications is not allowed for connector github. Re-list the connector catalog and choose one of the currently allowed read-only tools.',
},
]);
});
test('codex json stream finds connector tool errors after earlier noise json output', () => {
const { events, handler } = collectEvents('codex');
const noiseLine = JSON.stringify({
event: 'running',
message: 'starting connector call',
});
const connectorError = JSON.stringify({
ok: false,
status: 404,
error: {
code: 'CONNECTOR_TOOL_NOT_FOUND',
message: 'connector tool is not allowed',
details: {
connectorId: 'github',
toolName: 'github.github_list_notifications',
},
},
});
handler.feed(
JSON.stringify({
type: 'item.started',
item: {
id: 'item-connector-noise',
type: 'command_execution',
command: 'od tools connectors execute --connector github --tool github.github_list_notifications --input .daily-digest-tmp/notifications.json',
aggregated_output: '',
exit_code: null,
status: 'in_progress',
},
}) +
'\n' +
JSON.stringify({
type: 'item.completed',
item: {
id: 'item-connector-noise',
type: 'command_execution',
command: 'od tools connectors execute --connector github --tool github.github_list_notifications --input .daily-digest-tmp/notifications.json',
aggregated_output: `${noiseLine}\n${connectorError}\n`,
exit_code: 1,
status: 'failed',
},
}) +
'\n',
);
assert.deepEqual(events, [
{
type: 'tool_use',
id: 'item-connector-noise',
name: 'Bash',
input: {
command: 'od tools connectors execute --connector github --tool github.github_list_notifications --input .daily-digest-tmp/notifications.json',
},
},
{
type: 'tool_result',
toolUseId: 'item-connector-noise',
content: `${noiseLine}\n${connectorError}\n`,
isError: true,
},
{
type: 'error',
message: 'Connector tool github.github_list_notifications is not allowed for connector github. Re-list the connector catalog and choose one of the currently allowed read-only tools.',
},
]);
});
test('codex json stream surfaces wrapped connector tool errors as terminal errors', () => {
const { events, handler } = collectEvents('codex');
const connectorError = JSON.stringify({
error: {
data: {
error: {
code: 'CONNECTOR_TOOL_NOT_FOUND',
message: 'connector tool is not allowed',
details: {
connectorId: 'github',
toolName: 'github.github_list_notifications',
},
},
},
},
});
handler.feed(
JSON.stringify({
type: 'item.started',
item: {
id: 'item-connector-wrapped',
type: 'command_execution',
command: 'od tools connectors execute --connector github --tool github.github_list_notifications --input .daily-digest-tmp/notifications.json',
aggregated_output: '',
exit_code: null,
status: 'in_progress',
},
}) +
'\n' +
JSON.stringify({
type: 'item.completed',
item: {
id: 'item-connector-wrapped',
type: 'command_execution',
command: 'od tools connectors execute --connector github --tool github.github_list_notifications --input .daily-digest-tmp/notifications.json',
aggregated_output: `${connectorError}\n`,
exit_code: 1,
status: 'failed',
},
}) +
'\n',
);
assert.deepEqual(events, [
{
type: 'tool_use',
id: 'item-connector-wrapped',
name: 'Bash',
input: {
command: 'od tools connectors execute --connector github --tool github.github_list_notifications --input .daily-digest-tmp/notifications.json',
},
},
{
type: 'tool_result',
toolUseId: 'item-connector-wrapped',
content: `${connectorError}\n`,
isError: true,
},
{
type: 'error',
message: 'Connector tool github.github_list_notifications is not allowed for connector github. Re-list the connector catalog and choose one of the currently allowed read-only tools.',
},
]);
});
test('unhandled structured events fall back to raw', () => {
const { events, handler } = collectEvents('codex');