akm-cli 0.10.261012-beta → 0.10.261013-beta
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 +216 -0
- package/SECURITY.md +2 -2
- package/STABILITY.md +2 -2
- package/dist/assets/hints/cli-hints-full.md +6 -7
- package/dist/assets/hints/cli-hints-short.md +3 -4
- package/dist/commands/command/execution-source-loader.js +10 -14
- package/dist/commands/env/env-binding.js +47 -58
- package/dist/commands/env/env-cli.js +20 -22
- package/dist/commands/env/env.js +10 -58
- package/dist/commands/env/secret-cli.js +9 -27
- package/dist/commands/env/secret.js +3 -10
- package/dist/commands/health/scheduler-binary.js +1 -1
- package/dist/commands/improve/distill.js +0 -2
- package/dist/commands/improve/eligibility.js +0 -2
- package/dist/commands/lint/env-key-rules.js +7 -6
- package/dist/commands/lint/index.js +1 -1
- package/dist/commands/proposal/validators/proposal-validators.js +1 -4
- package/dist/commands/read/show.js +4 -25
- package/dist/commands/sources/add-cli.js +8 -63
- package/dist/commands/sources/bundle-cli.js +8 -9
- package/dist/commands/sources/installed-stashes.js +117 -61
- package/dist/commands/sources/schema-repair.js +0 -2
- package/dist/commands/sources/source-clone.js +31 -7
- package/dist/commands/tasks/explain.js +1 -1
- package/dist/commands/tasks/tasks.js +38 -56
- package/dist/commands/ui/redact.js +4 -4
- package/dist/commands/ui/workspace-actions.js +1 -1
- package/dist/commands/workflow/plan.js +47 -8
- package/dist/commands/workflow-cli.js +37 -41
- package/dist/core/adapter/adapters/akm-adapter.js +3 -3
- package/dist/core/adapter/adapters/akm-lint.js +3 -4
- package/dist/core/adapter/adapters/akm-metadata.js +2 -2
- package/dist/core/adapter/adapters/akm-task-adapter.js +5 -6
- package/dist/core/adapter/adapters/dotenv-adapter.js +4 -3
- package/dist/core/asset/resolve-ref.js +5 -0
- package/dist/core/bundle-rename.js +12 -4
- package/dist/core/common.js +4 -52
- package/dist/core/config/config-walker.js +1 -1
- package/dist/core/config/config.js +55 -18
- package/dist/core/config/schema/primitives.js +12 -6
- package/dist/core/config/schema/sources-bundles.js +2 -2
- package/dist/core/env-secret-ref.js +84 -73
- package/dist/core/errors.js +1 -1
- package/dist/core/paths.js +1 -6
- package/dist/core/redaction.js +16 -3
- package/dist/core/ref-token.js +82 -0
- package/dist/core/secret-value.js +14 -0
- package/dist/core/write-source.js +20 -4
- package/dist/indexer/indexer.js +27 -36
- package/dist/indexer/installations.js +1 -8
- package/dist/indexer/lookup/adapter-concept-owner.js +40 -14
- package/dist/indexer/materialize-embeddings.js +1 -1
- package/dist/indexer/scan/drain-dir.js +3 -3
- package/dist/indexer/search/search-source.js +4 -2
- package/dist/indexer/walk/path-resolver.js +13 -68
- package/dist/indexer/walk/walker.js +18 -22
- package/dist/integrations/agent/engine-resolution.js +9 -7
- package/dist/integrations/agent/runner-dispatch.js +4 -5
- package/dist/integrations/agent/runner.js +2 -0
- package/dist/llm/client.js +1 -1
- package/dist/llm/embedders/remote.js +4 -3
- package/dist/output/renderers.js +3 -4
- package/dist/output/shapes/helpers.js +0 -2
- package/dist/output/text/command-format.js +12 -2
- package/dist/output/text/show-directives.js +16 -28
- package/dist/output/text/workflow-format.js +37 -7
- package/dist/scripts/akm-migrate-node.js +45865 -44692
- package/dist/scripts/akm-migrate.js +45804 -44630
- package/dist/setup/steps/tasks.js +1 -2
- package/dist/sources/providers/provider-utils.js +12 -0
- package/dist/sources/resolve.js +12 -16
- package/dist/sources/snapshot-fetchers/secret-seam.js +2 -4
- package/dist/sources/snapshot-fetchers/x.js +1 -1
- package/dist/storage/repositories/proposals-repository.js +2 -2
- package/dist/storage/repositories/task-history-repository.js +2 -2
- package/dist/storage/repositories/workflow-runs-repository.js +38 -116
- package/dist/tasks/embedded.js +1 -1
- package/dist/tasks/prepare/prepare-support.js +3 -2
- package/dist/tasks/prepare/prepare.js +3 -4
- package/dist/tasks/prepare/script-capture.js +8 -22
- package/dist/tasks/run/load-task.js +46 -14
- package/dist/tasks/run/run-native-task.js +0 -8
- package/dist/tasks/run/run-workflow-task.js +2 -2
- package/dist/tasks/scheduler-sync.js +0 -1
- package/dist/tasks/source/bounded-document.js +0 -41
- package/dist/tasks/source/parse-task-source.js +0 -1
- package/dist/tasks/source/task-source-v4.js +1 -4
- package/dist/workflows/compile.js +1 -15
- package/dist/workflows/exec/child-workflow.js +1 -2
- package/dist/workflows/exec/dispatch-redaction.js +24 -6
- package/dist/workflows/exec/environment.js +11 -74
- package/dist/workflows/exec/exec-unit.js +31 -23
- package/dist/workflows/exec/frozen-judge.js +17 -3
- package/dist/workflows/exec/native-executor.js +70 -26
- package/dist/workflows/exec/run-workflow.js +31 -18
- package/dist/workflows/exec/step-work.js +4 -3
- package/dist/workflows/exec/unit-dispatch.js +1 -8
- package/dist/workflows/freeze/environment.js +14 -119
- package/dist/workflows/freeze/freeze.js +0 -1
- package/dist/workflows/freeze/step-values.js +1 -2
- package/dist/workflows/freeze/targets/child-workflow.js +3 -5
- package/dist/workflows/freeze/targets/command.js +6 -9
- package/dist/workflows/freeze/targets/shell.js +0 -3
- package/dist/workflows/parser.js +6 -25
- package/dist/workflows/plan.js +6 -2
- package/dist/workflows/renderer.js +77 -4
- package/dist/workflows/runtime/run-plan.js +37 -23
- package/dist/workflows/runtime/runs.js +41 -84
- package/dist/workflows/runtime/workflow-asset-loader.js +55 -86
- package/dist/workflows/source-files.js +7 -19
- package/docs/architecture/0.10-process-rules.md +6 -6
- package/docs/architecture/architecture.md +6 -6
- package/docs/architecture/comparisons/claude-code-vs-akm-workflows-full.md +12 -18
- package/docs/architecture/decisions/0003-child-env-allowlist-and-provenance.md +6 -8
- package/docs/architecture/internals/storage-locations.md +4 -4
- package/docs/architecture/persisted-data-compat.md +3 -2
- package/docs/architecture/specs/0.9.0-public-api-issue-backlog.md +1 -1
- package/docs/architecture/specs/akm-format-neutral-bundle-workspace-spec.md +3 -3
- package/docs/architecture/testing/manual-testing-checklist.md +46 -78
- package/docs/architecture/testing/task-workflow-manual-tests.md +22 -40
- package/docs/architecture/testing/testing-workflow.md +9 -11
- package/docs/architecture/workflow-engine.md +41 -27
- package/docs/guides/author-bundles.md +14 -17
- package/docs/guides/author-workflows.md +43 -9
- package/docs/guides/bundles.md +16 -11
- package/docs/guides/claude-code-vs-akm-workflows.md +1 -1
- package/docs/guides/run-workflows.md +25 -13
- package/docs/guides/scheduling.md +1 -1
- package/docs/maintainers/README.md +2 -3
- package/docs/maintainers/release-checklist.md +73 -126
- package/docs/migration/v0.9-to-v0.10.md +5 -1
- package/docs/reference/cli.md +124 -112
- package/docs/reference/configuration.md +16 -9
- package/docs/reference/data-and-telemetry.md +1 -1
- package/docs/reference/env-and-secrets.md +66 -15
- package/docs/reference/registry.md +6 -11
- package/docs/reference/tasks.md +11 -3
- package/docs/reference/ui-api.md +3 -3
- package/docs/reference/workflow-schema.md +109 -36
- package/package.json +1 -1
- package/schemas/akm-task.json +2 -4
- package/dist/commands/sources/dangerous-env-audit.js +0 -134
- package/dist/core/activation-policy.js +0 -67
- package/dist/execution/directory-identity.js +0 -78
- package/dist/workflows/authoring/scope-key.js +0 -79
- package/dist/workflows/source-semantics.js +0 -110
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
-
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
-
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
|
-
/**
|
|
5
|
-
* `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"]);
|
|
19
|
-
/**
|
|
20
|
-
* Decide whether injecting an env with the given dangerous keys is allowed,
|
|
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-dangerous-env-keys` (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.
|
|
29
|
-
*
|
|
30
|
-
* @param dangerousKeys The subset of injected keys flagged as process-hijacking
|
|
31
|
-
* (already filtered by the caller via `isDangerousEnvKey`).
|
|
32
|
-
* @param thirdParty `true` when the env's source is a third-party stash — i.e.
|
|
33
|
-
* its origin carries a `registryId`.
|
|
34
|
-
* @param allowDangerousEnvKeys `true` when the operator passed `--allow-dangerous-env-keys` (or
|
|
35
|
-
* its equivalent) for this injection. Defaults to `false`.
|
|
36
|
-
*/
|
|
37
|
-
export function decideDangerousEnvInjection(input) {
|
|
38
|
-
if (input.dangerousKeys.length === 0)
|
|
39
|
-
return "allow";
|
|
40
|
-
if (!input.thirdParty)
|
|
41
|
-
return "warn";
|
|
42
|
-
if (input.allowDangerousEnvKeys)
|
|
43
|
-
return "warn";
|
|
44
|
-
const onlyInteractiveTool = input.dangerousKeys.every((key) => INTERACTIVE_TOOL_ENV_KEYS.has(key));
|
|
45
|
-
return onlyInteractiveTool ? "warn" : "block";
|
|
46
|
-
}
|
|
47
|
-
/**
|
|
48
|
-
* Decide the baseline install stance for a freshly-installed stash's
|
|
49
|
-
* dangerous-key scan. The interactive confirm / rollback UX is applied by the
|
|
50
|
-
* caller on top of the `"gate"` stance — this predicate only fixes the policy.
|
|
51
|
-
* See rule 2 above.
|
|
52
|
-
*/
|
|
53
|
-
export function decideDangerousKeyInstall(input) {
|
|
54
|
-
if (!input.findingsPresent)
|
|
55
|
-
return "allow";
|
|
56
|
-
return input.allowDangerousEnvKeys ? "warn-allow" : "gate";
|
|
57
|
-
}
|
|
58
|
-
// ── Rule 3: write activation (search-source.ts, installations.ts) ────────────
|
|
59
|
-
/**
|
|
60
|
-
* Whether a resolved source is write-activated. Only the primary stash and
|
|
61
|
-
* sources explicitly marked `writable: true` are writable; registry-cached
|
|
62
|
-
* (installed, read-only) sources are never written in place because
|
|
63
|
-
* `akm update` overwrites them. See rule 3 above.
|
|
64
|
-
*/
|
|
65
|
-
export function isSourceWriteActivated(source) {
|
|
66
|
-
return source.writable === true;
|
|
67
|
-
}
|
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
-
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
-
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
|
-
import fs from "node:fs";
|
|
5
|
-
import path from "node:path";
|
|
6
|
-
import { UsageError } from "../core/errors.js";
|
|
7
|
-
export function captureFrozenDirectoryIdentity(rootInput, relativeCwd) {
|
|
8
|
-
const requestedRoot = path.resolve(rootInput);
|
|
9
|
-
const requestedCwd = path.resolve(requestedRoot, relativeCwd ?? ".");
|
|
10
|
-
requireContained(requestedRoot, requestedCwd);
|
|
11
|
-
const realRoot = fs.realpathSync(requestedRoot);
|
|
12
|
-
const realCwd = fs.realpathSync(requestedCwd);
|
|
13
|
-
requireContained(realRoot, realCwd);
|
|
14
|
-
const rootStat = fs.statSync(realRoot, { bigint: true });
|
|
15
|
-
const cwdStat = fs.statSync(realCwd, { bigint: true });
|
|
16
|
-
if (!rootStat.isDirectory() || !cwdStat.isDirectory()) {
|
|
17
|
-
throw new UsageError("Execution root and cwd must both be directories.", "INVALID_FLAG_VALUE");
|
|
18
|
-
}
|
|
19
|
-
return Object.freeze({
|
|
20
|
-
requestedRoot,
|
|
21
|
-
realRoot,
|
|
22
|
-
rootDevice: rootStat.dev.toString(),
|
|
23
|
-
rootInode: rootStat.ino.toString(),
|
|
24
|
-
requestedCwd,
|
|
25
|
-
realCwd,
|
|
26
|
-
cwdDevice: cwdStat.dev.toString(),
|
|
27
|
-
cwdInode: cwdStat.ino.toString(),
|
|
28
|
-
});
|
|
29
|
-
}
|
|
30
|
-
/**
|
|
31
|
-
* Re-verify, immediately before spawn, that a frozen cwd still resolves
|
|
32
|
-
* inside its execution root — path containment and resolved-path identity,
|
|
33
|
-
* NOT device/inode identity. Unlike the removed assertFrozenDirectoryIdentity,
|
|
34
|
-
* this does not compare device/inode numbers against the frozen snapshot: a
|
|
35
|
-
* remount, an rsync, or a container rebuild that leaves the same real
|
|
36
|
-
* content at the same real PATH must not abort a dispatch. What still
|
|
37
|
-
* throws is anything that changes the resolved real PATH itself — a
|
|
38
|
-
* symlink swap, an ancestor swap, a file replacing the directory, or the
|
|
39
|
-
* root itself being replaced by a symlink elsewhere (root-vs-cwd
|
|
40
|
-
* containment alone can't catch that last one, since both resolve together
|
|
41
|
-
* under the new root; comparing the root's own resolved path to its frozen
|
|
42
|
-
* value catches it).
|
|
43
|
-
*/
|
|
44
|
-
export function assertFrozenDirectoryContained(identity) {
|
|
45
|
-
let realRoot;
|
|
46
|
-
let realCwd;
|
|
47
|
-
try {
|
|
48
|
-
realRoot = fs.realpathSync(identity.requestedRoot);
|
|
49
|
-
realCwd = fs.realpathSync(identity.requestedCwd);
|
|
50
|
-
}
|
|
51
|
-
catch {
|
|
52
|
-
throw changed(identity.requestedCwd);
|
|
53
|
-
}
|
|
54
|
-
if (realRoot !== identity.realRoot)
|
|
55
|
-
throw changed(identity.requestedCwd);
|
|
56
|
-
const relative = path.relative(realRoot, realCwd);
|
|
57
|
-
if (relative.startsWith("..") || path.isAbsolute(relative)) {
|
|
58
|
-
throw changed(identity.requestedCwd);
|
|
59
|
-
}
|
|
60
|
-
let cwdStat;
|
|
61
|
-
try {
|
|
62
|
-
cwdStat = fs.statSync(realCwd, { bigint: true });
|
|
63
|
-
}
|
|
64
|
-
catch {
|
|
65
|
-
throw changed(identity.requestedCwd);
|
|
66
|
-
}
|
|
67
|
-
if (!cwdStat.isDirectory())
|
|
68
|
-
throw changed(identity.requestedCwd);
|
|
69
|
-
}
|
|
70
|
-
function changed(cwd) {
|
|
71
|
-
return new UsageError(`Frozen execution cwd ${cwd} changed and no longer resolves inside its root.`, "PATH_ESCAPE_VIOLATION");
|
|
72
|
-
}
|
|
73
|
-
function requireContained(root, candidate) {
|
|
74
|
-
const relative = path.relative(root, candidate);
|
|
75
|
-
if (relative.startsWith("..") || path.isAbsolute(relative)) {
|
|
76
|
-
throw new UsageError(`${candidate} escapes its execution root.`, "PATH_ESCAPE_VIOLATION");
|
|
77
|
-
}
|
|
78
|
-
}
|
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
-
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
-
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
|
-
import { createHash } from "node:crypto";
|
|
5
|
-
import fs from "node:fs";
|
|
6
|
-
import path from "node:path";
|
|
7
|
-
import { isWithin, resolveStashDir, safeRealpath, toPosix } from "../../core/common.js";
|
|
8
|
-
const PROJECT_CONFIG_RELATIVE_PATH = path.join(".akm", "config.json");
|
|
9
|
-
export function getCurrentWorkflowScopeKey() {
|
|
10
|
-
const anchor = resolveWorkflowScopeAnchor(process.cwd());
|
|
11
|
-
const normalized = normalizeScopePath(anchor);
|
|
12
|
-
const digest = createHash("sha256").update(normalized).digest("hex");
|
|
13
|
-
return `dir:v1:${digest}`;
|
|
14
|
-
}
|
|
15
|
-
export function resolveWorkflowScopeAnchor(startDir) {
|
|
16
|
-
const cwd = safeRealpath(startDir);
|
|
17
|
-
const projectRoot = findNearestProjectConfigRoot(cwd);
|
|
18
|
-
if (projectRoot)
|
|
19
|
-
return projectRoot;
|
|
20
|
-
const gitRoot = findNearestGitRoot(cwd);
|
|
21
|
-
if (gitRoot)
|
|
22
|
-
return gitRoot;
|
|
23
|
-
try {
|
|
24
|
-
const stashDir = safeRealpath(resolveStashDir());
|
|
25
|
-
if (isWithin(cwd, stashDir))
|
|
26
|
-
return stashDir;
|
|
27
|
-
}
|
|
28
|
-
catch {
|
|
29
|
-
// Ignore stash resolution failures and fall back to cwd.
|
|
30
|
-
}
|
|
31
|
-
return cwd;
|
|
32
|
-
}
|
|
33
|
-
function findNearestProjectConfigRoot(startDir) {
|
|
34
|
-
let currentDir = startDir;
|
|
35
|
-
while (true) {
|
|
36
|
-
const configPath = path.join(currentDir, PROJECT_CONFIG_RELATIVE_PATH);
|
|
37
|
-
if (isFile(configPath)) {
|
|
38
|
-
return safeRealpath(currentDir);
|
|
39
|
-
}
|
|
40
|
-
const parentDir = path.dirname(currentDir);
|
|
41
|
-
if (parentDir === currentDir)
|
|
42
|
-
return null;
|
|
43
|
-
currentDir = parentDir;
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
function findNearestGitRoot(startDir) {
|
|
47
|
-
let currentDir = startDir;
|
|
48
|
-
while (true) {
|
|
49
|
-
const gitPath = path.join(currentDir, ".git");
|
|
50
|
-
if (exists(gitPath)) {
|
|
51
|
-
return safeRealpath(currentDir);
|
|
52
|
-
}
|
|
53
|
-
const parentDir = path.dirname(currentDir);
|
|
54
|
-
if (parentDir === currentDir)
|
|
55
|
-
return null;
|
|
56
|
-
currentDir = parentDir;
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
function exists(candidate) {
|
|
60
|
-
try {
|
|
61
|
-
fs.accessSync(candidate, fs.constants.F_OK);
|
|
62
|
-
return true;
|
|
63
|
-
}
|
|
64
|
-
catch {
|
|
65
|
-
return false;
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
function isFile(candidate) {
|
|
69
|
-
try {
|
|
70
|
-
return fs.statSync(candidate).isFile();
|
|
71
|
-
}
|
|
72
|
-
catch {
|
|
73
|
-
return false;
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
function normalizeScopePath(value) {
|
|
77
|
-
const posix = toPosix(value);
|
|
78
|
-
return process.platform === "win32" ? posix.toLowerCase() : posix;
|
|
79
|
-
}
|
|
@@ -1,110 +0,0 @@
|
|
|
1
|
-
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
-
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
-
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
|
-
/** Semantic checks for the Markdown workflow grammar (`parser.ts`). */
|
|
5
|
-
import fs from "node:fs";
|
|
6
|
-
import path from "node:path";
|
|
7
|
-
export class WorkflowSourceSemanticError extends Error {
|
|
8
|
-
code;
|
|
9
|
-
constructor(code, message) {
|
|
10
|
-
super(message);
|
|
11
|
-
this.code = code;
|
|
12
|
-
this.name = "WorkflowSourceSemanticError";
|
|
13
|
-
}
|
|
14
|
-
}
|
|
15
|
-
export function canonicalizeWorkflowWorkingDirectory(value, workspaceRoot) {
|
|
16
|
-
if (hasControlCharacter(value)) {
|
|
17
|
-
throw new WorkflowSourceSemanticError("working-directory-control-character", "working-directory may not contain NUL or control characters.");
|
|
18
|
-
}
|
|
19
|
-
if (value === "" || value.trim() === "") {
|
|
20
|
-
throw new WorkflowSourceSemanticError("working-directory-escape", "working-directory must be a non-empty relative contained path.");
|
|
21
|
-
}
|
|
22
|
-
const portable = value.replaceAll("\\", "/");
|
|
23
|
-
const segments = portable.split("/");
|
|
24
|
-
if (path.posix.isAbsolute(portable) ||
|
|
25
|
-
path.win32.isAbsolute(value) ||
|
|
26
|
-
portable.startsWith("~") ||
|
|
27
|
-
segments.some((segment) => segment === "" || segment === "..")) {
|
|
28
|
-
throw new WorkflowSourceSemanticError("working-directory-escape", "working-directory must be relative and contained.");
|
|
29
|
-
}
|
|
30
|
-
const withoutDots = segments.filter((segment) => segment !== ".");
|
|
31
|
-
const canonical = withoutDots.length === 0 ? "." : withoutDots.join("/");
|
|
32
|
-
if (workspaceRoot !== undefined)
|
|
33
|
-
verifyPhysicalContainment(workspaceRoot, canonical);
|
|
34
|
-
return canonical;
|
|
35
|
-
}
|
|
36
|
-
function hasControlCharacter(value) {
|
|
37
|
-
for (const character of value) {
|
|
38
|
-
const codePoint = character.codePointAt(0) ?? 0;
|
|
39
|
-
if (codePoint <= 0x1f || codePoint === 0x7f)
|
|
40
|
-
return true;
|
|
41
|
-
}
|
|
42
|
-
return false;
|
|
43
|
-
}
|
|
44
|
-
function verifyPhysicalContainment(workspaceRoot, relative) {
|
|
45
|
-
let root;
|
|
46
|
-
try {
|
|
47
|
-
root = fs.realpathSync(workspaceRoot);
|
|
48
|
-
}
|
|
49
|
-
catch {
|
|
50
|
-
throw new WorkflowSourceSemanticError("working-directory-unverifiable", "Workspace root cannot be physically verified.");
|
|
51
|
-
}
|
|
52
|
-
const candidate = path.resolve(root, ...relative.split("/"));
|
|
53
|
-
if (!contained(root, candidate)) {
|
|
54
|
-
throw new WorkflowSourceSemanticError("working-directory-escape", "working-directory escapes the workspace.");
|
|
55
|
-
}
|
|
56
|
-
let current = root;
|
|
57
|
-
for (const segment of relative === "." ? [] : relative.split("/")) {
|
|
58
|
-
current = path.join(current, segment);
|
|
59
|
-
try {
|
|
60
|
-
const entry = fs.lstatSync(current);
|
|
61
|
-
if (entry.isSymbolicLink()) {
|
|
62
|
-
let physical;
|
|
63
|
-
try {
|
|
64
|
-
physical = fs.realpathSync(current);
|
|
65
|
-
}
|
|
66
|
-
catch {
|
|
67
|
-
throw new WorkflowSourceSemanticError("working-directory-unverifiable", "working-directory contains a dangling or unresolvable symlink.");
|
|
68
|
-
}
|
|
69
|
-
if (!contained(root, physical)) {
|
|
70
|
-
throw new WorkflowSourceSemanticError("working-directory-escape", "working-directory resolves through a symlink outside the workspace.");
|
|
71
|
-
}
|
|
72
|
-
let target;
|
|
73
|
-
try {
|
|
74
|
-
target = fs.statSync(current);
|
|
75
|
-
}
|
|
76
|
-
catch {
|
|
77
|
-
throw new WorkflowSourceSemanticError("working-directory-unverifiable", "working-directory symlink target cannot be physically verified.");
|
|
78
|
-
}
|
|
79
|
-
if (!target.isDirectory()) {
|
|
80
|
-
throw new WorkflowSourceSemanticError("working-directory-unverifiable", "working-directory must resolve through directories.");
|
|
81
|
-
}
|
|
82
|
-
continue;
|
|
83
|
-
}
|
|
84
|
-
if (!entry.isDirectory()) {
|
|
85
|
-
throw new WorkflowSourceSemanticError("working-directory-unverifiable", "working-directory must resolve through directories.");
|
|
86
|
-
}
|
|
87
|
-
const physical = fs.realpathSync(current);
|
|
88
|
-
if (!contained(root, physical)) {
|
|
89
|
-
throw new WorkflowSourceSemanticError("working-directory-escape", "working-directory resolves through a symlink outside the workspace.");
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
catch (cause) {
|
|
93
|
-
if (cause instanceof WorkflowSourceSemanticError)
|
|
94
|
-
throw cause;
|
|
95
|
-
const code = cause?.code;
|
|
96
|
-
if (code === "ENOENT") {
|
|
97
|
-
// A genuinely absent lexical component is allowed at source time; the
|
|
98
|
-
// runtime dispatch boundary revalidates containment when it appears.
|
|
99
|
-
// `lstat` distinguishes this from a dangling symlink, which fails
|
|
100
|
-
// closed above even before its target can appear.
|
|
101
|
-
return;
|
|
102
|
-
}
|
|
103
|
-
throw new WorkflowSourceSemanticError("working-directory-unverifiable", "working-directory cannot be physically verified.");
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
function contained(root, candidate) {
|
|
108
|
-
const relative = path.relative(root, candidate);
|
|
109
|
-
return relative === "" || (!relative.startsWith(`..${path.sep}`) && relative !== ".." && !path.isAbsolute(relative));
|
|
110
|
-
}
|