akm-cli 0.9.11 → 0.9.13
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 +227 -0
- package/STABILITY.md +6 -1
- package/dist/assets/hints/cli-hints-full.md +1 -1
- package/dist/assets/improve-strategies/consolidate.json +1 -1
- package/dist/assets/improve-strategies/default.json +1 -1
- package/dist/assets/improve-strategies/thorough.json +1 -2
- package/dist/assets/workflows/workflow-template.md +4 -0
- package/dist/cli/shared.js +16 -4
- package/dist/cli.js +15 -13
- package/dist/commands/agent/agent-dispatch.js +8 -0
- package/dist/commands/command/execution-source-loader.js +25 -22
- package/dist/commands/command/portable-template.js +4 -26
- package/dist/commands/config-cli.js +10 -4
- package/dist/commands/env/env-binding.js +10 -3
- package/dist/commands/env/env-cli.js +7 -0
- package/dist/commands/env/secret-cli.js +15 -4
- package/dist/commands/health/checks.js +186 -71
- package/dist/commands/health.js +16 -4
- package/dist/commands/improve/distill/quality-gate.js +2 -2
- package/dist/commands/improve/distill.js +28 -12
- package/dist/commands/improve/execution.js +1 -2
- package/dist/commands/improve/extract.js +82 -56
- package/dist/commands/improve/improve-strategies.js +26 -8
- package/dist/commands/improve/improve.js +14 -0
- package/dist/commands/improve/preparation.js +9 -6
- package/dist/commands/improve/reflect.js +61 -77
- package/dist/commands/lint/base-linter.js +10 -0
- package/dist/commands/lint/index.js +3 -1
- package/dist/commands/migrate-cli.js +6 -4
- package/dist/commands/proposal/drain-policies.js +22 -2
- package/dist/commands/proposal/drain.js +48 -6
- package/dist/commands/proposal/proposal-cli.js +1 -0
- package/dist/commands/proposal/repository.js +4 -4
- package/dist/commands/proposal/validators/proposal-quality-validators.js +23 -2
- package/dist/commands/proposal/validators/proposals.js +10 -19
- package/dist/commands/read/show.js +42 -31
- package/dist/commands/registry-cli.js +4 -2
- package/dist/commands/sources/init.js +4 -8
- package/dist/commands/sources/self-update.js +2 -2
- package/dist/commands/sources/source-clone.js +5 -7
- package/dist/commands/sources/sources-cli.js +3 -5
- package/dist/commands/tasks/tasks-cli.js +4 -12
- package/dist/commands/tasks/tasks.js +38 -35
- package/dist/commands/workflow-cli.js +17 -15
- package/dist/core/activation-policy.js +31 -3
- package/dist/core/adapter/execution-source.js +39 -11
- package/dist/core/asset/stash-meta.js +7 -41
- package/dist/core/common.js +8 -17
- package/dist/core/config/config-schema.js +3 -23
- package/dist/core/config/config-walker.js +56 -6
- package/dist/core/config/config.js +42 -17
- package/dist/core/config/legacy-source-shape-shim.js +79 -0
- package/dist/core/config/schema/embedding.js +2 -2
- package/dist/core/config/schema/engines.js +2 -2
- package/dist/core/config/schema/index-config.js +19 -21
- package/dist/core/config/schema/primitives.js +27 -10
- package/dist/core/config/schema/sources-bundles.js +1 -6
- package/dist/core/errors.js +4 -3
- package/dist/core/improve-types.js +17 -0
- package/dist/core/json-schema.js +1 -11
- package/dist/core/maintenance-barrier.js +17 -2
- package/dist/core/paths.js +12 -15
- package/dist/core/state/migrations.js +28 -0
- package/dist/core/state-db.js +28 -1
- package/dist/core/write-source.js +6 -6
- package/dist/indexer/bundle-identity-guard.js +3 -0
- package/dist/indexer/ensure-index.js +5 -0
- package/dist/indexer/indexer.js +11 -3
- package/dist/indexer/lookup/adapter-concept-owner.js +14 -3
- package/dist/indexer/passes/metadata.js +16 -5
- package/dist/indexer/search/search-fields.js +1 -30
- package/dist/integrations/agent/engine-resolution.js +15 -1
- package/dist/integrations/agent/model-map.js +16 -10
- package/dist/integrations/agent/prompts.js +13 -6
- package/dist/integrations/lockfile.js +22 -7
- package/dist/llm/client.js +28 -8
- package/dist/llm/embedders/remote.js +3 -2
- package/dist/llm/index-passes.js +3 -2
- package/dist/output/shapes/passthrough.js +9 -3
- package/dist/output/shapes.js +50 -3
- package/dist/output/text/proposal-format.js +5 -0
- package/dist/output/text/workflow-format.js +8 -1
- package/dist/scripts/akm-migrate-node.js +1737 -1392
- package/dist/scripts/akm-migrate.js +1736 -1391
- package/dist/setup/setup.js +14 -21
- package/dist/sources/include.js +150 -20
- package/dist/sources/providers/git-install.js +14 -12
- package/dist/sources/providers/git-provider.js +3 -3
- package/dist/sources/snapshot-fetchers/website-ingest.js +54 -16
- package/dist/sources/website-url.js +12 -4
- package/dist/storage/engines/sqlite-migrations.js +40 -10
- package/dist/storage/like-pattern.js +7 -0
- package/dist/storage/repositories/extract-sessions-repository.js +23 -0
- package/dist/storage/repositories/index-connection.js +27 -10
- package/dist/storage/repositories/index-entry-schema.js +19 -2
- package/dist/storage/repositories/index-schema.js +30 -9
- package/dist/storage/repositories/proposals-repository.js +2 -1
- package/dist/storage/repositories/task-history-repository.js +14 -7
- package/dist/storage/repositories/workflow-runs-repository.js +133 -11
- package/dist/storage/sqlite-read-snapshot.js +11 -9
- package/dist/tasks/backends/cron.js +34 -5
- package/dist/tasks/backends/launchd.js +23 -26
- package/dist/tasks/backends/schtasks.js +50 -3
- package/dist/tasks/frozen-script.js +2 -0
- package/dist/tasks/prepare/prepare.js +2 -7
- package/dist/tasks/prepare/script-capture.js +38 -6
- package/dist/tasks/schedule.js +154 -13
- package/dist/tasks/source/task-source-v3-frozen.js +0 -1
- package/dist/tasks/source/task-source-v4.js +0 -1
- package/dist/workflows/exec/child-workflow.js +2 -3
- package/dist/workflows/exec/exec-unit.js +3 -4
- package/dist/workflows/exec/run-workflow.js +20 -11
- package/dist/workflows/exec/step-work.js +76 -56
- package/dist/workflows/freeze/resolve-steps.js +19 -11
- package/dist/workflows/freeze/source-freeze.js +7 -0
- package/dist/workflows/freeze/targets/child-workflow.js +12 -18
- package/dist/workflows/freeze/targets/command.js +14 -2
- package/dist/workflows/ir/environment-v4.js +4 -2
- package/dist/workflows/ir/freeze-v4.js +2 -5
- package/dist/workflows/ir/plan-hash.js +0 -3
- package/dist/workflows/ir/schema-v4.js +14 -9
- package/dist/workflows/ir/schema.js +1 -3
- package/dist/workflows/parser.js +1 -1
- package/dist/workflows/resource-limits.js +35 -48
- package/dist/workflows/runtime/plan-classifier.js +89 -41
- package/dist/workflows/runtime/run-outputs.js +1 -21
- package/dist/workflows/runtime/runs.js +104 -154
- package/dist/workflows/source-files.js +28 -54
- package/dist/workflows/source-ir/program.js +2 -2
- package/dist/workflows/source-ir/semantics.js +5 -23
- package/docs/migration/v0.9.1-to-v0.9.2.md +20 -0
- package/docs/reference/cli.md +92 -17
- package/package.json +1 -1
- package/schemas/akm-config.json +5 -10
|
@@ -31,7 +31,7 @@ import { NotFoundError, rethrowIfDataDirUnreadable, rethrowIfTestIsolationError,
|
|
|
31
31
|
import { appendEvent } from "../../core/events.js";
|
|
32
32
|
import { SCRIPT_EXTENSIONS } from "../../core/recognition-util.js";
|
|
33
33
|
import { presentationFor } from "../../core/type-presentation.js";
|
|
34
|
-
import { warn } from "../../core/warn.js";
|
|
34
|
+
import { warn, warnOnce } from "../../core/warn.js";
|
|
35
35
|
import { hasGraphData } from "../../indexer/db/graph-db.js";
|
|
36
36
|
import { listRelatedPathsForFile } from "../../indexer/graph/graph-boost.js";
|
|
37
37
|
import { extractGraphForSingleFile } from "../../indexer/graph/graph-extraction.js";
|
|
@@ -71,12 +71,11 @@ export async function akmShowUnified(input) {
|
|
|
71
71
|
if (metaRef)
|
|
72
72
|
return showStashMeta(metaRef);
|
|
73
73
|
}
|
|
74
|
-
// Env/secret bodies have no safe fragment surface
|
|
75
|
-
//
|
|
76
|
-
//
|
|
77
|
-
//
|
|
78
|
-
|
|
79
|
-
assertSensitiveFragmentUnsupported(parseBundleRef(ref));
|
|
74
|
+
// Env/secret bodies have no safe fragment surface, and a fragment cannot
|
|
75
|
+
// widen what the env/secret renderers expose: both always omit the body
|
|
76
|
+
// (env — key names only; secret — never rendered), fragment or not. Warn
|
|
77
|
+
// and ignore the fragment rather than refusing the whole show.
|
|
78
|
+
warnSensitiveFragmentUnsupported(parseBundleRef(ref));
|
|
80
79
|
// Auto-index when stale so the index is current before lookup.
|
|
81
80
|
const { primarySource } = resolveReadSources();
|
|
82
81
|
await ensurePrimaryIndexForRead(primarySource);
|
|
@@ -177,7 +176,7 @@ function enforceScopeOrThrow(filePath, ref, scope) {
|
|
|
177
176
|
/** @internal Use akmShowUnified() for all external callers. */
|
|
178
177
|
export async function showLocal(input) {
|
|
179
178
|
const parsed = parseBundleRef(input.ref);
|
|
180
|
-
|
|
179
|
+
warnSensitiveFragmentUnsupported(parsed);
|
|
181
180
|
const assetParts = typeNameFromConceptId(parsed.conceptId);
|
|
182
181
|
const config = loadConfig();
|
|
183
182
|
const allSources = resolveSourceEntries(input.stashDir);
|
|
@@ -194,12 +193,19 @@ export async function showLocal(input) {
|
|
|
194
193
|
`Stash "${parsed.bundle}" is not installed. Run: ${installCmd}`);
|
|
195
194
|
}
|
|
196
195
|
if (!indexedEntry && !resolution.owner) {
|
|
197
|
-
const
|
|
198
|
-
if (
|
|
199
|
-
const displayExtension =
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
196
|
+
const unrecognized = findUnrecognizedScriptSource(assetParts, searchSources);
|
|
197
|
+
if (unrecognized) {
|
|
198
|
+
const displayExtension = unrecognized.extension || "no extension";
|
|
199
|
+
warn(`Script ref "${makeBundleRef(parsed.bundle, parsed.conceptId)}" has extension "${displayExtension}", which is outside the recognized set used for indexing (${[...SCRIPT_EXTENSIONS].join(", ")}); showing it as plain text.`);
|
|
200
|
+
const fileCtx = buildFileContext(unrecognized.sourceRoot, unrecognized.path);
|
|
201
|
+
const renderer = await getRenderer("script-source");
|
|
202
|
+
if (renderer) {
|
|
203
|
+
const match = { type: "script", specificity: 0, renderer: "script-source" };
|
|
204
|
+
const renderCtx = buildRenderContext(fileCtx, match, allSourceDirs);
|
|
205
|
+
const response = renderer.buildShowResponse(renderCtx);
|
|
206
|
+
response.name = assetParts?.name ?? response.name;
|
|
207
|
+
return response;
|
|
208
|
+
}
|
|
203
209
|
}
|
|
204
210
|
}
|
|
205
211
|
if (!indexedEntry || !assetPath) {
|
|
@@ -242,9 +248,11 @@ export async function showLocal(input) {
|
|
|
242
248
|
response = renderer.buildShowResponse(renderCtx);
|
|
243
249
|
if (parsed.fragment !== undefined) {
|
|
244
250
|
if (!match.renderer.endsWith("-md")) {
|
|
245
|
-
|
|
251
|
+
warn(`Fragment "#${parsed.fragment}" was ignored: ${makeBundleRef(parsed.bundle, parsed.conceptId)} is not a Markdown document, so heading fragments do not apply. Showing the whole asset.`);
|
|
252
|
+
}
|
|
253
|
+
else {
|
|
254
|
+
applyMarkdownFragment(response, fileCtx.content(), parsed.fragment, presentedName);
|
|
246
255
|
}
|
|
247
|
-
applyMarkdownFragment(response, fileCtx.content(), parsed.fragment, presentedName);
|
|
248
256
|
}
|
|
249
257
|
}
|
|
250
258
|
}
|
|
@@ -318,22 +326,22 @@ export async function showLocal(input) {
|
|
|
318
326
|
}
|
|
319
327
|
return fullResponse;
|
|
320
328
|
}
|
|
321
|
-
/**
|
|
322
|
-
|
|
329
|
+
/**
|
|
330
|
+
* Warn and ignore body fragments for namespaces whose authored bytes are
|
|
331
|
+
* sensitive. `warnOnce`-keyed on the exact ref: `akmShowUnified` calls this
|
|
332
|
+
* before delegating to `showLocal`, which calls it again as its own
|
|
333
|
+
* defense-in-depth for callers that use `showLocal` directly — a single
|
|
334
|
+
* request must not print the same warning twice.
|
|
335
|
+
*/
|
|
336
|
+
function warnSensitiveFragmentUnsupported(ref) {
|
|
323
337
|
if (ref.fragment === undefined)
|
|
324
338
|
return;
|
|
325
339
|
const type = typeNameFromConceptId(ref.conceptId)?.type;
|
|
326
340
|
if (type !== "env" && type !== "secret")
|
|
327
341
|
return;
|
|
328
|
-
|
|
342
|
+
warnOnce(`sensitive-fragment:${makeBundleRef(ref.bundle, ref.conceptId)}#${ref.fragment}`, `Fragment "#${ref.fragment}" was ignored: sensitive ${type} assets do not expose body fragments. Showing ${makeBundleRef(ref.bundle, ref.conceptId)} in full.`);
|
|
329
343
|
}
|
|
330
|
-
|
|
331
|
-
* Return the unsupported extension only when the exact canonical AKM script
|
|
332
|
-
* path exists as a contained regular file. Physical owner arbitration runs
|
|
333
|
-
* first; this is a diagnostic for its miss, never an alternate owner or
|
|
334
|
-
* runnable-file classifier. No authored bytes are read.
|
|
335
|
-
*/
|
|
336
|
-
function existingUnsupportedScriptExtension(assetParts, sources) {
|
|
344
|
+
function findUnrecognizedScriptSource(assetParts, sources) {
|
|
337
345
|
if (assetParts?.type !== "script")
|
|
338
346
|
return undefined;
|
|
339
347
|
const extension = path.extname(assetParts.name);
|
|
@@ -357,7 +365,7 @@ function existingUnsupportedScriptExtension(assetParts, sources) {
|
|
|
357
365
|
const realCandidate = fs.realpathSync(candidate);
|
|
358
366
|
if (!isWithin(realCandidate, realRoot) || !fs.statSync(realCandidate).isFile())
|
|
359
367
|
continue;
|
|
360
|
-
return extension;
|
|
368
|
+
return { extension, path: realCandidate, sourceRoot: realRoot };
|
|
361
369
|
}
|
|
362
370
|
catch {
|
|
363
371
|
// Missing, unreadable, dangling, or otherwise unsafe paths remain normal
|
|
@@ -447,7 +455,7 @@ async function maybeExtractGraphInline(config, sourceStashDir, assetPath) {
|
|
|
447
455
|
export async function showByRef(ref) {
|
|
448
456
|
const parsed = parseBundleRef(ref);
|
|
449
457
|
if (parsed.fragment !== undefined) {
|
|
450
|
-
|
|
458
|
+
warn(`Fragment "#${parsed.fragment}" was ignored by raw show: ${ref}. Returning the whole file.`);
|
|
451
459
|
}
|
|
452
460
|
const entry = await lookupBundleRef(parsed);
|
|
453
461
|
if (!entry) {
|
|
@@ -488,12 +496,15 @@ function indexedMatch(entry, renderer) {
|
|
|
488
496
|
return { type: entry.type, specificity: Number.MAX_SAFE_INTEGER, renderer, meta: { name: entry.name } };
|
|
489
497
|
}
|
|
490
498
|
function buildIndexedProjectionResponse(entry, assetPath, fragment) {
|
|
491
|
-
|
|
492
|
-
|
|
499
|
+
const isMarkdown = path.extname(assetPath).toLowerCase() === ".md";
|
|
500
|
+
if (fragment !== undefined && !isMarkdown) {
|
|
501
|
+
warn(`Fragment "#${fragment}" was ignored: ${entry.conceptId} is not a Markdown document, so heading fragments do not apply. Showing the whole asset.`);
|
|
493
502
|
}
|
|
494
503
|
const raw = fs.readFileSync(assetPath, "utf8");
|
|
495
504
|
const parsed = parseFrontmatter(raw);
|
|
496
|
-
const content = fragment
|
|
505
|
+
const content = fragment !== undefined && isMarkdown
|
|
506
|
+
? requireMarkdownSection(parsed.content, fragment, entry.name).content
|
|
507
|
+
: parsed.content;
|
|
497
508
|
const description = entry.document?.description ?? asNonEmptyString(parsed.data.description);
|
|
498
509
|
const tags = entry.document?.tags ??
|
|
499
510
|
(Array.isArray(parsed.data.tags)
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import { defineGroupCommand, defineJsonCommand, output } from "../cli/shared.js";
|
|
5
5
|
import { getEffectiveRegistries, loadUserConfig, mutateConfig } from "../core/config/config.js";
|
|
6
6
|
import { NotFoundError, UsageError } from "../core/errors.js";
|
|
7
|
-
import { formatRegistryUrl, hasRegistryUrlCredentials,
|
|
7
|
+
import { formatRegistryLabel, formatRegistryUrl, hasRegistryUrlCredentials, registryEntryForOutput, } from "../core/registry-url.js";
|
|
8
8
|
import { warn } from "../core/warn.js";
|
|
9
9
|
export const registryCommand = defineGroupCommand({
|
|
10
10
|
meta: { name: "registry", description: "Manage bundle registries" },
|
|
@@ -33,7 +33,9 @@ export const registryCommand = defineGroupCommand({
|
|
|
33
33
|
},
|
|
34
34
|
async run({ args }) {
|
|
35
35
|
if (hasRegistryUrlCredentials(args.url)) {
|
|
36
|
-
|
|
36
|
+
warn(`Registry ${formatRegistryLabel({ url: args.url, name: args.name })} was added, but its URL's ` +
|
|
37
|
+
"username/password is ignored: authenticated registries are not supported by the built-in " +
|
|
38
|
+
"static-index or skills-sh providers.");
|
|
37
39
|
}
|
|
38
40
|
if (!args.url.startsWith("http")) {
|
|
39
41
|
throw new UsageError("Registry URL must start with http:// or https://");
|
|
@@ -12,8 +12,8 @@ import fs from "node:fs";
|
|
|
12
12
|
import path from "node:path";
|
|
13
13
|
import { stashDirNames } from "../../core/asset/asset-placement.js";
|
|
14
14
|
import { mutateConfig } from "../../core/config/config.js";
|
|
15
|
-
import { ConfigError } from "../../core/errors.js";
|
|
16
15
|
import { assertSafeStashDir, getConfigPath, getDefaultStashDir } from "../../core/paths.js";
|
|
16
|
+
import { warnOnce } from "../../core/warn.js";
|
|
17
17
|
import { primaryBundlePath, withPrimaryBundle } from "./bundle-config-ops.js";
|
|
18
18
|
import { copyStashSkeleton, ensureStashGitignore, scaffoldStashMeta } from "./stash-skeleton.js";
|
|
19
19
|
/**
|
|
@@ -37,11 +37,9 @@ import { copyStashSkeleton, ensureStashGitignore, scaffoldStashMeta } from "./st
|
|
|
37
37
|
function assertInitSandbox(stashDir, dirExplicitlyProvided) {
|
|
38
38
|
if (!dirExplicitlyProvided)
|
|
39
39
|
return; // Only guard explicit --dir, not default HOME resolution.
|
|
40
|
-
const isUnderTest =
|
|
40
|
+
const isUnderTest = process.env.BUN_TEST === "1" || process.env.NODE_ENV === "test";
|
|
41
41
|
if (!isUnderTest)
|
|
42
42
|
return;
|
|
43
|
-
if (process.env.AKM_FORCE_INIT_TMP_STASH === "1")
|
|
44
|
-
return;
|
|
45
43
|
const isTmp = stashDir.startsWith("/tmp/") ||
|
|
46
44
|
stashDir === "/tmp" ||
|
|
47
45
|
stashDir.startsWith("/var/tmp/") ||
|
|
@@ -50,10 +48,8 @@ function assertInitSandbox(stashDir, dirExplicitlyProvided) {
|
|
|
50
48
|
stashDir.startsWith("/private/tmp/");
|
|
51
49
|
if (!isTmp)
|
|
52
50
|
return;
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
function isUnderTestRunner() {
|
|
56
|
-
return process.env.BUN_TEST === "1" || process.env.NODE_ENV === "test";
|
|
51
|
+
warnOnce(`init-tmp-stash:${stashDir}`, `Persisting --dir stashDir to a temporary path (${stashDir}) while a test-runner env var (BUN_TEST/NODE_ENV=test) is set; ` +
|
|
52
|
+
"the OS may reap this path, leaving the next run pointing at a deleted bundle.");
|
|
57
53
|
}
|
|
58
54
|
// ── Test seam ────────────────────────────────────────────────────────────────
|
|
59
55
|
// Swap-and-restore override. Inert in production; only tests call the setter.
|
|
@@ -346,8 +346,8 @@ export async function performUpgrade(check, opts, dependencies) {
|
|
|
346
346
|
}
|
|
347
347
|
}
|
|
348
348
|
if (fs.existsSync(backupPath)) {
|
|
349
|
-
|
|
350
|
-
|
|
349
|
+
warn(`A previous upgrade left a stale backup at ${backupPath}; overwriting it.`);
|
|
350
|
+
removeFileBestEffort(backupPath);
|
|
351
351
|
}
|
|
352
352
|
try {
|
|
353
353
|
if (IS_WINDOWS)
|
|
@@ -7,6 +7,7 @@ import path from "node:path";
|
|
|
7
7
|
import { stashDirFor } from "../../core/asset/asset-placement.js";
|
|
8
8
|
import { isBundleSlug } from "../../core/asset/asset-ref.js";
|
|
9
9
|
import { displayRef, parseQualifiedRefInput } from "../../core/asset/resolve-ref.js";
|
|
10
|
+
import { isWithin } from "../../core/common.js";
|
|
10
11
|
import { loadConfig } from "../../core/config/config.js";
|
|
11
12
|
import { ConfigError, NotFoundError, UsageError } from "../../core/errors.js";
|
|
12
13
|
import { defaultBundleForTarget } from "../../core/mutation-target.js";
|
|
@@ -224,18 +225,15 @@ function lstatIfExists(filePath) {
|
|
|
224
225
|
}
|
|
225
226
|
function assertNoDestinationSymlinkParent(root, destination) {
|
|
226
227
|
const resolvedRoot = path.resolve(root);
|
|
227
|
-
const
|
|
228
|
+
const parentDir = path.dirname(path.resolve(destination));
|
|
229
|
+
const relativeParent = path.relative(resolvedRoot, parentDir);
|
|
228
230
|
if (relativeParent === "" || relativeParent === ".")
|
|
229
231
|
return;
|
|
230
232
|
if (relativeParent.startsWith("..") || path.isAbsolute(relativeParent)) {
|
|
231
233
|
throw new UsageError(`Clone destination escapes the selected target: ${destination}.`, "PATH_ESCAPE_VIOLATION");
|
|
232
234
|
}
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
current = path.join(current, segment);
|
|
236
|
-
if (lstatIfExists(current)?.isSymbolicLink()) {
|
|
237
|
-
throw new UsageError(`Clone destination has a symbolic-link parent outside the selected target boundary: ${current}.`, "PATH_ESCAPE_VIOLATION");
|
|
238
|
-
}
|
|
235
|
+
if (!isWithin(parentDir, resolvedRoot)) {
|
|
236
|
+
throw new UsageError(`Clone destination has a symbolic-link parent outside the selected target boundary: ${parentDir}.`, "PATH_ESCAPE_VIOLATION");
|
|
239
237
|
}
|
|
240
238
|
}
|
|
241
239
|
function listTreeFiles(root) {
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
*/
|
|
28
28
|
import { defineCommand } from "citty";
|
|
29
29
|
import { getParsedInvocation } from "../../cli/invocation.js";
|
|
30
|
-
import { defineJsonCommand, EXIT_CODES, GLOBAL_OUTPUT_ARGS, output, runWithJsonErrors } from "../../cli/shared.js";
|
|
30
|
+
import { defineJsonCommand, EXIT_CODES, GLOBAL_OUTPUT_ARGS, output, outputWithExitCode, runWithJsonErrors, } from "../../cli/shared.js";
|
|
31
31
|
import { loadConfig } from "../../core/config/config.js";
|
|
32
32
|
import { UsageError } from "../../core/errors.js";
|
|
33
33
|
import { appendEvent } from "../../core/events.js";
|
|
@@ -54,12 +54,10 @@ export const upgradeCommand = defineJsonCommand({
|
|
|
54
54
|
}
|
|
55
55
|
const skipPostUpgrade = args["skip-post-upgrade"];
|
|
56
56
|
const result = await performUpgrade(check, { force: args.force, skipPostUpgrade });
|
|
57
|
-
output("upgrade", result);
|
|
58
57
|
// The install may have succeeded, but an upgrade whose migration is
|
|
59
58
|
// blocked or could not run is not done: exit like `akm migrate apply` does.
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
}
|
|
59
|
+
const migrationFailed = result.migration?.status === "blocked" || result.migration?.status === "failed";
|
|
60
|
+
outputWithExitCode("upgrade", result, migrationFailed ? EXIT_CODES.GENERAL : undefined);
|
|
63
61
|
},
|
|
64
62
|
});
|
|
65
63
|
// `sync` body, standalone so the git-commit/push logic stays in one place.
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
import { defineCommand } from "citty";
|
|
28
28
|
import { getParsedInvocation } from "../../cli/invocation.js";
|
|
29
29
|
import { parsePositiveIntFlag } from "../../cli/parse-args.js";
|
|
30
|
-
import { defineGroupCommand, defineJsonCommand, EXIT_CODES, GLOBAL_OUTPUT_ARGS, output, runWithJsonErrors, } from "../../cli/shared.js";
|
|
30
|
+
import { defineGroupCommand, defineJsonCommand, EXIT_CODES, GLOBAL_OUTPUT_ARGS, output, outputWithExitCode, runWithJsonErrors, } from "../../cli/shared.js";
|
|
31
31
|
import { UsageError } from "../../core/errors.js";
|
|
32
32
|
import { TASK_RUN_BOOLEAN_FLAGS, TASK_RUN_VALUE_FLAGS } from "../../tasks/task-run-reserved-flags.js";
|
|
33
33
|
import { akmTaskExplain } from "./explain.js";
|
|
@@ -356,21 +356,16 @@ const tasksSyncCommand = defineJsonCommand({
|
|
|
356
356
|
const rebind = args.rebind === true;
|
|
357
357
|
if (args["dry-run"] === true) {
|
|
358
358
|
const preview = await akmTasksSyncPlan({}, args.bundle, { rebind });
|
|
359
|
-
|
|
360
|
-
const exitCode = taskSyncDryRunExitCode(preview);
|
|
361
|
-
if (exitCode !== undefined)
|
|
362
|
-
process.exitCode = exitCode;
|
|
359
|
+
outputWithExitCode("task-sync-dry-run", preview, taskSyncDryRunExitCode(preview));
|
|
363
360
|
return;
|
|
364
361
|
}
|
|
365
362
|
const result = await akmTasksSync({}, args.bundle, { rebind });
|
|
366
|
-
output("task-sync", result);
|
|
367
363
|
// #867: sync degrades — sources that failed to parse/prepare are
|
|
368
364
|
// excluded from reconciliation and reported in `result.failures` rather
|
|
369
365
|
// than poisoning the whole sync, but their presence must still fail
|
|
370
366
|
// the command's exit code so the breakage stays visible. (#906: this key
|
|
371
367
|
// matches the `--dry-run` preview's `failures` field — no separate name.)
|
|
372
|
-
|
|
373
|
-
process.exitCode = EXIT_CODES.GENERAL;
|
|
368
|
+
outputWithExitCode("task-sync", result, result.failures.length > 0 ? EXIT_CODES.GENERAL : undefined);
|
|
374
369
|
},
|
|
375
370
|
});
|
|
376
371
|
// ── `akm task explain` — read-only introspection (P2b Lane B, spec
|
|
@@ -482,10 +477,7 @@ const tasksPruneCommand = defineJsonCommand({
|
|
|
482
477
|
.filter(Boolean)
|
|
483
478
|
: undefined;
|
|
484
479
|
const result = await akmTasksPrune({}, { yes: args.yes === true, id });
|
|
485
|
-
|
|
486
|
-
const exitCode = taskPruneExitCode(result);
|
|
487
|
-
if (exitCode !== undefined)
|
|
488
|
-
process.exitCode = exitCode;
|
|
480
|
+
outputWithExitCode("task-prune", result, taskPruneExitCode(result));
|
|
489
481
|
},
|
|
490
482
|
});
|
|
491
483
|
export const taskCommand = defineGroupCommand({
|
|
@@ -22,6 +22,7 @@ import { resolveConfiguredSources } from "../../core/config/config-sources.js";
|
|
|
22
22
|
import { IMPROVE_AUTONOMY_CONFIG_KEY, isImproveAutonomyEnabled } from "../../core/config/experimental.js";
|
|
23
23
|
import { ConfigError, NotFoundError, UsageError } from "../../core/errors.js";
|
|
24
24
|
import { getTaskHistoryDir, getTaskLogDir } from "../../core/paths.js";
|
|
25
|
+
import { warn } from "../../core/warn.js";
|
|
25
26
|
import { commitWriteTargetBoundary, deleteAssetFromSource, prepareWriteTargetForMutation, resolveWorkingStashTarget, resolveWriteTarget, writeAssetToSource, } from "../../core/write-source.js";
|
|
26
27
|
import { withEngineFallback } from "../../integrations/agent/engine-fallback.js";
|
|
27
28
|
import { resolveAssetPath } from "../../sources/resolve.js";
|
|
@@ -391,7 +392,7 @@ async function buildSchedulerSyncPlan(deps, bundleTarget, options) {
|
|
|
391
392
|
const expectedSignature = sched.expectedSignature?.bind(sched);
|
|
392
393
|
const needsRuntime = preflight.operations.some((operation) => operation.kind !== "remove" && operation.options?.binding === undefined);
|
|
393
394
|
const prepared = needsRuntime
|
|
394
|
-
? prepareSchedulerSyncRuntime(syncTarget ? { target: syncTarget } : undefined, deps,
|
|
395
|
+
? prepareSchedulerSyncRuntime(syncTarget ? { target: syncTarget } : undefined, deps, warnings, allEntries.map((entry) => entry.binding))
|
|
395
396
|
: undefined;
|
|
396
397
|
const plan = finalizeSchedulerSyncPlan({
|
|
397
398
|
...common,
|
|
@@ -768,22 +769,24 @@ async function prepareTaskAddSchedulerTransaction(input) {
|
|
|
768
769
|
contextPath: installedEntry.contextPath,
|
|
769
770
|
},
|
|
770
771
|
}
|
|
771
|
-
: prepareSchedulerSyncRuntime(input.installOpts, input.deps,
|
|
772
|
+
: prepareSchedulerSyncRuntime(input.installOpts, input.deps, []);
|
|
772
773
|
const runtimeOpts = preparedRuntime.options;
|
|
773
774
|
const removals = taskEntries.map((entry) => {
|
|
774
775
|
const invocation = entry.invocation;
|
|
775
|
-
if (!invocation) {
|
|
776
|
-
throw new UsageError(`Installed scheduler binding ${JSON.stringify(entry.id)} has no exact parsed owner; refusing replacement.`, "RESOURCE_ALREADY_EXISTS");
|
|
777
|
-
}
|
|
778
776
|
const nativeId = entry.nativeId ?? schedulerNativeBindingId(entry.id);
|
|
779
777
|
const artifact = assertSchedulerNativeArtifactCardinality(nativeArtifacts, nativeId, 1);
|
|
780
778
|
if (!artifact?.fingerprint || artifact.bindingId !== entry.id) {
|
|
781
779
|
throw new UsageError(`Installed scheduler binding ${JSON.stringify(entry.id)} has no exact coherent fingerprint.`, "RESOURCE_ALREADY_EXISTS");
|
|
782
780
|
}
|
|
783
781
|
const logicalSource = primary.logicalSource;
|
|
784
|
-
const ordinal = schedulerBindingOrdinal(entry.id, logicalSource, invocation);
|
|
785
|
-
if (ordinal === undefined) {
|
|
786
|
-
|
|
782
|
+
const ordinal = invocation ? schedulerBindingOrdinal(entry.id, logicalSource, invocation) : undefined;
|
|
783
|
+
if (!invocation || ordinal === undefined) {
|
|
784
|
+
warn(`Installed scheduler binding ${JSON.stringify(entry.id)} (native id ${JSON.stringify(nativeId)}) could not be exactly parsed — likely a hand-edited entry; replacing it without a compare-and-swap guard.`);
|
|
785
|
+
return Object.freeze({
|
|
786
|
+
kind: "remove",
|
|
787
|
+
id: entry.id,
|
|
788
|
+
nativeId,
|
|
789
|
+
});
|
|
787
790
|
}
|
|
788
791
|
return Object.freeze({
|
|
789
792
|
kind: "remove",
|
|
@@ -824,6 +827,8 @@ async function prepareTaskAddSchedulerTransaction(input) {
|
|
|
824
827
|
for (const removal of removals) {
|
|
825
828
|
if (removal.kind !== "remove")
|
|
826
829
|
continue;
|
|
830
|
+
if (!removal.expected)
|
|
831
|
+
continue;
|
|
827
832
|
initialByKey.set(schedulerNativeArtifactKey(removal.nativeId), Object.freeze({ ...removal.expected, state: "present" }));
|
|
828
833
|
}
|
|
829
834
|
for (const install of installs) {
|
|
@@ -841,16 +846,16 @@ async function prepareTaskAddSchedulerTransaction(input) {
|
|
|
841
846
|
publishOperationIndex: removals.length,
|
|
842
847
|
});
|
|
843
848
|
}
|
|
844
|
-
function prepareSchedulerSyncRuntime(base, deps,
|
|
849
|
+
function prepareSchedulerSyncRuntime(base, deps, warnings, installedBindings = []) {
|
|
845
850
|
if (deps.backend && !deps.schedulerRuntime)
|
|
846
851
|
return base ? { options: base } : {};
|
|
847
852
|
if (deps.schedulerRuntime) {
|
|
848
853
|
const runtime = deps.schedulerRuntime();
|
|
849
|
-
warnIneligibleRebind(runtime,
|
|
854
|
+
warnIneligibleRebind(runtime, warnings, installedBindings);
|
|
850
855
|
return { options: { ...base, binding: runtime.binding, contextPath: runtime.contextPath } };
|
|
851
856
|
}
|
|
852
|
-
const invocation = resolveAndValidateSchedulerInvocation(
|
|
853
|
-
warnIneligibleRebind(invocation,
|
|
857
|
+
const invocation = resolveAndValidateSchedulerInvocation();
|
|
858
|
+
warnIneligibleRebind(invocation, warnings, installedBindings);
|
|
854
859
|
const descriptor = schedulerContextDescriptor();
|
|
855
860
|
const contextPath = schedulerContextPath(descriptor);
|
|
856
861
|
return {
|
|
@@ -863,24 +868,21 @@ function prepareSchedulerSyncRuntime(base, deps, explicitRebind, operation, warn
|
|
|
863
868
|
},
|
|
864
869
|
};
|
|
865
870
|
}
|
|
866
|
-
function resolveAndValidateSchedulerInvocation(
|
|
871
|
+
function resolveAndValidateSchedulerInvocation() {
|
|
867
872
|
const invocation = resolveAkmInvocation();
|
|
868
|
-
if (!invocation.eligible && !explicitRebind) {
|
|
869
|
-
throw new UsageError(`Refusing to ${operation} from an ineligible ${invocation.kind ?? "unknown"} invocation (${invocation.argv.join(" ")}).`, "INVALID_FLAG_VALUE", "npm-global ownership could not be verified. Run `npm install --global akm-cli` and use that launcher, use a standalone installation, or explicitly repeat the operation with --rebind.");
|
|
870
|
-
}
|
|
871
873
|
return { binding: invocation.argv, contextPath: "", eligible: invocation.eligible, kind: invocation.kind };
|
|
872
874
|
}
|
|
873
|
-
function warnIneligibleRebind(runtime,
|
|
874
|
-
if (
|
|
875
|
+
function warnIneligibleRebind(runtime, warnings, installedBindings) {
|
|
876
|
+
if (runtime.eligible !== false || warnings.length > 0)
|
|
875
877
|
return;
|
|
876
|
-
// #868 residue:
|
|
877
|
-
//
|
|
878
|
-
//
|
|
879
|
-
//
|
|
880
|
-
//
|
|
878
|
+
// #868 residue: binding every currently-installed entry to the SAME
|
|
879
|
+
// invocation it already carries changes nothing — this is the steady
|
|
880
|
+
// state of an image-baked install re-running `task sync` on a timer.
|
|
881
|
+
// Only warn when the bind actually moves an entry to a different
|
|
882
|
+
// invocation.
|
|
881
883
|
if (installedBindings.length > 0 && installedBindings.every((bound) => sameArgv(bound, runtime.binding)))
|
|
882
884
|
return;
|
|
883
|
-
warnings.push(
|
|
885
|
+
warnings.push(`Scheduled tasks are bound to an ineligible ${runtime.kind ?? "unknown"} invocation (${runtime.binding.join(" ")}); scheduled runs will invoke a mutable, unproven binary. Install akm via \`npm install --global akm-cli\` or a standalone release, then re-run \`akm task sync --rebind\`.`);
|
|
884
886
|
}
|
|
885
887
|
function groupInstalledBindings(entries, invocation) {
|
|
886
888
|
const groups = new Map();
|
|
@@ -972,16 +974,20 @@ function captureTaskSourceExpectation(filePathInput, rootInput) {
|
|
|
972
974
|
const common = { filePath, rootRealPath };
|
|
973
975
|
let descriptor;
|
|
974
976
|
try {
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
const before = fs.fstatSync(descriptor, { bigint: true });
|
|
977
|
+
descriptor = fs.openSync(filePath, fs.constants.O_RDONLY);
|
|
978
|
+
let before = fs.fstatSync(descriptor, { bigint: true });
|
|
978
979
|
if (!before.isFile()) {
|
|
979
980
|
throw new UsageError(`${filePath} is not a regular task source.`, "INVALID_FLAG_VALUE");
|
|
980
981
|
}
|
|
981
|
-
|
|
982
|
-
const
|
|
983
|
-
|
|
984
|
-
|
|
982
|
+
let bytes = fs.readFileSync(descriptor);
|
|
983
|
+
const torn = !sameTaskSourceStat(before, fs.fstatSync(descriptor, { bigint: true })) ||
|
|
984
|
+
BigInt(bytes.byteLength) !== before.size;
|
|
985
|
+
if (torn) {
|
|
986
|
+
fs.closeSync(descriptor);
|
|
987
|
+
descriptor = fs.openSync(filePath, fs.constants.O_RDONLY);
|
|
988
|
+
before = fs.fstatSync(descriptor, { bigint: true });
|
|
989
|
+
bytes = fs.readFileSync(descriptor);
|
|
990
|
+
warn(`${filePath} changed while its guarded bytes were read; retried once and proceeding with the latest read (its SHA-256 is re-verified before anything is published).`);
|
|
985
991
|
}
|
|
986
992
|
const realPath = fs.realpathSync(filePath);
|
|
987
993
|
const physicalRelative = path.relative(rootRealPath, realPath);
|
|
@@ -1011,9 +1017,6 @@ function captureTaskSourceExpectation(filePathInput, rootInput) {
|
|
|
1011
1017
|
}
|
|
1012
1018
|
if (cause instanceof UsageError)
|
|
1013
1019
|
throw cause;
|
|
1014
|
-
if (cause.code === "ELOOP") {
|
|
1015
|
-
throw new UsageError(`${filePath} must not be a symbolic task source.`, "RESOURCE_ALREADY_EXISTS");
|
|
1016
|
-
}
|
|
1017
1020
|
throw new UsageError(`${filePath} could not be guarded as a contained regular task source: ${errorMessage(cause)}`, "PATH_ESCAPE_VIOLATION");
|
|
1018
1021
|
}
|
|
1019
1022
|
finally {
|
|
@@ -1264,7 +1267,7 @@ function assertInlineTaskPrompt(input) {
|
|
|
1264
1267
|
(!/\s/.test(value) && /[\\/]/.test(value) && path.extname(value) !== "");
|
|
1265
1268
|
if (!isFullRefInput(value) && !pathShaped)
|
|
1266
1269
|
return;
|
|
1267
|
-
|
|
1270
|
+
warn(`--prompt "${input}" looks like an asset ref or file path; --prompt sends it as literal text, not a reference. Did you mean --workflow?`);
|
|
1268
1271
|
}
|
|
1269
1272
|
function parseJsonObjectArg(raw) {
|
|
1270
1273
|
let parsed;
|
|
@@ -8,14 +8,14 @@
|
|
|
8
8
|
* GitHub-shaped `.yml` workflow sources. Validate with `akm lint --type workflows`.
|
|
9
9
|
*/
|
|
10
10
|
import { getStringArg } from "../cli/parse-args.js";
|
|
11
|
-
import { defineGroupCommand, defineJsonCommand, EXIT_CODES, output } from "../cli/shared.js";
|
|
11
|
+
import { defineGroupCommand, defineJsonCommand, EXIT_CODES, output, outputWithExitCode } from "../cli/shared.js";
|
|
12
12
|
import { armAbortDeadline } from "../core/abort-deadline.js";
|
|
13
13
|
import { assertFlatAssetName, combineCreatePath, normalizeCreateSubPath } from "../core/asset/asset-create.js";
|
|
14
14
|
import { NotFoundError, UsageError } from "../core/errors.js";
|
|
15
15
|
import { akmIndex } from "../indexer/indexer.js";
|
|
16
16
|
import { assertWorkflowMarkdownName, createWorkflowAsset, getWorkflowTemplate } from "../workflows/authoring/authoring.js";
|
|
17
17
|
import { WORKFLOW_MAX_TIMEOUT_MS } from "../workflows/ir/schema.js";
|
|
18
|
-
import { abandonWorkflowRun, getWorkflowStatus,
|
|
18
|
+
import { abandonWorkflowRun, getWorkflowStatus, listWorkflowRuns, resolveWorkflowRunTarget, resumeWorkflowRun, } from "../workflows/runtime/runs.js";
|
|
19
19
|
import { akmWorkflowPlan } from "./workflow/plan.js";
|
|
20
20
|
const workflowStatusCommand = defineJsonCommand({
|
|
21
21
|
meta: {
|
|
@@ -34,8 +34,9 @@ const workflowStatusCommand = defineJsonCommand({
|
|
|
34
34
|
async run({ args }) {
|
|
35
35
|
const target = args.target;
|
|
36
36
|
const includeUnits = args.units === true;
|
|
37
|
-
|
|
38
|
-
|
|
37
|
+
const resolvedRunId = await resolveWorkflowRunTarget(target);
|
|
38
|
+
if (resolvedRunId !== undefined) {
|
|
39
|
+
const result = await getWorkflowStatus(resolvedRunId, { includeUnits });
|
|
39
40
|
output("workflow-status", result);
|
|
40
41
|
return;
|
|
41
42
|
}
|
|
@@ -100,12 +101,12 @@ const workflowCreateCommand = defineJsonCommand({
|
|
|
100
101
|
},
|
|
101
102
|
force: {
|
|
102
103
|
type: "boolean",
|
|
103
|
-
description: "Overwrite an existing workflow
|
|
104
|
+
description: "Overwrite an existing workflow. Combined with --from, replaces its content; alone, replaces it with a fresh template.",
|
|
104
105
|
default: false,
|
|
105
106
|
},
|
|
106
107
|
reset: {
|
|
107
108
|
type: "boolean",
|
|
108
|
-
description: "
|
|
109
|
+
description: "Deprecated alias for --force with no --from (replaces an existing workflow with a fresh template).",
|
|
109
110
|
default: false,
|
|
110
111
|
},
|
|
111
112
|
print: {
|
|
@@ -130,9 +131,6 @@ const workflowCreateCommand = defineJsonCommand({
|
|
|
130
131
|
process.stdout.write(getWorkflowTemplate());
|
|
131
132
|
return;
|
|
132
133
|
}
|
|
133
|
-
if (args.force && !args.from && !args.reset) {
|
|
134
|
-
throw new UsageError("Refusing to overwrite with template: pass --from <file> to replace content, or --reset to explicitly replace with a fresh template.");
|
|
135
|
-
}
|
|
136
134
|
const result = createWorkflowAsset({
|
|
137
135
|
name: effectiveName,
|
|
138
136
|
from: args.from,
|
|
@@ -157,6 +155,12 @@ const workflowRunCommand = defineJsonCommand({
|
|
|
157
155
|
"max-steps": { type: "string", description: "Stop after executing this many steps" },
|
|
158
156
|
"max-retries": { type: "string", description: "Retry a failed workflow step this many additional times" },
|
|
159
157
|
timeout: { type: "string", description: "Whole-run timeout: N, Nms, Ns, or Nm (bare N is milliseconds)" },
|
|
158
|
+
new: {
|
|
159
|
+
type: "boolean",
|
|
160
|
+
description: "Start a fresh run even if one is already active for this ref, leaving the existing run untouched " +
|
|
161
|
+
"(never abandons it). A workflow ref only — passing a run id with --new is a usage error.",
|
|
162
|
+
default: false,
|
|
163
|
+
},
|
|
160
164
|
},
|
|
161
165
|
async run({ args, rawArgs }) {
|
|
162
166
|
const { runWorkflowSteps } = await import("../workflows/exec/run-workflow.js");
|
|
@@ -186,6 +190,7 @@ const workflowRunCommand = defineJsonCommand({
|
|
|
186
190
|
parameterFlags,
|
|
187
191
|
...(maxSteps !== undefined ? { maxSteps } : {}),
|
|
188
192
|
...(maxRetries !== undefined ? { maxRetries } : {}),
|
|
193
|
+
newRun: args.new,
|
|
189
194
|
signal: controller.signal,
|
|
190
195
|
});
|
|
191
196
|
// The abort is observed between steps, so a deadline landing in the run's
|
|
@@ -193,14 +198,11 @@ const workflowRunCommand = defineJsonCommand({
|
|
|
193
198
|
// timed out would send an operator to resume a run with nothing left to
|
|
194
199
|
// resume — `tasks/runner.ts` suppresses the same case.
|
|
195
200
|
const timedOut = deadline.timedOut() && result.run.status !== "completed";
|
|
196
|
-
const rendered = { ...result, ...(timedOut ? { timedOut: true } : {}) };
|
|
197
|
-
output("workflow-run", rendered);
|
|
198
201
|
// `blocked` is a stopped, unverified run — a verification-judge failure
|
|
199
202
|
// leaves it there for `akm workflow resume` — so it must not exit 0 and
|
|
200
203
|
// read as success to a script (it maps to 1 for scheduled tasks too).
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
}
|
|
204
|
+
const failed = result.run.status === "failed" || result.run.status === "blocked" || result.gateRejection || result.aborted;
|
|
205
|
+
outputWithExitCode("workflow-run", { ...result, ...(timedOut ? { timedOut: true } : {}) }, failed ? (signalExitCode ?? EXIT_CODES.GENERAL) : undefined);
|
|
204
206
|
}
|
|
205
207
|
finally {
|
|
206
208
|
deadline.disarm();
|
|
@@ -220,7 +222,7 @@ const WORKFLOW_RUN_VALUE_FLAGS = new Set([
|
|
|
220
222
|
"shape",
|
|
221
223
|
"output",
|
|
222
224
|
]);
|
|
223
|
-
const WORKFLOW_RUN_BOOLEAN_FLAGS = new Set(["quiet", "verbose", "help", "no-quiet", "no-verbose"]);
|
|
225
|
+
const WORKFLOW_RUN_BOOLEAN_FLAGS = new Set(["quiet", "verbose", "help", "no-quiet", "no-verbose", "new", "no-new"]);
|
|
224
226
|
export function parseWorkflowParameterFlags(rawArgs, target) {
|
|
225
227
|
const flags = [];
|
|
226
228
|
let targetSeen = false;
|
|
@@ -1,20 +1,48 @@
|
|
|
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
|
+
/**
|
|
5
|
+
* `EDITOR`/`VISUAL`/`PAGER` are flagged by `isDangerousEnvKey` for a
|
|
6
|
+
* documented RCE vector (many tools invoke them to launch an editor/pager),
|
|
7
|
+
* but env-key-rules.ts's own module doc calls out their "high FP rate" in
|
|
8
|
+
* the same breath — an installed bundle's env file can supply a value, but
|
|
9
|
+
* nothing in akm's own env-injection path ever *invokes* EDITOR/VISUAL/PAGER
|
|
10
|
+
* with that value, so the RCE vector these three describe cannot fire from
|
|
11
|
+
* an injected env the way LD_PRELOAD or GIT_SSH_COMMAND can. Blocking a
|
|
12
|
+
* third-party install over them protects nothing while making "the operator
|
|
13
|
+
* legitimately wants to set their editor" the common case that eats the
|
|
14
|
+
* refusal. Kept as a name-level literal set (not an import of
|
|
15
|
+
* `commands/lint/env-key-rules.ts`) so this module stays the pure leaf its
|
|
16
|
+
* own doc comment promises — no new import edges into the rest of the tree.
|
|
17
|
+
*/
|
|
18
|
+
const INTERACTIVE_TOOL_ENV_KEYS = new Set(["EDITOR", "VISUAL", "PAGER"]);
|
|
4
19
|
/**
|
|
5
20
|
* Decide whether injecting an env with the given dangerous keys is allowed,
|
|
6
|
-
* warned, or blocked. Third-party (registry-installed) stashes hard-block
|
|
7
|
-
* first-party stashes warn.
|
|
21
|
+
* warned, or blocked. Third-party (registry-installed) stashes hard-block a
|
|
22
|
+
* genuine RCE-class key; first-party stashes warn. The interactive-tool
|
|
23
|
+
* group (see {@link INTERACTIVE_TOOL_ENV_KEYS}) only ever warns, since akm's
|
|
24
|
+
* own env-injection path never invokes those keys as a command. An explicit
|
|
25
|
+
* `--allow-insecure` (threaded through by the caller, same override
|
|
26
|
+
* `decideDangerousKeyInstall`'s `"warn-allow"` already honors for rule 2)
|
|
27
|
+
* downgrades a remaining block to a warning too — the operator is not racing
|
|
28
|
+
* themselves. See rule 1 above.
|
|
8
29
|
*
|
|
9
30
|
* @param dangerousKeys The subset of injected keys flagged as process-hijacking
|
|
10
31
|
* (already filtered by the caller via `isDangerousEnvKey`).
|
|
11
32
|
* @param thirdParty `true` when the env's source is a third-party stash — i.e.
|
|
12
33
|
* its origin carries a `registryId`.
|
|
34
|
+
* @param allowInsecure `true` when the operator passed `--allow-insecure` (or
|
|
35
|
+
* its equivalent) for this injection. Defaults to `false`.
|
|
13
36
|
*/
|
|
14
37
|
export function decideDangerousEnvInjection(input) {
|
|
15
38
|
if (input.dangerousKeys.length === 0)
|
|
16
39
|
return "allow";
|
|
17
|
-
|
|
40
|
+
if (!input.thirdParty)
|
|
41
|
+
return "warn";
|
|
42
|
+
if (input.allowInsecure)
|
|
43
|
+
return "warn";
|
|
44
|
+
const onlyInteractiveTool = input.dangerousKeys.every((key) => INTERACTIVE_TOOL_ENV_KEYS.has(key));
|
|
45
|
+
return onlyInteractiveTool ? "warn" : "block";
|
|
18
46
|
}
|
|
19
47
|
/**
|
|
20
48
|
* Decide the baseline install stance for a freshly-installed stash's
|