@shipfox/api-agent 12.2.0 → 14.0.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 (132) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +30 -0
  3. package/dist/config.d.ts +5 -1
  4. package/dist/config.d.ts.map +1 -1
  5. package/dist/config.js +75 -7
  6. package/dist/config.js.map +1 -1
  7. package/dist/core/errors.d.ts +4 -0
  8. package/dist/core/errors.d.ts.map +1 -1
  9. package/dist/core/errors.js +6 -0
  10. package/dist/core/errors.js.map +1 -1
  11. package/dist/core/index.d.ts +2 -1
  12. package/dist/core/index.d.ts.map +1 -1
  13. package/dist/core/index.js +2 -1
  14. package/dist/core/index.js.map +1 -1
  15. package/dist/core/model-provider-catalog.d.ts +5 -2
  16. package/dist/core/model-provider-catalog.d.ts.map +1 -1
  17. package/dist/core/model-provider-catalog.js +33 -1
  18. package/dist/core/model-provider-catalog.js.map +1 -1
  19. package/dist/core/resolve-agent-config.d.ts +4 -2
  20. package/dist/core/resolve-agent-config.d.ts.map +1 -1
  21. package/dist/core/resolve-agent-config.js +86 -31
  22. package/dist/core/resolve-agent-config.js.map +1 -1
  23. package/dist/core/resolve-runtime-credentials.d.ts +6 -2
  24. package/dist/core/resolve-runtime-credentials.d.ts.map +1 -1
  25. package/dist/core/resolve-runtime-credentials.js +60 -3
  26. package/dist/core/resolve-runtime-credentials.js.map +1 -1
  27. package/dist/core/validation-catalog.d.ts +2 -1
  28. package/dist/core/validation-catalog.d.ts.map +1 -1
  29. package/dist/core/validation-catalog.js +65 -17
  30. package/dist/core/validation-catalog.js.map +1 -1
  31. package/dist/core/workspace-agent-defaults-resolver.d.ts +2 -1
  32. package/dist/core/workspace-agent-defaults-resolver.d.ts.map +1 -1
  33. package/dist/core/workspace-agent-defaults-resolver.js +4 -2
  34. package/dist/core/workspace-agent-defaults-resolver.js.map +1 -1
  35. package/dist/core/workspace-provider-policy.d.ts +7 -0
  36. package/dist/core/workspace-provider-policy.d.ts.map +1 -0
  37. package/dist/core/workspace-provider-policy.js +10 -0
  38. package/dist/core/workspace-provider-policy.js.map +1 -0
  39. package/dist/index.d.ts +3 -1
  40. package/dist/index.d.ts.map +1 -1
  41. package/dist/index.js +15 -4
  42. package/dist/index.js.map +1 -1
  43. package/dist/presentation/e2eRoutes/create-model-provider.d.ts +2 -1
  44. package/dist/presentation/e2eRoutes/create-model-provider.d.ts.map +1 -1
  45. package/dist/presentation/e2eRoutes/create-model-provider.js +7 -1
  46. package/dist/presentation/e2eRoutes/create-model-provider.js.map +1 -1
  47. package/dist/presentation/e2eRoutes/index.d.ts +2 -1
  48. package/dist/presentation/e2eRoutes/index.d.ts.map +1 -1
  49. package/dist/presentation/e2eRoutes/index.js +4 -2
  50. package/dist/presentation/e2eRoutes/index.js.map +1 -1
  51. package/dist/presentation/inter-module.d.ts +3 -0
  52. package/dist/presentation/inter-module.d.ts.map +1 -1
  53. package/dist/presentation/inter-module.js +22 -6
  54. package/dist/presentation/inter-module.js.map +1 -1
  55. package/dist/presentation/routes/create-custom-model-provider.d.ts +2 -1
  56. package/dist/presentation/routes/create-custom-model-provider.d.ts.map +1 -1
  57. package/dist/presentation/routes/create-custom-model-provider.js +5 -2
  58. package/dist/presentation/routes/create-custom-model-provider.js.map +1 -1
  59. package/dist/presentation/routes/delete-model-provider-config.d.ts +2 -1
  60. package/dist/presentation/routes/delete-model-provider-config.d.ts.map +1 -1
  61. package/dist/presentation/routes/delete-model-provider-config.js +7 -2
  62. package/dist/presentation/routes/delete-model-provider-config.js.map +1 -1
  63. package/dist/presentation/routes/discover-custom-model-provider-models-by-slug.d.ts +2 -1
  64. package/dist/presentation/routes/discover-custom-model-provider-models-by-slug.d.ts.map +1 -1
  65. package/dist/presentation/routes/discover-custom-model-provider-models-by-slug.js +5 -2
  66. package/dist/presentation/routes/discover-custom-model-provider-models-by-slug.js.map +1 -1
  67. package/dist/presentation/routes/discover-custom-model-provider-models.d.ts +2 -0
  68. package/dist/presentation/routes/discover-custom-model-provider-models.d.ts.map +1 -1
  69. package/dist/presentation/routes/discover-custom-model-provider-models.js +29 -23
  70. package/dist/presentation/routes/discover-custom-model-provider-models.js.map +1 -1
  71. package/dist/presentation/routes/errors.d.ts.map +1 -1
  72. package/dist/presentation/routes/errors.js +10 -1
  73. package/dist/presentation/routes/errors.js.map +1 -1
  74. package/dist/presentation/routes/index.d.ts +5 -1
  75. package/dist/presentation/routes/index.d.ts.map +1 -1
  76. package/dist/presentation/routes/index.js +21 -14
  77. package/dist/presentation/routes/index.js.map +1 -1
  78. package/dist/presentation/routes/list-model-provider-catalog.d.ts +5 -0
  79. package/dist/presentation/routes/list-model-provider-catalog.d.ts.map +1 -1
  80. package/dist/presentation/routes/list-model-provider-catalog.js +21 -13
  81. package/dist/presentation/routes/list-model-provider-catalog.js.map +1 -1
  82. package/dist/presentation/routes/set-default-model-provider.d.ts +2 -0
  83. package/dist/presentation/routes/set-default-model-provider.d.ts.map +1 -1
  84. package/dist/presentation/routes/set-default-model-provider.js +44 -38
  85. package/dist/presentation/routes/set-default-model-provider.js.map +1 -1
  86. package/dist/presentation/routes/update-custom-model-provider.d.ts +2 -1
  87. package/dist/presentation/routes/update-custom-model-provider.d.ts.map +1 -1
  88. package/dist/presentation/routes/update-custom-model-provider.js +5 -2
  89. package/dist/presentation/routes/update-custom-model-provider.js.map +1 -1
  90. package/dist/presentation/routes/update-model-provider-default-model.d.ts +2 -0
  91. package/dist/presentation/routes/update-model-provider-default-model.d.ts.map +1 -1
  92. package/dist/presentation/routes/update-model-provider-default-model.js +35 -29
  93. package/dist/presentation/routes/update-model-provider-default-model.js.map +1 -1
  94. package/dist/presentation/routes/upsert-model-provider-config.d.ts +2 -1
  95. package/dist/presentation/routes/upsert-model-provider-config.d.ts.map +1 -1
  96. package/dist/presentation/routes/upsert-model-provider-config.js +5 -2
  97. package/dist/presentation/routes/upsert-model-provider-config.js.map +1 -1
  98. package/dist/tsconfig.test.tsbuildinfo +1 -1
  99. package/package.json +2 -2
  100. package/src/config.test.ts +196 -4
  101. package/src/config.ts +100 -9
  102. package/src/core/errors.ts +7 -0
  103. package/src/core/index.ts +5 -0
  104. package/src/core/model-provider-catalog.test.ts +30 -0
  105. package/src/core/model-provider-catalog.ts +41 -1
  106. package/src/core/resolve-agent-config.test.ts +92 -0
  107. package/src/core/resolve-agent-config.ts +152 -43
  108. package/src/core/resolve-runtime-credentials.test.ts +289 -1
  109. package/src/core/resolve-runtime-credentials.ts +69 -3
  110. package/src/core/validation-catalog.test.ts +54 -0
  111. package/src/core/validation-catalog.ts +124 -19
  112. package/src/core/workspace-agent-defaults-resolver.ts +7 -2
  113. package/src/core/workspace-provider-policy.ts +19 -0
  114. package/src/index.ts +25 -4
  115. package/src/presentation/e2eRoutes/create-model-provider.ts +11 -1
  116. package/src/presentation/e2eRoutes/index.ts +9 -2
  117. package/src/presentation/inter-module.test.ts +51 -0
  118. package/src/presentation/inter-module.ts +38 -6
  119. package/src/presentation/routes/create-custom-model-provider.ts +10 -2
  120. package/src/presentation/routes/delete-model-provider-config.ts +12 -2
  121. package/src/presentation/routes/discover-custom-model-provider-models-by-slug.ts +7 -1
  122. package/src/presentation/routes/discover-custom-model-provider-models.ts +30 -18
  123. package/src/presentation/routes/errors.ts +11 -0
  124. package/src/presentation/routes/index.ts +32 -14
  125. package/src/presentation/routes/list-model-provider-catalog.ts +32 -11
  126. package/src/presentation/routes/model-provider-catalog.test.ts +49 -1
  127. package/src/presentation/routes/model-provider-config.test.ts +62 -1
  128. package/src/presentation/routes/set-default-model-provider.ts +42 -31
  129. package/src/presentation/routes/update-custom-model-provider.ts +10 -2
  130. package/src/presentation/routes/update-model-provider-default-model.ts +37 -26
  131. package/src/presentation/routes/upsert-model-provider-config.ts +10 -2
  132. package/tsconfig.build.tsbuildinfo +1 -1
@@ -1,3 +1,8 @@
1
+ import type {
2
+ HarnessDescriptor,
3
+ ManagedModelProvider,
4
+ WorkspaceProvidersPolicy,
5
+ } from '@shipfox/api-agent-dto';
1
6
  import {
2
7
  listEnabledHarnessTools,
3
8
  listHarnessDescriptors,
@@ -8,27 +13,127 @@ import {harnessToolDeploymentConfig} from '#config.js';
8
13
  import {listHarnessProviderModels} from './harness/index.js';
9
14
  import {getModelProviderEntry} from './model-provider-policy.js';
10
15
 
16
+ type HarnessValidationCatalog = AgentValidationCatalog['harnesses'][number];
17
+
11
18
  /** Produces the versioned, JSON-safe policy snapshot consumed by Definitions. */
12
- export function getAgentValidationCatalog(): AgentValidationCatalog {
19
+ export function getAgentValidationCatalog(
20
+ managedProvider?: ManagedModelProvider | undefined,
21
+ workspaceProviders: WorkspaceProvidersPolicy = 'enabled',
22
+ ): AgentValidationCatalog {
23
+ const managedOnly = workspaceProviders === 'disabled';
24
+
13
25
  return {
14
26
  version: 1,
15
- providers: MODEL_PROVIDER_IDS.map((id) => ({
16
- id,
17
- support_status: getModelProviderEntry(id)?.support_status ?? 'unsupported',
18
- })),
19
- harnesses: listHarnessDescriptors().map((harness) => ({
20
- id: harness.id,
21
- supported_provider_ids: [...harness.supportedProviderIds],
22
- model_ids_by_provider: Object.fromEntries(
23
- harness.supportedProviderIds.map((providerId) => [
24
- providerId,
25
- listHarnessProviderModels(harness.id, providerId).map((model) => model.id),
26
- ]),
27
- ),
28
- thinking_levels: [...harness.thinkingLevels],
29
- effective_tools: listEnabledHarnessTools(harness.id, harnessToolDeploymentConfig).map(
30
- (tool) => tool.name,
31
- ),
32
- })),
27
+ providers: buildValidationProviders(managedProvider, managedOnly),
28
+ harnesses: listHarnessDescriptors().map((harness) =>
29
+ buildHarnessValidationCatalog(harness, managedProvider, managedOnly),
30
+ ),
31
+ };
32
+ }
33
+
34
+ function buildValidationProviders(
35
+ managedProvider: ManagedModelProvider | undefined,
36
+ managedOnly: boolean,
37
+ ): AgentValidationCatalog['providers'] {
38
+ const providers: AgentValidationCatalog['providers'] = [];
39
+
40
+ if (!managedOnly) {
41
+ providers.push(
42
+ ...MODEL_PROVIDER_IDS.map((id) => ({
43
+ id,
44
+ support_status: getModelProviderEntry(id)?.support_status ?? 'unsupported',
45
+ })),
46
+ );
47
+ }
48
+
49
+ if (managedProvider !== undefined) {
50
+ providers.push({id: managedProvider.id, support_status: 'supported'});
51
+ }
52
+
53
+ return providers;
54
+ }
55
+
56
+ function buildHarnessValidationCatalog(
57
+ harness: HarnessDescriptor,
58
+ managedProvider: ManagedModelProvider | undefined,
59
+ managedOnly: boolean,
60
+ ): HarnessValidationCatalog {
61
+ const managedModels = getManagedModelsForHarness(harness, managedProvider);
62
+
63
+ return {
64
+ id: harness.id,
65
+ supported_provider_ids: buildSupportedProviderIds(
66
+ harness,
67
+ managedProvider,
68
+ managedModels,
69
+ managedOnly,
70
+ ),
71
+ model_ids_by_provider: buildModelIdsByProvider(
72
+ harness,
73
+ managedProvider,
74
+ managedModels,
75
+ managedOnly,
76
+ ),
77
+ thinking_levels: [...harness.thinkingLevels],
78
+ effective_tools: listEnabledHarnessTools(harness.id, harnessToolDeploymentConfig).map(
79
+ (tool) => tool.name,
80
+ ),
33
81
  };
34
82
  }
83
+
84
+ function buildSupportedProviderIds(
85
+ harness: HarnessDescriptor,
86
+ managedProvider: ManagedModelProvider | undefined,
87
+ managedModels: ManagedModelProvider['models'],
88
+ managedOnly: boolean,
89
+ ): string[] {
90
+ if (managedOnly) {
91
+ if (managedProvider === undefined || managedModels.length === 0) return [];
92
+ return [managedProvider.id];
93
+ }
94
+
95
+ const providerIds = [...harness.supportedProviderIds];
96
+ if (managedProvider !== undefined && managedModels.length > 0) {
97
+ providerIds.push(managedProvider.id);
98
+ }
99
+ return providerIds;
100
+ }
101
+
102
+ function buildModelIdsByProvider(
103
+ harness: HarnessDescriptor,
104
+ managedProvider: ManagedModelProvider | undefined,
105
+ managedModels: ManagedModelProvider['models'],
106
+ managedOnly: boolean,
107
+ ): Record<string, string[]> {
108
+ const modelIdsByProvider: Record<string, string[]> = {};
109
+
110
+ if (!managedOnly) {
111
+ for (const providerId of harness.supportedProviderIds) {
112
+ modelIdsByProvider[providerId] = listHarnessProviderModels(harness.id, providerId).map(
113
+ (model) => model.id,
114
+ );
115
+ }
116
+ }
117
+
118
+ if (managedProvider !== undefined) {
119
+ modelIdsByProvider[managedProvider.id] = managedModels.map((model) => model.id);
120
+ }
121
+
122
+ return modelIdsByProvider;
123
+ }
124
+
125
+ function getManagedModelsForHarness(
126
+ harness: HarnessDescriptor,
127
+ managedProvider: ManagedModelProvider | undefined,
128
+ ): ManagedModelProvider['models'] {
129
+ if (managedProvider === undefined) return [];
130
+ return managedModelsForHarness(harness.id, managedProvider);
131
+ }
132
+
133
+ function managedModelsForHarness(
134
+ harness: 'pi' | 'claude',
135
+ managedProvider: ManagedModelProvider,
136
+ ): ManagedModelProvider['models'] {
137
+ if (harness === 'pi') return managedProvider.models;
138
+ return managedProvider.models.filter((model) => model.api === 'anthropic-messages');
139
+ }
@@ -1,7 +1,8 @@
1
1
  import type {
2
2
  AgentThinking,
3
+ ManagedModelProvider,
3
4
  ModelProviderRef,
4
- SupportedModelProviderId,
5
+ WorkspaceProvidersPolicy,
5
6
  } from '@shipfox/api-agent-dto';
6
7
  import {config} from '#config.js';
7
8
  import {getAgentWorkspaceDefaultsSnapshot} from '#db/index.js';
@@ -10,6 +11,8 @@ import {resolveAgentConfig} from './resolve-agent-config.js';
10
11
 
11
12
  export async function createWorkspaceAgentDefaultsResolver(
12
13
  workspaceId: string,
14
+ managedProvider?: ManagedModelProvider | undefined,
15
+ workspaceProviders?: WorkspaceProvidersPolicy | undefined,
13
16
  ): Promise<AgentDefaultsResolver> {
14
17
  const snapshot = await getAgentWorkspaceDefaultsSnapshot(workspaceId);
15
18
  const workspaceProviderConfigs = new Map<
@@ -33,9 +36,11 @@ export async function createWorkspaceAgentDefaultsResolver(
33
36
  workspaceDefaultHarnessId: snapshot.defaultHarnessId ?? null,
34
37
  workspaceDefaultProviderId: snapshot.defaultProviderId ?? null,
35
38
  workspaceProviderConfigs,
36
- instanceDefaultProvider: config.AGENT_DEFAULT_PROVIDER as SupportedModelProviderId | undefined,
39
+ instanceDefaultProvider: config.AGENT_DEFAULT_PROVIDER,
37
40
  instanceDefaultModel: config.AGENT_DEFAULT_PROVIDER_MODEL,
38
41
  instanceDefaultThinking: config.AGENT_DEFAULT_PROVIDER_THINKING as AgentThinking | undefined,
42
+ managedProvider,
43
+ workspaceProviders,
39
44
  };
40
45
 
41
46
  return (step) => resolveAgentConfig(step, ctx);
@@ -0,0 +1,19 @@
1
+ import type {WorkspaceProvidersPolicy} from '@shipfox/api-agent-dto';
2
+ import {WorkspaceProvidersDisabledError} from './errors.js';
3
+
4
+ export interface WorkspaceProviderPolicyOptions {
5
+ readonly workspaceProviders: WorkspaceProvidersPolicy;
6
+ readonly managedProviderId?: string | undefined;
7
+ }
8
+
9
+ export function assertWorkspaceProviderConfigurationEnabled(
10
+ options: WorkspaceProviderPolicyOptions,
11
+ ): void {
12
+ if (options.workspaceProviders !== 'disabled') return;
13
+ if (options.managedProviderId === undefined) {
14
+ throw new Error(
15
+ 'workspace provider configuration is disabled but no managed provider is registered',
16
+ );
17
+ }
18
+ throw new WorkspaceProvidersDisabledError(options.managedProviderId);
19
+ }
package/src/index.ts CHANGED
@@ -1,4 +1,6 @@
1
+ import type {ManagedModelProvider} from '@shipfox/api-agent-dto';
1
2
  import type {ShipfoxModule} from '@shipfox/node-module';
3
+ import {assertAgentConfig, workspaceProvidersPolicy} from '#config.js';
2
4
  import type {AgentSecretsClient} from '#core/secrets-client.js';
3
5
  import {db, migrationsPath} from '#db/index.js';
4
6
  import {createAgentE2eRoutes} from '#presentation/e2eRoutes/index.js';
@@ -33,6 +35,7 @@ export {
33
35
  testAndSaveModelProviderConfig,
34
36
  UnsupportedModelProviderError,
35
37
  updateCustomModelProviderConfig,
38
+ WorkspaceProvidersDisabledError,
36
39
  } from '#core/index.js';
37
40
  export {
38
41
  getAgentWorkspaceSettings,
@@ -42,12 +45,30 @@ export {
42
45
  upsertModelProviderConfig,
43
46
  } from '#db/index.js';
44
47
 
45
- export function createAgentModule(params: {secrets: AgentSecretsClient}): ShipfoxModule {
48
+ export function createAgentModule(params: {
49
+ secrets: AgentSecretsClient;
50
+ managedProvider?: ManagedModelProvider | undefined;
51
+ }): ShipfoxModule {
52
+ assertAgentConfig(params.managedProvider);
53
+
46
54
  return {
47
55
  name: 'agent',
48
56
  database: {db, migrationsPath, databaseNamespace: 'agent'},
49
- routes: createAgentRoutes(params.secrets),
50
- e2eRoutes: [createAgentE2eRoutes(params.secrets)],
51
- interModulePresentations: [createAgentInterModulePresentation(params)],
57
+ routes: createAgentRoutes(params.secrets, {
58
+ managedProvider: params.managedProvider,
59
+ workspaceProviders: workspaceProvidersPolicy,
60
+ }),
61
+ e2eRoutes: [
62
+ createAgentE2eRoutes(params.secrets, {
63
+ managedProviderId: params.managedProvider?.id,
64
+ workspaceProviders: workspaceProvidersPolicy,
65
+ }),
66
+ ],
67
+ interModulePresentations: [
68
+ createAgentInterModulePresentation({
69
+ ...params,
70
+ workspaceProviders: workspaceProvidersPolicy,
71
+ }),
72
+ ],
52
73
  };
53
74
  }
@@ -3,9 +3,14 @@ import {ClientError, defineRoute} from '@shipfox/node-fastify';
3
3
  import {z} from 'zod';
4
4
  import {agentSystemNamespace} from '#core/credential-fingerprints.js';
5
5
  import {listHarnessProviderModels} from '#core/harness/registry.js';
6
+ import {
7
+ assertWorkspaceProviderConfigurationEnabled,
8
+ type WorkspaceProviderPolicyOptions,
9
+ } from '#core/index.js';
6
10
  import {getModelProviderEntry} from '#core/model-provider-policy.js';
7
11
  import type {AgentSecretsClient} from '#core/secrets-client.js';
8
12
  import {upsertModelProviderConfig} from '#db/index.js';
13
+ import {translateModelProviderRouteError} from '#presentation/routes/errors.js';
9
14
 
10
15
  const createE2eModelProviderBodySchema = z.object({
11
16
  workspace_id: z.string().uuid(),
@@ -19,7 +24,10 @@ const createE2eModelProviderResponseSchema = z.object({
19
24
  provider_id: z.literal('anthropic'),
20
25
  });
21
26
 
22
- export function createE2eModelProviderRoute(secrets: AgentSecretsClient) {
27
+ export function createE2eModelProviderRoute(
28
+ secrets: AgentSecretsClient,
29
+ workspaceProviderPolicy: WorkspaceProviderPolicyOptions = {workspaceProviders: 'enabled'},
30
+ ) {
23
31
  return defineRoute({
24
32
  method: 'POST',
25
33
  path: '/model-provider',
@@ -28,7 +36,9 @@ export function createE2eModelProviderRoute(secrets: AgentSecretsClient) {
28
36
  body: createE2eModelProviderBodySchema,
29
37
  response: {201: createE2eModelProviderResponseSchema},
30
38
  },
39
+ errorHandler: translateModelProviderRouteError,
31
40
  handler: async (request, reply) => {
41
+ assertWorkspaceProviderConfigurationEnabled(workspaceProviderPolicy);
32
42
  const entry = getModelProviderEntry(request.body.provider_id);
33
43
  if (entry === undefined || entry.default_model === null) {
34
44
  throw new ClientError('Unsupported E2E model provider', 'unsupported-model-provider', {
@@ -1,9 +1,16 @@
1
1
  import type {RouteGroup} from '@shipfox/node-fastify';
2
2
  import type {AgentSecretsClient} from '#core/secrets-client.js';
3
+ import type {WorkspaceProviderPolicyOptions} from '#core/workspace-provider-policy.js';
3
4
  import {createE2eModelProviderRoute} from './create-model-provider.js';
4
5
 
5
- export function createAgentE2eRoutes(secrets: AgentSecretsClient): RouteGroup {
6
- return {prefix: '/agent', routes: [createE2eModelProviderRoute(secrets)]};
6
+ export function createAgentE2eRoutes(
7
+ secrets: AgentSecretsClient,
8
+ workspaceProviderPolicy: WorkspaceProviderPolicyOptions = {workspaceProviders: 'enabled'},
9
+ ): RouteGroup {
10
+ return {
11
+ prefix: '/agent',
12
+ routes: [createE2eModelProviderRoute(secrets, workspaceProviderPolicy)],
13
+ };
7
14
  }
8
15
 
9
16
  export const agentE2eRoutes = createAgentE2eRoutes(undefined as unknown as AgentSecretsClient);
@@ -0,0 +1,51 @@
1
+ import type {ManagedModelProvider} from '@shipfox/api-agent-dto';
2
+ import {agentInterModuleContract} from '@shipfox/api-agent-dto/inter-module';
3
+ import {isInterModuleKnownError} from '@shipfox/inter-module';
4
+ import {agentTestSecretsClient} from '#test/fixtures/secrets-client.js';
5
+ import {createAgentInterModulePresentation} from './inter-module.js';
6
+
7
+ describe('agent inter-module presentation', () => {
8
+ test('preserves managed provider policy details for runtime credentials', async () => {
9
+ const managedProvider: ManagedModelProvider = {
10
+ id: 'shipfox',
11
+ label: 'Shipfox',
12
+ models: [{id: 'managed-model', label: 'Managed model', api: 'openai-responses'}],
13
+ defaultModel: 'managed-model',
14
+ resolveCredentials: vi.fn(),
15
+ };
16
+ const presentation = createAgentInterModulePresentation({
17
+ secrets: agentTestSecretsClient,
18
+ managedProvider,
19
+ workspaceProviders: 'disabled',
20
+ });
21
+ const input = {
22
+ workspaceId: crypto.randomUUID(),
23
+ runId: crypto.randomUUID(),
24
+ stepAttemptId: crypto.randomUUID(),
25
+ harness: 'pi' as const,
26
+ provider: 'anthropic' as const,
27
+ model: 'claude-opus-4-8',
28
+ thinking: 'high' as const,
29
+ };
30
+
31
+ const result = await Promise.resolve(
32
+ presentation.handlers.resolveRuntimeCredentials(input, {
33
+ signal: new AbortController().signal,
34
+ }),
35
+ ).catch((error: unknown) => error);
36
+
37
+ expect(
38
+ isInterModuleKnownError(agentInterModuleContract.methods.resolveRuntimeCredentials, result),
39
+ ).toBe(true);
40
+ if (
41
+ !isInterModuleKnownError(agentInterModuleContract.methods.resolveRuntimeCredentials, result)
42
+ ) {
43
+ throw new Error('Expected a managed provider policy error');
44
+ }
45
+ expect(result.code).toBe('workspace-providers-disabled');
46
+ expect(result.details).toEqual({
47
+ message: 'This instance only supports provider `shipfox`.',
48
+ managed_provider_id: 'shipfox',
49
+ });
50
+ });
51
+ });
@@ -1,3 +1,4 @@
1
+ import type {ManagedModelProvider, WorkspaceProvidersPolicy} from '@shipfox/api-agent-dto';
1
2
  import {agentInterModuleContract} from '@shipfox/api-agent-dto/inter-module';
2
3
  import {secretsInterModuleContract} from '@shipfox/api-secrets-dto/inter-module';
3
4
  import {
@@ -12,6 +13,7 @@ import {
12
13
  UnsupportedHarnessProviderError,
13
14
  UnsupportedHarnessThinkingError,
14
15
  UnsupportedModelProviderError,
16
+ WorkspaceProvidersDisabledError,
15
17
  } from '#core/errors.js';
16
18
  import {resolveAgentConfig} from '#core/resolve-agent-config.js';
17
19
  import {resolveRuntimeCredentials} from '#core/resolve-runtime-credentials.js';
@@ -21,15 +23,26 @@ import {createWorkspaceAgentDefaultsResolver} from '#core/workspace-agent-defaul
21
23
 
22
24
  export function createAgentInterModulePresentation(params: {
23
25
  secrets: AgentSecretsClient;
26
+ managedProvider?: ManagedModelProvider | undefined;
27
+ workspaceProviders?: WorkspaceProvidersPolicy | undefined;
24
28
  }): InterModulePresentation<typeof agentInterModuleContract> {
25
29
  return defineInterModulePresentation(agentInterModuleContract, {
26
- getValidationCatalog: () => getAgentValidationCatalog(),
30
+ getValidationCatalog: () =>
31
+ getAgentValidationCatalog(params.managedProvider, params.workspaceProviders),
27
32
  resolveAgentConfig: async ({workspaceId, config}) => {
28
33
  try {
29
34
  const resolve =
30
35
  workspaceId === null
31
- ? resolveAgentConfig
32
- : await createWorkspaceAgentDefaultsResolver(workspaceId);
36
+ ? (step: Parameters<typeof resolveAgentConfig>[0]) =>
37
+ resolveAgentConfig(step, {
38
+ managedProvider: params.managedProvider,
39
+ workspaceProviders: params.workspaceProviders,
40
+ })
41
+ : await createWorkspaceAgentDefaultsResolver(
42
+ workspaceId,
43
+ params.managedProvider,
44
+ params.workspaceProviders,
45
+ );
33
46
  return await resolve(config);
34
47
  } catch (error) {
35
48
  throw toResolveAgentConfigKnownError(error);
@@ -37,7 +50,11 @@ export function createAgentInterModulePresentation(params: {
37
50
  },
38
51
  resolveRuntimeCredentials: async (input) => {
39
52
  try {
40
- return await resolveRuntimeCredentials(input, {secrets: params.secrets});
53
+ return await resolveRuntimeCredentials(input, {
54
+ managedProvider: params.managedProvider,
55
+ secrets: params.secrets,
56
+ workspaceProviders: params.workspaceProviders,
57
+ });
41
58
  } catch (error) {
42
59
  throw toResolveRuntimeCredentialsKnownError(error);
43
60
  }
@@ -50,18 +67,33 @@ function toResolveAgentConfigKnownError(error: unknown): unknown {
50
67
  error instanceof InvalidAgentModelError ||
51
68
  error instanceof UnsupportedHarnessProviderError ||
52
69
  error instanceof UnsupportedHarnessThinkingError ||
53
- error instanceof UnsupportedModelProviderError
70
+ error instanceof UnsupportedModelProviderError ||
71
+ error instanceof WorkspaceProvidersDisabledError
54
72
  ) {
55
73
  return createInterModuleKnownError(
56
74
  agentInterModuleContract.methods.resolveAgentConfig,
57
75
  'agent-config-invalid',
58
- {},
76
+ {
77
+ ...(error instanceof WorkspaceProvidersDisabledError
78
+ ? {message: error.message, managed_provider_id: error.managedProviderId}
79
+ : {}),
80
+ },
59
81
  );
60
82
  }
61
83
  return error;
62
84
  }
63
85
 
64
86
  function toResolveRuntimeCredentialsKnownError(error: unknown): unknown {
87
+ if (error instanceof WorkspaceProvidersDisabledError) {
88
+ return createInterModuleKnownError(
89
+ agentInterModuleContract.methods.resolveRuntimeCredentials,
90
+ 'workspace-providers-disabled',
91
+ {
92
+ message: error.message,
93
+ managed_provider_id: error.managedProviderId,
94
+ },
95
+ );
96
+ }
65
97
  if (error instanceof ModelProviderConfigNotFoundError) {
66
98
  return createInterModuleKnownError(
67
99
  agentInterModuleContract.methods.resolveRuntimeCredentials,
@@ -5,12 +5,19 @@ import {
5
5
  import {requireWorkspaceAccess} from '@shipfox/api-auth-context';
6
6
  import {defineRoute} from '@shipfox/node-fastify';
7
7
  import {z} from 'zod';
8
- import {createCustomModelProviderConfig} from '#core/index.js';
8
+ import {
9
+ assertWorkspaceProviderConfigurationEnabled,
10
+ createCustomModelProviderConfig,
11
+ } from '#core/index.js';
9
12
  import type {AgentSecretsClient} from '#core/secrets-client.js';
13
+ import type {WorkspaceProviderPolicyOptions} from '#core/workspace-provider-policy.js';
10
14
  import {toCustomModelProviderConfigDto} from '#presentation/dto/index.js';
11
15
  import {translateModelProviderRouteError} from './errors.js';
12
16
 
13
- export function createCustomModelProviderRoute(secrets: AgentSecretsClient) {
17
+ export function createCustomModelProviderRoute(
18
+ secrets: AgentSecretsClient,
19
+ workspaceProviderPolicy: WorkspaceProviderPolicyOptions = {workspaceProviders: 'enabled'},
20
+ ) {
14
21
  return defineRoute({
15
22
  method: 'POST',
16
23
  path: '/custom-model-providers',
@@ -35,6 +42,7 @@ export function createCustomModelProviderRoute(secrets: AgentSecretsClient) {
35
42
  });
36
43
 
37
44
  requireWorkspaceAccess({request, workspaceId});
45
+ assertWorkspaceProviderConfigurationEnabled(workspaceProviderPolicy);
38
46
 
39
47
  const config = await createCustomModelProviderConfig(
40
48
  {
@@ -2,10 +2,18 @@ import {modelProviderRefSchema} from '@shipfox/api-agent-dto';
2
2
  import {requireWorkspaceAccess} from '@shipfox/api-auth-context';
3
3
  import {ClientError, defineRoute} from '@shipfox/node-fastify';
4
4
  import {z} from 'zod';
5
- import {deleteModelProviderConfig} from '#core/index.js';
5
+ import {
6
+ assertWorkspaceProviderConfigurationEnabled,
7
+ deleteModelProviderConfig,
8
+ } from '#core/index.js';
6
9
  import type {AgentSecretsClient} from '#core/secrets-client.js';
10
+ import type {WorkspaceProviderPolicyOptions} from '#core/workspace-provider-policy.js';
11
+ import {translateModelProviderRouteError} from './errors.js';
7
12
 
8
- export function createDeleteModelProviderConfigRoute(secrets: AgentSecretsClient) {
13
+ export function createDeleteModelProviderConfigRoute(
14
+ secrets: AgentSecretsClient,
15
+ workspaceProviderPolicy: WorkspaceProviderPolicyOptions = {workspaceProviders: 'enabled'},
16
+ ) {
9
17
  return defineRoute({
10
18
  method: 'DELETE',
11
19
  path: '/model-providers/:providerId',
@@ -19,9 +27,11 @@ export function createDeleteModelProviderConfigRoute(secrets: AgentSecretsClient
19
27
  204: z.void(),
20
28
  },
21
29
  },
30
+ errorHandler: translateModelProviderRouteError,
22
31
  handler: async (request, reply) => {
23
32
  const {workspaceId, providerId} = request.params;
24
33
  requireWorkspaceAccess({request, workspaceId});
34
+ assertWorkspaceProviderConfigurationEnabled(workspaceProviderPolicy);
25
35
 
26
36
  const deleted = await deleteModelProviderConfig({workspaceId, providerId}, {secrets});
27
37
  if (!deleted) {
@@ -7,13 +7,18 @@ import {requireWorkspaceAccess} from '@shipfox/api-auth-context';
7
7
  import {defineRoute} from '@shipfox/node-fastify';
8
8
  import {z} from 'zod';
9
9
  import {
10
+ assertWorkspaceProviderConfigurationEnabled,
10
11
  discoverCustomModelProviderModels,
11
12
  resolveCustomModelProviderDiscoveryParams,
12
13
  } from '#core/index.js';
13
14
  import type {AgentSecretsClient} from '#core/secrets-client.js';
15
+ import type {WorkspaceProviderPolicyOptions} from '#core/workspace-provider-policy.js';
14
16
  import {translateModelProviderRouteError} from './errors.js';
15
17
 
16
- export function createDiscoverCustomModelProviderModelsBySlugRoute(secrets: AgentSecretsClient) {
18
+ export function createDiscoverCustomModelProviderModelsBySlugRoute(
19
+ secrets: AgentSecretsClient,
20
+ workspaceProviderPolicy: WorkspaceProviderPolicyOptions = {workspaceProviders: 'enabled'},
21
+ ) {
17
22
  return defineRoute({
18
23
  method: 'POST',
19
24
  path: '/custom-model-providers/:slug/discover-models',
@@ -32,6 +37,7 @@ export function createDiscoverCustomModelProviderModelsBySlugRoute(secrets: Agen
32
37
  handler: async (request) => {
33
38
  const {workspaceId, slug} = request.params;
34
39
  requireWorkspaceAccess({request, workspaceId});
40
+ assertWorkspaceProviderConfigurationEnabled(workspaceProviderPolicy);
35
41
 
36
42
  const discoveryParams = await resolveCustomModelProviderDiscoveryParams(
37
43
  {
@@ -5,25 +5,37 @@ import {
5
5
  import {requireWorkspaceAccess} from '@shipfox/api-auth-context';
6
6
  import {defineRoute} from '@shipfox/node-fastify';
7
7
  import {z} from 'zod';
8
- import {discoverCustomModelProviderModels} from '#core/index.js';
8
+ import {
9
+ assertWorkspaceProviderConfigurationEnabled,
10
+ discoverCustomModelProviderModels,
11
+ } from '#core/index.js';
12
+ import type {WorkspaceProviderPolicyOptions} from '#core/workspace-provider-policy.js';
9
13
  import {translateModelProviderRouteError} from './errors.js';
10
14
 
11
- export const discoverCustomModelProviderModelsRoute = defineRoute({
12
- method: 'POST',
13
- path: '/custom-model-providers/discover-models',
14
- description: 'Discover models exposed by a custom model provider endpoint',
15
- schema: {
16
- params: z.object({workspaceId: z.string().uuid()}),
17
- body: discoverCustomModelProviderModelsBodySchema,
18
- response: {
19
- 200: discoverCustomModelProviderModelsResponseSchema,
15
+ export function createDiscoverCustomModelProviderModelsRoute(
16
+ workspaceProviderPolicy: WorkspaceProviderPolicyOptions = {workspaceProviders: 'enabled'},
17
+ ) {
18
+ return defineRoute({
19
+ method: 'POST',
20
+ path: '/custom-model-providers/discover-models',
21
+ description: 'Discover models exposed by a custom model provider endpoint',
22
+ schema: {
23
+ params: z.object({workspaceId: z.string().uuid()}),
24
+ body: discoverCustomModelProviderModelsBodySchema,
25
+ response: {
26
+ 200: discoverCustomModelProviderModelsResponseSchema,
27
+ },
28
+ },
29
+ errorHandler: translateModelProviderRouteError,
30
+ handler: async (request) => {
31
+ const {workspaceId} = request.params;
32
+ requireWorkspaceAccess({request, workspaceId});
33
+ assertWorkspaceProviderConfigurationEnabled(workspaceProviderPolicy);
34
+
35
+ return await discoverCustomModelProviderModels(request.body);
20
36
  },
21
- },
22
- errorHandler: translateModelProviderRouteError,
23
- handler: async (request) => {
24
- const {workspaceId} = request.params;
25
- requireWorkspaceAccess({request, workspaceId});
37
+ });
38
+ }
26
39
 
27
- return await discoverCustomModelProviderModels(request.body);
28
- },
29
- });
40
+ export const discoverCustomModelProviderModelsRoute =
41
+ createDiscoverCustomModelProviderModelsRoute();
@@ -12,6 +12,7 @@ import {
12
12
  ModelProviderConfigNotFoundError,
13
13
  ModelProviderValidationError,
14
14
  UnsupportedModelProviderError,
15
+ WorkspaceProvidersDisabledError,
15
16
  } from '#core/index.js';
16
17
  import {getModelProviderCredentialKeys} from '#core/model-provider-policy.js';
17
18
 
@@ -96,6 +97,16 @@ export function translateModelProviderRouteError(error: unknown): never {
96
97
  });
97
98
  }
98
99
 
100
+ if (error instanceof WorkspaceProvidersDisabledError) {
101
+ throw new ClientError(error.message, 'workspace-providers-disabled', {
102
+ status: 422,
103
+ details: {
104
+ managed_provider_id: error.managedProviderId,
105
+ message: error.message,
106
+ },
107
+ });
108
+ }
109
+
99
110
  if (error instanceof ModelProviderConfigNotFoundError) {
100
111
  throw new ClientError('Provider is not configured', 'provider-not-configured', {
101
112
  status: 422,