akm-cli 0.9.1 → 0.9.2-alpha.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +103 -28
- 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/stash-skeleton/facts/conventions/backlinks.md +3 -4
- package/dist/assets/stash-skeleton/facts/conventions/organization.md +1 -3
- 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 +5 -6
- 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-prompt.js +64 -22
- package/dist/commands/improve/extract.js +608 -360
- 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 +53 -22
- 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 +901 -177
- 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 +85 -35
- 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 +23 -26
- package/dist/core/config/schema/engines.js +8 -7
- package/dist/core/config/schema/improve-processes.js +29 -5
- package/dist/core/config/schema/index-config.js +0 -27
- 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 +78 -39
- package/dist/indexer/indexer.js +471 -432
- package/dist/indexer/installations.js +6 -0
- package/dist/indexer/lookup/adapter-concept-owner.js +283 -0
- package/dist/indexer/materialize-embeddings.js +155 -0
- package/dist/indexer/passes/memory-inference.js +227 -174
- package/dist/indexer/passes/metadata.js +263 -118
- package/dist/indexer/scan/doc-to-entry.js +7 -10
- package/dist/indexer/scan/drain-dir.js +51 -23
- package/dist/indexer/search/db-search.js +156 -50
- package/dist/indexer/search/fts-query.js +40 -40
- package/dist/indexer/search/ranking.js +36 -1
- package/dist/indexer/search/search-attribution.js +3 -1
- package/dist/indexer/search/search-fields.js +23 -14
- 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 +31 -33
- 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 +19334 -52269
- package/dist/scripts/akm-migrate.js +19270 -51612
- 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 +203 -229
- 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 +64 -71
- 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 +74 -350
- package/dist/storage/repositories/index-utility-repository.js +12 -17
- package/dist/storage/repositories/index-vec-repository.js +56 -7
- 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 +159 -36
- 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 -224
- 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,328 @@
|
|
|
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
|
+
* Durable-v4 symbolic workflow environments.
|
|
6
|
+
*
|
|
7
|
+
* Env-file values are the deliberately narrow live-value exception to a
|
|
8
|
+
* frozen workflow plan. Freeze persists only the qualified owner, physical
|
|
9
|
+
* containment, exact key set, and secret-token topology. Materialization
|
|
10
|
+
* later reads current values directly through that descriptor, without
|
|
11
|
+
* consulting config, the index, or authored workflow source.
|
|
12
|
+
*/
|
|
13
|
+
import fs from "node:fs";
|
|
14
|
+
import path from "node:path";
|
|
15
|
+
import dotenv from "dotenv";
|
|
16
|
+
import { assetPathForName } from "../../core/asset/asset-placement.js";
|
|
17
|
+
import { isWithin } from "../../core/common.js";
|
|
18
|
+
import { NotFoundError, UsageError } from "../../core/errors.js";
|
|
19
|
+
import { captureGuardedExecutionSource, GuardedExecutionSourceCollector } from "../../execution/guarded-source.js";
|
|
20
|
+
const SECRET_TOKEN_RE = /\$\{secret:([A-Za-z0-9_./-]+)\}/g;
|
|
21
|
+
const ENVIRONMENT_NAME_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
22
|
+
const QUALIFIED_ENV_REF_RE = /^([^/]+)\/\/env\/(.+)$/;
|
|
23
|
+
/** Freeze value-free descriptors in authored ref order. */
|
|
24
|
+
export function freezeWorkflowEnvironment(refs, options) {
|
|
25
|
+
const collector = options.collector ?? new GuardedExecutionSourceCollector();
|
|
26
|
+
const logical = new Set();
|
|
27
|
+
const physical = new Map();
|
|
28
|
+
const out = [];
|
|
29
|
+
for (const [precedence, inputRef] of refs.entries()) {
|
|
30
|
+
const resolved = options.resolveRef(inputRef);
|
|
31
|
+
const match = QUALIFIED_ENV_REF_RE.exec(resolved.ref);
|
|
32
|
+
if (!match || match[1] !== resolved.bundle) {
|
|
33
|
+
throw new UsageError(`Workflow env ref ${JSON.stringify(inputRef)} did not resolve to a canonical fully-qualified owner.`, "INVALID_FLAG_VALUE");
|
|
34
|
+
}
|
|
35
|
+
if (logical.has(resolved.ref)) {
|
|
36
|
+
throw new UsageError(`Workflow environment contains duplicate ref ${resolved.ref}.`, "INVALID_FLAG_VALUE");
|
|
37
|
+
}
|
|
38
|
+
logical.add(resolved.ref);
|
|
39
|
+
trackParentDirectories(collector, resolved.root, resolved.path);
|
|
40
|
+
const retained = collector.capture(resolved.path, resolved.root, { authored: true });
|
|
41
|
+
const captured = collector.bindIdentity(resolved.path, resolved.root, {
|
|
42
|
+
ref: resolved.ref,
|
|
43
|
+
bundle: resolved.bundle,
|
|
44
|
+
adapter: resolved.adapter,
|
|
45
|
+
file: retained.relativePath,
|
|
46
|
+
hash: retained.sha256,
|
|
47
|
+
});
|
|
48
|
+
const physicalKey = `${captured.containmentPhysicalIdentity}\0${captured.physicalIdentity}`;
|
|
49
|
+
const alias = physical.get(physicalKey);
|
|
50
|
+
if (alias !== undefined && alias !== resolved.ref) {
|
|
51
|
+
throw new UsageError(`${resolved.ref} aliases the same physical environment source as ${alias} under a different logical owner.`, "RESOURCE_ALREADY_EXISTS");
|
|
52
|
+
}
|
|
53
|
+
physical.set(physicalKey, resolved.ref);
|
|
54
|
+
const parsed = dotenv.parse(Buffer.from(captured.bytesBase64, "base64"));
|
|
55
|
+
const keys = sortedCodePoints(Object.keys(parsed));
|
|
56
|
+
const secretNames = secretTokenNames(parsed);
|
|
57
|
+
out.push(Object.freeze({
|
|
58
|
+
kind: "env-ref",
|
|
59
|
+
ref: resolved.ref,
|
|
60
|
+
owner: Object.freeze({
|
|
61
|
+
bundle: resolved.bundle,
|
|
62
|
+
adapter: resolved.adapter,
|
|
63
|
+
requestedRoot: captured.containmentRoot,
|
|
64
|
+
realRoot: captured.containmentRealPath,
|
|
65
|
+
rootPhysicalIdentity: captured.containmentPhysicalIdentity,
|
|
66
|
+
requestedPath: captured.sourcePath,
|
|
67
|
+
realPath: captured.realPath,
|
|
68
|
+
relativePath: captured.relativePath,
|
|
69
|
+
}),
|
|
70
|
+
keys: Object.freeze(keys),
|
|
71
|
+
secretNames: Object.freeze(secretNames),
|
|
72
|
+
precedence,
|
|
73
|
+
}));
|
|
74
|
+
}
|
|
75
|
+
return out;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Materialize all current values atomically from frozen descriptors. Nothing
|
|
79
|
+
* is returned until every owner/key/token check and every secret lookup has
|
|
80
|
+
* succeeded.
|
|
81
|
+
*/
|
|
82
|
+
export function materializeFrozenWorkflowEnvironment(descriptors, options = {}) {
|
|
83
|
+
const values = {};
|
|
84
|
+
const sensitive = new Set();
|
|
85
|
+
const audits = [];
|
|
86
|
+
const seenRefs = new Set();
|
|
87
|
+
let priorPrecedence = -1;
|
|
88
|
+
for (const descriptor of descriptors) {
|
|
89
|
+
if (!descriptor || typeof descriptor !== "object")
|
|
90
|
+
invalid("environment descriptor must be an object");
|
|
91
|
+
if (descriptor.kind === "literal") {
|
|
92
|
+
exactKeys(descriptor, ["kind", "name", "value"]);
|
|
93
|
+
environmentName(descriptor.name);
|
|
94
|
+
if (typeof descriptor.value !== "string")
|
|
95
|
+
invalid("literal environment value must be a string");
|
|
96
|
+
values[descriptor.name] = descriptor.value;
|
|
97
|
+
if (descriptor.value)
|
|
98
|
+
sensitive.add(descriptor.value);
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
if (descriptor.kind === "pass-through") {
|
|
102
|
+
exactKeys(descriptor, ["kind", "name"]);
|
|
103
|
+
environmentName(descriptor.name);
|
|
104
|
+
const value = options.readPassThrough ? options.readPassThrough(descriptor.name) : process.env[descriptor.name];
|
|
105
|
+
if (value !== undefined) {
|
|
106
|
+
values[descriptor.name] = value;
|
|
107
|
+
if (value)
|
|
108
|
+
sensitive.add(value);
|
|
109
|
+
}
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
if (descriptor.kind !== "env-ref")
|
|
113
|
+
invalid("environment descriptor has an unsupported kind");
|
|
114
|
+
exactKeys(descriptor, ["kind", "ref", "owner", "keys", "secretNames", "precedence"]);
|
|
115
|
+
validateEnvRefDescriptor(descriptor);
|
|
116
|
+
if (seenRefs.has(descriptor.ref))
|
|
117
|
+
invalid(`duplicate environment ref ${descriptor.ref}`);
|
|
118
|
+
if (descriptor.precedence <= priorPrecedence)
|
|
119
|
+
invalid("env-ref precedence must be strictly increasing");
|
|
120
|
+
seenRefs.add(descriptor.ref);
|
|
121
|
+
priorPrecedence = descriptor.precedence;
|
|
122
|
+
const source = options.readEnvFile ? options.readEnvFile(descriptor) : readFrozenEnvironmentFile(descriptor.owner);
|
|
123
|
+
const parsed = dotenv.parse(typeof source === "string" ? source : Buffer.from(source));
|
|
124
|
+
const keys = sortedCodePoints(Object.keys(parsed));
|
|
125
|
+
const tokenNames = secretTokenNames(parsed);
|
|
126
|
+
if (!sameStrings(keys, descriptor.keys)) {
|
|
127
|
+
invalid(`environment ${descriptor.ref} key set changed after it was frozen`);
|
|
128
|
+
}
|
|
129
|
+
if (!sameStrings(tokenNames, descriptor.secretNames)) {
|
|
130
|
+
invalid(`environment ${descriptor.ref} secret-token topology changed after it was frozen`);
|
|
131
|
+
}
|
|
132
|
+
const secretValues = new Map();
|
|
133
|
+
for (const name of descriptor.secretNames) {
|
|
134
|
+
const raw = options.readSecret
|
|
135
|
+
? options.readSecret({ name, descriptor })
|
|
136
|
+
: readFrozenSecret(descriptor.owner, name);
|
|
137
|
+
if (raw === undefined) {
|
|
138
|
+
throw new NotFoundError(`Environment ${descriptor.ref} references missing secret ${name}; nothing was materialized.`, "FILE_NOT_FOUND");
|
|
139
|
+
}
|
|
140
|
+
const value = typeof raw === "string" ? raw : Buffer.from(raw).toString("utf8");
|
|
141
|
+
secretValues.set(name, value);
|
|
142
|
+
if (value)
|
|
143
|
+
sensitive.add(value);
|
|
144
|
+
}
|
|
145
|
+
for (const [key, rawValue] of Object.entries(parsed)) {
|
|
146
|
+
const resolved = rawValue.replace(SECRET_TOKEN_RE, (_token, name) => {
|
|
147
|
+
const secret = secretValues.get(name);
|
|
148
|
+
if (secret === undefined)
|
|
149
|
+
invalid(`environment ${descriptor.ref} secret ${name} was not materialized`);
|
|
150
|
+
return secret;
|
|
151
|
+
});
|
|
152
|
+
values[key] = resolved;
|
|
153
|
+
if (resolved)
|
|
154
|
+
sensitive.add(resolved);
|
|
155
|
+
}
|
|
156
|
+
audits.push(Object.freeze({
|
|
157
|
+
eventType: "env_access",
|
|
158
|
+
ref: descriptor.ref,
|
|
159
|
+
keys: Object.freeze([...descriptor.keys]),
|
|
160
|
+
secretNames: Object.freeze([...descriptor.secretNames]),
|
|
161
|
+
}));
|
|
162
|
+
}
|
|
163
|
+
return {
|
|
164
|
+
values,
|
|
165
|
+
sensitiveValues: [...sensitive],
|
|
166
|
+
audits,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
function validateEnvRefDescriptor(descriptor) {
|
|
170
|
+
const match = QUALIFIED_ENV_REF_RE.exec(descriptor.ref);
|
|
171
|
+
if (!match || match[1] !== descriptor.owner.bundle)
|
|
172
|
+
invalid("env-ref has a noncanonical qualified owner");
|
|
173
|
+
exactKeys(descriptor.owner, [
|
|
174
|
+
"bundle",
|
|
175
|
+
"adapter",
|
|
176
|
+
"requestedRoot",
|
|
177
|
+
"realRoot",
|
|
178
|
+
"rootPhysicalIdentity",
|
|
179
|
+
"requestedPath",
|
|
180
|
+
"realPath",
|
|
181
|
+
"relativePath",
|
|
182
|
+
]);
|
|
183
|
+
for (const value of [descriptor.owner.bundle, descriptor.owner.adapter, descriptor.owner.rootPhysicalIdentity]) {
|
|
184
|
+
if (typeof value !== "string" || !value)
|
|
185
|
+
invalid("env-ref owner identity is incomplete");
|
|
186
|
+
}
|
|
187
|
+
for (const value of [
|
|
188
|
+
descriptor.owner.requestedRoot,
|
|
189
|
+
descriptor.owner.realRoot,
|
|
190
|
+
descriptor.owner.requestedPath,
|
|
191
|
+
descriptor.owner.realPath,
|
|
192
|
+
]) {
|
|
193
|
+
if (typeof value !== "string" || !path.isAbsolute(value))
|
|
194
|
+
invalid("env-ref owner paths must be absolute");
|
|
195
|
+
}
|
|
196
|
+
const relative = descriptor.owner.relativePath;
|
|
197
|
+
if (typeof relative !== "string" ||
|
|
198
|
+
!relative ||
|
|
199
|
+
relative.includes("\\") ||
|
|
200
|
+
relative.startsWith("../") ||
|
|
201
|
+
path.isAbsolute(relative)) {
|
|
202
|
+
invalid("env-ref owner relative path is noncanonical");
|
|
203
|
+
}
|
|
204
|
+
if (path.resolve(descriptor.owner.requestedRoot, relative) !== path.resolve(descriptor.owner.requestedPath)) {
|
|
205
|
+
invalid("env-ref requested path does not match its owner");
|
|
206
|
+
}
|
|
207
|
+
if (path.resolve(descriptor.owner.realRoot, relative) !== path.resolve(descriptor.owner.realPath)) {
|
|
208
|
+
invalid("env-ref real path does not match its owner");
|
|
209
|
+
}
|
|
210
|
+
sortedUnique(descriptor.keys, "env-ref keys", ENVIRONMENT_NAME_RE);
|
|
211
|
+
sortedUnique(descriptor.secretNames, "env-ref secret names", /^[A-Za-z0-9_./-]+$/);
|
|
212
|
+
if (!Number.isSafeInteger(descriptor.precedence) || descriptor.precedence < 0) {
|
|
213
|
+
invalid("env-ref precedence is invalid");
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
function readFrozenEnvironmentFile(owner) {
|
|
217
|
+
const rootLstat = fs.lstatSync(owner.requestedRoot, { bigint: true });
|
|
218
|
+
if (rootLstat.isSymbolicLink() || !rootLstat.isDirectory())
|
|
219
|
+
invalid("environment owner root physical identity changed");
|
|
220
|
+
const currentRealRoot = fs.realpathSync(owner.requestedRoot);
|
|
221
|
+
if (currentRealRoot !== owner.realRoot ||
|
|
222
|
+
physicalIdentity(currentRealRoot, rootLstat) !== owner.rootPhysicalIdentity) {
|
|
223
|
+
invalid("environment owner root physical identity changed");
|
|
224
|
+
}
|
|
225
|
+
const captured = captureGuardedExecutionSource(owner.requestedPath, owner.requestedRoot, { authored: true });
|
|
226
|
+
if (captured.realPath !== owner.realPath ||
|
|
227
|
+
captured.relativePath !== owner.relativePath ||
|
|
228
|
+
captured.containmentRealPath !== owner.realRoot ||
|
|
229
|
+
captured.containmentPhysicalIdentity !== owner.rootPhysicalIdentity) {
|
|
230
|
+
invalid("environment owner/path physical identity changed");
|
|
231
|
+
}
|
|
232
|
+
return Buffer.from(captured.bytesBase64, "base64");
|
|
233
|
+
}
|
|
234
|
+
function readFrozenSecret(owner, name) {
|
|
235
|
+
const secretsRoot = path.join(owner.requestedRoot, "secrets");
|
|
236
|
+
const secretPath = assetPathForName("secret", secretsRoot, name);
|
|
237
|
+
if (!isWithin(secretPath, secretsRoot))
|
|
238
|
+
invalid(`secret name ${name} escapes its frozen owner`);
|
|
239
|
+
let descriptor;
|
|
240
|
+
try {
|
|
241
|
+
const noFollow = typeof fs.constants.O_NOFOLLOW === "number" ? fs.constants.O_NOFOLLOW : 0;
|
|
242
|
+
descriptor = fs.openSync(secretPath, fs.constants.O_RDONLY | noFollow);
|
|
243
|
+
const before = fs.fstatSync(descriptor, { bigint: true });
|
|
244
|
+
if (!before.isFile())
|
|
245
|
+
invalid(`secret ${name} is not a regular file`);
|
|
246
|
+
const bytes = fs.readFileSync(descriptor);
|
|
247
|
+
const after = fs.fstatSync(descriptor, { bigint: true });
|
|
248
|
+
if (before.dev !== after.dev ||
|
|
249
|
+
before.ino !== after.ino ||
|
|
250
|
+
before.size !== after.size ||
|
|
251
|
+
before.mtimeNs !== after.mtimeNs) {
|
|
252
|
+
invalid(`secret ${name} changed while it was read`);
|
|
253
|
+
}
|
|
254
|
+
return bytes;
|
|
255
|
+
}
|
|
256
|
+
catch (cause) {
|
|
257
|
+
if (cause.code === "ENOENT")
|
|
258
|
+
return undefined;
|
|
259
|
+
throw cause;
|
|
260
|
+
}
|
|
261
|
+
finally {
|
|
262
|
+
if (descriptor !== undefined)
|
|
263
|
+
fs.closeSync(descriptor);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
function trackParentDirectories(collector, rootInput, fileInput) {
|
|
267
|
+
const root = path.resolve(rootInput);
|
|
268
|
+
const parent = path.dirname(path.resolve(fileInput));
|
|
269
|
+
const relative = path.relative(root, parent);
|
|
270
|
+
if (relative.startsWith("..") || path.isAbsolute(relative))
|
|
271
|
+
invalid("environment source escapes its owner root");
|
|
272
|
+
let current = root;
|
|
273
|
+
collector.trackDirectory(current, root);
|
|
274
|
+
if (relative === "")
|
|
275
|
+
return;
|
|
276
|
+
for (const segment of relative.split(path.sep)) {
|
|
277
|
+
current = path.join(current, segment);
|
|
278
|
+
collector.trackDirectory(current, root);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
function secretTokenNames(values) {
|
|
282
|
+
const names = new Set();
|
|
283
|
+
for (const value of Object.values(values)) {
|
|
284
|
+
for (const match of value.matchAll(SECRET_TOKEN_RE)) {
|
|
285
|
+
const name = match[1];
|
|
286
|
+
if (name)
|
|
287
|
+
names.add(name);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
return sortedCodePoints([...names]);
|
|
291
|
+
}
|
|
292
|
+
function sortedCodePoints(values) {
|
|
293
|
+
return values.sort((left, right) => (left < right ? -1 : left > right ? 1 : 0));
|
|
294
|
+
}
|
|
295
|
+
function sortedUnique(values, label, pattern) {
|
|
296
|
+
if (!Array.isArray(values))
|
|
297
|
+
invalid(`${label} must be an array`);
|
|
298
|
+
let prior;
|
|
299
|
+
for (const value of values) {
|
|
300
|
+
if (typeof value !== "string" || !pattern.test(value) || value.includes(".."))
|
|
301
|
+
invalid(`${label} is invalid`);
|
|
302
|
+
if (prior !== undefined && prior >= value)
|
|
303
|
+
invalid(`${label} must be sorted and unique`);
|
|
304
|
+
prior = value;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
function sameStrings(left, right) {
|
|
308
|
+
return left.length === right.length && left.every((value, index) => value === right[index]);
|
|
309
|
+
}
|
|
310
|
+
function exactKeys(value, keys) {
|
|
311
|
+
const allowed = new Set(keys);
|
|
312
|
+
for (const key of Object.keys(value))
|
|
313
|
+
if (!allowed.has(key))
|
|
314
|
+
invalid(`environment descriptor contains unknown key ${key}`);
|
|
315
|
+
for (const key of keys)
|
|
316
|
+
if (!Object.hasOwn(value, key))
|
|
317
|
+
invalid(`environment descriptor is missing ${key}`);
|
|
318
|
+
}
|
|
319
|
+
function environmentName(value) {
|
|
320
|
+
if (typeof value !== "string" || !ENVIRONMENT_NAME_RE.test(value))
|
|
321
|
+
invalid("environment name is invalid");
|
|
322
|
+
}
|
|
323
|
+
function physicalIdentity(realPath, stat) {
|
|
324
|
+
return stat.ino === 0n ? `path:${realPath}` : `inode:${stat.dev}:${stat.ino}`;
|
|
325
|
+
}
|
|
326
|
+
function invalid(message) {
|
|
327
|
+
throw new UsageError(`Invalid frozen workflow environment: ${message}.`, "INVALID_FLAG_VALUE");
|
|
328
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
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
|
+
import { createHash } from "node:crypto";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { parseBundleRef } from "../../core/asset/asset-ref.js";
|
|
7
|
+
import { UsageError } from "../../core/errors.js";
|
|
8
|
+
import { GuardedExecutionSourceCollector } from "../../execution/guarded-source.js";
|
|
9
|
+
import { defaultMapConcurrency, workflowMaxConcurrency } from "../concurrency-policy.js";
|
|
10
|
+
import { compileWorkflowPlan } from "./compile.js";
|
|
11
|
+
import { canonicalJson } from "./plan-hash.js";
|
|
12
|
+
import { decodeWorkflowPlanV4, WORKFLOW_IR_V4_VERSION, } from "./schema-v4.js";
|
|
13
|
+
import { resolveWorkflowSourceV4 } from "./source-freeze-v4.js";
|
|
14
|
+
/** Compile, resolve, and freeze one executable plan without publishing any state. */
|
|
15
|
+
export async function compileResolveFreezeWorkflowV4(asset, config, options = {}) {
|
|
16
|
+
const sourceCollector = options.sourceCollector ?? new GuardedExecutionSourceCollector();
|
|
17
|
+
const workflowSource = captureWorkflowSource(asset, sourceCollector);
|
|
18
|
+
const resolved = await resolveWorkflowSourceV4(asset, workflowSource, config, sourceCollector);
|
|
19
|
+
const compiled = compileWorkflowPlan(resolved.sourceIr, asset.title, resolved.units);
|
|
20
|
+
if (!compiled.ok) {
|
|
21
|
+
throw new UsageError(compiled.errors.map((error) => `${asset.path}:${error.line}: ${error.message}`).join("\n"), "INVALID_FLAG_VALUE");
|
|
22
|
+
}
|
|
23
|
+
const steps = compiled.plan.steps.map((step) => {
|
|
24
|
+
const frozenJudge = step.gate.criteria.length === 0 ? null : resolved.judges.get(step.stepId);
|
|
25
|
+
if (step.gate.criteria.length > 0 && !frozenJudge) {
|
|
26
|
+
throw new Error(`resolved workflow judge missing for step ${step.stepId}`);
|
|
27
|
+
}
|
|
28
|
+
const gate = Object.freeze({ ...step.gate, maxLoops: step.gate.maxLoops ?? 1, frozenJudge: frozenJudge ?? null });
|
|
29
|
+
if (!step.root) {
|
|
30
|
+
const { root: _root, ...withoutRoot } = step;
|
|
31
|
+
return Object.freeze({ ...withoutRoot, gate });
|
|
32
|
+
}
|
|
33
|
+
const frozen = resolved.units.get(step.stepId);
|
|
34
|
+
if (!frozen)
|
|
35
|
+
throw new Error(`resolved workflow target missing for step ${step.stepId}`);
|
|
36
|
+
const root = step.root.kind === "map"
|
|
37
|
+
? {
|
|
38
|
+
...step.root,
|
|
39
|
+
concurrency: step.root.concurrency ?? defaultMapConcurrency(config.workflow?.defaultMapConcurrency),
|
|
40
|
+
template: freezeResolvedUnit(step.root.template, frozen),
|
|
41
|
+
}
|
|
42
|
+
: freezeResolvedUnit(step.root, frozen);
|
|
43
|
+
return Object.freeze({ ...step, root, gate });
|
|
44
|
+
});
|
|
45
|
+
const sourceReadSet = sourceCollector
|
|
46
|
+
.snapshot()
|
|
47
|
+
.sources.filter((source) => Boolean(source.identity))
|
|
48
|
+
.map((source) => sourceSnapshot(source));
|
|
49
|
+
const plan = decodeWorkflowPlanV4({
|
|
50
|
+
irVersion: WORKFLOW_IR_V4_VERSION,
|
|
51
|
+
title: compiled.plan.title,
|
|
52
|
+
...(compiled.plan.params ? { params: compiled.plan.params } : {}),
|
|
53
|
+
...(compiled.plan.paramSchemas ? { paramSchemas: compiled.plan.paramSchemas } : {}),
|
|
54
|
+
...(compiled.plan.budget ? { budget: compiled.plan.budget } : {}),
|
|
55
|
+
execution: { maxConcurrency: workflowMaxConcurrency(config.workflow?.maxConcurrency) },
|
|
56
|
+
sourceReadSet,
|
|
57
|
+
steps,
|
|
58
|
+
});
|
|
59
|
+
return Object.freeze({
|
|
60
|
+
plan,
|
|
61
|
+
warnings: compiled.warnings,
|
|
62
|
+
...(resolved.engineAnnouncement ? { engineAnnouncement: resolved.engineAnnouncement } : {}),
|
|
63
|
+
sourceCollector,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
function freezeResolvedUnit(unit, resolved) {
|
|
67
|
+
const frozenTarget = resolved.target.kind === "shell"
|
|
68
|
+
? Object.freeze({
|
|
69
|
+
...resolved.target,
|
|
70
|
+
contentHash: createHash("sha256")
|
|
71
|
+
.update("akm.workflow.shell.v1\0")
|
|
72
|
+
.update(canonicalJson({
|
|
73
|
+
exec: resolved.target.exec,
|
|
74
|
+
environment: resolved.environment,
|
|
75
|
+
cwdIdentity: resolved.target.cwdIdentity,
|
|
76
|
+
}))
|
|
77
|
+
.digest("hex"),
|
|
78
|
+
})
|
|
79
|
+
: resolved.target;
|
|
80
|
+
const { exec: _exec, ...common } = unit;
|
|
81
|
+
return Object.freeze({
|
|
82
|
+
...common,
|
|
83
|
+
isolation: common.isolation ?? "none",
|
|
84
|
+
frozenTarget,
|
|
85
|
+
environment: Object.freeze([...resolved.environment]),
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
function captureWorkflowSource(asset, collector) {
|
|
89
|
+
const root = path.resolve(asset.sourcePath);
|
|
90
|
+
const file = path.resolve(asset.path);
|
|
91
|
+
collector.trackDirectory(root, root);
|
|
92
|
+
const parent = path.dirname(file);
|
|
93
|
+
const relativeParent = path.relative(root, parent);
|
|
94
|
+
if (relativeParent.startsWith("..") || path.isAbsolute(relativeParent)) {
|
|
95
|
+
throw new UsageError(`${file} resolves outside its workflow source root.`, "PATH_ESCAPE_VIOLATION");
|
|
96
|
+
}
|
|
97
|
+
let current = root;
|
|
98
|
+
for (const segment of relativeParent === "" ? [] : relativeParent.split(path.sep)) {
|
|
99
|
+
current = path.join(current, segment);
|
|
100
|
+
collector.trackDirectory(current, root);
|
|
101
|
+
}
|
|
102
|
+
const captured = collector.capture(file, root, { authored: true });
|
|
103
|
+
const parsed = parseBundleRef(asset.ref);
|
|
104
|
+
if (!parsed.bundle || !asset.adapterId) {
|
|
105
|
+
throw new UsageError(`Workflow ${asset.ref} has no fully-qualified bundle/adapter owner for durable publication.`, "INVALID_FLAG_VALUE");
|
|
106
|
+
}
|
|
107
|
+
return collector.bindIdentity(file, root, {
|
|
108
|
+
ref: asset.ref,
|
|
109
|
+
bundle: parsed.bundle,
|
|
110
|
+
adapter: asset.adapterId,
|
|
111
|
+
file: captured.relativePath,
|
|
112
|
+
hash: captured.sha256,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
function sourceSnapshot(source) {
|
|
116
|
+
return Object.freeze({
|
|
117
|
+
identity: source.identity,
|
|
118
|
+
containmentPhysicalIdentity: source.containmentPhysicalIdentity,
|
|
119
|
+
physicalIdentity: source.physicalIdentity,
|
|
120
|
+
size: source.size,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
*/
|
|
15
15
|
import { createHash } from "node:crypto";
|
|
16
16
|
import { utf8Bytes, WORKFLOW_MAX_JSON_DEPTH, WORKFLOW_MAX_PLAN_BYTES } from "../resource-limits.js";
|
|
17
|
-
import {
|
|
17
|
+
import { decodeWorkflowPlanV4, WORKFLOW_IR_V4_VERSION } from "./schema-v4.js";
|
|
18
18
|
/** sha256 hex of the canonical (recursively sorted-keys) JSON of the plan. */
|
|
19
19
|
export function computePlanHash(plan) {
|
|
20
20
|
return createHash("sha256").update(canonicalPlanJson(plan)).digest("hex");
|
|
@@ -23,12 +23,12 @@ export function computePlanHash(plan) {
|
|
|
23
23
|
export function canonicalPlanJson(plan) {
|
|
24
24
|
return canonicalJson(plan);
|
|
25
25
|
}
|
|
26
|
-
/** Canonical JSON used by every
|
|
26
|
+
/** Canonical JSON used by every plan and input hash. */
|
|
27
27
|
export function canonicalJson(value) {
|
|
28
28
|
return JSON.stringify(sortKeys(value));
|
|
29
29
|
}
|
|
30
30
|
/** Decode, require stored canonical bytes, then verify the stored SHA-256. */
|
|
31
|
-
export function decodeCanonicalPlan(runId, planJson, planHash) {
|
|
31
|
+
export function decodeCanonicalPlan(runId, planJson, planHash, expectedVersion) {
|
|
32
32
|
if (utf8Bytes(planJson) > WORKFLOW_MAX_PLAN_BYTES)
|
|
33
33
|
throw new Error(`Workflow run ${runId} frozen plan exceeds the 2 MiB resource limit.`);
|
|
34
34
|
let parsed;
|
|
@@ -38,13 +38,19 @@ export function decodeCanonicalPlan(runId, planJson, planHash) {
|
|
|
38
38
|
catch {
|
|
39
39
|
throw new Error(`Workflow run ${runId} has corrupt frozen plan JSON.`);
|
|
40
40
|
}
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
if (planJson !== canonical)
|
|
41
|
+
const canonicalWire = canonicalJson(parsed);
|
|
42
|
+
if (planJson !== canonicalWire)
|
|
44
43
|
throw new Error(`Workflow run ${runId} has noncanonical frozen plan JSON.`);
|
|
45
|
-
const actual =
|
|
44
|
+
const actual = createHash("sha256").update(planJson).digest("hex");
|
|
46
45
|
if (!planHash || !/^[0-9a-f]{64}$/.test(planHash) || actual !== planHash)
|
|
47
46
|
throw new Error(`Workflow run ${runId} frozen plan integrity check failed.`);
|
|
47
|
+
if (expectedVersion !== undefined && expectedVersion !== null && expectedVersion !== WORKFLOW_IR_V4_VERSION) {
|
|
48
|
+
throw new Error(`Workflow run ${runId} uses unsupported workflow IR version ${expectedVersion}; this runtime supports only workflow IR version 4.`);
|
|
49
|
+
}
|
|
50
|
+
const plan = decodeWorkflowPlanV4(parsed);
|
|
51
|
+
const canonical = canonicalPlanJson(plan);
|
|
52
|
+
if (planJson !== canonical)
|
|
53
|
+
throw new Error(`Workflow run ${runId} has noncanonical frozen plan JSON.`);
|
|
48
54
|
return plan;
|
|
49
55
|
}
|
|
50
56
|
function sortKeys(value, depth = 0) {
|