@shipfox/api-integration-core 12.3.0 → 12.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +4 -4
- package/CHANGELOG.md +27 -0
- package/dist/presentation/routes/agent-tools-gateway/dispatch.d.ts +7 -1
- package/dist/presentation/routes/agent-tools-gateway/dispatch.d.ts.map +1 -1
- package/dist/presentation/routes/agent-tools-gateway/dispatch.js +24 -10
- package/dist/presentation/routes/agent-tools-gateway/dispatch.js.map +1 -1
- package/dist/presentation/routes/errors.d.ts.map +1 -1
- package/dist/presentation/routes/errors.js +1 -1
- package/dist/presentation/routes/errors.js.map +1 -1
- package/dist/tsconfig.test.tsbuildinfo +1 -1
- package/package.json +10 -10
- package/src/presentation/routes/agent-tools-gateway/agent-tools-gateway.route.test.ts +11 -4
- package/src/presentation/routes/agent-tools-gateway/dispatch.test.ts +107 -0
- package/src/presentation/routes/agent-tools-gateway/dispatch.ts +54 -15
- package/src/presentation/routes/errors.ts +1 -0
- package/tsconfig.build.tsbuildinfo +1 -1
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shipfox/api-integration-core",
|
|
3
3
|
"license": "MIT",
|
|
4
|
-
"version": "12.
|
|
4
|
+
"version": "12.5.0",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
7
7
|
"url": "git+https://github.com/ShipfoxHQ/shipfox.git",
|
|
@@ -26,17 +26,17 @@
|
|
|
26
26
|
"zod": "^4.4.3",
|
|
27
27
|
"@shipfox/api-agent-dto": "12.2.0",
|
|
28
28
|
"@shipfox/api-auth-context": "12.2.0",
|
|
29
|
-
"@shipfox/api-workflows-dto": "12.
|
|
29
|
+
"@shipfox/api-workflows-dto": "12.5.0",
|
|
30
30
|
"@shipfox/api-integration-core-dto": "12.2.0",
|
|
31
|
-
"@shipfox/api-integration-spi": "1.1.
|
|
31
|
+
"@shipfox/api-integration-spi": "1.1.1",
|
|
32
32
|
"@shipfox/api-workspaces-dto": "12.0.0",
|
|
33
|
-
"@shipfox/api-integration-gitea": "12.
|
|
34
|
-
"@shipfox/api-integration-github": "12.
|
|
35
|
-
"@shipfox/api-integration-jira": "12.
|
|
36
|
-
"@shipfox/api-integration-linear": "12.
|
|
37
|
-
"@shipfox/api-integration-sentry": "12.
|
|
38
|
-
"@shipfox/api-integration-slack": "12.
|
|
39
|
-
"@shipfox/api-integration-webhook": "12.
|
|
33
|
+
"@shipfox/api-integration-gitea": "12.5.0",
|
|
34
|
+
"@shipfox/api-integration-github": "12.5.0",
|
|
35
|
+
"@shipfox/api-integration-jira": "12.5.0",
|
|
36
|
+
"@shipfox/api-integration-linear": "12.5.0",
|
|
37
|
+
"@shipfox/api-integration-sentry": "12.5.0",
|
|
38
|
+
"@shipfox/api-integration-slack": "12.5.0",
|
|
39
|
+
"@shipfox/api-integration-webhook": "12.5.0",
|
|
40
40
|
"@shipfox/config": "1.2.4",
|
|
41
41
|
"@shipfox/inter-module": "0.2.3",
|
|
42
42
|
"@shipfox/node-drizzle": "0.3.5",
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
} from '@shipfox/api-auth-context';
|
|
10
10
|
import {type AuthMethod, ClientError, closeApp, createApp} from '@shipfox/node-fastify';
|
|
11
11
|
import type {FastifyInstance, FastifyRequest} from 'fastify';
|
|
12
|
+
import {IntegrationProviderError} from '#core/errors.js';
|
|
12
13
|
import {
|
|
13
14
|
agentStepConfig,
|
|
14
15
|
catalogTool,
|
|
@@ -227,13 +228,19 @@ describe('agent tools gateway route', () => {
|
|
|
227
228
|
});
|
|
228
229
|
});
|
|
229
230
|
|
|
230
|
-
it('returns
|
|
231
|
+
it('returns the provider message and terminal code for rejected calls', async () => {
|
|
231
232
|
const lease = leaseContext({workspaceId: 'workspace-1'});
|
|
232
233
|
const integration = materializedIntegration({connectionId: 'connection-1'});
|
|
233
234
|
leases.set('provider-error-lease', lease);
|
|
235
|
+
const providerError = new IntegrationProviderError(
|
|
236
|
+
'provider-rejected',
|
|
237
|
+
'commit_id is missing',
|
|
238
|
+
undefined,
|
|
239
|
+
422,
|
|
240
|
+
);
|
|
234
241
|
const app = await createGatewayApp({
|
|
235
242
|
registry: registryWithAgentTools([catalogTool()], {
|
|
236
|
-
callError:
|
|
243
|
+
callError: providerError,
|
|
237
244
|
}),
|
|
238
245
|
loadLeasedAgentStep: async () => ({
|
|
239
246
|
workspaceId: lease.workspaceId,
|
|
@@ -269,8 +276,8 @@ describe('agent tools gateway route', () => {
|
|
|
269
276
|
|
|
270
277
|
expect(result).toEqual({
|
|
271
278
|
isError: true,
|
|
272
|
-
content: [{type: 'text', text: '
|
|
273
|
-
structuredContent: {code: 'provider-
|
|
279
|
+
content: [{type: 'text', text: 'commit_id is missing'}],
|
|
280
|
+
structuredContent: {code: 'provider-rejected', status: 422},
|
|
274
281
|
});
|
|
275
282
|
});
|
|
276
283
|
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import type {reportError as reportErrorType} from '@shipfox/node-error-monitoring';
|
|
2
|
+
import type {logger as loggerFactoryType} from '@shipfox/node-opentelemetry';
|
|
3
|
+
import {IntegrationProviderError} from '#core/errors.js';
|
|
4
|
+
import {
|
|
5
|
+
catalogTool,
|
|
6
|
+
connection,
|
|
7
|
+
materializedIntegration,
|
|
8
|
+
materializedTool,
|
|
9
|
+
registryWithAgentTools,
|
|
10
|
+
} from '#test/agent-tools-gateway-helpers.js';
|
|
11
|
+
import {createIntegrationToolDispatcher} from './dispatch.js';
|
|
12
|
+
import type {AuthorizedIntegrationTool} from './resolve-authorized-tools.js';
|
|
13
|
+
|
|
14
|
+
const dispatchMocks = vi.hoisted(() => ({
|
|
15
|
+
loggerError: vi.fn(),
|
|
16
|
+
reportError: vi.fn(),
|
|
17
|
+
}));
|
|
18
|
+
|
|
19
|
+
const loggerFactory = (() => ({
|
|
20
|
+
error: dispatchMocks.loggerError,
|
|
21
|
+
})) as unknown as typeof loggerFactoryType;
|
|
22
|
+
const reportError = dispatchMocks.reportError as unknown as typeof reportErrorType;
|
|
23
|
+
|
|
24
|
+
describe('createIntegrationToolDispatcher', () => {
|
|
25
|
+
beforeEach(() => {
|
|
26
|
+
dispatchMocks.loggerError.mockReset();
|
|
27
|
+
dispatchMocks.reportError.mockReset();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('preserves terminal provider errors without reporting them as outages', async () => {
|
|
31
|
+
const providerError = new IntegrationProviderError(
|
|
32
|
+
'provider-rejected',
|
|
33
|
+
'commit_id is missing',
|
|
34
|
+
undefined,
|
|
35
|
+
422,
|
|
36
|
+
);
|
|
37
|
+
const dispatch = createDispatcher(providerError);
|
|
38
|
+
|
|
39
|
+
const result = await dispatch({
|
|
40
|
+
authorizedTool: authorizedTool(),
|
|
41
|
+
arguments: {method: 'get', owner: 'shipfox', repo: 'platform', issue_number: 1},
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
expect(result).toEqual({
|
|
45
|
+
isError: true,
|
|
46
|
+
content: [{type: 'text', text: 'commit_id is missing'}],
|
|
47
|
+
structuredContent: {code: 'provider-rejected', status: 422},
|
|
48
|
+
});
|
|
49
|
+
expect(dispatchMocks.loggerError).not.toHaveBeenCalled();
|
|
50
|
+
expect(dispatchMocks.reportError).not.toHaveBeenCalled();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('reports provider outages while preserving their message and status', async () => {
|
|
54
|
+
const providerError = new IntegrationProviderError(
|
|
55
|
+
'provider-unavailable',
|
|
56
|
+
'GitHub returned HTTP 503',
|
|
57
|
+
undefined,
|
|
58
|
+
503,
|
|
59
|
+
);
|
|
60
|
+
const dispatch = createDispatcher(providerError);
|
|
61
|
+
|
|
62
|
+
const result = await dispatch({
|
|
63
|
+
authorizedTool: authorizedTool(),
|
|
64
|
+
arguments: {method: 'get', owner: 'shipfox', repo: 'platform', issue_number: 1},
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
expect(result).toEqual({
|
|
68
|
+
isError: true,
|
|
69
|
+
content: [{type: 'text', text: 'GitHub returned HTTP 503'}],
|
|
70
|
+
structuredContent: {code: 'provider-unavailable', status: 503},
|
|
71
|
+
});
|
|
72
|
+
expect(dispatchMocks.loggerError).toHaveBeenCalledWith(
|
|
73
|
+
{err: providerError, provider: 'github'},
|
|
74
|
+
'Integration agent tool provider was unavailable',
|
|
75
|
+
);
|
|
76
|
+
expect(dispatchMocks.reportError).toHaveBeenCalledWith(providerError, {
|
|
77
|
+
boundary: 'integration.agent-tool',
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
function createDispatcher(callError: IntegrationProviderError) {
|
|
83
|
+
return createIntegrationToolDispatcher(
|
|
84
|
+
{
|
|
85
|
+
registry: registryWithAgentTools([catalogTool()], {callError}),
|
|
86
|
+
},
|
|
87
|
+
{logger: loggerFactory, reportError},
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function authorizedTool(): AuthorizedIntegrationTool {
|
|
92
|
+
const integration = materializedIntegration({connectionId: 'connection-1'});
|
|
93
|
+
const tool = materializedTool();
|
|
94
|
+
return {
|
|
95
|
+
mcpName: 'github_main__issue_read',
|
|
96
|
+
integration,
|
|
97
|
+
tool,
|
|
98
|
+
connection: connection({
|
|
99
|
+
id: integration.connectionId,
|
|
100
|
+
workspaceId: 'workspace-1',
|
|
101
|
+
slug: integration.connectionSlug,
|
|
102
|
+
}),
|
|
103
|
+
description: 'Read issue metadata from GitHub.',
|
|
104
|
+
inputSchema: tool.inputSchema,
|
|
105
|
+
outputSchema: tool.outputSchema,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
@@ -15,17 +15,33 @@ export interface CreateIntegrationToolDispatcherParams {
|
|
|
15
15
|
registry: IntegrationProviderRegistry;
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
export interface IntegrationToolDispatcherDependencies {
|
|
19
|
+
logger?: typeof logger;
|
|
20
|
+
reportError?: typeof reportError;
|
|
21
|
+
}
|
|
22
|
+
|
|
18
23
|
const timeoutErrorPattern = /timed?\s*out|timeout/i;
|
|
19
24
|
const credentialErrorNamePattern = /Token|Credential|Secret|AccessToken/;
|
|
20
25
|
|
|
21
26
|
export function createIntegrationToolDispatcher(
|
|
22
27
|
params: CreateIntegrationToolDispatcherParams,
|
|
28
|
+
dependencies: IntegrationToolDispatcherDependencies = {},
|
|
23
29
|
): IntegrationToolDispatcher {
|
|
24
|
-
return (input) =>
|
|
30
|
+
return (input) =>
|
|
31
|
+
dispatchIntegrationToolCall({
|
|
32
|
+
...input,
|
|
33
|
+
registry: params.registry,
|
|
34
|
+
logger: dependencies.logger ?? logger,
|
|
35
|
+
reportError: dependencies.reportError ?? reportError,
|
|
36
|
+
});
|
|
25
37
|
}
|
|
26
38
|
|
|
27
39
|
async function dispatchIntegrationToolCall(
|
|
28
|
-
input: IntegrationToolDispatchInput & {
|
|
40
|
+
input: IntegrationToolDispatchInput & {
|
|
41
|
+
registry: IntegrationProviderRegistry;
|
|
42
|
+
logger: typeof logger;
|
|
43
|
+
reportError: typeof reportError;
|
|
44
|
+
},
|
|
29
45
|
): Promise<CallToolResult> {
|
|
30
46
|
let session: AgentToolSession<CallToolResult> | undefined;
|
|
31
47
|
|
|
@@ -52,15 +68,17 @@ async function dispatchIntegrationToolCall(
|
|
|
52
68
|
} catch (error) {
|
|
53
69
|
const result = errorResult(error);
|
|
54
70
|
if (result.code === 'provider-unavailable') {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
71
|
+
input
|
|
72
|
+
.logger()
|
|
73
|
+
.error(
|
|
74
|
+
{err: error, provider: input.authorizedTool.integration.provider},
|
|
75
|
+
'Integration agent tool provider was unavailable',
|
|
76
|
+
);
|
|
77
|
+
input.reportError(error, {boundary: 'integration.agent-tool'});
|
|
60
78
|
}
|
|
61
79
|
return toolError(result);
|
|
62
80
|
} finally {
|
|
63
|
-
await closeSession(session);
|
|
81
|
+
await closeSession(session, input.logger, input.reportError);
|
|
64
82
|
}
|
|
65
83
|
}
|
|
66
84
|
|
|
@@ -90,21 +108,36 @@ function agentToolCatalogEntry(input: IntegrationToolDispatchInput): AgentToolCa
|
|
|
90
108
|
};
|
|
91
109
|
}
|
|
92
110
|
|
|
93
|
-
async function closeSession(
|
|
111
|
+
async function closeSession(
|
|
112
|
+
session: {close?(): Promise<void>} | undefined,
|
|
113
|
+
loggerFactory: typeof logger,
|
|
114
|
+
reportErrorFn: typeof reportError,
|
|
115
|
+
): Promise<void> {
|
|
94
116
|
try {
|
|
95
117
|
await session?.close?.();
|
|
96
118
|
} catch (error) {
|
|
97
119
|
// Cleanup must not mask the tool result returned to the runner.
|
|
98
|
-
|
|
99
|
-
|
|
120
|
+
loggerFactory().error({err: error}, 'Failed to close integration agent tool session');
|
|
121
|
+
reportErrorFn(error, {boundary: 'integration.agent-tool', operation: 'close-session'});
|
|
100
122
|
}
|
|
101
123
|
}
|
|
102
124
|
|
|
103
|
-
|
|
125
|
+
interface IntegrationToolError {
|
|
126
|
+
code: string;
|
|
127
|
+
message: string;
|
|
128
|
+
retryAfterSeconds?: number | undefined;
|
|
129
|
+
status?: number | undefined;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function errorResult(error: unknown): IntegrationToolError {
|
|
104
133
|
if (error instanceof IntegrationProviderError) {
|
|
105
134
|
return {
|
|
106
135
|
code: error.reason,
|
|
107
|
-
message:
|
|
136
|
+
message: error.message,
|
|
137
|
+
...(error.retryAfterSeconds === undefined
|
|
138
|
+
? {}
|
|
139
|
+
: {retryAfterSeconds: error.retryAfterSeconds}),
|
|
140
|
+
...(error.status === undefined ? {} : {status: error.status}),
|
|
108
141
|
};
|
|
109
142
|
}
|
|
110
143
|
|
|
@@ -142,10 +175,16 @@ function isCredentialError(error: unknown): boolean {
|
|
|
142
175
|
return credentialErrorNamePattern.test(error.name);
|
|
143
176
|
}
|
|
144
177
|
|
|
145
|
-
function toolError(params:
|
|
178
|
+
function toolError(params: IntegrationToolError): CallToolResult {
|
|
146
179
|
return {
|
|
147
180
|
isError: true,
|
|
148
181
|
content: [{type: 'text', text: params.message}],
|
|
149
|
-
structuredContent: {
|
|
182
|
+
structuredContent: {
|
|
183
|
+
code: params.code,
|
|
184
|
+
...(params.retryAfterSeconds === undefined
|
|
185
|
+
? {}
|
|
186
|
+
: {retryAfterSeconds: params.retryAfterSeconds}),
|
|
187
|
+
...(params.status === undefined ? {} : {status: params.status}),
|
|
188
|
+
},
|
|
150
189
|
};
|
|
151
190
|
}
|
|
@@ -28,6 +28,7 @@ function isProviderError(error: unknown): error is IntegrationProviderError {
|
|
|
28
28
|
error.reason === 'rate-limited' ||
|
|
29
29
|
error.reason === 'timeout' ||
|
|
30
30
|
error.reason === 'provider-unavailable' ||
|
|
31
|
+
error.reason === 'provider-rejected' ||
|
|
31
32
|
error.reason === 'malformed-provider-response' ||
|
|
32
33
|
error.reason === 'content-too-large' ||
|
|
33
34
|
error.reason === 'too-many-files'))
|