@revisium/orchestrator 0.2.0-alpha.4 → 0.2.0-alpha.5
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/README.md +1 -1
- package/control-plane/bootstrap.config.json +5 -1
- package/control-plane/default-playbook/catalog/pipelines.json +51 -0
- package/control-plane/default-playbook/catalog/roles.json +1 -1
- package/control-plane/default-playbook/catalog/run-profiles.json +130 -0
- package/control-plane/default-playbook/prompts/integrator.md +1 -1
- package/dist/__generated__/client/internal/class.js +4 -4
- package/dist/__generated__/client/internal/class.js.map +1 -1
- package/dist/__generated__/client/internal/prismaNamespace.js +0 -1
- package/dist/__generated__/client/internal/prismaNamespace.js.map +1 -1
- package/dist/__generated__/client/internal/prismaNamespaceBrowser.js +0 -1
- package/dist/__generated__/client/internal/prismaNamespaceBrowser.js.map +1 -1
- package/dist/api/graphql-api/inbox/inputs/resolve-gate.input.js +9 -0
- package/dist/api/graphql-api/inbox/inputs/resolve-gate.input.js.map +1 -1
- package/dist/api/graphql-api/method/inputs/create-run-profile.input.js +54 -0
- package/dist/api/graphql-api/method/inputs/create-run-profile.input.js.map +1 -0
- package/dist/api/graphql-api/method/inputs/deprecate-run-profile.input.js +37 -0
- package/dist/api/graphql-api/method/inputs/deprecate-run-profile.input.js.map +1 -0
- package/dist/api/graphql-api/method/inputs/get-run-profile.input.js +32 -0
- package/dist/api/graphql-api/method/inputs/get-run-profile.input.js.map +1 -0
- package/dist/api/graphql-api/method/inputs/list-run-profiles.input.js +33 -0
- package/dist/api/graphql-api/method/inputs/list-run-profiles.input.js.map +1 -0
- package/dist/api/graphql-api/method/inputs/update-run-profile.input.js +59 -0
- package/dist/api/graphql-api/method/inputs/update-run-profile.input.js.map +1 -0
- package/dist/api/graphql-api/method/inputs/validate-run-profile.input.js +33 -0
- package/dist/api/graphql-api/method/inputs/validate-run-profile.input.js.map +1 -0
- package/dist/api/graphql-api/method/method.resolver.js +76 -1
- package/dist/api/graphql-api/method/method.resolver.js.map +1 -1
- package/dist/api/graphql-api/method/model/run-profile-connection.model.js +16 -0
- package/dist/api/graphql-api/method/model/run-profile-connection.model.js.map +1 -0
- package/dist/api/graphql-api/method/model/run-profile-status.model.js +6 -0
- package/dist/api/graphql-api/method/model/run-profile-status.model.js.map +1 -0
- package/dist/api/graphql-api/method/model/run-profile.model.js +79 -0
- package/dist/api/graphql-api/method/model/run-profile.model.js.map +1 -0
- package/dist/api/graphql-api/registerGraphqlEnums.js +4 -0
- package/dist/api/graphql-api/registerGraphqlEnums.js.map +1 -1
- package/dist/api/graphql-api/runs/inputs/create-run.input.js +6 -7
- package/dist/api/graphql-api/runs/inputs/create-run.input.js.map +1 -1
- package/dist/api/graphql-api/runs/inputs/simulate-route.input.js +6 -7
- package/dist/api/graphql-api/runs/inputs/simulate-route.input.js.map +1 -1
- package/dist/control-plane/default-playbook-policy.js +2 -11
- package/dist/control-plane/default-playbook-policy.js.map +1 -1
- package/dist/control-plane/engine-transport.js +2 -0
- package/dist/control-plane/engine-transport.js.map +1 -1
- package/dist/control-plane/json-fields.js +1 -1
- package/dist/control-plane/json-fields.js.map +1 -1
- package/dist/control-plane/pipeline-coverage-registry.js +26 -18
- package/dist/control-plane/pipeline-coverage-registry.js.map +1 -1
- package/dist/control-plane/run-profiles.js +101 -15
- package/dist/control-plane/run-profiles.js.map +1 -1
- package/dist/control-plane/transport.js.map +1 -1
- package/dist/control-plane/versioned-meaning.js +18 -3
- package/dist/control-plane/versioned-meaning.js.map +1 -1
- package/dist/e2e/kit/agents.js +3 -2
- package/dist/e2e/kit/agents.js.map +1 -1
- package/dist/e2e/kit/assertions.js +2 -2
- package/dist/e2e/kit/crash.js +8 -4
- package/dist/e2e/kit/crash.js.map +1 -1
- package/dist/e2e/kit/drive.js.map +1 -1
- package/dist/e2e/kit/fake-integrator.js +3 -36
- package/dist/e2e/kit/fake-integrator.js.map +1 -1
- package/dist/e2e/kit/gh-emulator.js +8 -2
- package/dist/e2e/kit/gh-emulator.js.map +1 -1
- package/dist/e2e/kit/index.js +1 -0
- package/dist/e2e/kit/index.js.map +1 -1
- package/dist/e2e/kit/run-profiles.js +50 -0
- package/dist/e2e/kit/run-profiles.js.map +1 -0
- package/dist/e2e/kit/scenario.js +48 -6
- package/dist/e2e/kit/scenario.js.map +1 -1
- package/dist/e2e/kit/scenarios.js +26 -27
- package/dist/e2e/kit/scenarios.js.map +1 -1
- package/dist/e2e/recovery-crash-child.js +2 -2
- package/dist/e2e/recovery-crash-child.js.map +1 -1
- package/dist/e2e/recovery-dd-crash-child.js +8 -6
- package/dist/e2e/recovery-dd-crash-child.js.map +1 -1
- package/dist/features/inbox/commands/impl/resolve-gate.command.js.map +1 -1
- package/dist/features/method/commands/handlers/method-command.handlers.js +64 -0
- package/dist/features/method/commands/handlers/method-command.handlers.js.map +1 -0
- package/dist/features/method/commands/impl/create-run-profile.command.js +7 -0
- package/dist/features/method/commands/impl/create-run-profile.command.js.map +1 -0
- package/dist/features/method/commands/impl/deprecate-run-profile.command.js +7 -0
- package/dist/features/method/commands/impl/deprecate-run-profile.command.js.map +1 -0
- package/dist/features/method/commands/impl/update-run-profile.command.js +7 -0
- package/dist/features/method/commands/impl/update-run-profile.command.js.map +1 -0
- package/dist/features/method/commands/index.js +6 -1
- package/dist/features/method/commands/index.js.map +1 -1
- package/dist/features/method/method-api.service.js +31 -3
- package/dist/features/method/method-api.service.js.map +1 -1
- package/dist/features/method/queries/handlers/method-query.handlers.js +58 -1
- package/dist/features/method/queries/handlers/method-query.handlers.js.map +1 -1
- package/dist/features/method/queries/impl/get-run-profile.query.js +7 -0
- package/dist/features/method/queries/impl/get-run-profile.query.js.map +1 -0
- package/dist/features/method/queries/impl/list-run-profiles.query.js +7 -0
- package/dist/features/method/queries/impl/list-run-profiles.query.js.map +1 -0
- package/dist/features/method/queries/impl/validate-run-profile.query.js +7 -0
- package/dist/features/method/queries/impl/validate-run-profile.query.js.map +1 -0
- package/dist/features/method/queries/index.js +4 -1
- package/dist/features/method/queries/index.js.map +1 -1
- package/dist/features/runs/commands/impl/create-run.command.js.map +1 -1
- package/dist/features/runs/queries/impl/simulate-route.query.js.map +1 -1
- package/dist/features/shared/connection.js +6 -3
- package/dist/features/shared/connection.js.map +1 -1
- package/dist/mcp/mcp-capabilities.js +5 -0
- package/dist/mcp/mcp-capabilities.js.map +1 -1
- package/dist/mcp/mcp-facade.service.js +34 -12
- package/dist/mcp/mcp-facade.service.js.map +1 -1
- package/dist/mcp/mcp-tool-result.js +22 -0
- package/dist/mcp/mcp-tool-result.js.map +1 -0
- package/dist/mcp/mcp-tools.js +90 -17
- package/dist/mcp/mcp-tools.js.map +1 -1
- package/dist/pipeline/await-human.js +27 -8
- package/dist/pipeline/await-human.js.map +1 -1
- package/dist/pipeline/data-driven-task.workflow.js +318 -69
- package/dist/pipeline/data-driven-task.workflow.js.map +1 -1
- package/dist/pipeline/pipeline.service.js +4 -10
- package/dist/pipeline/pipeline.service.js.map +1 -1
- package/dist/pipeline/route-contract.js +22 -101
- package/dist/pipeline/route-contract.js.map +1 -1
- package/dist/playbook/catalog-loader.js +1 -1
- package/dist/playbook/catalog-loader.js.map +1 -1
- package/dist/playbook/catalog-schema-validator.js +69 -39
- package/dist/playbook/catalog-schema-validator.js.map +1 -1
- package/dist/playbook/import-mapper.js +20 -9
- package/dist/playbook/import-mapper.js.map +1 -1
- package/dist/revisium/playbooks.service.js +208 -13
- package/dist/revisium/playbooks.service.js.map +1 -1
- package/dist/run/create-run.js +0 -3
- package/dist/run/create-run.js.map +1 -1
- package/dist/run/inspect-run.js +1 -1
- package/dist/run/inspect-run.js.map +1 -1
- package/dist/run/prisma-runtime-data-access.js +0 -3
- package/dist/run/prisma-runtime-data-access.js.map +1 -1
- package/dist/runners/gh-identity.js +29 -7
- package/dist/runners/gh-identity.js.map +1 -1
- package/dist/runners/integrator.js +73 -71
- package/dist/runners/integrator.js.map +1 -1
- package/dist/storage/ensure-storage.js +2 -1
- package/dist/storage/ensure-storage.js.map +1 -1
- package/dist/task-control-plane/task-control-plane-api.service.js +410 -159
- package/dist/task-control-plane/task-control-plane-api.service.js.map +1 -1
- package/dist/worker/claude-code-runner.js +1 -3
- package/dist/worker/claude-code-runner.js.map +1 -1
- package/package.json +1 -1
- package/prisma/migrations/20260707103000_prisma_runtime_tables/migration.sql +0 -1
- package/prisma/schema.prisma +16 -17
- package/dist/api/graphql-api/runs/inputs/execution-profile.input.js +0 -85
- package/dist/api/graphql-api/runs/inputs/execution-profile.input.js.map +0 -1
|
@@ -25,13 +25,14 @@ import { validateManualAdoptionAudit } from '../control-plane/manual-adoption-au
|
|
|
25
25
|
import { validateMergeOverrideAudit } from '../control-plane/merge-override-audit.js';
|
|
26
26
|
import { fnv1a64Hex } from '../control-plane/steps.js';
|
|
27
27
|
import { POLICY_VERSION } from '../control-plane/default-playbook-policy.js';
|
|
28
|
-
import {
|
|
28
|
+
import { launchBindingsFromRunProfile, runProfileHash, topologyProfileFromRunProfile, topologyStageTargetsFromRunProfile, } from '../control-plane/run-profiles.js';
|
|
29
29
|
import { materializeTemplate, MATERIALIZER_VERSION } from '../pipeline-core/materialize.js';
|
|
30
30
|
import { DbosService } from '../engine/dbos.service.js';
|
|
31
31
|
import { PipelineService } from '../pipeline/pipeline.service.js';
|
|
32
32
|
import { INTEGRATOR_PROGRESS_EVENT_TYPES, RUN_PROGRESS_EVENT_KEY } from '../pipeline/data-driven-task.workflow.js';
|
|
33
33
|
import { templateFromExecutionPolicy } from '../pipeline/data-driven-template.js';
|
|
34
|
-
import {
|
|
34
|
+
import { normalizeParams, normalizeRouteGates, resolveBindingForRole, resolveRunnerForRole, RUNNER_PERMISSION_MODES, } from '../pipeline/route-contract.js';
|
|
35
|
+
import { assertValidInlineRunProfile } from '../playbook/catalog-schema-validator.js';
|
|
35
36
|
import { InboxService } from '../revisium/inbox.service.js';
|
|
36
37
|
import { PlaybooksService } from '../revisium/playbooks.service.js';
|
|
37
38
|
import { RolesService } from '../revisium/roles.service.js';
|
|
@@ -39,15 +40,17 @@ import { RunService } from '../revisium/run.service.js';
|
|
|
39
40
|
import { CreateRunWorkflowError, previewCreateRunIds, } from '../run/create-run.js';
|
|
40
41
|
import { PrReadinessService } from './pr-readiness.service.js';
|
|
41
42
|
const execFileAsync = promisify(execFile);
|
|
42
|
-
const GATE_TOPICS = new Set(['plan', 'merge', 'question']);
|
|
43
|
-
const WORKFLOW_SUCCESS_EVENT_TYPES = new Set(['step_succeeded', 'gate_signaled']);
|
|
43
|
+
const GATE_TOPICS = new Set(['plan', 'merge', 'question', 'retry']);
|
|
44
|
+
const WORKFLOW_SUCCESS_EVENT_TYPES = new Set(['step_succeeded', 'gate_signaled', 'question_signaled']);
|
|
44
45
|
const WORKFLOW_FAILURE_EVENT_TYPES = new Set(['step_failed', 'attempt_failed']);
|
|
45
46
|
export const WORKFLOW_PROGRESS_EVENT_TYPES = new Set([
|
|
46
47
|
'pipeline_blocked',
|
|
48
|
+
'question_signal_pending',
|
|
49
|
+
'question_signaled',
|
|
47
50
|
'pr_polled',
|
|
48
51
|
...INTEGRATOR_PROGRESS_EVENT_TYPES,
|
|
49
52
|
]);
|
|
50
|
-
const BUILTIN_RUNNERS = new Set(['claude-code', 'codex', 'script', 'stub-agent'
|
|
53
|
+
const BUILTIN_RUNNERS = new Set(['claude-code', 'codex', 'script', 'stub-agent']);
|
|
51
54
|
function asErrorMessage(error) {
|
|
52
55
|
if (error instanceof Error)
|
|
53
56
|
return error.message;
|
|
@@ -58,6 +61,9 @@ function asRecord(value) {
|
|
|
58
61
|
return null;
|
|
59
62
|
return value;
|
|
60
63
|
}
|
|
64
|
+
function nonEmptyString(value) {
|
|
65
|
+
return typeof value === 'string' && value.length > 0 ? value : null;
|
|
66
|
+
}
|
|
61
67
|
function dateOrEpoch(value) {
|
|
62
68
|
if (value instanceof Date)
|
|
63
69
|
return Number.isNaN(value.getTime()) ? new Date(0) : value;
|
|
@@ -75,6 +81,39 @@ function gateTopic(item) {
|
|
|
75
81
|
return null;
|
|
76
82
|
return topic;
|
|
77
83
|
}
|
|
84
|
+
function gateSignalTopic(item, topic) {
|
|
85
|
+
const context = asRecord(item.context);
|
|
86
|
+
const signalTopic = context?.signalTopic;
|
|
87
|
+
return typeof signalTopic === 'string' && signalTopic.length > 0 ? signalTopic : topic;
|
|
88
|
+
}
|
|
89
|
+
function questionSignalTopic(item) {
|
|
90
|
+
if (item.kind !== 'question' || !item.runId)
|
|
91
|
+
return null;
|
|
92
|
+
const context = asRecord(item.context);
|
|
93
|
+
if (context?.topic !== 'question')
|
|
94
|
+
return null;
|
|
95
|
+
const signalTopic = context.signalTopic;
|
|
96
|
+
return typeof signalTopic === 'string' && signalTopic.length > 0 ? signalTopic : null;
|
|
97
|
+
}
|
|
98
|
+
function questionSummary(item) {
|
|
99
|
+
const context = asRecord(item.context);
|
|
100
|
+
return asRecord(context?.summary);
|
|
101
|
+
}
|
|
102
|
+
function questionSignalTaskId(item) {
|
|
103
|
+
if (item.taskId)
|
|
104
|
+
return item.taskId;
|
|
105
|
+
const taskId = questionSummary(item)?.taskId;
|
|
106
|
+
return typeof taskId === 'string' ? taskId : '';
|
|
107
|
+
}
|
|
108
|
+
function questionSignalStepKey(item, signalTopic) {
|
|
109
|
+
const step = questionSummary(item)?.step;
|
|
110
|
+
if (typeof step === 'string' && step.length > 0)
|
|
111
|
+
return step;
|
|
112
|
+
return signalTopic.startsWith('question:') ? signalTopic : `question:${signalTopic}`;
|
|
113
|
+
}
|
|
114
|
+
function questionSignalResolvedBy(item, answer, fallbackResolvedBy) {
|
|
115
|
+
return nonEmptyString(item.resolvedBy) ?? nonEmptyString(asRecord(answer)?.resolvedBy) ?? fallbackResolvedBy;
|
|
116
|
+
}
|
|
78
117
|
function gateDeclaredOutcomes(item) {
|
|
79
118
|
const context = asRecord(item.context);
|
|
80
119
|
const summary = asRecord(context?.summary);
|
|
@@ -112,6 +151,13 @@ function assertRequiredGateNote(item, outcome, note) {
|
|
|
112
151
|
throw new ControlPlaneError('VALIDATION_FAILURE', `questionGate ${outcome} requires a non-empty note`);
|
|
113
152
|
}
|
|
114
153
|
}
|
|
154
|
+
function normalizeGateReconcile(value) {
|
|
155
|
+
if (value === undefined)
|
|
156
|
+
return undefined;
|
|
157
|
+
if (value === 'keep')
|
|
158
|
+
return value;
|
|
159
|
+
throw new ControlPlaneError('VALIDATION_FAILURE', 'gate reconcile must be keep');
|
|
160
|
+
}
|
|
115
161
|
async function git(cwd, args) {
|
|
116
162
|
try {
|
|
117
163
|
const result = await execFileAsync('git', args, {
|
|
@@ -249,12 +295,6 @@ function optionalString(value) {
|
|
|
249
295
|
function optionalInteger(value) {
|
|
250
296
|
return typeof value === 'number' && Number.isSafeInteger(value) ? value : undefined;
|
|
251
297
|
}
|
|
252
|
-
function requiredRecord(value, field, runId) {
|
|
253
|
-
const record = asRecord(value);
|
|
254
|
-
if (record)
|
|
255
|
-
return record;
|
|
256
|
-
throw new ControlPlaneError('VALIDATION_FAILURE', `Cannot recover run ${runId}: parent ${field} is not a record`);
|
|
257
|
-
}
|
|
258
298
|
function optionalRecord(value) {
|
|
259
299
|
return asRecord(value) ?? {};
|
|
260
300
|
}
|
|
@@ -419,19 +459,15 @@ function isRouteDecision(value) {
|
|
|
419
459
|
const record = asRecord(value);
|
|
420
460
|
return Boolean(record?.playbookId && record?.pipelineRowId && Array.isArray(record.roleBindings));
|
|
421
461
|
}
|
|
422
|
-
function assertRunnerAvailable(runnerId,
|
|
423
|
-
|
|
424
|
-
if (available && !available.includes(runnerId)) {
|
|
425
|
-
throw new ControlPlaneError('VALIDATION_FAILURE', `runner unavailable for role ${roleId}: ${runnerId}`);
|
|
426
|
-
}
|
|
427
|
-
if (!available && !BUILTIN_RUNNERS.has(runnerId)) {
|
|
462
|
+
function assertRunnerAvailable(runnerId, roleId) {
|
|
463
|
+
if (!BUILTIN_RUNNERS.has(runnerId)) {
|
|
428
464
|
throw new ControlPlaneError('VALIDATION_FAILURE', `runner implementation is not registered for role ${roleId}: ${runnerId}`);
|
|
429
465
|
}
|
|
430
466
|
}
|
|
431
467
|
function assertProductionRunnerBinding(runnerId, runnerSource, roleId) {
|
|
432
|
-
if (runnerId !== 'stub-agent' || runnerSource === '
|
|
468
|
+
if (runnerId !== 'stub-agent' || runnerSource === 'profile')
|
|
433
469
|
return;
|
|
434
|
-
throw new ControlPlaneError('VALIDATION_FAILURE', `role ${roleId} binds production runner stub-agent; use
|
|
470
|
+
throw new ControlPlaneError('VALIDATION_FAILURE', `role ${roleId} binds production runner stub-agent; use a run profile binding for test stubs`);
|
|
435
471
|
}
|
|
436
472
|
let TaskControlPlaneApiService = class TaskControlPlaneApiService {
|
|
437
473
|
runs;
|
|
@@ -558,11 +594,11 @@ let TaskControlPlaneApiService = class TaskControlPlaneApiService {
|
|
|
558
594
|
playbookId: input.playbookId,
|
|
559
595
|
pipelineId: input.pipelineId,
|
|
560
596
|
profileId: input.profileId,
|
|
597
|
+
profile: input.profile,
|
|
561
598
|
params: input.params,
|
|
562
599
|
issueRef: input.issueRef,
|
|
563
600
|
issueAction: input.issueAction,
|
|
564
|
-
|
|
565
|
-
source: input.pipelineId ? 'explicit' : 'deterministic-installed-playbook',
|
|
601
|
+
source: 'explicit',
|
|
566
602
|
});
|
|
567
603
|
const result = await this.runs.createRun({
|
|
568
604
|
title: input.title,
|
|
@@ -576,7 +612,6 @@ let TaskControlPlaneApiService = class TaskControlPlaneApiService {
|
|
|
576
612
|
params: route.params,
|
|
577
613
|
issueRef: route.params.issueRef,
|
|
578
614
|
routeDecision: route,
|
|
579
|
-
executionProfile: route.executionProfile,
|
|
580
615
|
});
|
|
581
616
|
if (!input.start)
|
|
582
617
|
return { ...result, started: false, route };
|
|
@@ -691,9 +726,8 @@ let TaskControlPlaneApiService = class TaskControlPlaneApiService {
|
|
|
691
726
|
if (!isRouteDecision(routeDecision)) {
|
|
692
727
|
throw new ControlPlaneError('VALIDATION_FAILURE', `Cannot recover run ${parentRunId}: parent route_decision is invalid`);
|
|
693
728
|
}
|
|
694
|
-
const executionProfile = requiredRecord(parentData.execution_profile, 'execution_profile', parentRunId);
|
|
695
729
|
const idSuffix = fnv1a64Hex(`${parentRunId}|${blockedEvent.eventId}`).slice(0, 8);
|
|
696
|
-
return { title, repo, description, scope, priority, role: recoveryRole, playbookId, pipelineId, params, routeDecision,
|
|
730
|
+
return { title, repo, description, scope, priority, role: recoveryRole, playbookId, pipelineId, params, routeDecision, now, idSuffix };
|
|
697
731
|
}
|
|
698
732
|
async createRecoveryRunOrReuseExpected(input, expected) {
|
|
699
733
|
try {
|
|
@@ -1063,7 +1097,7 @@ let TaskControlPlaneApiService = class TaskControlPlaneApiService {
|
|
|
1063
1097
|
const item = await this.getInboxItem(input.inboxId);
|
|
1064
1098
|
const topic = gateTopic(item);
|
|
1065
1099
|
if (!topic) {
|
|
1066
|
-
throw new ControlPlaneError('VALIDATION_FAILURE', `inbox item is not a
|
|
1100
|
+
throw new ControlPlaneError('VALIDATION_FAILURE', `inbox item is not a named approval gate: ${input.inboxId}`);
|
|
1067
1101
|
}
|
|
1068
1102
|
const outcomes = gateOutcomes(item);
|
|
1069
1103
|
if (outcomes.length === 0) {
|
|
@@ -1074,6 +1108,7 @@ let TaskControlPlaneApiService = class TaskControlPlaneApiService {
|
|
|
1074
1108
|
throw new ControlPlaneError('VALIDATION_FAILURE', `invalid gate outcome ${outcome}; expected one of: ${outcomes.join(', ')}`);
|
|
1075
1109
|
}
|
|
1076
1110
|
const note = input.note?.trim();
|
|
1111
|
+
const reconcile = normalizeGateReconcile(input.reconcile);
|
|
1077
1112
|
assertRequiredGateNote(item, outcome, note);
|
|
1078
1113
|
const adoptionAudit = outcome === 'adopt_patch_manually'
|
|
1079
1114
|
? validateManualAdoptionAudit(input.adoptionAudit, item)
|
|
@@ -1085,6 +1120,7 @@ let TaskControlPlaneApiService = class TaskControlPlaneApiService {
|
|
|
1085
1120
|
const answer = {
|
|
1086
1121
|
outcome,
|
|
1087
1122
|
...(note ? { note } : {}),
|
|
1123
|
+
...(reconcile ? { reconcile } : {}),
|
|
1088
1124
|
...(adoptionAudit ? { adoptionAudit } : {}),
|
|
1089
1125
|
...(mergeOverrideAudit ? { mergeOverrideAudit } : {}),
|
|
1090
1126
|
resolvedBy,
|
|
@@ -1105,6 +1141,8 @@ let TaskControlPlaneApiService = class TaskControlPlaneApiService {
|
|
|
1105
1141
|
assertLegacyGateWrapperAllowed(item, inboxId, outcomes) {
|
|
1106
1142
|
if (outcomes.length > 2
|
|
1107
1143
|
|| outcomes.includes('approve_anyway')
|
|
1144
|
+
|| outcomes.includes('retry')
|
|
1145
|
+
|| outcomes.includes('give_up')
|
|
1108
1146
|
|| outcomes.some((outcome) => isQuestionGateReasonOutcome(item, outcome))) {
|
|
1109
1147
|
throw new ControlPlaneError('VALIDATION_FAILURE', `inbox item has named gate outcomes; use resolve_gate with an explicit outcome: ${inboxId}`);
|
|
1110
1148
|
}
|
|
@@ -1136,12 +1174,27 @@ let TaskControlPlaneApiService = class TaskControlPlaneApiService {
|
|
|
1136
1174
|
if (gateTopic(item)) {
|
|
1137
1175
|
throw new ControlPlaneError('VALIDATION_FAILURE', `inbox item is a gate; use resolve_gate, approve_gate, or reject_gate: ${input.inboxId}`);
|
|
1138
1176
|
}
|
|
1139
|
-
|
|
1177
|
+
const resolvedBy = input.resolvedBy ?? 'mcp';
|
|
1178
|
+
const answerRecord = asRecord(input.answer);
|
|
1179
|
+
if (typeof answerRecord?.outcome === 'string' && answerRecord.outcome.trim() === 'adopt_patch_manually') {
|
|
1180
|
+
validateManualAdoptionAudit(answerRecord.adoptionAudit, item);
|
|
1181
|
+
}
|
|
1182
|
+
const result = await this.inbox.resolveInbox(input.inboxId, input.answer, resolvedBy);
|
|
1183
|
+
const signalTopic = questionSignalTopic(item);
|
|
1184
|
+
const shouldSignal = signalTopic !== null;
|
|
1185
|
+
if (signalTopic) {
|
|
1186
|
+
const signalItem = result.status === 'pending' ? await this.getInboxItem(input.inboxId) : item;
|
|
1187
|
+
const signalResolvedBy = questionSignalResolvedBy(signalItem, result.answer, resolvedBy);
|
|
1188
|
+
await this.signalQuestion(signalItem, signalTopic, result.answer, input.inboxId, signalResolvedBy);
|
|
1189
|
+
}
|
|
1190
|
+
return {
|
|
1140
1191
|
inboxId: input.inboxId,
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1192
|
+
previousStatus: result.status,
|
|
1193
|
+
answer: result.answer,
|
|
1194
|
+
signaled: shouldSignal,
|
|
1195
|
+
topic: shouldSignal ? 'question' : null,
|
|
1196
|
+
runId: item.runId,
|
|
1197
|
+
};
|
|
1145
1198
|
}
|
|
1146
1199
|
async resolveInboxItem(input) {
|
|
1147
1200
|
const item = await this.getInboxItem(input.inboxId);
|
|
@@ -1162,6 +1215,7 @@ let TaskControlPlaneApiService = class TaskControlPlaneApiService {
|
|
|
1162
1215
|
}
|
|
1163
1216
|
this.assertGateOutcome(item, outcome);
|
|
1164
1217
|
const note = typeof answer?.note === 'string' ? answer.note.trim() : '';
|
|
1218
|
+
const reconcile = normalizeGateReconcile(answer.reconcile);
|
|
1165
1219
|
assertRequiredGateNote(item, outcome, note);
|
|
1166
1220
|
const adoptionAudit = outcome === 'adopt_patch_manually'
|
|
1167
1221
|
? validateManualAdoptionAudit(answer.adoptionAudit, item)
|
|
@@ -1173,6 +1227,7 @@ let TaskControlPlaneApiService = class TaskControlPlaneApiService {
|
|
|
1173
1227
|
...answer,
|
|
1174
1228
|
outcome,
|
|
1175
1229
|
...(typeof answer.note === 'string' ? { note } : {}),
|
|
1230
|
+
...(reconcile ? { reconcile } : {}),
|
|
1176
1231
|
...(adoptionAudit ? { adoptionAudit } : {}),
|
|
1177
1232
|
...(mergeOverrideAudit ? { mergeOverrideAudit } : {}),
|
|
1178
1233
|
};
|
|
@@ -1193,18 +1248,35 @@ let TaskControlPlaneApiService = class TaskControlPlaneApiService {
|
|
|
1193
1248
|
};
|
|
1194
1249
|
}
|
|
1195
1250
|
async signalGate(item, topic, answer, inboxId) {
|
|
1251
|
+
const signalTopic = gateSignalTopic(item, topic);
|
|
1196
1252
|
const eventBase = {
|
|
1197
1253
|
runId: item.runId,
|
|
1198
1254
|
taskId: item.taskId,
|
|
1199
1255
|
stepId: item.stepId,
|
|
1200
1256
|
stepKey: `gate:${topic}`,
|
|
1201
1257
|
actor: 'mcp',
|
|
1202
|
-
payload: { inboxId, topic },
|
|
1258
|
+
payload: { inboxId, topic, ...(signalTopic !== topic ? { signalTopic } : {}) },
|
|
1203
1259
|
};
|
|
1204
1260
|
await this.runs.appendEvent({ ...eventBase, type: 'gate_signal_pending' });
|
|
1205
|
-
await this.dbos.signal(item.runId,
|
|
1261
|
+
await this.dbos.signal(item.runId, signalTopic, answer, inboxId);
|
|
1206
1262
|
await this.runs.appendEvent({ ...eventBase, type: 'gate_signaled' });
|
|
1207
1263
|
}
|
|
1264
|
+
async signalQuestion(item, signalTopic, answer, inboxId, resolvedBy) {
|
|
1265
|
+
const stepKey = questionSignalStepKey(item, signalTopic);
|
|
1266
|
+
const eventBase = {
|
|
1267
|
+
runId: item.runId,
|
|
1268
|
+
taskId: questionSignalTaskId(item),
|
|
1269
|
+
stepId: item.stepId,
|
|
1270
|
+
stepKey,
|
|
1271
|
+
actor: 'mcp',
|
|
1272
|
+
idempotencyKey: inboxId,
|
|
1273
|
+
payload: { inboxId, topic: 'question', signalTopic, stepKey },
|
|
1274
|
+
};
|
|
1275
|
+
const signalPayload = { answer, resolvedBy, inboxId };
|
|
1276
|
+
await this.runs.appendEvent({ ...eventBase, type: 'question_signal_pending' });
|
|
1277
|
+
await this.dbos.signal(item.runId, signalTopic, signalPayload, inboxId);
|
|
1278
|
+
await this.runs.appendEvent({ ...eventBase, type: 'question_signaled' });
|
|
1279
|
+
}
|
|
1208
1280
|
async summarizeGateRisk(inboxId) {
|
|
1209
1281
|
const item = await this.getInboxItem(inboxId);
|
|
1210
1282
|
return {
|
|
@@ -1235,6 +1307,80 @@ let TaskControlPlaneApiService = class TaskControlPlaneApiService {
|
|
|
1235
1307
|
listProfiles(input = {}) {
|
|
1236
1308
|
return this.playbooks.listRunProfiles(input);
|
|
1237
1309
|
}
|
|
1310
|
+
listProfilesPage(input) {
|
|
1311
|
+
return this.playbooks.listRunProfilesPage(input);
|
|
1312
|
+
}
|
|
1313
|
+
async getProfile(input) {
|
|
1314
|
+
const playbook = await this.playbooks.resolvePlaybook(input.playbookId);
|
|
1315
|
+
const pipeline = await this.playbooks.resolvePipeline({ playbookId: playbook.id, pipelineId: input.pipelineId });
|
|
1316
|
+
return this.playbooks.resolveRunProfile({
|
|
1317
|
+
playbookId: playbook.id,
|
|
1318
|
+
pipelineId: pipeline.pipelineId,
|
|
1319
|
+
profileId: input.profileId,
|
|
1320
|
+
includeDeprecated: true,
|
|
1321
|
+
});
|
|
1322
|
+
}
|
|
1323
|
+
validateProfile(input) {
|
|
1324
|
+
return this.validateRunProfileForPipeline(input);
|
|
1325
|
+
}
|
|
1326
|
+
async createProfile(input) {
|
|
1327
|
+
const route = await this.validateRunProfileForPipeline(input);
|
|
1328
|
+
const profile = asRecord(route.profileSnapshot);
|
|
1329
|
+
if (!profile)
|
|
1330
|
+
throw new ControlPlaneError('VALIDATION_FAILURE', 'validated profile snapshot is missing');
|
|
1331
|
+
return this.playbooks.createRunProfile({
|
|
1332
|
+
playbookId: route.playbookId,
|
|
1333
|
+
pipelineId: route.basePipelineId ?? route.pipelineId,
|
|
1334
|
+
profileId: input.profileId,
|
|
1335
|
+
displayName: input.displayName,
|
|
1336
|
+
summary: input.summary,
|
|
1337
|
+
profile,
|
|
1338
|
+
status: input.status,
|
|
1339
|
+
});
|
|
1340
|
+
}
|
|
1341
|
+
async updateProfile(input) {
|
|
1342
|
+
let playbookId;
|
|
1343
|
+
let pipelineId = input.pipelineId;
|
|
1344
|
+
let profile;
|
|
1345
|
+
if (input.profile !== undefined) {
|
|
1346
|
+
const route = await this.validateRunProfileForPipeline({
|
|
1347
|
+
playbookId: input.playbookId,
|
|
1348
|
+
pipelineId: input.pipelineId,
|
|
1349
|
+
profile: input.profile,
|
|
1350
|
+
});
|
|
1351
|
+
playbookId = route.playbookId;
|
|
1352
|
+
pipelineId = route.basePipelineId ?? route.pipelineId;
|
|
1353
|
+
profile = asRecord(route.profileSnapshot) ?? undefined;
|
|
1354
|
+
if (!profile)
|
|
1355
|
+
throw new ControlPlaneError('VALIDATION_FAILURE', 'validated profile snapshot is missing');
|
|
1356
|
+
}
|
|
1357
|
+
else {
|
|
1358
|
+
const playbook = await this.playbooks.resolvePlaybook(input.playbookId);
|
|
1359
|
+
const pipeline = await this.playbooks.resolvePipeline({ playbookId: playbook.id, pipelineId: input.pipelineId });
|
|
1360
|
+
playbookId = playbook.id;
|
|
1361
|
+
pipelineId = pipeline.pipelineId;
|
|
1362
|
+
}
|
|
1363
|
+
return this.playbooks.updateRunProfile({
|
|
1364
|
+
playbookId,
|
|
1365
|
+
pipelineId,
|
|
1366
|
+
profileId: input.profileId,
|
|
1367
|
+
expectedProfileRevisionHash: input.expectedProfileRevisionHash,
|
|
1368
|
+
displayName: input.displayName,
|
|
1369
|
+
summary: input.summary,
|
|
1370
|
+
profile,
|
|
1371
|
+
status: input.status,
|
|
1372
|
+
});
|
|
1373
|
+
}
|
|
1374
|
+
async deprecateProfile(input) {
|
|
1375
|
+
const playbook = await this.playbooks.resolvePlaybook(input.playbookId);
|
|
1376
|
+
const pipeline = await this.playbooks.resolvePipeline({ playbookId: playbook.id, pipelineId: input.pipelineId });
|
|
1377
|
+
return this.playbooks.deprecateRunProfile({
|
|
1378
|
+
playbookId: playbook.id,
|
|
1379
|
+
pipelineId: pipeline.pipelineId,
|
|
1380
|
+
profileId: input.profileId,
|
|
1381
|
+
expectedProfileRevisionHash: input.expectedProfileRevisionHash,
|
|
1382
|
+
});
|
|
1383
|
+
}
|
|
1238
1384
|
async getPipeline(pipelineId) {
|
|
1239
1385
|
const result = await this.playbooks.getPipeline(pipelineId);
|
|
1240
1386
|
if (!result)
|
|
@@ -1248,9 +1394,9 @@ let TaskControlPlaneApiService = class TaskControlPlaneApiService {
|
|
|
1248
1394
|
playbookId: input.playbookId,
|
|
1249
1395
|
pipelineId: input.pipeline,
|
|
1250
1396
|
profileId: input.profileId,
|
|
1397
|
+
profile: input.profile,
|
|
1251
1398
|
params: input.params,
|
|
1252
|
-
|
|
1253
|
-
source: input.pipeline ? 'explicit' : 'deterministic-installed-playbook',
|
|
1399
|
+
source: 'explicit',
|
|
1254
1400
|
});
|
|
1255
1401
|
}
|
|
1256
1402
|
async previewPipelineSelection(input) {
|
|
@@ -1270,58 +1416,86 @@ let TaskControlPlaneApiService = class TaskControlPlaneApiService {
|
|
|
1270
1416
|
async routeForRun(run) {
|
|
1271
1417
|
if (isRouteDecision(run.data.route_decision))
|
|
1272
1418
|
return run.data.route_decision;
|
|
1273
|
-
|
|
1274
|
-
title: typeof run.data.title === 'string' ? run.data.title : 'Run',
|
|
1275
|
-
repo: Array.isArray(run.data.repos) && typeof run.data.repos[0] === 'string' ? run.data.repos[0] : '',
|
|
1276
|
-
description: typeof run.data.description === 'string' ? run.data.description : '',
|
|
1277
|
-
scope: typeof run.data.scope === 'string' ? run.data.scope : '',
|
|
1278
|
-
playbookId: typeof run.data.playbook_id === 'string' ? run.data.playbook_id : undefined,
|
|
1279
|
-
pipelineId: typeof run.data.pipeline_id === 'string' ? run.data.pipeline_id : undefined,
|
|
1280
|
-
params: run.data.params,
|
|
1281
|
-
source: run.data.pipeline_id ? 'explicit' : 'deterministic-installed-playbook',
|
|
1282
|
-
});
|
|
1419
|
+
throw new ControlPlaneError('VALIDATION_FAILURE', 'run route_decision is missing or invalid');
|
|
1283
1420
|
}
|
|
1284
1421
|
async resolveRouteDecision(input) {
|
|
1285
1422
|
const params = normalizeParams(input.params, input.issueRef, input.issueAction);
|
|
1286
|
-
const callerProfile = normalizeExecutionProfile(input.executionProfile);
|
|
1287
1423
|
const playbook = await this.playbooks.resolvePlaybook(input.playbookId);
|
|
1288
|
-
const requestedPipelineId = input.pipelineId
|
|
1289
|
-
|
|
1424
|
+
const requestedPipelineId = input.pipelineId?.trim();
|
|
1425
|
+
if (!requestedPipelineId) {
|
|
1426
|
+
throw new ControlPlaneError('VALIDATION_FAILURE', 'pipelineId is required');
|
|
1427
|
+
}
|
|
1428
|
+
const hasProfileId = input.profileId !== undefined;
|
|
1429
|
+
const hasInlineProfile = input.profile !== undefined;
|
|
1430
|
+
if (hasProfileId === hasInlineProfile) {
|
|
1431
|
+
throw new ControlPlaneError('VALIDATION_FAILURE', 'exactly one of profileId or profile is required');
|
|
1432
|
+
}
|
|
1433
|
+
if (input.profileId?.trim() === '') {
|
|
1434
|
+
throw new ControlPlaneError('VALIDATION_FAILURE', 'profileId must be a non-empty string');
|
|
1435
|
+
}
|
|
1290
1436
|
const pipeline = await this.playbooks.resolvePipeline({ playbookId: playbook.id, pipelineId: requestedPipelineId });
|
|
1291
|
-
let
|
|
1292
|
-
let
|
|
1293
|
-
|
|
1437
|
+
let profileSnapshot;
|
|
1438
|
+
let profileHash;
|
|
1439
|
+
let provenanceFields;
|
|
1294
1440
|
if (input.profileId !== undefined) {
|
|
1295
1441
|
const storedProfile = await this.playbooks.resolveRunProfile({
|
|
1296
1442
|
playbookId: playbook.id,
|
|
1297
1443
|
pipelineId: pipeline.pipelineId,
|
|
1298
1444
|
profileId: input.profileId,
|
|
1299
1445
|
});
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1446
|
+
profileSnapshot = storedProfile.profile;
|
|
1447
|
+
this.assertStoredRunProfileValid(storedProfile);
|
|
1448
|
+
profileHash = runProfileHash(profileSnapshot, {
|
|
1449
|
+
pipelineId: storedProfile.pipelineId,
|
|
1450
|
+
schemaVersion: storedProfile.schemaVersion,
|
|
1451
|
+
});
|
|
1452
|
+
if (storedProfile.profileHash !== profileHash) {
|
|
1453
|
+
throw new ControlPlaneError('VALIDATION_FAILURE', `stored run profile ${storedProfile.profileId} hash mismatch: expected ${storedProfile.profileHash}, got ${profileHash}`);
|
|
1454
|
+
}
|
|
1455
|
+
provenanceFields = {
|
|
1456
|
+
profileSource: 'stored',
|
|
1457
|
+
profileId: storedProfile.profileId,
|
|
1458
|
+
profileVersion: storedProfile.version,
|
|
1459
|
+
};
|
|
1460
|
+
}
|
|
1461
|
+
else {
|
|
1462
|
+
const inlineProfile = asRecord(input.profile);
|
|
1463
|
+
if (!inlineProfile) {
|
|
1464
|
+
throw new ControlPlaneError('VALIDATION_FAILURE', 'profile must be an object');
|
|
1465
|
+
}
|
|
1466
|
+
try {
|
|
1467
|
+
assertValidInlineRunProfile(inlineProfile, 'profile');
|
|
1304
1468
|
}
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
throw new ControlPlaneError('VALIDATION_FAILURE',
|
|
1469
|
+
catch (error) {
|
|
1470
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1471
|
+
throw new ControlPlaneError('VALIDATION_FAILURE', message);
|
|
1308
1472
|
}
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
provenanceFields
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
provenanceFields.
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
provenanceFields.policyVersion = POLICY_VERSION;
|
|
1473
|
+
profileSnapshot = inlineProfile;
|
|
1474
|
+
profileHash = runProfileHash(profileSnapshot, { pipelineId: pipeline.pipelineId });
|
|
1475
|
+
provenanceFields = { profileSource: 'inline' };
|
|
1476
|
+
}
|
|
1477
|
+
const topologyProfile = topologyProfileFromRunProfile(profileSnapshot, {
|
|
1478
|
+
pipelineId: pipeline.pipelineId,
|
|
1479
|
+
profileId: provenanceFields.profileId ?? 'inline',
|
|
1480
|
+
});
|
|
1481
|
+
const baseTemplate = pipeline.executionPolicy.template_json;
|
|
1482
|
+
if (!baseTemplate) {
|
|
1483
|
+
throw new ControlPlaneError('VALIDATION_FAILURE', `pipeline "${pipeline.pipelineId}" carries no template_json`);
|
|
1321
1484
|
}
|
|
1485
|
+
this.assertRunProfileStagesClosed(profileSnapshot, baseTemplate);
|
|
1486
|
+
const { template: materializedTemplate, materializedTemplateHash, diagnostics } = materializeTemplate(baseTemplate, topologyProfile, { allowlist: topologyProfile.toggles.map((toggle) => toggle.target) });
|
|
1487
|
+
if (diagnostics.length > 0) {
|
|
1488
|
+
throw new ControlPlaneError('VALIDATION_FAILURE', `run profile cannot be materialized: ${diagnostics.map((item) => item.message).join('; ')}`, { details: { diagnostics } });
|
|
1489
|
+
}
|
|
1490
|
+
const executionPolicy = { ...asRecord(pipeline.executionPolicy), template_json: materializedTemplate };
|
|
1491
|
+
const launchBindings = launchBindingsFromRunProfile(profileSnapshot, {
|
|
1492
|
+
lifecycleNodeIds: Object.entries(materializedTemplate.nodes)
|
|
1493
|
+
.filter(([, node]) => node.kind === 'script')
|
|
1494
|
+
.map(([nodeId]) => nodeId),
|
|
1495
|
+
});
|
|
1322
1496
|
const allRoles = (await this.roles.listRoles()).filter((role) => role.playbookId === playbook.id);
|
|
1323
|
-
await this.
|
|
1324
|
-
const roleBindings = await this.resolveRouteRoles(playbook.id, pipeline,
|
|
1497
|
+
await this.assertLaunchBindingsClosed(launchBindings, allRoles, materializedTemplate);
|
|
1498
|
+
const roleBindings = await this.resolveRouteRoles(playbook.id, pipeline, launchBindings, allRoles);
|
|
1325
1499
|
return {
|
|
1326
1500
|
playbookId: playbook.id,
|
|
1327
1501
|
pipelineId: requestedPipelineId,
|
|
@@ -1332,101 +1506,178 @@ let TaskControlPlaneApiService = class TaskControlPlaneApiService {
|
|
|
1332
1506
|
optionalRoles: pipeline.optionalRoles,
|
|
1333
1507
|
routeGates: normalizeRouteGates(pipeline.routeGates),
|
|
1334
1508
|
executionPolicy,
|
|
1335
|
-
|
|
1509
|
+
launchBindings,
|
|
1336
1510
|
roleBindings,
|
|
1337
1511
|
params,
|
|
1512
|
+
requestedPipelineId,
|
|
1513
|
+
basePipelineId: pipeline.pipelineId,
|
|
1514
|
+
profileHash,
|
|
1515
|
+
profileSnapshot,
|
|
1516
|
+
materializedTemplateHash,
|
|
1517
|
+
materializedTemplate,
|
|
1518
|
+
materializerVersion: MATERIALIZER_VERSION,
|
|
1519
|
+
policyVersion: POLICY_VERSION,
|
|
1338
1520
|
...provenanceFields,
|
|
1339
1521
|
};
|
|
1340
1522
|
}
|
|
1341
|
-
async
|
|
1342
|
-
|
|
1343
|
-
|
|
1523
|
+
async validateRunProfileForPipeline(input) {
|
|
1524
|
+
return this.resolveRouteDecision({
|
|
1525
|
+
title: 'Run profile validation',
|
|
1526
|
+
repo: '',
|
|
1527
|
+
playbookId: input.playbookId,
|
|
1528
|
+
pipelineId: input.pipelineId,
|
|
1529
|
+
profile: input.profile,
|
|
1530
|
+
source: 'explicit',
|
|
1531
|
+
});
|
|
1532
|
+
}
|
|
1533
|
+
assertStoredRunProfileValid(storedProfile) {
|
|
1534
|
+
try {
|
|
1535
|
+
assertValidInlineRunProfile(storedProfile.profile, `run_profiles.${storedProfile.profileId}.profile_json`);
|
|
1536
|
+
}
|
|
1537
|
+
catch (error) {
|
|
1538
|
+
throw new ControlPlaneError('VALIDATION_FAILURE', asErrorMessage(error));
|
|
1539
|
+
}
|
|
1540
|
+
}
|
|
1541
|
+
assertRunProfileStagesClosed(profileSnapshot, template) {
|
|
1542
|
+
const nodeIds = new Set(Object.keys(template.nodes));
|
|
1543
|
+
for (const stage of topologyStageTargetsFromRunProfile(profileSnapshot)) {
|
|
1544
|
+
if (nodeIds.has(stage))
|
|
1545
|
+
continue;
|
|
1546
|
+
throw this.profileSchemaClosed(`topology stage "${stage}" does not exist in pipeline ${template.pipelineId}`);
|
|
1547
|
+
}
|
|
1548
|
+
}
|
|
1549
|
+
async assertLaunchBindingsClosed(launchBindings, roles, template) {
|
|
1550
|
+
if (launchBindings.length === 0)
|
|
1344
1551
|
return;
|
|
1345
|
-
const available = executionProfile.availableRunners;
|
|
1346
1552
|
const byPlaybookRole = new Map(roles.map((r) => [r.playbookRoleId || r.name, r]));
|
|
1553
|
+
const nodes = template.nodes;
|
|
1347
1554
|
const cachedProfiles = new Map();
|
|
1348
|
-
for (const override of
|
|
1555
|
+
for (const override of launchBindings) {
|
|
1349
1556
|
const matchLabel = JSON.stringify(override.match);
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
}
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1557
|
+
this.assertBindingMatchIsClosed(override, matchLabel);
|
|
1558
|
+
this.assertBindingTargetIsKnown(override, matchLabel, byPlaybookRole, nodes);
|
|
1559
|
+
this.assertScriptBindingShape(override, matchLabel, nodes);
|
|
1560
|
+
this.assertBindingAccounts(override, matchLabel, nodes);
|
|
1561
|
+
this.assertRegisteredRunner(override.match.runnerId, matchLabel);
|
|
1562
|
+
this.assertRegisteredRunner(override.runnerId, matchLabel);
|
|
1563
|
+
await this.assertModelLevelAvailable(override.modelLevel, matchLabel, cachedProfiles);
|
|
1564
|
+
this.assertBindingTimeout(override, matchLabel);
|
|
1565
|
+
this.assertBindingPermissionMode(override, matchLabel, byPlaybookRole, launchBindings);
|
|
1566
|
+
}
|
|
1567
|
+
}
|
|
1568
|
+
profileSchemaClosed(message) {
|
|
1569
|
+
return new ControlPlaneError('VALIDATION_FAILURE', `PROFILE_SCHEMA_CLOSED: ${message}`);
|
|
1570
|
+
}
|
|
1571
|
+
assertBindingMatchIsClosed(override, matchLabel) {
|
|
1572
|
+
const { roleId, nodeId, runnerId } = override.match;
|
|
1573
|
+
if (roleId || nodeId || runnerId)
|
|
1574
|
+
return;
|
|
1575
|
+
throw this.profileSchemaClosed(`bindingOverride match ${matchLabel} must specify at least one of roleId, nodeId, runnerId`);
|
|
1576
|
+
}
|
|
1577
|
+
assertBindingTargetIsKnown(override, matchLabel, byPlaybookRole, nodes) {
|
|
1578
|
+
const { roleId, nodeId } = override.match;
|
|
1579
|
+
if (roleId !== undefined && !byPlaybookRole.has(roleId)) {
|
|
1580
|
+
throw this.profileSchemaClosed(`bindingOverride match ${matchLabel} roleId "${roleId}" does not exist in the selected playbook`);
|
|
1581
|
+
}
|
|
1582
|
+
if (nodeId !== undefined && !nodes[nodeId]) {
|
|
1583
|
+
throw this.profileSchemaClosed(`bindingOverride match ${matchLabel} nodeId "${nodeId}" does not exist in the selected pipeline`);
|
|
1584
|
+
}
|
|
1585
|
+
}
|
|
1586
|
+
assertScriptBindingShape(override, matchLabel, nodes) {
|
|
1587
|
+
const nodeId = override.match.nodeId;
|
|
1588
|
+
if (!nodeId || nodes[nodeId]?.kind !== 'script')
|
|
1589
|
+
return;
|
|
1590
|
+
if (override.runnerId || override.modelLevel || override.timeoutMs !== undefined || override.permissionMode) {
|
|
1591
|
+
throw this.profileSchemaClosed(`bindingOverride match ${matchLabel} targets script node "${nodeId}" and must not set runnerId, modelLevel, timeoutMs, or permissionMode`);
|
|
1592
|
+
}
|
|
1593
|
+
}
|
|
1594
|
+
assertBindingAccounts(override, matchLabel, nodes) {
|
|
1595
|
+
const github = override.accounts?.github;
|
|
1596
|
+
if (github === undefined)
|
|
1597
|
+
return;
|
|
1598
|
+
if (!github.trim()) {
|
|
1599
|
+
throw this.profileSchemaClosed(`bindingOverride match ${matchLabel} accounts.github must be a non-empty string`);
|
|
1600
|
+
}
|
|
1601
|
+
const nodeId = override.match.nodeId;
|
|
1602
|
+
if (!nodeId || nodes[nodeId]?.kind !== 'script') {
|
|
1603
|
+
throw this.profileSchemaClosed(`bindingOverride match ${matchLabel} accounts are only supported for script nodes`);
|
|
1604
|
+
}
|
|
1605
|
+
}
|
|
1606
|
+
assertRegisteredRunner(runnerId, matchLabel) {
|
|
1607
|
+
if (runnerId === undefined || BUILTIN_RUNNERS.has(runnerId))
|
|
1608
|
+
return;
|
|
1609
|
+
throw this.profileSchemaClosed(`bindingOverride match ${matchLabel} runnerId "${runnerId}" is not a registered runner`);
|
|
1610
|
+
}
|
|
1611
|
+
async assertModelLevelAvailable(modelLevel, matchLabel, cachedProfiles) {
|
|
1612
|
+
if (modelLevel === undefined)
|
|
1613
|
+
return;
|
|
1614
|
+
if (!VALID_MODEL_LEVELS.includes(modelLevel)) {
|
|
1615
|
+
throw this.unavailableModelLevel(matchLabel, modelLevel);
|
|
1616
|
+
}
|
|
1617
|
+
const cached = cachedProfiles.get(modelLevel);
|
|
1618
|
+
if (cached === true)
|
|
1619
|
+
return;
|
|
1620
|
+
if (cached === false)
|
|
1621
|
+
throw this.unavailableModelLevel(matchLabel, modelLevel);
|
|
1622
|
+
try {
|
|
1623
|
+
await this.roles.loadModelProfile(modelLevel);
|
|
1624
|
+
cachedProfiles.set(modelLevel, true);
|
|
1625
|
+
}
|
|
1626
|
+
catch (err) {
|
|
1627
|
+
const isUnavailable = err instanceof ControlPlaneError && (err.code === 'ROW_NOT_FOUND' || err.code === 'VALIDATION_FAILURE');
|
|
1628
|
+
if (!isUnavailable)
|
|
1629
|
+
throw err;
|
|
1630
|
+
cachedProfiles.set(modelLevel, false);
|
|
1631
|
+
throw this.unavailableModelLevel(matchLabel, modelLevel);
|
|
1632
|
+
}
|
|
1633
|
+
}
|
|
1634
|
+
unavailableModelLevel(matchLabel, modelLevel) {
|
|
1635
|
+
return this.profileSchemaClosed(`bindingOverride match ${matchLabel} modelLevel "${modelLevel}" is unavailable`);
|
|
1636
|
+
}
|
|
1637
|
+
assertBindingTimeout(override, matchLabel) {
|
|
1638
|
+
const ms = override.timeoutMs;
|
|
1639
|
+
if (ms === undefined || (Number.isInteger(ms) && ms > 0 && ms <= 86_400_000))
|
|
1640
|
+
return;
|
|
1641
|
+
throw this.profileSchemaClosed(`bindingOverride match ${matchLabel} timeoutMs must be a positive integer <= 86400000, got ${ms}`);
|
|
1642
|
+
}
|
|
1643
|
+
assertBindingPermissionMode(override, matchLabel, byPlaybookRole, launchBindings) {
|
|
1644
|
+
const permissionMode = override.permissionMode;
|
|
1645
|
+
if (permissionMode === undefined)
|
|
1646
|
+
return;
|
|
1647
|
+
const effectiveRunner = this.effectivePermissionRunner(override, matchLabel, byPlaybookRole, launchBindings);
|
|
1648
|
+
const validModes = RUNNER_PERMISSION_MODES[effectiveRunner];
|
|
1649
|
+
if (!validModes) {
|
|
1650
|
+
throw this.profileSchemaClosed(`bindingOverride match ${matchLabel} permissionMode not supported for runner ${effectiveRunner}`);
|
|
1651
|
+
}
|
|
1652
|
+
if (!validModes.includes(permissionMode)) {
|
|
1653
|
+
throw this.profileSchemaClosed(`bindingOverride match ${matchLabel} permissionMode "${permissionMode}" is not valid for runner ${effectiveRunner}; expected one of ${validModes.join(', ')}`);
|
|
1654
|
+
}
|
|
1655
|
+
}
|
|
1656
|
+
effectivePermissionRunner(override, matchLabel, byPlaybookRole, launchBindings) {
|
|
1657
|
+
if (override.runnerId)
|
|
1658
|
+
return override.runnerId;
|
|
1659
|
+
if (override.match.runnerId)
|
|
1660
|
+
return override.match.runnerId;
|
|
1661
|
+
const roleId = override.match.roleId;
|
|
1662
|
+
if (!roleId)
|
|
1663
|
+
throw this.unresolvablePermissionRunner(matchLabel);
|
|
1664
|
+
const role = byPlaybookRole.get(roleId);
|
|
1665
|
+
if (!role)
|
|
1666
|
+
throw this.unresolvablePermissionRunner(matchLabel);
|
|
1667
|
+
const roleOverride = this.findLastRoleRunnerOverride(launchBindings, roleId);
|
|
1668
|
+
return roleOverride?.runnerId ?? role.runner;
|
|
1669
|
+
}
|
|
1670
|
+
findLastRoleRunnerOverride(launchBindings, roleId) {
|
|
1671
|
+
for (let index = launchBindings.length - 1; index >= 0; index -= 1) {
|
|
1672
|
+
const candidate = launchBindings[index];
|
|
1673
|
+
if (candidate.match.roleId === roleId && !candidate.match.nodeId && candidate.runnerId !== undefined) {
|
|
1674
|
+
return candidate;
|
|
1428
1675
|
}
|
|
1429
1676
|
}
|
|
1677
|
+
return undefined;
|
|
1678
|
+
}
|
|
1679
|
+
unresolvablePermissionRunner(matchLabel) {
|
|
1680
|
+
return this.profileSchemaClosed(`bindingOverride match ${matchLabel} permissionMode override requires a resolvable runner; pin runnerId`);
|
|
1430
1681
|
}
|
|
1431
1682
|
async resolveAutoPipeline(playbookId, text) {
|
|
1432
1683
|
const pipelines = (await this.playbooks.listPipelines())
|
|
@@ -1448,7 +1699,7 @@ let TaskControlPlaneApiService = class TaskControlPlaneApiService {
|
|
|
1448
1699
|
}
|
|
1449
1700
|
return best.pipeline;
|
|
1450
1701
|
}
|
|
1451
|
-
async resolveRouteRoles(playbookId, pipeline,
|
|
1702
|
+
async resolveRouteRoles(playbookId, pipeline, launchBindings, preloadedRoles) {
|
|
1452
1703
|
const roles = preloadedRoles ?? (await this.roles.listRoles()).filter((role) => role.playbookId === playbookId);
|
|
1453
1704
|
const byPlaybookRole = new Map(roles.map((role) => [role.playbookRoleId || role.name, role]));
|
|
1454
1705
|
const selected = [...pipeline.requiredRoles];
|
|
@@ -1468,10 +1719,10 @@ let TaskControlPlaneApiService = class TaskControlPlaneApiService {
|
|
|
1468
1719
|
return selected.map((roleId) => {
|
|
1469
1720
|
const role = byPlaybookRole.get(roleId);
|
|
1470
1721
|
assertProductionRunnerBinding(role.runner, 'playbook', roleId);
|
|
1471
|
-
const runnerResolved = resolveRunnerForRole(role.runner, roleId,
|
|
1722
|
+
const runnerResolved = resolveRunnerForRole(role.runner, roleId, launchBindings);
|
|
1472
1723
|
assertProductionRunnerBinding(runnerResolved.runnerId, runnerResolved.source, roleId);
|
|
1473
|
-
assertRunnerAvailable(runnerResolved.runnerId,
|
|
1474
|
-
const bindingResolution = resolveBindingForRole(role, roleId, runnerResolved.runnerId,
|
|
1724
|
+
assertRunnerAvailable(runnerResolved.runnerId, roleId);
|
|
1725
|
+
const bindingResolution = resolveBindingForRole(role, roleId, runnerResolved.runnerId, launchBindings);
|
|
1475
1726
|
return {
|
|
1476
1727
|
roleId,
|
|
1477
1728
|
rowId: role.id,
|