@shipfox/api-workflows 13.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.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@shipfox/api-workflows",
3
3
  "license": "MIT",
4
- "version": "13.0.0",
4
+ "version": "13.1.0",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "git+https://github.com/ShipfoxHQ/shipfox.git",
@@ -25,14 +25,14 @@
25
25
  "zod": "^4.4.3",
26
26
  "@shipfox/api-auth-context": "12.2.0",
27
27
  "@shipfox/api-auth-dto": "12.0.0",
28
- "@shipfox/api-agent-dto": "12.2.0",
28
+ "@shipfox/api-agent-dto": "13.1.0",
29
29
  "@shipfox/annotations-dto": "12.3.0",
30
30
  "@shipfox/api-definitions-dto": "12.3.0",
31
31
  "@shipfox/api-projects-dto": "12.2.0",
32
32
  "@shipfox/api-integration-core-dto": "12.2.0",
33
33
  "@shipfox/api-runners-dto": "13.0.0",
34
34
  "@shipfox/api-secrets-dto": "12.0.0",
35
- "@shipfox/api-workflows-dto": "13.0.0",
35
+ "@shipfox/api-workflows-dto": "13.1.0",
36
36
  "@shipfox/api-workspaces-dto": "12.0.0",
37
37
  "@shipfox/config": "1.2.4",
38
38
  "@shipfox/inter-module": "0.2.3",
@@ -38,13 +38,28 @@ export class WorkspaceNotFoundError extends Error {
38
38
  }
39
39
  }
40
40
 
41
+ export interface AgentConfigUnresolvableErrorOptions {
42
+ readonly cause?: unknown;
43
+ readonly message?: string;
44
+ readonly code?: string;
45
+ readonly managedProviderId?: string;
46
+ }
47
+
41
48
  export class AgentConfigUnresolvableError extends Error {
49
+ readonly code?: string | undefined;
50
+ readonly managedProviderId?: string | undefined;
51
+
42
52
  constructor(
43
53
  readonly definitionId: string,
44
- options?: ErrorOptions | undefined,
54
+ options?: AgentConfigUnresolvableErrorOptions | undefined,
45
55
  ) {
46
- super(`Agent configuration cannot be resolved for definition ${definitionId}`, options);
56
+ super(
57
+ options?.message ?? `Agent configuration cannot be resolved for definition ${definitionId}`,
58
+ options?.cause === undefined ? undefined : {cause: options.cause},
59
+ );
47
60
  this.name = 'AgentConfigUnresolvableError';
61
+ this.code = options?.code;
62
+ this.managedProviderId = options?.managedProviderId;
48
63
  }
49
64
  }
50
65
 
@@ -1,3 +1,7 @@
1
+ import {
2
+ type AgentInterModuleClient,
3
+ agentInterModuleContract,
4
+ } from '@shipfox/api-agent-dto/inter-module';
1
5
  import {
2
6
  WORKFLOWS_JOB_STEPS_SETTLED,
3
7
  WORKFLOWS_STEP_ATTEMPT_TERMINATED,
@@ -10,6 +14,7 @@ import {
10
14
  parseWorkflowTemplate,
11
15
  planInterpolationField,
12
16
  } from '@shipfox/expression';
17
+ import {createInterModuleKnownError} from '@shipfox/inter-module';
13
18
  import {and, eq, sql} from 'drizzle-orm';
14
19
  import {db} from '#db/db.js';
15
20
  import {workflowsOutbox} from '#db/schema/outbox.js';
@@ -322,6 +327,55 @@ describe('nextStepForJob', () => {
322
327
  });
323
328
  });
324
329
 
330
+ test('reports managed provider policy failures as agent config errors', async () => {
331
+ const {jobId, steps} = await arrangeJobWithSteps(1);
332
+ const pending = steps[0];
333
+ if (!pending) throw new Error('Expected pending step');
334
+ await db()
335
+ .update(stepsTable)
336
+ .set({
337
+ config: {},
338
+ configPlan: {
339
+ agent: {
340
+ prompt: {segments: [{kind: 'literal', value: 'Review the change.'}]},
341
+ },
342
+ },
343
+ })
344
+ .where(eq(stepsTable.id, pending.id));
345
+
346
+ const agent = {
347
+ getValidationCatalog: vi.fn(),
348
+ resolveAgentConfig: vi.fn().mockRejectedValue(
349
+ createInterModuleKnownError(
350
+ agentInterModuleContract.methods.resolveAgentConfig,
351
+ 'agent-config-invalid',
352
+ {
353
+ message: 'This instance only supports provider `shipfox`.',
354
+ managed_provider_id: 'shipfox',
355
+ },
356
+ ),
357
+ ),
358
+ resolveRuntimeCredentials: vi.fn(),
359
+ } as unknown as AgentInterModuleClient;
360
+
361
+ const next = await nextStepForJob(jobId, agent);
362
+
363
+ expect(next).toEqual({kind: 'done', status: 'failed'});
364
+ const after = await getStepsByJobId(jobId);
365
+ expect(after[0]).toMatchObject({
366
+ status: 'failed',
367
+ error: {
368
+ message: 'This instance only supports provider `shipfox`.',
369
+ reason: 'agent_config_invalid',
370
+ field: 'agent',
371
+ source: 'agent',
372
+ code: 'workspace-providers-disabled',
373
+ managedProviderId: 'shipfox',
374
+ agentConfigIssue: 'provider_unsupported',
375
+ },
376
+ });
377
+ });
378
+
325
379
  test('all steps succeeded → {done, succeeded}', async () => {
326
380
  const {jobId, steps} = await arrangeJobWithSteps(2);
327
381
  for (const step of steps) {
@@ -339,11 +339,16 @@ function dispatchConfigError(error: DispatchConfigError): Record<string, unknown
339
339
  };
340
340
  }
341
341
 
342
+ const isManagedProviderPolicyFailure =
343
+ error.code === 'workspace-providers-disabled' && error.managedProviderId !== undefined;
342
344
  return {
343
345
  message: error.message,
344
- reason: 'config_unresolvable',
346
+ reason: isManagedProviderPolicyFailure ? 'agent_config_invalid' : 'config_unresolvable',
345
347
  field: 'agent',
346
348
  source: 'agent',
349
+ ...(error.code === undefined ? {} : {code: error.code}),
350
+ ...(error.managedProviderId === undefined ? {} : {managedProviderId: error.managedProviderId}),
351
+ ...(isManagedProviderPolicyFailure ? {agentConfigIssue: 'provider_unsupported'} : {}),
347
352
  };
348
353
  }
349
354
 
@@ -185,7 +185,14 @@ export async function completeAgentDefaults(params: {
185
185
  isInterModuleKnownError(agentInterModuleContract.methods.resolveAgentConfig, error) &&
186
186
  error.code === 'agent-config-invalid'
187
187
  ) {
188
- throw new AgentConfigUnresolvableError(params.definitionId, {cause: error});
188
+ const managedProviderId = error.details.managed_provider_id;
189
+ throw new AgentConfigUnresolvableError(params.definitionId, {
190
+ cause: error,
191
+ ...(error.details.message === undefined ? {} : {message: error.details.message}),
192
+ ...(managedProviderId === undefined
193
+ ? {}
194
+ : {code: 'workspace-providers-disabled', managedProviderId}),
195
+ });
189
196
  }
190
197
  throw error;
191
198
  }
@@ -453,6 +453,43 @@ describe('completeStepDispatchConfig', () => {
453
453
  await expect(act()).rejects.toThrow(AgentConfigUnresolvableError);
454
454
  });
455
455
 
456
+ it('preserves managed provider policy details in unresolvable agent config errors', async () => {
457
+ const pending = step({
458
+ type: 'agent',
459
+ config: {},
460
+ configPlan: {
461
+ agent: {
462
+ prompt: plannedField('Review the change.'),
463
+ },
464
+ },
465
+ });
466
+ const failingResolver: AgentDefaultsResolver = () => {
467
+ throw createInterModuleKnownError(
468
+ agentInterModuleContract.methods.resolveAgentConfig,
469
+ 'agent-config-invalid',
470
+ {
471
+ message: 'This instance only supports provider `shipfox`.',
472
+ managed_provider_id: 'shipfox',
473
+ },
474
+ );
475
+ };
476
+
477
+ const act = () =>
478
+ completeStepDispatchConfig({
479
+ step: pending,
480
+ context,
481
+ resolveAgentDefaults: failingResolver,
482
+ definitionId: 'def-1',
483
+ });
484
+
485
+ await expect(act()).rejects.toMatchObject({
486
+ name: 'AgentConfigUnresolvableError',
487
+ message: 'This instance only supports provider `shipfox`.',
488
+ code: 'workspace-providers-disabled',
489
+ managedProviderId: 'shipfox',
490
+ });
491
+ });
492
+
456
493
  it('throws when a server-side segment still survives dispatch', async () => {
457
494
  const pending = step({
458
495
  config: {},
@@ -43,12 +43,16 @@ describe('fromStepErrorDto', () => {
43
43
  it('persists the machine-readable agent config issue in camelCase', () => {
44
44
  const persisted = fromStepErrorDto({
45
45
  message: 'Missing credentials',
46
+ code: 'workspace-providers-disabled',
47
+ managed_provider_id: 'shipfox',
46
48
  reason: 'agent_config_invalid',
47
49
  agent_config_issue: 'provider_not_configured',
48
50
  });
49
51
 
50
52
  expect(persisted).toEqual({
51
53
  message: 'Missing credentials',
54
+ code: 'workspace-providers-disabled',
55
+ managedProviderId: 'shipfox',
52
56
  reason: 'agent_config_invalid',
53
57
  agentConfigIssue: 'provider_not_configured',
54
58
  });
@@ -180,6 +184,8 @@ describe('toStepDto error category', () => {
180
184
  type: 'agent',
181
185
  error: {
182
186
  message: 'Unknown provider "foo" for agent step.',
187
+ code: 'workspace-providers-disabled',
188
+ managedProviderId: 'shipfox',
183
189
  reason: 'agent_config_invalid',
184
190
  agentConfigIssue: 'provider_unsupported',
185
191
  },
@@ -188,6 +194,8 @@ describe('toStepDto error category', () => {
188
194
 
189
195
  expect(dto.error).toEqual({
190
196
  message: 'Unknown provider "foo" for agent step.',
197
+ code: 'workspace-providers-disabled',
198
+ managed_provider_id: 'shipfox',
191
199
  reason: 'agent_config_invalid',
192
200
  agent_config_issue: 'provider_unsupported',
193
201
  category: 'user',
@@ -22,6 +22,9 @@ function toStepErrorDto(
22
22
  ): StepErrorDto {
23
23
  if (error === null) return null;
24
24
  const message = typeof error.message === 'string' ? error.message : '';
25
+ const code = typeof error.code === 'string' ? error.code : undefined;
26
+ const managedProviderId =
27
+ typeof error.managedProviderId === 'string' ? error.managedProviderId : undefined;
25
28
  const exitCode = error.exitCode;
26
29
  const signal = typeof error.signal === 'string' ? error.signal : undefined;
27
30
  const field = typeof error.field === 'string' ? error.field : undefined;
@@ -30,6 +33,8 @@ function toStepErrorDto(
30
33
  const agentConfigIssue = agentConfigIssueSchema.safeParse(error.agentConfigIssue);
31
34
  return {
32
35
  message,
36
+ ...(code === undefined ? {} : {code}),
37
+ ...(managedProviderId === undefined ? {} : {managed_provider_id: managedProviderId}),
33
38
  ...(exitCode === null || typeof exitCode === 'number' ? {exit_code: exitCode} : {}),
34
39
  ...(signal === undefined ? {} : {signal}),
35
40
  ...(reason.success ? {reason: reason.data} : {}),
@@ -48,6 +53,10 @@ export function fromStepErrorDto(error: StepErrorDto | undefined): Record<string
48
53
  if (!error) return null;
49
54
  return {
50
55
  message: error.message,
56
+ ...(error.code === undefined ? {} : {code: error.code}),
57
+ ...(error.managed_provider_id === undefined
58
+ ? {}
59
+ : {managedProviderId: error.managed_provider_id}),
51
60
  ...(error.exit_code === null || typeof error.exit_code === 'number'
52
61
  ? {exitCode: error.exit_code}
53
62
  : {}),
@@ -8,10 +8,15 @@ import {closeApp, createApp, type FastifyInstance} from '@shipfox/node-fastify';
8
8
  import {createCapturingLogger} from '@shipfox/node-log/test';
9
9
  import {eq} from 'drizzle-orm';
10
10
  import type {StepStatus} from '#core/entities/step.js';
11
- import {db} from '#db/db.js';
11
+ import {db, withTransaction} from '#db/db.js';
12
12
  import {jobs} from '#db/schema/jobs.js';
13
13
  import {steps as stepsTable} from '#db/schema/steps.js';
14
- import {createWorkflowRun, getJobsByWorkflowRunId, getStepsByJobId} from '#db/workflow-runs.js';
14
+ import {
15
+ createWorkflowRun,
16
+ getJobsByWorkflowRunId,
17
+ getStepsByJobId,
18
+ insertRunningStepAttempt,
19
+ } from '#db/workflow-runs.js';
15
20
  import {workflowModel} from '#test/factories/workflow-model.js';
16
21
  import {insertRunningJobLease, mintActiveLeaseToken} from '#test/fixtures/active-lease-token.js';
17
22
  import {resolveTestAgentDefaults} from '#test/fixtures/agent-inter-module.js';
@@ -115,6 +120,13 @@ describe('GET /runs/jobs/current/agent-runtime-config', () => {
115
120
  thinking: 'xhigh',
116
121
  credentials: {api_key: 'sk-workspace-secret'},
117
122
  });
123
+ expect(resolveRuntimeCredentials).toHaveBeenCalledWith(
124
+ expect.objectContaining({
125
+ workspaceId: run.workspaceId,
126
+ runId: run.id,
127
+ stepAttemptId: expect.any(String),
128
+ }),
129
+ );
118
130
  });
119
131
 
120
132
  test('derives the credential workspace from the leased job instead of hostile lease claims', async () => {
@@ -288,6 +300,63 @@ describe('GET /runs/jobs/current/agent-runtime-config', () => {
288
300
  expect(res.json().code).toBe('model-provider-not-configured');
289
301
  });
290
302
 
303
+ test('returns managed provider policy details when workspace providers are disabled', async () => {
304
+ const {job, step} = await createRunningAgentStep();
305
+ resolveRuntimeCredentials.mockRejectedValueOnce(
306
+ createInterModuleKnownError(
307
+ agentInterModuleContract.methods.resolveRuntimeCredentials,
308
+ 'workspace-providers-disabled',
309
+ {
310
+ message: 'This instance only supports provider `shipfox`.',
311
+ managed_provider_id: 'shipfox',
312
+ },
313
+ ),
314
+ );
315
+ const token = await mintActiveLeaseToken({jobId: job.id});
316
+
317
+ const res = await app.inject({
318
+ method: 'GET',
319
+ url: runtimeConfigUrl(step.id, step.currentAttempt),
320
+ headers: {authorization: `Bearer ${token}`},
321
+ });
322
+
323
+ expect(res.statusCode).toBe(422);
324
+ expect(res.json()).toMatchObject({
325
+ code: 'workspace-providers-disabled',
326
+ details: {
327
+ message: 'This instance only supports provider `shipfox`.',
328
+ managed_provider_id: 'shipfox',
329
+ },
330
+ });
331
+ });
332
+
333
+ test('includes a fallback message when managed provider details omit one', async () => {
334
+ const {job, step} = await createRunningAgentStep();
335
+ resolveRuntimeCredentials.mockRejectedValueOnce(
336
+ createInterModuleKnownError(
337
+ agentInterModuleContract.methods.resolveRuntimeCredentials,
338
+ 'workspace-providers-disabled',
339
+ {managed_provider_id: 'shipfox'},
340
+ ),
341
+ );
342
+ const token = await mintActiveLeaseToken({jobId: job.id});
343
+
344
+ const res = await app.inject({
345
+ method: 'GET',
346
+ url: runtimeConfigUrl(step.id, step.currentAttempt),
347
+ headers: {authorization: `Bearer ${token}`},
348
+ });
349
+
350
+ expect(res.statusCode).toBe(422);
351
+ expect(res.json()).toMatchObject({
352
+ code: 'workspace-providers-disabled',
353
+ details: {
354
+ message: 'Workspace provider configuration is disabled',
355
+ managed_provider_id: 'shipfox',
356
+ },
357
+ });
358
+ });
359
+
291
360
  test('returns 409 and reports when workspace credentials cannot be decrypted', async () => {
292
361
  const {job, step} = await createRunningAgentStep();
293
362
  resolveRuntimeCredentials.mockRejectedValueOnce(
@@ -420,6 +489,19 @@ async function createStep(params: {
420
489
  const step = stepRows.find((candidate) => candidate.type === params.targetType);
421
490
  if (!step) throw new Error(`createStep: ${params.targetType} step not found`);
422
491
  await db().update(stepsTable).set({status: params.status}).where(eq(stepsTable.id, step.id));
492
+ if (params.status === 'running') {
493
+ await withTransaction((tx) =>
494
+ insertRunningStepAttempt(
495
+ {
496
+ jobExecutionId: step.jobExecutionId,
497
+ stepId: step.id,
498
+ attempt: step.currentAttempt,
499
+ config: step.config,
500
+ },
501
+ tx,
502
+ ),
503
+ );
504
+ }
423
505
 
424
506
  return {
425
507
  run,
@@ -13,6 +13,7 @@ import {isInterModuleKnownError} from '@shipfox/inter-module';
13
13
  import {captureException} from '@shipfox/node-error-monitoring';
14
14
  import {ClientError, defineRoute} from '@shipfox/node-fastify';
15
15
  import {ZodError} from 'zod';
16
+ import {getStepAttemptDetail} from '#db/index.js';
16
17
  import {loadRunningLeasedStep} from './leased-step.js';
17
18
 
18
19
  export function createAgentRuntimeConfigRoute(params: {
@@ -63,6 +64,22 @@ export function createAgentRuntimeConfigRoute(params: {
63
64
  },
64
65
  );
65
66
  }
67
+ if (
68
+ isInterModuleKnownError(
69
+ agentInterModuleContract.methods.resolveRuntimeCredentials,
70
+ error,
71
+ ) &&
72
+ error.code === 'workspace-providers-disabled'
73
+ ) {
74
+ const message = error.details.message ?? 'Workspace provider configuration is disabled';
75
+ throw new ClientError(message, 'workspace-providers-disabled', {
76
+ status: 422,
77
+ details: {
78
+ managed_provider_id: error.details.managed_provider_id,
79
+ message,
80
+ },
81
+ });
82
+ }
66
83
  throw error;
67
84
  },
68
85
  handler: async (request, reply) => {
@@ -91,8 +108,15 @@ export function createAgentRuntimeConfigRoute(params: {
91
108
  throw error;
92
109
  }
93
110
 
111
+ const stepAttempt = await getStepAttemptDetail({stepId, attempt});
112
+ if (!stepAttempt) {
113
+ throw new ClientError('Step attempt not found', 'step-attempt-not-found', {status: 409});
114
+ }
115
+
94
116
  const runtimeConfig = await params.agent.resolveRuntimeCredentials({
95
117
  workspaceId,
118
+ runId: stepAttempt.workflowRunId,
119
+ stepAttemptId: stepAttempt.attempt.id,
96
120
  harness: agentConfig.harness,
97
121
  provider: agentConfig.provider,
98
122
  model: agentConfig.model,