akm-cli 0.9.17-alpha.6 → 0.9.17-alpha.8
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 +285 -0
- package/STABILITY.md +2 -2
- package/dist/akm +62 -29
- package/dist/akm-migrate +38 -19
- package/dist/assets/prompts/retrieval-relevance-judge.md +6 -0
- package/dist/assets/stash-skeleton/facts/conventions/backlinks.md +5 -3
- package/dist/commands/improve/consolidate.js +11 -0
- package/dist/commands/improve/improve-cli.js +27 -7
- package/dist/commands/improve/ledger.js +7 -3
- package/dist/commands/improve/loop-stages.js +4 -3
- package/dist/commands/improve/preparation.js +40 -10
- package/dist/commands/improve/reflect.js +46 -22
- package/dist/commands/improve/retrieval-gate.js +127 -0
- package/dist/commands/improve/retrieval-scope.js +77 -0
- package/dist/commands/read/curate.js +41 -30
- package/dist/commands/read/show.js +55 -2
- package/dist/commands/sources/info.js +3 -0
- package/dist/commands/tasks/tasks-cli.js +10 -12
- package/dist/commands/tasks/tasks.js +57 -56
- package/dist/commands/tasks/validate.js +27 -46
- package/dist/core/adapter/adapters/akm-adapter.js +2 -0
- package/dist/core/adapter/adapters/akm-metadata.js +31 -0
- package/dist/core/adapter/adapters/akm-task-adapter.js +29 -8
- package/dist/core/improve-result.js +4 -1
- package/dist/core/non-task-input.js +20 -0
- package/dist/core/paths.js +0 -4
- package/dist/indexer/db/graph-db.js +0 -32
- package/dist/indexer/graph/graph-extraction.js +3 -1
- package/dist/indexer/indexer.js +1 -3
- package/dist/indexer/links/declared-links.js +90 -0
- package/dist/indexer/scan/doc-to-entry.js +1 -0
- package/dist/indexer/usage/usage-events.js +34 -0
- package/dist/llm/graph-extract.js +26 -37
- package/dist/output/shapes/helpers.js +3 -0
- package/dist/output/text/show-format.js +16 -0
- package/dist/scripts/akm-migrate-node.js +6377 -6437
- package/dist/scripts/akm-migrate.js +6860 -6920
- package/dist/storage/repositories/index-entries-repository.js +12 -6
- package/dist/storage/repositories/index-entry-schema.js +18 -1
- package/dist/storage/repositories/index-links-repository.js +143 -0
- package/dist/storage/repositories/index-schema.js +27 -0
- package/dist/storage/repositories/proposals-repository.js +4 -0
- package/dist/tasks/backends/cron.js +80 -43
- package/dist/tasks/backends/launchd.js +28 -15
- package/dist/tasks/backends/schtasks.js +25 -10
- package/dist/tasks/run/load-task.js +1 -1
- package/dist/tasks/scheduler-binding.js +4 -2
- package/dist/tasks/scheduler-invocation.js +127 -235
- package/dist/tasks/scheduler-sync.js +13 -8
- package/dist/tasks/source/parse-task-source.js +22 -126
- package/dist/tasks/source/task-to-v4.js +463 -87
- package/docs/migration/release-notes/0.9.17.md +7 -5
- package/docs/migration/v0.9.1-to-v0.9.2.md +7 -3
- package/docs/reference/cli.md +26 -10
- package/docs/reference/tasks.md +58 -40
- package/package.json +1 -1
- package/dist/tasks/source/task-to-v3.js +0 -507
|
@@ -23,7 +23,11 @@
|
|
|
23
23
|
* `TaskName,Next Run Time,Status` — so the regex anchors on the task
|
|
24
24
|
* name as the leading quoted field.
|
|
25
25
|
* • Task Scheduler runs a task with the account's own environment, so
|
|
26
|
-
* PATH is not carried anywhere
|
|
26
|
+
* PATH is not carried anywhere. The Exec action has no environment of
|
|
27
|
+
* its own: the AKM directory environment a row sets (see
|
|
28
|
+
* `src/tasks/scheduler-invocation.ts`) is a `$env:NAME='value';`
|
|
29
|
+
* assignment ahead of the call operator in the PowerShell command,
|
|
30
|
+
* inherited by the akm process it starts.
|
|
27
31
|
*
|
|
28
32
|
* Tests inject a fake exec + filesystem.
|
|
29
33
|
*/
|
|
@@ -37,7 +41,7 @@ import { getTaskLogDir } from "../../core/paths.js";
|
|
|
37
41
|
import { resolveAkmInvocation } from "../resolve-akm-bin.js";
|
|
38
42
|
import { parseSchedule, translateToSchtasks } from "../schedule.js";
|
|
39
43
|
import { schedulerBindingNativeId, schedulerLogicalBindingId } from "../scheduler-binding.js";
|
|
40
|
-
import {
|
|
44
|
+
import { buildScheduledInvocation, parseScheduledInvocationArgv, scheduledRowEnvironmentEntries, scheduledRowEnvironmentFrom, } from "../scheduler-invocation.js";
|
|
41
45
|
import { escapeXml, nodeExec, nodeFs, normalizeXmlForUtf16File, runOrThrow, } from "./exec-utils.js";
|
|
42
46
|
export const DEFAULT_FOLDER_PREFIX = "\\akm\\";
|
|
43
47
|
const SIGNATURE_PREFIX = "akm:v1:";
|
|
@@ -47,15 +51,13 @@ export function SCHTASKS_BACKEND(options = {}) {
|
|
|
47
51
|
const akmArgv = options.akmArgv ?? resolveAkmInvocation().argv;
|
|
48
52
|
const logDir = options.logDir ?? getTaskLogDir();
|
|
49
53
|
const folder = options.folderPrefix ?? DEFAULT_FOLDER_PREFIX;
|
|
50
|
-
const scheduledContext = options.scheduledContext ?? resolveScheduledTaskContext();
|
|
51
|
-
const defaultContextPath = schedulerContextPath(schedulerContextDescriptor(scheduledContext));
|
|
52
54
|
const userSid = options.userSid ?? resolveCurrentUserSid(exec);
|
|
53
55
|
const taskName = (nativeId) => `${folder}${nativeId}`;
|
|
54
56
|
const xmlFor = (task, opts) => buildSchtasksXml(task, akmArgv, logDir, {
|
|
55
57
|
folderPrefix: folder,
|
|
56
|
-
contextPath: opts?.contextPath ?? defaultContextPath,
|
|
57
58
|
userSid,
|
|
58
59
|
binding: [...(opts?.binding ?? akmArgv)],
|
|
60
|
+
...(opts?.environment ? { environment: opts.environment } : {}),
|
|
59
61
|
});
|
|
60
62
|
const queryXml = (nativeId) => runOrThrow(exec, ["schtasks", "/Query", "/TN", taskName(nativeId), "/XML"], {
|
|
61
63
|
message: (result) => `schtasks /Query /XML for "${taskName(nativeId)}" failed (exit ${result.status}): ${result.stderr || result.stdout || "no output"}.`,
|
|
@@ -116,7 +118,8 @@ export function SCHTASKS_BACKEND(options = {}) {
|
|
|
116
118
|
signature: taskXmlSignature(xml),
|
|
117
119
|
...(parsed.target !== undefined ? { target: parsed.target } : {}),
|
|
118
120
|
binding: parsed.binding,
|
|
119
|
-
contextPath: parsed.contextPath,
|
|
121
|
+
...(parsed.contextPath !== undefined ? { contextPath: parsed.contextPath } : {}),
|
|
122
|
+
...(parsed.environment !== undefined ? { environment: parsed.environment } : {}),
|
|
120
123
|
invocation: parsed.invocation,
|
|
121
124
|
});
|
|
122
125
|
}
|
|
@@ -139,7 +142,16 @@ export function extractSchtasksInvocation(xml) {
|
|
|
139
142
|
const invocationStart = findPowerShellInvocationOperator(commandLine);
|
|
140
143
|
if (invocationStart === undefined)
|
|
141
144
|
return undefined;
|
|
142
|
-
|
|
145
|
+
const parsed = parseScheduledInvocationArgv(parsePowerShellSingleQuotedArgs(commandLine, invocationStart + 1));
|
|
146
|
+
if (!parsed)
|
|
147
|
+
return undefined;
|
|
148
|
+
const variables = {};
|
|
149
|
+
const assignments = commandLine.slice(0, invocationStart);
|
|
150
|
+
for (const assignment of assignments.matchAll(/\$env:([A-Za-z_][A-Za-z0-9_]*)='((?:[^']|'')*)'\s*;/g)) {
|
|
151
|
+
variables[assignment[1]] = assignment[2].replaceAll("''", "'");
|
|
152
|
+
}
|
|
153
|
+
const environment = scheduledRowEnvironmentFrom(variables);
|
|
154
|
+
return environment ? { ...parsed, environment } : parsed;
|
|
143
155
|
}
|
|
144
156
|
function findPowerShellInvocationOperator(script) {
|
|
145
157
|
let inSingleQuote = false;
|
|
@@ -196,8 +208,11 @@ export function buildSchtasksXml(task, akmArgv, logDir, options) {
|
|
|
196
208
|
const folder = options.folderPrefix ?? DEFAULT_FOLDER_PREFIX;
|
|
197
209
|
const now = options.now ? options.now() : new Date();
|
|
198
210
|
const trigger = translateToSchtasks(parseSchedule(task.cron, "schtasks"));
|
|
199
|
-
const
|
|
200
|
-
const
|
|
211
|
+
const argv = buildScheduledInvocation(options.binding ?? akmArgv, task.invocation);
|
|
212
|
+
const assignments = scheduledRowEnvironmentEntries(options.environment)
|
|
213
|
+
.map(([name, value]) => `$env:${name}=${quotePowerShell(value)}; `)
|
|
214
|
+
.join("");
|
|
215
|
+
const script = `${assignments}& ${argv.map((arg) => quotePowerShell(arg)).join(" ")}; exit $LASTEXITCODE`;
|
|
201
216
|
const command = "powershell.exe";
|
|
202
217
|
const args = ["-NoLogo", "-NoProfile", "-NonInteractive", "-Command", script].map(quoteArg).join(" ");
|
|
203
218
|
const nativeId = schedulerBindingNativeId(task);
|
|
@@ -216,7 +231,7 @@ export function buildSchtasksXml(task, akmArgv, logDir, options) {
|
|
|
216
231
|
.replace('<Principal id="Author">', `<Principal id="Author">\n <UserId>${escapeXml(options.userSid)}</UserId>`)
|
|
217
232
|
.replace("{{ENABLED}}", task.enabled ? "true" : "false")
|
|
218
233
|
.replace("{{COMMAND}}", escapeXml(command))
|
|
219
|
-
.replace("{{ARGS}}", escapeXml(args))
|
|
234
|
+
.replace("{{ARGS}}", () => escapeXml(args))
|
|
220
235
|
.replace("{{LOG_PATH}}", escapeXml(logPath));
|
|
221
236
|
}
|
|
222
237
|
function renderSchtasksTrigger(trigger, now) {
|
|
@@ -100,7 +100,7 @@ export async function loadPreparedTask(id, options) {
|
|
|
100
100
|
bundleRoot: bundleDir,
|
|
101
101
|
config,
|
|
102
102
|
// Agent profiles build child env from an allowlist, so freeze the closed
|
|
103
|
-
//
|
|
103
|
+
// AKM directory context the scheduled row set before command preparation.
|
|
104
104
|
...(options.scheduled ? { schedulerContext: scheduledTaskContextEnv() } : {}),
|
|
105
105
|
resolveAsset: async ({ bundle, type, name }) => {
|
|
106
106
|
if (bundle === bundleName) {
|
|
@@ -6,8 +6,10 @@
|
|
|
6
6
|
* contract itself.
|
|
7
7
|
*
|
|
8
8
|
* A binding carries only stable source identity, trigger identity, and the
|
|
9
|
-
* public CLI tail
|
|
10
|
-
*
|
|
9
|
+
* public CLI tail; its install options add the launcher and the AKM
|
|
10
|
+
* directory environment the row sets inline. Source content, action inputs,
|
|
11
|
+
* task environment values, and credentials never cross this boundary or
|
|
12
|
+
* reach an OS scheduler.
|
|
11
13
|
*/
|
|
12
14
|
import { createHash } from "node:crypto";
|
|
13
15
|
import { bundleRefToString, parseBundleRef } from "../core/asset/asset-ref.js";
|
|
@@ -1,13 +1,39 @@
|
|
|
1
1
|
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
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
|
+
/**
|
|
5
|
+
* How a native scheduler row invokes akm.
|
|
6
|
+
*
|
|
7
|
+
* A row is `<launcher…> task run <id> --bundle <bundle> --scheduled
|
|
8
|
+
* [--<input> <value>…]`, or `<launcher…> workflow run <qualified-ref>`, and it
|
|
9
|
+
* carries its own context: the environment the scheduled process needs,
|
|
10
|
+
* set inline by the row itself (a cron `VAR=value` prefix, a launchd
|
|
11
|
+
* `EnvironmentVariables` entry, a PowerShell `$env:` assignment).
|
|
12
|
+
*
|
|
13
|
+
* - `AKM_BUNDLE_DIR`: the working stash of the process that ran
|
|
14
|
+
* `akm task sync`, on every row. The scheduled run uses the same working
|
|
15
|
+
* stash, `--bundle <name>` finds a stash no config names, and sync
|
|
16
|
+
* attributes the row to the installation that wrote it (#846).
|
|
17
|
+
* - `AKM_CONFIG_DIR`, `AKM_DATA_DIR`, `AKM_CACHE_DIR` and `AKM_STATE_DIR`
|
|
18
|
+
* only when the process that ran `akm task sync` set them explicitly. A
|
|
19
|
+
* default resolves at fire time exactly as it does for an interactive
|
|
20
|
+
* command.
|
|
21
|
+
*
|
|
22
|
+
* That is exactly what the `--scheduler-context` descriptor held.
|
|
23
|
+
*
|
|
24
|
+
* `PATH` is the scheduler's own: the crontab's `# akm:env` block, the plist's
|
|
25
|
+
* `EnvironmentVariables`.
|
|
26
|
+
*
|
|
27
|
+
* Rows written by 0.9.0 through 0.9.17-alpha.6 name a
|
|
28
|
+
* `--scheduler-context <descriptor>` file instead. The CLI still accepts that
|
|
29
|
+
* argument and applies the file's environment, so such a row keeps firing
|
|
30
|
+
* until the next `akm task sync` rewrites it.
|
|
31
|
+
*/
|
|
5
32
|
import fs from "node:fs";
|
|
6
33
|
import path from "node:path";
|
|
7
34
|
import { bundleRefToString, parseBundleRef } from "../core/asset/asset-ref.js";
|
|
8
35
|
import { resolveStashDir } from "../core/common.js";
|
|
9
36
|
import { ConfigError } from "../core/errors.js";
|
|
10
|
-
import { getTaskContextDir } from "../core/paths.js";
|
|
11
37
|
import { INPUT_NAME_PATTERN } from "../execution/input-contract.js";
|
|
12
38
|
import { normaliseTaskConceptId } from "./task-id.js";
|
|
13
39
|
export const SCHEDULED_TASK_CONTEXT_KEYS = [
|
|
@@ -18,20 +44,18 @@ export const SCHEDULED_TASK_CONTEXT_KEYS = [
|
|
|
18
44
|
"AKM_STATE_DIR",
|
|
19
45
|
];
|
|
20
46
|
/**
|
|
21
|
-
* Directories a
|
|
22
|
-
*
|
|
23
|
-
* defaults: a
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
* see. At fire time an absent key resolves exactly as it does for an
|
|
28
|
-
* interactive command on the same host.
|
|
47
|
+
* Directories a row carries ONLY when the process that ran `task sync` had
|
|
48
|
+
* them set explicitly in its environment. They are never resolved from
|
|
49
|
+
* defaults: a sync run from inside an app whose environment pointed `$STATE`
|
|
50
|
+
* somewhere private (an OpenCode desktop session, 2026-08 → 2026-09) froze
|
|
51
|
+
* that directory into eight cron rows, and the scheduled improve runs then
|
|
52
|
+
* held their locks in a `locks/` directory no interactive command could see.
|
|
29
53
|
*/
|
|
30
54
|
const EXPLICIT_CONTEXT_KEYS = ["AKM_CONFIG_DIR", "AKM_DATA_DIR", "AKM_CACHE_DIR", "AKM_STATE_DIR"];
|
|
31
55
|
/**
|
|
32
|
-
* The AKM directory context
|
|
33
|
-
*
|
|
34
|
-
*
|
|
56
|
+
* The AKM directory context of the current process. Child environments that
|
|
57
|
+
* are built from an allowlist must forward this closed set explicitly so
|
|
58
|
+
* nested AKM commands stay in the same installation.
|
|
35
59
|
*/
|
|
36
60
|
export function scheduledTaskContextEnv(env = process.env) {
|
|
37
61
|
const out = {};
|
|
@@ -42,141 +66,112 @@ export function scheduledTaskContextEnv(env = process.env) {
|
|
|
42
66
|
}
|
|
43
67
|
return out;
|
|
44
68
|
}
|
|
45
|
-
export const SCHEDULER_CONTEXT_ARG = "--scheduler-context";
|
|
46
69
|
/**
|
|
47
|
-
* The
|
|
48
|
-
*
|
|
49
|
-
* explicitly.
|
|
50
|
-
*
|
|
70
|
+
* The environment every row a sync writes sets inline: `AKM_BUNDLE_DIR`,
|
|
71
|
+
* the syncing process's working stash, plus whichever `AKM_*_DIR` overrides
|
|
72
|
+
* it set explicitly. With no working stash to resolve (no default bundle, no
|
|
73
|
+
* `AKM_BUNDLE_DIR`), rows name their bundle by `--bundle` alone.
|
|
51
74
|
*/
|
|
52
|
-
export function
|
|
53
|
-
const
|
|
75
|
+
export function scheduledRowEnvironment(env = process.env) {
|
|
76
|
+
const out = {};
|
|
77
|
+
try {
|
|
78
|
+
out.AKM_BUNDLE_DIR = path.resolve(resolveStashDir(env));
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
// No working stash: there is no primary bundle to attribute rows to either.
|
|
82
|
+
}
|
|
54
83
|
for (const key of EXPLICIT_CONTEXT_KEYS) {
|
|
55
84
|
const value = env[key]?.trim();
|
|
56
85
|
if (value)
|
|
57
|
-
|
|
86
|
+
out[key] = path.resolve(value);
|
|
58
87
|
}
|
|
59
|
-
return
|
|
60
|
-
}
|
|
61
|
-
/** Build an installed argv from one already-validated public scheduler tail. */
|
|
62
|
-
export function buildScheduledBindingInvocation(akmArgv, contextPath, invocation) {
|
|
63
|
-
const parsed = parsePublicSchedulerInvocation(invocation);
|
|
64
|
-
if (!parsed)
|
|
65
|
-
throw invalidSchedulerInvocation();
|
|
66
|
-
return {
|
|
67
|
-
argv: [...akmArgv, SCHEDULER_CONTEXT_ARG, assertAbsolutePath(contextPath), ...parsed.invocation],
|
|
68
|
-
};
|
|
69
|
-
}
|
|
70
|
-
export function schedulerContextDescriptor(context = resolveScheduledTaskContext()) {
|
|
71
|
-
return { version: 1, environment: canonicalContext(context) };
|
|
72
|
-
}
|
|
73
|
-
export function schedulerContextPath(descriptor) {
|
|
74
|
-
const bytes = serializeDescriptor(descriptor);
|
|
75
|
-
const digest = createHash("sha256").update(bytes).digest("hex");
|
|
76
|
-
// The descriptor lives under the data directory it names when it names
|
|
77
|
-
// one, otherwise under this process's own.
|
|
78
|
-
return path.join(getTaskContextDir({ ...process.env, ...descriptor.environment }), `${digest}.json`);
|
|
88
|
+
return out;
|
|
79
89
|
}
|
|
80
|
-
/**
|
|
81
|
-
export function
|
|
82
|
-
const
|
|
83
|
-
const
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
}
|
|
88
|
-
if (lstatIfExists(file)) {
|
|
89
|
-
if (serializeDescriptor(validateSchedulerContextDescriptor(file)) !== bytes)
|
|
90
|
-
throw invalidSchedulerContext();
|
|
91
|
-
return file;
|
|
92
|
-
}
|
|
93
|
-
const temp = path.join(path.dirname(file), `.${path.basename(file)}.${process.pid}.${Date.now()}.tmp`);
|
|
94
|
-
try {
|
|
95
|
-
fs.writeFileSync(temp, bytes, { encoding: "utf8", mode: 0o600, flag: "wx" });
|
|
96
|
-
try {
|
|
97
|
-
fs.renameSync(temp, file);
|
|
98
|
-
}
|
|
99
|
-
catch (error) {
|
|
100
|
-
if (!lstatIfExists(file) || serializeDescriptor(validateSchedulerContextDescriptor(file)) !== bytes)
|
|
101
|
-
throw error;
|
|
102
|
-
}
|
|
103
|
-
restrictDescriptor(file);
|
|
104
|
-
validateSchedulerContextDescriptor(file);
|
|
105
|
-
}
|
|
106
|
-
finally {
|
|
107
|
-
fs.rmSync(temp, { force: true });
|
|
90
|
+
/** A row environment's entries, in {@link SCHEDULED_TASK_CONTEXT_KEYS} order. */
|
|
91
|
+
export function scheduledRowEnvironmentEntries(environment) {
|
|
92
|
+
const entries = [];
|
|
93
|
+
for (const key of SCHEDULED_TASK_CONTEXT_KEYS) {
|
|
94
|
+
const value = environment?.[key];
|
|
95
|
+
if (value)
|
|
96
|
+
entries.push([key, value]);
|
|
108
97
|
}
|
|
109
|
-
return
|
|
98
|
+
return entries;
|
|
110
99
|
}
|
|
111
|
-
|
|
112
|
-
|
|
100
|
+
/** Keep the {@link SCHEDULED_TASK_CONTEXT_KEYS} a parsed row set; `undefined` when there are none. */
|
|
101
|
+
export function scheduledRowEnvironmentFrom(variables) {
|
|
102
|
+
const out = {};
|
|
113
103
|
for (const key of SCHEDULED_TASK_CONTEXT_KEYS) {
|
|
114
|
-
const value =
|
|
115
|
-
if (value
|
|
116
|
-
|
|
104
|
+
const value = variables[key];
|
|
105
|
+
if (value)
|
|
106
|
+
out[key] = value;
|
|
117
107
|
}
|
|
118
|
-
|
|
119
|
-
env.PATH = descriptor.environment.PATH;
|
|
108
|
+
return Object.keys(out).length > 0 ? out : undefined;
|
|
120
109
|
}
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
110
|
+
const SCHEDULER_CONTEXT_ARG = "--scheduler-context";
|
|
111
|
+
/** The installed argv: the launcher, then one already-validated public scheduler tail. */
|
|
112
|
+
export function buildScheduledInvocation(akmArgv, invocation) {
|
|
113
|
+
const parsed = parsePublicSchedulerInvocation(invocation);
|
|
114
|
+
if (!parsed)
|
|
115
|
+
throw invalidSchedulerInvocation();
|
|
116
|
+
return [...akmArgv, ...parsed.invocation];
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Parse an installed row's argv (after any inline environment): the current
|
|
120
|
+
* shape, the `--scheduler-context <descriptor>` shape 0.9.0 – 0.9.17-alpha.6
|
|
121
|
+
* wrote, or the bare shape before that. A row that carries
|
|
122
|
+
* `--scheduler-context` but does not parse around it is never reread as
|
|
123
|
+
* another shape.
|
|
124
|
+
*/
|
|
125
|
+
export function parseScheduledInvocationArgv(argv) {
|
|
126
|
+
const contextIndex = argv.indexOf(SCHEDULER_CONTEXT_ARG);
|
|
127
|
+
if (contextIndex !== -1) {
|
|
128
|
+
if (contextIndex < 1 || argv.indexOf(SCHEDULER_CONTEXT_ARG, contextIndex + 1) !== -1)
|
|
129
|
+
return undefined;
|
|
130
|
+
const contextPath = argv[contextIndex + 1];
|
|
131
|
+
if (!contextPath)
|
|
132
|
+
return undefined;
|
|
133
|
+
const publicInvocation = parsePublicSchedulerInvocation(argv.slice(contextIndex + 2));
|
|
134
|
+
if (!publicInvocation)
|
|
135
|
+
return undefined;
|
|
136
|
+
return { binding: argv.slice(0, contextIndex), contextPath, ...publicInvocation };
|
|
129
137
|
}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
let descriptorBytes;
|
|
135
|
-
let fd;
|
|
136
|
-
try {
|
|
137
|
-
const noFollow = process.platform === "win32" ? 0 : fs.constants.O_NOFOLLOW;
|
|
138
|
-
fd = fs.openSync(absolute, fs.constants.O_RDONLY | noFollow);
|
|
139
|
-
const stat = fs.fstatSync(fd);
|
|
140
|
-
if (!stat.isFile())
|
|
141
|
-
throw schedulerContextFileError(absolute, "path is not a regular file");
|
|
142
|
-
if (process.platform !== "win32") {
|
|
143
|
-
if (typeof process.getuid !== "function") {
|
|
144
|
-
throw schedulerContextFileError(absolute, "current uid is unavailable for ownership verification");
|
|
145
|
-
}
|
|
146
|
-
const uid = process.getuid();
|
|
147
|
-
if (stat.uid !== uid)
|
|
148
|
-
throw schedulerContextFileError(absolute, `file owner ${stat.uid} does not match uid ${uid}`);
|
|
149
|
-
if ((stat.mode & 0o077) !== 0) {
|
|
150
|
-
throw schedulerContextFileError(absolute, "group or other permissions must be disabled");
|
|
151
|
-
}
|
|
138
|
+
for (let index = 1; index < argv.length - 1; index += 1) {
|
|
139
|
+
if ((argv[index] === "task" || argv[index] === "workflow") && argv[index + 1] === "run") {
|
|
140
|
+
const publicInvocation = parsePublicSchedulerInvocation(argv.slice(index));
|
|
141
|
+
return publicInvocation ? { binding: argv.slice(0, index), ...publicInvocation } : undefined;
|
|
152
142
|
}
|
|
153
|
-
descriptorBytes = fs.readFileSync(fd);
|
|
154
|
-
}
|
|
155
|
-
catch (error) {
|
|
156
|
-
if (error instanceof ConfigError)
|
|
157
|
-
throw error;
|
|
158
|
-
throw schedulerContextFileError(absolute, error instanceof Error ? error.message : String(error));
|
|
159
|
-
}
|
|
160
|
-
finally {
|
|
161
|
-
if (fd !== undefined)
|
|
162
|
-
fs.closeSync(fd);
|
|
163
|
-
}
|
|
164
|
-
const filename = path.basename(absolute);
|
|
165
|
-
const match = /^([a-f0-9]{64})\.json$/.exec(filename);
|
|
166
|
-
const digest = createHash("sha256").update(descriptorBytes).digest("hex");
|
|
167
|
-
if (!match || match[1] !== digest) {
|
|
168
|
-
throw schedulerContextFileError(absolute, "content SHA-256 does not match the descriptor filename");
|
|
169
143
|
}
|
|
144
|
+
return undefined;
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* The environment a `--scheduler-context` descriptor names: its `AKM_*_DIR`
|
|
148
|
+
* keys and, in one written before 0.9.17, `PATH`. It is read as plain JSON —
|
|
149
|
+
* the file is this user's own, next to the data it names.
|
|
150
|
+
*/
|
|
151
|
+
export function readLegacySchedulerContext(file) {
|
|
170
152
|
let parsed;
|
|
171
153
|
try {
|
|
172
|
-
parsed = JSON.parse(
|
|
154
|
+
parsed = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
173
155
|
}
|
|
174
156
|
catch (error) {
|
|
175
|
-
throw
|
|
157
|
+
throw new ConfigError(`Cannot read scheduler context "${file}": ${error instanceof Error ? error.message : String(error)}. ` +
|
|
158
|
+
"Run `akm task sync` to rewrite this scheduled row.", "INVALID_CONFIG_FILE");
|
|
159
|
+
}
|
|
160
|
+
const environment = parsed !== null && typeof parsed === "object" ? parsed.environment : undefined;
|
|
161
|
+
const out = {};
|
|
162
|
+
if (environment === null || typeof environment !== "object")
|
|
163
|
+
return out;
|
|
164
|
+
for (const key of [...SCHEDULED_TASK_CONTEXT_KEYS, "PATH"]) {
|
|
165
|
+
const value = environment[key];
|
|
166
|
+
if (typeof value === "string" && value.length > 0)
|
|
167
|
+
out[key] = value;
|
|
176
168
|
}
|
|
177
|
-
return
|
|
169
|
+
return out;
|
|
178
170
|
}
|
|
179
|
-
/**
|
|
171
|
+
/**
|
|
172
|
+
* Apply and remove a row's `--scheduler-context <descriptor>` before citty
|
|
173
|
+
* parses argv. Only a row written by 0.9.0 – 0.9.17-alpha.6 passes it.
|
|
174
|
+
*/
|
|
180
175
|
export function consumeSchedulerContextArg(argv, env = process.env) {
|
|
181
176
|
const separator = argv.indexOf("--");
|
|
182
177
|
const index = argv.slice(0, separator === -1 ? argv.length : separator).indexOf(SCHEDULER_CONTEXT_ARG);
|
|
@@ -184,37 +179,16 @@ export function consumeSchedulerContextArg(argv, env = process.env) {
|
|
|
184
179
|
return argv;
|
|
185
180
|
const file = argv[index + 1];
|
|
186
181
|
if (!file) {
|
|
187
|
-
throw new ConfigError(`${SCHEDULER_CONTEXT_ARG} requires
|
|
182
|
+
throw new ConfigError(`${SCHEDULER_CONTEXT_ARG} requires a descriptor path.`, "INVALID_CONFIG_FILE");
|
|
188
183
|
}
|
|
189
|
-
|
|
184
|
+
Object.assign(env, readLegacySchedulerContext(file));
|
|
190
185
|
return [...argv.slice(0, index), ...argv.slice(index + 2)];
|
|
191
186
|
}
|
|
192
|
-
/** Parse either the public scheduled task or qualified workflow invocation. */
|
|
193
|
-
export function parseScheduledBindingArgv(argv) {
|
|
194
|
-
const contextIndex = argv.indexOf(SCHEDULER_CONTEXT_ARG);
|
|
195
|
-
if (contextIndex < 1 || argv.indexOf(SCHEDULER_CONTEXT_ARG, contextIndex + 1) !== -1)
|
|
196
|
-
return undefined;
|
|
197
|
-
const contextPath = argv[contextIndex + 1];
|
|
198
|
-
if (!contextPath)
|
|
199
|
-
return undefined;
|
|
200
|
-
const publicInvocation = parsePublicSchedulerInvocation(argv.slice(contextIndex + 2));
|
|
201
|
-
if (!publicInvocation)
|
|
202
|
-
return undefined;
|
|
203
|
-
return {
|
|
204
|
-
binding: [...argv.slice(0, contextIndex)],
|
|
205
|
-
contextPath: assertAbsolutePath(contextPath),
|
|
206
|
-
invocation: publicInvocation.invocation,
|
|
207
|
-
...(publicInvocation.target !== undefined ? { target: publicInvocation.target } : {}),
|
|
208
|
-
};
|
|
209
|
-
}
|
|
210
187
|
/**
|
|
211
|
-
* Parse just the public `task run …` / `workflow run …` tail,
|
|
212
|
-
*
|
|
213
|
-
* pre-`--scheduler-context` invocation still sitting inside akm's own
|
|
214
|
-
* ownership-marked block (see `extractCronInvocation` in
|
|
215
|
-
* `src/tasks/backends/cron.ts`) without re-implementing this grammar.
|
|
188
|
+
* Parse just the public `task run …` / `workflow run …` tail, the part of a
|
|
189
|
+
* row that names what it runs.
|
|
216
190
|
*/
|
|
217
|
-
|
|
191
|
+
function parsePublicSchedulerInvocation(invocation) {
|
|
218
192
|
if (invocation[0] === "task" && invocation[1] === "run" && invocation[2]) {
|
|
219
193
|
try {
|
|
220
194
|
if (normaliseTaskConceptId(invocation[2]) !== invocation[2])
|
|
@@ -308,88 +282,6 @@ function isValidSchedulerInputFlagTail(tail) {
|
|
|
308
282
|
}
|
|
309
283
|
return true;
|
|
310
284
|
}
|
|
311
|
-
function canonicalContext(input) {
|
|
312
|
-
if (Object.keys(input).some((key) => !SCHEDULED_TASK_CONTEXT_KEYS.includes(key))) {
|
|
313
|
-
throw invalidSchedulerContext();
|
|
314
|
-
}
|
|
315
|
-
const context = {};
|
|
316
|
-
for (const key of SCHEDULED_TASK_CONTEXT_KEYS) {
|
|
317
|
-
const value = input[key];
|
|
318
|
-
if (value === undefined) {
|
|
319
|
-
if (key === "AKM_BUNDLE_DIR")
|
|
320
|
-
throw invalidSchedulerContext();
|
|
321
|
-
continue;
|
|
322
|
-
}
|
|
323
|
-
if (typeof value !== "string" ||
|
|
324
|
-
value.trim().length === 0 ||
|
|
325
|
-
containsControlCharacter(value) ||
|
|
326
|
-
(!path.posix.isAbsolute(value) && !path.win32.isAbsolute(value))) {
|
|
327
|
-
throw invalidSchedulerContext();
|
|
328
|
-
}
|
|
329
|
-
context[key] = value;
|
|
330
|
-
}
|
|
331
|
-
return context;
|
|
332
|
-
}
|
|
333
|
-
function canonicalDescriptor(input) {
|
|
334
|
-
if (typeof input !== "object" || input === null || Array.isArray(input))
|
|
335
|
-
throw invalidSchedulerContext();
|
|
336
|
-
const record = input;
|
|
337
|
-
if (Object.keys(record).length !== 2 || record.version !== 1)
|
|
338
|
-
throw invalidSchedulerContext();
|
|
339
|
-
const rawEnvironment = record.environment;
|
|
340
|
-
if (typeof rawEnvironment !== "object" || rawEnvironment === null || Array.isArray(rawEnvironment)) {
|
|
341
|
-
throw invalidSchedulerContext();
|
|
342
|
-
}
|
|
343
|
-
const { PATH, ...rawContext } = rawEnvironment;
|
|
344
|
-
const context = canonicalContext(rawContext);
|
|
345
|
-
return {
|
|
346
|
-
version: 1,
|
|
347
|
-
environment: PATH === undefined ? context : { ...context, PATH: validatePathValue(PATH) },
|
|
348
|
-
};
|
|
349
|
-
}
|
|
350
|
-
function serializeDescriptor(descriptor) {
|
|
351
|
-
return `${JSON.stringify(canonicalDescriptor(descriptor))}\n`;
|
|
352
|
-
}
|
|
353
|
-
function validatePathValue(value) {
|
|
354
|
-
if (typeof value !== "string" || containsControlCharacter(value))
|
|
355
|
-
throw invalidSchedulerContext();
|
|
356
|
-
return value;
|
|
357
|
-
}
|
|
358
|
-
function assertAbsolutePath(value) {
|
|
359
|
-
if (containsControlCharacter(value) || (!path.posix.isAbsolute(value) && !path.win32.isAbsolute(value))) {
|
|
360
|
-
throw invalidSchedulerContext();
|
|
361
|
-
}
|
|
362
|
-
return value;
|
|
363
|
-
}
|
|
364
|
-
function restrictDescriptor(file) {
|
|
365
|
-
if (process.platform !== "win32")
|
|
366
|
-
fs.chmodSync(file, 0o600);
|
|
367
|
-
}
|
|
368
|
-
function lstatIfExists(file) {
|
|
369
|
-
try {
|
|
370
|
-
return fs.lstatSync(file);
|
|
371
|
-
}
|
|
372
|
-
catch (error) {
|
|
373
|
-
if (error.code === "ENOENT")
|
|
374
|
-
return undefined;
|
|
375
|
-
throw error;
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
function schedulerContextFileError(file, reason) {
|
|
379
|
-
return new ConfigError(`Invalid scheduler context descriptor "${file}": ${reason}.`, "INVALID_CONFIG_FILE");
|
|
380
|
-
}
|
|
381
|
-
function containsControlCharacter(value) {
|
|
382
|
-
for (const char of value) {
|
|
383
|
-
const code = char.codePointAt(0) ?? 0;
|
|
384
|
-
if (code < 0x20 || (code >= 0x7f && code <= 0x9f))
|
|
385
|
-
return true;
|
|
386
|
-
}
|
|
387
|
-
return false;
|
|
388
|
-
}
|
|
389
|
-
function invalidSchedulerContext() {
|
|
390
|
-
return new ConfigError("Invalid scheduler context; expected AKM_BUNDLE_DIR as an absolute path, " +
|
|
391
|
-
`optionally with ${EXPLICIT_CONTEXT_KEYS.join(", ")} as absolute paths.`, "INVALID_CONFIG_FILE");
|
|
392
|
-
}
|
|
393
285
|
function invalidSchedulerInvocation() {
|
|
394
286
|
return new ConfigError("Invalid scheduler invocation; expected public " +
|
|
395
287
|
"`task run <id> [--bundle <bundle>] --scheduled [--<input> <value>…]` or `workflow run <qualified-ref>` argv.", "INVALID_CONFIG_FILE");
|
|
@@ -263,9 +263,10 @@ export function scheduledInvocationBundle(invocation) {
|
|
|
263
263
|
/**
|
|
264
264
|
* The scope that owns an installed row, or none. A row written before
|
|
265
265
|
* `--bundle` existed names no bundle and belongs to the primary scope
|
|
266
|
-
* unless
|
|
267
|
-
* to the scope of that name — for the primary scope only
|
|
268
|
-
*
|
|
266
|
+
* unless the bundle path it carries says otherwise; a row that names a
|
|
267
|
+
* bundle belongs to the scope of that name — for the primary scope only
|
|
268
|
+
* when its `AKM_BUNDLE_DIR` (inline, or in an older row's descriptor) also
|
|
269
|
+
* proves the path.
|
|
269
270
|
*/
|
|
270
271
|
export function installedRowScope(row, scopes) {
|
|
271
272
|
const bundle = row.target ?? scheduledInvocationBundle(row.invocation);
|
|
@@ -393,12 +394,13 @@ export function planSchedulerSync(input) {
|
|
|
393
394
|
failures: Object.freeze(failures),
|
|
394
395
|
});
|
|
395
396
|
}
|
|
396
|
-
/** An installed row keeps its own launcher unless `rebind`;
|
|
397
|
+
/** An installed row keeps its own launcher unless `rebind`; its environment always follows the current policy. */
|
|
397
398
|
function installOptionsFor(input, current) {
|
|
398
399
|
if (current && !input.rebind) {
|
|
400
|
+
const environment = input.installOptions?.environment ?? current.environment;
|
|
399
401
|
return Object.freeze({
|
|
400
402
|
binding: Object.freeze([...current.binding]),
|
|
401
|
-
|
|
403
|
+
...(environment !== undefined ? { environment } : {}),
|
|
402
404
|
});
|
|
403
405
|
}
|
|
404
406
|
return input.installOptions ? Object.freeze({ ...input.installOptions }) : undefined;
|
|
@@ -417,9 +419,12 @@ function installedRowRef(row, scope) {
|
|
|
417
419
|
/** Where an installed row was scheduled from, for messages. */
|
|
418
420
|
export function installedRowOwner(row) {
|
|
419
421
|
const bundle = row.target ?? scheduledInvocationBundle(row.invocation);
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
422
|
+
const installation = row.ownerBundlePath
|
|
423
|
+
? `the installation whose working stash is ${JSON.stringify(row.ownerBundlePath)}`
|
|
424
|
+
: undefined;
|
|
425
|
+
if (bundle)
|
|
426
|
+
return installation ? `bundle ${JSON.stringify(bundle)} of ${installation}` : `bundle ${JSON.stringify(bundle)}`;
|
|
427
|
+
return installation ?? "an installation this sync cannot attribute";
|
|
423
428
|
}
|
|
424
429
|
function foreignRowMessage(nativeId, row) {
|
|
425
430
|
return `Scheduler id ${JSON.stringify(nativeId)} is already scheduled from ${installedRowOwner(row)}; desired source ids must not collide across bundles. Leaving it out of this sync.`;
|