akm-cli 0.9.17-alpha.2 → 0.9.17-alpha.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +756 -0
- package/dist/akm +94 -196
- package/dist/cli/shared.js +6 -2
- package/dist/cli.js +22 -9
- package/dist/commands/agent/agent-dispatch.js +1 -1
- package/dist/commands/command/command-execution.js +24 -62
- package/dist/commands/feedback-cli.js +0 -1
- package/dist/commands/health/accept-rate.js +2 -2
- package/dist/commands/health/checks.js +30 -75
- package/dist/commands/health/config-skew.js +38 -0
- package/dist/commands/health/egress.js +54 -0
- package/dist/commands/health/html-report.js +0 -38
- package/dist/commands/health/improve-metrics.js +123 -562
- package/dist/commands/health/plugin-staleness.js +53 -3
- package/dist/commands/health/renderers.js +12 -4
- package/dist/commands/health/report-view-model.js +11 -106
- package/dist/commands/health/types-improve.js +4 -19
- package/dist/commands/health/windows.js +64 -73
- package/dist/commands/health.js +122 -143
- package/dist/commands/improve/consolidate/chunking.js +25 -100
- package/dist/commands/improve/consolidate/sanitize.js +54 -149
- package/dist/commands/improve/consolidate.js +538 -1075
- package/dist/commands/improve/content-hash.js +16 -24
- package/dist/commands/improve/distill/content-repair.js +18 -100
- package/dist/commands/improve/distill-guards.js +20 -81
- package/dist/commands/improve/distill-promotion-policy.js +23 -243
- package/dist/commands/improve/distill.js +608 -1075
- package/dist/commands/improve/eligibility.js +126 -400
- package/dist/commands/improve/execution.js +3 -5
- package/dist/commands/improve/extract.js +487 -1046
- package/dist/commands/improve/feedback-valence.js +0 -25
- package/dist/commands/improve/improve-cli.js +29 -166
- package/dist/commands/improve/improve-result-file.js +10 -66
- package/dist/commands/improve/improve-strategies.js +12 -7
- package/dist/commands/improve/improve-usage-report.js +18 -64
- package/dist/commands/improve/improve.js +443 -1063
- package/dist/commands/improve/ledger.js +114 -0
- package/dist/commands/improve/locks.js +2 -8
- package/dist/commands/improve/loop-stages.js +459 -1172
- package/dist/commands/improve/memory/derived-ref.js +12 -77
- package/dist/commands/improve/memory/memory-belief.js +14 -118
- package/dist/commands/improve/memory/memory-improve.js +4 -3
- package/dist/commands/improve/outcome-loop.js +28 -156
- package/dist/commands/improve/planner.js +5 -10
- package/dist/commands/improve/preparation.js +851 -2339
- package/dist/commands/improve/proactive-maintenance.js +34 -101
- package/dist/commands/improve/reflect-noise.js +104 -280
- package/dist/commands/improve/reflect.js +621 -1367
- package/dist/commands/improve/salience.js +46 -232
- package/dist/commands/improve/session-asset.js +19 -100
- package/dist/commands/improve/stage.js +323 -0
- package/dist/commands/proposal/drain.js +251 -644
- package/dist/commands/proposal/proposal-cli.js +3 -18
- package/dist/commands/proposal/proposal-types.js +20 -41
- package/dist/commands/proposal/proposal.js +1 -2
- package/dist/commands/proposal/propose.js +134 -160
- package/dist/commands/proposal/repository.js +502 -1487
- package/dist/commands/proposal/validators/proposal-quality-validators.js +71 -174
- package/dist/commands/proposal/validators/proposal-validators.js +1 -1
- package/dist/commands/proposal/validators/proposals.js +13 -89
- package/dist/commands/read/curate.js +63 -413
- package/dist/commands/read/search-cli.js +16 -33
- package/dist/commands/read/search.js +17 -23
- package/dist/commands/read/show.js +2 -13
- package/dist/commands/sources/bundle-cli.js +25 -2
- package/dist/commands/sources/bundle-config-ops.js +7 -0
- package/dist/commands/sources/dangerous-env-audit.js +1 -2
- package/dist/commands/sources/info.js +2 -11
- package/dist/commands/sources/installed-stashes.js +197 -746
- package/dist/commands/sources/schema-repair.js +98 -129
- package/dist/commands/sources/source-add.js +62 -12
- package/dist/commands/sources/stash-cli.js +1 -1
- package/dist/commands/tasks/explain.js +10 -13
- package/dist/commands/tasks/tasks-cli.js +9 -8
- package/dist/commands/tasks/tasks.js +326 -930
- package/dist/commands/tasks/validate.js +42 -21
- package/dist/commands/workflow/plan.js +22 -29
- package/dist/commands/workflow-cli.js +4 -4
- package/dist/core/adapter/adapters/akm-adapter.js +0 -1
- package/dist/core/adapter/adapters/akm-lint.js +2 -3
- package/dist/core/adapter/adapters/akm-metadata.js +11 -12
- package/dist/core/adapter/adapters/akm-workflow-adapter.js +1 -1
- package/dist/core/adapter/execution-source.js +17 -29
- package/dist/core/asset/resolve-ref.js +1 -1
- package/dist/core/bundle-id.js +42 -5
- package/dist/core/bundle-rename.js +291 -0
- package/dist/core/config/config-io.js +1 -2
- package/dist/core/config/config-schema.js +1 -33
- package/dist/core/config/config-walker.js +1 -1
- package/dist/core/config/config.js +163 -68
- package/dist/core/config/legacy-source-shape-shim.js +38 -9
- package/dist/core/config/schema/embedding.js +20 -5
- package/dist/core/config/schema/engines.js +5 -0
- package/dist/core/config/schema/execution.js +1 -1
- package/dist/core/config/schema/experimental.js +1 -1
- package/dist/core/config/schema/improve-processes.js +21 -95
- package/dist/core/config/schema/improve.js +4 -42
- package/dist/core/config/schema/scheduler.js +12 -12
- package/dist/core/config/schema/search.js +6 -22
- package/dist/core/env-secret-ref.js +0 -1
- package/dist/core/errors.js +8 -9
- package/dist/core/file-lock.js +76 -173
- package/dist/core/logs-db.js +2 -2
- package/dist/core/paths.js +0 -27
- package/dist/core/redaction.js +109 -2
- package/dist/core/run-lock.js +2 -5
- package/dist/core/spawn-env.js +1 -1
- package/dist/core/state/migrations.js +108 -61
- package/dist/core/state-db-scope.js +2 -4
- package/dist/core/state-db.js +126 -692
- package/dist/core/type-presentation.js +1 -9
- package/dist/core/write-source.js +293 -1012
- package/dist/execution/input-contract.js +1 -1
- package/dist/execution/resolved-request.js +135 -689
- package/dist/execution/source.js +63 -257
- package/dist/execution/target-ref.js +1 -1
- package/dist/indexer/bundle-identity-guard.js +2 -2
- package/dist/indexer/db/graph-db.js +106 -46
- package/dist/indexer/ensure-index.js +44 -85
- package/dist/indexer/graph/graph-extraction.js +340 -562
- package/dist/indexer/graph/graph-related.js +130 -0
- package/dist/indexer/index-rebuild-lock.js +3 -11
- package/dist/indexer/index-writer-lock.js +8 -17
- package/dist/indexer/index-written-assets.js +139 -151
- package/dist/indexer/indexer.js +524 -846
- package/dist/indexer/materialize-embeddings.js +60 -397
- package/dist/indexer/passes/memory-inference.js +81 -90
- package/dist/indexer/passes/metadata.js +132 -200
- package/dist/indexer/read-preflight.js +0 -7
- package/dist/indexer/scan/doc-to-entry.js +1 -3
- package/dist/indexer/scan/drain-dir.js +1 -1
- package/dist/indexer/search/db-search.js +181 -590
- package/dist/indexer/search/fts-query.js +30 -41
- package/dist/indexer/search/ranking.js +28 -154
- package/dist/indexer/search/search-attribution.js +12 -32
- package/dist/indexer/search/search-fields.js +11 -15
- package/dist/indexer/search/search-hit-enrichers.js +54 -85
- package/dist/indexer/search/search-source.js +1 -4
- package/dist/indexer/usage/usage-events.js +2 -7
- package/dist/integrations/agent/engine-fallback.js +23 -40
- package/dist/integrations/agent/engine-resolution.js +93 -183
- package/dist/integrations/agent/execution.js +507 -0
- package/dist/integrations/agent/model-map.js +28 -156
- package/dist/integrations/agent/request-lowering.js +66 -141
- package/dist/integrations/agent/runner-dispatch.js +143 -321
- package/dist/integrations/agent/runner.js +54 -14
- package/dist/integrations/lockfile.js +53 -101
- package/dist/llm/embedders/deterministic.js +2 -3
- package/dist/llm/embedders/profile.js +71 -0
- package/dist/llm/embedders/remote.js +10 -15
- package/dist/llm/graph-extract.js +3 -12
- package/dist/llm/index-passes.js +3 -5
- package/dist/llm/memory-infer.js +1 -2
- package/dist/llm/metadata-enhance.js +1 -2
- package/dist/llm/structured-call.js +5 -24
- package/dist/output/generic-render.js +23 -11
- package/dist/output/html-render.js +13 -10
- package/dist/output/render-registry.js +3 -32
- package/dist/output/shapes/helpers.js +2 -34
- package/dist/output/shapes/passthrough.js +1 -9
- package/dist/{indexer/search/ranking-types.js → output/text/bundle-rename.js} +4 -1
- package/dist/output/text/command-format.js +60 -23
- package/dist/output/text/helpers.js +1 -1
- package/dist/output/text/migrate.js +5 -14
- package/dist/output/text/proposal-format.js +1 -2
- package/dist/output/text/workflow-format.js +0 -32
- package/dist/output/text.js +2 -0
- package/dist/registry/factory.js +4 -19
- package/dist/registry/network.js +66 -220
- package/dist/registry/providers/index.js +0 -2
- package/dist/registry/providers/skills-sh.js +3 -14
- package/dist/registry/providers/static-index.js +24 -26
- package/dist/registry/resolve.js +55 -131
- package/dist/scripts/akm-migrate-node.js +43937 -93313
- package/dist/scripts/akm-migrate.js +43697 -93071
- package/dist/setup/registry-stash-loader.js +4 -13
- package/dist/setup/semantic-assets.js +3 -44
- package/dist/setup/setup.js +1 -1
- package/dist/setup/steps/tasks.js +25 -15
- package/dist/sources/provider-factory.js +17 -18
- package/dist/sources/providers/filesystem.js +2 -3
- package/dist/sources/providers/git-install.js +7 -1
- package/dist/sources/providers/git-provider.js +0 -3
- package/dist/sources/providers/git-stash.js +0 -17
- package/dist/sources/providers/npm.js +2 -4
- package/dist/sources/providers/provider-utils.js +5 -10
- package/dist/sources/providers/website.js +0 -2
- package/dist/sources/snapshot-fetchers/website-ingest.js +1 -1
- package/dist/sources/website-url.js +2 -2
- package/dist/storage/database.js +9 -35
- package/dist/storage/repositories/improve-ledger-repository.js +168 -0
- package/dist/storage/repositories/index-connection.js +34 -70
- package/dist/storage/repositories/index-entries-repository.js +69 -111
- package/dist/storage/repositories/index-entry-mapper.js +1 -2
- package/dist/storage/repositories/index-entry-schema.js +83 -269
- package/dist/storage/repositories/index-fts-repository.js +86 -256
- package/dist/storage/repositories/index-llm-cache-repository.js +17 -0
- package/dist/storage/repositories/index-meta-repository.js +6 -4
- package/dist/storage/repositories/index-schema.js +192 -220
- package/dist/storage/repositories/index-utility-repository.js +8 -29
- package/dist/storage/repositories/index-vec-repository.js +133 -414
- package/dist/storage/repositories/outcome-repository.js +2 -1
- package/dist/storage/repositories/proposals-repository.js +35 -0
- package/dist/storage/repositories/registry-index-cache-repository.js +100 -0
- package/dist/storage/repositories/task-history-repository.js +26 -4
- package/dist/storage/repositories/workflow-runs-repository.js +53 -244
- package/dist/storage/sqlite-migrations.js +136 -0
- package/dist/storage/sqlite-pragmas.js +11 -9
- package/dist/storage/sqlite-transaction.js +170 -0
- package/dist/storage/state-db-integrity.js +34 -27
- package/dist/tasks/activation-config.js +134 -62
- package/dist/tasks/backends/cron.js +129 -277
- package/dist/tasks/backends/exec-utils.js +2 -5
- package/dist/tasks/backends/launchd.js +125 -745
- package/dist/tasks/backends/schtasks.js +101 -620
- package/dist/tasks/prepare/prepare-support.js +5 -15
- package/dist/tasks/prepare/prepare.js +0 -2
- package/dist/tasks/resolve-akm-bin.js +20 -79
- package/dist/tasks/run/attempt-lifecycle.js +0 -1
- package/dist/tasks/scheduler-binding.js +18 -238
- package/dist/tasks/scheduler-invocation.js +52 -52
- package/dist/tasks/scheduler-lock.js +53 -0
- package/dist/tasks/scheduler-sync.js +363 -679
- package/dist/tasks/source/parse-task-source.js +160 -10
- package/dist/tasks/source/task-source-v3-frozen.js +3 -4
- package/dist/tasks/source/task-to-v4.js +2 -2
- package/dist/workflows/authoring/authoring.js +3 -12
- package/dist/workflows/compile.js +211 -0
- package/dist/workflows/concurrency-policy.js +13 -74
- package/dist/workflows/exec/child-invocation.js +3 -17
- package/dist/workflows/exec/child-workflow.js +32 -141
- package/dist/workflows/exec/dispatch-redaction.js +13 -53
- package/dist/workflows/exec/environment.js +98 -0
- package/dist/workflows/exec/exec-unit.js +33 -140
- package/dist/workflows/exec/frozen-judge.js +7 -59
- package/dist/workflows/exec/native-executor.js +82 -341
- package/dist/workflows/exec/param-secrets.js +29 -47
- package/dist/workflows/exec/run-workflow.js +154 -387
- package/dist/workflows/exec/scheduler.js +9 -36
- package/dist/workflows/exec/step-work.js +127 -430
- package/dist/workflows/exec/unit-dispatch.js +11 -63
- package/dist/workflows/exec/unit-writer.js +8 -52
- package/dist/workflows/exec/worktree.js +39 -273
- package/dist/workflows/freeze/child-output-references.js +4 -15
- package/dist/workflows/freeze/environment.js +99 -92
- package/dist/workflows/freeze/freeze.js +172 -0
- package/dist/workflows/freeze/step-values.js +19 -21
- package/dist/workflows/freeze/targets/child-workflow.js +23 -92
- package/dist/workflows/freeze/targets/command.js +10 -33
- package/dist/workflows/freeze/targets/script.js +5 -12
- package/dist/workflows/freeze/targets/shell.js +3 -6
- package/dist/workflows/freeze/targets/task.js +25 -80
- package/dist/workflows/freeze/task-bindings.js +20 -67
- package/dist/workflows/{source-ir/github-yaml.js → github-yaml.js} +88 -206
- package/dist/workflows/ir/params.js +6 -51
- package/dist/workflows/ir/plan-hash.js +2 -34
- package/dist/workflows/parser.js +140 -43
- package/dist/{commands/improve/consolidate/types.js → workflows/plan.js} +2 -1
- package/dist/workflows/renderer.js +36 -69
- package/dist/workflows/resource-limits.js +12 -120
- package/dist/workflows/runtime/agent-identity.js +8 -40
- package/dist/workflows/runtime/run-outputs.js +3 -6
- package/dist/workflows/runtime/run-plan.js +316 -0
- package/dist/workflows/runtime/runs.js +48 -200
- package/dist/workflows/runtime/workflow-asset-loader.js +24 -57
- package/dist/workflows/{source-ir/semantics.js → source-semantics.js} +16 -20
- package/dist/workflows/validate-summary.js +2 -7
- package/docs/integration/bundling-akm.md +49 -42
- package/docs/migration/README.md +1 -0
- package/docs/migration/release-notes/0.9.17.md +41 -0
- package/docs/migration/v0.9.1-to-v0.9.2.md +19 -7
- package/docs/reference/cli.md +182 -125
- package/docs/reference/configuration.md +49 -56
- package/docs/reference/data-and-telemetry.md +19 -20
- package/docs/reference/tasks.md +86 -38
- package/docs/reference/workflow-schema.md +14 -18
- package/docs/reference/workflows.md +6 -9
- package/package.json +1 -1
- package/schemas/akm-config.json +87 -406
- package/dist/commands/health/advisories.js +0 -150
- package/dist/commands/health/metrics.js +0 -329
- package/dist/commands/health/surfaces.js +0 -102
- package/dist/commands/improve/anti-collapse.js +0 -83
- package/dist/commands/improve/collapse-detector.js +0 -432
- package/dist/commands/improve/consolidate/eligibility.js +0 -48
- package/dist/commands/improve/consolidate/merge.js +0 -146
- package/dist/commands/improve/distill/promote-memory.js +0 -329
- package/dist/commands/improve/distill/quality-gate.js +0 -500
- package/dist/commands/improve/memory/memory-contradiction-detect.js +0 -291
- package/dist/commands/improve/proposal-envelope.js +0 -31
- package/dist/commands/improve/run-context.js +0 -123
- package/dist/commands/improve/shared.js +0 -21
- package/dist/commands/improve/source-identity.js +0 -28
- package/dist/commands/improve/triage.js +0 -96
- package/dist/commands/proposal/drain-policies.js +0 -151
- package/dist/commands/sources/update-transaction.js +0 -220
- package/dist/core/action-contributors.js +0 -28
- package/dist/core/config/config-version-shim.js +0 -101
- package/dist/core/config/retired-experimental-keys-shim.js +0 -62
- package/dist/core/fs-txn.js +0 -405
- package/dist/core/lexical-score.js +0 -25
- package/dist/core/maintenance-barrier.js +0 -167
- package/dist/execution/executable-identity.js +0 -105
- package/dist/execution/guarded-source.js +0 -427
- package/dist/indexer/graph/graph-boost.js +0 -427
- package/dist/indexer/graph/graph-dedup.js +0 -95
- package/dist/indexer/search/name-match.js +0 -35
- package/dist/indexer/search/ranking-contributors.js +0 -515
- package/dist/indexer/walk/project-context.js +0 -192
- package/dist/integrations/agent/execution-cascade.js +0 -566
- package/dist/integrations/agent/execution-definitions.js +0 -202
- package/dist/integrations/agent/execution-lowering.js +0 -841
- package/dist/integrations/agent/execution-preparation.js +0 -98
- package/dist/integrations/agent/inline-execution.js +0 -74
- package/dist/registry/create-provider-registry.js +0 -29
- package/dist/registry/pinned-request-helper.js +0 -247
- package/dist/registry/pinned-transport.js +0 -717
- package/dist/sources/providers/index.js +0 -14
- package/dist/storage/engines/sqlite-migrations.js +0 -271
- package/dist/storage/repositories/canaries-repository.js +0 -107
- package/dist/storage/repositories/embedding-salvage-repository.js +0 -184
- package/dist/storage/repositories/registry-cache.js +0 -113
- package/dist/tasks/scheduler-sync-preview.js +0 -52
- package/dist/workflows/freeze/resolve-steps.js +0 -86
- package/dist/workflows/freeze/source-freeze.js +0 -64
- package/dist/workflows/ir/compile.js +0 -321
- package/dist/workflows/ir/environment-v4.js +0 -330
- package/dist/workflows/ir/freeze-v4.js +0 -153
- package/dist/workflows/ir/schema-v4.js +0 -745
- package/dist/workflows/ir/schema.js +0 -354
- package/dist/workflows/program/schema.js +0 -78
- package/dist/workflows/runtime/checkin.js +0 -57
- package/dist/workflows/runtime/plan-classifier.js +0 -196
- package/dist/workflows/runtime/unit-checkin.js +0 -45
- package/dist/workflows/runtime/unit-phases.js +0 -20
- package/dist/workflows/schema.js +0 -4
- package/dist/workflows/source-ir/compile.js +0 -200
- package/dist/workflows/source-ir/program.js +0 -50
- package/dist/workflows/source-ir/result.js +0 -26
- package/dist/workflows/source-ir/schema.js +0 -786
- package/dist/workflows/source-ir/triggers.js +0 -79
- package/dist/workflows/source-ir/uses.js +0 -40
- package/dist/workflows/validator.js +0 -60
|
@@ -1,745 +0,0 @@
|
|
|
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
|
-
* Additive durable workflow plan v4.
|
|
6
|
-
*
|
|
7
|
-
* This is the sole executable workflow plan. Older stored plans are rejected
|
|
8
|
-
* at the run-storage boundary instead of being replayed through a second
|
|
9
|
-
* runtime architecture.
|
|
10
|
-
*/
|
|
11
|
-
import { createHash } from "node:crypto";
|
|
12
|
-
import path from "node:path";
|
|
13
|
-
import { compareCodePoints } from "../../core/common.js";
|
|
14
|
-
import { UsageError } from "../../core/errors.js";
|
|
15
|
-
import { decodeFrozenExecutableIdentity } from "../../execution/executable-identity.js";
|
|
16
|
-
import { INPUT_NAME_PATTERN } from "../../execution/input-contract.js";
|
|
17
|
-
import { canonicalResolvedExecutionRequest, decodeResolvedExecutionRequest, } from "../../execution/resolved-request.js";
|
|
18
|
-
import { decodeExecutionSourceIdentity } from "../../execution/source.js";
|
|
19
|
-
import { decodeFrozenRunnerSpec } from "../../integrations/agent/execution-lowering.js";
|
|
20
|
-
import { parseReference } from "../program/expressions.js";
|
|
21
|
-
import { PROGRAM_PARAM_NAME_PATTERN } from "../program/schema.js";
|
|
22
|
-
import { utf8Bytes } from "../resource-limits.js";
|
|
23
|
-
import { decodeWorkflowExecSpec, validateWorkflowPlanStructure, } from "./schema.js";
|
|
24
|
-
export const WORKFLOW_IR_V5_VERSION = 5;
|
|
25
|
-
/**
|
|
26
|
-
* Strict v4 corruption gate. No authored source or config is consulted here.
|
|
27
|
-
*
|
|
28
|
-
* `depth` is the composition-depth recursion counter (0 at the root
|
|
29
|
-
* workflow plan; +1 for every embedded child-workflow plan), and `budget` is
|
|
30
|
-
* a MUTABLE, tree-wide running total of embedded child-plan bytes charged so
|
|
31
|
-
* far. Both are internal to the recursive child-workflow decode path below
|
|
32
|
-
* (`decodeChildWorkflowTarget` calls this function again on an embedded
|
|
33
|
-
* `frozenPlan`, sharing the SAME `budget` object across the whole recursion —
|
|
34
|
-
* mirroring `ChildCompositionContext.budget` on the freeze side) — every
|
|
35
|
-
* external caller decodes a plan at the top level and relies on the
|
|
36
|
-
* defaults. Re-enforces the composition depth bound AND the aggregate
|
|
37
|
-
* embedded-bytes bound at decode as corruption gates (spec docs/plans/specs/
|
|
38
|
-
* p3a-plan-v5-child-freeze.md §3.6 step 6, rows A-23, A-N6); the actionable
|
|
39
|
-
* freeze-time `COMPOSITION_INVALID` gates live in
|
|
40
|
-
* `src/workflows/freeze/targets/child-workflow.ts` (Lane B).
|
|
41
|
-
*/
|
|
42
|
-
export function decodeWorkflowPlanV4(input, depth = 0, budget = { embeddedBytes: 0 }) {
|
|
43
|
-
const raw = record(input, "plan");
|
|
44
|
-
if (raw.irVersion !== WORKFLOW_IR_V5_VERSION)
|
|
45
|
-
fail("irVersion must be 5");
|
|
46
|
-
assertKeys(raw, ["irVersion", "title", "params", "paramSchemas", "budget", "execution", "steps", "sourceReadSet", "outputs"], "plan");
|
|
47
|
-
if (!Object.hasOwn(raw, "sourceReadSet"))
|
|
48
|
-
fail("sourceReadSet is required");
|
|
49
|
-
const sourceReadSet = decodeSourceReadSet(raw.sourceReadSet);
|
|
50
|
-
validateWorkflowPlanStructure(raw, {
|
|
51
|
-
expectedVersion: WORKFLOW_IR_V5_VERSION,
|
|
52
|
-
planExtraKeys: ["sourceReadSet", "outputs"],
|
|
53
|
-
unitExtraKeys: ["frozenTarget", "environment"],
|
|
54
|
-
gateExtraKeys: ["frozenJudge"],
|
|
55
|
-
});
|
|
56
|
-
const rawSteps = raw.steps;
|
|
57
|
-
const stepIds = new Set(rawSteps.map((rawStep) => rawStep.stepId));
|
|
58
|
-
const requiredSources = [];
|
|
59
|
-
const steps = rawSteps.map((rawStep, index) => {
|
|
60
|
-
const step = rawStep;
|
|
61
|
-
const sourceStep = record(rawSteps[index], `step ${step.stepId}`);
|
|
62
|
-
const gate = decodeGateV4(sourceStep.gate, step.stepId, requiredSources);
|
|
63
|
-
if (!step.root)
|
|
64
|
-
return Object.freeze({ ...step, gate });
|
|
65
|
-
const rawRoot = record(sourceStep.root, `step ${step.stepId} root`);
|
|
66
|
-
const root = rawRoot.kind === "map"
|
|
67
|
-
? (() => {
|
|
68
|
-
const { template: _template, ...map } = rawRoot;
|
|
69
|
-
return {
|
|
70
|
-
...map,
|
|
71
|
-
template: decodeUnitV4(record(rawRoot.template, `map ${String(rawRoot.id)} template`), requiredSources, depth, budget),
|
|
72
|
-
};
|
|
73
|
-
})()
|
|
74
|
-
: decodeUnitV4(rawRoot, requiredSources, depth, budget);
|
|
75
|
-
return Object.freeze({ ...step, root, gate });
|
|
76
|
-
});
|
|
77
|
-
assertRequiredSources(sourceReadSet, requiredSources);
|
|
78
|
-
const outputs = Object.hasOwn(raw, "outputs") ? decodeWorkflowOutputs(raw.outputs, stepIds) : undefined;
|
|
79
|
-
const { outputs: _rawOutputs, ...restRaw } = raw;
|
|
80
|
-
return Object.freeze({
|
|
81
|
-
...restRaw,
|
|
82
|
-
irVersion: WORKFLOW_IR_V5_VERSION,
|
|
83
|
-
sourceReadSet,
|
|
84
|
-
steps,
|
|
85
|
-
...(outputs ? { outputs } : {}),
|
|
86
|
-
});
|
|
87
|
-
}
|
|
88
|
-
/**
|
|
89
|
-
* Decode a plan's `outputs` (P3b, spec §4.2): a corruption gate mirroring
|
|
90
|
-
* {@link decodeInputBindings} — non-empty, sorted-unique keys (canonical wire
|
|
91
|
-
* order, same rule `inputBindings` enforces), each name matching the
|
|
92
|
-
* `params:` name pattern, each entry's closed key set, `from` re-parsing as a
|
|
93
|
-
* `stepOutput` reference whose step id is declared in `plan.steps`.
|
|
94
|
-
*/
|
|
95
|
-
function decodeWorkflowOutputs(value, stepIds) {
|
|
96
|
-
const raw = record(value, "plan outputs");
|
|
97
|
-
const names = Object.keys(raw);
|
|
98
|
-
if (names.length === 0)
|
|
99
|
-
fail("plan outputs must be a non-empty object");
|
|
100
|
-
let prior;
|
|
101
|
-
const outputs = {};
|
|
102
|
-
for (const name of names) {
|
|
103
|
-
if (!PROGRAM_PARAM_NAME_PATTERN.test(name))
|
|
104
|
-
fail(`plan outputs has invalid name ${name}`);
|
|
105
|
-
if (prior !== undefined && compareCodePoints(prior, name) >= 0) {
|
|
106
|
-
fail("plan outputs must be sorted by unique name");
|
|
107
|
-
}
|
|
108
|
-
prior = name;
|
|
109
|
-
const entry = record(raw[name], `plan outputs.${name}`);
|
|
110
|
-
assertKeys(entry, ["from", "schema"], `plan outputs.${name}`);
|
|
111
|
-
if (typeof entry.from !== "string" || !entry.from)
|
|
112
|
-
fail(`plan outputs.${name} from is invalid`);
|
|
113
|
-
const parsed = parseReference(entry.from);
|
|
114
|
-
if (!parsed.ok || parsed.expr.kind !== "stepOutput") {
|
|
115
|
-
fail(`plan outputs.${name} from must be a steps.<id>.output reference`);
|
|
116
|
-
}
|
|
117
|
-
if (!stepIds.has(parsed.expr.stepId)) {
|
|
118
|
-
fail(`plan outputs.${name} from names step ${parsed.expr.stepId}, which is not in plan.steps`);
|
|
119
|
-
}
|
|
120
|
-
const schema = Object.hasOwn(entry, "schema") ? record(entry.schema, `plan outputs.${name} schema`) : undefined;
|
|
121
|
-
outputs[name] = Object.freeze({ from: entry.from, ...(schema ? { schema: Object.freeze(schema) } : {}) });
|
|
122
|
-
}
|
|
123
|
-
return Object.freeze(outputs);
|
|
124
|
-
}
|
|
125
|
-
function decodeUnitV4(raw, requiredSources, depth, budget) {
|
|
126
|
-
const id = raw.id;
|
|
127
|
-
if (!Object.hasOwn(raw, "frozenTarget"))
|
|
128
|
-
fail(`unit ${id} frozenTarget is required`);
|
|
129
|
-
if (!Object.hasOwn(raw, "environment"))
|
|
130
|
-
fail(`unit ${id} environment is required`);
|
|
131
|
-
const environment = decodeEnvironment(raw.environment, id);
|
|
132
|
-
const frozenTarget = decodeFrozenTarget(raw.frozenTarget, raw, environment, requiredSources, depth, budget);
|
|
133
|
-
const { frozenTarget: _target, environment: _environment, ...core } = raw;
|
|
134
|
-
return Object.freeze({
|
|
135
|
-
...core,
|
|
136
|
-
frozenTarget,
|
|
137
|
-
environment: Object.freeze(environment),
|
|
138
|
-
});
|
|
139
|
-
}
|
|
140
|
-
function decodeGateV4(value, stepId, requiredSources) {
|
|
141
|
-
const gate = record(value, `gate ${stepId}`);
|
|
142
|
-
const criteria = gate.criteria;
|
|
143
|
-
if (criteria.length === 0) {
|
|
144
|
-
if (gate.frozenJudge !== null)
|
|
145
|
-
fail(`gate ${stepId} without criteria cannot have a frozen judge target`);
|
|
146
|
-
return Object.freeze({
|
|
147
|
-
kind: "gate",
|
|
148
|
-
id: gate.id,
|
|
149
|
-
stepId,
|
|
150
|
-
criteria,
|
|
151
|
-
maxLoops: gate.maxLoops,
|
|
152
|
-
frozenJudge: null,
|
|
153
|
-
});
|
|
154
|
-
}
|
|
155
|
-
if (!Object.hasOwn(gate, "frozenJudge")) {
|
|
156
|
-
fail(`gate ${stepId} with criteria is missing its frozenJudge field`);
|
|
157
|
-
}
|
|
158
|
-
if (gate.frozenJudge === null) {
|
|
159
|
-
return Object.freeze({
|
|
160
|
-
kind: "gate",
|
|
161
|
-
id: gate.id,
|
|
162
|
-
stepId,
|
|
163
|
-
criteria,
|
|
164
|
-
maxLoops: gate.maxLoops,
|
|
165
|
-
frozenJudge: null,
|
|
166
|
-
});
|
|
167
|
-
}
|
|
168
|
-
const identity = {
|
|
169
|
-
kind: "unit",
|
|
170
|
-
id: gate.id,
|
|
171
|
-
instructions: criteria.join("\n"),
|
|
172
|
-
onError: "fail",
|
|
173
|
-
isolation: "none",
|
|
174
|
-
};
|
|
175
|
-
return Object.freeze({
|
|
176
|
-
kind: "gate",
|
|
177
|
-
id: gate.id,
|
|
178
|
-
stepId,
|
|
179
|
-
criteria,
|
|
180
|
-
maxLoops: gate.maxLoops,
|
|
181
|
-
frozenJudge: decodeCommandTarget(record(gate.frozenJudge, `gate ${stepId} frozenJudge`), identity, requiredSources),
|
|
182
|
-
});
|
|
183
|
-
}
|
|
184
|
-
function decodeFrozenTarget(value, unit, environment, requiredSources, depth, budget) {
|
|
185
|
-
const target = record(value, `unit ${unit.id} frozenTarget`);
|
|
186
|
-
if (target.kind === "command")
|
|
187
|
-
return decodeCommandTarget(target, unit, requiredSources);
|
|
188
|
-
if (target.kind === "shell")
|
|
189
|
-
return decodeShellTarget(target, unit, environment);
|
|
190
|
-
if (target.kind === "script")
|
|
191
|
-
return decodeScriptTarget(target, unit, requiredSources);
|
|
192
|
-
if (target.kind === "child-workflow")
|
|
193
|
-
return decodeChildWorkflowTarget(target, unit, depth, budget);
|
|
194
|
-
fail(`unit ${unit.id} frozenTarget has unsupported kind ${String(target.kind)}`);
|
|
195
|
-
}
|
|
196
|
-
/**
|
|
197
|
-
* Decode a `kind: "child-workflow"` frozen target (§3.5): the embedded
|
|
198
|
-
* COMPLETE child plan, re-verified against its own `planHash` and this
|
|
199
|
-
* target's own `contentHash` on every decode (rows A-20, A-21), recursively
|
|
200
|
-
* enforcing `irVersion` 5 (row A-22, via the recursive
|
|
201
|
-
* {@link decodeWorkflowPlanV4} call), the composition depth bound (row A-23),
|
|
202
|
-
* and the AGGREGATE embedded-plan-bytes bound (§3.6 step 6, A-N6) as the
|
|
203
|
-
* decoder recurses. `WORKFLOW_MAX_EMBEDDED_CHILD_PLAN_BYTES` — unlike the
|
|
204
|
-
* depth bound above — is imported directly from
|
|
205
|
-
* `src/workflows/resource-limits.ts` rather than reproduced: that constant
|
|
206
|
-
* was already Lane B's when this decode-time re-enforcement was added (no
|
|
207
|
-
* lane-ordering constraint left to honor), and re-deriving a byte cap by
|
|
208
|
-
* hand invites drift a depth integer does not. `budget` is charged with
|
|
209
|
-
* THIS child's own full canonical byte length AFTER it decodes (so any
|
|
210
|
-
* further-nested grandchildren it embeds have already charged themselves,
|
|
211
|
-
* mirroring the freeze-side `chargeEmbeddedBudget`'s post-recursion charge
|
|
212
|
-
* order in `src/workflows/freeze/targets/child-workflow.ts`) — a rejected
|
|
213
|
-
* plan therefore never partially charges the budget. `frozenPlan` is
|
|
214
|
-
* covered wholesale through `planHash`, so it is deliberately NOT
|
|
215
|
-
* re-serialized into `contentHash` (§3.5). No `requiredSources`
|
|
216
|
-
* contribution: unlike a command/script target's own referenced source, the
|
|
217
|
-
* child's transitive sources live in the child's OWN `sourceReadSet`,
|
|
218
|
-
* verified by the recursive decode call itself; the PARENT's
|
|
219
|
-
* `sourceReadSet` absorbing the child's files (row B-05) is a freeze-time
|
|
220
|
-
* concern (Lane B), not a decode-time structural one.
|
|
221
|
-
*/
|
|
222
|
-
function decodeChildWorkflowTarget(target, unit, depth, budget) {
|
|
223
|
-
assertKeys(target, ["kind", "ref", "planHash", "frozenPlan", "contentHash", "via", "taskRef", "inputBindings"], `unit ${unit.id} child workflow target`);
|
|
224
|
-
if (typeof target.ref !== "string" || !target.ref)
|
|
225
|
-
fail(`unit ${unit.id} child workflow ref is invalid`);
|
|
226
|
-
const planHash = digest(target.planHash, `unit ${unit.id} child workflow planHash`);
|
|
227
|
-
if (target.via !== "direct" && target.via !== "task")
|
|
228
|
-
fail(`unit ${unit.id} child workflow via is invalid`);
|
|
229
|
-
const via = target.via;
|
|
230
|
-
if (via === "task") {
|
|
231
|
-
if (typeof target.taskRef !== "string" || !target.taskRef) {
|
|
232
|
-
fail(`unit ${unit.id} child workflow via "task" requires a taskRef`);
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
else if (target.taskRef !== undefined) {
|
|
236
|
-
fail(`unit ${unit.id} child workflow via "direct" cannot carry a taskRef`);
|
|
237
|
-
}
|
|
238
|
-
const taskRef = target.taskRef;
|
|
239
|
-
const inputBindings = decodeInputBindings(target.inputBindings, `unit ${unit.id} child workflow target`);
|
|
240
|
-
const contentHash = digest(target.contentHash, `unit ${unit.id} child workflow contentHash`);
|
|
241
|
-
const expectedContentHash = childWorkflowContentHash({ ref: target.ref, planHash, via, taskRef, inputBindings });
|
|
242
|
-
if (contentHash !== expectedContentHash) {
|
|
243
|
-
fail(`unit ${unit.id} child workflow contentHash does not match its frozen dispatch`);
|
|
244
|
-
}
|
|
245
|
-
const childDepth = depth + 1;
|
|
246
|
-
const frozenPlan = decodeWorkflowPlanV4(target.frozenPlan, childDepth, budget);
|
|
247
|
-
const embeddedPlanJson = canonicalJsonLocal(frozenPlan);
|
|
248
|
-
const actualPlanHash = sha256(embeddedPlanJson);
|
|
249
|
-
if (actualPlanHash !== planHash) {
|
|
250
|
-
fail(`unit ${unit.id} child workflow embedded plan does not match its frozen planHash`);
|
|
251
|
-
}
|
|
252
|
-
budget.embeddedBytes += utf8Bytes(embeddedPlanJson);
|
|
253
|
-
return Object.freeze({
|
|
254
|
-
kind: "child-workflow",
|
|
255
|
-
ref: target.ref,
|
|
256
|
-
planHash,
|
|
257
|
-
frozenPlan,
|
|
258
|
-
contentHash,
|
|
259
|
-
via,
|
|
260
|
-
...(taskRef !== undefined ? { taskRef } : {}),
|
|
261
|
-
...(inputBindings ? { inputBindings } : {}),
|
|
262
|
-
});
|
|
263
|
-
}
|
|
264
|
-
/** §3.5's exact `contentHash` formula. */
|
|
265
|
-
function childWorkflowContentHash(fields) {
|
|
266
|
-
return createHash("sha256")
|
|
267
|
-
.update("akm.workflow.child-workflow\0v1\0")
|
|
268
|
-
.update(canonicalJsonLocal({
|
|
269
|
-
ref: fields.ref,
|
|
270
|
-
planHash: fields.planHash,
|
|
271
|
-
via: fields.via,
|
|
272
|
-
taskRef: fields.taskRef ?? null,
|
|
273
|
-
inputBindings: fields.inputBindings ?? null,
|
|
274
|
-
}))
|
|
275
|
-
.digest("hex");
|
|
276
|
-
}
|
|
277
|
-
function decodeCommandTarget(target, unit, requiredSources) {
|
|
278
|
-
assertKeys(target, [
|
|
279
|
-
"kind",
|
|
280
|
-
"ref",
|
|
281
|
-
"contentHash",
|
|
282
|
-
"request",
|
|
283
|
-
"runner",
|
|
284
|
-
"concurrency",
|
|
285
|
-
"cwdIdentity",
|
|
286
|
-
"executable",
|
|
287
|
-
"gitCommitOid",
|
|
288
|
-
"inputBindings",
|
|
289
|
-
], `unit ${unit.id} command target`);
|
|
290
|
-
if (target.ref !== null && typeof target.ref !== "string")
|
|
291
|
-
fail(`unit ${unit.id} command target ref is invalid`);
|
|
292
|
-
const request = decodeResolvedExecutionRequest(target.request);
|
|
293
|
-
if (request.authorization.status === "denied")
|
|
294
|
-
fail(`unit ${unit.id} command authorization is denied by policy`);
|
|
295
|
-
if (Object.hasOwn(request.runtime, "environment")) {
|
|
296
|
-
fail(`unit ${unit.id} resolved request runtime.environment is live and cannot be persisted`);
|
|
297
|
-
}
|
|
298
|
-
const contentHash = digest(target.contentHash, `unit ${unit.id} command contentHash`);
|
|
299
|
-
const actualContentHash = sha256(request.command.content);
|
|
300
|
-
if (contentHash !== actualContentHash)
|
|
301
|
-
fail(`unit ${unit.id} command contentHash does not match request content`);
|
|
302
|
-
const runner = decodeFrozenRunnerSpec(target.runner);
|
|
303
|
-
if (target.concurrency !== undefined &&
|
|
304
|
-
(!Number.isSafeInteger(target.concurrency) ||
|
|
305
|
-
target.concurrency < 1 ||
|
|
306
|
-
target.concurrency > 64)) {
|
|
307
|
-
fail(`unit ${unit.id} frozen target concurrency is invalid`);
|
|
308
|
-
}
|
|
309
|
-
if (runner.engine !== request.engine.name || runner.kind !== request.engine.kind) {
|
|
310
|
-
fail(`unit ${unit.id} frozen runner must match the resolved request engine`);
|
|
311
|
-
}
|
|
312
|
-
const source = request.command.source;
|
|
313
|
-
if (source) {
|
|
314
|
-
requiredSources.push(source);
|
|
315
|
-
if (target.ref !== source.ref)
|
|
316
|
-
fail(`unit ${unit.id} command target ref must match request source`);
|
|
317
|
-
}
|
|
318
|
-
else if (target.ref !== null) {
|
|
319
|
-
fail(`unit ${unit.id} inline command target ref must be null`);
|
|
320
|
-
}
|
|
321
|
-
if (request.persona)
|
|
322
|
-
requiredSources.push(request.persona.source);
|
|
323
|
-
const cwdIdentity = Object.hasOwn(target, "cwdIdentity")
|
|
324
|
-
? decodeDirectoryIdentity(target.cwdIdentity, unit.id)
|
|
325
|
-
: undefined;
|
|
326
|
-
const executable = Object.hasOwn(target, "executable")
|
|
327
|
-
? decodeFrozenExecutableIdentity(target.executable, `unit ${unit.id} executable`)
|
|
328
|
-
: undefined;
|
|
329
|
-
if (runner.kind === "agent" && (cwdIdentity !== undefined || executable !== undefined)) {
|
|
330
|
-
if (!cwdIdentity || !executable)
|
|
331
|
-
fail(`unit ${unit.id} CLI command target requires cwdIdentity and executable`);
|
|
332
|
-
if (runner.profile.bin !== executable.requested && runner.profile.bin !== executable.absolutePath) {
|
|
333
|
-
fail(`unit ${unit.id} executable does not match the frozen runner bin`);
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
else if (runner.kind !== "agent" && executable !== undefined) {
|
|
337
|
-
fail(`unit ${unit.id} non-CLI target cannot carry a host executable`);
|
|
338
|
-
}
|
|
339
|
-
const gitCommitOid = decodeGitCommitOid(target.gitCommitOid, unit);
|
|
340
|
-
const inputBindings = decodeInputBindings(target.inputBindings, `unit ${unit.id} command target`);
|
|
341
|
-
// Force the shared request canonicalizer across every accepted wire request.
|
|
342
|
-
canonicalResolvedExecutionRequest(request);
|
|
343
|
-
return Object.freeze({
|
|
344
|
-
kind: "command",
|
|
345
|
-
ref: target.ref,
|
|
346
|
-
contentHash,
|
|
347
|
-
request,
|
|
348
|
-
runner,
|
|
349
|
-
...(target.concurrency !== undefined ? { concurrency: target.concurrency } : {}),
|
|
350
|
-
...(cwdIdentity ? { cwdIdentity } : {}),
|
|
351
|
-
...(executable ? { executable } : {}),
|
|
352
|
-
...(gitCommitOid ? { gitCommitOid } : {}),
|
|
353
|
-
...(inputBindings ? { inputBindings } : {}),
|
|
354
|
-
});
|
|
355
|
-
}
|
|
356
|
-
function decodeShellTarget(target, unit, environment) {
|
|
357
|
-
assertKeys(target, ["kind", "contentHash", "exec", "cwdIdentity", "executable", "gitCommitOid", "inputBindings"], `unit ${unit.id} shell target`);
|
|
358
|
-
const exec = decodeWorkflowExecSpec(target.exec, `unit ${unit.id} shell target exec`);
|
|
359
|
-
const cwdIdentity = decodeDirectoryIdentity(target.cwdIdentity, unit.id);
|
|
360
|
-
const executable = Object.hasOwn(target, "executable")
|
|
361
|
-
? decodeFrozenExecutableIdentity(target.executable, `unit ${unit.id} executable`)
|
|
362
|
-
: undefined;
|
|
363
|
-
if (executable && exec.command[0] !== executable.requested && exec.command[0] !== executable.absolutePath) {
|
|
364
|
-
fail(`unit ${unit.id} shell executable does not match the frozen command`);
|
|
365
|
-
}
|
|
366
|
-
const gitCommitOid = decodeGitCommitOid(target.gitCommitOid, unit);
|
|
367
|
-
const contentHash = digest(target.contentHash, `unit ${unit.id} shell contentHash`);
|
|
368
|
-
// inputBindings deliberately sits OUTSIDE this preimage (P2b A-N7): identity
|
|
369
|
-
// coverage for a task-composed unit comes from computeUnitInputHash's own
|
|
370
|
-
// frozenTarget field (step-work.ts), which hashes this whole target anyway.
|
|
371
|
-
const expected = createHash("sha256")
|
|
372
|
-
.update("akm.workflow.shell.v1\0")
|
|
373
|
-
.update(canonicalJsonLocal({ exec, environment, cwdIdentity }))
|
|
374
|
-
.digest("hex");
|
|
375
|
-
if (contentHash !== expected)
|
|
376
|
-
fail(`unit ${unit.id} shell contentHash does not match its frozen dispatch`);
|
|
377
|
-
const inputBindings = decodeInputBindings(target.inputBindings, `unit ${unit.id} shell target`);
|
|
378
|
-
return Object.freeze({
|
|
379
|
-
kind: "shell",
|
|
380
|
-
contentHash,
|
|
381
|
-
exec,
|
|
382
|
-
cwdIdentity,
|
|
383
|
-
...(executable ? { executable } : {}),
|
|
384
|
-
...(gitCommitOid ? { gitCommitOid } : {}),
|
|
385
|
-
...(inputBindings ? { inputBindings } : {}),
|
|
386
|
-
});
|
|
387
|
-
}
|
|
388
|
-
function decodeScriptTarget(target, unit, requiredSources) {
|
|
389
|
-
assertKeys(target, [
|
|
390
|
-
"kind",
|
|
391
|
-
"ref",
|
|
392
|
-
"contentHash",
|
|
393
|
-
"exec",
|
|
394
|
-
"interpreter",
|
|
395
|
-
"extension",
|
|
396
|
-
"bytesBase64",
|
|
397
|
-
"byteLength",
|
|
398
|
-
"cwdIdentity",
|
|
399
|
-
"materialization",
|
|
400
|
-
"executable",
|
|
401
|
-
"gitCommitOid",
|
|
402
|
-
"inputBindings",
|
|
403
|
-
], `unit ${unit.id} script target`);
|
|
404
|
-
const exec = decodeWorkflowExecSpec(target.exec, `unit ${unit.id} script target exec`);
|
|
405
|
-
if (typeof target.ref !== "string" || !target.ref.includes("//"))
|
|
406
|
-
fail(`unit ${unit.id} script ref is invalid`);
|
|
407
|
-
if (typeof target.interpreter !== "string" || !target.interpreter)
|
|
408
|
-
fail(`unit ${unit.id} script interpreter is invalid`);
|
|
409
|
-
if (typeof target.extension !== "string" || !/^\.[A-Za-z0-9]+$/.test(target.extension)) {
|
|
410
|
-
fail(`unit ${unit.id} script extension is invalid`);
|
|
411
|
-
}
|
|
412
|
-
if (typeof target.bytesBase64 !== "string")
|
|
413
|
-
fail(`unit ${unit.id} script bytesBase64 is invalid`);
|
|
414
|
-
const bytes = Buffer.from(target.bytesBase64, "base64");
|
|
415
|
-
if (bytes.toString("base64") !== target.bytesBase64)
|
|
416
|
-
fail(`unit ${unit.id} script bytesBase64 is noncanonical`);
|
|
417
|
-
if (!Number.isSafeInteger(target.byteLength) || target.byteLength !== bytes.byteLength) {
|
|
418
|
-
fail(`unit ${unit.id} script byteLength does not match frozen bytes`);
|
|
419
|
-
}
|
|
420
|
-
const contentHash = digest(target.contentHash, `unit ${unit.id} script contentHash`);
|
|
421
|
-
if (contentHash !== sha256(bytes))
|
|
422
|
-
fail(`unit ${unit.id} script contentHash does not match frozen bytes`);
|
|
423
|
-
if (target.materialization !== "ephemeral-0700-delete")
|
|
424
|
-
fail(`unit ${unit.id} script materialization is invalid`);
|
|
425
|
-
const cwdIdentity = decodeDirectoryIdentity(target.cwdIdentity, unit.id);
|
|
426
|
-
const executable = Object.hasOwn(target, "executable")
|
|
427
|
-
? decodeFrozenExecutableIdentity(target.executable, `unit ${unit.id} executable`)
|
|
428
|
-
: undefined;
|
|
429
|
-
const gitCommitOid = decodeGitCommitOid(target.gitCommitOid, unit);
|
|
430
|
-
const inputBindings = decodeInputBindings(target.inputBindings, `unit ${unit.id} script target`);
|
|
431
|
-
requiredSources.push({ ref: target.ref, bundle: "", adapter: "", file: "", hash: contentHash });
|
|
432
|
-
return Object.freeze({
|
|
433
|
-
kind: "script",
|
|
434
|
-
ref: target.ref,
|
|
435
|
-
contentHash,
|
|
436
|
-
exec,
|
|
437
|
-
interpreter: target.interpreter,
|
|
438
|
-
extension: target.extension,
|
|
439
|
-
bytesBase64: target.bytesBase64,
|
|
440
|
-
byteLength: target.byteLength,
|
|
441
|
-
cwdIdentity,
|
|
442
|
-
materialization: "ephemeral-0700-delete",
|
|
443
|
-
...(executable ? { executable } : {}),
|
|
444
|
-
...(gitCommitOid ? { gitCommitOid } : {}),
|
|
445
|
-
...(inputBindings ? { inputBindings } : {}),
|
|
446
|
-
});
|
|
447
|
-
}
|
|
448
|
-
/**
|
|
449
|
-
* Decode a frozen target's `inputBindings` (P2b §3.2 A-N7): a composing
|
|
450
|
-
* step's `with:` normalized against the composed task's declared inputs.
|
|
451
|
-
* Closed `kind`, `INPUT_NAME_PATTERN` name, unique and sorted by name (never
|
|
452
|
-
* `[]` — absence is the identity-preserving default). A `literal` entry
|
|
453
|
-
* requires `value`; a `reference` entry requires a `parseReference`-valid
|
|
454
|
-
* `from` plus the declaration's bounded `schema` (§3.6's widened reference
|
|
455
|
-
* arm, `src/execution/input-contract.ts`).
|
|
456
|
-
*/
|
|
457
|
-
function decodeInputBindings(value, label) {
|
|
458
|
-
if (value === undefined)
|
|
459
|
-
return undefined;
|
|
460
|
-
if (!Array.isArray(value) || value.length === 0)
|
|
461
|
-
fail(`${label} inputBindings must be a non-empty array`);
|
|
462
|
-
let prior;
|
|
463
|
-
const bindings = value.map((raw, index) => {
|
|
464
|
-
const binding = record(raw, `${label} inputBindings[${index}]`);
|
|
465
|
-
if (typeof binding.name !== "string" || !INPUT_NAME_PATTERN.test(binding.name)) {
|
|
466
|
-
fail(`${label} inputBindings[${index}] name is invalid`);
|
|
467
|
-
}
|
|
468
|
-
if (prior !== undefined && compareCodePoints(prior, binding.name) >= 0) {
|
|
469
|
-
fail(`${label} inputBindings must be sorted by unique name`);
|
|
470
|
-
}
|
|
471
|
-
prior = binding.name;
|
|
472
|
-
if (binding.kind === "literal") {
|
|
473
|
-
assertKeys(binding, ["kind", "name", "value"], `${label} inputBindings[${index}]`);
|
|
474
|
-
if (!Object.hasOwn(binding, "value"))
|
|
475
|
-
fail(`${label} inputBindings[${index}] literal binding requires value`);
|
|
476
|
-
return Object.freeze({ kind: "literal", name: binding.name, value: binding.value });
|
|
477
|
-
}
|
|
478
|
-
if (binding.kind === "reference") {
|
|
479
|
-
assertKeys(binding, ["kind", "name", "from", "schema"], `${label} inputBindings[${index}]`);
|
|
480
|
-
if (typeof binding.from !== "string" || !parseReference(binding.from).ok) {
|
|
481
|
-
fail(`${label} inputBindings[${index}] from is not a valid reference`);
|
|
482
|
-
}
|
|
483
|
-
const schema = record(binding.schema, `${label} inputBindings[${index}] schema`);
|
|
484
|
-
return Object.freeze({
|
|
485
|
-
kind: "reference",
|
|
486
|
-
name: binding.name,
|
|
487
|
-
from: binding.from,
|
|
488
|
-
schema: Object.freeze(schema),
|
|
489
|
-
});
|
|
490
|
-
}
|
|
491
|
-
return fail(`${label} inputBindings[${index}] has unsupported kind ${String(binding.kind)}`);
|
|
492
|
-
});
|
|
493
|
-
return Object.freeze(bindings);
|
|
494
|
-
}
|
|
495
|
-
function decodeGitCommitOid(value, unit) {
|
|
496
|
-
if (unit.isolation === "worktree") {
|
|
497
|
-
if (typeof value !== "string" || !/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/.test(value)) {
|
|
498
|
-
fail(`unit ${unit.id} worktree target requires a canonical gitCommitOid`);
|
|
499
|
-
}
|
|
500
|
-
return value;
|
|
501
|
-
}
|
|
502
|
-
if (value !== undefined)
|
|
503
|
-
fail(`unit ${unit.id} gitCommitOid is only valid for worktree isolation`);
|
|
504
|
-
return undefined;
|
|
505
|
-
}
|
|
506
|
-
function decodeDirectoryIdentity(value, unitId) {
|
|
507
|
-
const identity = record(value, `unit ${unitId} cwdIdentity`);
|
|
508
|
-
const fields = [
|
|
509
|
-
"requestedRoot",
|
|
510
|
-
"realRoot",
|
|
511
|
-
"rootDevice",
|
|
512
|
-
"rootInode",
|
|
513
|
-
"requestedCwd",
|
|
514
|
-
"realCwd",
|
|
515
|
-
"cwdDevice",
|
|
516
|
-
"cwdInode",
|
|
517
|
-
];
|
|
518
|
-
assertKeys(identity, fields, `unit ${unitId} cwdIdentity`);
|
|
519
|
-
for (const field of fields) {
|
|
520
|
-
if (typeof identity[field] !== "string" || !identity[field])
|
|
521
|
-
fail(`unit ${unitId} cwdIdentity.${field} is invalid`);
|
|
522
|
-
}
|
|
523
|
-
for (const field of ["requestedRoot", "realRoot", "requestedCwd", "realCwd"]) {
|
|
524
|
-
if (!path.isAbsolute(identity[field]))
|
|
525
|
-
fail(`unit ${unitId} cwdIdentity.${field} must be absolute`);
|
|
526
|
-
}
|
|
527
|
-
return Object.freeze(identity);
|
|
528
|
-
}
|
|
529
|
-
function decodeEnvironment(value, unitId) {
|
|
530
|
-
if (!Array.isArray(value))
|
|
531
|
-
fail(`unit ${unitId} environment must be an array`);
|
|
532
|
-
const out = [];
|
|
533
|
-
const names = new Set();
|
|
534
|
-
for (const [index, raw] of value.entries()) {
|
|
535
|
-
const binding = record(raw, `unit ${unitId} environment[${index}]`);
|
|
536
|
-
if (binding.kind === "literal") {
|
|
537
|
-
assertKeys(binding, ["kind", "name", "value"], `unit ${unitId} environment[${index}]`);
|
|
538
|
-
environmentName(binding.name, unitId, index);
|
|
539
|
-
if (typeof binding.value !== "string")
|
|
540
|
-
fail(`unit ${unitId} literal environment value must be a string`);
|
|
541
|
-
if (literalLooksSecret(binding.name, binding.value)) {
|
|
542
|
-
fail(`unit ${unitId} literal environment value is secret-shaped`);
|
|
543
|
-
}
|
|
544
|
-
out.push(Object.freeze({ kind: "literal", name: binding.name, value: binding.value }));
|
|
545
|
-
}
|
|
546
|
-
else if (binding.kind === "pass-through") {
|
|
547
|
-
assertKeys(binding, ["kind", "name"], `unit ${unitId} environment[${index}]`);
|
|
548
|
-
environmentName(binding.name, unitId, index);
|
|
549
|
-
out.push(Object.freeze({ kind: "pass-through", name: binding.name }));
|
|
550
|
-
}
|
|
551
|
-
else if (binding.kind === "env-ref") {
|
|
552
|
-
assertKeys(binding, ["kind", "ref", "owner", "keys", "secretNames", "precedence"], `unit ${unitId} environment[${index}]`);
|
|
553
|
-
if (typeof binding.ref !== "string" || !binding.ref.includes("//")) {
|
|
554
|
-
fail(`unit ${unitId} environment[${index}] env-ref must be fully qualified`);
|
|
555
|
-
}
|
|
556
|
-
const owner = decodeEnvironmentOwner(binding.owner, binding.ref, unitId, index);
|
|
557
|
-
const keys = sortedUniqueStrings(binding.keys, `unit ${unitId} environment[${index}] keys`, (value) => /^[A-Za-z_][A-Za-z0-9_]*$/.test(value));
|
|
558
|
-
const secretNames = sortedUniqueStrings(binding.secretNames, `unit ${unitId} environment[${index}] secretNames`, (value) => /^[A-Za-z0-9_./-]+$/.test(value) && !value.includes(".."));
|
|
559
|
-
if (!Number.isSafeInteger(binding.precedence) || binding.precedence < 0) {
|
|
560
|
-
fail(`unit ${unitId} environment[${index}] precedence is invalid`);
|
|
561
|
-
}
|
|
562
|
-
out.push(Object.freeze({
|
|
563
|
-
kind: "env-ref",
|
|
564
|
-
ref: binding.ref,
|
|
565
|
-
owner,
|
|
566
|
-
keys: Object.freeze(keys),
|
|
567
|
-
secretNames: Object.freeze(secretNames),
|
|
568
|
-
precedence: binding.precedence,
|
|
569
|
-
}));
|
|
570
|
-
}
|
|
571
|
-
else {
|
|
572
|
-
fail(`unit ${unitId} environment[${index}] has unsupported kind`);
|
|
573
|
-
}
|
|
574
|
-
if (binding.kind !== "env-ref") {
|
|
575
|
-
const name = binding.name;
|
|
576
|
-
if (names.has(name))
|
|
577
|
-
fail(`unit ${unitId} environment contains duplicate name ${name}`);
|
|
578
|
-
names.add(name);
|
|
579
|
-
}
|
|
580
|
-
}
|
|
581
|
-
return out;
|
|
582
|
-
}
|
|
583
|
-
function decodeEnvironmentOwner(value, ref, unitId, index) {
|
|
584
|
-
const label = `unit ${unitId} environment[${index}] owner`;
|
|
585
|
-
const owner = record(value, label);
|
|
586
|
-
const fields = [
|
|
587
|
-
"bundle",
|
|
588
|
-
"adapter",
|
|
589
|
-
"requestedRoot",
|
|
590
|
-
"realRoot",
|
|
591
|
-
"rootPhysicalIdentity",
|
|
592
|
-
"requestedPath",
|
|
593
|
-
"realPath",
|
|
594
|
-
"relativePath",
|
|
595
|
-
];
|
|
596
|
-
assertKeys(owner, fields, label);
|
|
597
|
-
for (const field of fields) {
|
|
598
|
-
if (typeof owner[field] !== "string" || !owner[field])
|
|
599
|
-
fail(`${label}.${field} is invalid`);
|
|
600
|
-
}
|
|
601
|
-
if (!ref.startsWith(`${owner.bundle}//`))
|
|
602
|
-
fail(`${label}.bundle does not own env-ref ${ref}`);
|
|
603
|
-
for (const field of ["requestedRoot", "realRoot", "requestedPath", "realPath"]) {
|
|
604
|
-
if (!path.isAbsolute(owner[field]))
|
|
605
|
-
fail(`${label}.${field} must be absolute`);
|
|
606
|
-
}
|
|
607
|
-
const relativePath = owner.relativePath;
|
|
608
|
-
if (relativePath.includes("\\") ||
|
|
609
|
-
relativePath === "." ||
|
|
610
|
-
relativePath.startsWith("../") ||
|
|
611
|
-
path.isAbsolute(relativePath)) {
|
|
612
|
-
fail(`${label}.relativePath is not canonical`);
|
|
613
|
-
}
|
|
614
|
-
if (path.resolve(owner.requestedRoot, relativePath) !== path.resolve(owner.requestedPath)) {
|
|
615
|
-
fail(`${label}.requestedPath does not match relativePath`);
|
|
616
|
-
}
|
|
617
|
-
if (path.resolve(owner.realRoot, relativePath) !== path.resolve(owner.realPath)) {
|
|
618
|
-
fail(`${label}.realPath does not match relativePath`);
|
|
619
|
-
}
|
|
620
|
-
return Object.freeze(owner);
|
|
621
|
-
}
|
|
622
|
-
function sortedUniqueStrings(value, label, accepts) {
|
|
623
|
-
if (!Array.isArray(value))
|
|
624
|
-
fail(`${label} must be an array`);
|
|
625
|
-
const out = [];
|
|
626
|
-
let prior;
|
|
627
|
-
for (const item of value) {
|
|
628
|
-
if (typeof item !== "string" || !accepts(item))
|
|
629
|
-
fail(`${label} contains an invalid value`);
|
|
630
|
-
if (prior !== undefined && compareCodePoints(prior, item) >= 0)
|
|
631
|
-
fail(`${label} must be sorted and unique`);
|
|
632
|
-
prior = item;
|
|
633
|
-
out.push(item);
|
|
634
|
-
}
|
|
635
|
-
return out;
|
|
636
|
-
}
|
|
637
|
-
function decodeSourceReadSet(value) {
|
|
638
|
-
if (!Array.isArray(value) || value.length === 0)
|
|
639
|
-
fail("sourceReadSet must contain at least one source identity");
|
|
640
|
-
const out = [];
|
|
641
|
-
const logical = new Set();
|
|
642
|
-
const physical = new Map();
|
|
643
|
-
let priorKey;
|
|
644
|
-
for (const [index, raw] of value.entries()) {
|
|
645
|
-
const snapshot = record(raw, `sourceReadSet[${index}]`);
|
|
646
|
-
assertKeys(snapshot, ["identity", "containmentPhysicalIdentity", "physicalIdentity", "size"], `sourceReadSet[${index}]`);
|
|
647
|
-
const identity = decodeExecutionSourceIdentity(snapshot.identity, `sourceReadSet[${index}].identity`);
|
|
648
|
-
const key = sourceKey(identity);
|
|
649
|
-
if (logical.has(key))
|
|
650
|
-
fail(`sourceReadSet contains duplicate identity key ${key}`);
|
|
651
|
-
if (typeof snapshot.containmentPhysicalIdentity !== "string" || !snapshot.containmentPhysicalIdentity) {
|
|
652
|
-
fail(`sourceReadSet[${index}] containment physical identity is invalid`);
|
|
653
|
-
}
|
|
654
|
-
if (typeof snapshot.physicalIdentity !== "string" || !snapshot.physicalIdentity) {
|
|
655
|
-
fail(`sourceReadSet[${index}] physical identity is invalid`);
|
|
656
|
-
}
|
|
657
|
-
if (!Number.isSafeInteger(snapshot.size) || snapshot.size < 0)
|
|
658
|
-
fail(`sourceReadSet[${index}] size is invalid`);
|
|
659
|
-
const physicalKey = `${snapshot.containmentPhysicalIdentity}\0${snapshot.physicalIdentity}`;
|
|
660
|
-
const alias = physical.get(physicalKey);
|
|
661
|
-
if (alias !== undefined && alias !== key)
|
|
662
|
-
fail(`sourceReadSet logical refs alias the same physical file identity`);
|
|
663
|
-
physical.set(physicalKey, key);
|
|
664
|
-
if (priorKey !== undefined && compareCodePoints(priorKey, key) >= 0) {
|
|
665
|
-
fail("sourceReadSet is not in canonical ref, adapter, file sort order");
|
|
666
|
-
}
|
|
667
|
-
logical.add(key);
|
|
668
|
-
priorKey = key;
|
|
669
|
-
out.push(Object.freeze({
|
|
670
|
-
identity,
|
|
671
|
-
containmentPhysicalIdentity: snapshot.containmentPhysicalIdentity,
|
|
672
|
-
physicalIdentity: snapshot.physicalIdentity,
|
|
673
|
-
size: snapshot.size,
|
|
674
|
-
}));
|
|
675
|
-
}
|
|
676
|
-
return out;
|
|
677
|
-
}
|
|
678
|
-
function assertRequiredSources(readSet, required) {
|
|
679
|
-
for (const identity of required) {
|
|
680
|
-
const found = readSet.find((snapshot) => snapshot.identity.ref === identity.ref);
|
|
681
|
-
if (!found)
|
|
682
|
-
fail(`source read-set is missing required command/persona/script source ${identity.ref}`);
|
|
683
|
-
if (identity.bundle && sourceKey(found.identity) !== sourceKey(identity)) {
|
|
684
|
-
fail(`source read-set identity mismatch for ${identity.ref}`);
|
|
685
|
-
}
|
|
686
|
-
if (found.identity.hash !== identity.hash)
|
|
687
|
-
fail(`source read-set hash mismatch for ${identity.ref}`);
|
|
688
|
-
}
|
|
689
|
-
}
|
|
690
|
-
function sourceKey(identity) {
|
|
691
|
-
return `${identity.ref}\0${identity.adapter}\0${identity.file}`;
|
|
692
|
-
}
|
|
693
|
-
function environmentName(value, unitId, index) {
|
|
694
|
-
if (typeof value !== "string" || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(value)) {
|
|
695
|
-
fail(`unit ${unitId} environment[${index}] has an invalid name`);
|
|
696
|
-
}
|
|
697
|
-
}
|
|
698
|
-
function literalLooksSecret(name, value) {
|
|
699
|
-
const key = name.toLowerCase();
|
|
700
|
-
if (/(?:secret|token|password|passwd|api[_-]?key|access[_-]?key|private[_-]?key|credential|bearer|client_secret)/.test(key)) {
|
|
701
|
-
return true;
|
|
702
|
-
}
|
|
703
|
-
const candidate = value.trim();
|
|
704
|
-
if (candidate.length < 20 || /\s/.test(candidate))
|
|
705
|
-
return false;
|
|
706
|
-
if (/^(?:sk-|rk-|ghp_|gho_|ghu_|ghs_|ghr_|github_pat_|xox[baprs]-|AKIA|ASIA|AIza|ya29\.|-----BEGIN)/.test(candidate)) {
|
|
707
|
-
return true;
|
|
708
|
-
}
|
|
709
|
-
const classes = [/[a-z]/, /[A-Z]/, /[0-9]/, /[^A-Za-z0-9]/].filter((pattern) => pattern.test(candidate)).length;
|
|
710
|
-
return (candidate.length >= 24 && classes >= 3) || (candidate.length >= 32 && /^[A-Za-z0-9+/=_-]+$/.test(candidate));
|
|
711
|
-
}
|
|
712
|
-
function digest(value, label) {
|
|
713
|
-
if (typeof value !== "string" || !/^[0-9a-f]{64}$/.test(value))
|
|
714
|
-
fail(`${label} must be a SHA-256 digest`);
|
|
715
|
-
return value;
|
|
716
|
-
}
|
|
717
|
-
function sha256(value) {
|
|
718
|
-
return createHash("sha256").update(value).digest("hex");
|
|
719
|
-
}
|
|
720
|
-
function canonicalJsonLocal(value) {
|
|
721
|
-
const sort = (entry) => {
|
|
722
|
-
if (Array.isArray(entry))
|
|
723
|
-
return entry.map(sort);
|
|
724
|
-
if (entry && typeof entry === "object") {
|
|
725
|
-
return Object.fromEntries(Object.entries(entry)
|
|
726
|
-
.sort(([left], [right]) => compareCodePoints(left, right))
|
|
727
|
-
.map(([key, nested]) => [key, sort(nested)]));
|
|
728
|
-
}
|
|
729
|
-
return entry;
|
|
730
|
-
};
|
|
731
|
-
return JSON.stringify(sort(value));
|
|
732
|
-
}
|
|
733
|
-
function record(value, label) {
|
|
734
|
-
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
735
|
-
fail(`${label} must be an object`);
|
|
736
|
-
return value;
|
|
737
|
-
}
|
|
738
|
-
function assertKeys(value, allowed, label) {
|
|
739
|
-
for (const key of Object.keys(value))
|
|
740
|
-
if (!allowed.includes(key))
|
|
741
|
-
fail(`${label} contains unknown key ${key}`);
|
|
742
|
-
}
|
|
743
|
-
function fail(message) {
|
|
744
|
-
throw new UsageError(`Invalid frozen workflow plan v4: ${message}.`, "INVALID_JSON_ARGUMENT");
|
|
745
|
-
}
|