@shipfox/api-workflows 8.0.0 → 9.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +4 -4
- package/CHANGELOG.md +94 -0
- package/dist/core/agent-tools.d.ts +3 -1
- package/dist/core/agent-tools.d.ts.map +1 -1
- package/dist/core/agent-tools.js.map +1 -1
- package/dist/core/errors.d.ts +0 -4
- package/dist/core/errors.d.ts.map +1 -1
- package/dist/core/errors.js +0 -6
- package/dist/core/errors.js.map +1 -1
- package/dist/core/job-execution.d.ts +0 -2
- package/dist/core/job-execution.d.ts.map +1 -1
- package/dist/core/job-execution.js +3 -7
- package/dist/core/job-execution.js.map +1 -1
- package/dist/db/workflow-runs/job-executions.d.ts +0 -5
- package/dist/db/workflow-runs/job-executions.d.ts.map +1 -1
- package/dist/db/workflow-runs/job-executions.js +0 -7
- package/dist/db/workflow-runs/job-executions.js.map +1 -1
- package/dist/db/workflow-runs.d.ts +1 -1
- package/dist/db/workflow-runs.d.ts.map +1 -1
- package/dist/db/workflow-runs.js +1 -1
- package/dist/db/workflow-runs.js.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/presentation/routes/next-step.d.ts.map +1 -1
- package/dist/presentation/routes/next-step.js +11 -8
- package/dist/presentation/routes/next-step.js.map +1 -1
- package/dist/tsconfig.test.tsbuildinfo +1 -1
- package/package.json +34 -28
- package/src/core/agent-tool-capability-warning.test.ts +25 -55
- package/src/core/agent-tools.ts +7 -5
- package/src/core/errors.ts +0 -7
- package/src/core/job-execution.ts +3 -10
- package/src/core/run-workflow.test.ts +20 -55
- package/src/core/step-config/materialize-job-execution-steps.test.ts +1 -2
- package/src/core/step-config/materialize-workflow-model.test.ts +11 -23
- package/src/db/workflow-runs/job-executions.ts +0 -21
- package/src/db/workflow-runs/run-create.test.ts +31 -33
- package/src/db/workflow-runs.ts +0 -1
- package/src/index.ts +1 -1
- package/src/presentation/dto/checkout-token.test.ts +7 -1
- package/src/presentation/routes/agent-runtime-config.test.ts +38 -97
- package/src/presentation/routes/next-step.test.ts +18 -12
- package/src/presentation/routes/next-step.ts +9 -5
- package/test/factories/workflow-model.ts +3 -1
- package/test/fixtures/active-lease-token.ts +5 -32
- package/test/fixtures/agent-inter-module.ts +3 -2
- package/test/fixtures/runners-inter-module.ts +40 -48
- package/test/globalSetup.ts +0 -38
- package/test/setup.ts +2 -0
- package/tsconfig.build.tsbuildinfo +1 -1
- package/dist/db/runner-lease-table.d.ts +0 -469
- package/dist/db/runner-lease-table.d.ts.map +0 -1
- package/dist/db/runner-lease-table.js +0 -43
- package/dist/db/runner-lease-table.js.map +0 -1
- package/src/db/runner-lease-table.ts +0 -39
|
@@ -1,15 +1,12 @@
|
|
|
1
|
-
import {normalizeWorkflowDocument as normalizeWorkflowDocumentImpl} from '@shipfox/api-definitions';
|
|
2
1
|
import {WORKFLOWS_WORKFLOW_RUN_ATTEMPT_CREATED} from '@shipfox/api-workflows-dto';
|
|
3
2
|
import {and, eq, sql} from 'drizzle-orm';
|
|
4
3
|
import type {AgentDefaultsResolver} from '#core/agent-defaults.js';
|
|
5
4
|
import {InterpolationUnresolvableError} from '#core/errors.js';
|
|
6
5
|
import {nextStepForJob, recordStepResult} from '#core/job-execution.js';
|
|
7
|
-
import {
|
|
8
|
-
agentValidationCatalog,
|
|
9
|
-
resolveTestAgentDefaults,
|
|
10
|
-
} from '#test/fixtures/agent-inter-module.js';
|
|
6
|
+
import {resolveTestAgentDefaults} from '#test/fixtures/agent-inter-module.js';
|
|
11
7
|
import {createTestSecretsClient} from '#test/fixtures/secrets-inter-module.js';
|
|
12
8
|
import {buildModel, expression, shellRef, template} from '#test/helpers/workflow-runs.js';
|
|
9
|
+
import {workflowModel} from '#test/index.js';
|
|
13
10
|
import {db} from '../db.js';
|
|
14
11
|
import {workflowsOutbox} from '../schema/outbox.js';
|
|
15
12
|
import {workflowRuns} from '../schema/workflow-runs.js';
|
|
@@ -26,12 +23,6 @@ import {
|
|
|
26
23
|
updateJobExecutionStatus,
|
|
27
24
|
} from '../workflow-runs.js';
|
|
28
25
|
|
|
29
|
-
function normalizeWorkflowDocument(
|
|
30
|
-
document: Parameters<typeof normalizeWorkflowDocumentImpl>[0],
|
|
31
|
-
): ReturnType<typeof normalizeWorkflowDocumentImpl> {
|
|
32
|
-
return normalizeWorkflowDocumentImpl(document, {agentValidationCatalog});
|
|
33
|
-
}
|
|
34
|
-
|
|
35
26
|
describe('workflow run queries', () => {
|
|
36
27
|
let workspaceId: string;
|
|
37
28
|
let projectId: string;
|
|
@@ -339,7 +330,7 @@ describe('workflow run queries', () => {
|
|
|
339
330
|
const displayNameSource = ['Review batch $', '{{ execution.index }}'].join('');
|
|
340
331
|
const stepNameSource = ['Review $', '{{ execution.index }}'].join('');
|
|
341
332
|
const promptSource = ['Review $', '{{ execution.events[0].data.body }}'].join('');
|
|
342
|
-
const model =
|
|
333
|
+
const model = workflowModel({
|
|
343
334
|
name: 'Listening workflow',
|
|
344
335
|
runner: 'ubuntu-latest',
|
|
345
336
|
jobs: {
|
|
@@ -348,10 +339,10 @@ describe('workflow run queries', () => {
|
|
|
348
339
|
listening: {
|
|
349
340
|
on: [{source: 'github', event: 'pull_request_review'}],
|
|
350
341
|
until: [{source: 'github', event: 'pull_request'}],
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
batch: {
|
|
354
|
-
|
|
342
|
+
timeoutMs: 30 * 24 * 60 * 60 * 1000,
|
|
343
|
+
maxExecutions: 3,
|
|
344
|
+
batch: {debounceMs: 5000, maxSize: 10, maxWaitMs: 60 * 60 * 1000},
|
|
345
|
+
onResolve: 'cancel',
|
|
355
346
|
},
|
|
356
347
|
steps: [{name: stepNameSource, prompt: promptSource}],
|
|
357
348
|
},
|
|
@@ -409,7 +400,7 @@ describe('workflow run queries', () => {
|
|
|
409
400
|
test('persists the listening workflow model on the run attempt', async () => {
|
|
410
401
|
const displayNameSource = ['Review batch $', '{{ execution.index }}'].join('');
|
|
411
402
|
const promptSource = ['Review $', '{{ execution.events[0].data.body }}'].join('');
|
|
412
|
-
const model =
|
|
403
|
+
const model = workflowModel({
|
|
413
404
|
name: 'Listening workflow',
|
|
414
405
|
runner: 'ubuntu-latest',
|
|
415
406
|
jobs: {
|
|
@@ -418,10 +409,10 @@ describe('workflow run queries', () => {
|
|
|
418
409
|
listening: {
|
|
419
410
|
on: [{source: 'github', event: 'pull_request_review'}],
|
|
420
411
|
until: [{source: 'github', event: 'pull_request'}],
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
batch: {
|
|
424
|
-
|
|
412
|
+
timeoutMs: 30 * 24 * 60 * 60 * 1000,
|
|
413
|
+
maxExecutions: 3,
|
|
414
|
+
batch: {debounceMs: 5000, maxSize: 10, maxWaitMs: 60 * 60 * 1000},
|
|
415
|
+
onResolve: 'cancel',
|
|
425
416
|
},
|
|
426
417
|
steps: [{prompt: promptSource}],
|
|
427
418
|
},
|
|
@@ -450,7 +441,7 @@ describe('workflow run queries', () => {
|
|
|
450
441
|
});
|
|
451
442
|
|
|
452
443
|
test('does not load variables referenced only by listening job executions at run creation', async () => {
|
|
453
|
-
const model =
|
|
444
|
+
const model = workflowModel({
|
|
454
445
|
name: 'Listening vars workflow',
|
|
455
446
|
runner: 'ubuntu-latest',
|
|
456
447
|
jobs: {
|
|
@@ -458,6 +449,7 @@ describe('workflow run queries', () => {
|
|
|
458
449
|
listening: {
|
|
459
450
|
on: [{source: 'github', event: 'pull_request_review'}],
|
|
460
451
|
until: [{source: 'github', event: 'pull_request'}],
|
|
452
|
+
onResolve: 'finish',
|
|
461
453
|
},
|
|
462
454
|
steps: [
|
|
463
455
|
{
|
|
@@ -492,7 +484,7 @@ describe('workflow run queries', () => {
|
|
|
492
484
|
{
|
|
493
485
|
field: 'run',
|
|
494
486
|
model: () =>
|
|
495
|
-
|
|
487
|
+
workflowModel({
|
|
496
488
|
name: 'Missing run var',
|
|
497
489
|
runner: 'ubuntu-latest',
|
|
498
490
|
jobs: {build: {steps: [{run: `echo ${template('vars.REQUIRED')}`}]}},
|
|
@@ -502,7 +494,7 @@ describe('workflow run queries', () => {
|
|
|
502
494
|
{
|
|
503
495
|
field: 'env',
|
|
504
496
|
model: () =>
|
|
505
|
-
|
|
497
|
+
workflowModel({
|
|
506
498
|
name: 'Missing env var',
|
|
507
499
|
runner: 'ubuntu-latest',
|
|
508
500
|
jobs: {
|
|
@@ -516,7 +508,7 @@ describe('workflow run queries', () => {
|
|
|
516
508
|
{
|
|
517
509
|
field: 'agent.prompt',
|
|
518
510
|
model: () =>
|
|
519
|
-
|
|
511
|
+
workflowModel({
|
|
520
512
|
name: 'Missing prompt var',
|
|
521
513
|
runner: 'ubuntu-latest',
|
|
522
514
|
jobs: {fix: {steps: [{prompt: template('vars.REQUIRED')}]}},
|
|
@@ -526,7 +518,7 @@ describe('workflow run queries', () => {
|
|
|
526
518
|
{
|
|
527
519
|
field: 'agent.model',
|
|
528
520
|
model: () =>
|
|
529
|
-
|
|
521
|
+
workflowModel({
|
|
530
522
|
name: 'Missing model var',
|
|
531
523
|
runner: 'ubuntu-latest',
|
|
532
524
|
jobs: {fix: {steps: [{prompt: 'Fix it', model: template('vars.REQUIRED')}]}},
|
|
@@ -536,7 +528,7 @@ describe('workflow run queries', () => {
|
|
|
536
528
|
{
|
|
537
529
|
field: 'agent.provider',
|
|
538
530
|
model: () =>
|
|
539
|
-
|
|
531
|
+
workflowModel({
|
|
540
532
|
name: 'Missing provider var',
|
|
541
533
|
runner: 'ubuntu-latest',
|
|
542
534
|
jobs: {fix: {steps: [{prompt: 'Fix it', provider: template('vars.REQUIRED')}]}},
|
|
@@ -546,17 +538,23 @@ describe('workflow run queries', () => {
|
|
|
546
538
|
{
|
|
547
539
|
field: 'job.runner',
|
|
548
540
|
model: () =>
|
|
549
|
-
|
|
541
|
+
workflowModel({
|
|
550
542
|
name: 'Missing runner var',
|
|
551
543
|
runner: 'ubuntu-latest',
|
|
552
|
-
jobs: {
|
|
544
|
+
jobs: {
|
|
545
|
+
build: {
|
|
546
|
+
runner: [],
|
|
547
|
+
runnerTemplates: [template('vars.REQUIRED')],
|
|
548
|
+
steps: [{run: 'echo ok'}],
|
|
549
|
+
},
|
|
550
|
+
},
|
|
553
551
|
}),
|
|
554
552
|
expected: {field: 'job.runner', source: 'vars.REQUIRED'},
|
|
555
553
|
},
|
|
556
554
|
{
|
|
557
555
|
field: 'step.name',
|
|
558
556
|
model: () =>
|
|
559
|
-
|
|
557
|
+
workflowModel({
|
|
560
558
|
name: 'Missing step name var',
|
|
561
559
|
runner: 'ubuntu-latest',
|
|
562
560
|
jobs: {build: {steps: [{name: template('vars.REQUIRED'), run: 'echo ok'}]}},
|
|
@@ -623,7 +621,7 @@ describe('workflow run queries', () => {
|
|
|
623
621
|
});
|
|
624
622
|
|
|
625
623
|
test('persists resolved step config and authored step config separately', async () => {
|
|
626
|
-
const model =
|
|
624
|
+
const model = workflowModel({
|
|
627
625
|
name: 'Interpolated workflow',
|
|
628
626
|
runner: 'ubuntu-latest',
|
|
629
627
|
env: {RUN_ID: template('run.id'), REF: template('event.ref')},
|
|
@@ -674,7 +672,7 @@ describe('workflow run queries', () => {
|
|
|
674
672
|
});
|
|
675
673
|
|
|
676
674
|
test('resolves webhook trigger payload body and headers into step config', async () => {
|
|
677
|
-
const model =
|
|
675
|
+
const model = workflowModel({
|
|
678
676
|
name: 'Webhook workflow',
|
|
679
677
|
runner: 'ubuntu-latest',
|
|
680
678
|
env: {
|
|
@@ -720,7 +718,7 @@ describe('workflow run queries', () => {
|
|
|
720
718
|
});
|
|
721
719
|
|
|
722
720
|
test('fails for missing available untrusted interpolation paths', async () => {
|
|
723
|
-
const model =
|
|
721
|
+
const model = workflowModel({
|
|
724
722
|
name: 'Diagnostic workflow',
|
|
725
723
|
runner: 'ubuntu-latest',
|
|
726
724
|
env: {REF: template('event.ref')},
|
package/src/db/workflow-runs.ts
CHANGED
package/src/index.ts
CHANGED
|
@@ -95,7 +95,7 @@ export function createWorkflowsModule({
|
|
|
95
95
|
}): ShipfoxModule {
|
|
96
96
|
return {
|
|
97
97
|
name: 'workflows',
|
|
98
|
-
database: {db, migrationsPath},
|
|
98
|
+
database: {db, migrationsPath, databaseNamespace: 'workflows'},
|
|
99
99
|
routes: createWorkflowRoutes({
|
|
100
100
|
agent,
|
|
101
101
|
annotations,
|
|
@@ -1,6 +1,12 @@
|
|
|
1
|
-
import type {CheckoutSpec} from '@shipfox/api-integration-core';
|
|
2
1
|
import {toCheckoutTokenDto} from './checkout-token.js';
|
|
3
2
|
|
|
3
|
+
type CheckoutSpec = {
|
|
4
|
+
repositoryUrl: string;
|
|
5
|
+
ref: string;
|
|
6
|
+
credentials?: {username: string; token: string; expiresAt: Date};
|
|
7
|
+
gitAuthor?: {name: string; email: string};
|
|
8
|
+
};
|
|
9
|
+
|
|
4
10
|
describe('toCheckoutTokenDto', () => {
|
|
5
11
|
it('maps credentials to basic auth with an ISO expiry', () => {
|
|
6
12
|
const spec: CheckoutSpec = {
|
|
@@ -1,19 +1,11 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
} from '@shipfox/api-agent';
|
|
6
|
-
import {resolveRuntimeCredentials} from '@shipfox/api-agent/core/resolve-runtime-credentials';
|
|
7
|
-
import {agentInterModuleContract} from '@shipfox/api-agent-dto/inter-module';
|
|
2
|
+
type AgentInterModuleClient,
|
|
3
|
+
agentInterModuleContract,
|
|
4
|
+
} from '@shipfox/api-agent-dto/inter-module';
|
|
8
5
|
import type {WorkflowModel} from '@shipfox/api-definitions-dto';
|
|
9
|
-
import {secretsInterModuleContract} from '@shipfox/api-secrets-dto/inter-module';
|
|
10
6
|
import {createInterModuleKnownError} from '@shipfox/inter-module';
|
|
11
7
|
import {closeApp, createApp, type FastifyInstance} from '@shipfox/node-fastify';
|
|
12
8
|
import {createCapturingLogger} from '@shipfox/node-log/test';
|
|
13
|
-
import {
|
|
14
|
-
createInMemoryInterModuleTransport,
|
|
15
|
-
registerInterModulePresentations,
|
|
16
|
-
} from '@shipfox/node-module/inter-module';
|
|
17
9
|
import {eq} from 'drizzle-orm';
|
|
18
10
|
import type {StepStatus} from '#core/entities/step.js';
|
|
19
11
|
import {db} from '#db/db.js';
|
|
@@ -32,22 +24,32 @@ import {createTestSecretsClient} from '#test/fixtures/secrets-inter-module.js';
|
|
|
32
24
|
import {createLeaseTokenRouteGroup} from './index.js';
|
|
33
25
|
|
|
34
26
|
const {captureExceptionMock} = vi.hoisted(() => ({captureExceptionMock: vi.fn()}));
|
|
35
|
-
vi.mock('@shipfox/api-agent/core/resolve-runtime-credentials', async (importOriginal) => {
|
|
36
|
-
const actual =
|
|
37
|
-
await importOriginal<typeof import('@shipfox/api-agent/core/resolve-runtime-credentials')>();
|
|
38
|
-
return {...actual, resolveRuntimeCredentials: vi.fn(actual.resolveRuntimeCredentials)};
|
|
39
|
-
});
|
|
40
27
|
vi.mock('@shipfox/node-error-monitoring', () => ({captureException: captureExceptionMock}));
|
|
41
28
|
|
|
42
29
|
const URL = '/runs/jobs/current/agent-runtime-config';
|
|
43
30
|
const secrets = createTestSecretsClient();
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
})
|
|
50
|
-
|
|
31
|
+
const runtimeConfigs = new Map<
|
|
32
|
+
string,
|
|
33
|
+
Awaited<ReturnType<AgentInterModuleClient['resolveRuntimeCredentials']>>
|
|
34
|
+
>();
|
|
35
|
+
const resolveRuntimeCredentials = vi.fn<AgentInterModuleClient['resolveRuntimeCredentials']>(
|
|
36
|
+
({workspaceId}) => {
|
|
37
|
+
const runtimeConfig = runtimeConfigs.get(workspaceId);
|
|
38
|
+
if (runtimeConfig) return Promise.resolve(runtimeConfig);
|
|
39
|
+
return Promise.reject(
|
|
40
|
+
createInterModuleKnownError(
|
|
41
|
+
agentInterModuleContract.methods.resolveRuntimeCredentials,
|
|
42
|
+
'model-provider-not-configured',
|
|
43
|
+
{},
|
|
44
|
+
),
|
|
45
|
+
);
|
|
46
|
+
},
|
|
47
|
+
);
|
|
48
|
+
const agentTestClient: AgentInterModuleClient = {
|
|
49
|
+
getValidationCatalog: vi.fn(),
|
|
50
|
+
resolveAgentConfig: vi.fn(),
|
|
51
|
+
resolveRuntimeCredentials,
|
|
52
|
+
};
|
|
51
53
|
|
|
52
54
|
describe('GET /runs/jobs/current/agent-runtime-config', () => {
|
|
53
55
|
let app: FastifyInstance;
|
|
@@ -76,7 +78,8 @@ describe('GET /runs/jobs/current/agent-runtime-config', () => {
|
|
|
76
78
|
beforeEach(() => {
|
|
77
79
|
clearLogLines();
|
|
78
80
|
captureExceptionMock.mockReset();
|
|
79
|
-
|
|
81
|
+
runtimeConfigs.clear();
|
|
82
|
+
resolveRuntimeCredentials.mockClear();
|
|
80
83
|
});
|
|
81
84
|
|
|
82
85
|
afterAll(async () => {
|
|
@@ -160,67 +163,6 @@ describe('GET /runs/jobs/current/agent-runtime-config', () => {
|
|
|
160
163
|
expect(res.json().credentials.api_key).toBe('sk-gated-workspace-secret');
|
|
161
164
|
});
|
|
162
165
|
|
|
163
|
-
test('returns custom provider descriptors for a running leased custom agent step', async () => {
|
|
164
|
-
const workspaceId = crypto.randomUUID();
|
|
165
|
-
await createCustomModelProviderConfig(
|
|
166
|
-
{
|
|
167
|
-
workspaceId,
|
|
168
|
-
body: {
|
|
169
|
-
slug: 'local-vllm',
|
|
170
|
-
display_name: 'Local vLLM',
|
|
171
|
-
api: 'openai-responses',
|
|
172
|
-
base_url: 'http://127.0.0.1:11434/v1',
|
|
173
|
-
api_key: 'sk-local-secret',
|
|
174
|
-
headers: [{name: 'authorization', value: 'Bearer local', secret: true}],
|
|
175
|
-
models: [{id: 'llama-3.1', label: 'Llama 3.1'}],
|
|
176
|
-
},
|
|
177
|
-
},
|
|
178
|
-
{probe: async () => undefined, secrets},
|
|
179
|
-
);
|
|
180
|
-
const {job, step} = await createRunningAgentStep({
|
|
181
|
-
workspaceId,
|
|
182
|
-
resolveAgentDefaults: () => ({
|
|
183
|
-
harness: 'pi',
|
|
184
|
-
provider: 'local-vllm',
|
|
185
|
-
model: 'llama-3.1',
|
|
186
|
-
thinking: 'high',
|
|
187
|
-
}),
|
|
188
|
-
steps: [
|
|
189
|
-
{
|
|
190
|
-
provider: 'local-vllm',
|
|
191
|
-
model: 'llama-3.1',
|
|
192
|
-
thinking: 'high',
|
|
193
|
-
prompt: 'Fix the tests.',
|
|
194
|
-
},
|
|
195
|
-
],
|
|
196
|
-
});
|
|
197
|
-
const token = await mintActiveLeaseToken({jobId: job.id});
|
|
198
|
-
|
|
199
|
-
const res = await app.inject({
|
|
200
|
-
method: 'GET',
|
|
201
|
-
url: runtimeConfigUrl(step.id, step.currentAttempt),
|
|
202
|
-
headers: {authorization: `Bearer ${token}`},
|
|
203
|
-
});
|
|
204
|
-
|
|
205
|
-
expect(res.statusCode).toBe(200);
|
|
206
|
-
expect(res.json()).toMatchObject({
|
|
207
|
-
harness: 'pi',
|
|
208
|
-
provider_id: 'local-vllm',
|
|
209
|
-
model: 'llama-3.1',
|
|
210
|
-
credentials: {
|
|
211
|
-
api_key: 'sk-local-secret',
|
|
212
|
-
'header:authorization': 'Bearer local',
|
|
213
|
-
},
|
|
214
|
-
custom_provider: {
|
|
215
|
-
api: 'openai-responses',
|
|
216
|
-
base_url: 'http://127.0.0.1:11434/v1',
|
|
217
|
-
secret_header_names: ['authorization'],
|
|
218
|
-
models: [{id: 'llama-3.1', label: 'Llama 3.1'}],
|
|
219
|
-
requires_api_key: true,
|
|
220
|
-
},
|
|
221
|
-
});
|
|
222
|
-
});
|
|
223
|
-
|
|
224
166
|
test('returns 404 when the lease token is no longer the active job lease', async () => {
|
|
225
167
|
const {run, job, step} = await createRunningAgentStep();
|
|
226
168
|
await insertRunningJobLease({
|
|
@@ -348,10 +290,10 @@ describe('GET /runs/jobs/current/agent-runtime-config', () => {
|
|
|
348
290
|
|
|
349
291
|
test('returns 409 and reports when workspace credentials cannot be decrypted', async () => {
|
|
350
292
|
const {job, step} = await createRunningAgentStep();
|
|
351
|
-
|
|
293
|
+
resolveRuntimeCredentials.mockRejectedValueOnce(
|
|
352
294
|
createInterModuleKnownError(
|
|
353
|
-
|
|
354
|
-
'
|
|
295
|
+
agentInterModuleContract.methods.resolveRuntimeCredentials,
|
|
296
|
+
'model-provider-credentials-invalid',
|
|
355
297
|
{},
|
|
356
298
|
),
|
|
357
299
|
);
|
|
@@ -397,15 +339,14 @@ function runtimeConfigUrl(stepId: string, attempt: number): string {
|
|
|
397
339
|
return `${URL}?${search.toString()}`;
|
|
398
340
|
}
|
|
399
341
|
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
},
|
|
407
|
-
|
|
408
|
-
);
|
|
342
|
+
function saveWorkspaceCredential(workspaceId: string, apiKey: string): void {
|
|
343
|
+
runtimeConfigs.set(workspaceId, {
|
|
344
|
+
harness: 'pi',
|
|
345
|
+
provider_id: 'anthropic',
|
|
346
|
+
model: 'claude-opus-4-8',
|
|
347
|
+
thinking: 'xhigh',
|
|
348
|
+
credentials: {api_key: apiKey},
|
|
349
|
+
});
|
|
409
350
|
}
|
|
410
351
|
|
|
411
352
|
type TestStepInput =
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type {AnnotationsInterModuleClient} from '@shipfox/annotations-dto/inter-module';
|
|
2
2
|
import {closeApp, createApp, type FastifyInstance} from '@shipfox/node-fastify';
|
|
3
|
-
import {eq
|
|
3
|
+
import {eq} from 'drizzle-orm';
|
|
4
4
|
import {JobNotFoundError} from '#core/errors.js';
|
|
5
5
|
import {recordStepResult as recordJobExecutionStepResult} from '#core/job-execution.js';
|
|
6
6
|
import {db} from '#db/db.js';
|
|
@@ -21,7 +21,10 @@ import {
|
|
|
21
21
|
mintLeaseToken,
|
|
22
22
|
} from '#test/fixtures/lease-token.js';
|
|
23
23
|
import {projectsTestClient} from '#test/fixtures/projects-inter-module.js';
|
|
24
|
-
import {
|
|
24
|
+
import {
|
|
25
|
+
runnersTestClient,
|
|
26
|
+
setRunnerToolCapabilities as setTestRunnerToolCapabilities,
|
|
27
|
+
} from '#test/fixtures/runners-inter-module.js';
|
|
25
28
|
import {createTestSecretsClient} from '#test/fixtures/secrets-inter-module.js';
|
|
26
29
|
import {createLeaseTokenRouteGroup} from './index.js';
|
|
27
30
|
|
|
@@ -39,16 +42,11 @@ async function recordStepResult(
|
|
|
39
42
|
return recordJobExecutionStepResult({...rest, jobExecutionId: step.jobExecutionId});
|
|
40
43
|
}
|
|
41
44
|
|
|
42
|
-
|
|
45
|
+
function setRunnerToolCapabilities(
|
|
43
46
|
runnerSessionId: string,
|
|
44
|
-
capabilities:
|
|
45
|
-
):
|
|
46
|
-
|
|
47
|
-
UPDATE runners_runner_sessions
|
|
48
|
-
SET tool_capabilities = ${JSON.stringify(capabilities)}::jsonb,
|
|
49
|
-
tool_capabilities_reported_at = now()
|
|
50
|
-
WHERE id = ${runnerSessionId}
|
|
51
|
-
`);
|
|
47
|
+
capabilities: {harnesses: {pi?: {tools: string[]}; claude?: {tools: string[]}}},
|
|
48
|
+
): void {
|
|
49
|
+
setTestRunnerToolCapabilities(runnerSessionId, {capabilities, reportFresh: true});
|
|
52
50
|
}
|
|
53
51
|
|
|
54
52
|
const annotationWrites = vi.fn<AnnotationsInterModuleClient['replaceOrRemoveAnnotation']>();
|
|
@@ -116,6 +114,9 @@ describe('POST /runs/jobs/current/steps/next', () => {
|
|
|
116
114
|
test('returns the lowest-position pending step and marks it running', async () => {
|
|
117
115
|
const {jobId, steps} = await arrangeJobWithSteps(3);
|
|
118
116
|
const token = await mintActiveLeaseToken({jobId});
|
|
117
|
+
const lease = getLeaseTokenClaims(token);
|
|
118
|
+
if (!lease) throw new Error('Expected minted lease token to verify');
|
|
119
|
+
const getLeaseState = vi.spyOn(runnersTestClient, 'getLeaseState');
|
|
119
120
|
|
|
120
121
|
const res = await app.inject({
|
|
121
122
|
method: 'POST',
|
|
@@ -124,6 +125,11 @@ describe('POST /runs/jobs/current/steps/next', () => {
|
|
|
124
125
|
});
|
|
125
126
|
|
|
126
127
|
expect(res.statusCode).toBe(200);
|
|
128
|
+
expect(getLeaseState).toHaveBeenCalledWith({
|
|
129
|
+
jobId: lease.jobId,
|
|
130
|
+
jobExecutionId: lease.jobExecutionId,
|
|
131
|
+
runnerSessionId: lease.runnerSessionId,
|
|
132
|
+
});
|
|
127
133
|
const body = res.json();
|
|
128
134
|
expect(body.kind).toBe('step');
|
|
129
135
|
expect(body.step.id).toBe(steps[0]?.id);
|
|
@@ -182,7 +188,7 @@ describe('POST /runs/jobs/current/steps/next', () => {
|
|
|
182
188
|
const token = await mintActiveLeaseToken({jobId});
|
|
183
189
|
const lease = getLeaseTokenClaims(token);
|
|
184
190
|
if (!lease) throw new Error('Expected minted lease token to verify');
|
|
185
|
-
|
|
191
|
+
setRunnerToolCapabilities(lease.runnerSessionId, {harnesses: {pi: {tools: ['read']}}});
|
|
186
192
|
|
|
187
193
|
const first = await app.inject({
|
|
188
194
|
method: 'POST',
|
|
@@ -6,7 +6,7 @@ import type {RunnersInterModuleClient} from '@shipfox/api-runners-dto/inter-modu
|
|
|
6
6
|
import {nextStepResponseSchema} from '@shipfox/api-workflows-dto';
|
|
7
7
|
import {ClientError, defineRoute} from '@shipfox/node-fastify';
|
|
8
8
|
import {warnAgentToolCapabilityMismatchOnDispatch} from '#core/agent-tool-capability-warning.js';
|
|
9
|
-
import {
|
|
9
|
+
import {JobNotFoundError} from '#core/errors.js';
|
|
10
10
|
import {nextStepForLeasedJobExecution} from '#core/job-execution.js';
|
|
11
11
|
import {toStepDto} from '#presentation/dto/step.js';
|
|
12
12
|
|
|
@@ -30,17 +30,21 @@ export function createNextStepRoute(params: {
|
|
|
30
30
|
if (error instanceof JobNotFoundError) {
|
|
31
31
|
throw new ClientError(error.message, 'job-not-found', {status: 404});
|
|
32
32
|
}
|
|
33
|
-
if (error instanceof JobLeaseNotActiveError) {
|
|
34
|
-
throw new ClientError('Job lease is no longer active', 'lease-not-active', {status: 404});
|
|
35
|
-
}
|
|
36
33
|
throw error;
|
|
37
34
|
},
|
|
38
35
|
handler: async (request) => {
|
|
39
36
|
const leasedJob = requireLeasedJobContext(request);
|
|
40
|
-
const
|
|
37
|
+
const {active: leaseIsActive} = await params.runners.getLeaseState({
|
|
41
38
|
jobId: leasedJob.jobId,
|
|
42
39
|
jobExecutionId: leasedJob.jobExecutionId,
|
|
43
40
|
runnerSessionId: leasedJob.runnerSessionId,
|
|
41
|
+
});
|
|
42
|
+
if (!leaseIsActive) {
|
|
43
|
+
throw new ClientError('Job lease is no longer active', 'lease-not-active', {status: 404});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const next = await nextStepForLeasedJobExecution({
|
|
47
|
+
jobExecutionId: leasedJob.jobExecutionId,
|
|
44
48
|
agent: params.agent,
|
|
45
49
|
});
|
|
46
50
|
|
|
@@ -49,6 +49,7 @@ interface TestWorkflowJob {
|
|
|
49
49
|
readonly success?: string | undefined;
|
|
50
50
|
readonly outputs?: Readonly<Record<string, string>> | undefined;
|
|
51
51
|
readonly env?: WorkflowModel['env'] | undefined;
|
|
52
|
+
readonly listening?: WorkflowModel['jobs'][number]['listening'] | undefined;
|
|
52
53
|
readonly steps: readonly TestWorkflowStep[];
|
|
53
54
|
}
|
|
54
55
|
|
|
@@ -70,7 +71,7 @@ export function workflowModel(input: TestWorkflowModelInput = {}): WorkflowModel
|
|
|
70
71
|
return {
|
|
71
72
|
id: jobId,
|
|
72
73
|
key,
|
|
73
|
-
mode: 'one_shot' as const,
|
|
74
|
+
mode: job.listening === undefined ? ('one_shot' as const) : ('listening' as const),
|
|
74
75
|
runner: normalizeStringArray(job.runner ?? input.runner ?? DEFAULT_RUNNER_LABELS),
|
|
75
76
|
...(job.runnerTemplates === undefined
|
|
76
77
|
? {}
|
|
@@ -90,6 +91,7 @@ export function workflowModel(input: TestWorkflowModelInput = {}): WorkflowModel
|
|
|
90
91
|
{kind: 'literal' as const, value: job.name},
|
|
91
92
|
],
|
|
92
93
|
}),
|
|
94
|
+
...(job.listening === undefined ? {} : {listening: job.listening}),
|
|
93
95
|
...optionalScopedEnv(job.env),
|
|
94
96
|
dependencies: normalizeStringArray(job.needs).map(stableId),
|
|
95
97
|
steps: job.steps.map((step, stepIndex) => normalizeStep(step, jobId, stepIndex)),
|
|
@@ -1,11 +1,10 @@
|
|
|
1
|
-
import {db} from '#db/db.js';
|
|
2
|
-
import {runnerSessions, runningJobExecutions} from '#db/runner-lease-table.js';
|
|
3
1
|
import {
|
|
4
2
|
getFirstJobExecutionByJobId,
|
|
5
3
|
getJobById,
|
|
6
4
|
getWorkflowRunByAttemptId,
|
|
7
5
|
} from '#db/workflow-runs.js';
|
|
8
6
|
import {mintLeaseToken} from './lease-token.js';
|
|
7
|
+
import {registerActiveRunnerLease} from './runners-inter-module.js';
|
|
9
8
|
|
|
10
9
|
export interface MintActiveLeaseTokenParams {
|
|
11
10
|
jobId: string;
|
|
@@ -26,13 +25,9 @@ export async function mintActiveLeaseToken(params: MintActiveLeaseTokenParams):
|
|
|
26
25
|
if (!run) throw new Error('Expected workflow run to exist');
|
|
27
26
|
const runnerSessionId = crypto.randomUUID();
|
|
28
27
|
|
|
29
|
-
|
|
30
|
-
workspaceId: run.workspaceId,
|
|
31
|
-
workflowRunId: run.id,
|
|
32
|
-
workflowRunAttemptId: job.workflowRunAttemptId,
|
|
28
|
+
registerActiveRunnerLease({
|
|
33
29
|
jobId: params.jobId,
|
|
34
30
|
jobExecutionId: jobExecution.id,
|
|
35
|
-
projectId: run.projectId,
|
|
36
31
|
runnerSessionId,
|
|
37
32
|
});
|
|
38
33
|
|
|
@@ -57,29 +52,7 @@ export interface InsertRunningJobLeaseParams {
|
|
|
57
52
|
runnerSessionId: string;
|
|
58
53
|
}
|
|
59
54
|
|
|
60
|
-
export
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
.values({
|
|
64
|
-
id: params.runnerSessionId,
|
|
65
|
-
workspaceId: params.workspaceId,
|
|
66
|
-
scope: 'workspace',
|
|
67
|
-
registrationTokenId: crypto.randomUUID(),
|
|
68
|
-
registrationTokenKind: 'manual',
|
|
69
|
-
labels: ['linux'],
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
await db()
|
|
73
|
-
.insert(runningJobExecutions)
|
|
74
|
-
.values({
|
|
75
|
-
workspaceId: params.workspaceId,
|
|
76
|
-
workflowRunId: params.workflowRunId,
|
|
77
|
-
workflowRunAttemptId: params.workflowRunAttemptId,
|
|
78
|
-
jobId: params.jobId,
|
|
79
|
-
jobExecutionId: params.jobExecutionId,
|
|
80
|
-
projectId: params.projectId,
|
|
81
|
-
runnerSessionId: params.runnerSessionId,
|
|
82
|
-
requiredLabels: ['linux'],
|
|
83
|
-
runnerLabels: ['linux'],
|
|
84
|
-
});
|
|
55
|
+
export function insertRunningJobLease(params: InsertRunningJobLeaseParams): Promise<void> {
|
|
56
|
+
registerActiveRunnerLease(params);
|
|
57
|
+
return Promise.resolve();
|
|
85
58
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import {
|
|
2
2
|
DEFAULT_HARNESS_TOOL_DEPLOYMENT_CONFIG,
|
|
3
|
-
getModelProviderEntry,
|
|
4
3
|
listEnabledHarnessTools,
|
|
5
4
|
listHarnessDescriptors,
|
|
5
|
+
MODEL_PROVIDER_CATALOG_SEED,
|
|
6
6
|
MODEL_PROVIDER_IDS,
|
|
7
7
|
} from '@shipfox/api-agent-dto';
|
|
8
8
|
import type {
|
|
@@ -15,7 +15,8 @@ export const agentValidationCatalog: AgentValidationCatalog = {
|
|
|
15
15
|
version: 1,
|
|
16
16
|
providers: MODEL_PROVIDER_IDS.map((id) => ({
|
|
17
17
|
id,
|
|
18
|
-
support_status:
|
|
18
|
+
support_status:
|
|
19
|
+
MODEL_PROVIDER_CATALOG_SEED.find((entry) => entry.id === id)?.support_status ?? 'unsupported',
|
|
19
20
|
})),
|
|
20
21
|
harnesses: listHarnessDescriptors().map((harness) => ({
|
|
21
22
|
id: harness.id,
|