@shipfox/api-agent 5.0.0 → 7.1.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 +1 -1
- package/CHANGELOG.md +43 -0
- package/dist/core/custom-model-provider-config-service.d.ts +4 -0
- package/dist/core/custom-model-provider-config-service.d.ts.map +1 -1
- package/dist/core/custom-model-provider-config-service.js +36 -17
- package/dist/core/custom-model-provider-config-service.js.map +1 -1
- package/dist/core/model-provider-config-service.d.ts +4 -0
- package/dist/core/model-provider-config-service.d.ts.map +1 -1
- package/dist/core/model-provider-config-service.js +27 -9
- package/dist/core/model-provider-config-service.js.map +1 -1
- package/dist/core/resolve-runtime-credentials.d.ts +6 -2
- package/dist/core/resolve-runtime-credentials.d.ts.map +1 -1
- package/dist/core/resolve-runtime-credentials.js +10 -5
- package/dist/core/resolve-runtime-credentials.js.map +1 -1
- package/dist/core/secrets-client.d.ts +4 -0
- package/dist/core/secrets-client.d.ts.map +1 -0
- package/dist/core/secrets-client.js +6 -0
- package/dist/core/secrets-client.js.map +1 -0
- package/dist/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +19 -13
- package/dist/index.js.map +1 -1
- package/dist/presentation/e2eRoutes/create-model-provider.d.ts +2 -1
- package/dist/presentation/e2eRoutes/create-model-provider.d.ts.map +1 -1
- package/dist/presentation/e2eRoutes/create-model-provider.js +46 -45
- package/dist/presentation/e2eRoutes/create-model-provider.js.map +1 -1
- package/dist/presentation/e2eRoutes/index.d.ts +2 -0
- package/dist/presentation/e2eRoutes/index.d.ts.map +1 -1
- package/dist/presentation/e2eRoutes/index.js +9 -6
- package/dist/presentation/e2eRoutes/index.js.map +1 -1
- package/dist/presentation/index.d.ts +1 -1
- package/dist/presentation/index.d.ts.map +1 -1
- package/dist/presentation/index.js +1 -1
- package/dist/presentation/index.js.map +1 -1
- package/dist/presentation/inter-module.d.ts +7 -0
- package/dist/presentation/inter-module.d.ts.map +1 -0
- package/dist/presentation/inter-module.js +45 -0
- package/dist/presentation/inter-module.js.map +1 -0
- package/dist/presentation/routes/create-custom-model-provider.d.ts +2 -1
- package/dist/presentation/routes/create-custom-model-provider.d.ts.map +1 -1
- package/dist/presentation/routes/create-custom-model-provider.js +39 -35
- package/dist/presentation/routes/create-custom-model-provider.js.map +1 -1
- package/dist/presentation/routes/delete-model-provider-config.d.ts +2 -1
- package/dist/presentation/routes/delete-model-provider-config.d.ts.map +1 -1
- package/dist/presentation/routes/delete-model-provider-config.js +33 -29
- package/dist/presentation/routes/delete-model-provider-config.js.map +1 -1
- package/dist/presentation/routes/discover-custom-model-provider-models-by-slug.d.ts +2 -1
- package/dist/presentation/routes/discover-custom-model-provider-models-by-slug.d.ts.map +1 -1
- package/dist/presentation/routes/discover-custom-model-provider-models-by-slug.js +32 -28
- package/dist/presentation/routes/discover-custom-model-provider-models-by-slug.js.map +1 -1
- package/dist/presentation/routes/errors.d.ts.map +1 -1
- package/dist/presentation/routes/errors.js +13 -12
- package/dist/presentation/routes/errors.js.map +1 -1
- package/dist/presentation/routes/index.d.ts +2 -0
- package/dist/presentation/routes/index.d.ts.map +1 -1
- package/dist/presentation/routes/index.js +32 -29
- package/dist/presentation/routes/index.js.map +1 -1
- package/dist/presentation/routes/update-custom-model-provider.d.ts +2 -1
- package/dist/presentation/routes/update-custom-model-provider.d.ts.map +1 -1
- package/dist/presentation/routes/update-custom-model-provider.js +41 -37
- package/dist/presentation/routes/update-custom-model-provider.js.map +1 -1
- package/dist/presentation/routes/upsert-model-provider-config.d.ts +2 -1
- package/dist/presentation/routes/upsert-model-provider-config.d.ts.map +1 -1
- package/dist/presentation/routes/upsert-model-provider-config.js +46 -42
- package/dist/presentation/routes/upsert-model-provider-config.js.map +1 -1
- package/dist/tsconfig.test.tsbuildinfo +1 -1
- package/package.json +12 -18
- package/src/core/custom-model-provider-config-service.ts +63 -35
- package/src/core/model-provider-config-service.test.ts +1 -3
- package/src/core/model-provider-config-service.ts +31 -16
- package/src/core/resolve-runtime-credentials.test.ts +3 -3
- package/src/core/resolve-runtime-credentials.ts +22 -8
- package/src/core/secrets-client.ts +13 -0
- package/src/index.ts +13 -8
- package/src/presentation/e2eRoutes/create-model-provider.ts +41 -39
- package/src/presentation/e2eRoutes/index.ts +6 -4
- package/src/presentation/index.ts +1 -1
- package/src/presentation/inter-module.ts +78 -0
- package/src/presentation/routes/create-custom-model-provider.ts +36 -30
- package/src/presentation/routes/delete-model-provider-config.ts +25 -22
- package/src/presentation/routes/discover-custom-model-provider-models-by-slug.ts +31 -25
- package/src/presentation/routes/errors.ts +11 -11
- package/src/presentation/routes/index.ts +32 -27
- package/src/presentation/routes/model-provider-config.test.ts +1 -1
- package/src/presentation/routes/update-custom-model-provider.ts +40 -34
- package/src/presentation/routes/upsert-model-provider-config.ts +43 -37
- package/test/fixtures/secrets-client.ts +80 -0
- package/test/globalSetup.ts +0 -9
- package/test/setup.ts +8 -0
- package/tsconfig.build.tsbuildinfo +1 -1
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shipfox/api-agent",
|
|
3
3
|
"license": "MIT",
|
|
4
|
-
"version": "
|
|
4
|
+
"version": "7.1.0",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
7
7
|
"url": "git+https://github.com/ShipfoxHQ/shipfox.git",
|
|
@@ -42,31 +42,25 @@
|
|
|
42
42
|
"@opentelemetry/api": "^1.9.1",
|
|
43
43
|
"drizzle-orm": "^0.45.2",
|
|
44
44
|
"zod": "^4.4.3",
|
|
45
|
-
"@shipfox/
|
|
46
|
-
"@shipfox/api-
|
|
47
|
-
"@shipfox/api-
|
|
45
|
+
"@shipfox/node-error-monitoring": "0.2.0",
|
|
46
|
+
"@shipfox/api-agent-dto": "6.0.0",
|
|
47
|
+
"@shipfox/api-auth-context": "7.1.0",
|
|
48
|
+
"@shipfox/api-secrets-dto": "6.0.0",
|
|
49
|
+
"@shipfox/inter-module": "0.2.0",
|
|
48
50
|
"@shipfox/config": "1.2.2",
|
|
49
|
-
"@shipfox/node-drizzle": "0.3.
|
|
51
|
+
"@shipfox/node-drizzle": "0.3.2",
|
|
50
52
|
"@shipfox/node-egress-guard": "0.1.1",
|
|
51
|
-
"@shipfox/node-fastify": "0.
|
|
52
|
-
"@shipfox/node-module": "0.
|
|
53
|
-
"@shipfox/node-opentelemetry": "0.
|
|
53
|
+
"@shipfox/node-fastify": "0.3.0",
|
|
54
|
+
"@shipfox/node-module": "0.5.0",
|
|
55
|
+
"@shipfox/node-opentelemetry": "0.6.0",
|
|
54
56
|
"@shipfox/node-postgres": "0.4.2",
|
|
55
|
-
"@shipfox/redact": "0.2.
|
|
56
|
-
},
|
|
57
|
-
"devDependencies": {
|
|
58
|
-
"@types/pg": "^8.15.5",
|
|
59
|
-
"drizzle-kit": "^0.31.10",
|
|
60
|
-
"@shipfox/biome": "1.8.2",
|
|
61
|
-
"@shipfox/swc": "1.2.6",
|
|
62
|
-
"@shipfox/ts-config": "1.3.8",
|
|
63
|
-
"@shipfox/typescript": "1.1.7",
|
|
64
|
-
"@shipfox/vitest": "1.2.3"
|
|
57
|
+
"@shipfox/redact": "0.2.3"
|
|
65
58
|
},
|
|
66
59
|
"scripts": {
|
|
67
60
|
"build": "shipfox-swc",
|
|
68
61
|
"check": "shipfox-biome-check",
|
|
69
62
|
"check:fix": "shipfox-biome-check --write",
|
|
63
|
+
"depcruise": "shipfox-depcruise",
|
|
70
64
|
"test": "shipfox-vitest-run",
|
|
71
65
|
"test:watch": "shipfox-vitest-watch",
|
|
72
66
|
"type": "shipfox-tsc-check",
|
|
@@ -10,8 +10,9 @@ import {
|
|
|
10
10
|
type UpdateCustomModelProviderBodyDto,
|
|
11
11
|
type UpdateCustomModelProviderHeaderRequestDto,
|
|
12
12
|
} from '@shipfox/api-agent-dto';
|
|
13
|
-
import {deleteSecrets, getSecretsByNamespace, setSecrets} from '@shipfox/api-secrets';
|
|
14
13
|
import {assertEgressAllowed} from '@shipfox/node-egress-guard';
|
|
14
|
+
import {reportError} from '@shipfox/node-error-monitoring';
|
|
15
|
+
import {logger} from '@shipfox/node-opentelemetry';
|
|
15
16
|
import {
|
|
16
17
|
deleteModelProviderConfig,
|
|
17
18
|
getModelProviderConfig,
|
|
@@ -38,6 +39,7 @@ import {
|
|
|
38
39
|
probeCustomModelProviderCredentials,
|
|
39
40
|
runProviderProbe,
|
|
40
41
|
} from './model-provider-validation.js';
|
|
42
|
+
import {type AgentSecretsClient, requireAgentSecretsClient} from './secrets-client.js';
|
|
41
43
|
|
|
42
44
|
export interface CreateCustomModelProviderConfigParams {
|
|
43
45
|
workspaceId: string;
|
|
@@ -55,6 +57,7 @@ export interface UpdateCustomModelProviderConfigParams {
|
|
|
55
57
|
|
|
56
58
|
export interface CustomModelProviderConfigServiceOptions {
|
|
57
59
|
probe?: typeof probeCustomModelProviderCredentials | undefined;
|
|
60
|
+
secrets?: AgentSecretsClient | undefined;
|
|
58
61
|
}
|
|
59
62
|
|
|
60
63
|
interface SplitHeaders {
|
|
@@ -69,6 +72,7 @@ export async function createCustomModelProviderConfig(
|
|
|
69
72
|
options: CustomModelProviderConfigServiceOptions = {},
|
|
70
73
|
): Promise<ModelProviderConfig> {
|
|
71
74
|
const probe = options.probe ?? probeCustomModelProviderCredentials;
|
|
75
|
+
const secrets = requireAgentSecretsClient(options.secrets);
|
|
72
76
|
|
|
73
77
|
const existing = await getModelProviderConfig({
|
|
74
78
|
workspaceId: params.workspaceId,
|
|
@@ -123,7 +127,7 @@ export async function createCustomModelProviderConfig(
|
|
|
123
127
|
}
|
|
124
128
|
|
|
125
129
|
try {
|
|
126
|
-
await setSecrets({
|
|
130
|
+
await secrets.setSecrets({
|
|
127
131
|
workspaceId: params.workspaceId,
|
|
128
132
|
namespace: agentSystemNamespace(params.body.slug),
|
|
129
133
|
values: customCredentialsToStoreValues(secretCredentials),
|
|
@@ -134,7 +138,7 @@ export async function createCustomModelProviderConfig(
|
|
|
134
138
|
workspaceId: params.workspaceId,
|
|
135
139
|
providerId: params.body.slug,
|
|
136
140
|
});
|
|
137
|
-
await deleteSecrets({
|
|
141
|
+
await secrets.deleteSecrets({
|
|
138
142
|
workspaceId: params.workspaceId,
|
|
139
143
|
namespace: agentSystemNamespace(params.body.slug),
|
|
140
144
|
});
|
|
@@ -147,9 +151,10 @@ export async function updateCustomModelProviderConfig(
|
|
|
147
151
|
options: CustomModelProviderConfigServiceOptions = {},
|
|
148
152
|
): Promise<ModelProviderConfig> {
|
|
149
153
|
const probe = options.probe ?? probeCustomModelProviderCredentials;
|
|
154
|
+
const secrets = requireAgentSecretsClient(options.secrets);
|
|
150
155
|
|
|
151
156
|
const existing = await getExistingCustomConfig(params.workspaceId, params.providerId);
|
|
152
|
-
const existingSecrets = await getCustomProviderSecrets({
|
|
157
|
+
const existingSecrets = await getCustomProviderSecrets(secrets, {
|
|
153
158
|
workspaceId: params.workspaceId,
|
|
154
159
|
providerId: params.providerId,
|
|
155
160
|
});
|
|
@@ -204,7 +209,7 @@ export async function updateCustomModelProviderConfig(
|
|
|
204
209
|
});
|
|
205
210
|
|
|
206
211
|
const namespace = agentSystemNamespace(params.providerId);
|
|
207
|
-
await setSecrets({
|
|
212
|
+
await secrets.setSecrets({
|
|
208
213
|
workspaceId: params.workspaceId,
|
|
209
214
|
namespace,
|
|
210
215
|
values: customCredentialsToStoreValues(newSecretCredentials),
|
|
@@ -227,16 +232,26 @@ export async function updateCustomModelProviderConfig(
|
|
|
227
232
|
defaultThinking: existing.defaultThinking,
|
|
228
233
|
});
|
|
229
234
|
} catch (error) {
|
|
230
|
-
await rollbackCustomProviderSecretUpdate({
|
|
235
|
+
await rollbackCustomProviderSecretUpdate(secrets, {
|
|
231
236
|
workspaceId: params.workspaceId,
|
|
232
237
|
namespace,
|
|
233
238
|
existingSecrets,
|
|
234
239
|
attemptedSecrets: newSecretCredentials,
|
|
235
|
-
}).catch(() =>
|
|
240
|
+
}).catch((rollbackError) => {
|
|
241
|
+
logger().error(
|
|
242
|
+
{err: rollbackError, workspaceId: params.workspaceId, providerId: params.providerId},
|
|
243
|
+
'Failed to roll back custom model provider secrets',
|
|
244
|
+
);
|
|
245
|
+
reportError(rollbackError, {
|
|
246
|
+
boundary: 'agent.cleanup',
|
|
247
|
+
operation: 'rollback-secret-update',
|
|
248
|
+
extra: {workspaceId: params.workspaceId, providerId: params.providerId},
|
|
249
|
+
});
|
|
250
|
+
});
|
|
236
251
|
throw error;
|
|
237
252
|
}
|
|
238
253
|
|
|
239
|
-
await pruneStaleCustomHeaderSecrets({
|
|
254
|
+
await pruneStaleCustomHeaderSecrets(secrets, {
|
|
240
255
|
workspaceId: params.workspaceId,
|
|
241
256
|
providerId: params.providerId,
|
|
242
257
|
existingSecrets,
|
|
@@ -247,13 +262,17 @@ export async function updateCustomModelProviderConfig(
|
|
|
247
262
|
return updated;
|
|
248
263
|
}
|
|
249
264
|
|
|
250
|
-
export async function resolveCustomModelProviderDiscoveryParams(
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
265
|
+
export async function resolveCustomModelProviderDiscoveryParams(
|
|
266
|
+
params: {
|
|
267
|
+
workspaceId: string;
|
|
268
|
+
providerId: ModelProviderRef;
|
|
269
|
+
body: DiscoverCustomModelProviderModelsBySlugBodyDto;
|
|
270
|
+
},
|
|
271
|
+
options: {secrets?: AgentSecretsClient | undefined} = {},
|
|
272
|
+
): Promise<DiscoverCustomModelProviderModelsBodyDto> {
|
|
273
|
+
const secrets = requireAgentSecretsClient(options.secrets);
|
|
255
274
|
const existing = await getExistingCustomConfig(params.workspaceId, params.providerId);
|
|
256
|
-
const existingSecrets = await getCustomProviderSecrets({
|
|
275
|
+
const existingSecrets = await getCustomProviderSecrets(secrets, {
|
|
257
276
|
workspaceId: params.workspaceId,
|
|
258
277
|
providerId: params.providerId,
|
|
259
278
|
});
|
|
@@ -434,11 +453,14 @@ function selectProbeModel(
|
|
|
434
453
|
return model;
|
|
435
454
|
}
|
|
436
455
|
|
|
437
|
-
async function getCustomProviderSecrets(
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
456
|
+
async function getCustomProviderSecrets(
|
|
457
|
+
secrets: AgentSecretsClient,
|
|
458
|
+
params: {
|
|
459
|
+
workspaceId: string;
|
|
460
|
+
providerId: ModelProviderRef;
|
|
461
|
+
},
|
|
462
|
+
): Promise<Record<string, string>> {
|
|
463
|
+
const {values} = await secrets.getSecretsByNamespace({
|
|
442
464
|
workspaceId: params.workspaceId,
|
|
443
465
|
namespace: agentSystemNamespace(params.providerId),
|
|
444
466
|
});
|
|
@@ -474,12 +496,15 @@ function normalizedUrl(value: string): string {
|
|
|
474
496
|
return normalizeCustomProviderBaseUrl(value);
|
|
475
497
|
}
|
|
476
498
|
|
|
477
|
-
async function rollbackCustomProviderSecretUpdate(
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
499
|
+
async function rollbackCustomProviderSecretUpdate(
|
|
500
|
+
secrets: AgentSecretsClient,
|
|
501
|
+
params: {
|
|
502
|
+
workspaceId: string;
|
|
503
|
+
namespace: string;
|
|
504
|
+
existingSecrets: Record<string, string>;
|
|
505
|
+
attemptedSecrets: Record<string, string>;
|
|
506
|
+
},
|
|
507
|
+
): Promise<void> {
|
|
483
508
|
const attemptedKeys = Object.keys(params.attemptedSecrets);
|
|
484
509
|
if (attemptedKeys.length === 0) return;
|
|
485
510
|
|
|
@@ -490,7 +515,7 @@ async function rollbackCustomProviderSecretUpdate(params: {
|
|
|
490
515
|
}),
|
|
491
516
|
);
|
|
492
517
|
if (Object.keys(secretsToRestore).length > 0) {
|
|
493
|
-
await setSecrets({
|
|
518
|
+
await secrets.setSecrets({
|
|
494
519
|
workspaceId: params.workspaceId,
|
|
495
520
|
namespace: params.namespace,
|
|
496
521
|
values: customCredentialsToStoreValues(secretsToRestore),
|
|
@@ -500,20 +525,23 @@ async function rollbackCustomProviderSecretUpdate(params: {
|
|
|
500
525
|
const newKeys = attemptedKeys.filter((key) => params.existingSecrets[key] === undefined);
|
|
501
526
|
if (newKeys.length === 0) return;
|
|
502
527
|
|
|
503
|
-
await deleteSecrets({
|
|
528
|
+
await secrets.deleteSecrets({
|
|
504
529
|
workspaceId: params.workspaceId,
|
|
505
530
|
namespace: params.namespace,
|
|
506
531
|
keys: newKeys.map(customCredentialToStoreKey),
|
|
507
532
|
});
|
|
508
533
|
}
|
|
509
534
|
|
|
510
|
-
async function pruneStaleCustomHeaderSecrets(
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
535
|
+
async function pruneStaleCustomHeaderSecrets(
|
|
536
|
+
secrets: AgentSecretsClient,
|
|
537
|
+
params: {
|
|
538
|
+
workspaceId: string;
|
|
539
|
+
providerId: ModelProviderRef;
|
|
540
|
+
existingSecrets: Record<string, string>;
|
|
541
|
+
newSecrets: Record<string, string>;
|
|
542
|
+
replaceHeaders: boolean;
|
|
543
|
+
},
|
|
544
|
+
): Promise<void> {
|
|
517
545
|
if (!params.replaceHeaders) return;
|
|
518
546
|
|
|
519
547
|
const nextKeys = new Set(Object.keys(params.newSecrets));
|
|
@@ -522,7 +550,7 @@ async function pruneStaleCustomHeaderSecrets(params: {
|
|
|
522
550
|
);
|
|
523
551
|
if (staleKeys.length === 0) return;
|
|
524
552
|
|
|
525
|
-
await deleteSecrets({
|
|
553
|
+
await secrets.deleteSecrets({
|
|
526
554
|
workspaceId: params.workspaceId,
|
|
527
555
|
namespace: agentSystemNamespace(params.providerId),
|
|
528
556
|
keys: staleKeys.map(customCredentialToStoreKey),
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import crypto from 'node:crypto';
|
|
2
|
-
import * as secretStore from '@shipfox/api-secrets';
|
|
3
2
|
import * as modelProviderDb from '#db/index.js';
|
|
4
3
|
import {getModelProviderConfig, upsertModelProviderConfig} from '#db/index.js';
|
|
4
|
+
import {getSecretsByNamespace, setSecrets} from '#test/fixtures/secrets-client.js';
|
|
5
5
|
import {agentSystemNamespace} from './credential-fingerprints.js';
|
|
6
6
|
import {
|
|
7
7
|
createCustomModelProviderConfig,
|
|
@@ -23,8 +23,6 @@ import {
|
|
|
23
23
|
updateModelProviderConfigDefaultModel,
|
|
24
24
|
} from './model-provider-config-service.js';
|
|
25
25
|
|
|
26
|
-
const {getSecretsByNamespace, setSecrets} = secretStore;
|
|
27
|
-
|
|
28
26
|
describe('testAndSaveModelProviderConfig', () => {
|
|
29
27
|
let workspaceId: string;
|
|
30
28
|
|
|
@@ -5,7 +5,8 @@ import {
|
|
|
5
5
|
modelProviderCredentialKeysMatch,
|
|
6
6
|
type SupportedModelProviderId,
|
|
7
7
|
} from '@shipfox/api-agent-dto';
|
|
8
|
-
import {
|
|
8
|
+
import {reportError} from '@shipfox/node-error-monitoring';
|
|
9
|
+
import {logger} from '@shipfox/node-opentelemetry';
|
|
9
10
|
import {
|
|
10
11
|
deleteModelProviderConfig as deleteModelProviderConfigRow,
|
|
11
12
|
getModelProviderConfig,
|
|
@@ -22,6 +23,7 @@ import {
|
|
|
22
23
|
} from './errors.js';
|
|
23
24
|
import {buildModelProviderCatalog} from './model-provider-catalog.js';
|
|
24
25
|
import {probeModelProviderCredentials, runProviderProbe} from './model-provider-validation.js';
|
|
26
|
+
import {type AgentSecretsClient, requireAgentSecretsClient} from './secrets-client.js';
|
|
25
27
|
|
|
26
28
|
export interface TestAndSaveModelProviderConfigParams {
|
|
27
29
|
workspaceId: string;
|
|
@@ -35,6 +37,7 @@ export interface TestAndSaveModelProviderConfigParams {
|
|
|
35
37
|
|
|
36
38
|
export interface TestAndSaveModelProviderConfigOptions {
|
|
37
39
|
probe?: typeof probeModelProviderCredentials;
|
|
40
|
+
secrets?: AgentSecretsClient | undefined;
|
|
38
41
|
pruneStaleSecrets?:
|
|
39
42
|
| ((params: {workspaceId: string; namespace: string; expectedKeys: string[]}) => Promise<void>)
|
|
40
43
|
| undefined;
|
|
@@ -51,6 +54,7 @@ export async function testAndSaveModelProviderConfig(
|
|
|
51
54
|
options: TestAndSaveModelProviderConfigOptions = {},
|
|
52
55
|
): Promise<ModelProviderConfig> {
|
|
53
56
|
const probe = options.probe ?? probeModelProviderCredentials;
|
|
57
|
+
const secrets = requireAgentSecretsClient(options.secrets);
|
|
54
58
|
const entry = getModelProviderEntry(params.providerId);
|
|
55
59
|
if (entry === undefined || entry.support_status !== 'supported') {
|
|
56
60
|
throw new UnsupportedModelProviderError(params.providerId);
|
|
@@ -83,10 +87,11 @@ export async function testAndSaveModelProviderConfig(
|
|
|
83
87
|
secrets: Object.values(params.credentials),
|
|
84
88
|
signal: params.signal,
|
|
85
89
|
});
|
|
86
|
-
const pruneStaleSecrets =
|
|
90
|
+
const pruneStaleSecrets =
|
|
91
|
+
options.pruneStaleSecrets ?? ((input) => pruneStaleProviderCredentialSecrets(secrets, input));
|
|
87
92
|
const namespace = agentSystemNamespace(params.providerId);
|
|
88
93
|
const values = credentialsToStoreValues(params.providerId, params.credentials);
|
|
89
|
-
await setSecrets({
|
|
94
|
+
await secrets.setSecrets({
|
|
90
95
|
workspaceId: params.workspaceId,
|
|
91
96
|
namespace,
|
|
92
97
|
values,
|
|
@@ -105,7 +110,17 @@ export async function testAndSaveModelProviderConfig(
|
|
|
105
110
|
workspaceId: params.workspaceId,
|
|
106
111
|
namespace,
|
|
107
112
|
expectedKeys: Object.keys(values),
|
|
108
|
-
}).catch(() =>
|
|
113
|
+
}).catch((error) => {
|
|
114
|
+
logger().error(
|
|
115
|
+
{err: error, workspaceId: params.workspaceId, providerId: params.providerId},
|
|
116
|
+
'Failed to prune stale model provider secrets',
|
|
117
|
+
);
|
|
118
|
+
reportError(error, {
|
|
119
|
+
boundary: 'agent.cleanup',
|
|
120
|
+
operation: 'prune-stale-secrets',
|
|
121
|
+
extra: {workspaceId: params.workspaceId, providerId: params.providerId},
|
|
122
|
+
});
|
|
123
|
+
});
|
|
109
124
|
|
|
110
125
|
return config;
|
|
111
126
|
}
|
|
@@ -154,12 +169,13 @@ export async function updateModelProviderConfigDefaultModel(
|
|
|
154
169
|
return config;
|
|
155
170
|
}
|
|
156
171
|
|
|
157
|
-
export async function deleteModelProviderConfig(
|
|
158
|
-
workspaceId: string;
|
|
159
|
-
|
|
160
|
-
|
|
172
|
+
export async function deleteModelProviderConfig(
|
|
173
|
+
params: {workspaceId: string; providerId: ModelProviderRef},
|
|
174
|
+
options: {secrets?: AgentSecretsClient | undefined} = {},
|
|
175
|
+
): Promise<boolean> {
|
|
176
|
+
const secrets = requireAgentSecretsClient(options.secrets);
|
|
161
177
|
const deleted = await deleteModelProviderConfigRow(params);
|
|
162
|
-
await deleteSecrets({
|
|
178
|
+
await secrets.deleteSecrets({
|
|
163
179
|
workspaceId: params.workspaceId,
|
|
164
180
|
namespace: agentSystemNamespace(params.providerId),
|
|
165
181
|
});
|
|
@@ -186,12 +202,11 @@ function resolveDefaultModel(
|
|
|
186
202
|
return {probeModel: model, storedModel: requestedModel ?? null};
|
|
187
203
|
}
|
|
188
204
|
|
|
189
|
-
async function pruneStaleProviderCredentialSecrets(
|
|
190
|
-
|
|
191
|
-
namespace: string;
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
const stored = await getSecretsByNamespace({
|
|
205
|
+
async function pruneStaleProviderCredentialSecrets(
|
|
206
|
+
secrets: AgentSecretsClient,
|
|
207
|
+
params: {workspaceId: string; namespace: string; expectedKeys: string[]},
|
|
208
|
+
): Promise<void> {
|
|
209
|
+
const {values: stored} = await secrets.getSecretsByNamespace({
|
|
195
210
|
workspaceId: params.workspaceId,
|
|
196
211
|
namespace: params.namespace,
|
|
197
212
|
});
|
|
@@ -199,7 +214,7 @@ async function pruneStaleProviderCredentialSecrets(params: {
|
|
|
199
214
|
const staleKeys = Object.keys(stored).filter((key) => !expected.has(key));
|
|
200
215
|
if (staleKeys.length === 0) return;
|
|
201
216
|
|
|
202
|
-
await deleteSecrets({
|
|
217
|
+
await secrets.deleteSecrets({
|
|
203
218
|
workspaceId: params.workspaceId,
|
|
204
219
|
namespace: params.namespace,
|
|
205
220
|
keys: staleKeys,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type {ModelProviderRef} from '@shipfox/api-agent-dto';
|
|
2
|
-
import {SecretDecryptionError, setSecrets} from '@shipfox/api-secrets';
|
|
3
2
|
import {deleteModelProviderConfig, upsertModelProviderConfig} from '#db/index.js';
|
|
3
|
+
import {setSecrets} from '#test/fixtures/secrets-client.js';
|
|
4
4
|
import {agentSystemNamespace, customCredentialsToStoreValues} from './credential-fingerprints.js';
|
|
5
5
|
import {ModelProviderConfigNotFoundError} from './errors.js';
|
|
6
6
|
import {resolveRuntimeCredentials} from './resolve-runtime-credentials.js';
|
|
@@ -245,7 +245,7 @@ describe('resolveRuntimeCredentials', () => {
|
|
|
245
245
|
});
|
|
246
246
|
|
|
247
247
|
it('does not expose credential material on store decryption errors', async () => {
|
|
248
|
-
const error = new
|
|
248
|
+
const error = new Error('Secret decryption failed');
|
|
249
249
|
await upsertModelProviderConfig({
|
|
250
250
|
workspaceId,
|
|
251
251
|
providerId: 'anthropic',
|
|
@@ -264,7 +264,7 @@ describe('resolveRuntimeCredentials', () => {
|
|
|
264
264
|
{getCredentialBag: vi.fn().mockRejectedValue(error)},
|
|
265
265
|
);
|
|
266
266
|
|
|
267
|
-
await expect(result).rejects.
|
|
267
|
+
await expect(result).rejects.toBe(error);
|
|
268
268
|
try {
|
|
269
269
|
await result;
|
|
270
270
|
} catch (error) {
|
|
@@ -7,7 +7,8 @@ import {
|
|
|
7
7
|
modelProviderCredentialKeysMatch,
|
|
8
8
|
type SupportedModelProviderId,
|
|
9
9
|
} from '@shipfox/api-agent-dto';
|
|
10
|
-
import {
|
|
10
|
+
import {secretsInterModuleContract} from '@shipfox/api-secrets-dto/inter-module';
|
|
11
|
+
import {isInterModuleKnownError} from '@shipfox/inter-module';
|
|
11
12
|
import {config} from '#config.js';
|
|
12
13
|
import {getModelProviderConfig} from '#db/index.js';
|
|
13
14
|
import {agentRuntimeConfigResolvedCount} from '#metrics/index.js';
|
|
@@ -18,6 +19,7 @@ import {
|
|
|
18
19
|
} from './credential-fingerprints.js';
|
|
19
20
|
import type {ModelProviderConfig} from './entities/model-provider-config.js';
|
|
20
21
|
import {ModelProviderConfigNotFoundError} from './errors.js';
|
|
22
|
+
import {type AgentSecretsClient, requireAgentSecretsClient} from './secrets-client.js';
|
|
21
23
|
|
|
22
24
|
export interface ResolveRuntimeCredentialsParams {
|
|
23
25
|
workspaceId: string;
|
|
@@ -34,7 +36,10 @@ interface RuntimeCredentialsConfig {
|
|
|
34
36
|
|
|
35
37
|
interface ResolveRuntimeCredentialsOptions {
|
|
36
38
|
runtimeConfig?: RuntimeCredentialsConfig | undefined;
|
|
37
|
-
|
|
39
|
+
secrets?: AgentSecretsClient | undefined;
|
|
40
|
+
getCredentialBag?:
|
|
41
|
+
| ((params: {workspaceId: string; namespace: string}) => Promise<Record<string, string>>)
|
|
42
|
+
| undefined;
|
|
38
43
|
}
|
|
39
44
|
|
|
40
45
|
export async function resolveRuntimeCredentials(
|
|
@@ -42,7 +47,7 @@ export async function resolveRuntimeCredentials(
|
|
|
42
47
|
options?: ResolveRuntimeCredentialsOptions,
|
|
43
48
|
): Promise<AgentRuntimeCredentialsResponseDto> {
|
|
44
49
|
const runtimeConfig = options?.runtimeConfig ?? config;
|
|
45
|
-
const
|
|
50
|
+
const secrets = options?.secrets;
|
|
46
51
|
const providerConfig = await getModelProviderConfig({
|
|
47
52
|
workspaceId: params.workspaceId,
|
|
48
53
|
providerId: params.provider,
|
|
@@ -50,10 +55,17 @@ export async function resolveRuntimeCredentials(
|
|
|
50
55
|
|
|
51
56
|
if (providerConfig) {
|
|
52
57
|
try {
|
|
53
|
-
const values =
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
58
|
+
const values = options?.getCredentialBag
|
|
59
|
+
? await options.getCredentialBag({
|
|
60
|
+
workspaceId: params.workspaceId,
|
|
61
|
+
namespace: agentSystemNamespace(params.provider),
|
|
62
|
+
})
|
|
63
|
+
: (
|
|
64
|
+
await requireAgentSecretsClient(secrets).getSecretsByNamespace({
|
|
65
|
+
workspaceId: params.workspaceId,
|
|
66
|
+
namespace: agentSystemNamespace(params.provider),
|
|
67
|
+
})
|
|
68
|
+
).values;
|
|
57
69
|
if (providerConfig.kind === 'custom') {
|
|
58
70
|
const credentials = storeValuesToCustomRuntimeCredentials(values);
|
|
59
71
|
agentRuntimeConfigResolvedCount.add(1, {source: 'workspace', outcome: 'resolved'});
|
|
@@ -68,7 +80,9 @@ export async function resolveRuntimeCredentials(
|
|
|
68
80
|
agentRuntimeConfigResolvedCount.add(1, {source: 'workspace', outcome: 'resolved'});
|
|
69
81
|
return toResponse(params, credentials, providerConfig);
|
|
70
82
|
} catch (error) {
|
|
71
|
-
if (
|
|
83
|
+
if (
|
|
84
|
+
isInterModuleKnownError(secretsInterModuleContract.methods.getSecretsByNamespace, error)
|
|
85
|
+
) {
|
|
72
86
|
agentRuntimeConfigResolvedCount.add(1, {
|
|
73
87
|
source: 'workspace',
|
|
74
88
|
outcome: 'decryption_failed',
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type {SecretsInterModuleClient} from '@shipfox/api-secrets-dto/inter-module';
|
|
2
|
+
|
|
3
|
+
export type AgentSecretsClient = Pick<
|
|
4
|
+
SecretsInterModuleClient,
|
|
5
|
+
'deleteSecrets' | 'getSecretsByNamespace' | 'setSecrets'
|
|
6
|
+
>;
|
|
7
|
+
|
|
8
|
+
export function requireAgentSecretsClient(
|
|
9
|
+
secrets: AgentSecretsClient | undefined,
|
|
10
|
+
): AgentSecretsClient {
|
|
11
|
+
if (secrets) return secrets;
|
|
12
|
+
throw new Error('Agent Secrets client is not configured.');
|
|
13
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import type {ShipfoxModule} from '@shipfox/node-module';
|
|
2
|
+
import type {AgentSecretsClient} from '#core/secrets-client.js';
|
|
2
3
|
import {db, migrationsPath} from '#db/index.js';
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
4
|
+
import {createAgentE2eRoutes} from '#presentation/e2eRoutes/index.js';
|
|
5
|
+
import {createAgentInterModulePresentation} from '#presentation/inter-module.js';
|
|
6
|
+
import {createAgentRoutes} from '#presentation/routes/index.js';
|
|
5
7
|
|
|
6
8
|
export {
|
|
7
9
|
type AgentConfigResolutionContext,
|
|
@@ -35,9 +37,12 @@ export {
|
|
|
35
37
|
upsertModelProviderConfig,
|
|
36
38
|
} from '#db/index.js';
|
|
37
39
|
|
|
38
|
-
export
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
40
|
+
export function createAgentModule(params: {secrets: AgentSecretsClient}): ShipfoxModule {
|
|
41
|
+
return {
|
|
42
|
+
name: 'agent',
|
|
43
|
+
database: {db, migrationsPath},
|
|
44
|
+
routes: createAgentRoutes(params.secrets),
|
|
45
|
+
e2eRoutes: [createAgentE2eRoutes(params.secrets)],
|
|
46
|
+
interModulePresentations: [createAgentInterModulePresentation(params)],
|
|
47
|
+
};
|
|
48
|
+
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import {DEFAULT_AGENT_THINKING, getModelProviderEntry} from '@shipfox/api-agent-dto';
|
|
2
|
-
import {setSecrets} from '@shipfox/api-secrets';
|
|
3
2
|
import {ClientError, defineRoute} from '@shipfox/node-fastify';
|
|
4
3
|
import {z} from 'zod';
|
|
5
4
|
import {agentSystemNamespace} from '#core/credential-fingerprints.js';
|
|
6
5
|
import {listHarnessProviderModels} from '#core/harness/registry.js';
|
|
6
|
+
import type {AgentSecretsClient} from '#core/secrets-client.js';
|
|
7
7
|
import {upsertModelProviderConfig} from '#db/index.js';
|
|
8
8
|
|
|
9
9
|
const createE2eModelProviderBodySchema = z.object({
|
|
@@ -18,44 +18,46 @@ const createE2eModelProviderResponseSchema = z.object({
|
|
|
18
18
|
provider_id: z.literal('anthropic'),
|
|
19
19
|
});
|
|
20
20
|
|
|
21
|
-
export
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
21
|
+
export function createE2eModelProviderRoute(secrets: AgentSecretsClient) {
|
|
22
|
+
return defineRoute({
|
|
23
|
+
method: 'POST',
|
|
24
|
+
path: '/model-provider',
|
|
25
|
+
description: 'Create an Anthropic model provider config for E2E tests.',
|
|
26
|
+
schema: {
|
|
27
|
+
body: createE2eModelProviderBodySchema,
|
|
28
|
+
response: {201: createE2eModelProviderResponseSchema},
|
|
29
|
+
},
|
|
30
|
+
handler: async (request, reply) => {
|
|
31
|
+
const entry = getModelProviderEntry(request.body.provider_id);
|
|
32
|
+
if (entry === undefined || entry.default_model === null) {
|
|
33
|
+
throw new ClientError('Unsupported E2E model provider', 'unsupported-model-provider', {
|
|
34
|
+
status: 400,
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
const defaultModel = request.body.default_model ?? entry.default_model;
|
|
38
|
+
const supportedModels = listHarnessProviderModels('claude', request.body.provider_id);
|
|
39
|
+
if (!supportedModels.some((model) => model.id === defaultModel)) {
|
|
40
|
+
throw new ClientError('Unsupported Anthropic default model', 'unsupported-model', {
|
|
41
|
+
status: 400,
|
|
42
|
+
details: {default_model: defaultModel},
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
await secrets.setSecrets({
|
|
47
|
+
workspaceId: request.body.workspace_id,
|
|
48
|
+
namespace: agentSystemNamespace(request.body.provider_id),
|
|
49
|
+
values: {API_KEY: request.body.api_key},
|
|
34
50
|
});
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
details: {default_model: defaultModel},
|
|
51
|
+
await upsertModelProviderConfig({
|
|
52
|
+
workspaceId: request.body.workspace_id,
|
|
53
|
+
providerId: request.body.provider_id,
|
|
54
|
+
defaultModel,
|
|
55
|
+
defaultThinking: DEFAULT_AGENT_THINKING,
|
|
56
|
+
setAsDefault: request.body.set_as_default,
|
|
42
57
|
});
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
await setSecrets({
|
|
46
|
-
workspaceId: request.body.workspace_id,
|
|
47
|
-
namespace: agentSystemNamespace(request.body.provider_id),
|
|
48
|
-
values: {API_KEY: request.body.api_key},
|
|
49
|
-
});
|
|
50
|
-
await upsertModelProviderConfig({
|
|
51
|
-
workspaceId: request.body.workspace_id,
|
|
52
|
-
providerId: request.body.provider_id,
|
|
53
|
-
defaultModel,
|
|
54
|
-
defaultThinking: DEFAULT_AGENT_THINKING,
|
|
55
|
-
setAsDefault: request.body.set_as_default,
|
|
56
|
-
});
|
|
57
58
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
});
|
|
59
|
+
reply.code(201);
|
|
60
|
+
return {provider_id: request.body.provider_id};
|
|
61
|
+
},
|
|
62
|
+
});
|
|
63
|
+
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import type {RouteGroup} from '@shipfox/node-fastify';
|
|
2
|
+
import type {AgentSecretsClient} from '#core/secrets-client.js';
|
|
2
3
|
import {createE2eModelProviderRoute} from './create-model-provider.js';
|
|
3
4
|
|
|
4
|
-
export
|
|
5
|
-
prefix: '/agent',
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
export function createAgentE2eRoutes(secrets: AgentSecretsClient): RouteGroup {
|
|
6
|
+
return {prefix: '/agent', routes: [createE2eModelProviderRoute(secrets)]};
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export const agentE2eRoutes = createAgentE2eRoutes(undefined as unknown as AgentSecretsClient);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export {agentRoutes as routes} from './routes/index.js';
|
|
1
|
+
export {agentRoutes as routes, createAgentRoutes} from './routes/index.js';
|