mirror of
https://github.com/nexu-io/open-design.git
synced 2026-06-01 03:14:35 +07:00
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:
parent
b5b975769a
commit
9e8d01ee22
2 changed files with 253 additions and 1 deletions
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue