akm-cli 0.9.2-alpha.4 → 0.9.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +493 -0
- package/STABILITY.md +23 -5
- package/dist/assets/hints/cli-hints-full.md +12 -7
- package/dist/assets/tasks/core/extract.yml +3 -5
- package/dist/assets/tasks/core/improve.yml +3 -5
- package/dist/assets/tasks/core/index-refresh.yml +3 -5
- package/dist/assets/tasks/core/sync.yml +3 -5
- package/dist/assets/tasks/core/version-check.yml +3 -5
- package/dist/assets/tasks/improve/akm-graph-refresh-weekly.yml +3 -5
- package/dist/assets/tasks/improve/akm-improve-catchup.yml +6 -6
- package/dist/assets/tasks/improve/akm-improve-consolidate.yml +3 -5
- package/dist/assets/tasks/improve/akm-improve-frequent.yml +3 -5
- package/dist/assets/tasks/improve/akm-improve-nightly.yml +3 -5
- package/dist/cli/unknown-flags.js +12 -1
- package/dist/cli.js +8 -1
- package/dist/commands/command/command-execution.js +23 -2
- package/dist/commands/health/improve-metrics.js +38 -0
- package/dist/commands/health/windows.js +8 -4
- package/dist/commands/health.js +8 -4
- package/dist/commands/lint/index.js +1 -1
- package/dist/commands/migrate-cli.js +130 -24
- package/dist/commands/proposal/validators/proposal-validators.js +7 -2
- package/dist/commands/tasks/explain.js +304 -0
- package/dist/commands/tasks/tasks-cli.js +185 -3
- package/dist/commands/tasks/tasks.js +265 -52
- package/dist/commands/workflow/plan.js +159 -0
- package/dist/commands/workflow-cli.js +94 -2
- package/dist/core/activation-policy.js +2 -12
- package/dist/core/adapter/adapters/akm-lint.js +7 -4
- package/dist/core/adapter/adapters/akm-metadata.js +26 -14
- package/dist/core/adapter/adapters/akm-task-adapter.js +13 -10
- package/dist/core/errors.js +45 -0
- package/dist/core/json-schema.js +15 -5
- package/dist/core/state/migrations.js +57 -0
- package/dist/core/state-db.js +16 -14
- package/dist/core/subprocess.js +47 -13
- package/dist/execution/guarded-source.js +44 -0
- package/dist/execution/input-contract.js +250 -0
- package/dist/execution/target-ref.js +63 -0
- package/dist/indexer/usage/usage-events.js +14 -3
- package/dist/integrations/agent/execution-lowering.js +12 -1
- package/dist/output/shapes/passthrough.js +2 -0
- package/dist/output/text/helpers.js +1 -1
- package/dist/output/text/migrate.js +12 -3
- package/dist/output/text/workflow-format.js +192 -10
- package/dist/output/text/workflow.js +2 -1
- package/dist/runtime.js +1 -0
- package/dist/scripts/akm-migrate-node.js +11838 -10118
- package/dist/scripts/akm-migrate.js +11828 -10117
- package/dist/setup/steps/tasks.js +34 -17
- package/dist/storage/repositories/task-history-repository.js +5 -1
- package/dist/storage/repositories/workflow-runs-repository.js +144 -6
- package/dist/tasks/backends/launchd.js +31 -84
- package/dist/tasks/embedded.js +13 -7
- package/dist/tasks/model/invocation.js +4 -0
- package/dist/tasks/prepare/prepare-script-target.js +9 -0
- package/dist/tasks/prepare/prepare-support.js +154 -0
- package/dist/tasks/prepare/prepare.js +117 -0
- package/dist/tasks/prepare/prepared-execution.js +4 -0
- package/dist/tasks/prepare/script-capture.js +80 -0
- package/dist/tasks/run/attempt-lifecycle.js +165 -0
- package/dist/tasks/run/load-task.js +117 -0
- package/dist/tasks/run/provenance.js +20 -0
- package/dist/tasks/run/run-command-task.js +92 -0
- package/dist/tasks/run/run-native-task.js +222 -0
- package/dist/tasks/run/run-task.js +99 -0
- package/dist/tasks/run/run-workflow-task.js +222 -0
- package/dist/tasks/run/task-history.js +134 -0
- package/dist/tasks/run/task-log.js +179 -0
- package/dist/tasks/run/task-result.js +19 -0
- package/dist/tasks/scheduler-binding.js +66 -2
- package/dist/tasks/scheduler-invocation.js +63 -3
- package/dist/tasks/scheduler-sync.js +77 -14
- package/dist/tasks/source/bounded-document.js +455 -0
- package/dist/tasks/source/parse-task-source.js +59 -0
- package/dist/tasks/source/project-v4.js +62 -0
- package/dist/tasks/source/task-input-diagnostics.js +36 -0
- package/dist/tasks/source/task-source-v4.js +626 -0
- package/dist/tasks/source-v3.js +10 -733
- package/dist/tasks/task-run-reserved-flags.js +79 -0
- package/dist/workflows/authoring/authoring.js +17 -8
- package/dist/workflows/exec/child-invocation.js +34 -0
- package/dist/workflows/exec/child-workflow.js +370 -0
- package/dist/workflows/exec/exec-unit.js +50 -170
- package/dist/workflows/exec/frozen-judge.js +19 -2
- package/dist/workflows/exec/native-executor.js +49 -27
- package/dist/workflows/exec/param-secrets.js +12 -0
- package/dist/workflows/exec/run-workflow.js +48 -59
- package/dist/workflows/exec/step-work.js +222 -80
- package/dist/workflows/exec/unit-dispatch.js +72 -0
- package/dist/workflows/freeze/child-output-references.js +94 -0
- package/dist/workflows/freeze/environment.js +174 -0
- package/dist/workflows/freeze/identity.js +22 -0
- package/dist/workflows/freeze/resolve-steps.js +78 -0
- package/dist/workflows/freeze/source-freeze.js +57 -0
- package/dist/workflows/freeze/step-values.js +68 -0
- package/dist/workflows/freeze/targets/child-workflow.js +206 -0
- package/dist/workflows/freeze/targets/command.js +81 -0
- package/dist/workflows/freeze/targets/script.js +57 -0
- package/dist/workflows/freeze/targets/shell.js +31 -0
- package/dist/workflows/freeze/targets/task.js +179 -0
- package/dist/workflows/freeze/task-bindings.js +180 -0
- package/dist/workflows/ir/compile.js +59 -11
- package/dist/workflows/ir/environment-v4.js +3 -3
- package/dist/workflows/ir/freeze-v4.js +41 -7
- package/dist/workflows/ir/params.js +58 -131
- package/dist/workflows/ir/plan-hash.js +3 -3
- package/dist/workflows/ir/schema-v4.js +246 -17
- package/dist/workflows/parser.js +74 -2
- package/dist/workflows/program/schema.js +5 -2
- package/dist/workflows/resource-limits.js +20 -0
- package/dist/workflows/runtime/plan-classifier.js +24 -7
- package/dist/workflows/runtime/run-outputs.js +103 -0
- package/dist/workflows/runtime/runs.js +114 -9
- package/dist/workflows/runtime/workflow-asset-loader.js +14 -6
- package/dist/workflows/source-files.js +5 -5
- package/dist/workflows/source-ir/compare.js +17 -0
- package/dist/workflows/source-ir/compile.js +7 -3
- package/dist/workflows/source-ir/github-yaml.js +64 -17
- package/dist/workflows/source-ir/schema.js +69 -21
- package/dist/workflows/source-ir/semantics.js +7 -25
- package/dist/workflows/source-ir/triggers.js +79 -0
- package/dist/workflows/source-ir/uses.js +33 -7
- package/docs/migration/README.md +1 -1
- package/docs/migration/release-notes/0.9.2.md +87 -11
- package/docs/migration/release-notes/README.md +3 -2
- package/docs/migration/v0.8-to-v0.9.md +13 -11
- package/docs/migration/v0.9.0-troubleshooting.md +20 -13
- package/docs/migration/v0.9.1-to-v0.9.2.md +598 -49
- package/docs/reference/README.md +1 -1
- package/docs/reference/cli.md +140 -46
- package/docs/reference/configuration.md +6 -5
- package/docs/reference/supported-formats.md +9 -5
- package/docs/reference/tasks.md +338 -75
- package/docs/reference/workflow-schema.md +290 -16
- package/docs/reference/workflows.md +57 -7
- package/package.json +1 -1
- package/schemas/akm-task.json +173 -118
- package/schemas/akm-workflow.json +28 -0
- package/dist/tasks/runner.js +0 -941
- package/dist/tasks/runtime-v3.js +0 -281
- package/dist/workflows/ir/source-freeze-v4.js +0 -506
- package/dist/workflows/source-ir/ordering.js +0 -38
|
@@ -0,0 +1,134 @@
|
|
|
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
|
+
* The task_history read/write boundary: `appendHistory` (write) and
|
|
6
|
+
* `readTaskHistory` / `taskHistoryRowToResult` (read).
|
|
7
|
+
*
|
|
8
|
+
* Moved from src/tasks/runner.ts (spec docs/plans/specs/p1b-model-extraction.md
|
|
9
|
+
* §5.1, §9, runner.ts:1071-1158). `appendHistory` was module-private at head;
|
|
10
|
+
* it is exported here because every dispatch arm (run-native-task.ts,
|
|
11
|
+
* run-workflow-task.ts, run-command-task.ts), attempt-lifecycle.ts, and
|
|
12
|
+
* run-task.ts's disabled-task path all call it (spec §5.1's module split
|
|
13
|
+
* moves what was one file's internal call graph across several files).
|
|
14
|
+
*
|
|
15
|
+
* D8 result-vocabulary re-code (why, and the WRITE side's exact shape): see
|
|
16
|
+
* docs/architecture/decisions/0005-task-result-vocabulary-and-legacy-read-mapping.md.
|
|
17
|
+
* The READ side's mapping rule is a PERMANENT invariant kept here, not moved
|
|
18
|
+
* — a maintainer touching `taskHistoryRowToResult` needs it right here, and
|
|
19
|
+
* row B-51 (docs/plans/specs/p4-deletions-closeout.md) makes deleting it a
|
|
20
|
+
* review-blocking violation: it reads rows written by every previous
|
|
21
|
+
* release, forever.
|
|
22
|
+
*
|
|
23
|
+
* A legacy row (no `targetVocab` marker — written before D8) maps:
|
|
24
|
+
* `"prompt"` -> `{kind:"command", engine}`, `"command"` -> `{kind:"shell"}`,
|
|
25
|
+
* `"workflow"` unchanged, anything else (including the new vocabulary's own
|
|
26
|
+
* "shell"/"script"/"prompt" written WITHOUT a marker, which no production
|
|
27
|
+
* writer ever does) -> `"unknown"`. The P0-pinned null fallbacks survive:
|
|
28
|
+
* workflow `ref` falls back to `""`, the command/prompt arm's `engine`
|
|
29
|
+
* falls back to `null`. A row carrying `targetVocab: 2` reads `target_kind`
|
|
30
|
+
* directly in the current vocabulary — no mapping needed.
|
|
31
|
+
*
|
|
32
|
+
* A DAG leaf with respect to the rest of src/tasks/run/**: this module
|
|
33
|
+
* imports TaskRunResult/TaskRunStatus's TYPE from ./task-result but no VALUE
|
|
34
|
+
* from it, and nothing at all from ./task-log or ./attempt-lifecycle — so
|
|
35
|
+
* nothing here risks closing an import cycle with a module that itself
|
|
36
|
+
* imports appendHistory or readTaskHistory from here.
|
|
37
|
+
*/
|
|
38
|
+
import { rethrowIfTestIsolationError } from "../../core/errors.js";
|
|
39
|
+
import { withStateDb } from "../../core/state-db.js";
|
|
40
|
+
import { decodeTaskHistoryMetadata, finalizeTaskHistoryAttempt, getTaskHistory, getTaskHistoryRuns, queryTaskHistory, upsertTaskHistory, } from "../../storage/repositories/task-history-repository.js";
|
|
41
|
+
/** Append (or finalize a reserved attempt into) one task_history row. */
|
|
42
|
+
export function appendHistory(result, historyReserved = false) {
|
|
43
|
+
const row = {
|
|
44
|
+
task_id: result.id,
|
|
45
|
+
status: result.status,
|
|
46
|
+
started_at: result.startedAt,
|
|
47
|
+
completed_at: result.finishedAt,
|
|
48
|
+
failed_at: result.status === "failed" ? result.finishedAt : null,
|
|
49
|
+
log_path: result.log || null,
|
|
50
|
+
target_kind: result.target.kind === "unknown" ? null : result.target.kind,
|
|
51
|
+
target_ref: result.target.kind === "workflow" ? result.target.ref : null,
|
|
52
|
+
metadata_json: JSON.stringify({
|
|
53
|
+
metadataVersion: 2,
|
|
54
|
+
durationMs: result.durationMs,
|
|
55
|
+
detail: result.detail ?? null,
|
|
56
|
+
// D8 (spec §5.3): every NEW row carries the vocabulary marker.
|
|
57
|
+
targetVocab: 2,
|
|
58
|
+
...(result.target.kind === "command" ? { engine: result.target.engine } : {}),
|
|
59
|
+
}),
|
|
60
|
+
};
|
|
61
|
+
try {
|
|
62
|
+
withStateDb((db) => {
|
|
63
|
+
if (historyReserved && finalizeTaskHistoryAttempt(db, row))
|
|
64
|
+
return;
|
|
65
|
+
upsertTaskHistory(db, row);
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
rethrowIfTestIsolationError(error);
|
|
70
|
+
// History recording is fully best-effort and must not alter CLI output.
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
export function readTaskHistory(options = {}) {
|
|
74
|
+
return withStateDb((db) => {
|
|
75
|
+
if (options.limit === 0)
|
|
76
|
+
return [];
|
|
77
|
+
if (options.id) {
|
|
78
|
+
// An id-scoped query used the single-row helper, so `--limit` was silently
|
|
79
|
+
// discarded and `akm task history --id X --limit 20` always returned one
|
|
80
|
+
// run. The CLI documents --limit as "Maximum rows to return"; honour it.
|
|
81
|
+
if (options.limit !== undefined && options.limit > 0) {
|
|
82
|
+
return getTaskHistoryRuns(db, options.id, options.limit).map(taskHistoryRowToResult);
|
|
83
|
+
}
|
|
84
|
+
const row = getTaskHistory(db, options.id);
|
|
85
|
+
return row ? [taskHistoryRowToResult(row)] : [];
|
|
86
|
+
}
|
|
87
|
+
return queryTaskHistory(db, options.limit !== undefined && options.limit > 0 ? { limit: options.limit } : {}).map(taskHistoryRowToResult);
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Convert a `TaskHistoryRow` from state.db back to a `TaskRunResult` shape
|
|
92
|
+
* that callers of `readTaskHistory()` expect.
|
|
93
|
+
*
|
|
94
|
+
* D8 read boundary (spec §5.3): branches on the decoded metadata's
|
|
95
|
+
* `targetVocab` marker — see the module header's table.
|
|
96
|
+
*/
|
|
97
|
+
function taskHistoryRowToResult(row) {
|
|
98
|
+
const meta = decodeTaskHistoryMetadata(row.metadata_json);
|
|
99
|
+
const marked = meta.targetVocab === 2;
|
|
100
|
+
const target = (() => {
|
|
101
|
+
switch (row.target_kind) {
|
|
102
|
+
case "workflow":
|
|
103
|
+
// PRESERVED for both vintages (incl. the null-ref fallback).
|
|
104
|
+
return { kind: "workflow", ref: row.target_ref ?? "" };
|
|
105
|
+
case "command":
|
|
106
|
+
// NEW vocabulary: a prepared command (agent/LLM) result.
|
|
107
|
+
// LEGACY vocabulary: the native shell/script arm's shared string.
|
|
108
|
+
return marked ? { kind: "command", engine: meta.engine ?? null } : { kind: "shell" };
|
|
109
|
+
case "shell":
|
|
110
|
+
// Only the NEW vocabulary ever writes this string; an unmarked
|
|
111
|
+
// "shell" row is unreachable from any production writer.
|
|
112
|
+
return marked ? { kind: "shell" } : { kind: "unknown" };
|
|
113
|
+
case "script":
|
|
114
|
+
// Only the NEW vocabulary ever writes this string; an unmarked
|
|
115
|
+
// "script" row is unreachable from any production writer.
|
|
116
|
+
return marked ? { kind: "script" } : { kind: "unknown" };
|
|
117
|
+
case "prompt":
|
|
118
|
+
// Only LEGACY rows (pre-P1b) ever wrote this string.
|
|
119
|
+
return marked ? { kind: "unknown" } : { kind: "command", engine: meta.engine ?? null };
|
|
120
|
+
default:
|
|
121
|
+
return { kind: "unknown" };
|
|
122
|
+
}
|
|
123
|
+
})();
|
|
124
|
+
return {
|
|
125
|
+
id: row.task_id,
|
|
126
|
+
status: row.status,
|
|
127
|
+
startedAt: row.started_at,
|
|
128
|
+
finishedAt: row.completed_at ?? row.failed_at ?? row.started_at,
|
|
129
|
+
durationMs: meta.durationMs,
|
|
130
|
+
log: row.log_path ?? "",
|
|
131
|
+
target,
|
|
132
|
+
...(meta.detail ? { detail: meta.detail } : {}),
|
|
133
|
+
};
|
|
134
|
+
}
|
|
@@ -0,0 +1,179 @@
|
|
|
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
|
+
* Task-run log persistence: log path resolution, the flat per-run log file,
|
|
6
|
+
* structured logs.db rows, and the shared redaction pass both sinks go
|
|
7
|
+
* through before either is written.
|
|
8
|
+
*
|
|
9
|
+
* Moved body-intact from src/tasks/runner.ts (spec
|
|
10
|
+
* docs/plans/specs/p1b-model-extraction.md §5.1, runner.ts:779-968).
|
|
11
|
+
*
|
|
12
|
+
* A DAG leaf with respect to the rest of src/tasks/run/**: nothing here
|
|
13
|
+
* imports ./task-result, ./task-history, or ./attempt-lifecycle, so every
|
|
14
|
+
* other run/** module can depend on this one without risking an import
|
|
15
|
+
* cycle (tests/architecture/import-cycle-ratchet.test.ts counts type-only
|
|
16
|
+
* imports as real edges too).
|
|
17
|
+
*/
|
|
18
|
+
import fs from "node:fs";
|
|
19
|
+
import path from "node:path";
|
|
20
|
+
import { loadConfig } from "../../core/config/config.js";
|
|
21
|
+
import { rethrowIfTestIsolationError } from "../../core/errors.js";
|
|
22
|
+
import { buildTaskRunId, insertTaskLogLines, openLogsDatabase, } from "../../core/logs-db.js";
|
|
23
|
+
import { getTaskLogDir } from "../../core/paths.js";
|
|
24
|
+
import { redactCredentialPatterns, redactSensitiveText } from "../../core/redaction.js";
|
|
25
|
+
import { collectTaskLogSensitiveValues } from "../log-redaction.js";
|
|
26
|
+
function taskLogPath(logDir, taskId, startedAtIso) {
|
|
27
|
+
const tsSlug = startedAtIso.replace(/[:.]/g, "-");
|
|
28
|
+
return path.join(logDir, taskId, `${tsSlug}.log`);
|
|
29
|
+
}
|
|
30
|
+
export function resolveTaskLogPath(logDir, taskId, startedAtIso) {
|
|
31
|
+
try {
|
|
32
|
+
return taskLogPath(logDir ?? getTaskLogDir(), taskId, startedAtIso);
|
|
33
|
+
}
|
|
34
|
+
catch (error) {
|
|
35
|
+
rethrowIfTestIsolationError(error);
|
|
36
|
+
return "";
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Redact logs.db rows against the SAME contiguous text the file sink sees.
|
|
41
|
+
*
|
|
42
|
+
* The rows arrive already split on "\n" (see {@link streamLines}), but the
|
|
43
|
+
* redaction needles are whole env values — and a needle containing a newline
|
|
44
|
+
* can never match inside a single line. Scrubbing row-by-row therefore left
|
|
45
|
+
* multi-line secrets (PEM keys, multi-line service-account credentials) intact
|
|
46
|
+
* in logs.db while the flat .log was correctly scrubbed, defeating all three
|
|
47
|
+
* tiers including the explicit `redact:` opt-in.
|
|
48
|
+
*
|
|
49
|
+
* Consecutive rows sharing a stream and level are rejoined, scrubbed as one
|
|
50
|
+
* string, and re-split, so a needle spanning lines matches. Collapsing a
|
|
51
|
+
* multi-line secret into a single [REDACTED] row is the intended outcome.
|
|
52
|
+
*/
|
|
53
|
+
export function scrubDbLines(dbLines, scrub) {
|
|
54
|
+
const out = [];
|
|
55
|
+
for (let i = 0; i < dbLines.length;) {
|
|
56
|
+
const { stream, level } = dbLines[i];
|
|
57
|
+
let end = i;
|
|
58
|
+
while (end < dbLines.length && dbLines[end].stream === stream && dbLines[end].level === level)
|
|
59
|
+
end++;
|
|
60
|
+
const joined = dbLines
|
|
61
|
+
.slice(i, end)
|
|
62
|
+
.map((entry) => entry.line)
|
|
63
|
+
.join("\n");
|
|
64
|
+
for (const line of scrub(joined).split("\n")) {
|
|
65
|
+
if (line.length > 0)
|
|
66
|
+
out.push({ stream, level, line });
|
|
67
|
+
}
|
|
68
|
+
i = end;
|
|
69
|
+
}
|
|
70
|
+
return out;
|
|
71
|
+
}
|
|
72
|
+
/** Split captured pipe output into per-line logs.db rows (blank lines dropped). */
|
|
73
|
+
export function streamLines(text, stream, level) {
|
|
74
|
+
return (text
|
|
75
|
+
.split("\n")
|
|
76
|
+
// Windows child output is CRLF-terminated. Splitting on "\n" alone left a
|
|
77
|
+
// trailing "\r" on every row and turned blank CRLF lines into phantom
|
|
78
|
+
// rows containing just "\r" (length 1 passes the filter below).
|
|
79
|
+
.map((line) => (line.endsWith("\r") ? line.slice(0, -1) : line))
|
|
80
|
+
.filter((line) => line.length > 0)
|
|
81
|
+
.map((line) => ({ stream, level, line })));
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Exact secret values to scrub from this run's persisted output (#755).
|
|
85
|
+
*
|
|
86
|
+
* Best-effort by construction: this runs on the persistence path of a run that
|
|
87
|
+
* has already finished, so a config that will not load must degrade to
|
|
88
|
+
* "pattern-based redaction only" rather than fail the run. It does NOT degrade
|
|
89
|
+
* to "log it anyway with no redaction at all" — `redactCredentialPatterns`
|
|
90
|
+
* still runs unconditionally in the caller.
|
|
91
|
+
*/
|
|
92
|
+
function taskLogSensitiveValues(redactNames, environment) {
|
|
93
|
+
const env = { ...process.env, ...environment };
|
|
94
|
+
try {
|
|
95
|
+
return collectTaskLogSensitiveValues({
|
|
96
|
+
env,
|
|
97
|
+
config: loadConfig(),
|
|
98
|
+
declaredNames: redactNames,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
catch (error) {
|
|
102
|
+
rethrowIfTestIsolationError(error);
|
|
103
|
+
// No config — the name heuristic and the task's own `redact:` list still apply.
|
|
104
|
+
try {
|
|
105
|
+
return collectTaskLogSensitiveValues({ env, declaredNames: redactNames });
|
|
106
|
+
}
|
|
107
|
+
catch (fallbackError) {
|
|
108
|
+
rethrowIfTestIsolationError(fallbackError);
|
|
109
|
+
return [];
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
/** Scrub one piece of durable-result text against a prepared task's own secrets. */
|
|
114
|
+
export function scrubTaskOutput(task, text) {
|
|
115
|
+
const patterned = redactCredentialPatterns(text);
|
|
116
|
+
const sensitive = taskLogSensitiveValues(task.redact, task.environment);
|
|
117
|
+
return sensitive.length > 0 ? redactSensitiveText(patterned, sensitive) : patterned;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Persist a finished run's log: the flat text file (so `log_path` in
|
|
121
|
+
* task_history keeps resolving for humans and older consumers) plus
|
|
122
|
+
* structured rows in logs.db keyed by `buildTaskRunId(taskId, startedAt)`.
|
|
123
|
+
*
|
|
124
|
+
* Both sinks are pattern-redacted (`redactCredentialPatterns`) before being
|
|
125
|
+
* written — task output is raw command/agent/LLM text that can echo a
|
|
126
|
+
* credential-bearing URL (e.g. a Discord webhook) nothing upstream expects to
|
|
127
|
+
* scrub.
|
|
128
|
+
*
|
|
129
|
+
* The DB write is best-effort, mirroring history recording: an unwritable
|
|
130
|
+
* logs.db must never fail a task run.
|
|
131
|
+
*/
|
|
132
|
+
export function persistRunLog(input) {
|
|
133
|
+
// Two arms, and both are needed. `redactCredentialPatterns` catches
|
|
134
|
+
// credential SHAPES nobody listed; the exact-value pass catches configured
|
|
135
|
+
// secrets whose value is shaped like nothing in particular (#755). The
|
|
136
|
+
// command target had only the first, so a scheduled command that echoed an
|
|
137
|
+
// ordinary-looking secret persisted it verbatim to both sinks. Applying the
|
|
138
|
+
// exact pass here — the one sink all three target kinds funnel through —
|
|
139
|
+
// covers every arm once rather than per-arm; prompt/workflow runs already
|
|
140
|
+
// scrub upstream, and redaction is idempotent, so the overlap is free.
|
|
141
|
+
const sensitive = taskLogSensitiveValues(input.redactNames, input.environment);
|
|
142
|
+
const scrub = (text) => sensitive.length > 0
|
|
143
|
+
? redactSensitiveText(redactCredentialPatterns(text), sensitive)
|
|
144
|
+
: redactCredentialPatterns(text);
|
|
145
|
+
const fileText = scrub(input.fileText);
|
|
146
|
+
const dbLines = scrubDbLines(input.dbLines, scrub);
|
|
147
|
+
if (input.logPath) {
|
|
148
|
+
try {
|
|
149
|
+
// Written at the process umask. #756 pinned 0600/0700 here; that went out
|
|
150
|
+
// with the rest of akm's permission enforcement (#791) — the operator owns
|
|
151
|
+
// the mode of their own data directory, and akm neither sets nor reports
|
|
152
|
+
// on it.
|
|
153
|
+
fs.mkdirSync(path.dirname(input.logPath), { recursive: true });
|
|
154
|
+
fs.writeFileSync(input.logPath, fileText);
|
|
155
|
+
}
|
|
156
|
+
catch (error) {
|
|
157
|
+
rethrowIfTestIsolationError(error);
|
|
158
|
+
// Transitional file logging is fully best-effort.
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
try {
|
|
162
|
+
const db = openLogsDatabase();
|
|
163
|
+
try {
|
|
164
|
+
insertTaskLogLines(db, {
|
|
165
|
+
taskId: input.taskId,
|
|
166
|
+
runId: buildTaskRunId(input.taskId, input.startedAtIso),
|
|
167
|
+
ts: input.finishedAtIso,
|
|
168
|
+
lines: dbLines,
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
finally {
|
|
172
|
+
db.close();
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
catch (error) {
|
|
176
|
+
rethrowIfTestIsolationError(error);
|
|
177
|
+
// Structured logging is fully best-effort and must not alter CLI output.
|
|
178
|
+
}
|
|
179
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
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
|
+
* The exit code surfaced to the OS scheduler. Mapped from {@link TaskRunStatus}
|
|
6
|
+
* so cron / launchd / schtasks see a useful return value.
|
|
7
|
+
*/
|
|
8
|
+
export function exitCodeForStatus(status) {
|
|
9
|
+
switch (status) {
|
|
10
|
+
case "completed":
|
|
11
|
+
return 0;
|
|
12
|
+
case "active":
|
|
13
|
+
return 0;
|
|
14
|
+
case "blocked":
|
|
15
|
+
return 1;
|
|
16
|
+
case "failed":
|
|
17
|
+
return 1;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
import { createHash } from "node:crypto";
|
|
13
13
|
import { bundleRefToString, parseBundleRef } from "../core/asset/asset-ref.js";
|
|
14
14
|
import { UsageError } from "../core/errors.js";
|
|
15
|
+
import { canonicalInputJson } from "../execution/input-contract.js";
|
|
15
16
|
import { normaliseTaskConceptId } from "./task-id.js";
|
|
16
17
|
export function compileTaskSchedulerBindings(input) {
|
|
17
18
|
const id = normaliseTaskConceptId(input.id);
|
|
@@ -19,9 +20,20 @@ export function compileTaskSchedulerBindings(input) {
|
|
|
19
20
|
const bundle = parseBundleRef(ref).bundle;
|
|
20
21
|
if (!bundle)
|
|
21
22
|
throw new Error("invariant: qualified scheduler task ref lost its bundle");
|
|
22
|
-
|
|
23
|
+
// P2b Lane B (B-N3): the invocation is compiled PER schedule entry now —
|
|
24
|
+
// each entry's own `inputs` produces its own trailing flag tail, so it can
|
|
25
|
+
// no longer be hoisted as one binding shared by every entry.
|
|
23
26
|
return Object.freeze(input.schedules.map((schedule) => {
|
|
24
27
|
const bindingId = schedule.ordinal === 0 ? id : digestBindingId("task", ref, schedule.ordinal);
|
|
28
|
+
const invocation = Object.freeze([
|
|
29
|
+
"task",
|
|
30
|
+
"run",
|
|
31
|
+
id,
|
|
32
|
+
"--bundle",
|
|
33
|
+
bundle,
|
|
34
|
+
"--scheduled",
|
|
35
|
+
...schedulerInputFlagTail(schedule.inputs),
|
|
36
|
+
]);
|
|
25
37
|
return freezeBinding({
|
|
26
38
|
id: bindingId,
|
|
27
39
|
nativeId: schedulerNativeBindingId(bindingId),
|
|
@@ -29,11 +41,63 @@ export function compileTaskSchedulerBindings(input) {
|
|
|
29
41
|
cron: schedule.cron,
|
|
30
42
|
source: schedule.source,
|
|
31
43
|
ordinal: schedule.ordinal,
|
|
32
|
-
enabled: input.enabled,
|
|
44
|
+
enabled: schedule.enabled ?? input.enabled,
|
|
33
45
|
invocation,
|
|
34
46
|
});
|
|
35
47
|
}));
|
|
36
48
|
}
|
|
49
|
+
/**
|
|
50
|
+
* The canonically-sorted `--<name> <value>` flag tail for one schedule
|
|
51
|
+
* entry's `inputs` (spec §1.7 B-N3). Empty/absent `inputs` yields an empty
|
|
52
|
+
* tail — the fixed six-token invocation, byte-identical to every schedule
|
|
53
|
+
* entry before P2b (B-03).
|
|
54
|
+
*
|
|
55
|
+
* Code-review finding (scheduler-binding.ts:536): a value whose exact text
|
|
56
|
+
* begins with `-` (a negative number, or a string that just happens to
|
|
57
|
+
* start with a dash) is indistinguishable from a NEW flag in the two-token
|
|
58
|
+
* `--<name> <value>` form — `isValidSchedulerInputFlagTail`
|
|
59
|
+
* (scheduler-invocation.ts) refuses it outright, and even a looser parser
|
|
60
|
+
* would still be wrong for a non-numeric dash-leading string: the real `akm
|
|
61
|
+
* task run` flag parser (`parseTaskInputFlags`, tasks-cli.ts) only
|
|
62
|
+
* special-cases a dash-DIGIT lead, so it would silently treat `--scope
|
|
63
|
+
* -urgent` as the boolean flag `--scope` followed by an orphaned,
|
|
64
|
+
* dropped `-urgent` token. The inline `--<name>=<value>` form has no such
|
|
65
|
+
* ambiguity on EITHER side — `isValidSchedulerInputFlagTail` accepts it
|
|
66
|
+
* unconditionally, and `parseTaskInputFlags`'s own inline-`=` branch splits
|
|
67
|
+
* on the first `=` without ever inspecting the value's leading character —
|
|
68
|
+
* so it is used whenever the value's exact text would otherwise be
|
|
69
|
+
* ambiguous. Every other value keeps the existing two-token form
|
|
70
|
+
* byte-identical to before this fix (B-03/B-45).
|
|
71
|
+
*/
|
|
72
|
+
function schedulerInputFlagTail(inputs) {
|
|
73
|
+
if (!inputs)
|
|
74
|
+
return [];
|
|
75
|
+
const names = Object.keys(inputs).sort();
|
|
76
|
+
const tail = [];
|
|
77
|
+
for (const name of names) {
|
|
78
|
+
const text = schedulerInputFlagValueText(inputs[name]);
|
|
79
|
+
if (text.startsWith("-"))
|
|
80
|
+
tail.push(`--${name}=${text}`);
|
|
81
|
+
else
|
|
82
|
+
tail.push(`--${name}`, text);
|
|
83
|
+
}
|
|
84
|
+
return tail;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* One input value's argv text. A scalar is its exact text — `String(value)`
|
|
88
|
+
* for a number/boolean (a `true` boolean is `"true"`, never a bare flag, so
|
|
89
|
+
* the tail round-trips through one parser), the string itself for a string.
|
|
90
|
+
* An object/array value is its `canonicalInputJson` text, which
|
|
91
|
+
* `materializeInputFlags`'s JSON-shorthand path already coerces back through
|
|
92
|
+
* the declaration.
|
|
93
|
+
*/
|
|
94
|
+
function schedulerInputFlagValueText(value) {
|
|
95
|
+
if (typeof value === "string")
|
|
96
|
+
return value;
|
|
97
|
+
if (typeof value === "number" || typeof value === "boolean")
|
|
98
|
+
return String(value);
|
|
99
|
+
return canonicalInputJson(value);
|
|
100
|
+
}
|
|
37
101
|
export function compileWorkflowSchedulerBindings(input) {
|
|
38
102
|
const ref = assertQualifiedRef(input.qualifiedRef, "workflow");
|
|
39
103
|
const invocation = Object.freeze(["workflow", "run", ref]);
|
|
@@ -8,6 +8,7 @@ import { bundleRefToString, parseBundleRef } from "../core/asset/asset-ref.js";
|
|
|
8
8
|
import { resolveStashDir } from "../core/common.js";
|
|
9
9
|
import { ConfigError } from "../core/errors.js";
|
|
10
10
|
import { getCacheDir, getConfigDir, getDataDir, getTaskContextDir } from "../core/paths.js";
|
|
11
|
+
import { INPUT_NAME_PATTERN } from "../execution/input-contract.js";
|
|
11
12
|
import { normaliseTaskConceptId } from "./task-id.js";
|
|
12
13
|
export const SCHEDULED_TASK_CONTEXT_KEYS = [
|
|
13
14
|
"AKM_BUNDLE_DIR",
|
|
@@ -205,12 +206,20 @@ function parsePublicSchedulerInvocation(invocation) {
|
|
|
205
206
|
return undefined;
|
|
206
207
|
index += 2;
|
|
207
208
|
}
|
|
208
|
-
if (invocation[index] !== "--scheduled"
|
|
209
|
+
if (invocation[index] !== "--scheduled")
|
|
210
|
+
return undefined;
|
|
211
|
+
// P2b Lane B (spec §4.4, §1.7 B-N3): zero or more `--<name> <value>`
|
|
212
|
+
// schedule-supplied input flags may follow `--scheduled` — the same
|
|
213
|
+
// trailing tail `compileTaskSchedulerBindings` compiles from
|
|
214
|
+
// `schedule[i].inputs`. Absent/empty is the pre-P2b shape, byte-identical
|
|
215
|
+
// (B-03).
|
|
216
|
+
if (!isValidSchedulerInputFlagTail(invocation.slice(index + 1)))
|
|
209
217
|
return undefined;
|
|
210
218
|
return { invocation: [...invocation], ...(target !== undefined ? { target } : {}) };
|
|
211
219
|
}
|
|
212
|
-
if (invocation[0] !== "workflow" || invocation[1] !== "run" || invocation.length !== 3)
|
|
220
|
+
if (invocation[0] !== "workflow" || invocation[1] !== "run" || invocation.length !== 3) {
|
|
213
221
|
return undefined;
|
|
222
|
+
}
|
|
214
223
|
const ref = invocation[2];
|
|
215
224
|
if (!ref)
|
|
216
225
|
return undefined;
|
|
@@ -224,6 +233,56 @@ function parsePublicSchedulerInvocation(invocation) {
|
|
|
224
233
|
return undefined;
|
|
225
234
|
}
|
|
226
235
|
}
|
|
236
|
+
/**
|
|
237
|
+
* Validate an OPTIONAL trailing schedule-input flag tail (spec §1.7 B-N3):
|
|
238
|
+
* empty is valid (the pre-P2b shape). Otherwise the tail is a sequence of
|
|
239
|
+
* entries, each EITHER:
|
|
240
|
+
*
|
|
241
|
+
* - a single inline `--<name>=<value>` token (code-review finding,
|
|
242
|
+
* scheduler-binding.ts:536 — the ONLY encoding a dash-leading value's
|
|
243
|
+
* exact text can round-trip through, since `<value>` here is everything
|
|
244
|
+
* after the first `=`, whatever its leading character); or
|
|
245
|
+
* - a `(--<name>, <value>)` pair, where `<value>` is a single non-flag
|
|
246
|
+
* token (does not start with `-`) — the pre-P2b shape.
|
|
247
|
+
*
|
|
248
|
+
* In both forms `<name>` matches {@link INPUT_NAME_PATTERN} and no name
|
|
249
|
+
* repeats. A bare flag, a repeated name, a flag-shaped value in the pair
|
|
250
|
+
* form, or a malformed token are all refused. This validates SHAPE only;
|
|
251
|
+
* the real materialization against the task's declared contract happens
|
|
252
|
+
* through the same `parseTaskInputFlags` + `materializeInputFlags` path
|
|
253
|
+
* `akm task run --<name>` already uses (B-48) — `parseTaskInputFlags`
|
|
254
|
+
* accepts both forms natively (its inline-`=` branch never inspects the
|
|
255
|
+
* value's leading character).
|
|
256
|
+
*/
|
|
257
|
+
function isValidSchedulerInputFlagTail(tail) {
|
|
258
|
+
if (tail.length === 0)
|
|
259
|
+
return true;
|
|
260
|
+
const seen = new Set();
|
|
261
|
+
let index = 0;
|
|
262
|
+
while (index < tail.length) {
|
|
263
|
+
const token = tail[index];
|
|
264
|
+
if (!token || !token.startsWith("--"))
|
|
265
|
+
return false;
|
|
266
|
+
const body = token.slice(2);
|
|
267
|
+
const equalsAt = body.indexOf("=");
|
|
268
|
+
if (equalsAt !== -1) {
|
|
269
|
+
const name = body.slice(0, equalsAt);
|
|
270
|
+
if (!INPUT_NAME_PATTERN.test(name) || seen.has(name))
|
|
271
|
+
return false;
|
|
272
|
+
seen.add(name);
|
|
273
|
+
index += 1;
|
|
274
|
+
continue;
|
|
275
|
+
}
|
|
276
|
+
if (!INPUT_NAME_PATTERN.test(body) || seen.has(body))
|
|
277
|
+
return false;
|
|
278
|
+
const value = tail[index + 1];
|
|
279
|
+
if (value === undefined || value.startsWith("-"))
|
|
280
|
+
return false;
|
|
281
|
+
seen.add(body);
|
|
282
|
+
index += 2;
|
|
283
|
+
}
|
|
284
|
+
return true;
|
|
285
|
+
}
|
|
227
286
|
function canonicalContext(input) {
|
|
228
287
|
const inputKeys = Object.keys(input);
|
|
229
288
|
if (inputKeys.length !== SCHEDULED_TASK_CONTEXT_KEYS.length ||
|
|
@@ -325,5 +384,6 @@ function invalidSchedulerContext() {
|
|
|
325
384
|
return new ConfigError(`Invalid scheduler context; expected exactly ${SCHEDULED_TASK_CONTEXT_KEYS.join(", ")} as absolute paths.`, "INVALID_CONFIG_FILE");
|
|
326
385
|
}
|
|
327
386
|
function invalidSchedulerInvocation() {
|
|
328
|
-
return new ConfigError("Invalid scheduler invocation; expected public
|
|
387
|
+
return new ConfigError("Invalid scheduler invocation; expected public " +
|
|
388
|
+
"`task run <id> [--bundle <bundle>] --scheduled [--<input> <value>…]` or `workflow run <qualified-ref>` argv.", "INVALID_CONFIG_FILE");
|
|
329
389
|
}
|