@shipfox/api-agent 12.0.0 → 13.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 (136) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +29 -0
  3. package/dist/config.d.ts +6 -2
  4. package/dist/config.d.ts.map +1 -1
  5. package/dist/config.js +72 -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 +59 -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/db/db.d.ts +10 -10
  40. package/dist/db/schema/agent-workspace-settings.d.ts +1 -1
  41. package/dist/db/schema/model-provider-configs.d.ts +4 -4
  42. package/dist/index.d.ts +3 -1
  43. package/dist/index.d.ts.map +1 -1
  44. package/dist/index.js +15 -4
  45. package/dist/index.js.map +1 -1
  46. package/dist/metrics/instance.d.ts +4 -4
  47. package/dist/presentation/e2eRoutes/create-model-provider.d.ts +2 -1
  48. package/dist/presentation/e2eRoutes/create-model-provider.d.ts.map +1 -1
  49. package/dist/presentation/e2eRoutes/create-model-provider.js +7 -1
  50. package/dist/presentation/e2eRoutes/create-model-provider.js.map +1 -1
  51. package/dist/presentation/e2eRoutes/index.d.ts +2 -1
  52. package/dist/presentation/e2eRoutes/index.d.ts.map +1 -1
  53. package/dist/presentation/e2eRoutes/index.js +4 -2
  54. package/dist/presentation/e2eRoutes/index.js.map +1 -1
  55. package/dist/presentation/inter-module.d.ts +3 -0
  56. package/dist/presentation/inter-module.d.ts.map +1 -1
  57. package/dist/presentation/inter-module.js +22 -6
  58. package/dist/presentation/inter-module.js.map +1 -1
  59. package/dist/presentation/routes/create-custom-model-provider.d.ts +2 -1
  60. package/dist/presentation/routes/create-custom-model-provider.d.ts.map +1 -1
  61. package/dist/presentation/routes/create-custom-model-provider.js +5 -2
  62. package/dist/presentation/routes/create-custom-model-provider.js.map +1 -1
  63. package/dist/presentation/routes/delete-model-provider-config.d.ts +2 -1
  64. package/dist/presentation/routes/delete-model-provider-config.d.ts.map +1 -1
  65. package/dist/presentation/routes/delete-model-provider-config.js +7 -2
  66. package/dist/presentation/routes/delete-model-provider-config.js.map +1 -1
  67. package/dist/presentation/routes/discover-custom-model-provider-models-by-slug.d.ts +2 -1
  68. package/dist/presentation/routes/discover-custom-model-provider-models-by-slug.d.ts.map +1 -1
  69. package/dist/presentation/routes/discover-custom-model-provider-models-by-slug.js +5 -2
  70. package/dist/presentation/routes/discover-custom-model-provider-models-by-slug.js.map +1 -1
  71. package/dist/presentation/routes/discover-custom-model-provider-models.d.ts +2 -0
  72. package/dist/presentation/routes/discover-custom-model-provider-models.d.ts.map +1 -1
  73. package/dist/presentation/routes/discover-custom-model-provider-models.js +29 -23
  74. package/dist/presentation/routes/discover-custom-model-provider-models.js.map +1 -1
  75. package/dist/presentation/routes/errors.d.ts.map +1 -1
  76. package/dist/presentation/routes/errors.js +10 -1
  77. package/dist/presentation/routes/errors.js.map +1 -1
  78. package/dist/presentation/routes/index.d.ts +5 -1
  79. package/dist/presentation/routes/index.d.ts.map +1 -1
  80. package/dist/presentation/routes/index.js +21 -14
  81. package/dist/presentation/routes/index.js.map +1 -1
  82. package/dist/presentation/routes/list-model-provider-catalog.d.ts +5 -0
  83. package/dist/presentation/routes/list-model-provider-catalog.d.ts.map +1 -1
  84. package/dist/presentation/routes/list-model-provider-catalog.js +21 -13
  85. package/dist/presentation/routes/list-model-provider-catalog.js.map +1 -1
  86. package/dist/presentation/routes/set-default-model-provider.d.ts +2 -0
  87. package/dist/presentation/routes/set-default-model-provider.d.ts.map +1 -1
  88. package/dist/presentation/routes/set-default-model-provider.js +44 -38
  89. package/dist/presentation/routes/set-default-model-provider.js.map +1 -1
  90. package/dist/presentation/routes/update-custom-model-provider.d.ts +2 -1
  91. package/dist/presentation/routes/update-custom-model-provider.d.ts.map +1 -1
  92. package/dist/presentation/routes/update-custom-model-provider.js +5 -2
  93. package/dist/presentation/routes/update-custom-model-provider.js.map +1 -1
  94. package/dist/presentation/routes/update-model-provider-default-model.d.ts +2 -0
  95. package/dist/presentation/routes/update-model-provider-default-model.d.ts.map +1 -1
  96. package/dist/presentation/routes/update-model-provider-default-model.js +35 -29
  97. package/dist/presentation/routes/update-model-provider-default-model.js.map +1 -1
  98. package/dist/presentation/routes/upsert-model-provider-config.d.ts +2 -1
  99. package/dist/presentation/routes/upsert-model-provider-config.d.ts.map +1 -1
  100. package/dist/presentation/routes/upsert-model-provider-config.js +5 -2
  101. package/dist/presentation/routes/upsert-model-provider-config.js.map +1 -1
  102. package/dist/tsconfig.test.tsbuildinfo +1 -1
  103. package/package.json +6 -6
  104. package/src/config.test.ts +165 -4
  105. package/src/config.ts +94 -9
  106. package/src/core/errors.ts +7 -0
  107. package/src/core/index.ts +5 -0
  108. package/src/core/model-provider-catalog.test.ts +30 -0
  109. package/src/core/model-provider-catalog.ts +41 -1
  110. package/src/core/resolve-agent-config.test.ts +92 -0
  111. package/src/core/resolve-agent-config.ts +152 -43
  112. package/src/core/resolve-runtime-credentials.test.ts +169 -1
  113. package/src/core/resolve-runtime-credentials.ts +68 -3
  114. package/src/core/validation-catalog.test.ts +54 -0
  115. package/src/core/validation-catalog.ts +124 -19
  116. package/src/core/workspace-agent-defaults-resolver.ts +7 -2
  117. package/src/core/workspace-provider-policy.ts +19 -0
  118. package/src/index.ts +25 -4
  119. package/src/presentation/e2eRoutes/create-model-provider.ts +11 -1
  120. package/src/presentation/e2eRoutes/index.ts +9 -2
  121. package/src/presentation/inter-module.test.ts +51 -0
  122. package/src/presentation/inter-module.ts +38 -6
  123. package/src/presentation/routes/create-custom-model-provider.ts +10 -2
  124. package/src/presentation/routes/delete-model-provider-config.ts +12 -2
  125. package/src/presentation/routes/discover-custom-model-provider-models-by-slug.ts +7 -1
  126. package/src/presentation/routes/discover-custom-model-provider-models.ts +30 -18
  127. package/src/presentation/routes/errors.ts +11 -0
  128. package/src/presentation/routes/index.ts +32 -14
  129. package/src/presentation/routes/list-model-provider-catalog.ts +32 -11
  130. package/src/presentation/routes/model-provider-catalog.test.ts +49 -1
  131. package/src/presentation/routes/model-provider-config.test.ts +62 -1
  132. package/src/presentation/routes/set-default-model-provider.ts +42 -31
  133. package/src/presentation/routes/update-custom-model-provider.ts +10 -2
  134. package/src/presentation/routes/update-model-provider-default-model.ts +37 -26
  135. package/src/presentation/routes/upsert-model-provider-config.ts +10 -2
  136. package/tsconfig.build.tsbuildinfo +1 -1
@@ -2,12 +2,15 @@ import type {
2
2
  AgentRuntimeCredentialsResponseDto,
3
3
  AgentThinking,
4
4
  Harness,
5
+ ManagedModelProvider,
6
+ ManagedProviderRuntimeConfig,
5
7
  ModelProviderRef,
6
8
  SupportedModelProviderId,
9
+ WorkspaceProvidersPolicy,
7
10
  } from '@shipfox/api-agent-dto';
8
11
  import {secretsInterModuleContract} from '@shipfox/api-secrets-dto/inter-module';
9
12
  import {isInterModuleKnownError} from '@shipfox/inter-module';
10
- import {config} from '#config.js';
13
+ import {config, workspaceProvidersPolicy} from '#config.js';
11
14
  import {getModelProviderConfig} from '#db/index.js';
12
15
  import {agentRuntimeConfigResolvedCount} from '#metrics/index.js';
13
16
  import {
@@ -16,12 +19,14 @@ import {
16
19
  storeValuesToRuntimeCredentials,
17
20
  } from './credential-fingerprints.js';
18
21
  import type {ModelProviderConfig} from './entities/model-provider-config.js';
19
- import {ModelProviderConfigNotFoundError} from './errors.js';
22
+ import {ModelProviderConfigNotFoundError, WorkspaceProvidersDisabledError} from './errors.js';
20
23
  import {getModelProviderEntry, modelProviderCredentialKeysMatch} from './model-provider-policy.js';
21
24
  import {type AgentSecretsClient, requireAgentSecretsClient} from './secrets-client.js';
22
25
 
23
26
  export interface ResolveRuntimeCredentialsParams {
24
27
  workspaceId: string;
28
+ runId: string;
29
+ stepAttemptId: string;
25
30
  harness: Harness;
26
31
  provider: ModelProviderRef;
27
32
  model: string;
@@ -29,13 +34,15 @@ export interface ResolveRuntimeCredentialsParams {
29
34
  }
30
35
 
31
36
  interface RuntimeCredentialsConfig {
32
- AGENT_DEFAULT_PROVIDER?: SupportedModelProviderId | undefined;
37
+ AGENT_DEFAULT_PROVIDER?: ModelProviderRef | undefined;
33
38
  AGENT_DEFAULT_PROVIDER_API_KEY?: string | undefined;
34
39
  }
35
40
 
36
41
  interface ResolveRuntimeCredentialsOptions {
37
42
  runtimeConfig?: RuntimeCredentialsConfig | undefined;
43
+ workspaceProviders?: WorkspaceProvidersPolicy | undefined;
38
44
  secrets?: AgentSecretsClient | undefined;
45
+ managedProvider?: ManagedModelProvider | undefined;
39
46
  getCredentialBag?:
40
47
  | ((params: {workspaceId: string; namespace: string}) => Promise<Record<string, string>>)
41
48
  | undefined;
@@ -46,7 +53,34 @@ export async function resolveRuntimeCredentials(
46
53
  options?: ResolveRuntimeCredentialsOptions,
47
54
  ): Promise<AgentRuntimeCredentialsResponseDto> {
48
55
  const runtimeConfig = options?.runtimeConfig ?? config;
56
+ const configuredWorkspaceProviders = options?.workspaceProviders ?? workspaceProvidersPolicy;
49
57
  const secrets = options?.secrets;
58
+ const managedProvider = options?.managedProvider;
59
+ if (managedProvider?.id === params.provider) {
60
+ const managedRuntimeConfig = await managedProvider.resolveCredentials({
61
+ workspaceId: params.workspaceId,
62
+ runId: params.runId,
63
+ stepAttemptId: params.stepAttemptId,
64
+ model: params.model,
65
+ });
66
+ agentRuntimeConfigResolvedCount.add(1, {source: 'instance', outcome: 'resolved'});
67
+ return toResponse(params, managedRuntimeConfig.credentials, undefined, {
68
+ provider: managedProvider,
69
+ runtimeConfig: managedRuntimeConfig,
70
+ });
71
+ }
72
+
73
+ if (configuredWorkspaceProviders === 'disabled') {
74
+ agentRuntimeConfigResolvedCount.add(1, {
75
+ source: params.provider === runtimeConfig.AGENT_DEFAULT_PROVIDER ? 'instance' : 'workspace',
76
+ outcome: 'unavailable',
77
+ });
78
+ if (managedProvider !== undefined) {
79
+ throw new WorkspaceProvidersDisabledError(managedProvider.id);
80
+ }
81
+ throw new ModelProviderConfigNotFoundError(params.workspaceId, params.provider);
82
+ }
83
+
50
84
  const providerConfig = await getModelProviderConfig({
51
85
  workspaceId: params.workspaceId,
52
86
  providerId: params.provider,
@@ -133,6 +167,12 @@ function toResponse(
133
167
  params: ResolveRuntimeCredentialsParams,
134
168
  credentials: Record<string, string>,
135
169
  providerConfig?: ModelProviderConfig | undefined,
170
+ managed?:
171
+ | {
172
+ provider: ManagedModelProvider;
173
+ runtimeConfig: ManagedProviderRuntimeConfig;
174
+ }
175
+ | undefined,
136
176
  ): AgentRuntimeCredentialsResponseDto {
137
177
  const response: AgentRuntimeCredentialsResponseDto = {
138
178
  harness: params.harness,
@@ -156,5 +196,30 @@ function toResponse(
156
196
  };
157
197
  }
158
198
 
199
+ if (managed !== undefined) {
200
+ const model = managed.provider.models.find((candidate) => candidate.id === params.model);
201
+ const modelDescriptor = {id: params.model, label: model?.label ?? params.model};
202
+
203
+ if (params.harness === 'pi') {
204
+ response.custom_provider = {
205
+ api: managed.runtimeConfig.api,
206
+ base_url: managed.runtimeConfig.baseUrl,
207
+ headers: [],
208
+ secret_header_names: [],
209
+ models: [modelDescriptor],
210
+ requires_api_key: true,
211
+ };
212
+ } else {
213
+ const authToken = managed.runtimeConfig.credentials.api_key;
214
+ if (authToken === undefined) {
215
+ throw new ModelProviderConfigNotFoundError(params.workspaceId, params.provider);
216
+ }
217
+ response.claude = {
218
+ base_url: managed.runtimeConfig.baseUrl,
219
+ auth_token: authToken,
220
+ };
221
+ }
222
+ }
223
+
159
224
  return response;
160
225
  }
@@ -1,3 +1,4 @@
1
+ import type {ManagedModelProvider} from '@shipfox/api-agent-dto';
1
2
  import {getAgentValidationCatalog} from './validation-catalog.js';
2
3
 
3
4
  describe('getAgentValidationCatalog', () => {
@@ -13,4 +14,57 @@ describe('getAgentValidationCatalog', () => {
13
14
  expect(pi?.model_ids_by_provider?.openai).toContain('gpt-5.5-pro');
14
15
  expect(claude?.model_ids_by_provider?.anthropic).toContain('claude-opus-4-8');
15
16
  });
17
+
18
+ it('includes injected managed providers with harness-compatible models', () => {
19
+ const managedProvider = {
20
+ id: 'shipfox-managed',
21
+ label: 'Shipfox Managed',
22
+ models: [
23
+ {id: 'managed-claude', label: 'Managed Claude', api: 'anthropic-messages' as const},
24
+ {id: 'managed-responses', label: 'Managed Responses', api: 'openai-responses' as const},
25
+ ],
26
+ defaultModel: 'managed-claude',
27
+ resolveCredentials: async () => ({
28
+ api: 'anthropic-messages' as const,
29
+ baseUrl: 'https://gateway.example.com',
30
+ credentials: {api_key: 'token'},
31
+ }),
32
+ } satisfies ManagedModelProvider;
33
+
34
+ const catalog = getAgentValidationCatalog(managedProvider);
35
+ const pi = catalog.harnesses.find((harness) => harness.id === 'pi');
36
+ const claude = catalog.harnesses.find((harness) => harness.id === 'claude');
37
+
38
+ expect(catalog.providers).toContainEqual({id: managedProvider.id, support_status: 'supported'});
39
+ expect(pi?.supported_provider_ids).toContain(managedProvider.id);
40
+ expect(pi?.model_ids_by_provider?.[managedProvider.id]).toEqual([
41
+ 'managed-claude',
42
+ 'managed-responses',
43
+ ]);
44
+ expect(claude?.supported_provider_ids).toContain(managedProvider.id);
45
+ expect(claude?.model_ids_by_provider?.[managedProvider.id]).toEqual(['managed-claude']);
46
+ });
47
+
48
+ it('exposes only the managed provider when workspace providers are disabled', () => {
49
+ const managedProvider = {
50
+ id: 'shipfox-managed',
51
+ label: 'Shipfox Managed',
52
+ models: [{id: 'managed-claude', label: 'Managed Claude', api: 'anthropic-messages' as const}],
53
+ defaultModel: 'managed-claude',
54
+ resolveCredentials: async () => ({
55
+ api: 'anthropic-messages' as const,
56
+ baseUrl: 'https://gateway.example.com',
57
+ credentials: {api_key: 'token'},
58
+ }),
59
+ } satisfies ManagedModelProvider;
60
+
61
+ const catalog = getAgentValidationCatalog(managedProvider, 'disabled');
62
+
63
+ expect(catalog.providers).toEqual([{id: managedProvider.id, support_status: 'supported'}]);
64
+ expect(
65
+ catalog.harnesses.every((harness) =>
66
+ harness.supported_provider_ids.every((providerId) => providerId === managedProvider.id),
67
+ ),
68
+ ).toBe(true);
69
+ });
16
70
  });
@@ -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
  {