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.
Files changed (146) hide show
  1. package/CHANGELOG.md +216 -0
  2. package/SECURITY.md +2 -2
  3. package/STABILITY.md +2 -2
  4. package/dist/assets/hints/cli-hints-full.md +6 -7
  5. package/dist/assets/hints/cli-hints-short.md +3 -4
  6. package/dist/commands/command/execution-source-loader.js +10 -14
  7. package/dist/commands/env/env-binding.js +47 -58
  8. package/dist/commands/env/env-cli.js +20 -22
  9. package/dist/commands/env/env.js +10 -58
  10. package/dist/commands/env/secret-cli.js +9 -27
  11. package/dist/commands/env/secret.js +3 -10
  12. package/dist/commands/health/scheduler-binary.js +1 -1
  13. package/dist/commands/improve/distill.js +0 -2
  14. package/dist/commands/improve/eligibility.js +0 -2
  15. package/dist/commands/lint/env-key-rules.js +7 -6
  16. package/dist/commands/lint/index.js +1 -1
  17. package/dist/commands/proposal/validators/proposal-validators.js +1 -4
  18. package/dist/commands/read/show.js +4 -25
  19. package/dist/commands/sources/add-cli.js +8 -63
  20. package/dist/commands/sources/bundle-cli.js +8 -9
  21. package/dist/commands/sources/installed-stashes.js +117 -61
  22. package/dist/commands/sources/schema-repair.js +0 -2
  23. package/dist/commands/sources/source-clone.js +31 -7
  24. package/dist/commands/tasks/explain.js +1 -1
  25. package/dist/commands/tasks/tasks.js +38 -56
  26. package/dist/commands/ui/redact.js +4 -4
  27. package/dist/commands/ui/workspace-actions.js +1 -1
  28. package/dist/commands/workflow/plan.js +47 -8
  29. package/dist/commands/workflow-cli.js +37 -41
  30. package/dist/core/adapter/adapters/akm-adapter.js +3 -3
  31. package/dist/core/adapter/adapters/akm-lint.js +3 -4
  32. package/dist/core/adapter/adapters/akm-metadata.js +2 -2
  33. package/dist/core/adapter/adapters/akm-task-adapter.js +5 -6
  34. package/dist/core/adapter/adapters/dotenv-adapter.js +4 -3
  35. package/dist/core/asset/resolve-ref.js +5 -0
  36. package/dist/core/bundle-rename.js +12 -4
  37. package/dist/core/common.js +4 -52
  38. package/dist/core/config/config-walker.js +1 -1
  39. package/dist/core/config/config.js +55 -18
  40. package/dist/core/config/schema/primitives.js +12 -6
  41. package/dist/core/config/schema/sources-bundles.js +2 -2
  42. package/dist/core/env-secret-ref.js +84 -73
  43. package/dist/core/errors.js +1 -1
  44. package/dist/core/paths.js +1 -6
  45. package/dist/core/redaction.js +16 -3
  46. package/dist/core/ref-token.js +82 -0
  47. package/dist/core/secret-value.js +14 -0
  48. package/dist/core/write-source.js +20 -4
  49. package/dist/indexer/indexer.js +27 -36
  50. package/dist/indexer/installations.js +1 -8
  51. package/dist/indexer/lookup/adapter-concept-owner.js +40 -14
  52. package/dist/indexer/materialize-embeddings.js +1 -1
  53. package/dist/indexer/scan/drain-dir.js +3 -3
  54. package/dist/indexer/search/search-source.js +4 -2
  55. package/dist/indexer/walk/path-resolver.js +13 -68
  56. package/dist/indexer/walk/walker.js +18 -22
  57. package/dist/integrations/agent/engine-resolution.js +9 -7
  58. package/dist/integrations/agent/runner-dispatch.js +4 -5
  59. package/dist/integrations/agent/runner.js +2 -0
  60. package/dist/llm/client.js +1 -1
  61. package/dist/llm/embedders/remote.js +4 -3
  62. package/dist/output/renderers.js +3 -4
  63. package/dist/output/shapes/helpers.js +0 -2
  64. package/dist/output/text/command-format.js +12 -2
  65. package/dist/output/text/show-directives.js +16 -28
  66. package/dist/output/text/workflow-format.js +37 -7
  67. package/dist/scripts/akm-migrate-node.js +45865 -44692
  68. package/dist/scripts/akm-migrate.js +45804 -44630
  69. package/dist/setup/steps/tasks.js +1 -2
  70. package/dist/sources/providers/provider-utils.js +12 -0
  71. package/dist/sources/resolve.js +12 -16
  72. package/dist/sources/snapshot-fetchers/secret-seam.js +2 -4
  73. package/dist/sources/snapshot-fetchers/x.js +1 -1
  74. package/dist/storage/repositories/proposals-repository.js +2 -2
  75. package/dist/storage/repositories/task-history-repository.js +2 -2
  76. package/dist/storage/repositories/workflow-runs-repository.js +38 -116
  77. package/dist/tasks/embedded.js +1 -1
  78. package/dist/tasks/prepare/prepare-support.js +3 -2
  79. package/dist/tasks/prepare/prepare.js +3 -4
  80. package/dist/tasks/prepare/script-capture.js +8 -22
  81. package/dist/tasks/run/load-task.js +46 -14
  82. package/dist/tasks/run/run-native-task.js +0 -8
  83. package/dist/tasks/run/run-workflow-task.js +2 -2
  84. package/dist/tasks/scheduler-sync.js +0 -1
  85. package/dist/tasks/source/bounded-document.js +0 -41
  86. package/dist/tasks/source/parse-task-source.js +0 -1
  87. package/dist/tasks/source/task-source-v4.js +1 -4
  88. package/dist/workflows/compile.js +1 -15
  89. package/dist/workflows/exec/child-workflow.js +1 -2
  90. package/dist/workflows/exec/dispatch-redaction.js +24 -6
  91. package/dist/workflows/exec/environment.js +11 -74
  92. package/dist/workflows/exec/exec-unit.js +31 -23
  93. package/dist/workflows/exec/frozen-judge.js +17 -3
  94. package/dist/workflows/exec/native-executor.js +70 -26
  95. package/dist/workflows/exec/run-workflow.js +31 -18
  96. package/dist/workflows/exec/step-work.js +4 -3
  97. package/dist/workflows/exec/unit-dispatch.js +1 -8
  98. package/dist/workflows/freeze/environment.js +14 -119
  99. package/dist/workflows/freeze/freeze.js +0 -1
  100. package/dist/workflows/freeze/step-values.js +1 -2
  101. package/dist/workflows/freeze/targets/child-workflow.js +3 -5
  102. package/dist/workflows/freeze/targets/command.js +6 -9
  103. package/dist/workflows/freeze/targets/shell.js +0 -3
  104. package/dist/workflows/parser.js +6 -25
  105. package/dist/workflows/plan.js +6 -2
  106. package/dist/workflows/renderer.js +77 -4
  107. package/dist/workflows/runtime/run-plan.js +37 -23
  108. package/dist/workflows/runtime/runs.js +41 -84
  109. package/dist/workflows/runtime/workflow-asset-loader.js +55 -86
  110. package/dist/workflows/source-files.js +7 -19
  111. package/docs/architecture/0.10-process-rules.md +6 -6
  112. package/docs/architecture/architecture.md +6 -6
  113. package/docs/architecture/comparisons/claude-code-vs-akm-workflows-full.md +12 -18
  114. package/docs/architecture/decisions/0003-child-env-allowlist-and-provenance.md +6 -8
  115. package/docs/architecture/internals/storage-locations.md +4 -4
  116. package/docs/architecture/persisted-data-compat.md +3 -2
  117. package/docs/architecture/specs/0.9.0-public-api-issue-backlog.md +1 -1
  118. package/docs/architecture/specs/akm-format-neutral-bundle-workspace-spec.md +3 -3
  119. package/docs/architecture/testing/manual-testing-checklist.md +46 -78
  120. package/docs/architecture/testing/task-workflow-manual-tests.md +22 -40
  121. package/docs/architecture/testing/testing-workflow.md +9 -11
  122. package/docs/architecture/workflow-engine.md +41 -27
  123. package/docs/guides/author-bundles.md +14 -17
  124. package/docs/guides/author-workflows.md +43 -9
  125. package/docs/guides/bundles.md +16 -11
  126. package/docs/guides/claude-code-vs-akm-workflows.md +1 -1
  127. package/docs/guides/run-workflows.md +25 -13
  128. package/docs/guides/scheduling.md +1 -1
  129. package/docs/maintainers/README.md +2 -3
  130. package/docs/maintainers/release-checklist.md +73 -126
  131. package/docs/migration/v0.9-to-v0.10.md +5 -1
  132. package/docs/reference/cli.md +124 -112
  133. package/docs/reference/configuration.md +16 -9
  134. package/docs/reference/data-and-telemetry.md +1 -1
  135. package/docs/reference/env-and-secrets.md +66 -15
  136. package/docs/reference/registry.md +6 -11
  137. package/docs/reference/tasks.md +11 -3
  138. package/docs/reference/ui-api.md +3 -3
  139. package/docs/reference/workflow-schema.md +109 -36
  140. package/package.json +1 -1
  141. package/schemas/akm-task.json +2 -4
  142. package/dist/commands/sources/dangerous-env-audit.js +0 -134
  143. package/dist/core/activation-policy.js +0 -67
  144. package/dist/execution/directory-identity.js +0 -78
  145. package/dist/workflows/authoring/scope-key.js +0 -79
  146. 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 / findEnvSource / makeEnvRef / resolveEnvPath + the env-path
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 name (flat, e.g. prod → prod.env; use --path for a subdirectory)",
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 { name, absPath, source } = resolveEnvPath(args.ref);
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 — after scanning the injected keys
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
- const { name, absPath, source } = resolveEnvPath(target);
189
- if (!fs.existsSync(absPath)) {
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
- baseExists = fs.existsSync(resolveEnvPath(target.slice(0, slash)).absPath);
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 new NotFoundError(`Env not found: ${makeEnvRef(name, source)}`);
205
+ throw error;
209
206
  }
210
- // Load → filter → secret-substitute → dangerous-key policy → keys-only
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 `${secret:NAME}` token syntax documented for users, not interpolation
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 `${secret:NAME}` tokens, replaced at run time with the sibling `${secret:NAME}` value from the same bundle. Pass --clean to start the child with a minimal inherited environment instead of the full parent environment.",
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: "Allow injecting a process-hijacking variable (e.g. LD_PRELOAD, GIT_SSH_COMMAND) from a third-party stash, which otherwise blocks. Use only after explicitly reviewing the env file.",
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
  });
@@ -37,13 +37,11 @@
37
37
  *
38
38
  * Value parsing is delegated to the `dotenv` package.
39
39
  *
40
- * Secret-token substitution: env VALUES may embed `${secret:NAME}` tokens, which
41
- * are replaced at `env run` time with the value of the sibling `secret:NAME`
42
- * asset in the SAME stash (see `resolveSecretTokens`). Substitution applies to
43
- * values only, never keys; only the `${secret:...}` form is recognised —
44
- * shell-style `${VAR}` / `$VAR` are left untouched. The secret lookup is
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: `${secret:NAME}` token substitution is intentionally NOT applied here.
95
- * The export path emits values single-quoted as literals, so an unsubstituted
96
- * `${secret:NAME}` is written verbatim (it would expand to nothing under POSIX
97
- * shells, never to the secret). Secret-token resolution is scoped to the
98
- * `env run` value-injection path only; see `resolveSecretTokens`.
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, plus the shared
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 or just deploy-key; use --path for a subdirectory)",
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 a
174
- // dangerous/invalid name is rejected regardless of how the command is
175
- // supplied — and so the failure does not depend on argv parsing.
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
- if (!fs.existsSync(absPath)) {
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).toString("utf8");
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) so they
29
- * round-trip byte-exact, unlike env values which forbid literal newlines.
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 `secret://` engine
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,
@@ -1074,8 +1074,6 @@ async function defaultLookup(ref, stashDir) {
1074
1074
  return resolveAssetPath(ref, {
1075
1075
  stashDir,
1076
1076
  mode: "disk-only",
1077
- directoryIndexNames: ["SKILL.md"],
1078
- preserveDirectNameFallback: true,
1079
1077
  honorOrigin: false,
1080
1078
  });
1081
1079
  }
@@ -355,8 +355,6 @@ export async function findAssetFilePath(ref, stashDir, writableBundleIds) {
355
355
  stashDir,
356
356
  mode: "disk-only",
357
357
  writableBundleIds,
358
- directoryIndexNames: ["SKILL.md"],
359
- preserveDirectNameFallback: true,
360
358
  honorOrigin: true,
361
359
  });
362
360
  }
@@ -11,18 +11,19 @@
11
11
  *
12
12
  * Enforcement scope:
13
13
  * - `akm lint` reports findings as `dangerous-env-key` (non-blocking warn).
14
- * - `akm bundle add` BLOCKS install unless `--allow-dangerous-env-keys` is set (or, on TTY,
15
- * the user explicitly confirms at the prompt).
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. The gate exists only for third-party
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 — accept the
25
- * FP and bypass with `--allow-dangerous-env-keys` after review.
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, ctx.stashRoot));
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, isWithin } from "../../core/common.js";
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 (!isWithin(candidate, sourceRoot))
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 (!isWithin(realCandidate, realRoot) || !fs.statSync(realCandidate).isFile())
502
+ if (!fs.statSync(realCandidate).isFile())
524
503
  continue;
525
- return { extension, path: realCandidate, sourceRoot: realRoot };
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 { auditStashForDangerousKeys } from "./dangerous-env-audit.js";
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
- description: "Git HTTPS bearer credential reference ($VAR or secret://name); never a literal token",
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: "Allow reviewed process-hijacking env keys (e.g. LD_PRELOAD, PATH) in the installed bundle.",
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, allowDangerousEnvKeys: allowDangerousKeys });
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
  });