@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.
Files changed (90) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +43 -0
  3. package/dist/core/custom-model-provider-config-service.d.ts +4 -0
  4. package/dist/core/custom-model-provider-config-service.d.ts.map +1 -1
  5. package/dist/core/custom-model-provider-config-service.js +36 -17
  6. package/dist/core/custom-model-provider-config-service.js.map +1 -1
  7. package/dist/core/model-provider-config-service.d.ts +4 -0
  8. package/dist/core/model-provider-config-service.d.ts.map +1 -1
  9. package/dist/core/model-provider-config-service.js +27 -9
  10. package/dist/core/model-provider-config-service.js.map +1 -1
  11. package/dist/core/resolve-runtime-credentials.d.ts +6 -2
  12. package/dist/core/resolve-runtime-credentials.d.ts.map +1 -1
  13. package/dist/core/resolve-runtime-credentials.js +10 -5
  14. package/dist/core/resolve-runtime-credentials.js.map +1 -1
  15. package/dist/core/secrets-client.d.ts +4 -0
  16. package/dist/core/secrets-client.d.ts.map +1 -0
  17. package/dist/core/secrets-client.js +6 -0
  18. package/dist/core/secrets-client.js.map +1 -0
  19. package/dist/index.d.ts +4 -1
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js +19 -13
  22. package/dist/index.js.map +1 -1
  23. package/dist/presentation/e2eRoutes/create-model-provider.d.ts +2 -1
  24. package/dist/presentation/e2eRoutes/create-model-provider.d.ts.map +1 -1
  25. package/dist/presentation/e2eRoutes/create-model-provider.js +46 -45
  26. package/dist/presentation/e2eRoutes/create-model-provider.js.map +1 -1
  27. package/dist/presentation/e2eRoutes/index.d.ts +2 -0
  28. package/dist/presentation/e2eRoutes/index.d.ts.map +1 -1
  29. package/dist/presentation/e2eRoutes/index.js +9 -6
  30. package/dist/presentation/e2eRoutes/index.js.map +1 -1
  31. package/dist/presentation/index.d.ts +1 -1
  32. package/dist/presentation/index.d.ts.map +1 -1
  33. package/dist/presentation/index.js +1 -1
  34. package/dist/presentation/index.js.map +1 -1
  35. package/dist/presentation/inter-module.d.ts +7 -0
  36. package/dist/presentation/inter-module.d.ts.map +1 -0
  37. package/dist/presentation/inter-module.js +45 -0
  38. package/dist/presentation/inter-module.js.map +1 -0
  39. package/dist/presentation/routes/create-custom-model-provider.d.ts +2 -1
  40. package/dist/presentation/routes/create-custom-model-provider.d.ts.map +1 -1
  41. package/dist/presentation/routes/create-custom-model-provider.js +39 -35
  42. package/dist/presentation/routes/create-custom-model-provider.js.map +1 -1
  43. package/dist/presentation/routes/delete-model-provider-config.d.ts +2 -1
  44. package/dist/presentation/routes/delete-model-provider-config.d.ts.map +1 -1
  45. package/dist/presentation/routes/delete-model-provider-config.js +33 -29
  46. package/dist/presentation/routes/delete-model-provider-config.js.map +1 -1
  47. package/dist/presentation/routes/discover-custom-model-provider-models-by-slug.d.ts +2 -1
  48. package/dist/presentation/routes/discover-custom-model-provider-models-by-slug.d.ts.map +1 -1
  49. package/dist/presentation/routes/discover-custom-model-provider-models-by-slug.js +32 -28
  50. package/dist/presentation/routes/discover-custom-model-provider-models-by-slug.js.map +1 -1
  51. package/dist/presentation/routes/errors.d.ts.map +1 -1
  52. package/dist/presentation/routes/errors.js +13 -12
  53. package/dist/presentation/routes/errors.js.map +1 -1
  54. package/dist/presentation/routes/index.d.ts +2 -0
  55. package/dist/presentation/routes/index.d.ts.map +1 -1
  56. package/dist/presentation/routes/index.js +32 -29
  57. package/dist/presentation/routes/index.js.map +1 -1
  58. package/dist/presentation/routes/update-custom-model-provider.d.ts +2 -1
  59. package/dist/presentation/routes/update-custom-model-provider.d.ts.map +1 -1
  60. package/dist/presentation/routes/update-custom-model-provider.js +41 -37
  61. package/dist/presentation/routes/update-custom-model-provider.js.map +1 -1
  62. package/dist/presentation/routes/upsert-model-provider-config.d.ts +2 -1
  63. package/dist/presentation/routes/upsert-model-provider-config.d.ts.map +1 -1
  64. package/dist/presentation/routes/upsert-model-provider-config.js +46 -42
  65. package/dist/presentation/routes/upsert-model-provider-config.js.map +1 -1
  66. package/dist/tsconfig.test.tsbuildinfo +1 -1
  67. package/package.json +12 -18
  68. package/src/core/custom-model-provider-config-service.ts +63 -35
  69. package/src/core/model-provider-config-service.test.ts +1 -3
  70. package/src/core/model-provider-config-service.ts +31 -16
  71. package/src/core/resolve-runtime-credentials.test.ts +3 -3
  72. package/src/core/resolve-runtime-credentials.ts +22 -8
  73. package/src/core/secrets-client.ts +13 -0
  74. package/src/index.ts +13 -8
  75. package/src/presentation/e2eRoutes/create-model-provider.ts +41 -39
  76. package/src/presentation/e2eRoutes/index.ts +6 -4
  77. package/src/presentation/index.ts +1 -1
  78. package/src/presentation/inter-module.ts +78 -0
  79. package/src/presentation/routes/create-custom-model-provider.ts +36 -30
  80. package/src/presentation/routes/delete-model-provider-config.ts +25 -22
  81. package/src/presentation/routes/discover-custom-model-provider-models-by-slug.ts +31 -25
  82. package/src/presentation/routes/errors.ts +11 -11
  83. package/src/presentation/routes/index.ts +32 -27
  84. package/src/presentation/routes/model-provider-config.test.ts +1 -1
  85. package/src/presentation/routes/update-custom-model-provider.ts +40 -34
  86. package/src/presentation/routes/upsert-model-provider-config.ts +43 -37
  87. package/test/fixtures/secrets-client.ts +80 -0
  88. package/test/globalSetup.ts +0 -9
  89. package/test/setup.ts +8 -0
  90. 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": "5.0.0",
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/api-agent-dto": "5.0.0",
46
- "@shipfox/api-auth-context": "5.0.0",
47
- "@shipfox/api-secrets": "5.0.0",
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.1",
51
+ "@shipfox/node-drizzle": "0.3.2",
50
52
  "@shipfox/node-egress-guard": "0.1.1",
51
- "@shipfox/node-fastify": "0.2.3",
52
- "@shipfox/node-module": "0.3.2",
53
- "@shipfox/node-opentelemetry": "0.5.2",
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.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(() => undefined);
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(params: {
251
- workspaceId: string;
252
- providerId: ModelProviderRef;
253
- body: DiscoverCustomModelProviderModelsBySlugBodyDto;
254
- }): Promise<DiscoverCustomModelProviderModelsBodyDto> {
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(params: {
438
- workspaceId: string;
439
- providerId: ModelProviderRef;
440
- }): Promise<Record<string, string>> {
441
- const values = await getSecretsByNamespace({
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(params: {
478
- workspaceId: string;
479
- namespace: string;
480
- existingSecrets: Record<string, string>;
481
- attemptedSecrets: Record<string, string>;
482
- }): Promise<void> {
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(params: {
511
- workspaceId: string;
512
- providerId: ModelProviderRef;
513
- existingSecrets: Record<string, string>;
514
- newSecrets: Record<string, string>;
515
- replaceHeaders: boolean;
516
- }): Promise<void> {
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 {deleteSecrets, getSecretsByNamespace, setSecrets} from '@shipfox/api-secrets';
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 = options.pruneStaleSecrets ?? pruneStaleProviderCredentialSecrets;
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(() => undefined);
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(params: {
158
- workspaceId: string;
159
- providerId: ModelProviderRef;
160
- }): Promise<boolean> {
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(params: {
190
- workspaceId: string;
191
- namespace: string;
192
- expectedKeys: string[];
193
- }): Promise<void> {
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 SecretDecryptionError();
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.toThrow(SecretDecryptionError);
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 {getSecretsByNamespace, SecretDecryptionError} from '@shipfox/api-secrets';
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
- getCredentialBag?: typeof getSecretsByNamespace | undefined;
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 getCredentialBag = options?.getCredentialBag ?? getSecretsByNamespace;
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 = await getCredentialBag({
54
- workspaceId: params.workspaceId,
55
- namespace: agentSystemNamespace(params.provider),
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 (error instanceof SecretDecryptionError) {
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 {agentE2eRoutes} from '#presentation/e2eRoutes/index.js';
4
- import {routes} from '#presentation/index.js';
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 const agentModule: ShipfoxModule = {
39
- name: 'agent',
40
- database: {db, migrationsPath},
41
- routes,
42
- e2eRoutes: [agentE2eRoutes],
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 const createE2eModelProviderRoute = defineRoute({
22
- method: 'POST',
23
- path: '/model-provider',
24
- description: 'Create an Anthropic model provider config for E2E tests.',
25
- schema: {
26
- body: createE2eModelProviderBodySchema,
27
- response: {201: createE2eModelProviderResponseSchema},
28
- },
29
- handler: async (request, reply) => {
30
- const entry = getModelProviderEntry(request.body.provider_id);
31
- if (entry === undefined || entry.default_model === null) {
32
- throw new ClientError('Unsupported E2E model provider', 'unsupported-model-provider', {
33
- status: 400,
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
- const defaultModel = request.body.default_model ?? entry.default_model;
37
- const supportedModels = listHarnessProviderModels('claude', request.body.provider_id);
38
- if (!supportedModels.some((model) => model.id === defaultModel)) {
39
- throw new ClientError('Unsupported Anthropic default model', 'unsupported-model', {
40
- status: 400,
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
- reply.code(201);
59
- return {provider_id: request.body.provider_id};
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 const agentE2eRoutes: RouteGroup = {
5
- prefix: '/agent',
6
- routes: [createE2eModelProviderRoute],
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';