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
|
@@ -5,8 +5,7 @@
|
|
|
5
5
|
* `akm env` command family. Extracted verbatim from src/cli.ts (WS6) so the God
|
|
6
6
|
* Module shrinks; the `main.subCommands.env` key and every subcommand's
|
|
7
7
|
* args/output shape are byte-identical. The ref-resolution helpers
|
|
8
|
-
* (parseEnvRef /
|
|
9
|
-
* traversal guard) live in src/core/env-secret-ref.ts so env + secret share one
|
|
8
|
+
* (parseEnvRef / makeEnvRef / resolveEnvPath) live in src/core/env-secret-ref.ts so env + secret share one
|
|
10
9
|
* copy.
|
|
11
10
|
*
|
|
12
11
|
* `akm env` manages whole `.env` files under each stash's env/ directory.
|
|
@@ -42,7 +41,7 @@ const envCreateCommand = defineJsonCommand({
|
|
|
42
41
|
args: {
|
|
43
42
|
name: {
|
|
44
43
|
type: "positional",
|
|
45
|
-
description: "Env
|
|
44
|
+
description: "Env ref (flat name, e.g. env/prod → prod.env; use --path for a subdirectory)",
|
|
46
45
|
required: true,
|
|
47
46
|
},
|
|
48
47
|
path: {
|
|
@@ -119,10 +118,7 @@ const envPathCommand = defineJsonCommand({
|
|
|
119
118
|
quiet: { type: "boolean", alias: "q", description: "Suppress the unsafe-source warning", default: false },
|
|
120
119
|
},
|
|
121
120
|
async run({ args }) {
|
|
122
|
-
const {
|
|
123
|
-
if (!fs.existsSync(absPath)) {
|
|
124
|
-
throw new NotFoundError(`Env not found: ${makeEnvRef(name, source)}`);
|
|
125
|
-
}
|
|
121
|
+
const { absPath } = resolveEnvPath(args.ref);
|
|
126
122
|
// The raw `.env` may contain `X=$(cmd)`, which executes if `source`d.
|
|
127
123
|
// Warning goes to stderr (never contaminates the path on stdout) and is
|
|
128
124
|
// suppressed with --quiet for the legitimate `_FILE` / `--env-file` use.
|
|
@@ -166,9 +162,6 @@ const envExportCommand = defineJsonCommand({
|
|
|
166
162
|
" session). export never prints values to stdout, to avoid leaking them into a captured context.", "MISSING_REQUIRED_ARGUMENT");
|
|
167
163
|
}
|
|
168
164
|
const { name, absPath, source } = resolveEnvPath(args.ref);
|
|
169
|
-
if (!fs.existsSync(absPath)) {
|
|
170
|
-
throw new NotFoundError(`Env not found: ${makeEnvRef(name, source)}`);
|
|
171
|
-
}
|
|
172
165
|
const { buildShellExportScript } = await import("./env.js");
|
|
173
166
|
const resolvedOut = path.resolve(outPath);
|
|
174
167
|
writeFileAtomic(resolvedOut, buildShellExportScript(absPath), 0o600);
|
|
@@ -177,16 +170,19 @@ const envExportCommand = defineJsonCommand({
|
|
|
177
170
|
});
|
|
178
171
|
/**
|
|
179
172
|
* Shared implementation for `env run`. Injects an entire env file's values into
|
|
180
|
-
* the child process env — never via a shell
|
|
181
|
-
* for process-hijacking variables.
|
|
173
|
+
* the child process env — never via a shell.
|
|
182
174
|
*/
|
|
183
175
|
async function runEnvInjected(target, opts) {
|
|
184
176
|
const command = getParsedInvocation().passthroughArgs();
|
|
185
177
|
if (command.length === 0) {
|
|
186
178
|
throw new UsageError("Missing command. Usage: akm env run <ref> -- <command>");
|
|
187
179
|
}
|
|
188
|
-
|
|
189
|
-
|
|
180
|
+
try {
|
|
181
|
+
resolveEnvPath(target);
|
|
182
|
+
}
|
|
183
|
+
catch (error) {
|
|
184
|
+
if (!(error instanceof NotFoundError))
|
|
185
|
+
throw error;
|
|
190
186
|
// Help users who reach for the removed single-key `ref/KEY` form.
|
|
191
187
|
const slash = target.lastIndexOf("/");
|
|
192
188
|
if (slash > 0) {
|
|
@@ -194,7 +190,8 @@ async function runEnvInjected(target, opts) {
|
|
|
194
190
|
if (/^[A-Za-z_][A-Za-z0-9_]*$/.test(maybeKey)) {
|
|
195
191
|
let baseExists = false;
|
|
196
192
|
try {
|
|
197
|
-
|
|
193
|
+
resolveEnvPath(target.slice(0, slash));
|
|
194
|
+
baseExists = true;
|
|
198
195
|
}
|
|
199
196
|
catch {
|
|
200
197
|
baseExists = false;
|
|
@@ -205,16 +202,15 @@ async function runEnvInjected(target, opts) {
|
|
|
205
202
|
}
|
|
206
203
|
}
|
|
207
204
|
}
|
|
208
|
-
throw
|
|
205
|
+
throw error;
|
|
209
206
|
}
|
|
210
|
-
// Load → filter → secret-substitute →
|
|
207
|
+
// Load → filter → secret-substitute → keys-only
|
|
211
208
|
// audit event. Shared with the workflow engine's per-unit env bindings —
|
|
212
209
|
// see env-binding.ts for the extracted core and its safety invariants.
|
|
213
210
|
const { resolveEnvBinding } = await import("./env-binding.js");
|
|
214
211
|
const { values: envValues } = resolveEnvBinding(target, {
|
|
215
212
|
only: opts.only,
|
|
216
213
|
except: opts.except,
|
|
217
|
-
allowDangerousEnvKeys: opts.allowDangerousEnvKeys,
|
|
218
214
|
});
|
|
219
215
|
const mergedEnv = buildChildEnv(process.env, {
|
|
220
216
|
clean: opts.clean === true,
|
|
@@ -264,8 +260,8 @@ const envRunCommand = defineJsonCommand({
|
|
|
264
260
|
meta: {
|
|
265
261
|
name: "run",
|
|
266
262
|
description:
|
|
267
|
-
// biome-ignore lint/suspicious/noTemplateCurlyInString: literal `${
|
|
268
|
-
"Run a command with the env file injected into its environment: `akm env run <ref> -- <command>`. Use `-- $SHELL` for an interactive session. Restrict which variables are injected with --only / --except. Values may embed `${
|
|
263
|
+
// biome-ignore lint/suspicious/noTemplateCurlyInString: literal `${ref}` token syntax documented for users, not interpolation
|
|
264
|
+
"Run a command with the env file injected into its environment: `akm env run <ref> -- <command>`. Use `-- $SHELL` for an interactive session. Restrict which variables are injected with --only / --except. Values may embed `${ref}` tokens, replaced at run time: a `${secrets/NAME}` (or `${bundle//secrets/NAME}`) token with the secret's value, any other asset ref with its absolute path. Pass --clean to start the child with a minimal inherited environment instead of the full parent environment.",
|
|
269
265
|
},
|
|
270
266
|
args: {
|
|
271
267
|
target: { type: "positional", description: "Env ref", required: true },
|
|
@@ -283,9 +279,12 @@ const envRunCommand = defineJsonCommand({
|
|
|
283
279
|
type: "string",
|
|
284
280
|
description: "When used with --clean, also inherit these parent env vars (comma-separated). Ignored without --clean.",
|
|
285
281
|
},
|
|
282
|
+
// Deprecated no-op, accepted through 0.10 so scripts that pass it do not
|
|
283
|
+
// fail with UNKNOWN_FLAG. Installing a bundle is the trust decision: env
|
|
284
|
+
// run no longer blocks process-hijacking keys.
|
|
286
285
|
"allow-dangerous-env-keys": {
|
|
287
286
|
type: "boolean",
|
|
288
|
-
description: "
|
|
287
|
+
description: "Deprecated, no effect: env run no longer blocks process-hijacking variables. Accepted through 0.10 for existing scripts.",
|
|
289
288
|
default: false,
|
|
290
289
|
},
|
|
291
290
|
},
|
|
@@ -295,7 +294,6 @@ const envRunCommand = defineJsonCommand({
|
|
|
295
294
|
except: parseKeyListFlag(args.except),
|
|
296
295
|
clean: args.clean === true,
|
|
297
296
|
inherit: parseKeyListFlag(args.inherit) ?? [],
|
|
298
|
-
allowDangerousEnvKeys: args["allow-dangerous-env-keys"] === true,
|
|
299
297
|
});
|
|
300
298
|
},
|
|
301
299
|
});
|
package/dist/commands/env/env.js
CHANGED
|
@@ -37,13 +37,11 @@
|
|
|
37
37
|
*
|
|
38
38
|
* Value parsing is delegated to the `dotenv` package.
|
|
39
39
|
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
* values only, never keys;
|
|
44
|
-
*
|
|
45
|
-
* injected so this module keeps its narrow dependency surface (dotenv +
|
|
46
|
-
* core/common) and never reaches into the secret resolver/source machinery.
|
|
40
|
+
* Ref-token substitution: env VALUES may embed `${ref}` tokens (a secret's
|
|
41
|
+
* value, or any other asset's path), replaced when the env is injected (see
|
|
42
|
+
* `substituteRefTokens` in core/ref-token.ts and `resolveEnvBinding`).
|
|
43
|
+
* Substitution applies to values only, never keys; shell-style `${VAR}` /
|
|
44
|
+
* `$VAR` are left untouched.
|
|
47
45
|
*/
|
|
48
46
|
import fs from "node:fs";
|
|
49
47
|
import path from "node:path";
|
|
@@ -91,11 +89,11 @@ export function loadEnv(envPath) {
|
|
|
91
89
|
* it to the literal string `$(rm -rf ~)` and we re-emit it single-quoted. This
|
|
92
90
|
* backs `akm env export <ref> --out <file>` (file-only; never printed to stdout).
|
|
93
91
|
*
|
|
94
|
-
* NOTE: `${
|
|
95
|
-
*
|
|
96
|
-
* `${
|
|
97
|
-
* shells, never to the secret).
|
|
98
|
-
*
|
|
92
|
+
* NOTE: `${ref}` token substitution is intentionally NOT applied here. The
|
|
93
|
+
* export path emits values single-quoted as literals, so an unsubstituted
|
|
94
|
+
* `${secrets/name}` is written verbatim (it would expand to nothing under POSIX
|
|
95
|
+
* shells, never to the secret). Token resolution is scoped to the value
|
|
96
|
+
* injection path only; see `resolveEnvBinding`.
|
|
99
97
|
*/
|
|
100
98
|
export function buildShellExportScript(envPath) {
|
|
101
99
|
const env = loadEnv(envPath);
|
|
@@ -110,52 +108,6 @@ export function buildShellExportScript(envPath) {
|
|
|
110
108
|
}
|
|
111
109
|
return lines.length > 0 ? `${lines.join("\n")}\n` : "";
|
|
112
110
|
}
|
|
113
|
-
/**
|
|
114
|
-
* Matches a `${secret:NAME}` substitution token in an env value. The captured
|
|
115
|
-
* NAME accepts the same character set as a secret asset name (letters, digits,
|
|
116
|
-
* `_`, `.`, `/`, `-`). Only this exact form is recognised — shell-style
|
|
117
|
-
* `${VAR}` and `$VAR` deliberately do not match and are left untouched.
|
|
118
|
-
*/
|
|
119
|
-
const SECRET_TOKEN_RE = /\$\{secret:([A-Za-z0-9_./-]+)\}/g;
|
|
120
|
-
/**
|
|
121
|
-
* Replace every `${secret:NAME}` token in each value with the corresponding
|
|
122
|
-
* secret value, looked up via the injected `resolveSecret`. Keys are never
|
|
123
|
-
* touched. Multiple tokens per value and tokens embedded in larger strings
|
|
124
|
-
* (e.g. `Bearer ${secret:a}:${secret:b}`) are all substituted.
|
|
125
|
-
*
|
|
126
|
-
* `resolveSecret` returns `undefined` for an unknown secret name; such names are
|
|
127
|
-
* collected into `missing` (de-duplicated, in first-seen order) and their tokens
|
|
128
|
-
* are left unsubstituted in the returned values. Callers MUST treat a non-empty
|
|
129
|
-
* `missing` as a hard error and inject NOTHING — never partially inject.
|
|
130
|
-
*
|
|
131
|
-
* The lookup is injected so this module does not import the secret
|
|
132
|
-
* resolver/source machinery directly, preserving its narrow dependency surface.
|
|
133
|
-
* Resolved secret values must never be logged or printed by callers.
|
|
134
|
-
*/
|
|
135
|
-
export function resolveSecretTokens(values, resolveSecret) {
|
|
136
|
-
const missing = [];
|
|
137
|
-
const missingSeen = new Set();
|
|
138
|
-
const out = {};
|
|
139
|
-
const cache = new Map();
|
|
140
|
-
for (const [key, value] of Object.entries(values)) {
|
|
141
|
-
out[key] = value.replace(SECRET_TOKEN_RE, (match, name) => {
|
|
142
|
-
let resolved = cache.get(name);
|
|
143
|
-
if (!cache.has(name)) {
|
|
144
|
-
resolved = resolveSecret(name);
|
|
145
|
-
cache.set(name, resolved);
|
|
146
|
-
}
|
|
147
|
-
if (resolved === undefined) {
|
|
148
|
-
if (!missingSeen.has(name)) {
|
|
149
|
-
missingSeen.add(name);
|
|
150
|
-
missing.push(name);
|
|
151
|
-
}
|
|
152
|
-
return match;
|
|
153
|
-
}
|
|
154
|
-
return resolved;
|
|
155
|
-
});
|
|
156
|
-
}
|
|
157
|
-
return { values: out, missing };
|
|
158
|
-
}
|
|
159
111
|
/** Create an empty env file (does nothing if it already exists). */
|
|
160
112
|
export function createEnv(envPath) {
|
|
161
113
|
ensureParentDir(envPath);
|
|
@@ -5,8 +5,7 @@
|
|
|
5
5
|
* `akm secret` command family. Extracted verbatim from src/cli.ts (WS6) so the
|
|
6
6
|
* God Module shrinks; the `main.subCommands.secret` key and every subcommand's
|
|
7
7
|
* args/output shape are byte-identical. The ref-resolution helpers
|
|
8
|
-
* (parseSecretRef / makeSecretRef / resolveSecretPath
|
|
9
|
-
* findEnvSource) live in src/core/env-secret-ref.ts so env + secret share one
|
|
8
|
+
* (parseSecretRef / makeSecretRef / resolveSecretPath) live in src/core/env-secret-ref.ts so env + secret share one
|
|
10
9
|
* copy.
|
|
11
10
|
*
|
|
12
11
|
* `akm secret` manages whole-file secrets under each stash's secrets/ directory.
|
|
@@ -32,14 +31,13 @@ import path from "node:path";
|
|
|
32
31
|
import { getParsedInvocation } from "../../cli/invocation.js";
|
|
33
32
|
import { getStringArg } from "../../cli/parse-args.js";
|
|
34
33
|
import { defineGroupCommand, defineJsonCommand, output } from "../../cli/shared.js";
|
|
35
|
-
import { decideDangerousEnvInjection } from "../../core/activation-policy.js";
|
|
36
34
|
import { deriveCanonicalAssetName } from "../../core/asset/asset-placement.js";
|
|
37
35
|
import { loadConfig } from "../../core/config/config.js";
|
|
38
36
|
import { makeSecretRef, resolveSecretPath, resolveSecretWriteTarget, sensitiveMarkerPath, withEnvSecretWrite, } from "../../core/env-secret-ref.js";
|
|
39
37
|
import { ConfigError, NotFoundError, UsageError } from "../../core/errors.js";
|
|
40
38
|
import { appendEvent } from "../../core/events.js";
|
|
41
|
-
import { warn } from "../../core/warn.js";
|
|
42
39
|
import { resolveSourceEntries } from "../../indexer/search/search-source.js";
|
|
40
|
+
import { isFileOrFileSymlink } from "../../indexer/walk/walker.js";
|
|
43
41
|
import { readStdin } from "../../runtime.js";
|
|
44
42
|
import { buildChildEnv } from "./child-env.js";
|
|
45
43
|
function parseKeyListFlag(raw) {
|
|
@@ -65,7 +63,7 @@ function listSecretsRecursive() {
|
|
|
65
63
|
walk(full);
|
|
66
64
|
continue;
|
|
67
65
|
}
|
|
68
|
-
if (!entry.isFile())
|
|
66
|
+
if (!entry.isFile() && !(entry.isSymbolicLink() && isFileOrFileSymlink(full)))
|
|
69
67
|
continue;
|
|
70
68
|
if (entry.name.endsWith(".lock") || entry.name.endsWith(".sensitive"))
|
|
71
69
|
continue;
|
|
@@ -99,7 +97,7 @@ const secretSetCommand = defineJsonCommand({
|
|
|
99
97
|
args: {
|
|
100
98
|
ref: {
|
|
101
99
|
type: "positional",
|
|
102
|
-
description: "Secret ref (flat name, e.g. secrets/deploy-key
|
|
100
|
+
description: "Secret ref (flat name, e.g. secrets/deploy-key; use --path for a subdirectory)",
|
|
103
101
|
required: true,
|
|
104
102
|
},
|
|
105
103
|
path: {
|
|
@@ -170,9 +168,9 @@ const secretRunCommand = defineJsonCommand({
|
|
|
170
168
|
},
|
|
171
169
|
},
|
|
172
170
|
async run({ args }) {
|
|
173
|
-
// Validate the target env var name FIRST (before the command split) so
|
|
174
|
-
//
|
|
175
|
-
//
|
|
171
|
+
// Validate the target env var name FIRST (before the command split) so an
|
|
172
|
+
// invalid name is rejected regardless of how the command is supplied — and
|
|
173
|
+
// so the failure does not depend on argv parsing.
|
|
176
174
|
const varName = args.var;
|
|
177
175
|
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(varName)) {
|
|
178
176
|
throw new UsageError(`"${varName}" is not a valid environment variable name.`, "INVALID_FLAG_VALUE");
|
|
@@ -182,28 +180,12 @@ const secretRunCommand = defineJsonCommand({
|
|
|
182
180
|
throw new UsageError("Missing command. Usage: akm secret run <ref> <VAR> -- <command>");
|
|
183
181
|
}
|
|
184
182
|
const { name, absPath, source } = resolveSecretPath(args.ref);
|
|
185
|
-
|
|
186
|
-
throw new NotFoundError(`Secret not found: ${makeSecretRef(name, source)}`);
|
|
187
|
-
}
|
|
188
|
-
const { isDangerousEnvKey } = await import("../lint/env-key-rules.js");
|
|
189
|
-
if (isDangerousEnvKey(varName)) {
|
|
190
|
-
const detail = `"${varName}" is a known process-hijacking variable (e.g. LD_PRELOAD, PATH).`;
|
|
191
|
-
const decision = decideDangerousEnvInjection({
|
|
192
|
-
dangerousKeys: [varName],
|
|
193
|
-
thirdParty: Boolean(source.registryId),
|
|
194
|
-
});
|
|
195
|
-
if (decision === "block") {
|
|
196
|
-
throw new UsageError(`Refusing to inject a secret from a third-party stash into ${detail}\n` +
|
|
197
|
-
" Review the secret, then copy it into a first-party secret if you trust it.", "INVALID_FLAG_VALUE");
|
|
198
|
-
}
|
|
199
|
-
warn(`Injecting a secret into ${detail} Injecting anyway (first-party stash).`);
|
|
200
|
-
}
|
|
201
|
-
const { readValue } = await import("./secret.js");
|
|
183
|
+
const { readValue } = await import("../../core/secret-value.js");
|
|
202
184
|
const mergedEnv = buildChildEnv(process.env, {
|
|
203
185
|
clean: args.clean === true,
|
|
204
186
|
inherit: parseKeyListFlag(args.inherit) ?? [],
|
|
205
187
|
});
|
|
206
|
-
mergedEnv[varName] = readValue(absPath)
|
|
188
|
+
mergedEnv[varName] = readValue(absPath);
|
|
207
189
|
// Audit trail: record access by ref + var name only — never the value.
|
|
208
190
|
appendEvent({
|
|
209
191
|
eventType: "secret_access",
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* The supported value-use path is:
|
|
18
18
|
*
|
|
19
19
|
* - `akm secret run <ref> <VAR> -- <cmd>` — value injected into the child
|
|
20
|
-
* process env as `VAR=<value>` (see `readValue`).
|
|
20
|
+
* process env as `VAR=<value>` (see `readValue` in `core/secret-value.ts`).
|
|
21
21
|
*
|
|
22
22
|
* `akm secret path <ref>` (print the file path for the Docker `/run/secrets` +
|
|
23
23
|
* `_FILE` convention) was removed in 0.9.0 alongside `akm secret remove` — an
|
|
@@ -25,8 +25,8 @@
|
|
|
25
25
|
* and could silently target different files. A ref's file still lives at
|
|
26
26
|
* `<stash>/secrets/<name>`; locate or delete it there directly.
|
|
27
27
|
*
|
|
28
|
-
* Values are stored as raw bytes (no quoting, multi-line allowed)
|
|
29
|
-
*
|
|
28
|
+
* Values are stored as raw bytes (no quoting, multi-line allowed), unlike env
|
|
29
|
+
* values which forbid literal newlines; they are read back trimmed (`core/secret-value.ts`).
|
|
30
30
|
*/
|
|
31
31
|
import { createHash } from "node:crypto";
|
|
32
32
|
import fs from "node:fs";
|
|
@@ -82,13 +82,6 @@ function ensureParentDir(filePath) {
|
|
|
82
82
|
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
83
83
|
}
|
|
84
84
|
// ── Public API ──────────────────────────────────────────────────────────────
|
|
85
|
-
/**
|
|
86
|
-
* Read a secret's raw bytes. Internal use only (for `secret run`). Callers
|
|
87
|
-
* MUST NOT write the returned value to stdout or any log.
|
|
88
|
-
*/
|
|
89
|
-
export function readValue(secretPath) {
|
|
90
|
-
return fs.readFileSync(secretPath);
|
|
91
|
-
}
|
|
92
85
|
/**
|
|
93
86
|
* Write (create or overwrite) a secret with the given raw bytes, atomically at
|
|
94
87
|
* mode 0600 under a write-lock. No quoting; multi-line / binary allowed.
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* stale after upgrading akm through a different installer than the one
|
|
11
11
|
* active at the last `task sync` (e.g. npm-global to a standalone binary):
|
|
12
12
|
* the schedule kept invoking a version behind, with nothing in `akm health`
|
|
13
|
-
* surfacing it, until an unrelated failure (a rejected `
|
|
13
|
+
* surfacing it, until an unrelated failure (a rejected `${ref}` engine
|
|
14
14
|
* key on the stale binary) surfaced the drift.
|
|
15
15
|
*
|
|
16
16
|
* Modelled 1:1 on `version-drift.ts`'s shape: an injectable seam,
|
|
@@ -11,18 +11,19 @@
|
|
|
11
11
|
*
|
|
12
12
|
* Enforcement scope:
|
|
13
13
|
* - `akm lint` reports findings as `dangerous-env-key` (non-blocking warn).
|
|
14
|
-
* -
|
|
15
|
-
*
|
|
14
|
+
* - Nothing else consults this list. Installing a bundle is the trust
|
|
15
|
+
* decision, so `akm bundle add` and `akm bundle update` do not scan env
|
|
16
|
+
* files, and `akm env run` does not refuse a key. The
|
|
17
|
+
* `--allow-dangerous-env-keys` flag is a deprecated no-op.
|
|
16
18
|
* - Local env writes do NOT consult this list — by design, the operator may
|
|
17
|
-
* legitimately store any key locally.
|
|
18
|
-
* stash installation.
|
|
19
|
+
* legitimately store any key locally.
|
|
19
20
|
*
|
|
20
21
|
* False-positive tradeoff:
|
|
21
22
|
* A handful of keys (EDITOR, VISUAL, PAGER) are included because they are
|
|
22
23
|
* invoked by many interactive tools and are a documented RCE vector when
|
|
23
24
|
* sourced from untrusted environments. They will also flag on benign files
|
|
24
|
-
* where the operator legitimately wants to set their editor —
|
|
25
|
-
*
|
|
25
|
+
* where the operator legitimately wants to set their editor — the warning is
|
|
26
|
+
* advisory, so ignore it after review.
|
|
26
27
|
*/
|
|
27
28
|
import fs from "node:fs";
|
|
28
29
|
import { listKeys } from "../env/env.js";
|
|
@@ -497,7 +497,7 @@ export function lintAssetFile(ctx, subdir) {
|
|
|
497
497
|
issues.push(...factDiagnostics(ctx.relPath, ctx.data));
|
|
498
498
|
break;
|
|
499
499
|
case "tasks":
|
|
500
|
-
issues.push(...taskDiagnostics(ctx.relPath, ctx.raw
|
|
500
|
+
issues.push(...taskDiagnostics(ctx.relPath, ctx.raw));
|
|
501
501
|
break;
|
|
502
502
|
case "memories":
|
|
503
503
|
appendMemoryStubIssue(ctx, issues);
|
|
@@ -80,10 +80,7 @@ const canonicalProposalValidators = {
|
|
|
80
80
|
];
|
|
81
81
|
}
|
|
82
82
|
const sourcePath = proposal.changes[0]?.path || proposal.ref;
|
|
83
|
-
const result = compileWorkflowSource(content, {
|
|
84
|
-
path: sourcePath,
|
|
85
|
-
workspaceRoot: proposal.proposedTarget.root,
|
|
86
|
-
});
|
|
83
|
+
const result = compileWorkflowSource(content, { path: sourcePath });
|
|
87
84
|
if (result.ok)
|
|
88
85
|
return [];
|
|
89
86
|
return result.errors.map((error) => ({
|
|
@@ -26,7 +26,7 @@ import { extractSection, markdownFragmentSlugs } from "../../core/asset/markdown
|
|
|
26
26
|
import { buildMarkdownLeadContext, fragmentForSelector, MARKDOWN_FRAGMENT_CONTEXT_DEFAULT_MAX_CHARS, } from "../../core/asset/markdown-fragments.js";
|
|
27
27
|
import { displayRef, displayRefForConceptId, typeNameFromConceptId } from "../../core/asset/resolve-ref.js";
|
|
28
28
|
import { META_DIR, parseMetaRef, readMetaFile } from "../../core/asset/stash-meta.js";
|
|
29
|
-
import { asNonEmptyString,
|
|
29
|
+
import { asNonEmptyString, isContainedResolvedPath } from "../../core/common.js";
|
|
30
30
|
import { loadConfig } from "../../core/config/config.js";
|
|
31
31
|
import { NotFoundError, rethrowIfDataDirUnreadable, rethrowIfTestIsolationError, UsageError } from "../../core/errors.js";
|
|
32
32
|
import { appendEvent } from "../../core/events.js";
|
|
@@ -45,9 +45,7 @@ import { withIndexDb } from "../../storage/repositories/index-db.js";
|
|
|
45
45
|
import { getEntryById, getEntryIdByFilePath } from "../../storage/repositories/index-entries-repository.js";
|
|
46
46
|
import { getIndexedMarkdownFragment } from "../../storage/repositories/index-fts-repository.js";
|
|
47
47
|
import { readEntryLinks } from "../../storage/repositories/index-links-repository.js";
|
|
48
|
-
import { getCurrentWorkflowScopeKey } from "../../workflows/authoring/scope-key.js";
|
|
49
48
|
import { buildWorkflowAction } from "../../workflows/renderer.js";
|
|
50
|
-
import { getActiveWorkflowRun } from "../../workflows/runtime/runs.js";
|
|
51
49
|
/**
|
|
52
50
|
* Unified show: queries the local FTS5 index, then reads that row's file path.
|
|
53
51
|
* A physical owner can arbitrate collisions during lookup, but show never
|
|
@@ -381,20 +379,6 @@ export async function showLocal(input) {
|
|
|
381
379
|
}
|
|
382
380
|
if (response.type === "workflow")
|
|
383
381
|
response.action = buildWorkflowAction(canonicalRef);
|
|
384
|
-
// 07 P1-D: provenance-aware toolPolicy CEILING. An agent's self-declared
|
|
385
|
-
// `tools` frontmatter is honoured ONLY for the operator's own PRIMARY stash —
|
|
386
|
-
// the assets they authored. Every other source is content pulled from
|
|
387
|
-
// elsewhere and must not name its own tool grant: registry-installed packs, a
|
|
388
|
-
// configured secondary source, and even a git source the operator marked
|
|
389
|
-
// `--writable` to contribute edits upstream (writability is "can I push", not
|
|
390
|
-
// "do I trust this content to grant itself tools"). Drop the policy so dispatch
|
|
391
|
-
// falls back to the parent/default grant. Keys off primary-stash identity —
|
|
392
|
-
// `allSources[0]` is always the primary (search-source.ts) — not a
|
|
393
|
-
// name-derived registryId or the orthogonal `writable` bit. `source` undefined
|
|
394
|
-
// (unresolved path) also fails closed.
|
|
395
|
-
if (response.toolPolicy !== undefined && !isPrimaryStash) {
|
|
396
|
-
delete response.toolPolicy;
|
|
397
|
-
}
|
|
398
382
|
const editable = isEditable(assetPath, config, allSources);
|
|
399
383
|
// Evidence must describe the same revision as the rendered claim: ordinary
|
|
400
384
|
// Markdown show is source-live, while indexed fragment show is a snapshot.
|
|
@@ -421,10 +405,6 @@ export async function showLocal(input) {
|
|
|
421
405
|
...(!editable ? { editHint: buildEditHint(canonicalRef) } : {}),
|
|
422
406
|
...showLinks(indexedEntry.itemRef, displayDefaultBundle, input.readOnly === true),
|
|
423
407
|
};
|
|
424
|
-
const activeRun = input.readOnly ? null : await getActiveWorkflowRun(getCurrentWorkflowScopeKey());
|
|
425
|
-
if (activeRun) {
|
|
426
|
-
fullResponse.activeRun = activeRun;
|
|
427
|
-
}
|
|
428
408
|
if (input.detail === "brief") {
|
|
429
409
|
return buildBriefResponse(fullResponse, assetPath);
|
|
430
410
|
}
|
|
@@ -513,16 +493,15 @@ function findUnrecognizedScriptSource(assetParts, sources) {
|
|
|
513
493
|
continue;
|
|
514
494
|
const sourceRoot = path.resolve(source.path);
|
|
515
495
|
const candidate = path.resolve(assetPathForName("script", path.join(sourceRoot, scriptDir), assetParts.name));
|
|
516
|
-
if (!
|
|
496
|
+
if (!isContainedResolvedPath(candidate, sourceRoot))
|
|
517
497
|
continue;
|
|
518
498
|
const authoredStat = fs.lstatSync(candidate);
|
|
519
499
|
if (!authoredStat.isFile() && !authoredStat.isSymbolicLink())
|
|
520
500
|
continue;
|
|
521
|
-
const realRoot = fs.realpathSync(sourceRoot);
|
|
522
501
|
const realCandidate = fs.realpathSync(candidate);
|
|
523
|
-
if (!
|
|
502
|
+
if (!fs.statSync(realCandidate).isFile())
|
|
524
503
|
continue;
|
|
525
|
-
return { extension, path: realCandidate, sourceRoot:
|
|
504
|
+
return { extension, path: realCandidate, sourceRoot: fs.realpathSync(sourceRoot) };
|
|
526
505
|
}
|
|
527
506
|
catch {
|
|
528
507
|
// Missing, unreadable, dangling, or otherwise unsafe paths remain normal
|
|
@@ -7,8 +7,7 @@ import { VALID_ADAPTER_IDS } from "../../core/adapter/adapter-ids.js";
|
|
|
7
7
|
import { UsageError } from "../../core/errors.js";
|
|
8
8
|
import { appendEvent } from "../../core/events.js";
|
|
9
9
|
import { warn } from "../../core/warn.js";
|
|
10
|
-
import {
|
|
11
|
-
import { akmRemove, akmUpdate } from "./installed-stashes.js";
|
|
10
|
+
import { akmUpdate } from "./installed-stashes.js";
|
|
12
11
|
import { akmAdd } from "./source-add.js";
|
|
13
12
|
import { addStash, removeStash } from "./source-manage.js";
|
|
14
13
|
// ── Shared website-options helper ──────────
|
|
@@ -36,31 +35,6 @@ export function shouldWarnOnPlainHttp(ref) {
|
|
|
36
35
|
return true;
|
|
37
36
|
}
|
|
38
37
|
}
|
|
39
|
-
/**
|
|
40
|
-
* Audit a freshly-installed stash for dangerous env keys and decide whether the
|
|
41
|
-
* install must be blocked. The scanner and decision policy are shared with
|
|
42
|
-
* `bundle update` in dangerous-env-audit.ts.
|
|
43
|
-
*/
|
|
44
|
-
export async function auditInstalledStashForDangerousKeys(opts) {
|
|
45
|
-
const { installedStashRoot, ref, allowDangerousKeys, rollbackTarget, isTTY } = opts;
|
|
46
|
-
return auditStashForDangerousKeys({
|
|
47
|
-
stashRoot: installedStashRoot,
|
|
48
|
-
ref,
|
|
49
|
-
allowDangerousKeys,
|
|
50
|
-
isTTY,
|
|
51
|
-
operation: "install",
|
|
52
|
-
rollback: async () => {
|
|
53
|
-
try {
|
|
54
|
-
await akmRemove({ target: rollbackTarget });
|
|
55
|
-
return undefined;
|
|
56
|
-
}
|
|
57
|
-
catch {
|
|
58
|
-
return (`Rollback failed — stash may still be installed at ${installedStashRoot}. ` +
|
|
59
|
-
`Remove it manually with: akm bundle remove ${rollbackTarget}`);
|
|
60
|
-
}
|
|
61
|
-
},
|
|
62
|
-
});
|
|
63
|
-
}
|
|
64
38
|
// ── Command definition ────────────────────────────────────────────────────────
|
|
65
39
|
export const addCommand = defineJsonCommand({
|
|
66
40
|
meta: {
|
|
@@ -90,7 +64,8 @@ export const addCommand = defineJsonCommand({
|
|
|
90
64
|
after: { type: "string", description: "Insert the new bundle after this configured bundle" },
|
|
91
65
|
credential: {
|
|
92
66
|
type: "string",
|
|
93
|
-
|
|
67
|
+
// biome-ignore lint/suspicious/noTemplateCurlyInString: literal `${ref}` token syntax documented for users, not interpolation
|
|
68
|
+
description: "Git HTTPS bearer credential reference ($VAR or ${secrets/name}); never a literal token",
|
|
94
69
|
},
|
|
95
70
|
"max-pages": { type: "string", description: "Maximum pages to crawl for website sources (default: 50)" },
|
|
96
71
|
"max-depth": { type: "string", description: "Maximum crawl depth for website sources (default: 3)" },
|
|
@@ -99,16 +74,18 @@ export const addCommand = defineJsonCommand({
|
|
|
99
74
|
description: "Allow a plain HTTP source URL after explicitly accepting transport substitution risk.",
|
|
100
75
|
default: false,
|
|
101
76
|
},
|
|
77
|
+
// Deprecated no-op, accepted through 0.10 so scripts that pass it do not
|
|
78
|
+
// fail with UNKNOWN_FLAG. Installing a bundle is the trust decision: its
|
|
79
|
+
// env files are no longer scanned for process-hijacking keys.
|
|
102
80
|
"allow-dangerous-env-keys": {
|
|
103
81
|
type: "boolean",
|
|
104
|
-
description: "
|
|
82
|
+
description: "Deprecated, no effect: add no longer scans env files for process-hijacking keys. Accepted through 0.10 for existing scripts.",
|
|
105
83
|
default: false,
|
|
106
84
|
},
|
|
107
85
|
},
|
|
108
86
|
async run({ args }) {
|
|
109
87
|
const ref = args.ref.trim();
|
|
110
88
|
const allowInsecureTransport = args["allow-insecure-transport"];
|
|
111
|
-
const allowDangerousKeys = args["allow-dangerous-env-keys"];
|
|
112
89
|
if (args.before && args.after)
|
|
113
90
|
throw new UsageError("Only one of --before or --after may be used.");
|
|
114
91
|
// --provider → declarative bundle source (URL for git/website; bare
|
|
@@ -153,7 +130,7 @@ export const addCommand = defineJsonCommand({
|
|
|
153
130
|
if (args.provider === "git" && result.added && result.entry?.name) {
|
|
154
131
|
let updated;
|
|
155
132
|
try {
|
|
156
|
-
updated = await akmUpdate({ target: result.entry.name
|
|
133
|
+
updated = await akmUpdate({ target: result.entry.name });
|
|
157
134
|
}
|
|
158
135
|
catch (error) {
|
|
159
136
|
removeStash(result.entry.name);
|
|
@@ -204,38 +181,6 @@ export const addCommand = defineJsonCommand({
|
|
|
204
181
|
writable: args.writable === true,
|
|
205
182
|
},
|
|
206
183
|
});
|
|
207
|
-
// ── Post-install env key audit ──────────────────────────────────────────
|
|
208
|
-
// Resolve the stash root from the install result and scan any env files
|
|
209
|
-
// for dangerous env var keys. When findings are present the install is
|
|
210
|
-
// gated: TTY → interactive confirmation prompt; non-TTY without
|
|
211
|
-
// --allow-dangerous-env-keys → hard failure (exit 1). Pass the dedicated
|
|
212
|
-
// flag to skip the prompt non-interactively.
|
|
213
|
-
const installedStashRoot = result.installed?.stashRoot ??
|
|
214
|
-
(result.sourceAdded && "stashRoot" in result.sourceAdded ? result.sourceAdded.stashRoot : undefined);
|
|
215
|
-
if (installedStashRoot) {
|
|
216
|
-
// Use the canonical installed id (most reliably resolved by akmRemove) rather
|
|
217
|
-
// than the raw user-supplied ref which may not match after URL normalisation.
|
|
218
|
-
const rollbackTarget = result.installed?.id ?? result.sourceAdded?.stashRoot ?? ref;
|
|
219
|
-
// The audit RETURNS its decision; we decide the exit outcome here,
|
|
220
|
-
// OUTSIDE any catch, so the abort cannot be lost to a swallowed
|
|
221
|
-
// exception (C3). F4: `process.exitCode = …; return;` instead of
|
|
222
|
-
// `process.exit(...)` — the DANGEROUS_ENV_KEY error envelope has
|
|
223
|
-
// already been written to stderr by the audit itself (or is about to
|
|
224
|
-
// be), so an explicit `return` here (skipping the success `output("add",
|
|
225
|
-
// …)` below) is load-bearing, not merely cosmetic: it is what stops a
|
|
226
|
-
// blocked install from ALSO printing a success envelope.
|
|
227
|
-
const decision = await auditInstalledStashForDangerousKeys({
|
|
228
|
-
installedStashRoot,
|
|
229
|
-
ref,
|
|
230
|
-
allowDangerousKeys,
|
|
231
|
-
rollbackTarget,
|
|
232
|
-
isTTY: process.stdin.isTTY === true,
|
|
233
|
-
});
|
|
234
|
-
if (decision.blocked) {
|
|
235
|
-
process.exitCode = decision.exitCode;
|
|
236
|
-
return;
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
184
|
output("add", result);
|
|
240
185
|
},
|
|
241
186
|
});
|