akm-cli 0.9.1 → 0.9.2-alpha.1
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/CHANGELOG.md +88 -0
- package/README.md +3 -1
- package/SECURITY.md +1 -1
- package/STABILITY.md +1 -1
- package/dist/akm +2 -2
- package/dist/akm-migrate +2 -2
- package/dist/assets/hints/cli-hints-full.md +14 -9
- package/dist/assets/improve-strategies/proactive-maintenance.json +1 -1
- package/dist/assets/improve-strategies/reflect-distill.json +1 -1
- package/dist/assets/models.json +35 -0
- package/dist/assets/tasks/core/extract.yml +6 -5
- package/dist/assets/tasks/core/improve.yml +6 -5
- package/dist/assets/tasks/core/index-refresh.yml +6 -5
- package/dist/assets/tasks/core/sync.yml +6 -5
- package/dist/assets/tasks/core/version-check.yml +6 -5
- package/dist/assets/tasks/improve/akm-graph-refresh-weekly.yml +6 -5
- package/dist/assets/tasks/improve/akm-improve-catchup.yml +6 -5
- package/dist/assets/tasks/improve/akm-improve-consolidate.yml +6 -5
- package/dist/assets/tasks/improve/akm-improve-frequent.yml +6 -5
- package/dist/assets/tasks/improve/akm-improve-nightly.yml +6 -5
- package/dist/cli/confirm.js +2 -2
- package/dist/cli/parse-args.js +3 -24
- package/dist/cli/retired-commands.js +1 -1
- package/dist/cli/shared.js +2 -2
- package/dist/cli.js +11 -9
- package/dist/commands/agent/agent-dispatch.js +55 -89
- package/dist/commands/agent/contribute-cli.js +12 -45
- package/dist/commands/command/builtin-action.js +32 -0
- package/dist/commands/command/command-cli.js +99 -0
- package/dist/commands/command/command-execution.js +308 -0
- package/dist/commands/command/execution-source-loader.js +176 -0
- package/dist/commands/command/portable-template.js +60 -0
- package/dist/commands/config-cli.js +10 -4
- package/dist/commands/env/env.js +4 -2
- package/dist/commands/feedback-cli.js +1 -1
- package/dist/commands/health/checks.js +241 -29
- package/dist/commands/health/html-report.js +0 -14
- package/dist/commands/health/report-view-model.js +0 -1
- package/dist/commands/health/surfaces.js +6 -7
- package/dist/commands/health/types.js +0 -2
- package/dist/commands/health.js +63 -18
- package/dist/commands/improve/collapse-detector.js +2 -2
- package/dist/commands/improve/consolidate.js +251 -214
- package/dist/commands/improve/distill/promote-memory.js +71 -34
- package/dist/commands/improve/distill/quality-gate.js +17 -5
- package/dist/commands/improve/distill.js +232 -155
- package/dist/commands/improve/eligibility.js +112 -79
- package/dist/commands/improve/execution.js +57 -0
- package/dist/commands/improve/extract-cli.js +5 -5
- package/dist/commands/improve/extract.js +521 -342
- package/dist/commands/improve/improve-strategies.js +43 -14
- package/dist/commands/improve/improve.js +249 -29
- package/dist/commands/improve/loop-stages.js +11 -17
- package/dist/commands/improve/memory/memory-contradiction-detect.js +90 -66
- package/dist/commands/improve/outcome-loop.js +22 -38
- package/dist/commands/improve/planner.js +134 -0
- package/dist/commands/improve/preparation.js +730 -409
- package/dist/commands/improve/reflect.js +386 -223
- package/dist/commands/improve/run-context.js +3 -4
- package/dist/commands/improve/salience.js +6 -58
- package/dist/commands/improve/session-asset.js +12 -12
- package/dist/commands/lint/index.js +101 -29
- package/dist/commands/migrate-cli.js +11 -69
- package/dist/commands/migration-tool.js +6 -9
- package/dist/commands/models-cli.js +27 -0
- package/dist/commands/proposal/drain.js +258 -186
- package/dist/commands/proposal/proposal-cli.js +32 -10
- package/dist/commands/proposal/proposal.js +2 -5
- package/dist/commands/proposal/propose.js +192 -172
- package/dist/commands/proposal/repository.js +54 -91
- package/dist/commands/proposal/validators/proposal-validators.js +9 -7
- package/dist/commands/read/curate.js +10 -0
- package/dist/commands/read/registry-search.js +25 -9
- package/dist/commands/read/remember-cli.js +14 -2
- package/dist/commands/read/search.js +10 -4
- package/dist/commands/read/show.js +139 -153
- package/dist/commands/registry-cli.js +16 -7
- package/dist/commands/remember.js +33 -18
- package/dist/commands/sources/add-cli.js +19 -178
- package/dist/commands/sources/bundle-cli.js +15 -3
- package/dist/commands/sources/dangerous-env-audit.js +135 -0
- package/dist/commands/sources/info.js +2 -1
- package/dist/commands/sources/installed-stashes.js +835 -152
- package/dist/commands/sources/schema-repair.js +174 -95
- package/dist/commands/sources/self-update.js +30 -74
- package/dist/commands/sources/source-add.js +3 -5
- package/dist/commands/sources/sources-cli.js +2 -15
- package/dist/commands/sources/update-transaction.js +220 -0
- package/dist/commands/tasks/tasks-cli.js +3 -3
- package/dist/commands/tasks/tasks.js +736 -317
- package/dist/commands/workflow-cli.js +2 -2
- package/dist/core/adapter/adapters/agent-skills-adapter.js +3 -0
- package/dist/core/adapter/adapters/akm-adapter.js +83 -34
- package/dist/core/adapter/adapters/akm-lint.js +54 -39
- package/dist/core/adapter/adapters/akm-metadata.js +45 -45
- package/dist/core/adapter/adapters/akm-task-adapter.js +32 -49
- package/dist/core/adapter/adapters/akm-workflow-adapter.js +38 -23
- package/dist/core/adapter/adapters/dotenv-adapter.js +30 -1
- package/dist/core/adapter/adapters/generic-files-adapter.js +11 -0
- package/dist/core/adapter/adapters/index.js +0 -9
- package/dist/core/adapter/adapters/llm-wiki-adapter.js +4 -0
- package/dist/core/adapter/adapters/okf-adapter.js +4 -0
- package/dist/core/adapter/adapters/opencode-adapter.js +5 -8
- package/dist/core/adapter/adapters/tool-dir-shared.js +63 -6
- package/dist/core/adapter/adapters/website-snapshot-adapter.js +4 -0
- package/dist/core/adapter/execution-source.js +308 -0
- package/dist/core/adapter/recognize-match.js +36 -13
- package/dist/core/adapter/registry.js +0 -9
- package/dist/core/asset/stash-meta.js +94 -4
- package/dist/core/common.js +6 -11
- package/dist/core/config/config-io.js +3 -3
- package/dist/core/config/config-schema.js +18 -40
- package/dist/core/config/config-sources.js +11 -21
- package/dist/core/config/config-walker.js +31 -13
- package/dist/core/config/config.js +21 -20
- package/dist/core/config/schema/engines.js +8 -7
- package/dist/core/config/schema/improve-processes.js +29 -5
- package/dist/core/config/schema/primitives.js +1 -23
- package/dist/core/config/schema/sources-bundles.js +13 -16
- package/dist/core/errors.js +2 -0
- package/dist/core/events.js +68 -32
- package/dist/core/extra-params.js +1 -0
- package/dist/core/improve-result.js +315 -0
- package/dist/core/lesson-lint.js +0 -6
- package/dist/core/maintenance-barrier.js +4 -4
- package/dist/core/network-policy.js +152 -0
- package/dist/core/paths.js +1 -1
- package/dist/core/recognition-util.js +4 -4
- package/dist/core/registry-url.js +456 -0
- package/dist/core/state/migrations.js +161 -47
- package/dist/core/state-db.js +453 -80
- package/dist/core/system-error.js +32 -0
- package/dist/core/time.js +2 -12
- package/dist/core/write-source.js +0 -18
- package/dist/execution/directory-identity.js +52 -0
- package/dist/execution/executable-identity.js +107 -0
- package/dist/execution/guarded-source.js +398 -0
- package/dist/execution/json.js +95 -0
- package/dist/{commands/health/types-session-log.js → execution/limits.js} +2 -1
- package/dist/execution/record.js +55 -0
- package/dist/execution/resolved-request.js +730 -0
- package/dist/execution/source.js +320 -0
- package/dist/indexer/bundle-identity-guard.js +5 -4
- package/dist/indexer/db/graph-db.js +33 -0
- package/dist/indexer/graph/graph-boost.js +3 -4
- package/dist/indexer/graph/graph-extraction.js +562 -373
- package/dist/indexer/index-written-assets.js +65 -34
- package/dist/indexer/indexer.js +443 -222
- package/dist/indexer/installations.js +6 -0
- package/dist/indexer/lookup/adapter-concept-owner.js +283 -0
- package/dist/indexer/passes/memory-inference.js +227 -174
- package/dist/indexer/scan/doc-to-entry.js +7 -9
- package/dist/indexer/scan/drain-dir.js +51 -23
- package/dist/indexer/search/db-search.js +98 -22
- package/dist/indexer/search/search-fields.js +1 -1
- package/dist/indexer/search/search-hit-enrichers.js +1 -1
- package/dist/indexer/search/search-source.js +7 -16
- package/dist/indexer/search/semantic-status.js +10 -1
- package/dist/indexer/usage/show-usage.js +105 -0
- package/dist/indexer/usage/usage-events.js +7 -2
- package/dist/indexer/walk/matchers.js +40 -10
- package/dist/indexer/walk/path-resolver.js +5 -2
- package/dist/indexer/walk/walker.js +20 -2
- package/dist/integrations/agent/builder-shared.js +3 -6
- package/dist/integrations/agent/conversation-fallback.js +16 -0
- package/dist/integrations/agent/engine-resolution.js +87 -87
- package/dist/integrations/agent/execution-cascade.js +566 -0
- package/dist/integrations/agent/execution-definitions.js +211 -0
- package/dist/integrations/agent/execution-lowering.js +811 -0
- package/dist/integrations/agent/execution-preparation.js +67 -0
- package/dist/integrations/agent/index.js +0 -2
- package/dist/integrations/agent/inline-execution.js +74 -0
- package/dist/integrations/agent/model-map.js +515 -0
- package/dist/integrations/agent/persona-fallback.js +30 -0
- package/dist/integrations/agent/request-lowering.js +186 -0
- package/dist/integrations/agent/runner-dispatch.js +230 -37
- package/dist/integrations/agent/runner.js +12 -83
- package/dist/integrations/harnesses/aider/agent-builder.js +8 -0
- package/dist/integrations/harnesses/aider/index.js +0 -1
- package/dist/integrations/harnesses/amazonq/agent-builder.js +8 -0
- package/dist/integrations/harnesses/amazonq/index.js +0 -1
- package/dist/integrations/harnesses/claude/agent-builder.js +14 -1
- package/dist/integrations/harnesses/claude/index.js +1 -5
- package/dist/integrations/harnesses/claude/session-log.js +3 -33
- package/dist/integrations/harnesses/codex/agent-builder.js +8 -0
- package/dist/integrations/harnesses/codex/index.js +0 -1
- package/dist/integrations/harnesses/copilot/agent-builder.js +8 -0
- package/dist/integrations/harnesses/copilot/index.js +0 -1
- package/dist/integrations/harnesses/gemini/agent-builder.js +8 -0
- package/dist/integrations/harnesses/gemini/index.js +0 -1
- package/dist/integrations/harnesses/index.js +4 -44
- package/dist/integrations/harnesses/opencode/agent-builder.js +16 -9
- package/dist/integrations/harnesses/opencode/index.js +0 -2
- package/dist/integrations/harnesses/opencode/session-log.js +14 -204
- package/dist/integrations/harnesses/opencode-sdk/harness.js +12 -1
- package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +40 -42
- package/dist/integrations/harnesses/openhands/agent-builder.js +8 -0
- package/dist/integrations/harnesses/openhands/index.js +0 -1
- package/dist/integrations/harnesses/pi/agent-builder.js +8 -0
- package/dist/integrations/harnesses/pi/index.js +0 -1
- package/dist/integrations/harnesses/shared.js +0 -1
- package/dist/integrations/harnesses/types.js +1 -3
- package/dist/integrations/lockfile.js +82 -79
- package/dist/integrations/session-logs/index.js +6 -17
- package/dist/integrations/session-logs/provider-base.js +1 -29
- package/dist/llm/client.js +10 -5
- package/dist/llm/embedder.js +6 -7
- package/dist/llm/embedders/local.js +37 -88
- package/dist/llm/embedders/types.js +1 -1
- package/dist/llm/graph-extract.js +75 -50
- package/dist/llm/index-passes.js +43 -5
- package/dist/llm/memory-infer.js +8 -6
- package/dist/llm/metadata-enhance.js +5 -3
- package/dist/llm/structured-call.js +122 -25
- package/dist/output/format-exempt.js +1 -1
- package/dist/output/render-registry.js +0 -16
- package/dist/output/renderers.js +12 -7
- package/dist/output/shapes/curate.js +1 -0
- package/dist/output/shapes/helpers.js +10 -2
- package/dist/output/shapes/passthrough.js +2 -0
- package/dist/output/text/command-format.js +28 -32
- package/dist/output/text/health-format.js +1 -29
- package/dist/output/text/migrate.js +6 -56
- package/dist/output/text/proposal-format.js +16 -1
- package/dist/output/text/workflow-format.js +16 -0
- package/dist/registry/network.js +279 -0
- package/dist/registry/pinned-request-helper.js +247 -0
- package/dist/registry/pinned-transport.js +717 -0
- package/dist/registry/providers/skills-sh.js +18 -6
- package/dist/registry/providers/static-index.js +20 -7
- package/dist/registry/resolve.js +53 -28
- package/dist/scripts/akm-migrate-node.js +19122 -52218
- package/dist/scripts/akm-migrate.js +19064 -51567
- package/dist/setup/registry-stash-loader.js +64 -20
- package/dist/setup/semantic-assets.js +9 -34
- package/dist/setup/setup.js +12 -30
- package/dist/setup/source-identity.js +17 -0
- package/dist/setup/steps/sources.js +36 -15
- package/dist/setup/steps/tasks.js +39 -11
- package/dist/sources/providers/git-provider.js +3 -3
- package/dist/sources/providers/npm.js +2 -2
- package/dist/sources/providers/provider-utils.js +4 -3
- package/dist/sources/providers/website.js +11 -7
- package/dist/sources/snapshot-fetchers/host-guard.js +9 -136
- package/dist/sources/snapshot-fetchers/website-ingest.js +25 -109
- package/dist/sources/website-url.js +73 -0
- package/dist/storage/engines/sqlite-migrations.js +81 -26
- package/dist/storage/managed-db.js +27 -24
- package/dist/storage/repositories/events-repository.js +3 -0
- package/dist/storage/repositories/index-connection.js +42 -10
- package/dist/storage/repositories/index-entries-repository.js +167 -207
- package/dist/storage/repositories/index-entry-mapper.js +8 -12
- package/dist/storage/repositories/index-entry-schema.js +255 -0
- package/dist/storage/repositories/index-fts-repository.js +10 -10
- package/dist/storage/repositories/index-llm-cache-repository.js +8 -13
- package/dist/storage/repositories/index-meta-repository.js +0 -11
- package/dist/storage/repositories/index-schema.js +71 -342
- package/dist/storage/repositories/index-utility-repository.js +12 -17
- package/dist/storage/repositories/index-vec-repository.js +1 -1
- package/dist/storage/repositories/proposals-repository.js +4 -127
- package/dist/storage/repositories/registry-cache.js +2 -1
- package/dist/storage/repositories/task-history-repository.js +20 -40
- package/dist/storage/repositories/workflow-runs-repository.js +228 -129
- package/dist/storage/sqlite-read-snapshot.js +148 -0
- package/dist/tasks/backends/cron.js +170 -42
- package/dist/tasks/backends/index.js +1 -1
- package/dist/tasks/backends/launchd.js +787 -202
- package/dist/tasks/backends/schtasks.js +282 -83
- package/dist/tasks/embedded.js +7 -7
- package/dist/tasks/frozen-script.js +50 -0
- package/dist/tasks/resolve-akm-bin.js +5 -1
- package/dist/tasks/runner.js +239 -251
- package/dist/tasks/runtime-v3.js +281 -0
- package/dist/tasks/scheduler-binding.js +272 -0
- package/dist/tasks/scheduler-invocation.js +57 -43
- package/dist/tasks/scheduler-sync.js +654 -0
- package/dist/tasks/source-v3.js +752 -0
- package/dist/tasks/standalone-script-entry.js +5 -0
- package/dist/tasks/task-id.js +29 -0
- package/dist/workflows/authoring/authoring.js +15 -32
- package/dist/workflows/exec/dispatch-redaction.js +14 -8
- package/dist/workflows/exec/exec-unit.js +7 -28
- package/dist/workflows/exec/frozen-judge.js +57 -89
- package/dist/workflows/exec/lowering-notices.js +23 -0
- package/dist/workflows/exec/native-executor.js +301 -458
- package/dist/workflows/exec/param-secrets.js +4 -3
- package/dist/workflows/exec/run-workflow.js +26 -32
- package/dist/workflows/exec/step-work.js +105 -109
- package/dist/workflows/exec/unit-dispatch.js +103 -27
- package/dist/workflows/exec/unit-writer.js +3 -3
- package/dist/workflows/exec/worktree.js +2 -2
- package/dist/workflows/ir/compile.js +86 -72
- package/dist/workflows/ir/environment-v4.js +328 -0
- package/dist/workflows/ir/freeze-v4.js +122 -0
- package/dist/workflows/ir/plan-hash.js +13 -7
- package/dist/workflows/ir/schema-v4.js +525 -0
- package/dist/workflows/ir/schema.js +25 -284
- package/dist/workflows/ir/source-freeze-v4.js +506 -0
- package/dist/workflows/parser.js +27 -24
- package/dist/workflows/program/schema.js +1 -2
- package/dist/workflows/renderer.js +42 -29
- package/dist/workflows/resource-limits.js +4 -5
- package/dist/workflows/runtime/agent-identity.js +11 -13
- package/dist/workflows/runtime/plan-classifier.js +8 -8
- package/dist/workflows/runtime/runs.js +27 -43
- package/dist/workflows/runtime/workflow-asset-loader.js +45 -205
- package/dist/workflows/source-files.js +373 -0
- package/dist/workflows/source-ir/compile.js +196 -0
- package/dist/workflows/source-ir/github-yaml.js +577 -0
- package/dist/workflows/source-ir/ordering.js +38 -0
- package/dist/workflows/source-ir/program.js +50 -0
- package/dist/workflows/source-ir/result.js +26 -0
- package/dist/workflows/source-ir/schema.js +772 -0
- package/dist/workflows/source-ir/semantics.js +242 -0
- package/dist/workflows/source-ir/uses.js +14 -0
- package/docs/README.md +2 -0
- package/docs/migration/README.md +3 -1
- package/docs/migration/release-notes/0.9.2.md +55 -0
- package/docs/migration/release-notes/README.md +5 -0
- package/docs/migration/v0.8-to-v0.9.md +76 -1077
- package/docs/migration/v0.9.0-troubleshooting.md +104 -516
- package/docs/migration/v0.9.1-to-v0.9.2.md +150 -0
- package/docs/reference/README.md +1 -0
- package/docs/reference/cli.md +230 -98
- package/docs/reference/configuration.md +153 -27
- package/docs/reference/data-and-telemetry.md +19 -1
- package/docs/reference/supported-formats.md +23 -3
- package/docs/reference/tasks.md +182 -0
- package/docs/reference/workflow-schema.md +91 -40
- package/docs/reference/workflows.md +33 -6
- package/package.json +10 -6
- package/schemas/akm-config.json +372 -216
- package/schemas/akm-task.json +324 -80
- package/schemas/akm-workflow.json +6 -9
- package/dist/core/migration-operation.js +0 -75
- package/dist/integrations/agent/model-aliases.js +0 -74
- package/dist/tasks/parser.js +0 -380
- package/dist/tasks/schema.js +0 -123
- package/dist/tasks/validator.js +0 -80
- package/dist/workflows/ir/freeze.js +0 -320
- package/dist/workflows/runtime/document-cache.js +0 -13
|
@@ -0,0 +1,772 @@
|
|
|
1
|
+
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
|
+
/**
|
|
5
|
+
* Strict, adapter-neutral workflow source IR.
|
|
6
|
+
*
|
|
7
|
+
* This is deliberately separate from the durable workflow plan in `../ir`.
|
|
8
|
+
* Source adapters produce this representation; the durable freeze lane owns
|
|
9
|
+
* resolving executable targets and creating resumable dispatch state.
|
|
10
|
+
*/
|
|
11
|
+
import { types as utilTypes } from "node:util";
|
|
12
|
+
import { bundleRefToString, parseBundleRef } from "../../core/asset/asset-ref.js";
|
|
13
|
+
import { validateExtraParams } from "../../core/extra-params.js";
|
|
14
|
+
import { checkJsonSchemaDefinition } from "../../core/json-schema.js";
|
|
15
|
+
import { parseReference } from "../program/expressions.js";
|
|
16
|
+
import { PROGRAM_RETRY_REASONS } from "../program/schema.js";
|
|
17
|
+
import { jsonBytes, utf8Bytes, WORKFLOW_ENGINE_NAME_PATTERN, WORKFLOW_ENV_VAR_NAME_PATTERN, WORKFLOW_MAX_CONCURRENCY, WORKFLOW_MAX_ENGINE_NAME_LENGTH, WORKFLOW_MAX_EXEC_ARG_BYTES, WORKFLOW_MAX_EXEC_ARGV, WORKFLOW_MAX_EXEC_CWD_LENGTH, WORKFLOW_MAX_EXEC_PASS_ENV, WORKFLOW_MAX_EXTRA_PARAMS_BYTES, WORKFLOW_MAX_GATE_LOOPS, WORKFLOW_MAX_INPUTS, WORKFLOW_MAX_MAP_EXPANSION, WORKFLOW_MAX_PARAMS, WORKFLOW_MAX_RETRIES, WORKFLOW_MAX_ROUTE_BRANCHES, WORKFLOW_MAX_SCHEMA_BYTES, WORKFLOW_MAX_TIMEOUT_MS, } from "../resource-limits.js";
|
|
18
|
+
import { canonicalTopologicalJobs, compareWorkflowSourceCodePoints } from "./ordering.js";
|
|
19
|
+
import { canonicalizeWorkflowCron, canonicalizeWorkflowRun, canonicalizeWorkflowWorkingDirectory, classifyWorkflowStepUses, rejectNulInArgv, validateWorkflowBuiltinCommand, WorkflowSourceSemanticError, } from "./semantics.js";
|
|
20
|
+
export const WORKFLOW_SOURCE_IR_VERSION = 1;
|
|
21
|
+
export const WORKFLOW_SOURCE_IR_MAX_BYTES = 2 * 1024 * 1024;
|
|
22
|
+
export const WORKFLOW_SOURCE_IR_MAX_DEPTH = 64;
|
|
23
|
+
export const WORKFLOW_SOURCE_IR_MAX_NODES = 50_000;
|
|
24
|
+
export const WORKFLOW_SOURCE_EXTENSION_OWNER_PATTERN = /^(?:[a-z0-9](?:[a-z0-9.-]*[a-z0-9])?)\/[a-z0-9](?:[a-z0-9._-]*[a-z0-9])?$/;
|
|
25
|
+
export const WORKFLOW_SOURCE_HOST_SHELLS = ["bash", "sh", "zsh", "pwsh", "powershell", "cmd"];
|
|
26
|
+
const UNSAFE_KEYS = new Set(["__proto__", "prototype", "constructor"]);
|
|
27
|
+
const HOST_SHELLS = new Set(WORKFLOW_SOURCE_HOST_SHELLS);
|
|
28
|
+
/** Strictly decode untrusted plain data before it reaches source lowering. */
|
|
29
|
+
export function decodeWorkflowSourceIrV1(input, options = {}) {
|
|
30
|
+
const decoded = snapshotPlainData(input);
|
|
31
|
+
const root = record(decoded, "source IR");
|
|
32
|
+
keys(root, [
|
|
33
|
+
"sourceIrVersion",
|
|
34
|
+
"name",
|
|
35
|
+
"description",
|
|
36
|
+
"tags",
|
|
37
|
+
"params",
|
|
38
|
+
"defaults",
|
|
39
|
+
"budget",
|
|
40
|
+
"preamble",
|
|
41
|
+
"triggers",
|
|
42
|
+
"jobs",
|
|
43
|
+
"extensions",
|
|
44
|
+
"source",
|
|
45
|
+
], "source IR");
|
|
46
|
+
if (root.sourceIrVersion !== WORKFLOW_SOURCE_IR_VERSION)
|
|
47
|
+
fail("sourceIrVersion must be 1");
|
|
48
|
+
nonEmptyString(root.name, "name");
|
|
49
|
+
optionalString(root.description, "description");
|
|
50
|
+
optionalString(root.preamble, "preamble");
|
|
51
|
+
optionalStringList(root.tags, "tags", 256);
|
|
52
|
+
validateParams(root.params);
|
|
53
|
+
validateUnit(root.defaults, "defaults", true);
|
|
54
|
+
validateBudget(root.budget);
|
|
55
|
+
span(root.source, "source");
|
|
56
|
+
extensions(root.extensions, "extensions");
|
|
57
|
+
if (!Array.isArray(root.triggers) || root.triggers.length === 0 || root.triggers.length > 64) {
|
|
58
|
+
fail("triggers must contain 1 through 64 entries");
|
|
59
|
+
}
|
|
60
|
+
validateTriggers(root.triggers);
|
|
61
|
+
if (!Array.isArray(root.jobs) || root.jobs.length === 0 || root.jobs.length > 256) {
|
|
62
|
+
fail("jobs must contain 1 through 256 entries");
|
|
63
|
+
}
|
|
64
|
+
const jobIds = new Set();
|
|
65
|
+
for (const [index, job] of root.jobs.entries())
|
|
66
|
+
validateJob(job, index, jobIds, options);
|
|
67
|
+
for (const job of root.jobs) {
|
|
68
|
+
for (const need of job.needs)
|
|
69
|
+
if (!jobIds.has(need))
|
|
70
|
+
fail(`job ${job.id} needs missing job ${need}`);
|
|
71
|
+
}
|
|
72
|
+
validateTopologicalJobs(root.jobs);
|
|
73
|
+
return decoded;
|
|
74
|
+
}
|
|
75
|
+
function validateTrigger(value, index) {
|
|
76
|
+
const trigger = record(value, `trigger ${index}`);
|
|
77
|
+
if (trigger.kind === "workflow_dispatch") {
|
|
78
|
+
keys(trigger, ["kind", "source"], `trigger ${index}`);
|
|
79
|
+
span(trigger.source, `trigger ${index} source`);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
if (trigger.kind !== "schedule")
|
|
83
|
+
fail(`trigger ${index} has an unsupported kind`);
|
|
84
|
+
keys(trigger, ["kind", "cron", "ordinal", "source"], `trigger ${index}`);
|
|
85
|
+
nonEmptyString(trigger.cron, `trigger ${index} cron`);
|
|
86
|
+
try {
|
|
87
|
+
trigger.cron = canonicalizeWorkflowCron(trigger.cron);
|
|
88
|
+
}
|
|
89
|
+
catch (cause) {
|
|
90
|
+
semanticFail(cause, `trigger ${index} cron`);
|
|
91
|
+
}
|
|
92
|
+
if (!Number.isSafeInteger(trigger.ordinal) || trigger.ordinal < 0) {
|
|
93
|
+
fail(`trigger ${index} ordinal must be a non-negative integer`);
|
|
94
|
+
}
|
|
95
|
+
span(trigger.source, `trigger ${index} source`);
|
|
96
|
+
}
|
|
97
|
+
function validateTriggers(value) {
|
|
98
|
+
let nextScheduleOrdinal = 0;
|
|
99
|
+
let manualSeen = false;
|
|
100
|
+
for (const [index, trigger] of value.entries()) {
|
|
101
|
+
validateTrigger(trigger, index);
|
|
102
|
+
const decoded = trigger;
|
|
103
|
+
if (decoded.kind === "schedule") {
|
|
104
|
+
if (manualSeen || decoded.ordinal !== nextScheduleOrdinal) {
|
|
105
|
+
fail("triggers are not in canonical schedule-then-manual order");
|
|
106
|
+
}
|
|
107
|
+
nextScheduleOrdinal++;
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
if (manualSeen)
|
|
111
|
+
fail("triggers contain duplicate workflow_dispatch entries");
|
|
112
|
+
manualSeen = true;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
function validateJob(value, index, jobIds, options) {
|
|
117
|
+
const job = record(value, `job ${index}`);
|
|
118
|
+
keys(job, ["id", "name", "needs", "steps", "extensions", "source"], `job ${index}`);
|
|
119
|
+
const id = sourceId(job.id, `job ${index} id`);
|
|
120
|
+
if (jobIds.has(id))
|
|
121
|
+
fail(`duplicate job id ${id}`);
|
|
122
|
+
jobIds.add(id);
|
|
123
|
+
optionalString(job.name, `job ${id} name`);
|
|
124
|
+
stringList(job.needs, `job ${id} needs`, 256, true);
|
|
125
|
+
const needs = job.needs;
|
|
126
|
+
if (needs.some((need, needIndex) => needIndex > 0 && compareCodePoints(needs[needIndex - 1] ?? "", need) > 0)) {
|
|
127
|
+
fail(`job ${id} needs are not in canonical order`);
|
|
128
|
+
}
|
|
129
|
+
if (!Array.isArray(job.steps) || job.steps.length === 0 || job.steps.length > 256) {
|
|
130
|
+
fail(`job ${id} steps must contain 1 through 256 entries`);
|
|
131
|
+
}
|
|
132
|
+
const stepIds = new Set();
|
|
133
|
+
for (const [stepIndex, step] of job.steps.entries()) {
|
|
134
|
+
validateStep(step, id, stepIndex, stepIds, options);
|
|
135
|
+
}
|
|
136
|
+
validateRouteTargets(job.steps, id);
|
|
137
|
+
extensions(job.extensions, `job ${id} extensions`);
|
|
138
|
+
span(job.source, `job ${id} source`);
|
|
139
|
+
}
|
|
140
|
+
function validateStep(value, jobId, index, stepIds, options) {
|
|
141
|
+
const step = record(value, `job ${jobId} step ${index}`);
|
|
142
|
+
keys(step, [
|
|
143
|
+
"id",
|
|
144
|
+
"name",
|
|
145
|
+
"uses",
|
|
146
|
+
"run",
|
|
147
|
+
"commandMode",
|
|
148
|
+
"exec",
|
|
149
|
+
"with",
|
|
150
|
+
"env",
|
|
151
|
+
"shell",
|
|
152
|
+
"workingDirectory",
|
|
153
|
+
"unit",
|
|
154
|
+
"map",
|
|
155
|
+
"route",
|
|
156
|
+
"inputs",
|
|
157
|
+
"output",
|
|
158
|
+
"gate",
|
|
159
|
+
"instructions",
|
|
160
|
+
"extensions",
|
|
161
|
+
"source",
|
|
162
|
+
], `job ${jobId} step ${index}`);
|
|
163
|
+
const id = sourceId(step.id, `job ${jobId} step ${index} id`);
|
|
164
|
+
if (stepIds.has(id))
|
|
165
|
+
fail(`job ${jobId} has duplicate step id ${id}`);
|
|
166
|
+
stepIds.add(id);
|
|
167
|
+
optionalString(step.name, `step ${id} name`);
|
|
168
|
+
const hasUses = typeof step.uses === "string" && step.uses.length > 0;
|
|
169
|
+
const hasRun = typeof step.run === "string" && step.run.length > 0;
|
|
170
|
+
const hasExec = step.exec !== undefined;
|
|
171
|
+
const hasRoute = step.route !== undefined;
|
|
172
|
+
const targetCount = Number(hasUses) + Number(hasRun) + Number(hasExec);
|
|
173
|
+
if ((hasRoute && targetCount !== 0) || (!hasRoute && targetCount !== 1)) {
|
|
174
|
+
fail(`step ${id} must contain exactly one of uses, run, or exec unless it is a route`);
|
|
175
|
+
}
|
|
176
|
+
if (step.uses !== undefined && !hasUses)
|
|
177
|
+
fail(`step ${id} uses must be a non-empty string`);
|
|
178
|
+
if (step.run !== undefined && !hasRun)
|
|
179
|
+
fail(`step ${id} run must be a non-empty string`);
|
|
180
|
+
if (hasUses) {
|
|
181
|
+
let target;
|
|
182
|
+
try {
|
|
183
|
+
target = classifyWorkflowStepUses(step.uses);
|
|
184
|
+
}
|
|
185
|
+
catch (cause) {
|
|
186
|
+
semanticFail(cause, `step ${id} uses`);
|
|
187
|
+
}
|
|
188
|
+
if (target.kind === "builtin-command") {
|
|
189
|
+
if (step.commandMode !== "literal" &&
|
|
190
|
+
step.commandMode !== "portable-template" &&
|
|
191
|
+
step.commandMode !== "stored-ref") {
|
|
192
|
+
fail(`step ${id} akm/command requires an explicit commandMode`);
|
|
193
|
+
}
|
|
194
|
+
try {
|
|
195
|
+
validateWorkflowBuiltinCommand(step.with, step.commandMode);
|
|
196
|
+
}
|
|
197
|
+
catch (cause) {
|
|
198
|
+
semanticFail(cause, `step ${id} uses`);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
else if (step.commandMode !== undefined) {
|
|
202
|
+
fail(`step ${id} commandMode is legal only with uses akm/command`);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
else if (step.commandMode !== undefined) {
|
|
206
|
+
fail(`step ${id} commandMode is legal only with uses akm/command`);
|
|
207
|
+
}
|
|
208
|
+
if (hasRun) {
|
|
209
|
+
try {
|
|
210
|
+
step.run = canonicalizeWorkflowRun(step.run);
|
|
211
|
+
}
|
|
212
|
+
catch (cause) {
|
|
213
|
+
semanticFail(cause, `step ${id} run`);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
validateExec(step.exec, `step ${id} exec`, options);
|
|
217
|
+
scalarRecord(step.with, `step ${id} with`, true);
|
|
218
|
+
environment(step.env, `step ${id} env`);
|
|
219
|
+
rejectStepWithExpressions(step, id);
|
|
220
|
+
rejectExpressionsInRecord(step.env, `step ${id} env`);
|
|
221
|
+
if (step.with !== undefined && !hasUses)
|
|
222
|
+
fail(`step ${id} with is legal only with uses`);
|
|
223
|
+
if ((step.shell !== undefined || step.workingDirectory !== undefined) && !hasRun) {
|
|
224
|
+
fail(`step ${id} shell and workingDirectory are legal only with run`);
|
|
225
|
+
}
|
|
226
|
+
if (step.shell !== undefined && (typeof step.shell !== "string" || !HOST_SHELLS.has(step.shell))) {
|
|
227
|
+
fail(`step ${id} has an unsupported shell`);
|
|
228
|
+
}
|
|
229
|
+
optionalString(step.workingDirectory, `step ${id} workingDirectory`);
|
|
230
|
+
if (step.workingDirectory !== undefined) {
|
|
231
|
+
try {
|
|
232
|
+
step.workingDirectory = canonicalizeWorkflowWorkingDirectory(step.workingDirectory, options.workspaceRoot);
|
|
233
|
+
}
|
|
234
|
+
catch (cause) {
|
|
235
|
+
semanticFail(cause, `step ${id} workingDirectory`);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
validateUnit(step.unit, `step ${id} unit`, false);
|
|
239
|
+
if (hasExec && step.unit !== undefined) {
|
|
240
|
+
const unit = step.unit;
|
|
241
|
+
if (unit.engine !== undefined || unit.model !== undefined || unit.llm !== undefined) {
|
|
242
|
+
fail(`step ${id} exec cannot also select an engine, model, or llm override`);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
validateMap(step.map, `step ${id} map`);
|
|
246
|
+
validateRoute(step.route, `step ${id} route`);
|
|
247
|
+
optionalStringList(step.inputs, `step ${id} inputs`, WORKFLOW_MAX_INPUTS);
|
|
248
|
+
if (Array.isArray(step.inputs)) {
|
|
249
|
+
for (const [inputIndex, input] of step.inputs.entries())
|
|
250
|
+
validateReference(input, `step ${id} inputs[${inputIndex}]`);
|
|
251
|
+
}
|
|
252
|
+
validateSchema(step.output, `step ${id} output`);
|
|
253
|
+
validateGate(step.gate, `step ${id} gate`);
|
|
254
|
+
optionalString(step.instructions, `step ${id} instructions`);
|
|
255
|
+
if (hasRoute && (step.map !== undefined || step.unit !== undefined || step.inputs !== undefined)) {
|
|
256
|
+
fail(`step ${id} route cannot contain map, unit, or inputs`);
|
|
257
|
+
}
|
|
258
|
+
extensions(step.extensions, `step ${id} extensions`);
|
|
259
|
+
span(step.source, `step ${id} source`);
|
|
260
|
+
}
|
|
261
|
+
function validateTopologicalJobs(jobs) {
|
|
262
|
+
const result = canonicalTopologicalJobs(jobs);
|
|
263
|
+
if (!result.ok) {
|
|
264
|
+
if (result.kind === "missing")
|
|
265
|
+
fail(`job ${result.job.id} needs missing job ${result.dependency}`);
|
|
266
|
+
fail("jobs contain a dependency cycle");
|
|
267
|
+
}
|
|
268
|
+
if (result.jobs.some((job, index) => job.id !== jobs[index]?.id)) {
|
|
269
|
+
fail("jobs are not in canonical dependency-topological order");
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
function compareCodePoints(left, right) {
|
|
273
|
+
return compareWorkflowSourceCodePoints(left, right);
|
|
274
|
+
}
|
|
275
|
+
function validateExec(value, location, options) {
|
|
276
|
+
if (value === undefined)
|
|
277
|
+
return;
|
|
278
|
+
const exec = record(value, location);
|
|
279
|
+
keys(exec, ["command", "cwd", "passEnv"], location);
|
|
280
|
+
stringList(exec.command, `${location}.command`, WORKFLOW_MAX_EXEC_ARGV, false);
|
|
281
|
+
for (const [index, argument] of exec.command.entries()) {
|
|
282
|
+
if (utf8Bytes(argument) > WORKFLOW_MAX_EXEC_ARG_BYTES) {
|
|
283
|
+
fail(`${location}.command[${index}] exceeds ${WORKFLOW_MAX_EXEC_ARG_BYTES} bytes`);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
try {
|
|
287
|
+
rejectNulInArgv(exec.command);
|
|
288
|
+
}
|
|
289
|
+
catch (cause) {
|
|
290
|
+
semanticFail(cause, location);
|
|
291
|
+
}
|
|
292
|
+
optionalString(exec.cwd, `${location}.cwd`);
|
|
293
|
+
if (exec.cwd !== undefined) {
|
|
294
|
+
if (exec.cwd.length > WORKFLOW_MAX_EXEC_CWD_LENGTH) {
|
|
295
|
+
fail(`${location}.cwd exceeds ${WORKFLOW_MAX_EXEC_CWD_LENGTH} characters`);
|
|
296
|
+
}
|
|
297
|
+
try {
|
|
298
|
+
exec.cwd = canonicalizeWorkflowWorkingDirectory(exec.cwd, options.workspaceRoot);
|
|
299
|
+
}
|
|
300
|
+
catch (cause) {
|
|
301
|
+
semanticFail(cause, `${location}.cwd`);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
if (exec.passEnv !== undefined) {
|
|
305
|
+
stringList(exec.passEnv, `${location}.passEnv`, WORKFLOW_MAX_EXEC_PASS_ENV, false);
|
|
306
|
+
}
|
|
307
|
+
if (Array.isArray(exec.passEnv)) {
|
|
308
|
+
for (const name of exec.passEnv) {
|
|
309
|
+
if (!WORKFLOW_ENV_VAR_NAME_PATTERN.test(name))
|
|
310
|
+
fail(`${location}.passEnv has invalid environment name ${name}`);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
function validateUnit(value, location, defaults) {
|
|
315
|
+
if (value === undefined)
|
|
316
|
+
return;
|
|
317
|
+
const unit = record(value, location);
|
|
318
|
+
const allowed = defaults
|
|
319
|
+
? ["engine", "model", "llm", "timeoutMs", "onError"]
|
|
320
|
+
: ["engine", "model", "llm", "timeoutMs", "retry", "onError", "output", "env", "isolation"];
|
|
321
|
+
keys(unit, allowed, location);
|
|
322
|
+
optionalString(unit.engine, `${location}.engine`);
|
|
323
|
+
if (typeof unit.engine === "string") {
|
|
324
|
+
const engine = unit.engine.trim();
|
|
325
|
+
unit.engine = engine;
|
|
326
|
+
if (!WORKFLOW_ENGINE_NAME_PATTERN.test(engine) || engine.length > WORKFLOW_MAX_ENGINE_NAME_LENGTH) {
|
|
327
|
+
fail(`${location}.engine has an invalid engine name`);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
optionalString(unit.model, `${location}.model`);
|
|
331
|
+
if (typeof unit.model === "string")
|
|
332
|
+
unit.model = unit.model.trim();
|
|
333
|
+
validateLlm(unit.llm, `${location}.llm`);
|
|
334
|
+
if (unit.timeoutMs !== undefined &&
|
|
335
|
+
unit.timeoutMs !== null &&
|
|
336
|
+
(!Number.isSafeInteger(unit.timeoutMs) ||
|
|
337
|
+
unit.timeoutMs < 1 ||
|
|
338
|
+
unit.timeoutMs > WORKFLOW_MAX_TIMEOUT_MS)) {
|
|
339
|
+
fail(`${location}.timeoutMs must be null or an integer from 1 through ${WORKFLOW_MAX_TIMEOUT_MS}`);
|
|
340
|
+
}
|
|
341
|
+
if (unit.onError !== undefined && unit.onError !== "fail" && unit.onError !== "continue") {
|
|
342
|
+
fail(`${location}.onError must be fail or continue`);
|
|
343
|
+
}
|
|
344
|
+
if (!defaults) {
|
|
345
|
+
validateRetry(unit.retry, `${location}.retry`);
|
|
346
|
+
validateSchema(unit.output, `${location}.output`);
|
|
347
|
+
optionalStringList(unit.env, `${location}.env`, 256);
|
|
348
|
+
if (Array.isArray(unit.env)) {
|
|
349
|
+
for (const [index, ref] of unit.env.entries())
|
|
350
|
+
validateAssetRef(ref, `${location}.env[${index}]`);
|
|
351
|
+
}
|
|
352
|
+
if (unit.isolation !== undefined && unit.isolation !== "none" && unit.isolation !== "worktree") {
|
|
353
|
+
fail(`${location}.isolation must be none or worktree`);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
function validateRetry(value, location) {
|
|
358
|
+
if (value === undefined)
|
|
359
|
+
return;
|
|
360
|
+
const retry = record(value, location);
|
|
361
|
+
keys(retry, ["max", "on"], location);
|
|
362
|
+
if (!Number.isSafeInteger(retry.max) || retry.max < 0 || retry.max > WORKFLOW_MAX_RETRIES) {
|
|
363
|
+
fail(`${location}.max must be an integer from 0 through ${WORKFLOW_MAX_RETRIES}`);
|
|
364
|
+
}
|
|
365
|
+
stringList(retry.on, `${location}.on`, PROGRAM_RETRY_REASONS.length, false);
|
|
366
|
+
for (const reason of retry.on) {
|
|
367
|
+
if (!PROGRAM_RETRY_REASONS.includes(reason)) {
|
|
368
|
+
fail(`${location}.on contains unknown failure reason ${reason}`);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
function validateMap(value, location) {
|
|
373
|
+
if (value === undefined)
|
|
374
|
+
return;
|
|
375
|
+
const map = record(value, location);
|
|
376
|
+
keys(map, ["over", "concurrency", "reducer"], location);
|
|
377
|
+
nonEmptyString(map.over, `${location}.over`);
|
|
378
|
+
validateReference(map.over, `${location}.over`);
|
|
379
|
+
if (map.concurrency !== undefined &&
|
|
380
|
+
(!Number.isSafeInteger(map.concurrency) ||
|
|
381
|
+
map.concurrency < 1 ||
|
|
382
|
+
map.concurrency > WORKFLOW_MAX_CONCURRENCY)) {
|
|
383
|
+
fail(`${location}.concurrency must be an integer from 1 through ${WORKFLOW_MAX_CONCURRENCY}`);
|
|
384
|
+
}
|
|
385
|
+
if (map.reducer !== undefined && map.reducer !== "collect" && map.reducer !== "vote") {
|
|
386
|
+
fail(`${location}.reducer must be collect or vote`);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
function validateRoute(value, location) {
|
|
390
|
+
if (value === undefined)
|
|
391
|
+
return;
|
|
392
|
+
const route = record(value, location);
|
|
393
|
+
keys(route, ["input", "branches", "defaultStepId"], location);
|
|
394
|
+
nonEmptyString(route.input, `${location}.input`);
|
|
395
|
+
validateReference(route.input, `${location}.input`);
|
|
396
|
+
if (!Array.isArray(route.branches) ||
|
|
397
|
+
route.branches.length === 0 ||
|
|
398
|
+
route.branches.length > WORKFLOW_MAX_ROUTE_BRANCHES) {
|
|
399
|
+
fail(`${location}.branches must contain 1 through ${WORKFLOW_MAX_ROUTE_BRANCHES} entries`);
|
|
400
|
+
}
|
|
401
|
+
const matches = new Set();
|
|
402
|
+
for (const [index, value] of route.branches.entries()) {
|
|
403
|
+
const branch = record(value, `${location}.branches[${index}]`);
|
|
404
|
+
keys(branch, ["match", "stepId"], `${location}.branches[${index}]`);
|
|
405
|
+
nonEmptyString(branch.match, `${location}.branches[${index}].match`);
|
|
406
|
+
sourceId(branch.stepId, `${location}.branches[${index}].stepId`);
|
|
407
|
+
if (matches.has(branch.match))
|
|
408
|
+
fail(`${location} contains duplicate match ${branch.match}`);
|
|
409
|
+
matches.add(branch.match);
|
|
410
|
+
}
|
|
411
|
+
if (route.defaultStepId !== undefined)
|
|
412
|
+
sourceId(route.defaultStepId, `${location}.defaultStepId`);
|
|
413
|
+
}
|
|
414
|
+
function validateGate(value, location) {
|
|
415
|
+
if (value === undefined)
|
|
416
|
+
return;
|
|
417
|
+
const gate = record(value, location);
|
|
418
|
+
keys(gate, ["maxLoops", "rubric"], location);
|
|
419
|
+
if (gate.maxLoops !== undefined &&
|
|
420
|
+
(!Number.isSafeInteger(gate.maxLoops) ||
|
|
421
|
+
gate.maxLoops < 1 ||
|
|
422
|
+
gate.maxLoops > WORKFLOW_MAX_GATE_LOOPS)) {
|
|
423
|
+
fail(`${location}.maxLoops must be an integer from 1 through ${WORKFLOW_MAX_GATE_LOOPS}`);
|
|
424
|
+
}
|
|
425
|
+
optionalString(gate.rubric, `${location}.rubric`);
|
|
426
|
+
}
|
|
427
|
+
function validateRouteTargets(steps, jobId) {
|
|
428
|
+
const indexById = new Map(steps.map((step, index) => [step.id, index]));
|
|
429
|
+
for (const [index, step] of steps.entries()) {
|
|
430
|
+
if (!step.route)
|
|
431
|
+
continue;
|
|
432
|
+
const targets = [...step.route.branches.map(({ stepId }) => stepId)];
|
|
433
|
+
if (step.route.defaultStepId !== undefined)
|
|
434
|
+
targets.push(step.route.defaultStepId);
|
|
435
|
+
for (const target of targets) {
|
|
436
|
+
const targetIndex = indexById.get(target);
|
|
437
|
+
if (targetIndex === undefined)
|
|
438
|
+
fail(`job ${jobId} route ${step.id} targets missing step ${target}`);
|
|
439
|
+
if (targetIndex <= index)
|
|
440
|
+
fail(`job ${jobId} route ${step.id} must target a later step`);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
function validateReference(value, location) {
|
|
445
|
+
const parsed = parseReference(value);
|
|
446
|
+
if (!parsed.ok)
|
|
447
|
+
fail(`${location} is invalid: ${parsed.message}`);
|
|
448
|
+
}
|
|
449
|
+
function validateParams(value) {
|
|
450
|
+
if (value === undefined)
|
|
451
|
+
return;
|
|
452
|
+
const params = record(value, "params");
|
|
453
|
+
if (Object.keys(params).length === 0 || Object.keys(params).length > WORKFLOW_MAX_PARAMS) {
|
|
454
|
+
fail(`params must contain 1 through ${WORKFLOW_MAX_PARAMS} entries`);
|
|
455
|
+
}
|
|
456
|
+
for (const [name, schema] of Object.entries(params)) {
|
|
457
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name))
|
|
458
|
+
fail(`params has invalid name ${name}`);
|
|
459
|
+
validateSchema(schema, `params.${name}`, false);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
function validateBudget(value) {
|
|
463
|
+
if (value === undefined)
|
|
464
|
+
return;
|
|
465
|
+
const budget = record(value, "budget");
|
|
466
|
+
keys(budget, ["maxTokens", "maxUnits"], "budget");
|
|
467
|
+
for (const key of ["maxTokens", "maxUnits"]) {
|
|
468
|
+
if (budget[key] !== undefined && (!Number.isSafeInteger(budget[key]) || budget[key] < 1)) {
|
|
469
|
+
fail(`budget.${key} must be a positive integer`);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
if (typeof budget.maxUnits === "number" && budget.maxUnits > WORKFLOW_MAX_MAP_EXPANSION) {
|
|
473
|
+
fail(`budget.maxUnits must be at most ${WORKFLOW_MAX_MAP_EXPANSION}`);
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
function validateLlm(value, location) {
|
|
477
|
+
if (value === undefined)
|
|
478
|
+
return;
|
|
479
|
+
const llm = record(value, location);
|
|
480
|
+
keys(llm, [
|
|
481
|
+
"temperature",
|
|
482
|
+
"maxTokens",
|
|
483
|
+
"supportsJsonSchema",
|
|
484
|
+
"extraParams",
|
|
485
|
+
"contextLength",
|
|
486
|
+
"enableThinking",
|
|
487
|
+
"reasoningEffort",
|
|
488
|
+
], location);
|
|
489
|
+
if (llm.temperature !== undefined && typeof llm.temperature !== "number") {
|
|
490
|
+
fail(`${location}.temperature must be a finite number`);
|
|
491
|
+
}
|
|
492
|
+
for (const key of ["maxTokens", "contextLength"]) {
|
|
493
|
+
if (llm[key] !== undefined && (!Number.isSafeInteger(llm[key]) || llm[key] < 1)) {
|
|
494
|
+
fail(`${location}.${key} must be a positive integer`);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
for (const key of ["supportsJsonSchema", "enableThinking"]) {
|
|
498
|
+
if (llm[key] !== undefined && typeof llm[key] !== "boolean")
|
|
499
|
+
fail(`${location}.${key} must be a boolean`);
|
|
500
|
+
}
|
|
501
|
+
if (llm.reasoningEffort !== undefined &&
|
|
502
|
+
(typeof llm.reasoningEffort !== "string" || llm.reasoningEffort.trim() === "")) {
|
|
503
|
+
fail(`${location}.reasoningEffort must be a non-empty string`);
|
|
504
|
+
}
|
|
505
|
+
if (llm.extraParams !== undefined) {
|
|
506
|
+
const extra = record(llm.extraParams, `${location}.extraParams`);
|
|
507
|
+
const issues = validateExtraParams(extra);
|
|
508
|
+
if (issues.length > 0)
|
|
509
|
+
fail(`${location}.extraParams is invalid: ${issues[0]?.message ?? "invalid value"}`);
|
|
510
|
+
if (jsonBytes(extra) > WORKFLOW_MAX_EXTRA_PARAMS_BYTES) {
|
|
511
|
+
fail(`${location}.extraParams exceeds ${WORKFLOW_MAX_EXTRA_PARAMS_BYTES} bytes`);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
function validateSchema(value, location, optional = true) {
|
|
516
|
+
if (value === undefined && optional)
|
|
517
|
+
return;
|
|
518
|
+
const schema = record(value, location);
|
|
519
|
+
if (jsonBytes(schema) > WORKFLOW_MAX_SCHEMA_BYTES)
|
|
520
|
+
fail(`${location} exceeds ${WORKFLOW_MAX_SCHEMA_BYTES} bytes`);
|
|
521
|
+
const issue = checkJsonSchemaDefinition(schema)[0];
|
|
522
|
+
if (issue)
|
|
523
|
+
fail(`${location} is invalid: ${issue.message}`);
|
|
524
|
+
}
|
|
525
|
+
function validateAssetRef(value, location) {
|
|
526
|
+
try {
|
|
527
|
+
const parsed = parseBundleRef(value);
|
|
528
|
+
if (parsed.fragment !== undefined || bundleRefToString(parsed) !== value)
|
|
529
|
+
fail(`${location} is not canonical`);
|
|
530
|
+
}
|
|
531
|
+
catch (cause) {
|
|
532
|
+
fail(`${location} is invalid: ${cause instanceof Error ? cause.message : String(cause)}`);
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
function optionalStringList(value, location, max) {
|
|
536
|
+
if (value === undefined)
|
|
537
|
+
return;
|
|
538
|
+
stringList(value, location, max, true);
|
|
539
|
+
}
|
|
540
|
+
function rejectExpressionsInRecord(value, location) {
|
|
541
|
+
if (value === undefined)
|
|
542
|
+
return;
|
|
543
|
+
for (const [key, item] of Object.entries(record(value, location))) {
|
|
544
|
+
if (typeof item === "string" && item.includes("${{"))
|
|
545
|
+
fail(`${location}.${key} contains an unsupported expression`);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
function rejectStepWithExpressions(step, id) {
|
|
549
|
+
if (step.with === undefined)
|
|
550
|
+
return;
|
|
551
|
+
for (const [key, item] of Object.entries(record(step.with, `step ${id} with`))) {
|
|
552
|
+
if (typeof item !== "string" || !item.includes("${{"))
|
|
553
|
+
continue;
|
|
554
|
+
if (step.uses === "akm/command" && step.commandMode === "literal" && key === "content")
|
|
555
|
+
continue;
|
|
556
|
+
fail(`step ${id} with.${key} contains an unsupported expression`);
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
function semanticFail(cause, location) {
|
|
560
|
+
if (cause instanceof WorkflowSourceSemanticError)
|
|
561
|
+
fail(`${location}: ${cause.message}`);
|
|
562
|
+
fail(`${location}: ${cause instanceof Error ? cause.message : String(cause)}`);
|
|
563
|
+
}
|
|
564
|
+
function snapshotPlainData(input) {
|
|
565
|
+
return snapshotValue(input, 0, "source IR", { nodes: 0, bytes: 0 });
|
|
566
|
+
}
|
|
567
|
+
function snapshotValue(value, depth, location, state) {
|
|
568
|
+
state.nodes++;
|
|
569
|
+
if (state.nodes > WORKFLOW_SOURCE_IR_MAX_NODES)
|
|
570
|
+
fail("source IR exceeds the node limit");
|
|
571
|
+
if (depth > WORKFLOW_SOURCE_IR_MAX_DEPTH)
|
|
572
|
+
fail("source IR exceeds the depth limit");
|
|
573
|
+
if (value === null || typeof value === "boolean") {
|
|
574
|
+
countJsonBytes(state, value === null ? "null" : String(value));
|
|
575
|
+
return value;
|
|
576
|
+
}
|
|
577
|
+
if (typeof value === "string") {
|
|
578
|
+
if (!wellFormedUnicode(value))
|
|
579
|
+
fail(`${location} must contain well-formed Unicode`);
|
|
580
|
+
countJsonBytes(state, JSON.stringify(value));
|
|
581
|
+
return value;
|
|
582
|
+
}
|
|
583
|
+
if (typeof value === "number") {
|
|
584
|
+
if (!Number.isFinite(value))
|
|
585
|
+
fail(`${location} contains a non-finite number`);
|
|
586
|
+
const canonical = Object.is(value, -0) ? 0 : value;
|
|
587
|
+
countJsonBytes(state, JSON.stringify(canonical));
|
|
588
|
+
return canonical;
|
|
589
|
+
}
|
|
590
|
+
if (typeof value !== "object")
|
|
591
|
+
fail(`${location} contains non-JSON data`);
|
|
592
|
+
if (utilTypes.isProxy(value))
|
|
593
|
+
fail(`${location} must not be a Proxy object`);
|
|
594
|
+
return Array.isArray(value)
|
|
595
|
+
? snapshotArray(value, depth, location, state)
|
|
596
|
+
: snapshotObject(value, depth, location, state);
|
|
597
|
+
}
|
|
598
|
+
function snapshotArray(value, depth, location, state) {
|
|
599
|
+
if (Object.getPrototypeOf(value) !== Array.prototype)
|
|
600
|
+
fail(`${location} must be a plain array`);
|
|
601
|
+
const ownKeys = Reflect.ownKeys(value);
|
|
602
|
+
const lengthDescriptor = Reflect.getOwnPropertyDescriptor(value, "length");
|
|
603
|
+
if (!lengthDescriptor || !("value" in lengthDescriptor) || !Number.isSafeInteger(lengthDescriptor.value)) {
|
|
604
|
+
fail(`${location} has an invalid array length`);
|
|
605
|
+
}
|
|
606
|
+
const length = lengthDescriptor.value;
|
|
607
|
+
if (length < 0 || length > WORKFLOW_SOURCE_IR_MAX_NODES)
|
|
608
|
+
fail(`${location} exceeds the array length limit`);
|
|
609
|
+
const expected = new Set(["length", ...Array.from({ length }, (_, index) => String(index))]);
|
|
610
|
+
for (const key of ownKeys) {
|
|
611
|
+
if (typeof key === "symbol")
|
|
612
|
+
fail(`${location} contains symbol keys`);
|
|
613
|
+
if (!expected.has(key))
|
|
614
|
+
fail(`${location} contains unexpected array property ${key}`);
|
|
615
|
+
}
|
|
616
|
+
const snapshot = new Array(length);
|
|
617
|
+
countJsonBytes(state, "[");
|
|
618
|
+
for (let index = 0; index < length; index++) {
|
|
619
|
+
const descriptor = Reflect.getOwnPropertyDescriptor(value, String(index));
|
|
620
|
+
if (!descriptor)
|
|
621
|
+
fail(`${location} contains a sparse array`);
|
|
622
|
+
if (!("value" in descriptor))
|
|
623
|
+
fail(`${location}[${index}] is an accessor property`);
|
|
624
|
+
if (!descriptor.enumerable)
|
|
625
|
+
fail(`${location}[${index}] is non-enumerable`);
|
|
626
|
+
if (index > 0)
|
|
627
|
+
countJsonBytes(state, ",");
|
|
628
|
+
snapshot[index] = snapshotValue(descriptor.value, depth + 1, `${location}[${index}]`, state);
|
|
629
|
+
}
|
|
630
|
+
countJsonBytes(state, "]");
|
|
631
|
+
return snapshot;
|
|
632
|
+
}
|
|
633
|
+
function snapshotObject(value, depth, location, state) {
|
|
634
|
+
if (Object.getPrototypeOf(value) !== Object.prototype)
|
|
635
|
+
fail(`${location} must be a plain object`);
|
|
636
|
+
const ownKeys = Reflect.ownKeys(value);
|
|
637
|
+
if (ownKeys.length > WORKFLOW_SOURCE_IR_MAX_NODES)
|
|
638
|
+
fail(`${location} exceeds the object key limit`);
|
|
639
|
+
const snapshot = {};
|
|
640
|
+
countJsonBytes(state, "{");
|
|
641
|
+
for (const [index, key] of ownKeys.entries()) {
|
|
642
|
+
if (typeof key === "symbol")
|
|
643
|
+
fail(`${location} contains symbol keys`);
|
|
644
|
+
if (!wellFormedUnicode(key))
|
|
645
|
+
fail(`${location} contains an ill-formed key`);
|
|
646
|
+
if (UNSAFE_KEYS.has(key))
|
|
647
|
+
fail(`${location} contains unsafe key ${key}`);
|
|
648
|
+
const descriptor = Reflect.getOwnPropertyDescriptor(value, key);
|
|
649
|
+
if (!descriptor)
|
|
650
|
+
fail(`${location}.${key} has no stable own descriptor`);
|
|
651
|
+
if (!("value" in descriptor))
|
|
652
|
+
fail(`${location}.${key} is an accessor property`);
|
|
653
|
+
if (!descriptor.enumerable)
|
|
654
|
+
fail(`${location}.${key} is non-enumerable`);
|
|
655
|
+
if (index > 0)
|
|
656
|
+
countJsonBytes(state, ",");
|
|
657
|
+
countJsonBytes(state, `${JSON.stringify(key)}:`);
|
|
658
|
+
Object.defineProperty(snapshot, key, {
|
|
659
|
+
configurable: true,
|
|
660
|
+
enumerable: true,
|
|
661
|
+
value: snapshotValue(descriptor.value, depth + 1, `${location}.${key}`, state),
|
|
662
|
+
writable: true,
|
|
663
|
+
});
|
|
664
|
+
}
|
|
665
|
+
countJsonBytes(state, "}");
|
|
666
|
+
return snapshot;
|
|
667
|
+
}
|
|
668
|
+
function countJsonBytes(state, token) {
|
|
669
|
+
state.bytes += Buffer.byteLength(token, "utf8");
|
|
670
|
+
if (state.bytes > WORKFLOW_SOURCE_IR_MAX_BYTES)
|
|
671
|
+
fail("source IR exceeds the byte limit");
|
|
672
|
+
}
|
|
673
|
+
function wellFormedUnicode(value) {
|
|
674
|
+
for (let index = 0; index < value.length; index++) {
|
|
675
|
+
const code = value.charCodeAt(index);
|
|
676
|
+
if (code >= 0xd800 && code <= 0xdbff) {
|
|
677
|
+
const next = value.charCodeAt(index + 1);
|
|
678
|
+
if (!(next >= 0xdc00 && next <= 0xdfff))
|
|
679
|
+
return false;
|
|
680
|
+
index++;
|
|
681
|
+
}
|
|
682
|
+
else if (code >= 0xdc00 && code <= 0xdfff) {
|
|
683
|
+
return false;
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
return true;
|
|
687
|
+
}
|
|
688
|
+
function record(value, location) {
|
|
689
|
+
if (value === null || typeof value !== "object" || Array.isArray(value))
|
|
690
|
+
fail(`${location} must be an object`);
|
|
691
|
+
return value;
|
|
692
|
+
}
|
|
693
|
+
function keys(value, allowed, location) {
|
|
694
|
+
const accepted = new Set(allowed);
|
|
695
|
+
for (const key of Object.keys(value))
|
|
696
|
+
if (!accepted.has(key))
|
|
697
|
+
fail(`${location} has unknown key ${key}`);
|
|
698
|
+
}
|
|
699
|
+
function sourceId(value, location) {
|
|
700
|
+
nonEmptyString(value, location);
|
|
701
|
+
if (!/^[A-Za-z_][A-Za-z0-9_-]{0,127}$/.test(value))
|
|
702
|
+
fail(`${location} has an invalid identifier`);
|
|
703
|
+
return value;
|
|
704
|
+
}
|
|
705
|
+
function nonEmptyString(value, location) {
|
|
706
|
+
if (typeof value !== "string" || value.trim() === "")
|
|
707
|
+
fail(`${location} must be a non-empty string`);
|
|
708
|
+
}
|
|
709
|
+
function optionalString(value, location) {
|
|
710
|
+
if (value !== undefined)
|
|
711
|
+
nonEmptyString(value, location);
|
|
712
|
+
}
|
|
713
|
+
function stringList(value, location, max, allowEmpty) {
|
|
714
|
+
if (!Array.isArray(value) || (!allowEmpty && value.length === 0) || value.length > max) {
|
|
715
|
+
fail(`${location} must be ${allowEmpty ? "an" : "a non-empty"} array with at most ${max} entries`);
|
|
716
|
+
}
|
|
717
|
+
const seen = new Set();
|
|
718
|
+
for (const [index, item] of value.entries()) {
|
|
719
|
+
nonEmptyString(item, `${location}[${index}]`);
|
|
720
|
+
if (seen.has(item))
|
|
721
|
+
fail(`${location} contains duplicate ${item}`);
|
|
722
|
+
seen.add(item);
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
function scalarRecord(value, location, allowNull) {
|
|
726
|
+
if (value === undefined)
|
|
727
|
+
return;
|
|
728
|
+
const map = record(value, location);
|
|
729
|
+
for (const [key, item] of Object.entries(map)) {
|
|
730
|
+
if (!/^[A-Za-z_][A-Za-z0-9_.-]{0,127}$/.test(key))
|
|
731
|
+
fail(`${location} has invalid key ${key}`);
|
|
732
|
+
if (item === null && allowNull)
|
|
733
|
+
continue;
|
|
734
|
+
if (typeof item !== "string" && typeof item !== "number" && typeof item !== "boolean") {
|
|
735
|
+
fail(`${location}.${key} must be a scalar`);
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
function environment(value, location) {
|
|
740
|
+
if (value === undefined)
|
|
741
|
+
return;
|
|
742
|
+
const map = record(value, location);
|
|
743
|
+
for (const [key, item] of Object.entries(map)) {
|
|
744
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key))
|
|
745
|
+
fail(`${location} has invalid environment name ${key}`);
|
|
746
|
+
if (typeof item !== "string" && typeof item !== "number" && typeof item !== "boolean") {
|
|
747
|
+
fail(`${location}.${key} must be a string, number, or boolean`);
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
function extensions(value, location) {
|
|
752
|
+
if (value === undefined)
|
|
753
|
+
return;
|
|
754
|
+
const map = record(value, location);
|
|
755
|
+
for (const owner of Object.keys(map)) {
|
|
756
|
+
if (!WORKFLOW_SOURCE_EXTENSION_OWNER_PATTERN.test(owner))
|
|
757
|
+
fail(`${location} has invalid owner ${owner}`);
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
function span(value, location) {
|
|
761
|
+
const source = record(value, location);
|
|
762
|
+
keys(source, ["path", "start", "end"], location);
|
|
763
|
+
nonEmptyString(source.path, `${location}.path`);
|
|
764
|
+
if (!Number.isSafeInteger(source.start) || source.start < 1)
|
|
765
|
+
fail(`${location}.start is invalid`);
|
|
766
|
+
if (!Number.isSafeInteger(source.end) || source.end < source.start) {
|
|
767
|
+
fail(`${location}.end is invalid`);
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
function fail(message) {
|
|
771
|
+
throw new Error(`Invalid workflow source IR v1: ${message}`);
|
|
772
|
+
}
|