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
@@ -19,6 +19,16 @@ command; use `env` or `secret`.
19
19
  | Discoverable | key *names* (not values) | name only (the whole file is the value) |
20
20
  | Injects | many vars at once (`env run`) | one var (`secret run <ref> <VAR>`) |
21
21
 
22
+ ## Addressing: the akm ref
23
+
24
+ `env` and `secret` assets are addressed like every other asset, by their akm
25
+ ref `[bundle//]conceptId`: `env/prod`, `secrets/gitea_token`,
26
+ `team//secrets/discord_bot_token`. There is no bare-name form. `akm secret
27
+ run gitea_token ...` and `akm env run prod -- ...` are usage errors that name
28
+ the full ref (`secrets/<name>`, `env/<name>`). An unqualified ref resolves the
29
+ way every unqualified ref does: the default bundle first, then installation
30
+ priority.
31
+
22
32
  ## Core security guarantee
23
33
 
24
34
  Values never appear in akm's structured output. `env list` and `akm show
@@ -31,8 +41,8 @@ the child process — never through akm's own stdout, the index, or `akm
31
41
  show`.
32
42
 
33
43
  ```sh
34
- akm env create prod # create an empty .env group
35
- akm env create prod --from-file ./.env # or ingest an existing .env
44
+ akm env create env/prod # create an empty .env group
45
+ akm env create env/prod --from-file ./.env # or ingest an existing .env
36
46
 
37
47
  # akm does not manage individual keys — edit the whole file with your own editor:
38
48
  $EDITOR "$(akm env path env/prod --quiet)"
@@ -58,17 +68,51 @@ script (re-serialised single-quoted, so shell substitutions in the source
58
68
  `.env` become literal strings) — it requires `--out` and never prints values
59
69
  to stdout.
60
70
 
61
- An env value can be a `${secret:<name>}` token, such as
62
- `DISCORD_BOT_TOKEN=${secret:discord_bot_token}`. `akm env run` and a workflow
63
- unit's `env:` binding both replace it with the secret at `secrets/<name>` in
64
- the env asset's own bundle; `env export` writes the token verbatim. `<name>` is
65
- a path under that `secrets/` directory, so `bundle//name` inside the token is
66
- not a bundle qualifier. A missing secret is an error and nothing is injected.
67
- In a workflow, every value of a bound env asset is redacted from the run's
68
- output and each read records an `env_access` event, so keep non-secret ids out
69
- of env assets used there. See
71
+ ## The `${ref}` token
72
+
73
+ Wherever akm substitutes a reference into a string it hands to a process or a
74
+ provider, the token is `${[bundle//]conceptId}`:
75
+
76
+ - an env value: `DISCORD_BOT_TOKEN=${team//secrets/discord_bot_token}`;
77
+ - a config credential (`engines.<name>.apiKey`, `embedding.apiKey`, a git
78
+ bundle's `credential`): `"${secrets/openai}"`.
79
+
80
+ Where the token may appear decides what it may name:
81
+
82
+ - **In an env value**, any asset ref substitutes. A `secrets/...` ref stands for
83
+ the secret's value; any other asset ref (for example
84
+ `${scripts/lab/fetch.sh}`) stands for the asset's absolute path.
85
+ - **In a config credential**, only a `secrets/...` ref is accepted. A `${...}`
86
+ naming any other kind of asset is rejected when the config loads.
87
+
88
+ The ref is resolved with the one ref walk, so it may name any bundle and any
89
+ asset its owner holds. A `${...}` body is a ref when it leads with a native type
90
+ directory, qualified or not (`${secrets/x}`, `${scripts/run.sh}`,
91
+ `${team//secrets/x}`), or when it is bundle-qualified and that bundle is
92
+ installed (`${docs//tables/customers}`, whatever adapter owns it). `akm env
93
+ run` and a workflow unit's `env:` binding replace the tokens in an env file's
94
+ values; `env export` writes them verbatim. A `${...}` body that is not an akm
95
+ ref (`${HOME}`, `${PATH//:/ }`, and shell substitution such as `${v//-/_}`
96
+ unless a bundle named `v` is installed) is plain text: it is left untouched in an env
97
+ value, and in a config credential a `${NAME}` that is a valid variable name is
98
+ expanded from the process environment. A ref no bundle owns is an error and
99
+ nothing is injected. Each read records an `env_access` event. See
70
100
  [Workflow Schema: Binding a secret to a unit](workflow-schema.md#binding-a-secret-to-a-unit).
71
101
 
102
+ A secret's value is its file content with surrounding whitespace trimmed. That
103
+ one read is used by `akm secret run`, env tokens, config credentials and the
104
+ website fetchers, so a hand-written file's trailing newline never lands in an
105
+ HTTP header.
106
+
107
+ `${secret:NAME}` was the old env-value spelling, meaning the secret `NAME` in the
108
+ env file's own bundle. akm still reads it that way, as `${<bundle>//secrets/NAME}`
109
+ with the bundle that owns the env file, and warns once naming the env ref. The
110
+ env file is bundle content and is not rewritten; write the full ref.
111
+
112
+ `secret://name` was the config spelling of the same thing. It is retired:
113
+ config still reads it as `${secrets/name}` and names the key once, and `akm
114
+ migrate apply` rewrites the file.
115
+
72
116
  ## File modes and storage
73
117
 
74
118
  `.env` files are stored at mode 0600 under `env/` in the target bundle;
@@ -80,6 +124,12 @@ secret can carry a sibling `.sensitive` marker that excludes it from `list`
80
124
  output and from the search index entirely; the value remains usable via
81
125
  `run`.
82
126
 
127
+ An env file or secret may be a symlink to a file anywhere else, such as a
128
+ container's `/run/secrets/<name>`: `akm env`, `akm secret`, `${secrets/name}`
129
+ substitution and a workflow `env:` binding follow it. A name still cannot climb
130
+ out of `env/` or `secrets/` with `..` or an absolute path. Writes never follow
131
+ a symlink out of the bundle.
132
+
83
133
  ## Write-target resolution
84
134
 
85
135
  Env/secret **mutations** (`env create`, `env remove`, `secret set`) choose
@@ -108,10 +158,11 @@ subprocess the child spawns.
108
158
  - Avoid running commands that print the environment (`env`, `printenv`,
109
159
  shell tracing, verbose diagnostics) in agent contexts unless you
110
160
  explicitly intend to expose the child environment.
111
- - Before spawning, injected key names are scanned for known
112
- process-hijacking variables (`LD_PRELOAD`, `PATH`, `GIT_CONFIG_*`, ...): a
113
- first-party bundle warns and proceeds, a third-party-sourced bundle is
114
- refused.
161
+ - Installing a bundle is the trust decision: `env run`, `secret run` and
162
+ workflow `env:` bindings inject what the bundle's files define, including
163
+ keys such as `PATH` or `LD_PRELOAD`, from any bundle, with no warning, block
164
+ or flag. `akm lint` still reports such keys (`dangerous-env-key`,
165
+ non-blocking) so an author can review their own files.
115
166
 
116
167
  ## Stability
117
168
 
@@ -215,17 +215,12 @@ akm bundle add file:///absolute/path/to/bundle
215
215
  3. **Download and extract** -- The tarball is downloaded (or repo cloned) to a
216
216
  cache directory under `~/.cache/akm/registry/` and extracted securely
217
217
  (path traversal is rejected).
218
- 4. **Security audit** -- Before install completes, the extracted bundle's
219
- `env`/`secret` asset **key names** are checked against a denylist of
220
- process-execution-hijacking variables -- dynamic-linker hooks
221
- (`LD_PRELOAD`, `DYLD_INSERT_LIBRARIES`, ...), `PATH`, shell/runtime startup
222
- hooks (`BASH_ENV`, `PROMPT_COMMAND`, `NODE_OPTIONS`, `PYTHONSTARTUP`, ...),
223
- and interactive-tool overrides (`EDITOR`, `PAGER`, `GIT_SSH_COMMAND`, ...).
224
- `akm bundle add` **blocks the install** when a dangerous key is present unless
225
- `--allow-dangerous-env-keys` is set (or you confirm at an interactive prompt).
226
- This is a **key-name audit only** (plus the path-traversal rejection in step
227
- 3) -- akm does **not** scan source files, prompts, metadata, or install
228
- scripts for prompt-injection phrases, shell pipes, or lifecycle hooks.
218
+ 4. **No content audit** -- Installing a bundle is the trust decision. akm does
219
+ **not** scan the extracted bundle's source files, prompts, metadata, env
220
+ files or install scripts (beyond the path-traversal rejection in step 3), and
221
+ `akm bundle add` never blocks or prompts over the keys an env file sets.
222
+ `akm lint` reports env keys that can hijack a process (`dangerous-env-key`)
223
+ for authors who want to review their own bundle.
229
224
  5. **Bundle root detection** -- The extracted contents are scanned for asset
230
225
  type directories (`scripts/`, `skills/`, etc.) or a `.stash/` marker. If the
231
226
  bundle nests its bundle under an `opencode/` subdirectory, that is detected
@@ -116,9 +116,17 @@ Script refs use this closed table; any other extension fails before dispatch:
116
116
  `run` is one non-empty shell string. It may specify `shell` from the closed host
117
117
  shell table `bash`, `sh`, `zsh`, `pwsh`, `powershell`, or `cmd`. Shell expansion
118
118
  is runtime behavior for an explicitly authored task `run`; AKM does not infer a
119
- shell from `uses`. `working-directory` must be a relative, contained path under
120
- the task's workspace root. Absolute paths, traversal, dangling links, and
121
- symlink escapes fail before execution.
119
+ shell from `uses`. `working-directory` is a non-empty string, relative to the
120
+ task's bundle root or absolute, like a workflow's `exec.cwd`. akm does not
121
+ keep it inside the bundle or check it when the task source is parsed, so a
122
+ symlink that resolves outside the bundle works. The task runs in
123
+ `<bundle root>/<working-directory>` (or the absolute path as written): akm does
124
+ not capture the directory's identity or re-check it when the task runs. A
125
+ `uses: scripts/<file>` target may itself be a symlink to a file outside the
126
+ bundle; the task freezes that file's bytes, not its location. The interpreter
127
+ comes from the name the script has in the bundle, not from the file the link
128
+ points to, so `scripts/deploy.sh` linked to `real-script.txt` runs under the
129
+ shell.
122
130
 
123
131
  Every field that used to live under v3's `akm:` options bag is a top-level
124
132
  key in task source v4: `agent`, `engine`, `model`, `inference`, `tools`,
@@ -32,7 +32,7 @@ release, and nothing outside the page should script against them. Use the CLI's
32
32
  `ORIGIN_NOT_ALLOWED`, `METHOD_NOT_ALLOWED`, `UNSUPPORTED_MEDIA_TYPE`, `BODY_TOO_LARGE`, `INVALID_JSON_ARGUMENT`,
33
33
  `NOT_FOUND`, `INTERNAL_ERROR`.
34
34
  - **Redaction.** Config values under secret-like keys (`apiKey`, `token`, `password`, `secret`, `credential`,
35
- `authorization`, anything under `env` or `secrets`) are `"[redacted]"`; symbolic references (`$VAR`, `secret://name`)
35
+ `authorization`, anything under `env` or `secrets`) are `"[redacted]"`; symbolic references (`$VAR`, `${secrets/name}`)
36
36
  are kept. Credentials inside any URL in any response (userinfo, `token=`, `api_key=`, `sig=`, ...) are masked. Env and
37
37
  secret assets are never read; see `/api/asset`.
38
38
  - **Timestamps** are ISO 8601 UTC unless noted. Counts that cannot be computed are `null`, never `NaN`.
@@ -616,7 +616,7 @@ general config editor or command-execution endpoint.
616
616
  | `POST` route | Body and existing operation |
617
617
  | --- | --- |
618
618
  | `/api/feedback` | `{ref, signal: "positive"\|"negative", reason?, tags?: string[]}` → `akm feedback`. The ref is passed to `akm feedback` as given, which resolves it as the CLI does; tags follow existing `key:value` validation. Negative feedback requires a reason when `feedback.requireReason` is enabled. Submissions are recorded with source `user` unless the server runs with an `AKM_EVENT_SOURCE` already set (for example under an improve or task run), which wins as it does for every akm child; they retain existing ranking/utility/event effects. Feedback does not save config, so it works under a config that still holds a literal API key. No fix, supersession or model flags are exposed. |
619
- | `/api/bundles/add` | `{source, name?, writable?}` → `akm bundle add`, including source validation and installation audit. Existing provider restrictions apply; no dangerous-key or insecure-transport bypass is exposed. |
619
+ | `/api/bundles/add` | `{source, name?, writable?}` → `akm bundle add`, including source validation. Existing provider restrictions apply; no insecure-transport bypass is exposed. |
620
620
  | `/api/bundles/update` | `{bundle}` → `akm bundle update -- <bundle>`. Fetches/reindexes only on explicit submission. A rare update requiring destructive confirmation remains refused; the UI does not silently pass `--yes`. |
621
621
  | `/api/bundles/remove` | `{bundle, confirm: true}` → `akm bundle remove --yes -- <bundle>`. The UI requires an inline confirmation before this call. Managed installs may be deleted; local filesystem source files remain on disk. |
622
622
  | `/api/bundles/settings` | `{bundle?, enabled?, writable?, componentWritability?: {existingComponent: boolean}, defaultBundle?, defaultWriteTarget?}` → existing schema-walking config setters inside `mutateConfig`. Only changed controls are submitted. Defaults take existing names or `""` to unset a local value; inherited values cannot be unset locally. Existing validation rejects invalid defaults, disabled targets and writable npm/website sources. |
@@ -624,7 +624,7 @@ general config editor or command-execution endpoint.
624
624
  | `/api/asset/save` | `{ref, revision, content}` → the existing shared Markdown writer with `force` and the expected raw-content hash. Only indexed writable AKM knowledge/memory documents accepted by `/api/asset/source` can be edited. The writer takes the asset-mutation lease and compares the live source with `revision` inside it, immediately before writing. While another writer holds the lease the save fails fast with `409 ASSET_MUTATION_LEASE_HELD` instead of waiting. A stale revision returns `409 ASSET_CONTENT_CHANGED` without writing; a source deleted after it was loaded is refused (`404 ASSET_NOT_FOUND`) rather than recreated. The browser keeps the draft after a rejected save. An identical save returns `unchanged: true` without rewriting, reindexing or recording an event. Changed saves retain normal writer validation, indexing and Git effects. |
625
625
 
626
626
  Configuration-affecting actions do not round-trip the redacted API configuration. Existing mutation helpers retain
627
- unknown/unrelated fields, local overrides and supported `$VAR`/`secret://` credential references. If the existing
627
+ unknown/unrelated fields, local overrides and supported `$VAR`/`${ref}` credential references. If the existing
628
628
  config sanitizer would remove unrelated legacy literal credentials, a bundle add or remove, or a settings change that
629
629
  would write config, is refused with `400 INVALID_FLAG_VALUE` and the user is asked to correct those settings through
630
630
  the CLI first. A settings request that changes nothing writes nothing and is not refused. It never writes
@@ -515,7 +515,7 @@ steps:
515
515
  unit:
516
516
  exec:
517
517
  command: ["bun", "run", "test:unit"]
518
- cwd: packages/core # optional, relative
518
+ cwd: packages/core # optional: relative to the workflow's bundle root, or absolute
519
519
  timeout: "10m"
520
520
  retry: { max: 1, on: [timeout] }
521
521
  ```
@@ -554,11 +554,66 @@ Bounds: 1–64 argv entries, each a non-empty string of at most 4096 bytes.
554
554
 
555
555
  ### `cwd`
556
556
 
557
- Optional and **relative**. It resolves inside the unit's working directory —
558
- the engine invocation's working directory. Absolute paths, Windows drive
559
- letters, `~`, and `..` segments are rejected by the parser *and* by the
560
- frozen-plan decoder, and containment is re-checked against the *resolved* base
561
- (symlinks included) immediately before the command is spawned.
557
+ Optional: a non-empty string, **relative or absolute**, and free to carry
558
+ `${ref}` tokens (see [`${ref}` in argv and `cwd`](#ref-in-argv-and-cwd)). It is
559
+ the only static check; there is no containment.
560
+
561
+ An exec unit's working directory is the **root of the bundle that owns the
562
+ workflow**. akm finds that bundle again for each step, by the run's workflow
563
+ ref, so a `bundle update` that moves the bundle to a new revision directory
564
+ between two steps of a running workflow is harmless: the next step runs from
565
+ the new directory. `cwd` is joined onto that root **once**: `cwd: sub` runs in
566
+ `<bundle>/sub`, and an absolute `cwd` replaces the root. Agent and SDK units
567
+ get the same directory as their workspace, and so does a step's gate judge. If the workflow's bundle no longer
568
+ resolves, the step fails with a message naming the ref; a step with a gate is
569
+ blocked by its judge first, for `akm workflow resume`, because the judge is built
570
+ before anything is dispatched. akm never falls back to its own working
571
+ directory.
572
+
573
+ ### The exec contract
574
+
575
+ What a command can rely on, in one place. The sections below give each point in
576
+ full.
577
+
578
+ | | |
579
+ | --- | --- |
580
+ | **Working directory** | The owner root of the run's workflow, found at each step's dispatch. `cwd` joins it once and may be absolute. |
581
+ | **Argv** | Spawned directly, never shell-interpreted. A `${ref}` token in an entry is replaced at dispatch. |
582
+ | **Environment** | Starts empty: the [default allowlist](#the-childs-environment-is-an-allowlist) plus `pass_env`, then the unit's `env:` bindings, then the engine's `AKM_*` [context](#context-reaching-the-command). |
583
+ | **`${ref}` tokens** | In argv, `cwd` and env-asset values. `${secrets/name}` becomes the secret's value, any other asset ref the asset's absolute path. A ref no bundle owns fails the unit before it spawns. Prompts are not substituted. |
584
+ | **Redaction** | Only values substituted from `secrets/` refs, engine credentials and unsafe passthrough values are scrubbed from stdout, stderr, diagnostics and the journal. Configuration, such as a URL bound through an env asset, is not. |
585
+
586
+ #### What is frozen, and what is found at dispatch
587
+
588
+ The frozen plan records the identities the engine itself dereferences: the
589
+ workflow ref, each unit's `env:` refs qualified with their bundle
590
+ (`team//env/prod`), and child workflow refs, plus the argv, `cwd` and
591
+ `pass_env` names as authored. It records no directory or file location. The
592
+ bundle root, the env files, the secrets and the paths behind `${ref}` tokens are
593
+ all looked up when the unit dispatches, through the same ref lookup `akm show`
594
+ uses. Plans frozen by earlier releases (`irVersion` 5 and 6) also carry a frozen
595
+ `cwdIdentity` and the env asset's owner paths; they decode, those fields are
596
+ ignored, and such a run dispatches by ref like any other.
597
+
598
+ #### `${ref}` in argv and `cwd`
599
+
600
+ An argv entry or `cwd` can name an asset in any bundle with the same
601
+ `${[bundle//]conceptId}` token env assets use:
602
+
603
+ ```yaml
604
+ - id: status
605
+ unit:
606
+ env: [team//env/discord]
607
+ exec:
608
+ command: ["bun", "${homelab//scripts/lab-status/fetch-status.ts}"]
609
+ ```
610
+
611
+ A `secrets/` ref gives the secret's value, trimmed of surrounding whitespace,
612
+ and joins the redaction set; any other asset ref gives the asset's absolute
613
+ path. Putting a secret in argv is the author's choice (it shows up in the
614
+ process list); prefer an `env:` binding. A `${...}` body that is not an akm ref
615
+ (`${HOME}`) is left as text. A ref no bundle owns fails the unit, naming it,
616
+ before anything is spawned.
562
617
 
563
618
  ### The output rule
564
619
 
@@ -623,13 +678,12 @@ failure reason.
623
678
  | non-zero exit | `non_zero_exit` | yes |
624
679
  | exceeded `timeout` | `timeout` | yes |
625
680
  | run cancelled (`Ctrl-C`, `--timeout`, budget) | `aborted` | yes |
626
- | binary missing / working directory unusable | `spawn_failed` | yes |
681
+ | binary missing / working directory missing | `spawn_failed` | yes |
627
682
  | **stdout** capture never completed | `exec_capture_incomplete` | **no** (the command already ran) |
628
683
  | **stderr** capture never completed | — (unit succeeds; a warning says the stderr tail may be missing) | — |
629
684
  | stdout past the retention limit, **and** the unit declares `output:` | `exec_output_limit` | **no** (deterministic) |
630
685
  | stdout past the retention limit, no `output:` schema | — (unit succeeds; artifact marked truncated) | — |
631
686
  | `AKM_*` context too large for **this platform** to spawn | `exec_context_too_large` | **no** (an authoring/data problem) |
632
- | `cwd` resolved outside its base | `exec_cwd_escape` | **no** (tampering, never transient) |
633
687
 
634
688
  `non_zero_exit` in `retry.on` re-runs a command that already ran, and a
635
689
  command's exit is usually deterministic, so the retry mostly repeats the
@@ -689,9 +743,10 @@ want the whole thing, have it write to a file and print the **path**:
689
743
 
690
744
  ### Context reaching the command
691
745
 
692
- A frozen argv is never interpolated (this format has no substitution language),
693
- so data reaches an exec unit as **environment**, the argv analogue of the
694
- context blocks a model unit gets in its prompt:
746
+ A frozen argv never carries run data (this format has no substitution language
747
+ for params or step output; the only substitution is a [`${ref}` token](#ref-in-argv-and-cwd)
748
+ naming an asset), so data reaches an exec unit as **environment**, the argv
749
+ analogue of the context blocks a model unit gets in its prompt:
695
750
 
696
751
  | Variable | Value |
697
752
  | --- | --- |
@@ -773,8 +828,8 @@ an explicit error is the smaller, more predictable surface.
773
828
  An exec unit's command does **not** inherit akm's environment. The child starts
774
829
  from an **empty** environment and is built in three layers, in this order:
775
830
 
776
- 1. the **default allowlist** below, copied through from akm's own environment
777
- (plus any names the unit adds with `pass_env:`);
831
+ 1. akm's own directories and the **default allowlist** below, copied through
832
+ from akm's own environment (plus any names the unit adds with `pass_env:`);
778
833
  2. the unit's resolved **`env:` bindings**;
779
834
  3. the engine-authored **`AKM_*` context** — last, so a binding can never
780
835
  shadow it.
@@ -798,6 +853,14 @@ environment is simply absent from the child):
798
853
  when akm is running in a scheduler context (cron/launchd/Task Scheduler) that
799
854
  stripped it — the same treatment an agent-harness child gets.
800
855
 
856
+ The child also gets akm's own directories, whatever the parent found them
857
+ through: `AKM_CONFIG_DIR`, `AKM_DATA_DIR`, `AKM_CACHE_DIR` and `AKM_STATE_DIR` are
858
+ set to the directories the akm that started it resolved (from `AKM_*_DIR`,
859
+ `XDG_*_HOME` or the platform default), and `AKM_BUNDLE_DIR` is passed when that
860
+ akm had one. A step that runs `akm` therefore uses the same bundle, config, data,
861
+ cache and state, with no `pass_env:`. Naming one of these in `pass_env:` or
862
+ binding it in `env:` overrides it.
863
+
801
864
  Deliberately **not** on the list: credentials of any kind, cloud/CI variables,
802
865
  and the proxy family (`HTTP_PROXY` and friends — proxy URLs routinely embed
803
866
  credentials). Reach a required value with an exact named `env:` binding, or
@@ -818,16 +881,16 @@ a committed *value*, so it cannot carry "whatever this build agent's
818
881
  `CARGO_HOME` happens to be".
819
882
 
820
883
  Values passed through this way are **not** redacted from the command's output
821
- the way `env:` binding values are, so never list a credential here.
884
+ the way a secret bound through `env:` is, so never list a credential here.
822
885
 
823
886
  #### Binding a secret to a unit
824
887
 
825
888
  A unit's `env:` lists env assets by ref. A value inside an env asset can be a
826
- `${secret:<name>}` token, which akm replaces with a secret when the unit is
889
+ `${ref}` token, which akm replaces with the secret's value when the unit is
827
890
  dispatched. Say the bundle `team` holds `env/discord.env`:
828
891
 
829
892
  ```sh
830
- DISCORD_BOT_TOKEN=${secret:discord_bot_token}
893
+ DISCORD_BOT_TOKEN=${team//secrets/discord_bot_token}
831
894
  ```
832
895
 
833
896
  and the secret beside it, set with
@@ -843,33 +906,42 @@ env asset:
843
906
  command: ["node", "announce.mjs"]
844
907
  ```
845
908
 
846
- - `<name>` is read from the **env asset's own bundle's** `secrets/` directory.
847
- It is a path there, not a ref: `bundle//name` inside the token is **not** a
848
- bundle qualifier. `${secret:team//token}` reads `secrets/team/token` in the
849
- env asset's bundle.
909
+ - The token body is an akm ref, `[bundle//]conceptId`, resolved with the one
910
+ ref walk: `${team//secrets/discord_bot_token}` reads that secret from the
911
+ `team` bundle whichever bundle holds the env asset, and an unqualified
912
+ `${secrets/discord_bot_token}` resolves by the default bundle then
913
+ installation priority. A `secrets/...` ref gives the secret's value, trimmed
914
+ of surrounding whitespace; any other asset ref (`${scripts/x.sh}`) gives the
915
+ asset's absolute path. A `${...}` body that is not an akm ref (`${HOME}`) is
916
+ left as text.
850
917
  - It is the same token `akm env run` resolves. `akm env export` does not
851
918
  resolve it; it writes the token verbatim.
852
- - Freezing records the env ref, its key names and the secret names, never a
853
- value. Values and secrets are read at dispatch. A missing secret fails the
854
- step at dispatch ("frozen environment preflight failed") and nothing is
855
- injected.
919
+ - Freezing records the env ref, never a value. At each dispatch the unit's env
920
+ assets are read live with `resolveEnvBinding`, the same function `akm env run`
921
+ uses, so an edit to the env file, or to a secret, after the run started is
922
+ picked up by the next dispatch; there is no key-set check against the frozen
923
+ plan. A ref no bundle owns fails the step at dispatch ("frozen environment
924
+ preflight failed") and nothing is injected.
856
925
  - Each dispatch that reads the env asset records an `env_access` event with the
857
- ref, key names and secret names, never values.
858
- - Every value from an env asset is treated as sensitive, secret or not, and is
859
- redacted out of stdout, stderr and diagnostics before they are journaled. A
860
- non-secret value such as a channel id would be redacted from the output too,
861
- so pass it as a param or in `command:` instead.
926
+ ref and key names, never values.
927
+ - Redaction covers secrets, not configuration. A value substituted from a
928
+ `secrets/` ref is treated as sensitive and redacted out of stdout, stderr and
929
+ diagnostics before they are journaled. The rest of an env asset, such as a URL
930
+ or a channel id, reaches the journal and the next step unredacted. A
931
+ credential typed straight into an env file is not redacted, so keep
932
+ credentials in `secrets/` and bind them with a `${secrets/name}` token.
862
933
  - `env:` needs a child process: it works on exec units and on agent and SDK
863
934
  engines. A unit on an `llm` engine that declares `env:` fails
864
935
  `env_unsupported`.
865
936
 
866
- #### Durable v4 (`irVersion: 5`) forbids `inherit_env`
937
+ #### Durable v4 (`irVersion: 7`) forbids `inherit_env`
867
938
 
868
939
  Every new workflow start freezes the durable plan v4 family's current
869
- executable format, `irVersion: 5`. It rejects `inherit_env: true` and any other
870
- request for whole-process inheritance; use exact named environment bindings
871
- and `pass_env:` instead. Both mechanisms are dispatch-significant, keep the
872
- visible environment surface bounded, and form part of the unit's input hash.
940
+ executable format (`irVersion: 7`; 5 and 6 still decode). It rejects
941
+ `inherit_env: true` and any other request for whole-process inheritance; use
942
+ exact named environment bindings and `pass_env:` instead. Both mechanisms are
943
+ dispatch-significant, keep the visible environment surface bounded, and form
944
+ part of the unit's input hash.
873
945
 
874
946
  The historical `inherit_env` spelling is unsupported.
875
947
 
@@ -941,8 +1013,9 @@ They do not widen it, and they do not narrow it:
941
1013
  the ref names, the replay hash carries only the ref names, and the resolved
942
1014
  values are collected and scrubbed out of stdout, stderr, and the failure
943
1015
  diagnostic by the same redaction contract every other dispatch uses — before
944
- anything is journaled. Never inline a secret into `command:`; argv is stored
945
- verbatim in `plan_json`.
1016
+ anything is journaled. Never inline a secret value into `command:`; argv is
1017
+ stored as authored in `plan_json`. A `${secrets/name}` token in argv is
1018
+ stored as the token and replaced only when the unit dispatches.
946
1019
  - **Read a workflow before you run it.** `exec:` makes what a workflow will run
947
1020
  explicit and auditable in one place, which is a real improvement over
948
1021
  instructing a model to "run the tests" — but a bundle you do not trust is
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "akm-cli",
3
- "version": "0.10.261012-beta",
3
+ "version": "0.10.261013-beta",
4
4
  "type": "module",
5
5
  "description": "akm (Agent Knowledge Manager) — a portable, local-first capability library for AI agents. Discover, load, share, and improve reusable skills, scripts, workflows, and knowledge across any shell-capable coding agent, including Claude Code, OpenCode, and Cursor.",
6
6
  "keywords": [
@@ -9,8 +9,7 @@
9
9
  "maxStringUtf8Bytes": 262144,
10
10
  "maxJsonDepth": 64,
11
11
  "maxJsonNodes": 10000,
12
- "canonicalRefsRequireNfc": true,
13
- "workingDirectoryRequiresWorkspaceRoot": true
12
+ "canonicalRefsRequireNfc": true
14
13
  },
15
14
  "type": "object",
16
15
  "required": ["version"],
@@ -55,8 +54,7 @@
55
54
  "working-directory": {
56
55
  "type": "string",
57
56
  "minLength": 1,
58
- "maxLength": 262144,
59
- "pattern": "^(?![\\\\/])(?![A-Za-z]:[\\\\/])(?!.*(?:^|[\\\\/])\\.\\.(?:[\\\\/]|$))(?!.*[\\\\/]{2})(?!.*[\\\\/]$)[^\\u0000]+$"
57
+ "maxLength": 262144
60
58
  },
61
59
  "schedule": {
62
60
  "oneOf": [
@@ -1,134 +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 * as p from "../../cli/clack.js";
7
- import { decideDangerousKeyInstall } from "../../core/activation-policy.js";
8
- import { ConfigError } from "../../core/errors.js";
9
- import { warn } from "../../core/warn.js";
10
- import { sanitizeString } from "../../sources/providers/provider-utils.js";
11
- import { scanEnvKeyNames } from "../env/env.js";
12
- import { isDangerousEnvKey } from "../lint/env-key-rules.js";
13
- let scannerOverride;
14
- /** TEST-ONLY. Inject a scanner fault at the audit boundary; undefined restores. */
15
- export function _setDangerousKeyScannerForTests(scanner) {
16
- scannerOverride = scanner;
17
- }
18
- function collectEnvFilePathsRecursive(rootDir) {
19
- const results = [];
20
- const walk = (dir) => {
21
- const entries = fs.readdirSync(dir, { withFileTypes: true });
22
- for (const entry of entries) {
23
- const full = path.join(dir, entry.name);
24
- if (entry.isDirectory()) {
25
- walk(full);
26
- continue;
27
- }
28
- if (!entry.isFile())
29
- continue;
30
- if (entry.name !== ".env" && !entry.name.endsWith(".env"))
31
- continue;
32
- results.push(full);
33
- }
34
- };
35
- walk(rootDir);
36
- return results;
37
- }
38
- function collectDangerousKeyFindings(installedStashRoot, checkEnvForDangerousKeys) {
39
- const allFindings = [];
40
- if (!fs.statSync(installedStashRoot).isDirectory()) {
41
- throw new Error(`Expected ${installedStashRoot} to be a directory.`);
42
- }
43
- const dir = path.join(installedStashRoot, "env");
44
- try {
45
- if (!fs.statSync(dir).isDirectory()) {
46
- throw new Error(`Expected ${dir} to be a directory.`);
47
- }
48
- }
49
- catch (error) {
50
- if (error.code === "ENOENT")
51
- return allFindings;
52
- throw error;
53
- }
54
- for (const envPath of collectEnvFilePathsRecursive(dir)) {
55
- const relToEnvDir = path.relative(dir, envPath);
56
- const segments = relToEnvDir.split(path.sep);
57
- const lastSegment = segments[segments.length - 1] ?? "";
58
- const lastSegmentBase = lastSegment.endsWith(".env") ? lastSegment.slice(0, -".env".length) : lastSegment;
59
- const refSegments = [...segments.slice(0, -1), lastSegmentBase === "" ? "default" : lastSegmentBase];
60
- const envRef = `env/${refSegments.join("/")}`;
61
- const relPath = path.join("env", relToEnvDir);
62
- for (const finding of checkEnvForDangerousKeys(envPath, relPath, envRef)) {
63
- const keyMatch = finding.detail.match(/Env key `([^`]+)`/);
64
- allFindings.push({ envRef, keyName: keyMatch?.[1] ?? finding.file, relPath });
65
- }
66
- }
67
- return allFindings;
68
- }
69
- function scanStashForDangerousKeys(installedStashRoot) {
70
- const scanner = scannerOverride ??
71
- ((envPath, relPath) => scanEnvKeyNames(fs.readFileSync(envPath, "utf8"))
72
- .filter(isDangerousEnvKey)
73
- .map((key) => ({ file: relPath, detail: `Env key \`${key}\`` })));
74
- return collectDangerousKeyFindings(installedStashRoot, scanner);
75
- }
76
- export async function auditStashForDangerousKeys(opts) {
77
- let allFindings;
78
- try {
79
- allFindings = scanStashForDangerousKeys(opts.stashRoot);
80
- }
81
- catch (error) {
82
- const rollbackWarning = await opts.rollback?.();
83
- throw new ConfigError(`Dangerous environment-key audit failed for "${opts.ref}"; refusing to ${opts.operation} unverified content. ${error instanceof Error ? error.message : String(error)}${rollbackWarning ? ` ${rollbackWarning}` : ""}`, "DANGEROUS_ENV_AUDIT_FAILED");
84
- }
85
- const stance = decideDangerousKeyInstall({
86
- findingsPresent: allFindings.length > 0,
87
- allowDangerousEnvKeys: opts.allowDangerousKeys,
88
- });
89
- if (stance === "allow")
90
- return { blocked: false, findings: allFindings };
91
- if (stance === "warn-allow") {
92
- for (const finding of allFindings) {
93
- warn(`[dangerous-env-key] ${finding.relPath}: key \`${finding.keyName}\` in ${finding.envRef} can hijack process execution via \`akm env run\`. Proceeding because --allow-dangerous-env-keys was set.`);
94
- }
95
- return { blocked: false, findings: allFindings };
96
- }
97
- const operationTitle = opts.operation === "install" ? "Install" : "Update";
98
- const operationPast = opts.operation === "install" ? "installed" : "updated";
99
- let error;
100
- if (opts.isTTY) {
101
- const groupedByEnv = new Map();
102
- for (const finding of allFindings) {
103
- const existing = groupedByEnv.get(finding.envRef) ?? [];
104
- existing.push(finding.keyName);
105
- groupedByEnv.set(finding.envRef, existing);
106
- }
107
- for (const [envRef, keys] of groupedByEnv) {
108
- warn(`[warn] Env "${sanitizeString(envRef)}" in stash "${sanitizeString(opts.ref)}" contains potentially dangerous keys:`);
109
- for (const key of keys)
110
- warn(` - ${sanitizeString(key)}: can hijack process execution via \`akm env run\``);
111
- }
112
- const confirmed = await p.confirm({ message: `${operationTitle} anyway?`, initialValue: false });
113
- if (!p.isCancel(confirmed) && confirmed === true)
114
- return { blocked: false, findings: allFindings };
115
- error = `${operationTitle} aborted: stash contains dangerous env keys. Remove the keys or re-run with --allow-dangerous-env-keys to bypass.`;
116
- }
117
- else {
118
- const keyList = allFindings.map((finding) => ` - ${finding.keyName} (${finding.envRef})`).join("\n");
119
- error = `${operationTitle} blocked: stash "${opts.ref}" contains dangerous env keys that can hijack process execution via \`akm env run\`:\n${keyList}\nRe-run with --allow-dangerous-env-keys to bypass this check after reviewing the env file.`;
120
- }
121
- const rollbackWarning = await opts.rollback?.();
122
- if (opts.renderBlockedError !== false) {
123
- console.error(JSON.stringify({
124
- ok: false,
125
- error,
126
- code: "DANGEROUS_ENV_KEY",
127
- ...(rollbackWarning ? { rollbackWarning } : {}),
128
- }, null, 2));
129
- }
130
- if (rollbackWarning) {
131
- warn(`The blocked ${opts.operation} could not be fully rolled back after content was ${operationPast}: ${rollbackWarning}`);
132
- }
133
- return { blocked: true, exitCode: 1, code: "DANGEROUS_ENV_KEY", error, findings: allFindings };
134
- }