@shipfox/api-agent-access 20.2.0 → 20.3.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 +2 -0
- package/.turbo/turbo-type$colon$emit.log +1 -0
- package/.turbo/turbo-type.log +1 -0
- package/CHANGELOG.md +16 -0
- package/dist/core/paged-tools.d.ts +15 -0
- package/dist/core/paged-tools.d.ts.map +1 -0
- package/dist/core/paged-tools.js +454 -0
- package/dist/core/paged-tools.js.map +1 -0
- package/dist/core/response.d.ts +22 -0
- package/dist/core/response.d.ts.map +1 -0
- package/dist/core/response.js +75 -0
- package/dist/core/response.js.map +1 -0
- package/dist/core/tools.d.ts +2 -0
- package/dist/core/tools.d.ts.map +1 -1
- package/dist/core/tools.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/presentation/mcp-server.d.ts.map +1 -1
- package/dist/presentation/mcp-server.js +23 -3
- package/dist/presentation/mcp-server.js.map +1 -1
- package/dist/presentation/routes.d.ts +10 -0
- package/dist/presentation/routes.d.ts.map +1 -1
- package/dist/presentation/routes.js +20 -3
- package/dist/presentation/routes.js.map +1 -1
- package/dist/tsconfig.test.tsbuildinfo +1 -1
- package/package.json +31 -14
- package/src/core/paged-tools.test.ts +519 -0
- package/src/core/paged-tools.ts +657 -0
- package/src/core/response.test.ts +65 -0
- package/src/core/response.ts +101 -0
- package/src/core/tools.ts +2 -0
- package/src/index.ts +11 -0
- package/src/presentation/mcp-server.test.ts +58 -0
- package/src/presentation/mcp-server.ts +25 -3
- package/src/presentation/routes.ts +37 -1
- package/tsconfig.build.tsbuildinfo +1 -1
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import {agentAccessSuccess} from './envelope.js';
|
|
2
|
+
import {
|
|
3
|
+
reducePagedAgentAccessResponse,
|
|
4
|
+
serializedAgentAccessEnvelopeByteLength,
|
|
5
|
+
truncateAgentAccessUtf8,
|
|
6
|
+
} from './response.js';
|
|
7
|
+
|
|
8
|
+
describe('agent-access response bounds', () => {
|
|
9
|
+
test('truncates on a complete UTF-8 code-point boundary', () => {
|
|
10
|
+
const result = truncateAgentAccessUtf8('🙂'.repeat(4), 7);
|
|
11
|
+
|
|
12
|
+
expect(result).toEqual({value: '🙂', truncated: true, totalBytes: 16});
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test('regenerates a page cursor from the last retained item', () => {
|
|
16
|
+
const items = Array.from({length: 20}, (_, index) => ({
|
|
17
|
+
id: `item-${index}-${'x'.repeat(20)}`,
|
|
18
|
+
}));
|
|
19
|
+
const envelope = agentAccessSuccess({items, next_cursor: 'producer-cursor'});
|
|
20
|
+
const initialBytes = serializedAgentAccessEnvelopeByteLength(envelope);
|
|
21
|
+
const reduced = reducePagedAgentAccessResponse({
|
|
22
|
+
envelope,
|
|
23
|
+
itemKey: 'items',
|
|
24
|
+
items,
|
|
25
|
+
cursorForItem: (item) => `cursor:${String(item.id)}`,
|
|
26
|
+
maxBytes: initialBytes - 20,
|
|
27
|
+
});
|
|
28
|
+
if (!reduced.ok) throw new Error('Expected a reduced page');
|
|
29
|
+
const result = reduced.result as {items: Array<{id: string}>; next_cursor: string | null};
|
|
30
|
+
const last = result.items.at(-1);
|
|
31
|
+
|
|
32
|
+
expect(reduced).toMatchObject({
|
|
33
|
+
response_truncated: true,
|
|
34
|
+
response_total_bytes: initialBytes,
|
|
35
|
+
});
|
|
36
|
+
expect(result.items.length).toBeLessThan(items.length);
|
|
37
|
+
expect(last).toBeDefined();
|
|
38
|
+
expect(result.next_cursor).toBe(`cursor:${last?.id}`);
|
|
39
|
+
expect(serializedAgentAccessEnvelopeByteLength(reduced)).toBeLessThanOrEqual(initialBytes - 20);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test('returns a content-too-large error instead of dropping every item', () => {
|
|
43
|
+
const envelope = agentAccessSuccess({
|
|
44
|
+
items: [{id: 'x'.repeat(1_000)}],
|
|
45
|
+
next_cursor: null,
|
|
46
|
+
});
|
|
47
|
+
const initialBytes = serializedAgentAccessEnvelopeByteLength(envelope);
|
|
48
|
+
const emptyTruncatedEnvelope = {
|
|
49
|
+
...envelope,
|
|
50
|
+
result: {items: [], next_cursor: null},
|
|
51
|
+
response_truncated: true,
|
|
52
|
+
response_total_bytes: initialBytes,
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
expect(
|
|
56
|
+
reducePagedAgentAccessResponse({
|
|
57
|
+
envelope,
|
|
58
|
+
itemKey: 'items',
|
|
59
|
+
items: [{id: 'x'.repeat(1_000)}],
|
|
60
|
+
cursorForItem: () => 'cursor',
|
|
61
|
+
maxBytes: serializedAgentAccessEnvelopeByteLength(emptyTruncatedEnvelope),
|
|
62
|
+
}),
|
|
63
|
+
).toEqual({ok: false, error: {code: 'content-too-large'}});
|
|
64
|
+
});
|
|
65
|
+
});
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AGENT_ACCESS_RESPONSE_MAX_BYTES,
|
|
3
|
+
type AgentAccessEnvelopeDto,
|
|
4
|
+
} from '@shipfox/api-agent-access-dto';
|
|
5
|
+
import {agentAccessError} from './envelope.js';
|
|
6
|
+
|
|
7
|
+
const utf8Encoder = new TextEncoder();
|
|
8
|
+
|
|
9
|
+
export interface AgentAccessUtf8Truncation {
|
|
10
|
+
value: string;
|
|
11
|
+
truncated: boolean;
|
|
12
|
+
totalBytes: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function truncateAgentAccessUtf8(
|
|
16
|
+
value: string,
|
|
17
|
+
maxBytes: number,
|
|
18
|
+
): AgentAccessUtf8Truncation {
|
|
19
|
+
const totalBytes = utf8Encoder.encode(value).byteLength;
|
|
20
|
+
if (totalBytes <= maxBytes) return {value, truncated: false, totalBytes};
|
|
21
|
+
if (maxBytes <= 0) return {value: '', truncated: true, totalBytes};
|
|
22
|
+
|
|
23
|
+
let bytes = 0;
|
|
24
|
+
let result = '';
|
|
25
|
+
for (const codePoint of value) {
|
|
26
|
+
const codePointBytes = utf8Encoder.encode(codePoint).byteLength;
|
|
27
|
+
if (bytes + codePointBytes > maxBytes) break;
|
|
28
|
+
result += codePoint;
|
|
29
|
+
bytes += codePointBytes;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return {value: result, truncated: true, totalBytes};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function serializedAgentAccessEnvelopeByteLength(envelope: AgentAccessEnvelopeDto): number {
|
|
36
|
+
const serialized = JSON.stringify(envelope);
|
|
37
|
+
if (serialized === undefined) throw new Error('Agent-access envelope is not serializable');
|
|
38
|
+
return utf8Encoder.encode(serialized).byteLength;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface ReducePagedAgentAccessResponseParams {
|
|
42
|
+
envelope: AgentAccessEnvelopeDto;
|
|
43
|
+
itemKey: string;
|
|
44
|
+
items: readonly Record<string, unknown>[];
|
|
45
|
+
cursorForItem: (item: Record<string, unknown>, index: number) => string;
|
|
46
|
+
maxBytes?: number | undefined;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Fits a paged success response without reusing a producer cursor that points past dropped rows.
|
|
51
|
+
* The cursor is always rebuilt from the final retained item.
|
|
52
|
+
*/
|
|
53
|
+
export function reducePagedAgentAccessResponse(
|
|
54
|
+
params: ReducePagedAgentAccessResponseParams,
|
|
55
|
+
): AgentAccessEnvelopeDto {
|
|
56
|
+
const maxBytes = params.maxBytes ?? AGENT_ACCESS_RESPONSE_MAX_BYTES;
|
|
57
|
+
const initialBytes = serializedAgentAccessEnvelopeByteLength(params.envelope);
|
|
58
|
+
if (initialBytes <= maxBytes) return params.envelope;
|
|
59
|
+
if (!params.envelope.ok || !isRecord(params.envelope.result)) {
|
|
60
|
+
return agentAccessError('content-too-large');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const itemCounts =
|
|
64
|
+
params.items.length === 0
|
|
65
|
+
? [0]
|
|
66
|
+
: Array.from(
|
|
67
|
+
{length: Math.max(0, params.items.length - 1)},
|
|
68
|
+
(_, index) => params.items.length - index - 1,
|
|
69
|
+
);
|
|
70
|
+
for (const itemCount of itemCounts) {
|
|
71
|
+
const retained = params.items.slice(0, itemCount);
|
|
72
|
+
const last = retained.at(-1);
|
|
73
|
+
const nextCursor = last === undefined ? null : params.cursorForItem(last, itemCount - 1);
|
|
74
|
+
const candidate: AgentAccessEnvelopeDto = {
|
|
75
|
+
...params.envelope,
|
|
76
|
+
result: {
|
|
77
|
+
...params.envelope.result,
|
|
78
|
+
[params.itemKey]: retained,
|
|
79
|
+
next_cursor: nextCursor,
|
|
80
|
+
},
|
|
81
|
+
response_truncated: true,
|
|
82
|
+
response_total_bytes: initialBytes,
|
|
83
|
+
};
|
|
84
|
+
if (serializedAgentAccessEnvelopeByteLength(candidate) <= maxBytes) return candidate;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return agentAccessError('content-too-large');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function fitAgentAccessResponseToCeiling(
|
|
91
|
+
envelope: AgentAccessEnvelopeDto,
|
|
92
|
+
maxBytes = AGENT_ACCESS_RESPONSE_MAX_BYTES,
|
|
93
|
+
): AgentAccessEnvelopeDto {
|
|
94
|
+
return serializedAgentAccessEnvelopeByteLength(envelope) <= maxBytes
|
|
95
|
+
? envelope
|
|
96
|
+
: agentAccessError('content-too-large');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
100
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
101
|
+
}
|
package/src/core/tools.ts
CHANGED
|
@@ -17,8 +17,10 @@ export interface AgentAccessTool {
|
|
|
17
17
|
description: string;
|
|
18
18
|
inputSchema: AgentAccessObjectSchema;
|
|
19
19
|
outputSchema: AgentAccessObjectSchema;
|
|
20
|
+
validateInput?: ((input: unknown) => boolean) | undefined;
|
|
20
21
|
annotations: {readonly readOnlyHint: true};
|
|
21
22
|
execute: (call: AgentAccessToolCall) => Promise<AgentAccessEnvelopeDto> | AgentAccessEnvelopeDto;
|
|
23
|
+
validateResult?: ((result: unknown) => boolean) | undefined;
|
|
22
24
|
}
|
|
23
25
|
|
|
24
26
|
export type AgentAccessToolMap = ReadonlyMap<string, AgentAccessTool>;
|
package/src/index.ts
CHANGED
|
@@ -13,12 +13,23 @@ export {
|
|
|
13
13
|
parseAgentAccessEnvelope,
|
|
14
14
|
serializeAgentAccessEnvelope,
|
|
15
15
|
} from '#core/envelope.js';
|
|
16
|
+
export {
|
|
17
|
+
type AgentAccessPagedToolsOptions,
|
|
18
|
+
createAgentAccessTools,
|
|
19
|
+
} from '#core/paged-tools.js';
|
|
16
20
|
export {
|
|
17
21
|
type AgentAccessRateLimitDecision,
|
|
18
22
|
type AgentAccessRateLimiter,
|
|
19
23
|
type CreateAgentAccessRateLimiterOptions,
|
|
20
24
|
createAgentAccessRateLimiter,
|
|
21
25
|
} from '#core/rate-limiter.js';
|
|
26
|
+
export {
|
|
27
|
+
type AgentAccessUtf8Truncation,
|
|
28
|
+
fitAgentAccessResponseToCeiling,
|
|
29
|
+
reducePagedAgentAccessResponse,
|
|
30
|
+
serializedAgentAccessEnvelopeByteLength,
|
|
31
|
+
truncateAgentAccessUtf8,
|
|
32
|
+
} from '#core/response.js';
|
|
22
33
|
export {
|
|
23
34
|
type AgentAccessTool,
|
|
24
35
|
type AgentAccessToolCall,
|
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
import {Client} from '@modelcontextprotocol/sdk/client/index.js';
|
|
2
2
|
import {InMemoryTransport} from '@modelcontextprotocol/sdk/inMemory.js';
|
|
3
3
|
import {CallToolResultSchema} from '@modelcontextprotocol/sdk/types.js';
|
|
4
|
+
import type {AnnotationsInterModuleClient} from '@shipfox/annotations-dto/inter-module';
|
|
4
5
|
import {agentAccessEnvelopeSchema} from '@shipfox/api-agent-access-dto';
|
|
5
6
|
import type {AgentAccessContext} from '@shipfox/api-auth-context';
|
|
7
|
+
import type {DefinitionsInterModuleClient} from '@shipfox/api-definitions-dto/inter-module';
|
|
8
|
+
import type {ProjectsModuleClient} from '@shipfox/api-projects-dto/inter-module';
|
|
9
|
+
import type {TriggersInterModuleClient} from '@shipfox/api-triggers-dto/inter-module';
|
|
10
|
+
import type {WorkflowsModuleClient} from '@shipfox/api-workflows-dto/inter-module';
|
|
6
11
|
import {agentAccessSuccess} from '#core/envelope.js';
|
|
12
|
+
import {createAgentAccessTools} from '#core/paged-tools.js';
|
|
7
13
|
import {createAgentAccessRateLimiter} from '#core/rate-limiter.js';
|
|
8
14
|
import {createAgentAccessFixtureTool} from '#core/tools.js';
|
|
9
15
|
import {AGENT_ACCESS_PACKAGE_VERSION} from '#version.js';
|
|
@@ -71,6 +77,35 @@ describe('buildAgentAccessMcpServer', () => {
|
|
|
71
77
|
]);
|
|
72
78
|
});
|
|
73
79
|
|
|
80
|
+
test('rejects multibyte input at the MCP boundary before calling a producer', async () => {
|
|
81
|
+
const listWorkflowRuns = vi.fn();
|
|
82
|
+
const tool = createAgentAccessTools({
|
|
83
|
+
projects: {} as unknown as ProjectsModuleClient,
|
|
84
|
+
definitions: {} as unknown as DefinitionsInterModuleClient,
|
|
85
|
+
workflows: {listWorkflowRuns} as unknown as WorkflowsModuleClient,
|
|
86
|
+
annotations: {} as unknown as AnnotationsInterModuleClient,
|
|
87
|
+
triggers: {} as unknown as TriggersInterModuleClient,
|
|
88
|
+
}).find((candidate) => candidate.name === 'list_workflow_runs');
|
|
89
|
+
if (!tool) throw new Error('Expected list_workflow_runs tool');
|
|
90
|
+
|
|
91
|
+
const {client, close} = await connectClient(createAgentAccessRateLimiter(), [tool]);
|
|
92
|
+
const result = await client.callTool(
|
|
93
|
+
{
|
|
94
|
+
name: 'list_workflow_runs',
|
|
95
|
+
arguments: {
|
|
96
|
+
project_id: '00000000-0000-4000-8000-000000000001',
|
|
97
|
+
trigger_source: '🙂'.repeat(129),
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
CallToolResultSchema,
|
|
101
|
+
);
|
|
102
|
+
await close();
|
|
103
|
+
|
|
104
|
+
expect(result.isError).toBe(true);
|
|
105
|
+
expect(result.structuredContent).toEqual({ok: false, error: {code: 'invalid-request'}});
|
|
106
|
+
expect(listWorkflowRuns).not.toHaveBeenCalled();
|
|
107
|
+
});
|
|
108
|
+
|
|
74
109
|
test('records only the exception when serializing a tool result fails', async () => {
|
|
75
110
|
const recordCall = vi.fn();
|
|
76
111
|
const fixture = createAgentAccessFixtureTool();
|
|
@@ -125,6 +160,29 @@ describe('buildAgentAccessMcpServer', () => {
|
|
|
125
160
|
]);
|
|
126
161
|
});
|
|
127
162
|
|
|
163
|
+
test('converts an oversized unpaged success into a bounded content-too-large error', async () => {
|
|
164
|
+
const fixture = createAgentAccessFixtureTool();
|
|
165
|
+
const oversizedTool = {
|
|
166
|
+
...fixture,
|
|
167
|
+
name: 'oversized_fixture',
|
|
168
|
+
execute: () => agentAccessSuccess({message: 'x'.repeat(128 * 1024)}),
|
|
169
|
+
};
|
|
170
|
+
const {client, close} = await connectClient(createAgentAccessRateLimiter(), [oversizedTool]);
|
|
171
|
+
|
|
172
|
+
const result = await client.callTool(
|
|
173
|
+
{name: 'oversized_fixture', arguments: {message: 'ignored'}},
|
|
174
|
+
CallToolResultSchema,
|
|
175
|
+
);
|
|
176
|
+
await close();
|
|
177
|
+
|
|
178
|
+
expect(result.isError).toBe(true);
|
|
179
|
+
expect(result.structuredContent).toEqual({
|
|
180
|
+
ok: false,
|
|
181
|
+
error: {code: 'content-too-large'},
|
|
182
|
+
});
|
|
183
|
+
expect(agentAccessEnvelopeSchema.safeParse(result.structuredContent).success).toBe(true);
|
|
184
|
+
});
|
|
185
|
+
|
|
128
186
|
test('does not count tool discovery against the credential window', async () => {
|
|
129
187
|
const limiter = createAgentAccessRateLimiter({limit: 1, now: () => 1_000});
|
|
130
188
|
const {client, close} = await connectClient(limiter);
|
|
@@ -11,6 +11,7 @@ import {logger} from '@shipfox/node-opentelemetry';
|
|
|
11
11
|
import {AGENT_ACCESS_MCP_INSTRUCTIONS, AGENT_ACCESS_MCP_SERVER_NAME} from '#constants.js';
|
|
12
12
|
import {agentAccessError, serializeAgentAccessEnvelope} from '#core/envelope.js';
|
|
13
13
|
import {type AgentAccessRateLimiter, createAgentAccessRateLimiter} from '#core/rate-limiter.js';
|
|
14
|
+
import {fitAgentAccessResponseToCeiling} from '#core/response.js';
|
|
14
15
|
import {
|
|
15
16
|
type AgentAccessTool,
|
|
16
17
|
type AgentAccessToolMap,
|
|
@@ -124,6 +125,16 @@ async function executeAgentAccessTool(params: {
|
|
|
124
125
|
recordCall: AgentAccessToolCallRecorder;
|
|
125
126
|
}): Promise<CallToolResult> {
|
|
126
127
|
try {
|
|
128
|
+
if (params.tool.validateInput?.(params.input) === false) {
|
|
129
|
+
recordToolCall(params.recordCall, {
|
|
130
|
+
tool: params.tool.name,
|
|
131
|
+
outcome: 'invalid-request',
|
|
132
|
+
errorCode: 'invalid-request',
|
|
133
|
+
context: params.context,
|
|
134
|
+
});
|
|
135
|
+
return toolResult(agentAccessError('invalid-request'), true);
|
|
136
|
+
}
|
|
137
|
+
|
|
127
138
|
const response = await params.tool.execute({context: params.context, arguments: params.input});
|
|
128
139
|
const envelope = agentAccessEnvelopeSchema.safeParse(response);
|
|
129
140
|
if (!envelope.success) {
|
|
@@ -136,12 +147,23 @@ async function executeAgentAccessTool(params: {
|
|
|
136
147
|
return toolResult(agentAccessError('invalid-tool-response'), true);
|
|
137
148
|
}
|
|
138
149
|
|
|
139
|
-
|
|
140
|
-
|
|
150
|
+
if (envelope.data.ok && params.tool.validateResult?.(envelope.data.result) === false) {
|
|
151
|
+
recordToolCall(params.recordCall, {
|
|
152
|
+
tool: params.tool.name,
|
|
153
|
+
outcome: 'exception',
|
|
154
|
+
errorCode: 'invalid-tool-response',
|
|
155
|
+
context: params.context,
|
|
156
|
+
});
|
|
157
|
+
return toolResult(agentAccessError('invalid-tool-response'), true);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const boundedEnvelope = fitAgentAccessResponseToCeiling(envelope.data);
|
|
161
|
+
const outcome: AgentAccessToolCallOutcome = boundedEnvelope.ok ? 'success' : 'tool-error';
|
|
162
|
+
const result = toolResult(boundedEnvelope, !boundedEnvelope.ok);
|
|
141
163
|
recordToolCall(params.recordCall, {
|
|
142
164
|
tool: params.tool.name,
|
|
143
165
|
outcome,
|
|
144
|
-
errorCode:
|
|
166
|
+
errorCode: boundedEnvelope.ok ? 'none' : (boundedEnvelope.error?.code ?? 'unknown'),
|
|
145
167
|
context: params.context,
|
|
146
168
|
});
|
|
147
169
|
return result;
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import {StreamableHTTPServerTransport} from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
2
2
|
import type {Transport} from '@modelcontextprotocol/sdk/shared/transport.js';
|
|
3
|
+
import type {AnnotationsInterModuleClient} from '@shipfox/annotations-dto/inter-module';
|
|
3
4
|
import {AUTH_AGENT_ACCESS, requireAgentAccessContext} from '@shipfox/api-auth-context';
|
|
5
|
+
import type {DefinitionsInterModuleClient} from '@shipfox/api-definitions-dto/inter-module';
|
|
6
|
+
import type {ProjectsModuleClient} from '@shipfox/api-projects-dto/inter-module';
|
|
7
|
+
import type {TriggersInterModuleClient} from '@shipfox/api-triggers-dto/inter-module';
|
|
8
|
+
import type {WorkflowsModuleClient} from '@shipfox/api-workflows-dto/inter-module';
|
|
4
9
|
import {reportError} from '@shipfox/node-error-monitoring';
|
|
5
10
|
import {
|
|
6
11
|
ClientError,
|
|
@@ -14,6 +19,7 @@ import {
|
|
|
14
19
|
} from '@shipfox/node-fastify';
|
|
15
20
|
import {logger} from '@shipfox/node-opentelemetry';
|
|
16
21
|
import {AGENT_ACCESS_MCP_PATH, AGENT_ACCESS_PROTECTED_RESOURCE_METADATA_PATH} from '#constants.js';
|
|
22
|
+
import {createAgentAccessTools} from '#core/paged-tools.js';
|
|
17
23
|
import {type AgentAccessRateLimiter, createAgentAccessRateLimiter} from '#core/rate-limiter.js';
|
|
18
24
|
import {type AgentAccessTool, createAgentAccessFixtureTool} from '#core/tools.js';
|
|
19
25
|
import {recordAgentAccessAuthFailure} from '#metrics/index.js';
|
|
@@ -29,10 +35,15 @@ export interface CreateAgentAccessRoutesOptions {
|
|
|
29
35
|
rateLimiter?: AgentAccessRateLimiter | undefined;
|
|
30
36
|
recordCall?: AgentAccessToolCallRecorder | undefined;
|
|
31
37
|
isOriginAllowed?: ((origin: string | undefined) => boolean) | undefined;
|
|
38
|
+
projects?: ProjectsModuleClient | undefined;
|
|
39
|
+
definitions?: DefinitionsInterModuleClient | undefined;
|
|
40
|
+
workflows?: WorkflowsModuleClient | undefined;
|
|
41
|
+
annotations?: AnnotationsInterModuleClient | undefined;
|
|
42
|
+
triggers?: TriggersInterModuleClient | undefined;
|
|
32
43
|
}
|
|
33
44
|
|
|
34
45
|
export function createAgentAccessRoutes(options: CreateAgentAccessRoutesOptions = {}): RouteGroup {
|
|
35
|
-
const tools = options.tools ??
|
|
46
|
+
const tools = options.tools ?? toolsFromProducerClients(options);
|
|
36
47
|
const rateLimiter = options.rateLimiter ?? createAgentAccessRateLimiter();
|
|
37
48
|
const recordCall = options.recordCall ?? createAgentAccessToolCallRecorder();
|
|
38
49
|
const originMatcher = options.isOriginAllowed ?? createAllowedOriginMatcher();
|
|
@@ -107,6 +118,31 @@ export function createAgentAccessRoutes(options: CreateAgentAccessRoutesOptions
|
|
|
107
118
|
};
|
|
108
119
|
}
|
|
109
120
|
|
|
121
|
+
function toolsFromProducerClients(
|
|
122
|
+
options: CreateAgentAccessRoutesOptions,
|
|
123
|
+
): readonly AgentAccessTool[] {
|
|
124
|
+
const {projects, definitions, workflows, annotations, triggers} = options;
|
|
125
|
+
if (
|
|
126
|
+
projects === undefined &&
|
|
127
|
+
definitions === undefined &&
|
|
128
|
+
workflows === undefined &&
|
|
129
|
+
annotations === undefined &&
|
|
130
|
+
triggers === undefined
|
|
131
|
+
) {
|
|
132
|
+
return [createAgentAccessFixtureTool()];
|
|
133
|
+
}
|
|
134
|
+
if (
|
|
135
|
+
projects === undefined ||
|
|
136
|
+
definitions === undefined ||
|
|
137
|
+
workflows === undefined ||
|
|
138
|
+
annotations === undefined ||
|
|
139
|
+
triggers === undefined
|
|
140
|
+
) {
|
|
141
|
+
throw new Error('Agent-access producer clients must be configured together');
|
|
142
|
+
}
|
|
143
|
+
return createAgentAccessTools({projects, definitions, workflows, annotations, triggers});
|
|
144
|
+
}
|
|
145
|
+
|
|
110
146
|
function methodNotAllowed(_request: FastifyRequest, reply: FastifyReply) {
|
|
111
147
|
return reply.code(405).header('allow', 'POST').send({code: 'method-not-allowed'});
|
|
112
148
|
}
|