@revisium/orchestrator 0.2.0-alpha.3 → 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 +4 -4
- package/control-plane/bootstrap.config.json +108 -550
- package/control-plane/default-playbook/catalog/pipelines.json +446 -1123
- package/control-plane/default-playbook/catalog/roles.json +1 -1
- package/control-plane/default-playbook/catalog/run-profiles.json +338 -0
- package/control-plane/default-playbook/package.json +1 -1
- package/control-plane/default-playbook/playbook.json +2 -1
- package/control-plane/default-playbook/prompts/developer.md +6 -6
- package/control-plane/default-playbook/prompts/integrator.md +1 -1
- package/dist/__generated__/client/browser.js +18 -0
- package/dist/__generated__/client/browser.js.map +1 -0
- package/dist/__generated__/client/client.js +32 -0
- package/dist/__generated__/client/client.js.map +1 -0
- package/dist/__generated__/client/commonInputTypes.js +11 -0
- package/dist/__generated__/client/commonInputTypes.js.map +1 -0
- package/dist/__generated__/client/enums.js +19 -0
- package/dist/__generated__/client/enums.js.map +1 -0
- package/dist/__generated__/client/internal/class.js +53 -0
- package/dist/__generated__/client/internal/class.js.map +1 -0
- package/dist/__generated__/client/internal/prismaNamespace.js +450 -0
- package/dist/__generated__/client/internal/prismaNamespace.js.map +1 -0
- package/dist/__generated__/client/internal/prismaNamespaceBrowser.js +421 -0
- package/dist/__generated__/client/internal/prismaNamespaceBrowser.js.map +1 -0
- package/dist/__generated__/client/models/Branch.js +2 -0
- package/dist/__generated__/client/models/Branch.js.map +1 -0
- package/dist/__generated__/client/models/CostLedgerEntry.js +2 -0
- package/dist/__generated__/client/models/CostLedgerEntry.js.map +1 -0
- package/dist/__generated__/client/models/FileBlob.js +2 -0
- package/dist/__generated__/client/models/FileBlob.js.map +1 -0
- package/dist/__generated__/client/models/InboxItem.js +2 -0
- package/dist/__generated__/client/models/InboxItem.js.map +1 -0
- package/dist/__generated__/client/models/ProjectFileUsage.js +2 -0
- package/dist/__generated__/client/models/ProjectFileUsage.js.map +1 -0
- package/dist/__generated__/client/models/Revision.js +2 -0
- package/dist/__generated__/client/models/Revision.js.map +1 -0
- package/dist/__generated__/client/models/RevoProject.js +2 -0
- package/dist/__generated__/client/models/RevoProject.js.map +1 -0
- package/dist/__generated__/client/models/Row.js +2 -0
- package/dist/__generated__/client/models/Row.js.map +1 -0
- package/dist/__generated__/client/models/RunAttempt.js +2 -0
- package/dist/__generated__/client/models/RunAttempt.js.map +1 -0
- package/dist/__generated__/client/models/RunEvent.js +2 -0
- package/dist/__generated__/client/models/RunEvent.js.map +1 -0
- package/dist/__generated__/client/models/RunOutput.js +2 -0
- package/dist/__generated__/client/models/RunOutput.js.map +1 -0
- package/dist/__generated__/client/models/RunTask.js +2 -0
- package/dist/__generated__/client/models/RunTask.js.map +1 -0
- package/dist/__generated__/client/models/Table.js +2 -0
- package/dist/__generated__/client/models/Table.js.map +1 -0
- package/dist/__generated__/client/models/TableMigration.js +2 -0
- package/dist/__generated__/client/models/TableMigration.js.map +1 -0
- package/dist/__generated__/client/models/TaskRun.js +2 -0
- package/dist/__generated__/client/models/TaskRun.js.map +1 -0
- package/dist/__generated__/client/models.js +2 -0
- package/dist/__generated__/client/models.js.map +1 -0
- package/dist/api/graphql-api/graphql-ws/pubsub.module.js +2 -1
- package/dist/api/graphql-api/graphql-ws/pubsub.module.js.map +1 -1
- package/dist/api/graphql-api/graphql-ws/subscription-bridge.service.js +110 -28
- package/dist/api/graphql-api/graphql-ws/subscription-bridge.service.js.map +1 -1
- package/dist/api/graphql-api/graphql-ws/subscription-mappers.js +3 -1
- package/dist/api/graphql-api/graphql-ws/subscription-mappers.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/pr/inputs/pr-readiness.input.js +6 -0
- package/dist/api/graphql-api/pr/inputs/pr-readiness.input.js.map +1 -1
- package/dist/api/graphql-api/registerGraphqlEnums.js +6 -0
- package/dist/api/graphql-api/registerGraphqlEnums.js.map +1 -1
- package/dist/api/graphql-api/runs/inputs/create-run.input.js +17 -1
- 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 +11 -1
- package/dist/api/graphql-api/runs/inputs/simulate-route.input.js.map +1 -1
- package/dist/api/graphql-api/runs/model/run-workflow.model.js +5 -0
- package/dist/api/graphql-api/runs/model/run-workflow.model.js.map +1 -1
- package/dist/api/graphql-api/share/model/issue-action.model.js +7 -0
- package/dist/api/graphql-api/share/model/issue-action.model.js.map +1 -0
- package/dist/cli/commands/doctor-report.js +1 -13
- package/dist/cli/commands/doctor-report.js.map +1 -1
- package/dist/cli/commands/lifecycle.js +50 -65
- package/dist/cli/commands/lifecycle.js.map +1 -1
- package/dist/cli/commands/{revisium-helpers.js → process-helpers.js} +1 -24
- package/dist/cli/commands/process-helpers.js.map +1 -0
- package/dist/config.js +17 -55
- package/dist/config.js.map +1 -1
- package/dist/control-plane/bootstrap-json-validator.js +65 -0
- package/dist/control-plane/bootstrap-json-validator.js.map +1 -0
- package/dist/control-plane/bootstrap.js +25 -38
- package/dist/control-plane/bootstrap.js.map +1 -1
- package/dist/control-plane/change-notifications.js +45 -14
- package/dist/control-plane/change-notifications.js.map +1 -1
- package/dist/control-plane/data-access.js +1 -106
- package/dist/control-plane/data-access.js.map +1 -1
- package/dist/control-plane/default-playbook-policy.js +488 -94
- package/dist/control-plane/default-playbook-policy.js.map +1 -1
- package/dist/control-plane/definitions.js +4 -5
- package/dist/control-plane/definitions.js.map +1 -1
- package/dist/control-plane/engine-transport.js +383 -0
- package/dist/control-plane/engine-transport.js.map +1 -0
- package/dist/control-plane/inbox.js +15 -7
- package/dist/control-plane/inbox.js.map +1 -1
- package/dist/control-plane/index.js +0 -1
- package/dist/control-plane/index.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/merge-override-audit.js +2 -2
- package/dist/control-plane/merge-override-audit.js.map +1 -1
- package/dist/control-plane/pipeline-coverage-registry.js +486 -0
- package/dist/control-plane/pipeline-coverage-registry.js.map +1 -0
- package/dist/control-plane/query-types.js +2 -0
- package/dist/control-plane/query-types.js.map +1 -0
- package/dist/control-plane/run-profiles.js +175 -0
- package/dist/control-plane/run-profiles.js.map +1 -0
- package/dist/control-plane/schema-migration.js +0 -35
- package/dist/control-plane/schema-migration.js.map +1 -1
- package/dist/control-plane/seed-default-playbook.js +2 -31
- package/dist/control-plane/seed-default-playbook.js.map +1 -1
- package/dist/control-plane/tables.js +8 -0
- package/dist/control-plane/tables.js.map +1 -1
- package/dist/control-plane/transport.js +16 -0
- package/dist/control-plane/transport.js.map +1 -0
- package/dist/control-plane/versioned-meaning.js +72 -15
- package/dist/control-plane/versioned-meaning.js.map +1 -1
- package/dist/e2e/kit/agents.js +63 -8
- package/dist/e2e/kit/agents.js.map +1 -1
- package/dist/e2e/kit/assertions.js +15 -3
- package/dist/e2e/kit/assertions.js.map +1 -1
- package/dist/e2e/kit/crash.js +8 -4
- package/dist/e2e/kit/crash.js.map +1 -1
- package/dist/e2e/kit/drive.js +46 -11
- package/dist/e2e/kit/drive.js.map +1 -1
- package/dist/e2e/kit/env.js +17 -1
- package/dist/e2e/kit/env.js.map +1 -1
- package/dist/e2e/kit/fake-integrator.js +9 -22
- package/dist/e2e/kit/fake-integrator.js.map +1 -1
- package/dist/e2e/kit/gh-emulator.js +166 -26
- package/dist/e2e/kit/gh-emulator.js.map +1 -1
- package/dist/e2e/kit/harness.js +44 -10
- package/dist/e2e/kit/harness.js.map +1 -1
- package/dist/e2e/kit/index.js +2 -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 +207 -0
- package/dist/e2e/kit/scenario.js.map +1 -0
- package/dist/e2e/kit/scenarios.js +38 -45
- 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/engine/dbos.service.js +8 -1
- package/dist/engine/dbos.service.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/pr/queries/impl/get-pr-readiness.query.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/host/daemon.js +32 -8
- package/dist/host/daemon.js.map +1 -1
- package/dist/host/ensure-host.js +3 -3
- package/dist/host/ensure-host.js.map +1 -1
- package/dist/host/host-runtime.js.map +1 -1
- package/dist/host/host.lifecycle.js +3 -16
- package/dist/host/host.lifecycle.js.map +1 -1
- package/dist/http/graphql-host.js +17 -8
- package/dist/http/graphql-host.js.map +1 -1
- package/dist/mcp/mcp-capabilities.js +8 -2
- package/dist/mcp/mcp-capabilities.js.map +1 -1
- package/dist/mcp/mcp-facade.service.js +53 -10
- 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 +108 -1
- 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 +573 -105
- package/dist/pipeline/data-driven-task.workflow.js.map +1 -1
- package/dist/pipeline/pipeline.service.js +28 -12
- package/dist/pipeline/pipeline.service.js.map +1 -1
- package/dist/pipeline/route-contract.js +80 -39
- package/dist/pipeline/route-contract.js.map +1 -1
- package/dist/pipeline-core/condition-scopes.js +15 -0
- package/dist/pipeline-core/condition-scopes.js.map +1 -0
- package/dist/pipeline-core/index.js +2 -0
- package/dist/pipeline-core/index.js.map +1 -1
- package/dist/pipeline-core/kit/fixtures.js +81 -44
- package/dist/pipeline-core/kit/fixtures.js.map +1 -1
- package/dist/pipeline-core/materialize.js +221 -0
- package/dist/pipeline-core/materialize.js.map +1 -0
- package/dist/pipeline-core/types.js +5 -0
- package/dist/pipeline-core/types.js.map +1 -1
- package/dist/pipeline-core/validate-loops.js +2 -15
- package/dist/pipeline-core/validate-loops.js.map +1 -1
- package/dist/pipeline-core/validate-resilience.js +257 -0
- package/dist/pipeline-core/validate-resilience.js.map +1 -0
- package/dist/pipeline-core/validate.js +2 -0
- package/dist/pipeline-core/validate.js.map +1 -1
- package/dist/playbook/catalog-loader.js +40 -3
- package/dist/playbook/catalog-loader.js.map +1 -1
- package/dist/playbook/catalog-schema-validator.js +468 -0
- package/dist/playbook/catalog-schema-validator.js.map +1 -0
- package/dist/playbook/import-mapper.js +55 -3
- package/dist/playbook/import-mapper.js.map +1 -1
- package/dist/playbook/manifest.js +5 -0
- package/dist/playbook/manifest.js.map +1 -1
- package/dist/playbook/playbook-installer.js +17 -1
- package/dist/playbook/playbook-installer.js.map +1 -1
- package/dist/poller/pr-readiness-core.js +257 -93
- package/dist/poller/pr-readiness-core.js.map +1 -1
- package/dist/poller/pr-readiness.js +2 -2
- package/dist/poller/pr-readiness.js.map +1 -1
- package/dist/revisium/inbox.service.js +8 -8
- package/dist/revisium/inbox.service.js.map +1 -1
- package/dist/revisium/playbooks.service.js +311 -21
- package/dist/revisium/playbooks.service.js.map +1 -1
- package/dist/revisium/revisium.module.js +8 -8
- package/dist/revisium/revisium.module.js.map +1 -1
- package/dist/revisium/roles.service.js +3 -1
- package/dist/revisium/roles.service.js.map +1 -1
- package/dist/revisium/run.service.js +9 -8
- package/dist/revisium/run.service.js.map +1 -1
- package/dist/revisium/tokens.js +0 -1
- package/dist/revisium/tokens.js.map +1 -1
- package/dist/run/create-run.js +1 -4
- package/dist/run/create-run.js.map +1 -1
- package/dist/run/inspect-run.js +33 -29
- package/dist/run/inspect-run.js.map +1 -1
- package/dist/run/issue-ref.js +97 -2
- package/dist/run/issue-ref.js.map +1 -1
- package/dist/run/prisma-runtime-data-access.js +616 -0
- package/dist/run/prisma-runtime-data-access.js.map +1 -0
- package/dist/run/run-outputs.js +39 -14
- package/dist/run/run-outputs.js.map +1 -1
- package/dist/run/terminal-run-status.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 +692 -89
- package/dist/runners/integrator.js.map +1 -1
- package/dist/runners/worktree.service.js +10 -8
- package/dist/runners/worktree.service.js.map +1 -1
- package/dist/schema/ajv-errors.js +15 -0
- package/dist/schema/ajv-errors.js.map +1 -0
- package/dist/smoke/isolation.js +7 -16
- package/dist/smoke/isolation.js.map +1 -1
- package/dist/storage/ensure-storage.js +118 -0
- package/dist/storage/ensure-storage.js.map +1 -0
- package/dist/storage/revo-database.js +39 -0
- package/dist/storage/revo-database.js.map +1 -0
- package/dist/storage/revo-prisma.service.js +31 -0
- package/dist/storage/revo-prisma.service.js.map +1 -0
- package/dist/storage/revo-storage.module.js +18 -0
- package/dist/storage/revo-storage.module.js.map +1 -0
- package/dist/task-control-plane/pr-readiness.service.js +5 -2
- package/dist/task-control-plane/pr-readiness.service.js.map +1 -1
- package/dist/task-control-plane/run-watch.service.js +3 -6
- package/dist/task-control-plane/run-watch.service.js.map +1 -1
- package/dist/task-control-plane/task-control-plane-api.service.js +518 -85
- package/dist/task-control-plane/task-control-plane-api.service.js.map +1 -1
- package/dist/testing/runtime-data-access.js +175 -0
- package/dist/testing/runtime-data-access.js.map +1 -0
- package/dist/worker/build-context.js +178 -2
- package/dist/worker/build-context.js.map +1 -1
- package/dist/worker/claude-code-runner.js +23 -6
- package/dist/worker/claude-code-runner.js.map +1 -1
- package/dist/worker/codex-runner.js +6 -1
- package/dist/worker/codex-runner.js.map +1 -1
- package/package.json +20 -7
- package/prisma/migrations/20260707080500_initial_schema/migration.sql +259 -0
- package/prisma/migrations/20260707103000_prisma_runtime_tables/migration.sql +234 -0
- package/prisma/migrations/migration_lock.toml +3 -0
- package/prisma/schema.prisma +338 -0
- package/prisma.config.ts +11 -0
- package/dist/cli/commands/revisium-helpers.js.map +0 -1
- package/dist/control-plane/client-transport.js +0 -168
- package/dist/control-plane/client-transport.js.map +0 -1
- package/dist/engine/ensure-postgres.js +0 -46
- package/dist/engine/ensure-postgres.js.map +0 -1
- package/dist/host/ensure-revisium.js +0 -135
- package/dist/host/ensure-revisium.js.map +0 -1
- package/revisium.config.json +0 -10
|
@@ -15,17 +15,24 @@ import { existsSync, readFileSync, statSync } from 'node:fs';
|
|
|
15
15
|
import { basename, join, resolve } from 'node:path';
|
|
16
16
|
import { promisify } from 'node:util';
|
|
17
17
|
import { Inject, Injectable } from '@nestjs/common';
|
|
18
|
-
import { baseUrl, getConfig, isAlive
|
|
18
|
+
import { baseUrl, getConfig, isAlive } from '../cli/config.js';
|
|
19
|
+
import { isGraphqlHealthy } from '../host/ensure-host.js';
|
|
20
|
+
import { readHostRuntime } from '../host/host-runtime.js';
|
|
19
21
|
import { AgentObservabilityService } from '../observability/agent-observability.service.js';
|
|
20
22
|
import { ControlPlaneError } from '../control-plane/errors.js';
|
|
23
|
+
import { VALID_MODEL_LEVELS } from '../control-plane/definitions.js';
|
|
21
24
|
import { validateManualAdoptionAudit } from '../control-plane/manual-adoption-audit.js';
|
|
22
25
|
import { validateMergeOverrideAudit } from '../control-plane/merge-override-audit.js';
|
|
23
26
|
import { fnv1a64Hex } from '../control-plane/steps.js';
|
|
27
|
+
import { POLICY_VERSION } from '../control-plane/default-playbook-policy.js';
|
|
28
|
+
import { launchBindingsFromRunProfile, runProfileHash, topologyProfileFromRunProfile, topologyStageTargetsFromRunProfile, } from '../control-plane/run-profiles.js';
|
|
29
|
+
import { materializeTemplate, MATERIALIZER_VERSION } from '../pipeline-core/materialize.js';
|
|
24
30
|
import { DbosService } from '../engine/dbos.service.js';
|
|
25
31
|
import { PipelineService } from '../pipeline/pipeline.service.js';
|
|
26
|
-
import { RUN_PROGRESS_EVENT_KEY } from '../pipeline/data-driven-task.workflow.js';
|
|
32
|
+
import { INTEGRATOR_PROGRESS_EVENT_TYPES, RUN_PROGRESS_EVENT_KEY } from '../pipeline/data-driven-task.workflow.js';
|
|
27
33
|
import { templateFromExecutionPolicy } from '../pipeline/data-driven-template.js';
|
|
28
|
-
import {
|
|
34
|
+
import { normalizeParams, normalizeRouteGates, resolveBindingForRole, resolveRunnerForRole, RUNNER_PERMISSION_MODES, } from '../pipeline/route-contract.js';
|
|
35
|
+
import { assertValidInlineRunProfile } from '../playbook/catalog-schema-validator.js';
|
|
29
36
|
import { InboxService } from '../revisium/inbox.service.js';
|
|
30
37
|
import { PlaybooksService } from '../revisium/playbooks.service.js';
|
|
31
38
|
import { RolesService } from '../revisium/roles.service.js';
|
|
@@ -33,10 +40,17 @@ import { RunService } from '../revisium/run.service.js';
|
|
|
33
40
|
import { CreateRunWorkflowError, previewCreateRunIds, } from '../run/create-run.js';
|
|
34
41
|
import { PrReadinessService } from './pr-readiness.service.js';
|
|
35
42
|
const execFileAsync = promisify(execFile);
|
|
36
|
-
const GATE_TOPICS = new Set(['plan', 'merge', 'question']);
|
|
37
|
-
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']);
|
|
38
45
|
const WORKFLOW_FAILURE_EVENT_TYPES = new Set(['step_failed', 'attempt_failed']);
|
|
39
|
-
const
|
|
46
|
+
export const WORKFLOW_PROGRESS_EVENT_TYPES = new Set([
|
|
47
|
+
'pipeline_blocked',
|
|
48
|
+
'question_signal_pending',
|
|
49
|
+
'question_signaled',
|
|
50
|
+
'pr_polled',
|
|
51
|
+
...INTEGRATOR_PROGRESS_EVENT_TYPES,
|
|
52
|
+
]);
|
|
53
|
+
const BUILTIN_RUNNERS = new Set(['claude-code', 'codex', 'script', 'stub-agent']);
|
|
40
54
|
function asErrorMessage(error) {
|
|
41
55
|
if (error instanceof Error)
|
|
42
56
|
return error.message;
|
|
@@ -47,6 +61,9 @@ function asRecord(value) {
|
|
|
47
61
|
return null;
|
|
48
62
|
return value;
|
|
49
63
|
}
|
|
64
|
+
function nonEmptyString(value) {
|
|
65
|
+
return typeof value === 'string' && value.length > 0 ? value : null;
|
|
66
|
+
}
|
|
50
67
|
function dateOrEpoch(value) {
|
|
51
68
|
if (value instanceof Date)
|
|
52
69
|
return Number.isNaN(value.getTime()) ? new Date(0) : value;
|
|
@@ -64,6 +81,39 @@ function gateTopic(item) {
|
|
|
64
81
|
return null;
|
|
65
82
|
return topic;
|
|
66
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
|
+
}
|
|
67
117
|
function gateDeclaredOutcomes(item) {
|
|
68
118
|
const context = asRecord(item.context);
|
|
69
119
|
const summary = asRecord(context?.summary);
|
|
@@ -76,6 +126,38 @@ function gateDeclaredOutcomes(item) {
|
|
|
76
126
|
function gateOutcomes(item) {
|
|
77
127
|
return gateDeclaredOutcomes(item);
|
|
78
128
|
}
|
|
129
|
+
function gateSummaryNodeId(item) {
|
|
130
|
+
const context = asRecord(item.context);
|
|
131
|
+
const summary = asRecord(context?.summary);
|
|
132
|
+
const nodeId = summary?.nodeId;
|
|
133
|
+
return typeof nodeId === 'string' ? nodeId : undefined;
|
|
134
|
+
}
|
|
135
|
+
function isQuestionGateReasonOutcome(item, outcome) {
|
|
136
|
+
if (outcome !== 'fix' && outcome !== 'wontfix')
|
|
137
|
+
return false;
|
|
138
|
+
if (gateSummaryNodeId(item) === 'questionGate')
|
|
139
|
+
return true;
|
|
140
|
+
const outcomes = gateOutcomes(item);
|
|
141
|
+
return gateTopic(item) === 'question' && outcomes.includes('fix') && outcomes.includes('wontfix');
|
|
142
|
+
}
|
|
143
|
+
function assertRequiredGateNote(item, outcome, note) {
|
|
144
|
+
if (outcome === 'approve_anyway' && !note) {
|
|
145
|
+
throw new ControlPlaneError('VALIDATION_FAILURE', 'approve_anyway requires a non-empty note');
|
|
146
|
+
}
|
|
147
|
+
if (outcome === 'override_merge' && !note) {
|
|
148
|
+
throw new ControlPlaneError('VALIDATION_FAILURE', 'override_merge requires a non-empty note');
|
|
149
|
+
}
|
|
150
|
+
if (isQuestionGateReasonOutcome(item, outcome) && !note) {
|
|
151
|
+
throw new ControlPlaneError('VALIDATION_FAILURE', `questionGate ${outcome} requires a non-empty note`);
|
|
152
|
+
}
|
|
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
|
+
}
|
|
79
161
|
async function git(cwd, args) {
|
|
80
162
|
try {
|
|
81
163
|
const result = await execFileAsync('git', args, {
|
|
@@ -136,13 +218,11 @@ function summarizeAttempts(attempts) {
|
|
|
136
218
|
function normalizedRunStatus(status) {
|
|
137
219
|
return status === 'paused' ? 'blocked' : status;
|
|
138
220
|
}
|
|
139
|
-
function hasWorkflowProgress(events) {
|
|
221
|
+
export function hasWorkflowProgress(events) {
|
|
140
222
|
return events.some((event) => (event.type.startsWith('step_')
|
|
141
223
|
|| event.type.startsWith('attempt_')
|
|
142
224
|
|| event.type.startsWith('gate_')
|
|
143
|
-
|| event.type
|
|
144
|
-
|| event.type === 'pr_polled'
|
|
145
|
-
|| event.type === 'integrate_succeeded'));
|
|
225
|
+
|| WORKFLOW_PROGRESS_EVENT_TYPES.has(event.type)));
|
|
146
226
|
}
|
|
147
227
|
function observedRunStatus(rowStatus, workflowStatus, events) {
|
|
148
228
|
if (rowStatus !== 'ready')
|
|
@@ -215,12 +295,6 @@ function optionalString(value) {
|
|
|
215
295
|
function optionalInteger(value) {
|
|
216
296
|
return typeof value === 'number' && Number.isSafeInteger(value) ? value : undefined;
|
|
217
297
|
}
|
|
218
|
-
function requiredRecord(value, field, runId) {
|
|
219
|
-
const record = asRecord(value);
|
|
220
|
-
if (record)
|
|
221
|
-
return record;
|
|
222
|
-
throw new ControlPlaneError('VALIDATION_FAILURE', `Cannot recover run ${runId}: parent ${field} is not a record`);
|
|
223
|
-
}
|
|
224
298
|
function optionalRecord(value) {
|
|
225
299
|
return asRecord(value) ?? {};
|
|
226
300
|
}
|
|
@@ -385,19 +459,15 @@ function isRouteDecision(value) {
|
|
|
385
459
|
const record = asRecord(value);
|
|
386
460
|
return Boolean(record?.playbookId && record?.pipelineRowId && Array.isArray(record.roleBindings));
|
|
387
461
|
}
|
|
388
|
-
function assertRunnerAvailable(runnerId,
|
|
389
|
-
|
|
390
|
-
if (available && !available.includes(runnerId)) {
|
|
391
|
-
throw new ControlPlaneError('VALIDATION_FAILURE', `runner unavailable for role ${roleId}: ${runnerId}`);
|
|
392
|
-
}
|
|
393
|
-
if (!available && !BUILTIN_RUNNERS.has(runnerId)) {
|
|
462
|
+
function assertRunnerAvailable(runnerId, roleId) {
|
|
463
|
+
if (!BUILTIN_RUNNERS.has(runnerId)) {
|
|
394
464
|
throw new ControlPlaneError('VALIDATION_FAILURE', `runner implementation is not registered for role ${roleId}: ${runnerId}`);
|
|
395
465
|
}
|
|
396
466
|
}
|
|
397
467
|
function assertProductionRunnerBinding(runnerId, runnerSource, roleId) {
|
|
398
|
-
if (runnerId !== 'stub-agent' || runnerSource === '
|
|
468
|
+
if (runnerId !== 'stub-agent' || runnerSource === 'profile')
|
|
399
469
|
return;
|
|
400
|
-
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`);
|
|
401
471
|
}
|
|
402
472
|
let TaskControlPlaneApiService = class TaskControlPlaneApiService {
|
|
403
473
|
runs;
|
|
@@ -419,17 +489,17 @@ let TaskControlPlaneApiService = class TaskControlPlaneApiService {
|
|
|
419
489
|
this.prReadiness = prReadiness;
|
|
420
490
|
}
|
|
421
491
|
async getStatus() {
|
|
422
|
-
const
|
|
423
|
-
const
|
|
424
|
-
const
|
|
492
|
+
const host = readHostRuntime();
|
|
493
|
+
const hostAlive = host ? isAlive(host.pid) : false;
|
|
494
|
+
const hostHealthy = host && hostAlive ? await isGraphqlHealthy(host.graphqlPort) : false;
|
|
425
495
|
return {
|
|
426
496
|
daemon: {
|
|
427
|
-
running: Boolean(
|
|
428
|
-
healthy,
|
|
429
|
-
pid:
|
|
430
|
-
baseUrl:
|
|
431
|
-
|
|
432
|
-
|
|
497
|
+
running: Boolean(host && hostAlive),
|
|
498
|
+
healthy: hostHealthy,
|
|
499
|
+
pid: host?.pid ?? null,
|
|
500
|
+
baseUrl: host ? `${baseUrl(host.graphqlPort)}/graphql` : null,
|
|
501
|
+
graphqlPort: host?.graphqlPort ?? null,
|
|
502
|
+
mcpPort: host?.mcpPort ?? null,
|
|
433
503
|
},
|
|
434
504
|
project: this.getProject(),
|
|
435
505
|
};
|
|
@@ -438,9 +508,9 @@ let TaskControlPlaneApiService = class TaskControlPlaneApiService {
|
|
|
438
508
|
const status = await this.getStatus();
|
|
439
509
|
const issues = [];
|
|
440
510
|
if (!status.daemon.running)
|
|
441
|
-
issues.push('
|
|
511
|
+
issues.push('Revo host daemon is not running.');
|
|
442
512
|
if (status.daemon.running && !status.daemon.healthy)
|
|
443
|
-
issues.push('
|
|
513
|
+
issues.push('Revo host daemon is running but unhealthy.');
|
|
444
514
|
try {
|
|
445
515
|
await this.roles.loadPipelinePolicy();
|
|
446
516
|
}
|
|
@@ -523,10 +593,12 @@ let TaskControlPlaneApiService = class TaskControlPlaneApiService {
|
|
|
523
593
|
scope: input.scope,
|
|
524
594
|
playbookId: input.playbookId,
|
|
525
595
|
pipelineId: input.pipelineId,
|
|
596
|
+
profileId: input.profileId,
|
|
597
|
+
profile: input.profile,
|
|
526
598
|
params: input.params,
|
|
527
599
|
issueRef: input.issueRef,
|
|
528
|
-
|
|
529
|
-
source:
|
|
600
|
+
issueAction: input.issueAction,
|
|
601
|
+
source: 'explicit',
|
|
530
602
|
});
|
|
531
603
|
const result = await this.runs.createRun({
|
|
532
604
|
title: input.title,
|
|
@@ -540,7 +612,6 @@ let TaskControlPlaneApiService = class TaskControlPlaneApiService {
|
|
|
540
612
|
params: route.params,
|
|
541
613
|
issueRef: route.params.issueRef,
|
|
542
614
|
routeDecision: route,
|
|
543
|
-
executionProfile: route.executionProfile,
|
|
544
615
|
});
|
|
545
616
|
if (!input.start)
|
|
546
617
|
return { ...result, started: false, route };
|
|
@@ -655,9 +726,8 @@ let TaskControlPlaneApiService = class TaskControlPlaneApiService {
|
|
|
655
726
|
if (!isRouteDecision(routeDecision)) {
|
|
656
727
|
throw new ControlPlaneError('VALIDATION_FAILURE', `Cannot recover run ${parentRunId}: parent route_decision is invalid`);
|
|
657
728
|
}
|
|
658
|
-
const executionProfile = requiredRecord(parentData.execution_profile, 'execution_profile', parentRunId);
|
|
659
729
|
const idSuffix = fnv1a64Hex(`${parentRunId}|${blockedEvent.eventId}`).slice(0, 8);
|
|
660
|
-
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 };
|
|
661
731
|
}
|
|
662
732
|
async createRecoveryRunOrReuseExpected(input, expected) {
|
|
663
733
|
try {
|
|
@@ -815,6 +885,16 @@ let TaskControlPlaneApiService = class TaskControlPlaneApiService {
|
|
|
815
885
|
routeGates: route.routeGates,
|
|
816
886
|
activeNodeIds: activeIds,
|
|
817
887
|
status: cursorStatus(progress),
|
|
888
|
+
provenance: Object.fromEntries([
|
|
889
|
+
['requestedPipelineId', route.requestedPipelineId],
|
|
890
|
+
['basePipelineId', route.basePipelineId],
|
|
891
|
+
['profileId', route.profileId],
|
|
892
|
+
['profileVersion', route.profileVersion],
|
|
893
|
+
['profileHash', route.profileHash],
|
|
894
|
+
['materializedTemplateHash', route.materializedTemplateHash],
|
|
895
|
+
['materializerVersion', route.materializerVersion],
|
|
896
|
+
['policyVersion', route.policyVersion],
|
|
897
|
+
].filter(([, v]) => v !== undefined)),
|
|
818
898
|
},
|
|
819
899
|
nodes,
|
|
820
900
|
edges,
|
|
@@ -987,7 +1067,7 @@ let TaskControlPlaneApiService = class TaskControlPlaneApiService {
|
|
|
987
1067
|
async approveGate(input) {
|
|
988
1068
|
const item = await this.getInboxItem(input.inboxId);
|
|
989
1069
|
const outcomes = gateDeclaredOutcomes(item);
|
|
990
|
-
this.assertLegacyGateWrapperAllowed(input.inboxId, outcomes);
|
|
1070
|
+
this.assertLegacyGateWrapperAllowed(item, input.inboxId, outcomes);
|
|
991
1071
|
if (outcomes.length === 0 || (outcomes.length === 1 && outcomes[0] === 'approved')) {
|
|
992
1072
|
const resolvedBy = input.resolvedBy ?? 'mcp';
|
|
993
1073
|
return this.resolveLegacyGate(item, { decision: 'approve', resolvedBy }, resolvedBy, input.inboxId);
|
|
@@ -1001,7 +1081,7 @@ let TaskControlPlaneApiService = class TaskControlPlaneApiService {
|
|
|
1001
1081
|
async rejectGate(input) {
|
|
1002
1082
|
const item = await this.getInboxItem(input.inboxId);
|
|
1003
1083
|
const outcomes = gateDeclaredOutcomes(item);
|
|
1004
|
-
this.assertLegacyGateWrapperAllowed(input.inboxId, outcomes);
|
|
1084
|
+
this.assertLegacyGateWrapperAllowed(item, input.inboxId, outcomes);
|
|
1005
1085
|
if (outcomes.length === 0) {
|
|
1006
1086
|
const resolvedBy = input.resolvedBy ?? 'mcp';
|
|
1007
1087
|
return this.resolveLegacyGate(item, { decision: 'reject', resolvedBy }, resolvedBy, input.inboxId);
|
|
@@ -1017,7 +1097,7 @@ let TaskControlPlaneApiService = class TaskControlPlaneApiService {
|
|
|
1017
1097
|
const item = await this.getInboxItem(input.inboxId);
|
|
1018
1098
|
const topic = gateTopic(item);
|
|
1019
1099
|
if (!topic) {
|
|
1020
|
-
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}`);
|
|
1021
1101
|
}
|
|
1022
1102
|
const outcomes = gateOutcomes(item);
|
|
1023
1103
|
if (outcomes.length === 0) {
|
|
@@ -1028,9 +1108,8 @@ let TaskControlPlaneApiService = class TaskControlPlaneApiService {
|
|
|
1028
1108
|
throw new ControlPlaneError('VALIDATION_FAILURE', `invalid gate outcome ${outcome}; expected one of: ${outcomes.join(', ')}`);
|
|
1029
1109
|
}
|
|
1030
1110
|
const note = input.note?.trim();
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
}
|
|
1111
|
+
const reconcile = normalizeGateReconcile(input.reconcile);
|
|
1112
|
+
assertRequiredGateNote(item, outcome, note);
|
|
1034
1113
|
const adoptionAudit = outcome === 'adopt_patch_manually'
|
|
1035
1114
|
? validateManualAdoptionAudit(input.adoptionAudit, item)
|
|
1036
1115
|
: undefined;
|
|
@@ -1041,6 +1120,7 @@ let TaskControlPlaneApiService = class TaskControlPlaneApiService {
|
|
|
1041
1120
|
const answer = {
|
|
1042
1121
|
outcome,
|
|
1043
1122
|
...(note ? { note } : {}),
|
|
1123
|
+
...(reconcile ? { reconcile } : {}),
|
|
1044
1124
|
...(adoptionAudit ? { adoptionAudit } : {}),
|
|
1045
1125
|
...(mergeOverrideAudit ? { mergeOverrideAudit } : {}),
|
|
1046
1126
|
resolvedBy,
|
|
@@ -1058,8 +1138,12 @@ let TaskControlPlaneApiService = class TaskControlPlaneApiService {
|
|
|
1058
1138
|
runId: item.runId,
|
|
1059
1139
|
};
|
|
1060
1140
|
}
|
|
1061
|
-
assertLegacyGateWrapperAllowed(inboxId, outcomes) {
|
|
1062
|
-
if (outcomes.length > 2
|
|
1141
|
+
assertLegacyGateWrapperAllowed(item, inboxId, outcomes) {
|
|
1142
|
+
if (outcomes.length > 2
|
|
1143
|
+
|| outcomes.includes('approve_anyway')
|
|
1144
|
+
|| outcomes.includes('retry')
|
|
1145
|
+
|| outcomes.includes('give_up')
|
|
1146
|
+
|| outcomes.some((outcome) => isQuestionGateReasonOutcome(item, outcome))) {
|
|
1063
1147
|
throw new ControlPlaneError('VALIDATION_FAILURE', `inbox item has named gate outcomes; use resolve_gate with an explicit outcome: ${inboxId}`);
|
|
1064
1148
|
}
|
|
1065
1149
|
}
|
|
@@ -1090,12 +1174,27 @@ let TaskControlPlaneApiService = class TaskControlPlaneApiService {
|
|
|
1090
1174
|
if (gateTopic(item)) {
|
|
1091
1175
|
throw new ControlPlaneError('VALIDATION_FAILURE', `inbox item is a gate; use resolve_gate, approve_gate, or reject_gate: ${input.inboxId}`);
|
|
1092
1176
|
}
|
|
1093
|
-
|
|
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 {
|
|
1094
1191
|
inboxId: input.inboxId,
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1192
|
+
previousStatus: result.status,
|
|
1193
|
+
answer: result.answer,
|
|
1194
|
+
signaled: shouldSignal,
|
|
1195
|
+
topic: shouldSignal ? 'question' : null,
|
|
1196
|
+
runId: item.runId,
|
|
1197
|
+
};
|
|
1099
1198
|
}
|
|
1100
1199
|
async resolveInboxItem(input) {
|
|
1101
1200
|
const item = await this.getInboxItem(input.inboxId);
|
|
@@ -1116,9 +1215,8 @@ let TaskControlPlaneApiService = class TaskControlPlaneApiService {
|
|
|
1116
1215
|
}
|
|
1117
1216
|
this.assertGateOutcome(item, outcome);
|
|
1118
1217
|
const note = typeof answer?.note === 'string' ? answer.note.trim() : '';
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
}
|
|
1218
|
+
const reconcile = normalizeGateReconcile(answer.reconcile);
|
|
1219
|
+
assertRequiredGateNote(item, outcome, note);
|
|
1122
1220
|
const adoptionAudit = outcome === 'adopt_patch_manually'
|
|
1123
1221
|
? validateManualAdoptionAudit(answer.adoptionAudit, item)
|
|
1124
1222
|
: undefined;
|
|
@@ -1129,6 +1227,7 @@ let TaskControlPlaneApiService = class TaskControlPlaneApiService {
|
|
|
1129
1227
|
...answer,
|
|
1130
1228
|
outcome,
|
|
1131
1229
|
...(typeof answer.note === 'string' ? { note } : {}),
|
|
1230
|
+
...(reconcile ? { reconcile } : {}),
|
|
1132
1231
|
...(adoptionAudit ? { adoptionAudit } : {}),
|
|
1133
1232
|
...(mergeOverrideAudit ? { mergeOverrideAudit } : {}),
|
|
1134
1233
|
};
|
|
@@ -1149,18 +1248,35 @@ let TaskControlPlaneApiService = class TaskControlPlaneApiService {
|
|
|
1149
1248
|
};
|
|
1150
1249
|
}
|
|
1151
1250
|
async signalGate(item, topic, answer, inboxId) {
|
|
1251
|
+
const signalTopic = gateSignalTopic(item, topic);
|
|
1152
1252
|
const eventBase = {
|
|
1153
1253
|
runId: item.runId,
|
|
1154
1254
|
taskId: item.taskId,
|
|
1155
1255
|
stepId: item.stepId,
|
|
1156
1256
|
stepKey: `gate:${topic}`,
|
|
1157
1257
|
actor: 'mcp',
|
|
1158
|
-
payload: { inboxId, topic },
|
|
1258
|
+
payload: { inboxId, topic, ...(signalTopic !== topic ? { signalTopic } : {}) },
|
|
1159
1259
|
};
|
|
1160
1260
|
await this.runs.appendEvent({ ...eventBase, type: 'gate_signal_pending' });
|
|
1161
|
-
await this.dbos.signal(item.runId,
|
|
1261
|
+
await this.dbos.signal(item.runId, signalTopic, answer, inboxId);
|
|
1162
1262
|
await this.runs.appendEvent({ ...eventBase, type: 'gate_signaled' });
|
|
1163
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
|
+
}
|
|
1164
1280
|
async summarizeGateRisk(inboxId) {
|
|
1165
1281
|
const item = await this.getInboxItem(inboxId);
|
|
1166
1282
|
return {
|
|
@@ -1188,6 +1304,83 @@ let TaskControlPlaneApiService = class TaskControlPlaneApiService {
|
|
|
1188
1304
|
listPipelines() {
|
|
1189
1305
|
return this.playbooks.listPipelines();
|
|
1190
1306
|
}
|
|
1307
|
+
listProfiles(input = {}) {
|
|
1308
|
+
return this.playbooks.listRunProfiles(input);
|
|
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
|
+
}
|
|
1191
1384
|
async getPipeline(pipelineId) {
|
|
1192
1385
|
const result = await this.playbooks.getPipeline(pipelineId);
|
|
1193
1386
|
if (!result)
|
|
@@ -1200,8 +1393,10 @@ let TaskControlPlaneApiService = class TaskControlPlaneApiService {
|
|
|
1200
1393
|
repo: input.repo ?? '',
|
|
1201
1394
|
playbookId: input.playbookId,
|
|
1202
1395
|
pipelineId: input.pipeline,
|
|
1396
|
+
profileId: input.profileId,
|
|
1397
|
+
profile: input.profile,
|
|
1203
1398
|
params: input.params,
|
|
1204
|
-
source:
|
|
1399
|
+
source: 'explicit',
|
|
1205
1400
|
});
|
|
1206
1401
|
}
|
|
1207
1402
|
async previewPipelineSelection(input) {
|
|
@@ -1221,40 +1416,269 @@ let TaskControlPlaneApiService = class TaskControlPlaneApiService {
|
|
|
1221
1416
|
async routeForRun(run) {
|
|
1222
1417
|
if (isRouteDecision(run.data.route_decision))
|
|
1223
1418
|
return run.data.route_decision;
|
|
1224
|
-
|
|
1225
|
-
title: typeof run.data.title === 'string' ? run.data.title : 'Run',
|
|
1226
|
-
repo: Array.isArray(run.data.repos) && typeof run.data.repos[0] === 'string' ? run.data.repos[0] : '',
|
|
1227
|
-
description: typeof run.data.description === 'string' ? run.data.description : '',
|
|
1228
|
-
scope: typeof run.data.scope === 'string' ? run.data.scope : '',
|
|
1229
|
-
playbookId: typeof run.data.playbook_id === 'string' ? run.data.playbook_id : undefined,
|
|
1230
|
-
pipelineId: typeof run.data.pipeline_id === 'string' ? run.data.pipeline_id : undefined,
|
|
1231
|
-
params: run.data.params,
|
|
1232
|
-
source: run.data.pipeline_id ? 'explicit' : 'deterministic-installed-playbook',
|
|
1233
|
-
});
|
|
1419
|
+
throw new ControlPlaneError('VALIDATION_FAILURE', 'run route_decision is missing or invalid');
|
|
1234
1420
|
}
|
|
1235
1421
|
async resolveRouteDecision(input) {
|
|
1236
|
-
const params = normalizeParams(input.params, input.issueRef);
|
|
1237
|
-
const executionProfile = normalizeExecutionProfile(input.executionProfile);
|
|
1422
|
+
const params = normalizeParams(input.params, input.issueRef, input.issueAction);
|
|
1238
1423
|
const playbook = await this.playbooks.resolvePlaybook(input.playbookId);
|
|
1239
|
-
const
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
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
|
+
}
|
|
1436
|
+
const pipeline = await this.playbooks.resolvePipeline({ playbookId: playbook.id, pipelineId: requestedPipelineId });
|
|
1437
|
+
let profileSnapshot;
|
|
1438
|
+
let profileHash;
|
|
1439
|
+
let provenanceFields;
|
|
1440
|
+
if (input.profileId !== undefined) {
|
|
1441
|
+
const storedProfile = await this.playbooks.resolveRunProfile({
|
|
1442
|
+
playbookId: playbook.id,
|
|
1443
|
+
pipelineId: pipeline.pipelineId,
|
|
1444
|
+
profileId: input.profileId,
|
|
1445
|
+
});
|
|
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');
|
|
1468
|
+
}
|
|
1469
|
+
catch (error) {
|
|
1470
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1471
|
+
throw new ControlPlaneError('VALIDATION_FAILURE', message);
|
|
1472
|
+
}
|
|
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`);
|
|
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
|
+
});
|
|
1496
|
+
const allRoles = (await this.roles.listRoles()).filter((role) => role.playbookId === playbook.id);
|
|
1497
|
+
await this.assertLaunchBindingsClosed(launchBindings, allRoles, materializedTemplate);
|
|
1498
|
+
const roleBindings = await this.resolveRouteRoles(playbook.id, pipeline, launchBindings, allRoles);
|
|
1243
1499
|
return {
|
|
1244
1500
|
playbookId: playbook.id,
|
|
1245
|
-
pipelineId:
|
|
1501
|
+
pipelineId: requestedPipelineId,
|
|
1246
1502
|
pipelineRowId: pipeline.id,
|
|
1247
1503
|
source: input.source,
|
|
1248
1504
|
roles: roleBindings.map((binding) => binding.roleId),
|
|
1249
1505
|
requiredRoles: pipeline.requiredRoles,
|
|
1250
1506
|
optionalRoles: pipeline.optionalRoles,
|
|
1251
1507
|
routeGates: normalizeRouteGates(pipeline.routeGates),
|
|
1252
|
-
executionPolicy
|
|
1253
|
-
|
|
1508
|
+
executionPolicy,
|
|
1509
|
+
launchBindings,
|
|
1254
1510
|
roleBindings,
|
|
1255
1511
|
params,
|
|
1512
|
+
requestedPipelineId,
|
|
1513
|
+
basePipelineId: pipeline.pipelineId,
|
|
1514
|
+
profileHash,
|
|
1515
|
+
profileSnapshot,
|
|
1516
|
+
materializedTemplateHash,
|
|
1517
|
+
materializedTemplate,
|
|
1518
|
+
materializerVersion: MATERIALIZER_VERSION,
|
|
1519
|
+
policyVersion: POLICY_VERSION,
|
|
1520
|
+
...provenanceFields,
|
|
1256
1521
|
};
|
|
1257
1522
|
}
|
|
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)
|
|
1551
|
+
return;
|
|
1552
|
+
const byPlaybookRole = new Map(roles.map((r) => [r.playbookRoleId || r.name, r]));
|
|
1553
|
+
const nodes = template.nodes;
|
|
1554
|
+
const cachedProfiles = new Map();
|
|
1555
|
+
for (const override of launchBindings) {
|
|
1556
|
+
const matchLabel = JSON.stringify(override.match);
|
|
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;
|
|
1675
|
+
}
|
|
1676
|
+
}
|
|
1677
|
+
return undefined;
|
|
1678
|
+
}
|
|
1679
|
+
unresolvablePermissionRunner(matchLabel) {
|
|
1680
|
+
return this.profileSchemaClosed(`bindingOverride match ${matchLabel} permissionMode override requires a resolvable runner; pin runnerId`);
|
|
1681
|
+
}
|
|
1258
1682
|
async resolveAutoPipeline(playbookId, text) {
|
|
1259
1683
|
const pipelines = (await this.playbooks.listPipelines())
|
|
1260
1684
|
.filter((pipeline) => pipeline.playbookId === playbookId)
|
|
@@ -1275,8 +1699,8 @@ let TaskControlPlaneApiService = class TaskControlPlaneApiService {
|
|
|
1275
1699
|
}
|
|
1276
1700
|
return best.pipeline;
|
|
1277
1701
|
}
|
|
1278
|
-
async resolveRouteRoles(playbookId, pipeline,
|
|
1279
|
-
const roles = (await this.roles.listRoles()).filter((role) => role.playbookId === playbookId);
|
|
1702
|
+
async resolveRouteRoles(playbookId, pipeline, launchBindings, preloadedRoles) {
|
|
1703
|
+
const roles = preloadedRoles ?? (await this.roles.listRoles()).filter((role) => role.playbookId === playbookId);
|
|
1280
1704
|
const byPlaybookRole = new Map(roles.map((role) => [role.playbookRoleId || role.name, role]));
|
|
1281
1705
|
const selected = [...pipeline.requiredRoles];
|
|
1282
1706
|
for (const group of pipeline.alternativeRoles) {
|
|
@@ -1295,16 +1719,25 @@ let TaskControlPlaneApiService = class TaskControlPlaneApiService {
|
|
|
1295
1719
|
return selected.map((roleId) => {
|
|
1296
1720
|
const role = byPlaybookRole.get(roleId);
|
|
1297
1721
|
assertProductionRunnerBinding(role.runner, 'playbook', roleId);
|
|
1298
|
-
const
|
|
1299
|
-
assertProductionRunnerBinding(
|
|
1300
|
-
assertRunnerAvailable(
|
|
1722
|
+
const runnerResolved = resolveRunnerForRole(role.runner, roleId, launchBindings);
|
|
1723
|
+
assertProductionRunnerBinding(runnerResolved.runnerId, runnerResolved.source, roleId);
|
|
1724
|
+
assertRunnerAvailable(runnerResolved.runnerId, roleId);
|
|
1725
|
+
const bindingResolution = resolveBindingForRole(role, roleId, runnerResolved.runnerId, launchBindings);
|
|
1301
1726
|
return {
|
|
1302
1727
|
roleId,
|
|
1303
1728
|
rowId: role.id,
|
|
1304
1729
|
modelLevel: role.modelLevel,
|
|
1305
1730
|
runnerId: role.runner,
|
|
1306
|
-
resolvedRunnerId:
|
|
1307
|
-
runnerSource:
|
|
1731
|
+
resolvedRunnerId: runnerResolved.runnerId,
|
|
1732
|
+
runnerSource: runnerResolved.source,
|
|
1733
|
+
resolvedModelLevel: bindingResolution.resolvedModelLevel,
|
|
1734
|
+
modelSource: bindingResolution.modelSource,
|
|
1735
|
+
timeoutMs: role.timeoutMs,
|
|
1736
|
+
resolvedTimeoutMs: bindingResolution.resolvedTimeoutMs,
|
|
1737
|
+
timeoutSource: bindingResolution.timeoutSource,
|
|
1738
|
+
permissionMode: role.permissionMode,
|
|
1739
|
+
resolvedPermissionMode: bindingResolution.resolvedPermissionMode,
|
|
1740
|
+
permissionSource: bindingResolution.permissionSource,
|
|
1308
1741
|
};
|
|
1309
1742
|
});
|
|
1310
1743
|
}
|