akm-cli 0.9.4 → 0.9.6
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 +265 -0
- package/dist/commands/env/env-cli.js +1 -2
- package/dist/commands/env/secret-cli.js +1 -5
- package/dist/commands/feedback-cli.js +0 -4
- package/dist/commands/health/checks.js +0 -32
- package/dist/commands/health/surfaces.js +2 -2
- package/dist/commands/health.js +6 -15
- package/dist/commands/improve/anti-collapse.js +4 -91
- package/dist/commands/improve/autonomy-gate.js +1 -1
- package/dist/commands/improve/consolidate.js +25 -26
- package/dist/commands/improve/distill.js +2 -2
- package/dist/commands/improve/extract.js +8 -1
- package/dist/commands/improve/improve.js +3 -5
- package/dist/commands/improve/locks.js +13 -5
- package/dist/commands/improve/loop-stages.js +8 -9
- package/dist/commands/improve/memory/memory-contradiction-detect.js +1 -19
- package/dist/commands/improve/outcome-loop.js +0 -6
- package/dist/commands/improve/reflect.js +20 -9
- package/dist/commands/lint/index.js +4 -6
- package/dist/commands/proposal/validators/proposal-quality-validators.js +0 -7
- package/dist/commands/proposal/validators/proposal-validators.js +12 -0
- package/dist/commands/read/search.js +14 -24
- package/dist/commands/sources/bundle-config-ops.js +9 -4
- package/dist/commands/sources/info.js +14 -10
- package/dist/commands/tasks/tasks-cli.js +56 -3
- package/dist/commands/tasks/tasks.js +101 -18
- package/dist/commands/workflow-cli.js +2 -2
- package/dist/core/adapter/adapters/akm-adapter.js +2 -0
- package/dist/core/common.js +61 -24
- package/dist/core/config/config-io.js +2 -2
- package/dist/core/config/config-sources.js +32 -2
- package/dist/core/config/config-version-shim.js +101 -0
- package/dist/core/config/config.js +7 -7
- package/dist/core/config/schema/engines.js +9 -7
- package/dist/core/config/schema/primitives.js +0 -5
- package/dist/core/config/schema/search.js +2 -1
- package/dist/core/file-lock.js +2 -1
- package/dist/core/improve-result.js +35 -14
- package/dist/core/maintenance-barrier.js +2 -14
- package/dist/core/paths.js +0 -3
- package/dist/core/redaction.js +2 -2
- package/dist/core/spawn-env.js +8 -12
- package/dist/core/state/migrations.js +1 -12
- package/dist/core/state-db.js +9 -27
- package/dist/core/write-source.js +12 -19
- package/dist/execution/directory-identity.js +36 -10
- package/dist/execution/guarded-source.js +0 -10
- package/dist/indexer/graph/graph-boost.js +0 -4
- package/dist/indexer/index-writer-lock.js +43 -24
- package/dist/indexer/index-written-assets.js +5 -6
- package/dist/indexer/indexer.js +2 -39
- package/dist/indexer/materialize-embeddings.js +85 -41
- package/dist/indexer/passes/metadata.js +12 -4
- package/dist/indexer/scan/doc-to-entry.js +2 -0
- package/dist/indexer/search/db-search.js +15 -48
- package/dist/indexer/search/ranking-contributors.js +0 -25
- package/dist/indexer/search/ranking.js +3 -13
- package/dist/indexer/search/search-fields.js +16 -1
- package/dist/integrations/agent/builder-shared.js +0 -25
- package/dist/integrations/agent/model-map.js +2 -60
- package/dist/integrations/harnesses/aider/agent-builder.js +1 -3
- package/dist/integrations/harnesses/amazonq/agent-builder.js +2 -7
- package/dist/integrations/harnesses/claude/agent-builder.js +1 -4
- package/dist/integrations/harnesses/codex/agent-builder.js +1 -4
- package/dist/integrations/harnesses/copilot/agent-builder.js +2 -6
- package/dist/integrations/harnesses/gemini/agent-builder.js +2 -7
- package/dist/integrations/harnesses/opencode/agent-builder.js +1 -4
- package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +2 -16
- package/dist/integrations/harnesses/openhands/agent-builder.js +1 -3
- package/dist/integrations/harnesses/pi/agent-builder.js +2 -4
- package/dist/integrations/session-logs/index.js +0 -9
- package/dist/llm/client.js +75 -42
- package/dist/llm/embedder.js +7 -3
- package/dist/llm/embedders/remote.js +141 -42
- package/dist/output/shapes/passthrough.js +17 -5
- package/dist/registry/network.js +5 -37
- package/dist/runtime.js +2 -10
- package/dist/scripts/akm-migrate-node.js +180 -238
- package/dist/scripts/akm-migrate.js +180 -238
- package/dist/setup/engine-config.js +2 -5
- package/dist/setup/registry-stash-loader.js +0 -8
- package/dist/setup/setup.js +9 -46
- package/dist/setup/steps/connection-shared.js +10 -13
- package/dist/sources/providers/git-install.js +1 -1
- package/dist/storage/engines/sqlite-migrations.js +20 -1
- package/dist/storage/repositories/index-entries-repository.js +0 -15
- package/dist/storage/repositories/proposals-repository.js +32 -6
- package/dist/tasks/backends/launchd.js +15 -20
- package/dist/tasks/backends/schtasks.js +18 -8
- package/dist/tasks/run/run-native-task.js +8 -6
- package/dist/tasks/scheduler-binding.js +15 -5
- package/dist/tasks/scheduler-sync-preview.js +4 -2
- package/dist/tasks/scheduler-sync.js +77 -42
- package/dist/tasks/source/bounded-document.js +2 -4
- package/dist/tasks/source/task-source-v3-frozen.js +5 -7
- package/dist/tasks/source/task-source-v4.js +5 -10
- package/dist/tasks/source/task-to-v3.js +29 -22
- package/dist/tasks/source/task-to-v4.js +1 -14
- package/dist/tasks/source-v3.js +6 -6
- package/dist/workflows/exec/native-executor.js +21 -31
- package/dist/workflows/exec/run-workflow.js +5 -6
- package/dist/workflows/exec/scheduler.js +3 -19
- package/dist/workflows/exec/step-work.js +1 -4
- package/dist/workflows/exec/unit-dispatch.js +2 -2
- package/dist/workflows/exec/worktree.js +1 -13
- package/dist/workflows/freeze/targets/child-workflow.js +2 -10
- package/dist/workflows/ir/plan-hash.js +4 -6
- package/dist/workflows/ir/schema-v4.js +0 -12
- package/dist/workflows/ir/schema.js +20 -31
- package/dist/workflows/parser.js +11 -52
- package/dist/workflows/renderer.js +2 -3
- package/dist/workflows/resource-limits.js +11 -41
- package/dist/workflows/runtime/runs.js +3 -4
- package/dist/workflows/source-ir/schema.js +14 -30
- package/dist/workflows/validator.js +1 -7
- package/docs/reference/cli.md +25 -3
- package/docs/reference/configuration.md +28 -7
- package/docs/reference/tasks.md +10 -0
- package/package.json +1 -1
- package/schemas/akm-config.json +4 -23
- package/schemas/akm-task.json +1 -2
- package/schemas/akm-workflow.json +1 -13
- package/dist/indexer/search/semantic-status.js +0 -142
|
@@ -15,17 +15,9 @@
|
|
|
15
15
|
* - Cooperative cancellation via AbortSignal (workers stop claiming items;
|
|
16
16
|
* the same signal is passed into each dispatch so in-flight units can be
|
|
17
17
|
* preempted too).
|
|
18
|
-
*
|
|
19
|
-
* The lifetime unit cap ({@link LIFETIME_UNIT_CAP}) is DECLARED here but
|
|
20
|
-
* enforced per actual dispatch by the native executor: a pre-batch check
|
|
21
|
-
* (`journaled + items.length`) counted durable-row REUSES as new dispatches,
|
|
22
|
-
* which made any partially-completed fan-out with more than ~cap/2 journaled
|
|
23
|
-
* units impossible to resume (peer review R1). Only work that really
|
|
24
|
-
* dispatches consumes the cap.
|
|
25
18
|
*/
|
|
26
19
|
import { concurrentMap } from "../../core/concurrent.js";
|
|
27
20
|
import { cpuDerivedUnitConcurrency, workflowMaxConcurrency } from "../concurrency-policy.js";
|
|
28
|
-
import { WORKFLOW_MAX_MAP_EXPANSION } from "../resource-limits.js";
|
|
29
21
|
export { clampMaxConcurrency, cpuDerivedUnitConcurrency, WORKFLOW_MAX_CONCURRENCY_CEILING, } from "../concurrency-policy.js";
|
|
30
22
|
/**
|
|
31
23
|
* Hard ceiling on an EXPLICIT `workflow.maxConcurrency`. A user value above
|
|
@@ -49,21 +41,13 @@ export { clampMaxConcurrency, cpuDerivedUnitConcurrency, WORKFLOW_MAX_CONCURRENC
|
|
|
49
41
|
export function maxUnitConcurrency(cpuCount, configured) {
|
|
50
42
|
return workflowMaxConcurrency(configured, cpuCount);
|
|
51
43
|
}
|
|
52
|
-
/** Lifetime unit cap per run — a runaway-loop backstop, far above real use. */
|
|
53
|
-
export const LIFETIME_UNIT_CAP = WORKFLOW_MAX_MAP_EXPANSION;
|
|
54
|
-
export class UnitCapExceededError extends Error {
|
|
55
|
-
constructor(cap) {
|
|
56
|
-
super(`workflow run exceeded the lifetime unit cap (${cap}). Aborting dispatch — check for a runaway fan-out.`);
|
|
57
|
-
this.name = "UnitCapExceededError";
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
44
|
/**
|
|
61
45
|
* Run `dispatch` over `items` under the engine concurrency caps. Individual
|
|
62
46
|
* failures do not cancel siblings (allSettled semantics from `concurrentMap`);
|
|
63
47
|
* a slot whose dispatch threw or that was never claimed after an abort stays
|
|
64
|
-
* `undefined` in the result array.
|
|
65
|
-
* — the executor consumes it per actual dispatch, so
|
|
66
|
-
* resume stay free.
|
|
48
|
+
* `undefined` in the result array. A declared `budget.max_units` ceiling is
|
|
49
|
+
* NOT checked here — the executor consumes it per actual dispatch, so
|
|
50
|
+
* durable-row reuses on resume stay free.
|
|
67
51
|
*/
|
|
68
52
|
export async function scheduleUnits(items, dispatch, options = {}) {
|
|
69
53
|
const concurrency = Math.max(1, Math.min(options.concurrency ?? 1, options.maxConcurrency ?? Number.POSITIVE_INFINITY, options.llmConcurrency ?? Number.POSITIVE_INFINITY, options.hostConcurrency ?? cpuDerivedUnitConcurrency()));
|
|
@@ -22,7 +22,7 @@ import { canonicalInputJson, validateInputs } from "../../execution/input-contra
|
|
|
22
22
|
import { withWorkflowRunsRepo, } from "../../storage/repositories/workflow-runs-repository.js";
|
|
23
23
|
import { canonicalJson as canonicalJsonString } from "../ir/plan-hash.js";
|
|
24
24
|
import { parseReference, resolveReferenceString, } from "../program/expressions.js";
|
|
25
|
-
import { clip,
|
|
25
|
+
import { clip, WORKFLOW_UNIT_DIAGNOSTIC_CLIP } from "../resource-limits.js";
|
|
26
26
|
import { completeWorkflowStep, isTruncatedEvidence, } from "../runtime/runs.js";
|
|
27
27
|
import { GATE_EVALUATION_PHASE } from "../runtime/unit-phases.js";
|
|
28
28
|
import { parseJudgeVerdict } from "../validate-summary.js";
|
|
@@ -60,9 +60,6 @@ const GATE_ARTIFACT_CLIP = 4_000;
|
|
|
60
60
|
* deterministically, before dispatch.
|
|
61
61
|
*/
|
|
62
62
|
function validateFanOutItems(stepId, items) {
|
|
63
|
-
if (items.length > WORKFLOW_MAX_MAP_EXPANSION) {
|
|
64
|
-
return `Step "${stepId}" fan-out expands to ${items.length} units, exceeding the ${WORKFLOW_MAX_MAP_EXPANSION}-unit resource limit.`;
|
|
65
|
-
}
|
|
66
63
|
const nullIndex = items.findIndex((item) => item === null || item === undefined);
|
|
67
64
|
if (nullIndex !== -1) {
|
|
68
65
|
return (`Step "${stepId}" fan-out list contains a null item (index ${nullIndex}). ` +
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
3
|
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
4
|
import { ConfigError } from "../../core/errors.js";
|
|
5
|
-
import {
|
|
5
|
+
import { assertFrozenDirectoryContained } from "../../execution/directory-identity.js";
|
|
6
6
|
import { assertFrozenExecutableIdentity } from "../../execution/executable-identity.js";
|
|
7
7
|
import { canonicalResolvedExecutionRequest, decodeResolvedExecutionRequest, } from "../../execution/resolved-request.js";
|
|
8
8
|
import { dispatchLoweredExecutionRequest, lowerResolvedExecutionRequestWithRunner, } from "../../integrations/agent/execution-lowering.js";
|
|
@@ -11,7 +11,7 @@ import { getHarness } from "../../integrations/harnesses/index.js";
|
|
|
11
11
|
export function prepareWorkflowExecution(request, prompt = request.prompt) {
|
|
12
12
|
const target = request.frozenTarget;
|
|
13
13
|
if (target.cwdIdentity)
|
|
14
|
-
|
|
14
|
+
assertFrozenDirectoryContained(target.cwdIdentity);
|
|
15
15
|
if (target.executable)
|
|
16
16
|
assertFrozenExecutableIdentity(target.executable, `unit ${request.unitId} executable`);
|
|
17
17
|
const wire = JSON.parse(canonicalResolvedExecutionRequest(target.request));
|
|
@@ -67,7 +67,7 @@ import { spawnSync } from "node:child_process";
|
|
|
67
67
|
import fsp from "node:fs/promises";
|
|
68
68
|
import os from "node:os";
|
|
69
69
|
import path from "node:path";
|
|
70
|
-
import { isWithinAsync, safeRealpathAsync } from "../../core/common.js";
|
|
70
|
+
import { isProcessAlive, isWithinAsync, safeRealpathAsync } from "../../core/common.js";
|
|
71
71
|
import { serializeByKey } from "../../core/concurrent.js";
|
|
72
72
|
import { runManagedSubprocess } from "../../core/subprocess.js";
|
|
73
73
|
import { warn } from "../../core/warn.js";
|
|
@@ -417,18 +417,6 @@ async function isWorktreeLeaseLive(candidate) {
|
|
|
417
417
|
return false;
|
|
418
418
|
return isProcessAlive(lease.pid);
|
|
419
419
|
}
|
|
420
|
-
/** `kill(pid, 0)` liveness probe. EPERM proves the process exists but is not ours. */
|
|
421
|
-
function isProcessAlive(pid) {
|
|
422
|
-
if (typeof pid !== "number" || !Number.isInteger(pid) || pid <= 0)
|
|
423
|
-
return false;
|
|
424
|
-
try {
|
|
425
|
-
process.kill(pid, 0);
|
|
426
|
-
return true;
|
|
427
|
-
}
|
|
428
|
-
catch (err) {
|
|
429
|
-
return err.code === "EPERM";
|
|
430
|
-
}
|
|
431
|
-
}
|
|
432
420
|
// ── Garbage collection ──────────────────────────────────────────────────────
|
|
433
421
|
/**
|
|
434
422
|
* Drop the run-scoped root once its last unit worktree is gone. `rmdir`
|
|
@@ -31,7 +31,7 @@ import { UsageError } from "../../../core/errors.js";
|
|
|
31
31
|
import { GuardedExecutionSourceCollector } from "../../../execution/guarded-source.js";
|
|
32
32
|
import { workflowParamContract } from "../../ir/params.js";
|
|
33
33
|
import { canonicalJson, canonicalPlanJson } from "../../ir/plan-hash.js";
|
|
34
|
-
import { utf8Bytes,
|
|
34
|
+
import { utf8Bytes, WORKFLOW_MAX_EMBEDDED_CHILD_PLAN_BYTES } from "../../resource-limits.js";
|
|
35
35
|
import { loadWorkflowAsset } from "../../runtime/workflow-asset-loader.js";
|
|
36
36
|
import { resolveOwnedAsset } from "../environment.js";
|
|
37
37
|
import { declaredParamNames, earlierStepIds } from "../step-values.js";
|
|
@@ -91,13 +91,6 @@ function assertNoCompositionCycle(stepId, childRef, refPath) {
|
|
|
91
91
|
`Path: ${compositionPath(refPath, childRef)}.`, "COMPOSITION_INVALID", "Break the cycle: remove or redirect one of the compositions in the path above so no workflow ends up " +
|
|
92
92
|
"composing itself, directly or through intermediates.");
|
|
93
93
|
}
|
|
94
|
-
function assertCompositionDepthAllowed(stepId, childRef, childDepth, refPath) {
|
|
95
|
-
if (childDepth <= WORKFLOW_MAX_COMPOSITION_DEPTH)
|
|
96
|
-
return;
|
|
97
|
-
throw new UsageError(`Workflow step ${stepId} cannot compose ${childRef}: workflow composition is limited to ` +
|
|
98
|
-
`${WORKFLOW_MAX_COMPOSITION_DEPTH} levels. Path: ${compositionPath(refPath, childRef)}.`, "COMPOSITION_INVALID", `Flatten the composition chain to ${WORKFLOW_MAX_COMPOSITION_DEPTH} levels or fewer — inline one of the ` +
|
|
99
|
-
"intermediate workflows, or restructure the chain so fewer child compositions are nested.");
|
|
100
|
-
}
|
|
101
94
|
/**
|
|
102
95
|
* Add the child's embedded plan bytes to the shared, tree-wide budget
|
|
103
96
|
* (A-N6) and fail before publication if the AGGREGATE crosses the cap.
|
|
@@ -126,10 +119,9 @@ export async function childWorkflowDispatch(input) {
|
|
|
126
119
|
// path to reach the child run and must reject, not vanish (see
|
|
127
120
|
// assertNoStepEnvironment's doc comment).
|
|
128
121
|
assertNoStepEnvironment(source.id, childRef, source);
|
|
129
|
-
// §4.2
|
|
122
|
+
// §4.2 step 2: the composition cycle check, before any child compilation.
|
|
130
123
|
assertNoCompositionCycle(source.id, childRef, context.composition.refPath);
|
|
131
124
|
const childDepth = context.composition.depth + 1;
|
|
132
|
-
assertCompositionDepthAllowed(source.id, childRef, childDepth, context.composition.refPath);
|
|
133
125
|
// §4.2 step 4: freeze the child COMPLETELY (compile -> validate -> freeze),
|
|
134
126
|
// with its OWN fresh collector (A-N7) so its plan is a pure function of its
|
|
135
127
|
// own source, and the SAME mutable budget object so the aggregate bound
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* Pure module: no IO beyond node:crypto, no engine imports.
|
|
14
14
|
*/
|
|
15
15
|
import { createHash } from "node:crypto";
|
|
16
|
-
import { utf8Bytes,
|
|
16
|
+
import { utf8Bytes, WORKFLOW_MAX_PLAN_BYTES } from "../resource-limits.js";
|
|
17
17
|
import { decodeWorkflowPlanV4, WORKFLOW_IR_V5_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) {
|
|
@@ -53,9 +53,7 @@ export function decodeCanonicalPlan(runId, planJson, planHash, expectedVersion)
|
|
|
53
53
|
throw new Error(`Workflow run ${runId} has noncanonical frozen plan JSON.`);
|
|
54
54
|
return plan;
|
|
55
55
|
}
|
|
56
|
-
function sortKeys(value
|
|
57
|
-
if (depth > WORKFLOW_MAX_JSON_DEPTH)
|
|
58
|
-
throw new TypeError("JSON value exceeds maximum depth");
|
|
56
|
+
function sortKeys(value) {
|
|
59
57
|
if (value === null || typeof value === "string" || typeof value === "boolean")
|
|
60
58
|
return value;
|
|
61
59
|
if (typeof value === "number") {
|
|
@@ -64,11 +62,11 @@ function sortKeys(value, depth = 0) {
|
|
|
64
62
|
return value;
|
|
65
63
|
}
|
|
66
64
|
if (Array.isArray(value))
|
|
67
|
-
return value.map((item) => sortKeys(item
|
|
65
|
+
return value.map((item) => sortKeys(item));
|
|
68
66
|
if (value && typeof value === "object") {
|
|
69
67
|
return Object.fromEntries(Object.entries(value)
|
|
70
68
|
.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
|
|
71
|
-
.map(([k, v]) => [k, sortKeys(v
|
|
69
|
+
.map(([k, v]) => [k, sortKeys(v)]));
|
|
72
70
|
}
|
|
73
71
|
throw new TypeError("JSON value contains a non-JSON value");
|
|
74
72
|
}
|
|
@@ -182,15 +182,6 @@ function decodeFrozenTarget(value, unit, environment, requiredSources, depth, bu
|
|
|
182
182
|
return decodeChildWorkflowTarget(target, unit, depth, budget);
|
|
183
183
|
fail(`unit ${unit.id} frozenTarget has unsupported kind ${String(target.kind)}`);
|
|
184
184
|
}
|
|
185
|
-
/**
|
|
186
|
-
* Composition-depth corruption bound (spec §3.6 step 6, §4.5, row A-23).
|
|
187
|
-
* Mirrors `WORKFLOW_MAX_COMPOSITION_DEPTH` (= 8), which Lane B adds to
|
|
188
|
-
* `src/workflows/resource-limits.ts` in a later commit as the freeze-time
|
|
189
|
-
* `COMPOSITION_INVALID` gate. Reproduced here — the decode-time corruption
|
|
190
|
-
* gate — rather than imported across the lane boundary, matching
|
|
191
|
-
* `tests/workflows/plan-v5-schema.test.ts`'s own header comment.
|
|
192
|
-
*/
|
|
193
|
-
const CHILD_WORKFLOW_DECODE_MAX_DEPTH = 8;
|
|
194
185
|
/**
|
|
195
186
|
* Decode a `kind: "child-workflow"` frozen target (§3.5): the embedded
|
|
196
187
|
* COMPLETE child plan, re-verified against its own `planHash` and this
|
|
@@ -241,9 +232,6 @@ function decodeChildWorkflowTarget(target, unit, depth, budget) {
|
|
|
241
232
|
fail(`unit ${unit.id} child workflow contentHash does not match its frozen dispatch`);
|
|
242
233
|
}
|
|
243
234
|
const childDepth = depth + 1;
|
|
244
|
-
if (childDepth > CHILD_WORKFLOW_DECODE_MAX_DEPTH) {
|
|
245
|
-
fail(`unit ${unit.id} child workflow ${target.ref} exceeds the max composition depth of ${CHILD_WORKFLOW_DECODE_MAX_DEPTH}`);
|
|
246
|
-
}
|
|
247
235
|
const frozenPlan = decodeWorkflowPlanV4(target.frozenPlan, {}, childDepth, budget);
|
|
248
236
|
const embeddedPlanJson = canonicalJsonLocal(frozenPlan);
|
|
249
237
|
const actualPlanHash = sha256(embeddedPlanJson);
|
|
@@ -5,12 +5,11 @@ import { isContainedRelativePath } from "../../core/common.js";
|
|
|
5
5
|
import { UsageError } from "../../core/errors.js";
|
|
6
6
|
import { parseReference } from "../program/expressions.js";
|
|
7
7
|
import { PROGRAM_PARAM_NAME_PATTERN, PROGRAM_RETRY_REASONS, PROGRAM_STEP_ID_PATTERN } from "../program/schema.js";
|
|
8
|
-
import { jsonBytes, utf8Bytes, WORKFLOW_ENV_VAR_NAME_PATTERN, WORKFLOW_MAX_CONCURRENCY,
|
|
8
|
+
import { jsonBytes, utf8Bytes, WORKFLOW_ENV_VAR_NAME_PATTERN, WORKFLOW_MAX_CONCURRENCY, WORKFLOW_MAX_INSTRUCTION_BYTES, WORKFLOW_MAX_PLAN_BYTES, WORKFLOW_MAX_SCHEMA_BYTES, WORKFLOW_MAX_TIMEOUT_MS, } from "../resource-limits.js";
|
|
9
9
|
// Shared dispatch-significant bounds now live in `../resource-limits` so the
|
|
10
10
|
// parser, the published JSON Schema, and this decoder enforce identical
|
|
11
11
|
// values. Re-exported here for existing importers (e.g. `commands/workflow-cli.ts`).
|
|
12
|
-
export { WORKFLOW_MAX_CONCURRENCY,
|
|
13
|
-
export const WORKFLOW_MAX_UNITS = WORKFLOW_MAX_MAP_EXPANSION;
|
|
12
|
+
export { WORKFLOW_MAX_CONCURRENCY, WORKFLOW_MAX_TIMEOUT_MS };
|
|
14
13
|
const MAX_LIST_ITEMS = 1024;
|
|
15
14
|
const MAX_STRING_LENGTH = 1_000_000;
|
|
16
15
|
/** Validate the current durable execution graph. */
|
|
@@ -33,8 +32,8 @@ export function validateWorkflowPlanStructure(input, options, hooks = {}) {
|
|
|
33
32
|
fail(`execution.maxConcurrency must be an integer from 1 through ${WORKFLOW_MAX_CONCURRENCY}`);
|
|
34
33
|
}
|
|
35
34
|
assertKeys(plan.execution, ["maxConcurrency"], "execution");
|
|
36
|
-
if (!Array.isArray(plan.steps) || plan.steps.length === 0
|
|
37
|
-
fail("steps must
|
|
35
|
+
if (!Array.isArray(plan.steps) || plan.steps.length === 0)
|
|
36
|
+
fail("steps must be a non-empty array");
|
|
38
37
|
const stepIds = new Set();
|
|
39
38
|
const nodeIds = new Set();
|
|
40
39
|
for (let index = 0; index < plan.steps.length; index++) {
|
|
@@ -126,7 +125,7 @@ function validateNode(node, stepId, nodeIds, unitExtraKeys) {
|
|
|
126
125
|
validateSchema(node.schema, `unit ${node.id} schema`);
|
|
127
126
|
validateRetry(node.retry, node.id);
|
|
128
127
|
validateStringArray(node.env, `unit ${node.id} env`, MAX_LIST_ITEMS, true);
|
|
129
|
-
validateStringArray(node.inputs, `unit ${node.id} inputs`,
|
|
128
|
+
validateStringArray(node.inputs, `unit ${node.id} inputs`, Infinity, true);
|
|
130
129
|
validateSource(node.source, `unit ${node.id} source`);
|
|
131
130
|
}
|
|
132
131
|
/**
|
|
@@ -143,15 +142,11 @@ function validateExecSpec(value, label) {
|
|
|
143
142
|
const command = value.command;
|
|
144
143
|
if (!Array.isArray(command) ||
|
|
145
144
|
command.length === 0 ||
|
|
146
|
-
command.length >
|
|
147
|
-
|
|
148
|
-
fail(`${label}.command must be an argv array of 1 through ${WORKFLOW_MAX_EXEC_ARGV} bounded non-empty strings`);
|
|
145
|
+
!command.every((arg) => typeof arg === "string" && arg.length > 0)) {
|
|
146
|
+
fail(`${label}.command must be an argv array of non-empty strings`);
|
|
149
147
|
}
|
|
150
148
|
if (value.cwd !== undefined) {
|
|
151
|
-
if (typeof value.cwd !== "string" ||
|
|
152
|
-
value.cwd.length === 0 ||
|
|
153
|
-
value.cwd.length > WORKFLOW_MAX_EXEC_CWD_LENGTH ||
|
|
154
|
-
!isContainedRelativePath(value.cwd)) {
|
|
149
|
+
if (typeof value.cwd !== "string" || value.cwd.length === 0 || !isContainedRelativePath(value.cwd)) {
|
|
155
150
|
fail(`${label}.cwd must be a relative path contained in the unit working directory`);
|
|
156
151
|
}
|
|
157
152
|
}
|
|
@@ -183,11 +178,10 @@ function validateExecEnvScope(value, label) {
|
|
|
183
178
|
const passEnv = value.passEnv;
|
|
184
179
|
if (!Array.isArray(passEnv) ||
|
|
185
180
|
passEnv.length === 0 ||
|
|
186
|
-
passEnv.length > WORKFLOW_MAX_EXEC_PASS_ENV ||
|
|
187
181
|
!passEnv.every((name) => typeof name === "string" && WORKFLOW_ENV_VAR_NAME_PATTERN.test(name)) ||
|
|
188
182
|
new Set(passEnv).size !== passEnv.length) {
|
|
189
|
-
fail(`${label}.passEnv must be
|
|
190
|
-
|
|
183
|
+
fail(`${label}.passEnv must be a non-empty list of distinct environment variable names matching ` +
|
|
184
|
+
`${WORKFLOW_ENV_VAR_NAME_PATTERN.source}`);
|
|
191
185
|
}
|
|
192
186
|
}
|
|
193
187
|
function validateGate(gate, stepId, nodeIds, gateExtraKeys) {
|
|
@@ -199,8 +193,7 @@ function validateGate(gate, stepId, nodeIds, gateExtraKeys) {
|
|
|
199
193
|
gate.criteria.length > MAX_LIST_ITEMS ||
|
|
200
194
|
!gate.criteria.every((x) => typeof x === "string" && x.length > 0 && x.length <= MAX_STRING_LENGTH) ||
|
|
201
195
|
!Number.isInteger(gate.maxLoops) ||
|
|
202
|
-
gate.maxLoops < 1
|
|
203
|
-
gate.maxLoops > WORKFLOW_MAX_GATE_LOOPS)
|
|
196
|
+
gate.maxLoops < 1)
|
|
204
197
|
fail(`gate for step ${stepId} is invalid`);
|
|
205
198
|
if (nodeIds.has(gate.id))
|
|
206
199
|
fail(`gate id ${gate.id} collides with a node`);
|
|
@@ -213,7 +206,6 @@ function validateRoute(route, stepId) {
|
|
|
213
206
|
!route.input ||
|
|
214
207
|
!isRecord(route.when) ||
|
|
215
208
|
Object.keys(route.when).length === 0 ||
|
|
216
|
-
Object.keys(route.when).length > WORKFLOW_MAX_ROUTE_BRANCHES ||
|
|
217
209
|
!Object.keys(route.when).every((match) => match.length > 0 && match.length <= MAX_STRING_LENGTH) ||
|
|
218
210
|
!Object.values(route.when).every((target) => typeof target === "string" && target))
|
|
219
211
|
fail(`route for step ${stepId} is invalid`);
|
|
@@ -226,9 +218,7 @@ function assertKeys(value, allowed, label) {
|
|
|
226
218
|
if (!allowed.includes(key))
|
|
227
219
|
fail(`${label} contains unknown key ${key}`);
|
|
228
220
|
}
|
|
229
|
-
function assertJson(value
|
|
230
|
-
if (depth > WORKFLOW_MAX_JSON_DEPTH)
|
|
231
|
-
fail("plan exceeds JSON depth limit of 64");
|
|
221
|
+
function assertJson(value) {
|
|
232
222
|
if (value === null || typeof value === "boolean")
|
|
233
223
|
return;
|
|
234
224
|
if (typeof value === "string") {
|
|
@@ -243,23 +233,23 @@ function assertJson(value, depth = 0) {
|
|
|
243
233
|
}
|
|
244
234
|
if (Array.isArray(value)) {
|
|
245
235
|
for (const item of value)
|
|
246
|
-
assertJson(item
|
|
236
|
+
assertJson(item);
|
|
247
237
|
return;
|
|
248
238
|
}
|
|
249
239
|
if (isRecord(value)) {
|
|
250
240
|
for (const item of Object.values(value))
|
|
251
|
-
assertJson(item
|
|
241
|
+
assertJson(item);
|
|
252
242
|
return;
|
|
253
243
|
}
|
|
254
244
|
fail("plan contains a non-JSON value");
|
|
255
245
|
}
|
|
256
246
|
function validateParams(params, schemas) {
|
|
257
|
-
validateStringArray(params, "params",
|
|
247
|
+
validateStringArray(params, "params", Infinity, true);
|
|
258
248
|
if (params?.some((name) => !PROGRAM_PARAM_NAME_PATTERN.test(name)))
|
|
259
249
|
fail("params contains an invalid name");
|
|
260
250
|
if (schemas !== undefined) {
|
|
261
|
-
if (!isRecord(schemas)
|
|
262
|
-
fail("paramSchemas must be
|
|
251
|
+
if (!isRecord(schemas))
|
|
252
|
+
fail("paramSchemas must be an object");
|
|
263
253
|
for (const [name, schema] of Object.entries(schemas)) {
|
|
264
254
|
if (!PROGRAM_PARAM_NAME_PATTERN.test(name))
|
|
265
255
|
fail(`paramSchemas.${name} is invalid`);
|
|
@@ -284,9 +274,8 @@ function validateBudget(budget) {
|
|
|
284
274
|
fail("budget must declare a ceiling");
|
|
285
275
|
validateOptionalPositiveInteger(budget.maxTokens, "budget.maxTokens");
|
|
286
276
|
const maxUnits = budget.maxUnits;
|
|
287
|
-
if (maxUnits !== undefined &&
|
|
288
|
-
(
|
|
289
|
-
fail(`budget.maxUnits must be an integer from 1 through ${WORKFLOW_MAX_UNITS}`);
|
|
277
|
+
if (maxUnits !== undefined && (!Number.isSafeInteger(maxUnits) || maxUnits < 1))
|
|
278
|
+
fail("budget.maxUnits must be a positive integer");
|
|
290
279
|
}
|
|
291
280
|
function validateRetry(retry, nodeId) {
|
|
292
281
|
if (retry === undefined)
|
|
@@ -294,7 +283,7 @@ function validateRetry(retry, nodeId) {
|
|
|
294
283
|
if (!isRecord(retry))
|
|
295
284
|
fail(`unit ${nodeId} retry must be an object`);
|
|
296
285
|
assertKeys(retry, ["max", "on"], `unit ${nodeId} retry`);
|
|
297
|
-
if (!Number.isSafeInteger(retry.max) || retry.max < 0
|
|
286
|
+
if (!Number.isSafeInteger(retry.max) || retry.max < 0)
|
|
298
287
|
fail(`unit ${nodeId} retry.max is invalid`);
|
|
299
288
|
validateStringArray(retry.on, `unit ${nodeId} retry.on`, PROGRAM_RETRY_REASONS.length, true);
|
|
300
289
|
if (retry.on === undefined ||
|
package/dist/workflows/parser.js
CHANGED
|
@@ -38,7 +38,7 @@ import { formatExtraParamsIssue, validateExtraParams } from "../core/extra-param
|
|
|
38
38
|
import { checkJsonSchemaDefinition, JSON_SCHEMA_SUBSET_SUPPORTED_KEYWORDS } from "../core/json-schema.js";
|
|
39
39
|
import { parseReference } from "./program/expressions.js";
|
|
40
40
|
import { PROGRAM_ISOLATION_KINDS, PROGRAM_ON_ERROR, PROGRAM_PARAM_NAME_PATTERN, PROGRAM_REDUCERS, PROGRAM_RETRY_REASONS, PROGRAM_STEP_ID_PATTERN, } from "./program/schema.js";
|
|
41
|
-
import { jsonBytes, utf8Bytes, WORKFLOW_ENGINE_NAME_PATTERN, WORKFLOW_ENV_VAR_NAME_PATTERN, WORKFLOW_MAX_CONCURRENCY, WORKFLOW_MAX_ENGINE_NAME_LENGTH,
|
|
41
|
+
import { jsonBytes, utf8Bytes, WORKFLOW_ENGINE_NAME_PATTERN, WORKFLOW_ENV_VAR_NAME_PATTERN, WORKFLOW_MAX_CONCURRENCY, WORKFLOW_MAX_ENGINE_NAME_LENGTH, WORKFLOW_MAX_EXTRA_PARAMS_BYTES, WORKFLOW_MAX_SCHEMA_BYTES, WORKFLOW_MAX_SOURCE_BYTES, WORKFLOW_MAX_TIMEOUT_MS, } from "./resource-limits.js";
|
|
42
42
|
import { WORKFLOW_SCHEMA_VERSION, } from "./schema.js";
|
|
43
43
|
import { runSemanticChecks } from "./validator.js";
|
|
44
44
|
/** Envelope keys every AKM markdown asset carries ($ref'd from schemas/akm-asset-envelope.json). */
|
|
@@ -406,9 +406,6 @@ function parseParams(ctx, raw) {
|
|
|
406
406
|
ctx.err(["params"], `"params" must be a mapping of param name to a JSON Schema object (e.g. changed_files: { type: array }).`);
|
|
407
407
|
return undefined;
|
|
408
408
|
}
|
|
409
|
-
if (Object.keys(raw).length > WORKFLOW_MAX_PARAMS) {
|
|
410
|
-
ctx.err(["params"], `"params" must contain at most ${WORKFLOW_MAX_PARAMS} entries.`);
|
|
411
|
-
}
|
|
412
409
|
const params = {};
|
|
413
410
|
for (const [paramName, value] of Object.entries(raw)) {
|
|
414
411
|
if (!PROGRAM_PARAM_NAME_PATTERN.test(paramName)) {
|
|
@@ -448,9 +445,6 @@ function parseOutputs(ctx, raw) {
|
|
|
448
445
|
ctx.err(["outputs"], `"outputs" must be a mapping of output name to { from, schema? } (e.g. report: { from: steps.summarize.output }).`);
|
|
449
446
|
return undefined;
|
|
450
447
|
}
|
|
451
|
-
if (Object.keys(raw).length > WORKFLOW_MAX_OUTPUTS) {
|
|
452
|
-
ctx.err(["outputs"], `"outputs" must contain at most ${WORKFLOW_MAX_OUTPUTS} entries.`);
|
|
453
|
-
}
|
|
454
448
|
const outputs = {};
|
|
455
449
|
for (const [outputName, value] of Object.entries(raw)) {
|
|
456
450
|
const path = ["outputs", outputName];
|
|
@@ -546,14 +540,11 @@ function parseBudget(ctx, raw) {
|
|
|
546
540
|
}
|
|
547
541
|
}
|
|
548
542
|
if (raw.max_units !== undefined) {
|
|
549
|
-
if (typeof raw.max_units === "number" &&
|
|
550
|
-
Number.isInteger(raw.max_units) &&
|
|
551
|
-
raw.max_units >= 1 &&
|
|
552
|
-
raw.max_units <= WORKFLOW_MAX_MAP_EXPANSION) {
|
|
543
|
+
if (typeof raw.max_units === "number" && Number.isInteger(raw.max_units) && raw.max_units >= 1) {
|
|
553
544
|
budget.maxUnits = raw.max_units;
|
|
554
545
|
}
|
|
555
546
|
else {
|
|
556
|
-
ctx.err([...path, "max_units"], `"budget.max_units" must be an integer
|
|
547
|
+
ctx.err([...path, "max_units"], `"budget.max_units" must be an integer >= 1.`);
|
|
557
548
|
}
|
|
558
549
|
}
|
|
559
550
|
return Object.keys(budget).length > 0 ? budget : undefined;
|
|
@@ -563,9 +554,6 @@ function parseSteps(ctx, raw) {
|
|
|
563
554
|
ctx.err(["steps"], `"steps" is required and must be a list with at least one step.`);
|
|
564
555
|
return [];
|
|
565
556
|
}
|
|
566
|
-
if (raw.length > WORKFLOW_MAX_STEPS) {
|
|
567
|
-
ctx.err(["steps"], `"steps" must contain at most ${WORKFLOW_MAX_STEPS} entries.`);
|
|
568
|
-
}
|
|
569
557
|
// First pass: collect ids so route targets can be checked against ALL steps
|
|
570
558
|
// (including ones that fail their own validation).
|
|
571
559
|
const idIndex = new Map();
|
|
@@ -775,11 +763,6 @@ function parseExecPassEnv(ctx, raw, path, stepLabel) {
|
|
|
775
763
|
`akm's own environment, e.g. pass_env: [CARGO_HOME]. Values never appear here — use "env:" bindings for those.`);
|
|
776
764
|
return undefined;
|
|
777
765
|
}
|
|
778
|
-
if (raw.length > WORKFLOW_MAX_EXEC_PASS_ENV) {
|
|
779
|
-
ctx.err(path, `${stepLabel} "exec.pass_env" must have at most ${WORKFLOW_MAX_EXEC_PASS_ENV} entries. A command needing ` +
|
|
780
|
-
`more than that should use explicit named "env:" bindings instead.`);
|
|
781
|
-
return undefined;
|
|
782
|
-
}
|
|
783
766
|
const names = [];
|
|
784
767
|
for (const [index, entry] of raw.entries()) {
|
|
785
768
|
if (typeof entry !== "string" || !WORKFLOW_ENV_VAR_NAME_PATTERN.test(entry)) {
|
|
@@ -795,17 +778,13 @@ function parseExecPassEnv(ctx, raw, path, stepLabel) {
|
|
|
795
778
|
}
|
|
796
779
|
return names;
|
|
797
780
|
}
|
|
798
|
-
/** The argv array itself:
|
|
781
|
+
/** The argv array itself: a non-empty list of non-empty strings. */
|
|
799
782
|
function parseExecCommand(ctx, raw, path, stepLabel) {
|
|
800
783
|
if (!Array.isArray(raw) || raw.length === 0) {
|
|
801
784
|
ctx.err(path, `${stepLabel} "exec" requires "command": a non-empty argv list, e.g. command: ["bun", "run", "test:unit"]. ` +
|
|
802
785
|
`A single shell string is not accepted — the command is spawned directly, never through a shell.`);
|
|
803
786
|
return undefined;
|
|
804
787
|
}
|
|
805
|
-
if (raw.length > WORKFLOW_MAX_EXEC_ARGV) {
|
|
806
|
-
ctx.err(path, `${stepLabel} "exec.command" must have at most ${WORKFLOW_MAX_EXEC_ARGV} entries.`);
|
|
807
|
-
return undefined;
|
|
808
|
-
}
|
|
809
788
|
const argv = [];
|
|
810
789
|
for (const [index, entry] of raw.entries()) {
|
|
811
790
|
if (typeof entry !== "string" || entry === "") {
|
|
@@ -816,10 +795,6 @@ function parseExecCommand(ctx, raw, path, stepLabel) {
|
|
|
816
795
|
ctx.err([...path, index], `${stepLabel} "exec.command[${index}]" may not contain NUL bytes.`);
|
|
817
796
|
return undefined;
|
|
818
797
|
}
|
|
819
|
-
if (utf8Bytes(entry) > WORKFLOW_MAX_EXEC_ARG_BYTES) {
|
|
820
|
-
ctx.err(path, `${stepLabel} "exec.command[${index}]" exceeds ${WORKFLOW_MAX_EXEC_ARG_BYTES} bytes.`);
|
|
821
|
-
return undefined;
|
|
822
|
-
}
|
|
823
798
|
argv.push(entry);
|
|
824
799
|
}
|
|
825
800
|
return argv;
|
|
@@ -837,10 +812,6 @@ function parseExecCwd(ctx, raw, path, stepLabel) {
|
|
|
837
812
|
return undefined;
|
|
838
813
|
}
|
|
839
814
|
const value = raw.trim();
|
|
840
|
-
if (value.length > WORKFLOW_MAX_EXEC_CWD_LENGTH) {
|
|
841
|
-
ctx.err(path, `${stepLabel} "exec.cwd" exceeds ${WORKFLOW_MAX_EXEC_CWD_LENGTH} characters.`);
|
|
842
|
-
return undefined;
|
|
843
|
-
}
|
|
844
815
|
if (!isContainedRelativePath(value)) {
|
|
845
816
|
ctx.err(path, `${stepLabel} "exec.cwd" (${JSON.stringify(value)}) must be a RELATIVE path inside the unit's working ` +
|
|
846
817
|
`directory — absolute paths, Windows drive letters, "~", and ".." segments are rejected.`);
|
|
@@ -876,14 +847,11 @@ function parseMap(ctx, raw, path, stepLabel) {
|
|
|
876
847
|
}
|
|
877
848
|
let concurrency;
|
|
878
849
|
if (raw.concurrency !== undefined) {
|
|
879
|
-
if (typeof raw.concurrency === "number" &&
|
|
880
|
-
|
|
881
|
-
raw.concurrency > 0 &&
|
|
882
|
-
raw.concurrency <= WORKFLOW_MAX_CONCURRENCY) {
|
|
883
|
-
concurrency = raw.concurrency;
|
|
850
|
+
if (typeof raw.concurrency === "number" && Number.isInteger(raw.concurrency) && raw.concurrency > 0) {
|
|
851
|
+
concurrency = Math.min(raw.concurrency, WORKFLOW_MAX_CONCURRENCY);
|
|
884
852
|
}
|
|
885
853
|
else {
|
|
886
|
-
ctx.err([...path, "concurrency"], `${stepLabel} "concurrency" must be
|
|
854
|
+
ctx.err([...path, "concurrency"], `${stepLabel} "concurrency" must be a positive integer.`);
|
|
887
855
|
}
|
|
888
856
|
}
|
|
889
857
|
const reducer = parseEnumField(ctx, raw.reducer, [...path, "reducer"], `${stepLabel} "reducer"`, PROGRAM_REDUCERS);
|
|
@@ -917,9 +885,6 @@ function parseRoute(ctx, raw, path, stepLabel, stepIndex, routeChecks) {
|
|
|
917
885
|
ctx.err(whenPath, `${stepLabel} "route" requires "when": a non-empty list of { match, step } branches (e.g. when: [{ match: pass, step: ship }]).`);
|
|
918
886
|
}
|
|
919
887
|
else {
|
|
920
|
-
if (raw.when.length > WORKFLOW_MAX_ROUTE_BRANCHES) {
|
|
921
|
-
ctx.err(whenPath, `${stepLabel} "when" must contain at most ${WORKFLOW_MAX_ROUTE_BRANCHES} branches.`);
|
|
922
|
-
}
|
|
923
888
|
const seenMatches = new Map();
|
|
924
889
|
raw.when.forEach((branch, i) => {
|
|
925
890
|
const branchPath = [...whenPath, i];
|
|
@@ -980,9 +945,6 @@ function parseInputs(ctx, raw, path, stepLabel) {
|
|
|
980
945
|
ctx.err(path, `${stepLabel} "inputs" must be a non-empty list of reference strings.`);
|
|
981
946
|
return undefined;
|
|
982
947
|
}
|
|
983
|
-
if (raw.length > WORKFLOW_MAX_INPUTS) {
|
|
984
|
-
ctx.err(path, `${stepLabel} "inputs" must contain at most ${WORKFLOW_MAX_INPUTS} entries.`);
|
|
985
|
-
}
|
|
986
948
|
const out = [];
|
|
987
949
|
// The frozen-plan decoder requires uniqueness (validateStringArray(..., true)),
|
|
988
950
|
// so duplicates parsed and linted clean and then failed the run with an
|
|
@@ -1012,14 +974,11 @@ function parseGate(ctx, raw, path, stepLabel) {
|
|
|
1012
974
|
checkUnknownKeys(ctx, raw, path, GATE_KEYS, `${stepLabel} "gate"`);
|
|
1013
975
|
const gate = {};
|
|
1014
976
|
if (raw.max_loops !== undefined) {
|
|
1015
|
-
if (typeof raw.max_loops === "number" &&
|
|
1016
|
-
Number.isInteger(raw.max_loops) &&
|
|
1017
|
-
raw.max_loops >= 1 &&
|
|
1018
|
-
raw.max_loops <= WORKFLOW_MAX_GATE_LOOPS) {
|
|
977
|
+
if (typeof raw.max_loops === "number" && Number.isInteger(raw.max_loops) && raw.max_loops >= 1) {
|
|
1019
978
|
gate.maxLoops = raw.max_loops;
|
|
1020
979
|
}
|
|
1021
980
|
else {
|
|
1022
|
-
ctx.err([...path, "max_loops"], `${stepLabel} "gate.max_loops" must be an integer
|
|
981
|
+
ctx.err([...path, "max_loops"], `${stepLabel} "gate.max_loops" must be an integer of at least 1.`);
|
|
1023
982
|
}
|
|
1024
983
|
}
|
|
1025
984
|
return gate;
|
|
@@ -1056,8 +1015,8 @@ function parseRetry(ctx, raw, path, stepLabel) {
|
|
|
1056
1015
|
}
|
|
1057
1016
|
checkUnknownKeys(ctx, raw, path, RETRY_KEYS, `${stepLabel} "retry"`);
|
|
1058
1017
|
let ok = true;
|
|
1059
|
-
if (!(typeof raw.max === "number" && Number.isInteger(raw.max) && raw.max >= 0
|
|
1060
|
-
ctx.err([...path, "max"], `${stepLabel} "retry.max" is required and must be
|
|
1018
|
+
if (!(typeof raw.max === "number" && Number.isInteger(raw.max) && raw.max >= 0)) {
|
|
1019
|
+
ctx.err([...path, "max"], `${stepLabel} "retry.max" is required and must be a non-negative integer.`);
|
|
1061
1020
|
ok = false;
|
|
1062
1021
|
}
|
|
1063
1022
|
const on = [];
|
|
@@ -87,9 +87,8 @@ function projectParameters(sourceIr) {
|
|
|
87
87
|
* visible in the workflow file `show` is rendering, and (b) pointless to clip:
|
|
88
88
|
* the whole finding this projection answers is `show` describing something
|
|
89
89
|
* other than what runs, and a truncated argv is that same bug in miniature.
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
* `instructions` this same projection already carries whole.
|
|
90
|
+
* It is far smaller than the step `instructions` this same projection
|
|
91
|
+
* already carries whole.
|
|
93
92
|
*/
|
|
94
93
|
function summarizeStepOrchestration(step, defaults) {
|
|
95
94
|
const unit = step.unit;
|
|
@@ -4,22 +4,12 @@
|
|
|
4
4
|
import { EXECUTION_MAX_TIMEOUT_MS } from "../execution/limits.js";
|
|
5
5
|
export const WORKFLOW_MAX_PLAN_BYTES = 2 * 1024 * 1024;
|
|
6
6
|
export const WORKFLOW_MAX_SOURCE_BYTES = 1024 * 1024;
|
|
7
|
-
export const WORKFLOW_MAX_STEPS = 256;
|
|
8
|
-
export const WORKFLOW_MAX_ENGINES = 64;
|
|
9
|
-
export const WORKFLOW_MAX_PARAMS = 128;
|
|
10
|
-
export const WORKFLOW_MAX_ROUTE_BRANCHES = 256;
|
|
11
7
|
export const WORKFLOW_MAX_INSTRUCTION_BYTES = 256 * 1024;
|
|
12
8
|
export const WORKFLOW_MAX_SCHEMA_BYTES = 256 * 1024;
|
|
13
9
|
export const WORKFLOW_MAX_EXTRA_PARAMS_BYTES = 64 * 1024;
|
|
14
|
-
export const WORKFLOW_MAX_JSON_DEPTH = 64;
|
|
15
|
-
export const WORKFLOW_MAX_MAP_EXPANSION = 10_000;
|
|
16
|
-
/** Max declared `inputs:` reference strings on one unit/map step. */
|
|
17
|
-
export const WORKFLOW_MAX_INPUTS = 64;
|
|
18
|
-
/** Max declared `outputs:` entries on one workflow document (P3b, spec §4.1). */
|
|
19
|
-
export const WORKFLOW_MAX_OUTPUTS = 64;
|
|
20
10
|
// ── Dispatch-significant bounds shared across validation layers ──────────────
|
|
21
11
|
//
|
|
22
|
-
// Defined ONCE here so the
|
|
12
|
+
// Defined ONCE here so the enforcement layers cannot drift:
|
|
23
13
|
// 1. the parser (`../parser.ts`) — line-anchored authoring-time errors,
|
|
24
14
|
// 2. the published JSON Schema (`schemas/akm-workflow.json`) — mirrored
|
|
25
15
|
// `maximum`/`pattern`/`maxLength` values, pinned against these constants
|
|
@@ -31,33 +21,12 @@ export const WORKFLOW_MAX_OUTPUTS = 64;
|
|
|
31
21
|
// `workflow create` already said the document was fine.
|
|
32
22
|
/** Max per-step map fan-out concurrency (also the run-level concurrency ceiling). */
|
|
33
23
|
export const WORKFLOW_MAX_CONCURRENCY = 64;
|
|
34
|
-
/** Max evaluator-optimizer gate loops per step. */
|
|
35
|
-
export const WORKFLOW_MAX_GATE_LOOPS = 100;
|
|
36
|
-
/** Max retry attempts per unit. */
|
|
37
|
-
export const WORKFLOW_MAX_RETRIES = 100;
|
|
38
24
|
/** Max timeout in milliseconds (setTimeout's 32-bit signed ceiling: 2^31-1, ~24.8 days). */
|
|
39
25
|
export const WORKFLOW_MAX_TIMEOUT_MS = EXECUTION_MAX_TIMEOUT_MS;
|
|
40
26
|
/** Engine names: lowercase dash-separated runs of letters/digits, starting with a letter. */
|
|
41
27
|
export const WORKFLOW_ENGINE_NAME_PATTERN = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
|
|
42
28
|
export const WORKFLOW_MAX_ENGINE_NAME_LENGTH = 63;
|
|
43
29
|
// ── exec (shell) unit bounds ─────────────────────────────────────────────────
|
|
44
|
-
/**
|
|
45
|
-
* Max entries in an exec unit's `command:` argv array. Generous for a real
|
|
46
|
-
* command line, small enough that a corrupted plan cannot ask the OS to spawn
|
|
47
|
-
* a megabyte of arguments.
|
|
48
|
-
*/
|
|
49
|
-
export const WORKFLOW_MAX_EXEC_ARGV = 64;
|
|
50
|
-
/** Max UTF-8 bytes of ONE argv entry (well under every platform's ARG_MAX per-arg limit). */
|
|
51
|
-
export const WORKFLOW_MAX_EXEC_ARG_BYTES = 4096;
|
|
52
|
-
/** Max characters of an exec unit's relative `cwd:`. */
|
|
53
|
-
export const WORKFLOW_MAX_EXEC_CWD_LENGTH = 1024;
|
|
54
|
-
/**
|
|
55
|
-
* Max entries in an exec unit's `pass_env:` list.
|
|
56
|
-
*
|
|
57
|
-
* `pass_env` is bounded; use explicit named `env:` bindings for a larger set.
|
|
58
|
-
* New v4 starts do not offer whole-process inheritance.
|
|
59
|
-
*/
|
|
60
|
-
export const WORKFLOW_MAX_EXEC_PASS_ENV = 32;
|
|
61
30
|
/**
|
|
62
31
|
* Grammar for an env var NAME in `pass_env:`. Matches the frozen-plan
|
|
63
32
|
* `envPassthrough` grammar in `ir/schema.ts` so both allowlist surfaces accept
|
|
@@ -86,8 +55,7 @@ export const DEFAULT_EXEC_TIMEOUT_MS = 600_000;
|
|
|
86
55
|
* ten minutes to do so. What the cap buys is a BOUND where there was none: the
|
|
87
56
|
* retained prefix is promoted into the unit's outcome text and every outcome is
|
|
88
57
|
* held until the step reduces, so the worst case is (units in the STEP × this
|
|
89
|
-
* cap)
|
|
90
|
-
* what sizes it, up to {@link WORKFLOW_MAX_MAP_EXPANSION}.
|
|
58
|
+
* cap) — the step's width, not the in-flight width, is what sizes it.
|
|
91
59
|
*
|
|
92
60
|
* On reaching the cap the reader switches to DRAIN-AND-DISCARD: it keeps
|
|
93
61
|
* pulling from the pipe (so the child never blocks on backpressure) and stops
|
|
@@ -180,11 +148,10 @@ export function clip(text, max) {
|
|
|
180
148
|
*
|
|
181
149
|
* The promoted step artifact (`evidence.output`) is deliberately NOT clipped
|
|
182
150
|
* when it is built — gates judge the full artifact and downstream
|
|
183
|
-
* `steps.<id>.output` references need it intact — but a `collect`
|
|
184
|
-
*
|
|
185
|
-
*
|
|
186
|
-
*
|
|
187
|
-
* bounded here, at the write boundary, by
|
|
151
|
+
* `steps.<id>.output` references need it intact — but a `collect` reducer
|
|
152
|
+
* over an unbounded fan-out (each unit contributing up to a full unit
|
|
153
|
+
* result) would otherwise write an unbounded blob into a single SQLite row.
|
|
154
|
+
* Persistence is therefore bounded here, at the write boundary, by
|
|
188
155
|
* `clipStepEvidenceForPersistence` (`runtime/runs.ts`), which replaces
|
|
189
156
|
* oversized values with an explicitly-marked truncation envelope rather than
|
|
190
157
|
* silently shortening them.
|
|
@@ -213,8 +180,11 @@ export function jsonBytes(value) {
|
|
|
213
180
|
// (`src/workflows/ir/schema-v4.ts`'s recursive `decodeChildWorkflowTarget`).
|
|
214
181
|
// Full design history, including the rejected alternative for the byte cap:
|
|
215
182
|
// docs/architecture/decisions/0007-workflow-composition-bounds.md.
|
|
216
|
-
|
|
217
|
-
|
|
183
|
+
//
|
|
184
|
+
// Composition DEPTH is unbounded — `assertNoCompositionCycle` (freeze/targets/
|
|
185
|
+
// child-workflow.ts) already makes infinite composition mathematically
|
|
186
|
+
// impossible, so a depth ceiling on top of it only bounded how many
|
|
187
|
+
// legitimately distinct workflows an author could nest.
|
|
218
188
|
/**
|
|
219
189
|
* Max AGGREGATE canonical-JSON bytes of every embedded child plan in ONE root
|
|
220
190
|
* freeze (the sum across the whole composition tree, not per child).
|