@rigkit/engine 0.2.2 → 0.2.4
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 +1 -1
- package/src/authoring.ts +108 -61
- package/src/authoring.typecheck.ts +87 -0
- package/src/db/schema/core.ts +3 -7
- package/src/engine.test.ts +270 -131
- package/src/engine.ts +566 -503
- package/src/host-storage.ts +128 -0
- package/src/index.ts +5 -0
- package/src/provider/types.ts +4 -30
- package/src/state.ts +46 -14
- package/src/types.ts +347 -140
- package/src/version.ts +1 -1
package/src/engine.ts
CHANGED
|
@@ -3,16 +3,20 @@ import { dirname, join, resolve } from "node:path";
|
|
|
3
3
|
import { pathToFileURL } from "node:url";
|
|
4
4
|
import { isRigkitConfig, isProviderDefinition, isWorkflowNode } from "./authoring.ts";
|
|
5
5
|
import { loadDotEnv } from "./env-file.ts";
|
|
6
|
-
import { hash
|
|
6
|
+
import { hash } from "./hash.ts";
|
|
7
|
+
import {
|
|
8
|
+
createFileProviderHostStorage,
|
|
9
|
+
defaultProviderHostStorageDir,
|
|
10
|
+
type ProviderHostStorageFactory,
|
|
11
|
+
} from "./host-storage.ts";
|
|
12
|
+
import { RESERVED_WORKFLOW_OPERATION_IDS, STEP_INVALIDATION_KIND } from "./types.ts";
|
|
7
13
|
import type {
|
|
8
14
|
BaseProviderPlugin,
|
|
9
15
|
InteractionPresenter,
|
|
10
16
|
InteractionPresentationRequest,
|
|
11
17
|
ProviderFactory,
|
|
12
18
|
ProviderRuntimeContext,
|
|
13
|
-
SshConnection,
|
|
14
19
|
WorkflowProviderController,
|
|
15
|
-
WorkflowWorkspaceProvider,
|
|
16
20
|
} from "./provider/types.ts";
|
|
17
21
|
import {
|
|
18
22
|
createStateStore,
|
|
@@ -30,20 +34,21 @@ import type {
|
|
|
30
34
|
LocalWorkspaceRuntime,
|
|
31
35
|
OutputSchema,
|
|
32
36
|
ProviderRuntimeMap,
|
|
33
|
-
ProviderWorkspaceContext,
|
|
34
37
|
WorkflowInputFieldDefinition,
|
|
35
38
|
WorkflowDefinition,
|
|
36
39
|
WorkflowEvent,
|
|
40
|
+
WorkflowLogStream,
|
|
37
41
|
WorkflowNodeDefinition,
|
|
38
42
|
WorkflowOperationDefinition,
|
|
39
|
-
WorkflowHostCapabilityRequirement,
|
|
40
|
-
WorkflowHostMethodRequirement,
|
|
41
43
|
WorkflowPlan,
|
|
42
44
|
WorkflowPlanNode,
|
|
43
45
|
WorkflowProviderMap,
|
|
46
|
+
WorkflowStepInvalidation,
|
|
47
|
+
WorkflowTaskCacheTTL,
|
|
44
48
|
WorkflowTaskNode,
|
|
45
49
|
WorkspaceRecord,
|
|
46
50
|
WorkspaceRuntimeRecord,
|
|
51
|
+
WorkflowWorkspaceOperationDefinition,
|
|
47
52
|
} from "./types.ts";
|
|
48
53
|
|
|
49
54
|
export type CreateDevMachineEngineOptions = {
|
|
@@ -54,6 +59,8 @@ export type CreateDevMachineEngineOptions = {
|
|
|
54
59
|
providers?: BaseProviderPlugin[];
|
|
55
60
|
providerFactory?: ProviderFactory;
|
|
56
61
|
stateFactory?: StateServiceFactory;
|
|
62
|
+
hostStorageDir?: string;
|
|
63
|
+
hostStorageFactory?: ProviderHostStorageFactory;
|
|
57
64
|
interaction?: {
|
|
58
65
|
present?: InteractionPresenter;
|
|
59
66
|
};
|
|
@@ -119,8 +126,6 @@ export type EngineOperationSummary = {
|
|
|
119
126
|
title?: string;
|
|
120
127
|
description?: string;
|
|
121
128
|
createsWorkspace?: boolean;
|
|
122
|
-
requiredHostMethods?: readonly WorkflowHostMethodRequirement[];
|
|
123
|
-
requiredHostCapabilities?: readonly WorkflowHostCapabilityRequirement[];
|
|
124
129
|
inputFields: readonly WorkflowInputFieldDefinition[];
|
|
125
130
|
cli?: EngineOperationCli;
|
|
126
131
|
};
|
|
@@ -152,10 +157,16 @@ type EvaluationMode = "plan" | "apply";
|
|
|
152
157
|
type EvaluationState = {
|
|
153
158
|
context: Record<string, JsonValue>;
|
|
154
159
|
upstreamRunIds: string[];
|
|
160
|
+
previousTasks: EvaluationPreviousTask[];
|
|
155
161
|
known: boolean;
|
|
156
162
|
blockedReason?: string;
|
|
157
163
|
};
|
|
158
164
|
|
|
165
|
+
type EvaluationPreviousTask = {
|
|
166
|
+
name: string;
|
|
167
|
+
path: string;
|
|
168
|
+
};
|
|
169
|
+
|
|
159
170
|
type EvaluationResult = EvaluationState & {
|
|
160
171
|
planNodes: WorkflowPlanNode[];
|
|
161
172
|
};
|
|
@@ -179,6 +190,35 @@ type RuntimeOperationEntry = {
|
|
|
179
190
|
readonly run: (input: { workflow?: string; input?: unknown }) => Promise<unknown>;
|
|
180
191
|
};
|
|
181
192
|
|
|
193
|
+
type RuntimeWorkspaceOperationEntry = {
|
|
194
|
+
readonly summary: EngineOperationSummary;
|
|
195
|
+
readonly run: (input: { workspace: string; workflow?: string; input?: unknown }) => Promise<unknown>;
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
class StepInvalidationRestart extends Error {
|
|
199
|
+
readonly workflow: string;
|
|
200
|
+
readonly target: string;
|
|
201
|
+
readonly targetNodePath: string;
|
|
202
|
+
readonly currentNodePath: string;
|
|
203
|
+
readonly invalidatedRunIds: string[];
|
|
204
|
+
|
|
205
|
+
constructor(input: {
|
|
206
|
+
workflow: string;
|
|
207
|
+
target: string;
|
|
208
|
+
targetNodePath: string;
|
|
209
|
+
currentNodePath: string;
|
|
210
|
+
invalidatedRunIds: string[];
|
|
211
|
+
}) {
|
|
212
|
+
super(`Task ${input.currentNodePath} invalidated ${input.targetNodePath}`);
|
|
213
|
+
this.name = "StepInvalidationRestart";
|
|
214
|
+
this.workflow = input.workflow;
|
|
215
|
+
this.target = input.target;
|
|
216
|
+
this.targetNodePath = input.targetNodePath;
|
|
217
|
+
this.currentNodePath = input.currentNodePath;
|
|
218
|
+
this.invalidatedRunIds = input.invalidatedRunIds;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
182
222
|
let configImportCounter = 0;
|
|
183
223
|
|
|
184
224
|
export class DevMachineEngine {
|
|
@@ -189,6 +229,9 @@ export class DevMachineEngine {
|
|
|
189
229
|
private providers: BaseProviderPlugin[];
|
|
190
230
|
private readonly providerFactory: ProviderFactory;
|
|
191
231
|
private readonly stateFactory: StateServiceFactory;
|
|
232
|
+
private readonly hostStorageDir: string;
|
|
233
|
+
private readonly hostStorageFactory: ProviderHostStorageFactory;
|
|
234
|
+
private readonly providerHostStorage = new Map<string, ReturnType<ProviderHostStorageFactory>>();
|
|
192
235
|
private readonly interactionPresenter: InteractionPresenter;
|
|
193
236
|
private readonly local: LocalWorkspaceRuntime;
|
|
194
237
|
private readonly handlers = new Set<EventHandler>();
|
|
@@ -204,6 +247,8 @@ export class DevMachineEngine {
|
|
|
204
247
|
this.providers = options.providers ?? [];
|
|
205
248
|
this.providerFactory = options.providerFactory ?? ((input) => this.createProviderFromPlugin(input));
|
|
206
249
|
this.stateFactory = options.stateFactory ?? createStateStore;
|
|
250
|
+
this.hostStorageDir = options.hostStorageDir ? resolve(options.hostStorageDir) : defaultProviderHostStorageDir();
|
|
251
|
+
this.hostStorageFactory = options.hostStorageFactory ?? createFileProviderHostStorage;
|
|
207
252
|
this.interactionPresenter = options.interaction?.present ?? defaultInteractionPresenter;
|
|
208
253
|
this.local = {
|
|
209
254
|
open: options.local?.open ?? openLocalTarget,
|
|
@@ -306,6 +351,10 @@ export class DevMachineEngine {
|
|
|
306
351
|
return this.listRuntimeOperationEntries().map((entry) => entry.summary);
|
|
307
352
|
}
|
|
308
353
|
|
|
354
|
+
listRuntimeWorkspaceOperations(): EngineOperationSummary[] {
|
|
355
|
+
return this.listRuntimeWorkspaceOperationEntries().map((entry) => entry.summary);
|
|
356
|
+
}
|
|
357
|
+
|
|
309
358
|
private listRuntimeOperationEntries(): RuntimeOperationEntry[] {
|
|
310
359
|
const configOperations = this.listConfigOperationEntries();
|
|
311
360
|
const configOperationIds = new Set(configOperations.map((entry) => entry.summary.id));
|
|
@@ -319,6 +368,16 @@ export class DevMachineEngine {
|
|
|
319
368
|
];
|
|
320
369
|
}
|
|
321
370
|
|
|
371
|
+
private listRuntimeWorkspaceOperationEntries(): RuntimeWorkspaceOperationEntry[] {
|
|
372
|
+
const configOperations = this.listConfigWorkspaceOperationEntries();
|
|
373
|
+
const configOperationIds = new Set(configOperations.map((entry) => entry.summary.id));
|
|
374
|
+
const coreOperations = this.listCoreWorkspaceOperationEntries();
|
|
375
|
+
return [
|
|
376
|
+
...coreOperations.filter((entry) => !configOperationIds.has(entry.summary.id)),
|
|
377
|
+
...configOperations,
|
|
378
|
+
];
|
|
379
|
+
}
|
|
380
|
+
|
|
322
381
|
private listConfigOperationEntries(): RuntimeOperationEntry[] {
|
|
323
382
|
return this.listConfigOperationSummaries().map((summary) => ({
|
|
324
383
|
summary,
|
|
@@ -341,18 +400,65 @@ export class DevMachineEngine {
|
|
|
341
400
|
source: "config" as const,
|
|
342
401
|
title: operation.title,
|
|
343
402
|
description: operation.description,
|
|
344
|
-
createsWorkspace: operation.createsWorkspace,
|
|
345
|
-
requiredHostMethods: operation.requiredHostMethods,
|
|
346
|
-
requiredHostCapabilities: operation.requiredHostCapabilities,
|
|
347
403
|
inputFields: operation.input?.fields ?? [],
|
|
348
404
|
};
|
|
349
405
|
}),
|
|
350
406
|
);
|
|
351
407
|
}
|
|
352
408
|
|
|
409
|
+
private listConfigWorkspaceOperationEntries(): RuntimeWorkspaceOperationEntry[] {
|
|
410
|
+
return this.listWorkflows().flatMap((workflow) =>
|
|
411
|
+
workflow.workspaceOperations.map((operation) => ({
|
|
412
|
+
summary: this.workspaceOperationSummary(workflow, operation),
|
|
413
|
+
run: async (input) => {
|
|
414
|
+
const workspace = this.getStateService().getWorkspace(input.workspace);
|
|
415
|
+
if (!workspace) throw new Error(`Unknown workspace ${input.workspace}`);
|
|
416
|
+
if (workspace.workflow !== workflow.name) {
|
|
417
|
+
throw new EngineOperationValidationError({
|
|
418
|
+
operation: operation.id,
|
|
419
|
+
message: `Workspace ${workspace.name} belongs to workflow ${workspace.workflow}, not ${workflow.name}`,
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
const providers = await this.createProviders(workflow);
|
|
423
|
+
return await this.runConfigWorkspaceOperation({
|
|
424
|
+
workflow,
|
|
425
|
+
providers,
|
|
426
|
+
workspace,
|
|
427
|
+
operation,
|
|
428
|
+
rawInput: input.input,
|
|
429
|
+
});
|
|
430
|
+
},
|
|
431
|
+
}))
|
|
432
|
+
);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
private listConfigWorkspaceOperationSummaries(): EngineOperationSummary[] {
|
|
436
|
+
return this.listWorkflows().flatMap((workflow) =>
|
|
437
|
+
workflow.workspaceOperations.map((operation) => {
|
|
438
|
+
assertAllowedConfigOperationId(operation.id);
|
|
439
|
+
return this.workspaceOperationSummary(workflow, operation);
|
|
440
|
+
}),
|
|
441
|
+
);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
private workspaceOperationSummary(
|
|
445
|
+
workflow: LoadedWorkflow,
|
|
446
|
+
operation: WorkflowWorkspaceOperationDefinition<any, any, any, any>,
|
|
447
|
+
): EngineOperationSummary {
|
|
448
|
+
return {
|
|
449
|
+
workflow: workflow.name,
|
|
450
|
+
id: operation.id,
|
|
451
|
+
source: "config" as const,
|
|
452
|
+
kind: "workspace-action" as const,
|
|
453
|
+
title: operation.title,
|
|
454
|
+
description: operation.description,
|
|
455
|
+
inputFields: operation.input?.fields ?? [],
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
|
|
353
459
|
private listCoreOperationEntries(): RuntimeOperationEntry[] {
|
|
354
460
|
const workflows = this.listWorkflows();
|
|
355
|
-
const hasWorkspaceCreator = workflows.some((workflow) => workflow.
|
|
461
|
+
const hasWorkspaceCreator = workflows.some((workflow) => workflow.workspace);
|
|
356
462
|
const workflowField = stringField({
|
|
357
463
|
name: "workflow",
|
|
358
464
|
required: false,
|
|
@@ -421,7 +527,7 @@ export class DevMachineEngine {
|
|
|
421
527
|
source: "core",
|
|
422
528
|
kind: "command",
|
|
423
529
|
title: "Create",
|
|
424
|
-
description: "Create a workspace
|
|
530
|
+
description: "Create a workspace",
|
|
425
531
|
createsWorkspace: true,
|
|
426
532
|
inputFields: [
|
|
427
533
|
workflowField,
|
|
@@ -436,7 +542,7 @@ export class DevMachineEngine {
|
|
|
436
542
|
},
|
|
437
543
|
async (input) => {
|
|
438
544
|
const parsed = parseCoreOperationInput("create", input.input);
|
|
439
|
-
return await this.
|
|
545
|
+
return await this.createWorkspace({
|
|
440
546
|
workflow: optionalStringInput("create", parsed, "workflow"),
|
|
441
547
|
name: requiredStringInput("create", parsed, "name"),
|
|
442
548
|
});
|
|
@@ -444,117 +550,40 @@ export class DevMachineEngine {
|
|
|
444
550
|
),
|
|
445
551
|
]
|
|
446
552
|
: []),
|
|
553
|
+
];
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
private listCoreWorkspaceOperationEntries(): RuntimeWorkspaceOperationEntry[] {
|
|
557
|
+
const workflows = this.listWorkflows().filter((workflow) => workflow.workspace);
|
|
558
|
+
if (workflows.length === 0) return [];
|
|
559
|
+
const coreOperation = (
|
|
560
|
+
summary: EngineOperationSummary,
|
|
561
|
+
run: RuntimeWorkspaceOperationEntry["run"],
|
|
562
|
+
): RuntimeWorkspaceOperationEntry => ({ summary, run });
|
|
563
|
+
|
|
564
|
+
return workflows.map((workflow) =>
|
|
447
565
|
coreOperation(
|
|
448
566
|
{
|
|
449
|
-
workflow:
|
|
450
|
-
id: "
|
|
451
|
-
source: "core",
|
|
452
|
-
kind: "command",
|
|
453
|
-
title: "SSH",
|
|
454
|
-
description: "Get an SSH command for a workspace or VM",
|
|
455
|
-
requiredHostMethods: [{ id: "host.command.run", modes: ["interactive"] }],
|
|
456
|
-
inputFields: [
|
|
457
|
-
workflowField,
|
|
458
|
-
stringField({ name: "workspaceOrVmId", position: 0, required: true }),
|
|
459
|
-
stringField({ name: "user", required: false }),
|
|
460
|
-
booleanField({ name: "print", required: false, defaultValue: false }),
|
|
461
|
-
],
|
|
462
|
-
cli: {
|
|
463
|
-
positionals: [
|
|
464
|
-
{ name: "workspaceOrVmId", index: 0 },
|
|
465
|
-
],
|
|
466
|
-
options: [
|
|
467
|
-
{ name: "workflow", flag: "--workflow" },
|
|
468
|
-
{ name: "user", flag: "--user" },
|
|
469
|
-
{ name: "print", flag: "--print", type: "boolean" },
|
|
470
|
-
],
|
|
471
|
-
},
|
|
472
|
-
},
|
|
473
|
-
async (input) => {
|
|
474
|
-
const parsed = parseCoreOperationInput("ssh", input.input);
|
|
475
|
-
const workspaceOrVmId = requiredStringInput("ssh", parsed, "workspaceOrVmId");
|
|
476
|
-
const terminal = await this.attachTerminal({
|
|
477
|
-
workflow: optionalStringInput("ssh", parsed, "workflow"),
|
|
478
|
-
workspaceOrVmId,
|
|
479
|
-
printOnly: true,
|
|
480
|
-
user: optionalStringInput("ssh", parsed, "user"),
|
|
481
|
-
});
|
|
482
|
-
if (optionalBooleanInput("ssh", parsed, "print", false)) return terminal;
|
|
483
|
-
const commandResult = await (this.local.command ?? runLocalCommand)({
|
|
484
|
-
argv: ["sh", "-lc", terminal.command],
|
|
485
|
-
cwd: this.projectDir,
|
|
486
|
-
mode: "interactive",
|
|
487
|
-
reason: `Open an SSH session to ${workspaceOrVmId}`,
|
|
488
|
-
presentation: {
|
|
489
|
-
visible: true,
|
|
490
|
-
label: "SSH into workspace",
|
|
491
|
-
},
|
|
492
|
-
});
|
|
493
|
-
return { ...terminal, commandResult };
|
|
494
|
-
},
|
|
495
|
-
),
|
|
496
|
-
coreOperation(
|
|
497
|
-
{
|
|
498
|
-
workflow: "",
|
|
499
|
-
id: "snapshot",
|
|
500
|
-
source: "core",
|
|
501
|
-
kind: "command",
|
|
502
|
-
title: "Snapshot",
|
|
503
|
-
description: "Capture a snapshot from a workspace VM",
|
|
504
|
-
inputFields: [
|
|
505
|
-
workflowField,
|
|
506
|
-
stringField({ name: "workspace", position: 0, required: true }),
|
|
507
|
-
stringField({ name: "label", required: false }),
|
|
508
|
-
],
|
|
509
|
-
cli: {
|
|
510
|
-
positionals: [
|
|
511
|
-
{ name: "workspace", index: 0 },
|
|
512
|
-
],
|
|
513
|
-
options: [
|
|
514
|
-
{ name: "workflow", flag: "--workflow" },
|
|
515
|
-
{ name: "label", flag: "--label" },
|
|
516
|
-
],
|
|
517
|
-
},
|
|
518
|
-
},
|
|
519
|
-
async (input) => {
|
|
520
|
-
const parsed = parseCoreOperationInput("snapshot", input.input);
|
|
521
|
-
return await this.snapshotWorkspace({
|
|
522
|
-
workflow: optionalStringInput("snapshot", parsed, "workflow"),
|
|
523
|
-
workspace: requiredStringInput("snapshot", parsed, "workspace"),
|
|
524
|
-
label: optionalStringInput("snapshot", parsed, "label"),
|
|
525
|
-
});
|
|
526
|
-
},
|
|
527
|
-
),
|
|
528
|
-
coreOperation(
|
|
529
|
-
{
|
|
530
|
-
workflow: "",
|
|
531
|
-
id: "delete",
|
|
532
|
-
aliases: ["rm"],
|
|
567
|
+
workflow: workflow.name,
|
|
568
|
+
id: "remove",
|
|
533
569
|
source: "core",
|
|
534
|
-
kind: "
|
|
535
|
-
title: "
|
|
536
|
-
description: "
|
|
537
|
-
inputFields: [
|
|
538
|
-
workflowField,
|
|
539
|
-
stringField({ name: "workspace", position: 0, required: true }),
|
|
540
|
-
],
|
|
570
|
+
kind: "workspace-action",
|
|
571
|
+
title: "Remove",
|
|
572
|
+
description: "Remove a workspace",
|
|
573
|
+
inputFields: [],
|
|
541
574
|
cli: {
|
|
542
|
-
positionals: [{ name: "workspace", index: 0 }],
|
|
543
575
|
options: [
|
|
544
|
-
{ name: "
|
|
545
|
-
{ name: "yes", flag: "--yes", aliases: ["-y"], required: true, type: "boolean", runtime: false },
|
|
576
|
+
{ name: "yes", flag: "--yes", aliases: ["-y"], type: "boolean", runtime: false },
|
|
546
577
|
],
|
|
547
578
|
},
|
|
548
579
|
},
|
|
549
|
-
async (input) =>
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
),
|
|
557
|
-
];
|
|
580
|
+
async (input) =>
|
|
581
|
+
await this.removeWorkspace({
|
|
582
|
+
workflow: input.workflow,
|
|
583
|
+
workspace: input.workspace,
|
|
584
|
+
}),
|
|
585
|
+
)
|
|
586
|
+
);
|
|
558
587
|
}
|
|
559
588
|
|
|
560
589
|
listNodeRuns(): WorkflowNodeRunRecord[] {
|
|
@@ -566,6 +595,15 @@ export class DevMachineEngine {
|
|
|
566
595
|
}
|
|
567
596
|
|
|
568
597
|
async runRuntimeOperation(input: { operation: string; workflow?: string; input?: unknown }): Promise<unknown> {
|
|
598
|
+
const workspaceTarget = parseWorkspaceOperationId(input.operation);
|
|
599
|
+
if (workspaceTarget) {
|
|
600
|
+
return await this.runWorkspaceOperation({
|
|
601
|
+
workspace: workspaceTarget.workspace,
|
|
602
|
+
operation: workspaceTarget.operation,
|
|
603
|
+
workflow: input.workflow,
|
|
604
|
+
input: input.input,
|
|
605
|
+
});
|
|
606
|
+
}
|
|
569
607
|
const operation = this.findRuntimeOperationEntry(input.operation);
|
|
570
608
|
if (!operation) throw new EngineOperationNotFoundError(input.operation);
|
|
571
609
|
return await operation.run({ workflow: input.workflow, input: input.input });
|
|
@@ -588,12 +626,47 @@ export class DevMachineEngine {
|
|
|
588
626
|
providers: runtime,
|
|
589
627
|
local: this.local,
|
|
590
628
|
workflow: workflow.name,
|
|
629
|
+
step: this.createStepRuntime(workflow.name, `operation.${operation.id}`, metadata),
|
|
591
630
|
});
|
|
592
631
|
if (result !== undefined) assertJsonValue(result, `Operation ${operation.id} result`);
|
|
593
|
-
if (operation.createsWorkspace) return this.saveOperationWorkspace(workflow, operation, input.input, result);
|
|
594
632
|
return result ?? null;
|
|
595
633
|
}
|
|
596
634
|
|
|
635
|
+
async runWorkspaceOperation(input: {
|
|
636
|
+
workspace: string;
|
|
637
|
+
operation: string;
|
|
638
|
+
workflow?: string;
|
|
639
|
+
input?: unknown;
|
|
640
|
+
}): Promise<unknown> {
|
|
641
|
+
const workspace = this.getStateService().getWorkspace(input.workspace);
|
|
642
|
+
if (!workspace) throw new Error(`Unknown workspace ${input.workspace}`);
|
|
643
|
+
const workflow = this.getWorkflow(input.workflow ?? workspace.workflow);
|
|
644
|
+
if (workspace.workflow !== workflow.name) {
|
|
645
|
+
throw new EngineOperationValidationError({
|
|
646
|
+
operation: input.operation,
|
|
647
|
+
message: `Workspace ${workspace.name} belongs to workflow ${workspace.workflow}, not ${workflow.name}`,
|
|
648
|
+
});
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
const core = this.listCoreWorkspaceOperationEntries().find((entry) =>
|
|
652
|
+
entry.summary.workflow === workflow.name && entry.summary.id === input.operation
|
|
653
|
+
);
|
|
654
|
+
if (core) {
|
|
655
|
+
return await core.run({ workspace: workspace.name, workflow: workflow.name, input: input.input });
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
const operation = workflow.workspaceOperations.find((item) => item.id === input.operation);
|
|
659
|
+
if (!operation) throw new EngineOperationNotFoundError(`${workspace.name}/${input.operation}`);
|
|
660
|
+
const providers = await this.createProviders(workflow);
|
|
661
|
+
return await this.runConfigWorkspaceOperation({
|
|
662
|
+
workflow,
|
|
663
|
+
providers,
|
|
664
|
+
workspace,
|
|
665
|
+
operation,
|
|
666
|
+
rawInput: input.input,
|
|
667
|
+
});
|
|
668
|
+
}
|
|
669
|
+
|
|
597
670
|
async plan(input: { workflow?: string; machine?: string } = {}): Promise<WorkflowPlan> {
|
|
598
671
|
const workflow = this.getWorkflow(input.workflow ?? input.machine);
|
|
599
672
|
const providers = await this.createProviders(workflow);
|
|
@@ -621,163 +694,128 @@ export class DevMachineEngine {
|
|
|
621
694
|
}> {
|
|
622
695
|
const workflow = this.getWorkflow(input.workflow ?? input.machine);
|
|
623
696
|
const providers = await this.createProviders(workflow);
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
697
|
+
let result: { context: Record<string, JsonValue>; plan: WorkflowPlan } | undefined;
|
|
698
|
+
const maxRestarts = 8;
|
|
699
|
+
for (let attempt = 0; attempt <= maxRestarts; attempt++) {
|
|
700
|
+
try {
|
|
701
|
+
result = await this.evaluate({
|
|
702
|
+
workflow,
|
|
703
|
+
providers,
|
|
704
|
+
mode: "apply",
|
|
705
|
+
});
|
|
706
|
+
break;
|
|
707
|
+
} catch (error) {
|
|
708
|
+
if (!(error instanceof StepInvalidationRestart)) throw error;
|
|
709
|
+
if (attempt === maxRestarts) {
|
|
710
|
+
throw new Error(
|
|
711
|
+
`Task ${error.currentNodePath} repeatedly invalidated ${error.targetNodePath}; stopping after ${maxRestarts + 1} attempts`,
|
|
712
|
+
{ cause: error },
|
|
713
|
+
);
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
if (!result) throw new Error(`Workflow ${workflow.name} did not produce an apply result`);
|
|
630
718
|
|
|
631
719
|
return {
|
|
632
720
|
context: result.context,
|
|
633
|
-
snapshotId: snapshotIdOf(workspaceSource),
|
|
634
|
-
workspaceSource,
|
|
635
721
|
plan: result.plan,
|
|
636
722
|
};
|
|
637
723
|
}
|
|
638
724
|
|
|
639
725
|
async fork(input: { workflow?: string; machine?: string; name: string }): Promise<WorkspaceRecord> {
|
|
640
|
-
|
|
726
|
+
return await this.createWorkspace(input);
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
async createWorkspace(input: { workflow?: string; machine?: string; name: string }): Promise<WorkspaceRecord> {
|
|
730
|
+
if (!input.name) throw new Error(`create requires a workspace name`);
|
|
731
|
+
if (this.getStateService().getWorkspace(input.name)) {
|
|
732
|
+
throw new Error(`Workspace ${input.name} already exists`);
|
|
733
|
+
}
|
|
641
734
|
|
|
642
735
|
const applied = await this.apply({ workflow: input.workflow ?? input.machine });
|
|
643
736
|
const workflow = this.getWorkflow(input.workflow ?? input.machine);
|
|
644
737
|
const providers = await this.createProviders(workflow);
|
|
645
|
-
if (workflow.
|
|
646
|
-
|
|
647
|
-
workflow,
|
|
648
|
-
providers,
|
|
649
|
-
context: applied.context,
|
|
650
|
-
name: input.name,
|
|
651
|
-
});
|
|
738
|
+
if (!workflow.workspace) {
|
|
739
|
+
throw new Error(`Workflow ${workflow.name} does not define a workspace`);
|
|
652
740
|
}
|
|
653
741
|
|
|
654
|
-
const sourceRef = this.resolveWorkspaceSource(workflow, applied.context, providers, { required: true })!;
|
|
655
|
-
const workspaceProvider = this.findWorkspaceProvider(providers, sourceRef);
|
|
656
|
-
const created = await workspaceProvider.createWorkspace(sourceRef, { name: input.name });
|
|
657
|
-
const providerId = created.providerId ?? providerIdOf(sourceRef) ?? this.providerIdForWorkspaceProvider(providers, workspaceProvider);
|
|
658
742
|
const now = new Date().toISOString();
|
|
659
|
-
|
|
660
743
|
const workspace: WorkspaceRecord = {
|
|
661
744
|
id: crypto.randomUUID(),
|
|
662
745
|
name: input.name,
|
|
663
|
-
providerId,
|
|
664
746
|
workflow: workflow.name,
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
sourceRef: created.sourceRef ?? sourceRef,
|
|
668
|
-
context: { ...applied.context },
|
|
747
|
+
workflowCtx: { ...applied.context },
|
|
748
|
+
ctx: {},
|
|
669
749
|
createdAt: now,
|
|
670
750
|
updatedAt: now,
|
|
671
|
-
metadata: created.metadata ?? {},
|
|
672
751
|
};
|
|
673
752
|
|
|
674
753
|
this.getStateService().saveWorkspace(workspace);
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
providers,
|
|
678
|
-
workspaceProvider,
|
|
679
|
-
workspace,
|
|
680
|
-
context: applied.context,
|
|
681
|
-
});
|
|
682
|
-
|
|
683
|
-
this.emit({
|
|
684
|
-
type: "workspace.ready",
|
|
685
|
-
workspaceId: input.name,
|
|
686
|
-
providerId: workspace.providerId,
|
|
687
|
-
resourceId: workspace.resourceId,
|
|
688
|
-
snapshotId: workspace.snapshotId,
|
|
689
|
-
});
|
|
690
|
-
|
|
691
|
-
return workspace;
|
|
692
|
-
}
|
|
693
|
-
|
|
694
|
-
async attachTerminal(input: {
|
|
695
|
-
workspaceOrVmId: string;
|
|
696
|
-
workflow?: string;
|
|
697
|
-
machine?: string;
|
|
698
|
-
printOnly?: boolean;
|
|
699
|
-
user?: string;
|
|
700
|
-
}): Promise<{ command: string }> {
|
|
701
|
-
const workspace = this.getStateService().findWorkspace(input.workspaceOrVmId);
|
|
702
|
-
const workflow = this.getWorkflow(input.workflow ?? input.machine ?? workspace?.workflow);
|
|
703
|
-
const providers = await this.createProviders(workflow);
|
|
704
|
-
const workspaceProvider = workspace
|
|
705
|
-
? this.workspaceProviderById(providers, workspace.providerId)
|
|
706
|
-
: this.singleWorkspaceProvider(providers);
|
|
707
|
-
if (workspace) {
|
|
708
|
-
await this.runWorkspaceOpenHook({
|
|
754
|
+
try {
|
|
755
|
+
await this.runWorkspaceCreate({
|
|
709
756
|
workflow,
|
|
710
757
|
providers,
|
|
711
|
-
workspaceProvider,
|
|
712
758
|
workspace,
|
|
713
|
-
context:
|
|
759
|
+
context: applied.context,
|
|
760
|
+
name: input.name,
|
|
714
761
|
});
|
|
762
|
+
} catch (error) {
|
|
763
|
+
this.getStateService().deleteWorkspace(workspace.name);
|
|
764
|
+
throw error;
|
|
715
765
|
}
|
|
716
|
-
const
|
|
766
|
+
const ready = this.getStateService().getWorkspace(input.name) ?? workspace;
|
|
717
767
|
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
stderr: "inherit",
|
|
723
|
-
});
|
|
724
|
-
await proc.exited;
|
|
725
|
-
}
|
|
768
|
+
this.emit({
|
|
769
|
+
type: "workspace.ready",
|
|
770
|
+
workspaceId: ready.name,
|
|
771
|
+
});
|
|
726
772
|
|
|
727
|
-
return
|
|
773
|
+
return ready;
|
|
728
774
|
}
|
|
729
775
|
|
|
730
776
|
async deleteWorkspace(input: { workspace: string; workflow?: string; machine?: string }): Promise<WorkspaceRecord> {
|
|
731
|
-
|
|
732
|
-
if (!workspace) throw new Error(`Unknown workspace ${input.workspace}`);
|
|
733
|
-
|
|
734
|
-
const workflow = this.getWorkflow(input.workflow ?? input.machine ?? workspace.workflow);
|
|
735
|
-
const providers = await this.createProviders(workflow);
|
|
736
|
-
const workspaceProvider = this.workspaceProviderById(providers, workspace.providerId);
|
|
737
|
-
await workspaceProvider.deleteWorkspace(workspace);
|
|
738
|
-
|
|
739
|
-
this.getStateService().deleteWorkspace(input.workspace);
|
|
740
|
-
|
|
741
|
-
return workspace;
|
|
777
|
+
return await this.removeWorkspace(input);
|
|
742
778
|
}
|
|
743
779
|
|
|
744
|
-
async
|
|
780
|
+
async removeWorkspace(input: { workspace: string; workflow?: string; machine?: string }): Promise<WorkspaceRecord> {
|
|
745
781
|
const workspace = this.getStateService().getWorkspace(input.workspace);
|
|
746
782
|
if (!workspace) throw new Error(`Unknown workspace ${input.workspace}`);
|
|
747
783
|
|
|
748
784
|
const workflow = this.getWorkflow(input.workflow ?? input.machine ?? workspace.workflow);
|
|
785
|
+
if (workspace.workflow !== workflow.name) {
|
|
786
|
+
throw new EngineOperationValidationError({
|
|
787
|
+
operation: "remove",
|
|
788
|
+
message: `Workspace ${workspace.name} belongs to workflow ${workspace.workflow}, not ${workflow.name}`,
|
|
789
|
+
});
|
|
790
|
+
}
|
|
791
|
+
if (!workflow.workspace) {
|
|
792
|
+
throw new Error(`Workflow ${workflow.name} does not define a workspace`);
|
|
793
|
+
}
|
|
749
794
|
const providers = await this.createProviders(workflow);
|
|
750
|
-
const
|
|
751
|
-
const
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
}),
|
|
766
|
-
providerFingerprint,
|
|
767
|
-
upstreamRunIds: [],
|
|
768
|
-
output: { sourceRef },
|
|
769
|
-
artifacts: collectArtifacts(sourceRef),
|
|
770
|
-
invalidated: false,
|
|
771
|
-
createdAt: now,
|
|
772
|
-
metadata: {
|
|
773
|
-
workspace: workspace.name,
|
|
774
|
-
label: input.label ?? null,
|
|
775
|
-
snapshotId: snapshot.snapshotId ?? null,
|
|
795
|
+
const metadata: JsonObject = {};
|
|
796
|
+
const runtime = await this.createTaskRuntime({
|
|
797
|
+
workflow,
|
|
798
|
+
providers,
|
|
799
|
+
nodePath: `workspace.${workspace.name}.remove`,
|
|
800
|
+
metadata,
|
|
801
|
+
});
|
|
802
|
+
const draft = cloneWorkspace(workspace);
|
|
803
|
+
const workspaceRuntime = this.createWorkspaceRuntime(draft);
|
|
804
|
+
|
|
805
|
+
await workflow.workspace.remove({
|
|
806
|
+
...runtime,
|
|
807
|
+
workflow: {
|
|
808
|
+
name: workflow.name,
|
|
809
|
+
ctx: Object.freeze({ ...workspace.workflowCtx }),
|
|
776
810
|
},
|
|
777
|
-
|
|
811
|
+
workspace: workspaceRuntime,
|
|
812
|
+
providers: runtime,
|
|
813
|
+
local: this.local,
|
|
814
|
+
step: this.createStepRuntime(workflow.name, `workspace.${workspace.name}.remove`, metadata),
|
|
815
|
+
});
|
|
778
816
|
|
|
779
|
-
this.getStateService().
|
|
780
|
-
return
|
|
817
|
+
this.getStateService().deleteWorkspace(input.workspace);
|
|
818
|
+
return workspace;
|
|
781
819
|
}
|
|
782
820
|
|
|
783
821
|
private async evaluate(input: {
|
|
@@ -796,6 +834,7 @@ export class DevMachineEngine {
|
|
|
796
834
|
state: {
|
|
797
835
|
context: {},
|
|
798
836
|
upstreamRunIds: [],
|
|
837
|
+
previousTasks: [],
|
|
799
838
|
known: true,
|
|
800
839
|
},
|
|
801
840
|
prefix: [],
|
|
@@ -844,6 +883,7 @@ export class DevMachineEngine {
|
|
|
844
883
|
state = {
|
|
845
884
|
context: result.context,
|
|
846
885
|
upstreamRunIds: result.upstreamRunIds,
|
|
886
|
+
previousTasks: result.previousTasks,
|
|
847
887
|
known: result.known,
|
|
848
888
|
blockedReason: result.blockedReason,
|
|
849
889
|
};
|
|
@@ -856,6 +896,7 @@ export class DevMachineEngine {
|
|
|
856
896
|
const branches = parallelBranches(input.node);
|
|
857
897
|
const branchOutputs: Record<string, JsonValue> = {};
|
|
858
898
|
const joinedRunIds: string[] = [];
|
|
899
|
+
let joinedPreviousTasks = [...input.state.previousTasks];
|
|
859
900
|
let known = input.state.known;
|
|
860
901
|
let blockedReason = input.state.blockedReason;
|
|
861
902
|
|
|
@@ -870,6 +911,7 @@ export class DevMachineEngine {
|
|
|
870
911
|
state: {
|
|
871
912
|
context: { ...input.state.context },
|
|
872
913
|
upstreamRunIds: [...input.state.upstreamRunIds],
|
|
914
|
+
previousTasks: [...input.state.previousTasks],
|
|
873
915
|
known: input.state.known,
|
|
874
916
|
blockedReason: input.state.blockedReason,
|
|
875
917
|
},
|
|
@@ -881,6 +923,7 @@ export class DevMachineEngine {
|
|
|
881
923
|
if (branchState.known) {
|
|
882
924
|
branchOutputs[branchName] = branchState.context;
|
|
883
925
|
joinedRunIds.push(...branchState.upstreamRunIds);
|
|
926
|
+
joinedPreviousTasks = mergePreviousTasks(joinedPreviousTasks, branchState.previousTasks);
|
|
884
927
|
} else {
|
|
885
928
|
known = false;
|
|
886
929
|
blockedReason ??= branchState.blockedReason ?? `depends on ${branchName}`;
|
|
@@ -890,6 +933,7 @@ export class DevMachineEngine {
|
|
|
890
933
|
return {
|
|
891
934
|
context: known ? { ...input.state.context, ...branchOutputs } : { ...input.state.context },
|
|
892
935
|
upstreamRunIds: known ? joinedRunIds.sort() : [],
|
|
936
|
+
previousTasks: known ? joinedPreviousTasks : input.state.previousTasks,
|
|
893
937
|
known,
|
|
894
938
|
blockedReason,
|
|
895
939
|
planNodes: input.planNodes,
|
|
@@ -900,7 +944,7 @@ export class DevMachineEngine {
|
|
|
900
944
|
const nodePath = [...input.prefix, input.node.name].join(".");
|
|
901
945
|
const upstreamRunIds = [...input.state.upstreamRunIds];
|
|
902
946
|
const nodeKey = hash({
|
|
903
|
-
cache: "task-
|
|
947
|
+
cache: "task-v3",
|
|
904
948
|
kind: "task",
|
|
905
949
|
path: nodePath,
|
|
906
950
|
name: input.node.name,
|
|
@@ -922,6 +966,7 @@ export class DevMachineEngine {
|
|
|
922
966
|
return {
|
|
923
967
|
context: input.state.context,
|
|
924
968
|
upstreamRunIds: [],
|
|
969
|
+
previousTasks: input.state.previousTasks,
|
|
925
970
|
known: false,
|
|
926
971
|
blockedReason: input.state.blockedReason ?? `depends on ${nodePath}`,
|
|
927
972
|
planNodes: input.planNodes,
|
|
@@ -936,9 +981,14 @@ export class DevMachineEngine {
|
|
|
936
981
|
upstreamRunIds,
|
|
937
982
|
providers: input.providers,
|
|
938
983
|
outputSchema: input.node.options?.output,
|
|
984
|
+
cacheTTL: input.node.options?.cacheTTL,
|
|
939
985
|
});
|
|
940
986
|
|
|
941
987
|
if (cached) {
|
|
988
|
+
const previousTasks = appendPreviousTask(input.state.previousTasks, {
|
|
989
|
+
name: input.node.name,
|
|
990
|
+
path: nodePath,
|
|
991
|
+
});
|
|
942
992
|
this.emit({ type: "node.cached", nodePath, runId: cached.id });
|
|
943
993
|
input.planNodes.push({
|
|
944
994
|
index: planIndex,
|
|
@@ -949,8 +999,9 @@ export class DevMachineEngine {
|
|
|
949
999
|
upstreamRunIds,
|
|
950
1000
|
});
|
|
951
1001
|
return {
|
|
952
|
-
context:
|
|
1002
|
+
context: cached.output,
|
|
953
1003
|
upstreamRunIds: [cached.id],
|
|
1004
|
+
previousTasks,
|
|
954
1005
|
known: true,
|
|
955
1006
|
planNodes: input.planNodes,
|
|
956
1007
|
};
|
|
@@ -969,6 +1020,7 @@ export class DevMachineEngine {
|
|
|
969
1020
|
return {
|
|
970
1021
|
context: input.state.context,
|
|
971
1022
|
upstreamRunIds: [],
|
|
1023
|
+
previousTasks: input.state.previousTasks,
|
|
972
1024
|
known: false,
|
|
973
1025
|
blockedReason: `depends on ${nodePath}`,
|
|
974
1026
|
planNodes: input.planNodes,
|
|
@@ -983,28 +1035,38 @@ export class DevMachineEngine {
|
|
|
983
1035
|
nodePath,
|
|
984
1036
|
metadata,
|
|
985
1037
|
});
|
|
1038
|
+
const step = this.createStepRuntime(
|
|
1039
|
+
input.workflow.name,
|
|
1040
|
+
nodePath,
|
|
1041
|
+
metadata,
|
|
1042
|
+
input.state.context,
|
|
1043
|
+
input.state.previousTasks,
|
|
1044
|
+
);
|
|
986
1045
|
const result = await input.node.handler({
|
|
987
1046
|
...runtime,
|
|
988
1047
|
providers: runtime,
|
|
989
|
-
|
|
990
|
-
runtime: {
|
|
991
|
-
workflow: input.workflow.name,
|
|
992
|
-
nodePath,
|
|
993
|
-
metadata: (value) => {
|
|
994
|
-
Object.assign(metadata, value);
|
|
995
|
-
},
|
|
996
|
-
log: (data, options = {}) => {
|
|
997
|
-
this.emit({
|
|
998
|
-
type: "log.output",
|
|
999
|
-
nodePath,
|
|
1000
|
-
stream: options.stream ?? "info",
|
|
1001
|
-
label: options.label,
|
|
1002
|
-
data,
|
|
1003
|
-
});
|
|
1004
|
-
},
|
|
1005
|
-
},
|
|
1048
|
+
step,
|
|
1006
1049
|
});
|
|
1007
|
-
|
|
1050
|
+
if (isStepInvalidation(result)) {
|
|
1051
|
+
const invalidatedRunIds = this.getStateService().invalidateNodeRuns({
|
|
1052
|
+
workflow: input.workflow.name,
|
|
1053
|
+
nodePaths: [result.targetNodePath, nodePath],
|
|
1054
|
+
});
|
|
1055
|
+
throw new StepInvalidationRestart({
|
|
1056
|
+
workflow: input.workflow.name,
|
|
1057
|
+
target: result.target,
|
|
1058
|
+
targetNodePath: result.targetNodePath,
|
|
1059
|
+
currentNodePath: nodePath,
|
|
1060
|
+
invalidatedRunIds,
|
|
1061
|
+
});
|
|
1062
|
+
}
|
|
1063
|
+
const output = normalizeTaskOutput(
|
|
1064
|
+
nodePath,
|
|
1065
|
+
result,
|
|
1066
|
+
input.node.options?.output,
|
|
1067
|
+
"fresh",
|
|
1068
|
+
input.state.context,
|
|
1069
|
+
);
|
|
1008
1070
|
if (!output) {
|
|
1009
1071
|
throw new Error(`Task ${nodePath} output failed schema validation`);
|
|
1010
1072
|
}
|
|
@@ -1038,9 +1100,15 @@ export class DevMachineEngine {
|
|
|
1038
1100
|
}
|
|
1039
1101
|
this.emit({ type: "node.completed", nodePath, runId: record.id });
|
|
1040
1102
|
|
|
1103
|
+
const previousTasks = appendPreviousTask(input.state.previousTasks, {
|
|
1104
|
+
name: input.node.name,
|
|
1105
|
+
path: nodePath,
|
|
1106
|
+
});
|
|
1107
|
+
|
|
1041
1108
|
return {
|
|
1042
|
-
context:
|
|
1109
|
+
context: output,
|
|
1043
1110
|
upstreamRunIds: [record.id],
|
|
1111
|
+
previousTasks,
|
|
1044
1112
|
known: true,
|
|
1045
1113
|
planNodes: input.planNodes,
|
|
1046
1114
|
};
|
|
@@ -1054,9 +1122,11 @@ export class DevMachineEngine {
|
|
|
1054
1122
|
upstreamRunIds: readonly string[];
|
|
1055
1123
|
providers: ProviderControllers;
|
|
1056
1124
|
outputSchema?: OutputSchema;
|
|
1125
|
+
cacheTTL?: WorkflowTaskCacheTTL;
|
|
1057
1126
|
}): Promise<WorkflowNodeRunRecord | undefined> {
|
|
1058
1127
|
const cached = this.getStateService().findReusableNodeRun(input);
|
|
1059
1128
|
if (!cached) return undefined;
|
|
1129
|
+
if (!isCacheFresh(cached.createdAt, input.cacheTTL)) return undefined;
|
|
1060
1130
|
|
|
1061
1131
|
const parsed = normalizeTaskOutput(input.nodePath, cached.output, input.outputSchema, "cached");
|
|
1062
1132
|
if (!parsed) return undefined;
|
|
@@ -1134,65 +1204,133 @@ export class DevMachineEngine {
|
|
|
1134
1204
|
return Object.fromEntries(entries) as ProviderRuntimeMap<WorkflowProviderMap>;
|
|
1135
1205
|
}
|
|
1136
1206
|
|
|
1137
|
-
private async
|
|
1207
|
+
private async runWorkspaceCreate(input: {
|
|
1138
1208
|
workflow: LoadedWorkflow;
|
|
1139
1209
|
providers: ProviderControllers;
|
|
1140
|
-
workspaceProvider: WorkflowWorkspaceProvider;
|
|
1141
1210
|
workspace: WorkspaceRecord;
|
|
1142
1211
|
context: Record<string, JsonValue>;
|
|
1212
|
+
name: string;
|
|
1143
1213
|
}): Promise<void> {
|
|
1144
|
-
|
|
1214
|
+
if (!input.workflow.workspace) {
|
|
1215
|
+
throw new Error(`Workflow ${input.workflow.name} does not define a workspace`);
|
|
1216
|
+
}
|
|
1217
|
+
const draft = cloneWorkspace(input.workspace);
|
|
1218
|
+
const metadata: JsonObject = {};
|
|
1219
|
+
const providers = await this.createTaskRuntime({
|
|
1220
|
+
workflow: input.workflow,
|
|
1221
|
+
providers: input.providers,
|
|
1222
|
+
nodePath: `workspace.${input.name}.create`,
|
|
1223
|
+
metadata,
|
|
1224
|
+
});
|
|
1225
|
+
|
|
1226
|
+
try {
|
|
1227
|
+
const data = await input.workflow.workspace.create({
|
|
1228
|
+
...providers,
|
|
1229
|
+
workflow: {
|
|
1230
|
+
name: input.workflow.name,
|
|
1231
|
+
ctx: Object.freeze({ ...input.context }),
|
|
1232
|
+
},
|
|
1233
|
+
workspace: {
|
|
1234
|
+
name: input.name,
|
|
1235
|
+
},
|
|
1236
|
+
providers,
|
|
1237
|
+
local: this.local,
|
|
1238
|
+
step: this.createStepRuntime(input.workflow.name, `workspace.${input.name}.create`, metadata),
|
|
1239
|
+
});
|
|
1240
|
+
assertJsonValue(data, `Workflow ${input.workflow.name} workspace create result`);
|
|
1241
|
+
if (!isPlainObject(data)) {
|
|
1242
|
+
throw new Error(`Workflow ${input.workflow.name} workspace create result must be an object`);
|
|
1243
|
+
}
|
|
1244
|
+
draft.ctx = { ...data };
|
|
1245
|
+
} finally {
|
|
1246
|
+
draft.updatedAt = new Date().toISOString();
|
|
1247
|
+
this.getStateService().saveWorkspace(draft);
|
|
1248
|
+
}
|
|
1145
1249
|
}
|
|
1146
1250
|
|
|
1147
|
-
private async
|
|
1251
|
+
private async runConfigWorkspaceOperation(input: {
|
|
1148
1252
|
workflow: LoadedWorkflow;
|
|
1149
1253
|
providers: ProviderControllers;
|
|
1150
|
-
workspaceProvider: WorkflowWorkspaceProvider;
|
|
1151
1254
|
workspace: WorkspaceRecord;
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
}
|
|
1156
|
-
|
|
1157
|
-
private async runWorkspaceHook(
|
|
1158
|
-
lifecycle: "created" | "open",
|
|
1159
|
-
hook: ((context: {
|
|
1160
|
-
workspace: WorkspaceRuntimeRecord;
|
|
1161
|
-
ctx: Readonly<Record<string, JsonValue>>;
|
|
1162
|
-
providers: ProviderRuntimeMap<WorkflowProviderMap>;
|
|
1163
|
-
providerContext: ProviderWorkspaceContext;
|
|
1164
|
-
local: LocalWorkspaceRuntime;
|
|
1165
|
-
}) => Promise<void> | void) | undefined,
|
|
1166
|
-
input: {
|
|
1167
|
-
workflow: LoadedWorkflow;
|
|
1168
|
-
providers: ProviderControllers;
|
|
1169
|
-
workspaceProvider: WorkflowWorkspaceProvider;
|
|
1170
|
-
workspace: WorkspaceRecord;
|
|
1171
|
-
context: Record<string, JsonValue>;
|
|
1172
|
-
},
|
|
1173
|
-
): Promise<void> {
|
|
1174
|
-
if (!hook) return;
|
|
1175
|
-
|
|
1176
|
-
const providerContext = await input.workspaceProvider.workspaceContext?.(input.workspace) ?? {};
|
|
1177
|
-
const workspace: WorkspaceRuntimeRecord = {
|
|
1178
|
-
...input.workspace,
|
|
1179
|
-
cwd: resolveWorkspaceCwd(input.workflow, input.context),
|
|
1180
|
-
};
|
|
1255
|
+
operation: WorkflowWorkspaceOperationDefinition<any, any, any, any>;
|
|
1256
|
+
rawInput: unknown;
|
|
1257
|
+
}): Promise<unknown> {
|
|
1181
1258
|
const metadata: JsonObject = {};
|
|
1182
1259
|
const providers = await this.createTaskRuntime({
|
|
1183
1260
|
workflow: input.workflow,
|
|
1184
1261
|
providers: input.providers,
|
|
1185
|
-
nodePath: `workspace.${input.workspace.name}.${
|
|
1262
|
+
nodePath: `workspace.${input.workspace.name}.${input.operation.id}`,
|
|
1186
1263
|
metadata,
|
|
1187
1264
|
});
|
|
1188
|
-
|
|
1189
|
-
|
|
1265
|
+
const draft = cloneWorkspace(input.workspace);
|
|
1266
|
+
const workspace = this.createWorkspaceRuntime(draft);
|
|
1267
|
+
const operationInput = this.resolveOperationInput(input.workflow, input.operation, input.rawInput ?? {});
|
|
1268
|
+
|
|
1269
|
+
const result = await input.operation.run({
|
|
1270
|
+
...providers,
|
|
1271
|
+
workflow: {
|
|
1272
|
+
name: input.workflow.name,
|
|
1273
|
+
ctx: Object.freeze({ ...input.workspace.workflowCtx }),
|
|
1274
|
+
},
|
|
1275
|
+
input: Object.freeze(operationInput),
|
|
1190
1276
|
workspace,
|
|
1191
|
-
ctx: Object.freeze({ ...input.context }),
|
|
1192
1277
|
providers,
|
|
1193
|
-
providerContext: normalizeProviderWorkspaceContext(providerContext),
|
|
1194
1278
|
local: this.local,
|
|
1279
|
+
step: this.createStepRuntime(
|
|
1280
|
+
input.workflow.name,
|
|
1281
|
+
`workspace.${input.workspace.name}.${input.operation.id}`,
|
|
1282
|
+
metadata,
|
|
1283
|
+
),
|
|
1195
1284
|
});
|
|
1285
|
+
if (result !== undefined) assertJsonValue(result, `Workspace operation ${input.operation.id} result`);
|
|
1286
|
+
return result ?? null;
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
private createWorkspaceRuntime<Data extends object>(draft: WorkspaceRecord): WorkspaceRuntimeRecord<Data> {
|
|
1290
|
+
return Object.freeze({
|
|
1291
|
+
name: draft.name,
|
|
1292
|
+
ctx: Object.freeze({ ...draft.ctx }) as Data,
|
|
1293
|
+
}) as WorkspaceRuntimeRecord<Data>;
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
private createStepRuntime<Context extends JsonObject = JsonObject>(
|
|
1297
|
+
workflow: string,
|
|
1298
|
+
nodePath: string,
|
|
1299
|
+
metadata: JsonObject,
|
|
1300
|
+
context: Context = {} as Context,
|
|
1301
|
+
previousTasks: readonly EvaluationPreviousTask[] = [],
|
|
1302
|
+
) {
|
|
1303
|
+
return {
|
|
1304
|
+
workflow,
|
|
1305
|
+
nodePath,
|
|
1306
|
+
ctx: Object.freeze({ ...context }) as Readonly<Context>,
|
|
1307
|
+
metadata: (value: JsonObject) => {
|
|
1308
|
+
Object.assign(metadata, value);
|
|
1309
|
+
},
|
|
1310
|
+
log: (data: string, options: { stream?: WorkflowLogStream; label?: string } = {}) => {
|
|
1311
|
+
this.emit({
|
|
1312
|
+
type: "log.output",
|
|
1313
|
+
nodePath,
|
|
1314
|
+
stream: options.stream ?? "info",
|
|
1315
|
+
label: options.label,
|
|
1316
|
+
data,
|
|
1317
|
+
});
|
|
1318
|
+
},
|
|
1319
|
+
invalidate: <Target extends string>(target: Target) => {
|
|
1320
|
+
const matches = previousTasks.filter((task) => task.name === target || task.path === target);
|
|
1321
|
+
if (matches.length === 0) {
|
|
1322
|
+
throw new Error(`Task ${nodePath} cannot invalidate ${target} because it has not run earlier in this workflow`);
|
|
1323
|
+
}
|
|
1324
|
+
if (matches.length > 1) {
|
|
1325
|
+
throw new Error(`Task ${nodePath} cannot invalidate ${target} because it matches multiple earlier tasks`);
|
|
1326
|
+
}
|
|
1327
|
+
return {
|
|
1328
|
+
kind: STEP_INVALIDATION_KIND,
|
|
1329
|
+
target,
|
|
1330
|
+
targetNodePath: matches[0]!.path,
|
|
1331
|
+
};
|
|
1332
|
+
},
|
|
1333
|
+
};
|
|
1196
1334
|
}
|
|
1197
1335
|
|
|
1198
1336
|
private getWorkflow(name: string | undefined): LoadedWorkflow {
|
|
@@ -1234,7 +1372,7 @@ export class DevMachineEngine {
|
|
|
1234
1372
|
|
|
1235
1373
|
private resolveOperationInput(
|
|
1236
1374
|
workflow: LoadedWorkflow,
|
|
1237
|
-
operation:
|
|
1375
|
+
operation: { id: string; input?: { fields: readonly WorkflowInputFieldDefinition[] } },
|
|
1238
1376
|
value: unknown,
|
|
1239
1377
|
): Record<string, unknown> {
|
|
1240
1378
|
const raw = isPlainObject(value) ? value : {};
|
|
@@ -1274,11 +1412,7 @@ export class DevMachineEngine {
|
|
|
1274
1412
|
message: `Workspace ${workspace.name} belongs to workflow ${workspace.workflow}, not ${workflow.name}`,
|
|
1275
1413
|
});
|
|
1276
1414
|
}
|
|
1277
|
-
resolved[field.name] =
|
|
1278
|
-
...workspace,
|
|
1279
|
-
cwd: resolveWorkspaceCwd(workflow, workspace.context),
|
|
1280
|
-
data: workspace.metadata,
|
|
1281
|
-
};
|
|
1415
|
+
resolved[field.name] = this.createWorkspaceRuntime(cloneWorkspace(workspace));
|
|
1282
1416
|
continue;
|
|
1283
1417
|
}
|
|
1284
1418
|
|
|
@@ -1318,115 +1452,6 @@ export class DevMachineEngine {
|
|
|
1318
1452
|
return resolved;
|
|
1319
1453
|
}
|
|
1320
1454
|
|
|
1321
|
-
private saveOperationWorkspace(
|
|
1322
|
-
workflow: LoadedWorkflow,
|
|
1323
|
-
operation: WorkflowOperationDefinition<any, any>,
|
|
1324
|
-
rawInput: unknown,
|
|
1325
|
-
result: unknown,
|
|
1326
|
-
): WorkspaceRecord {
|
|
1327
|
-
if (!isPlainObject(result)) {
|
|
1328
|
-
throw new Error(`Operation ${operation.id} must return an object when createsWorkspace is true`);
|
|
1329
|
-
}
|
|
1330
|
-
const data = result as Record<string, JsonValue>;
|
|
1331
|
-
const raw = isPlainObject(rawInput) ? rawInput : {};
|
|
1332
|
-
const name = typeof data.name === "string"
|
|
1333
|
-
? data.name
|
|
1334
|
-
: typeof raw.name === "string"
|
|
1335
|
-
? raw.name
|
|
1336
|
-
: undefined;
|
|
1337
|
-
if (!name) throw new Error(`Operation ${operation.id} must return or receive a workspace name`);
|
|
1338
|
-
const resourceId = typeof data.resourceId === "string"
|
|
1339
|
-
? data.resourceId
|
|
1340
|
-
: typeof data.vmId === "string"
|
|
1341
|
-
? data.vmId
|
|
1342
|
-
: name;
|
|
1343
|
-
const now = new Date().toISOString();
|
|
1344
|
-
const workspace: WorkspaceRecord = {
|
|
1345
|
-
id: crypto.randomUUID(),
|
|
1346
|
-
name,
|
|
1347
|
-
providerId: typeof data.providerId === "string" ? data.providerId : "config",
|
|
1348
|
-
workflow: workflow.name,
|
|
1349
|
-
resourceId,
|
|
1350
|
-
snapshotId: typeof data.snapshotId === "string" ? data.snapshotId : undefined,
|
|
1351
|
-
sourceRef: isJsonValue(data.sourceRef) ? data.sourceRef : data,
|
|
1352
|
-
context: {},
|
|
1353
|
-
createdAt: now,
|
|
1354
|
-
updatedAt: now,
|
|
1355
|
-
metadata: data,
|
|
1356
|
-
};
|
|
1357
|
-
this.getStateService().saveWorkspace(workspace);
|
|
1358
|
-
this.emit({
|
|
1359
|
-
type: "workspace.ready",
|
|
1360
|
-
workspaceId: workspace.name,
|
|
1361
|
-
providerId: workspace.providerId,
|
|
1362
|
-
resourceId: workspace.resourceId,
|
|
1363
|
-
snapshotId: workspace.snapshotId,
|
|
1364
|
-
});
|
|
1365
|
-
return workspace;
|
|
1366
|
-
}
|
|
1367
|
-
|
|
1368
|
-
private async createWorkspaceFromCallback(input: {
|
|
1369
|
-
workflow: LoadedWorkflow;
|
|
1370
|
-
providers: ProviderControllers;
|
|
1371
|
-
context: Record<string, JsonValue>;
|
|
1372
|
-
name: string;
|
|
1373
|
-
}): Promise<WorkspaceRecord> {
|
|
1374
|
-
if (!input.workflow.create) {
|
|
1375
|
-
throw new Error(`Workflow ${input.workflow.name} does not define a create callback`);
|
|
1376
|
-
}
|
|
1377
|
-
const metadata: JsonObject = {};
|
|
1378
|
-
const runtime = await this.createTaskRuntime({
|
|
1379
|
-
workflow: input.workflow,
|
|
1380
|
-
providers: input.providers,
|
|
1381
|
-
nodePath: `create.${input.name}`,
|
|
1382
|
-
metadata,
|
|
1383
|
-
});
|
|
1384
|
-
const result = await input.workflow.create.handler({
|
|
1385
|
-
...runtime,
|
|
1386
|
-
ctx: Object.freeze({ ...input.context }),
|
|
1387
|
-
name: input.name,
|
|
1388
|
-
providers: runtime,
|
|
1389
|
-
local: this.local,
|
|
1390
|
-
workflow: input.workflow.name,
|
|
1391
|
-
});
|
|
1392
|
-
assertJsonValue(result, `Workflow ${input.workflow.name} create result`);
|
|
1393
|
-
if (!isPlainObject(result)) {
|
|
1394
|
-
throw new Error(`Workflow ${input.workflow.name} create result must be an object`);
|
|
1395
|
-
}
|
|
1396
|
-
|
|
1397
|
-
const data = result as Record<string, JsonValue>;
|
|
1398
|
-
const name = typeof data.name === "string" ? data.name : input.name;
|
|
1399
|
-
const resourceId = typeof data.resourceId === "string"
|
|
1400
|
-
? data.resourceId
|
|
1401
|
-
: typeof data.vmId === "string"
|
|
1402
|
-
? data.vmId
|
|
1403
|
-
: name;
|
|
1404
|
-
const now = new Date().toISOString();
|
|
1405
|
-
const workspace: WorkspaceRecord = {
|
|
1406
|
-
id: crypto.randomUUID(),
|
|
1407
|
-
name,
|
|
1408
|
-
providerId: typeof data.providerId === "string" ? data.providerId : "config",
|
|
1409
|
-
workflow: input.workflow.name,
|
|
1410
|
-
resourceId,
|
|
1411
|
-
snapshotId: typeof data.snapshotId === "string" ? data.snapshotId : undefined,
|
|
1412
|
-
sourceRef: isJsonValue(data.sourceRef) ? data.sourceRef : data,
|
|
1413
|
-
context: { ...input.context },
|
|
1414
|
-
createdAt: now,
|
|
1415
|
-
updatedAt: now,
|
|
1416
|
-
metadata: data,
|
|
1417
|
-
};
|
|
1418
|
-
|
|
1419
|
-
this.getStateService().saveWorkspace(workspace);
|
|
1420
|
-
this.emit({
|
|
1421
|
-
type: "workspace.ready",
|
|
1422
|
-
workspaceId: workspace.name,
|
|
1423
|
-
providerId: workspace.providerId,
|
|
1424
|
-
resourceId: workspace.resourceId,
|
|
1425
|
-
snapshotId: workspace.snapshotId,
|
|
1426
|
-
});
|
|
1427
|
-
return workspace;
|
|
1428
|
-
}
|
|
1429
|
-
|
|
1430
1455
|
private getStateService(): StateService {
|
|
1431
1456
|
if (!this.state) {
|
|
1432
1457
|
throw new Error(`No state database loaded. Call engine.load() first.`);
|
|
@@ -1440,6 +1465,8 @@ export class DevMachineEngine {
|
|
|
1440
1465
|
const controller = await this.providerFactory({
|
|
1441
1466
|
provider,
|
|
1442
1467
|
storage: this.getStateService().providerStorage(provider.providerId),
|
|
1468
|
+
hostStorage: this.getProviderHostStorage(provider.providerId),
|
|
1469
|
+
local: this.local,
|
|
1443
1470
|
});
|
|
1444
1471
|
return [name, controller] as const;
|
|
1445
1472
|
}),
|
|
@@ -1458,6 +1485,18 @@ export class DevMachineEngine {
|
|
|
1458
1485
|
return await plugin.createProvider(input);
|
|
1459
1486
|
}
|
|
1460
1487
|
|
|
1488
|
+
private getProviderHostStorage(providerId: string): ReturnType<ProviderHostStorageFactory> {
|
|
1489
|
+
let storage = this.providerHostStorage.get(providerId);
|
|
1490
|
+
if (!storage) {
|
|
1491
|
+
storage = this.hostStorageFactory({
|
|
1492
|
+
providerId,
|
|
1493
|
+
rootDir: this.hostStorageDir,
|
|
1494
|
+
});
|
|
1495
|
+
this.providerHostStorage.set(providerId, storage);
|
|
1496
|
+
}
|
|
1497
|
+
return storage;
|
|
1498
|
+
}
|
|
1499
|
+
|
|
1461
1500
|
private async resolveWorkflow(root: WorkflowNodeDefinition<any, any, any>): Promise<LoadedWorkflow> {
|
|
1462
1501
|
const providers: Record<string, LoadedProviderDefinition> = {};
|
|
1463
1502
|
for (const [name, definition] of Object.entries(root.workflow.providers)) {
|
|
@@ -1472,75 +1511,11 @@ export class DevMachineEngine {
|
|
|
1472
1511
|
providers,
|
|
1473
1512
|
root,
|
|
1474
1513
|
workspace: root.workspaceDefinition,
|
|
1475
|
-
create: root.createDefinition,
|
|
1476
1514
|
operations: root.operations ?? [],
|
|
1515
|
+
workspaceOperations: root.workspaceOperations ?? [],
|
|
1477
1516
|
};
|
|
1478
1517
|
}
|
|
1479
1518
|
|
|
1480
|
-
private resolveWorkspaceSource(
|
|
1481
|
-
workflow: LoadedWorkflow,
|
|
1482
|
-
context: Record<string, JsonValue>,
|
|
1483
|
-
providers: ProviderControllers,
|
|
1484
|
-
options: { required: boolean },
|
|
1485
|
-
): JsonValue | undefined {
|
|
1486
|
-
const source = workflow.workspace?.source?.(context);
|
|
1487
|
-
if (source !== undefined) {
|
|
1488
|
-
assertJsonValue(source, `Workflow ${workflow.name} workspace source`);
|
|
1489
|
-
return source;
|
|
1490
|
-
}
|
|
1491
|
-
|
|
1492
|
-
const candidates = collectArtifacts(context).filter((artifact) =>
|
|
1493
|
-
Object.values(providers).some((provider) => provider.workspace?.canUse(artifact)),
|
|
1494
|
-
);
|
|
1495
|
-
|
|
1496
|
-
if (candidates.length === 1) return candidates[0];
|
|
1497
|
-
if (!options.required && candidates.length === 0) return undefined;
|
|
1498
|
-
if (candidates.length === 0) {
|
|
1499
|
-
throw new Error(`Workflow ${workflow.name} did not produce a provider artifact that can be forked`);
|
|
1500
|
-
}
|
|
1501
|
-
throw new Error(`Workflow ${workflow.name} produced multiple forkable artifacts; configure workspace.source`);
|
|
1502
|
-
}
|
|
1503
|
-
|
|
1504
|
-
private findWorkspaceProvider(
|
|
1505
|
-
providers: ProviderControllers,
|
|
1506
|
-
sourceRef: JsonValue,
|
|
1507
|
-
): WorkflowWorkspaceProvider {
|
|
1508
|
-
const provider = Object.values(providers).find((controller) => controller.workspace?.canUse(sourceRef));
|
|
1509
|
-
if (!provider?.workspace) {
|
|
1510
|
-
throw new Error(`No workflow provider can create a workspace from ${stableJson(sourceRef)}`);
|
|
1511
|
-
}
|
|
1512
|
-
return provider.workspace;
|
|
1513
|
-
}
|
|
1514
|
-
|
|
1515
|
-
private workspaceProviderById(
|
|
1516
|
-
providers: ProviderControllers,
|
|
1517
|
-
providerId: string,
|
|
1518
|
-
): WorkflowWorkspaceProvider {
|
|
1519
|
-
const provider = Object.values(providers).find((controller) => controller.providerId === providerId);
|
|
1520
|
-
if (!provider?.workspace) {
|
|
1521
|
-
throw new Error(`Provider ${providerId} does not support workspaces`);
|
|
1522
|
-
}
|
|
1523
|
-
return provider.workspace;
|
|
1524
|
-
}
|
|
1525
|
-
|
|
1526
|
-
private singleWorkspaceProvider(providers: ProviderControllers): WorkflowWorkspaceProvider {
|
|
1527
|
-
const workspaceProviders = Object.values(providers).filter((provider) => provider.workspace);
|
|
1528
|
-
if (workspaceProviders.length !== 1 || !workspaceProviders[0]?.workspace) {
|
|
1529
|
-
throw new Error(`Expected exactly one workspace-capable provider`);
|
|
1530
|
-
}
|
|
1531
|
-
return workspaceProviders[0].workspace;
|
|
1532
|
-
}
|
|
1533
|
-
|
|
1534
|
-
private providerIdForWorkspaceProvider(
|
|
1535
|
-
providers: ProviderControllers,
|
|
1536
|
-
workspaceProvider: WorkflowWorkspaceProvider,
|
|
1537
|
-
): string {
|
|
1538
|
-
for (const provider of Object.values(providers)) {
|
|
1539
|
-
if (provider.workspace === workspaceProvider) return provider.providerId;
|
|
1540
|
-
}
|
|
1541
|
-
return "unknown";
|
|
1542
|
-
}
|
|
1543
|
-
|
|
1544
1519
|
private emit(event: WorkflowEvent): void {
|
|
1545
1520
|
for (const handler of this.handlers) handler(event);
|
|
1546
1521
|
}
|
|
@@ -1552,6 +1527,23 @@ export async function createDevMachineEngine(
|
|
|
1552
1527
|
return new DevMachineEngine(options);
|
|
1553
1528
|
}
|
|
1554
1529
|
|
|
1530
|
+
function cloneWorkspace(workspace: WorkspaceRecord): WorkspaceRecord {
|
|
1531
|
+
return {
|
|
1532
|
+
...workspace,
|
|
1533
|
+
workflowCtx: { ...workspace.workflowCtx },
|
|
1534
|
+
ctx: { ...workspace.ctx },
|
|
1535
|
+
};
|
|
1536
|
+
}
|
|
1537
|
+
|
|
1538
|
+
function parseWorkspaceOperationId(value: string): { workspace: string; operation: string } | undefined {
|
|
1539
|
+
const slash = value.indexOf("/");
|
|
1540
|
+
if (slash <= 0 || slash === value.length - 1) return undefined;
|
|
1541
|
+
return {
|
|
1542
|
+
workspace: value.slice(0, slash),
|
|
1543
|
+
operation: value.slice(slash + 1),
|
|
1544
|
+
};
|
|
1545
|
+
}
|
|
1546
|
+
|
|
1555
1547
|
async function resolveProviderDefinition(
|
|
1556
1548
|
definition: WorkflowDefinition<any, any>["providers"][string],
|
|
1557
1549
|
): Promise<LoadedProviderDefinition> {
|
|
@@ -1650,7 +1642,7 @@ function summarizeWorkflow(workflow: LoadedWorkflow): WorkflowSummary {
|
|
|
1650
1642
|
providers: Object.entries(workflow.providers).map(([name, provider]) => `${name}:${provider.providerId}`),
|
|
1651
1643
|
nodes: collectNodePaths(workflow.root),
|
|
1652
1644
|
operations: workflow.operations.map((operation) => operation.id),
|
|
1653
|
-
createsWorkspace: Boolean(workflow.
|
|
1645
|
+
createsWorkspace: Boolean(workflow.workspace),
|
|
1654
1646
|
workspace: workflow.workspace,
|
|
1655
1647
|
};
|
|
1656
1648
|
}
|
|
@@ -1716,26 +1708,123 @@ function parallelBranches(node: WorkflowNodeDefinition<any, any, any>): Record<s
|
|
|
1716
1708
|
return (node as { branches?: Record<string, WorkflowNodeDefinition<any, any, any>> }).branches ?? {};
|
|
1717
1709
|
}
|
|
1718
1710
|
|
|
1711
|
+
function appendPreviousTask(
|
|
1712
|
+
tasks: readonly EvaluationPreviousTask[],
|
|
1713
|
+
task: EvaluationPreviousTask,
|
|
1714
|
+
): EvaluationPreviousTask[] {
|
|
1715
|
+
return mergePreviousTasks([...tasks], [task]);
|
|
1716
|
+
}
|
|
1717
|
+
|
|
1718
|
+
function mergePreviousTasks(
|
|
1719
|
+
left: readonly EvaluationPreviousTask[],
|
|
1720
|
+
right: readonly EvaluationPreviousTask[],
|
|
1721
|
+
): EvaluationPreviousTask[] {
|
|
1722
|
+
const seen = new Set<string>();
|
|
1723
|
+
const result: EvaluationPreviousTask[] = [];
|
|
1724
|
+
for (const task of [...left, ...right]) {
|
|
1725
|
+
if (seen.has(task.path)) continue;
|
|
1726
|
+
seen.add(task.path);
|
|
1727
|
+
result.push(task);
|
|
1728
|
+
}
|
|
1729
|
+
return result;
|
|
1730
|
+
}
|
|
1731
|
+
|
|
1719
1732
|
function normalizeTaskOutput(
|
|
1720
1733
|
nodePath: string,
|
|
1721
1734
|
result: unknown,
|
|
1722
1735
|
schema: OutputSchema | undefined,
|
|
1723
1736
|
source: "fresh" | "cached",
|
|
1737
|
+
currentContext: Record<string, JsonValue> = {},
|
|
1724
1738
|
): Record<string, JsonValue> | undefined {
|
|
1739
|
+
if (source === "fresh") {
|
|
1740
|
+
if (result === undefined) return { ...currentContext };
|
|
1741
|
+
if (!isPlainObject(result) || !("ctx" in result)) {
|
|
1742
|
+
throw new Error(`Task ${nodePath} must return { ctx: { ... } } or step.invalidate(...)`);
|
|
1743
|
+
}
|
|
1744
|
+
const ctx = result.ctx;
|
|
1745
|
+
const value = schema ? parseWithSchema(schema, ctx, source) : ctx;
|
|
1746
|
+
if (!isPlainObject(value)) {
|
|
1747
|
+
throw new Error(`Task ${nodePath} ctx must be a JSON-serializable object`);
|
|
1748
|
+
}
|
|
1749
|
+
|
|
1750
|
+
for (const [key, item] of Object.entries(value)) {
|
|
1751
|
+
assertJsonValue(item, `Task ${nodePath} ctx value ${key}`);
|
|
1752
|
+
}
|
|
1753
|
+
|
|
1754
|
+
return value as Record<string, JsonValue>;
|
|
1755
|
+
}
|
|
1756
|
+
|
|
1725
1757
|
const value = schema ? parseWithSchema(schema, result, source) : result;
|
|
1726
|
-
if (value === undefined) return
|
|
1758
|
+
if (value === undefined) return schema ? undefined : {};
|
|
1727
1759
|
if (!isPlainObject(value)) {
|
|
1728
1760
|
if (source === "cached") return undefined;
|
|
1729
|
-
throw new Error(`Task ${nodePath}
|
|
1761
|
+
throw new Error(`Task ${nodePath} cached ctx must be a JSON-serializable object`);
|
|
1730
1762
|
}
|
|
1731
1763
|
|
|
1732
1764
|
for (const [key, item] of Object.entries(value)) {
|
|
1733
|
-
assertJsonValue(item, `Task ${nodePath}
|
|
1765
|
+
assertJsonValue(item, `Task ${nodePath} ctx value ${key}`);
|
|
1734
1766
|
}
|
|
1735
1767
|
|
|
1736
1768
|
return value as Record<string, JsonValue>;
|
|
1737
1769
|
}
|
|
1738
1770
|
|
|
1771
|
+
function isCacheFresh(createdAt: string, ttl: WorkflowTaskCacheTTL | undefined): boolean {
|
|
1772
|
+
const ttlMs = parseCacheTTL(ttl);
|
|
1773
|
+
if (ttlMs === undefined) return true;
|
|
1774
|
+
if (ttlMs <= 0) return false;
|
|
1775
|
+
const createdTime = Date.parse(createdAt);
|
|
1776
|
+
if (Number.isNaN(createdTime)) return false;
|
|
1777
|
+
return Date.now() - createdTime <= ttlMs;
|
|
1778
|
+
}
|
|
1779
|
+
|
|
1780
|
+
function parseCacheTTL(ttl: WorkflowTaskCacheTTL | undefined): number | undefined {
|
|
1781
|
+
if (ttl === undefined) return undefined;
|
|
1782
|
+
if (typeof ttl === "number") {
|
|
1783
|
+
assertFiniteTTL(ttl, "cacheTTL");
|
|
1784
|
+
return ttl;
|
|
1785
|
+
}
|
|
1786
|
+
if (typeof ttl === "string") return parseCacheTTLString(ttl);
|
|
1787
|
+
|
|
1788
|
+
const total =
|
|
1789
|
+
(ttl.seconds ?? 0) * 1000 +
|
|
1790
|
+
(ttl.minutes ?? 0) * 60 * 1000 +
|
|
1791
|
+
(ttl.hours ?? 0) * 60 * 60 * 1000 +
|
|
1792
|
+
(ttl.days ?? 0) * 24 * 60 * 60 * 1000;
|
|
1793
|
+
assertFiniteTTL(total, "cacheTTL");
|
|
1794
|
+
return total;
|
|
1795
|
+
}
|
|
1796
|
+
|
|
1797
|
+
function parseCacheTTLString(value: string): number {
|
|
1798
|
+
const input = value.trim();
|
|
1799
|
+
const match = input.match(/^(\d+(?:\.\d+)?)\s*(ms|s|m|h|d)$/i);
|
|
1800
|
+
if (!match) {
|
|
1801
|
+
throw new Error(`cacheTTL must be a number, an object, or a string like "30m", "6h", or "1d"`);
|
|
1802
|
+
}
|
|
1803
|
+
const amount = Number(match[1]);
|
|
1804
|
+
assertFiniteTTL(amount, "cacheTTL");
|
|
1805
|
+
const unit = match[2].toLowerCase();
|
|
1806
|
+
const multiplier =
|
|
1807
|
+
unit === "ms" ? 1
|
|
1808
|
+
: unit === "s" ? 1000
|
|
1809
|
+
: unit === "m" ? 60 * 1000
|
|
1810
|
+
: unit === "h" ? 60 * 60 * 1000
|
|
1811
|
+
: 24 * 60 * 60 * 1000;
|
|
1812
|
+
return amount * multiplier;
|
|
1813
|
+
}
|
|
1814
|
+
|
|
1815
|
+
function assertFiniteTTL(value: number, label: string): void {
|
|
1816
|
+
if (!Number.isFinite(value) || value < 0) {
|
|
1817
|
+
throw new Error(`${label} must be a finite non-negative duration`);
|
|
1818
|
+
}
|
|
1819
|
+
}
|
|
1820
|
+
|
|
1821
|
+
function isStepInvalidation(value: unknown): value is WorkflowStepInvalidation<string> {
|
|
1822
|
+
return isPlainObject(value) &&
|
|
1823
|
+
value.kind === STEP_INVALIDATION_KIND &&
|
|
1824
|
+
typeof value.target === "string" &&
|
|
1825
|
+
typeof value.targetNodePath === "string";
|
|
1826
|
+
}
|
|
1827
|
+
|
|
1739
1828
|
function parseWithSchema(
|
|
1740
1829
|
schema: OutputSchema,
|
|
1741
1830
|
value: unknown,
|
|
@@ -1786,23 +1875,6 @@ function kindOf(value: unknown): string | undefined {
|
|
|
1786
1875
|
return isPlainObject(value) && typeof value.kind === "string" ? value.kind : undefined;
|
|
1787
1876
|
}
|
|
1788
1877
|
|
|
1789
|
-
function snapshotIdOf(value: unknown): string | undefined {
|
|
1790
|
-
return isPlainObject(value) && typeof value.snapshotId === "string" ? value.snapshotId : undefined;
|
|
1791
|
-
}
|
|
1792
|
-
|
|
1793
|
-
function resolveWorkspaceCwd(workflow: LoadedWorkflow, context: Record<string, JsonValue>): string | undefined {
|
|
1794
|
-
const cwd = workflow.workspace?.cwd;
|
|
1795
|
-
return typeof cwd === "function" ? cwd(context) : cwd;
|
|
1796
|
-
}
|
|
1797
|
-
|
|
1798
|
-
function normalizeProviderWorkspaceContext(value: unknown): ProviderWorkspaceContext {
|
|
1799
|
-
if (value === undefined) return {};
|
|
1800
|
-
if (!isPlainObject(value)) {
|
|
1801
|
-
throw new Error(`Provider workspace context must be an object`);
|
|
1802
|
-
}
|
|
1803
|
-
return value as ProviderWorkspaceContext;
|
|
1804
|
-
}
|
|
1805
|
-
|
|
1806
1878
|
function assertJsonValue(value: unknown, label: string): asserts value is JsonValue {
|
|
1807
1879
|
if (
|
|
1808
1880
|
value === null ||
|
|
@@ -1837,15 +1909,7 @@ function isJsonValue(value: unknown): value is JsonValue {
|
|
|
1837
1909
|
}
|
|
1838
1910
|
}
|
|
1839
1911
|
|
|
1840
|
-
const RESERVED_HOST_OPERATION_IDS = new Set(
|
|
1841
|
-
"completion",
|
|
1842
|
-
"doctor",
|
|
1843
|
-
"help",
|
|
1844
|
-
"init",
|
|
1845
|
-
"projects",
|
|
1846
|
-
"run",
|
|
1847
|
-
"version",
|
|
1848
|
-
]);
|
|
1912
|
+
const RESERVED_HOST_OPERATION_IDS = new Set<string>(RESERVED_WORKFLOW_OPERATION_IDS);
|
|
1849
1913
|
|
|
1850
1914
|
const CORE_OPERATION_INPUT_FIELDS: Record<string, readonly string[]> = {
|
|
1851
1915
|
plan: ["workflow"],
|
|
@@ -1853,7 +1917,6 @@ const CORE_OPERATION_INPUT_FIELDS: Record<string, readonly string[]> = {
|
|
|
1853
1917
|
create: ["workflow", "name"],
|
|
1854
1918
|
ssh: ["workflow", "workspaceOrVmId", "user", "print"],
|
|
1855
1919
|
snapshot: ["workflow", "workspace", "label"],
|
|
1856
|
-
delete: ["workflow", "workspace"],
|
|
1857
1920
|
};
|
|
1858
1921
|
|
|
1859
1922
|
function assertAllowedConfigOperationId(operationId: string): void {
|