akm-cli 0.9.11 → 0.9.12
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 +166 -0
- package/STABILITY.md +6 -1
- package/dist/assets/hints/cli-hints-full.md +1 -1
- package/dist/assets/improve-strategies/consolidate.json +1 -1
- package/dist/assets/improve-strategies/default.json +1 -1
- package/dist/assets/improve-strategies/thorough.json +1 -2
- package/dist/cli/shared.js +16 -4
- package/dist/cli.js +15 -13
- package/dist/commands/agent/agent-dispatch.js +8 -0
- package/dist/commands/command/execution-source-loader.js +25 -22
- package/dist/commands/command/portable-template.js +4 -26
- package/dist/commands/config-cli.js +10 -4
- package/dist/commands/env/env-binding.js +10 -3
- package/dist/commands/env/env-cli.js +7 -0
- package/dist/commands/env/secret-cli.js +15 -4
- package/dist/commands/health/checks.js +186 -71
- package/dist/commands/health.js +16 -4
- package/dist/commands/improve/distill/quality-gate.js +2 -2
- package/dist/commands/improve/distill.js +28 -12
- package/dist/commands/improve/execution.js +1 -2
- package/dist/commands/improve/extract.js +82 -56
- package/dist/commands/improve/improve-strategies.js +26 -8
- package/dist/commands/improve/improve.js +13 -0
- package/dist/commands/improve/preparation.js +9 -6
- package/dist/commands/improve/reflect.js +61 -77
- package/dist/commands/lint/index.js +3 -1
- package/dist/commands/migrate-cli.js +6 -4
- package/dist/commands/proposal/drain-policies.js +22 -2
- package/dist/commands/proposal/repository.js +4 -4
- package/dist/commands/proposal/validators/proposal-quality-validators.js +23 -2
- package/dist/commands/proposal/validators/proposals.js +10 -19
- package/dist/commands/read/show.js +42 -31
- package/dist/commands/registry-cli.js +4 -2
- package/dist/commands/sources/init.js +4 -8
- package/dist/commands/sources/self-update.js +2 -2
- package/dist/commands/sources/source-clone.js +5 -7
- package/dist/commands/sources/sources-cli.js +3 -5
- package/dist/commands/tasks/tasks-cli.js +4 -12
- package/dist/commands/tasks/tasks.js +38 -35
- package/dist/commands/workflow-cli.js +17 -15
- package/dist/core/activation-policy.js +31 -3
- package/dist/core/adapter/execution-source.js +39 -11
- package/dist/core/asset/stash-meta.js +7 -41
- package/dist/core/common.js +8 -17
- package/dist/core/config/config-schema.js +3 -23
- package/dist/core/config/config-walker.js +50 -4
- package/dist/core/config/config.js +21 -5
- package/dist/core/config/legacy-source-shape-shim.js +79 -0
- package/dist/core/config/schema/embedding.js +2 -2
- package/dist/core/config/schema/engines.js +2 -2
- package/dist/core/config/schema/index-config.js +19 -21
- package/dist/core/config/schema/primitives.js +21 -10
- package/dist/core/config/schema/sources-bundles.js +1 -6
- package/dist/core/errors.js +2 -3
- package/dist/core/improve-types.js +17 -0
- package/dist/core/json-schema.js +1 -11
- package/dist/core/maintenance-barrier.js +17 -2
- package/dist/core/paths.js +12 -15
- package/dist/core/state/migrations.js +28 -0
- package/dist/core/state-db.js +28 -1
- package/dist/core/write-source.js +6 -6
- package/dist/indexer/bundle-identity-guard.js +3 -0
- package/dist/indexer/ensure-index.js +5 -0
- package/dist/indexer/indexer.js +11 -3
- package/dist/indexer/lookup/adapter-concept-owner.js +14 -3
- package/dist/indexer/passes/metadata.js +16 -5
- package/dist/indexer/search/search-fields.js +1 -30
- package/dist/integrations/agent/engine-resolution.js +15 -1
- package/dist/integrations/agent/model-map.js +16 -10
- package/dist/integrations/agent/prompts.js +13 -6
- package/dist/integrations/lockfile.js +22 -7
- package/dist/llm/client.js +16 -0
- package/dist/llm/index-passes.js +3 -2
- package/dist/output/shapes/passthrough.js +9 -3
- package/dist/output/shapes.js +5 -3
- package/dist/output/text/workflow-format.js +8 -1
- package/dist/scripts/akm-migrate-node.js +1461 -1267
- package/dist/scripts/akm-migrate.js +1460 -1266
- package/dist/setup/setup.js +14 -21
- package/dist/sources/include.js +150 -20
- package/dist/sources/providers/git-install.js +14 -12
- package/dist/sources/providers/git-provider.js +3 -3
- package/dist/sources/snapshot-fetchers/website-ingest.js +54 -16
- package/dist/sources/website-url.js +12 -4
- package/dist/storage/engines/sqlite-migrations.js +40 -10
- package/dist/storage/like-pattern.js +7 -0
- package/dist/storage/repositories/extract-sessions-repository.js +23 -0
- package/dist/storage/repositories/index-connection.js +27 -10
- package/dist/storage/repositories/index-entry-schema.js +19 -2
- package/dist/storage/repositories/index-schema.js +30 -9
- package/dist/storage/repositories/proposals-repository.js +2 -1
- package/dist/storage/repositories/task-history-repository.js +14 -7
- package/dist/storage/repositories/workflow-runs-repository.js +15 -1
- package/dist/storage/sqlite-read-snapshot.js +11 -9
- package/dist/tasks/backends/cron.js +34 -5
- package/dist/tasks/backends/launchd.js +23 -26
- package/dist/tasks/backends/schtasks.js +50 -3
- package/dist/tasks/frozen-script.js +2 -0
- package/dist/tasks/prepare/prepare.js +2 -7
- package/dist/tasks/prepare/script-capture.js +38 -6
- package/dist/tasks/schedule.js +154 -13
- package/dist/tasks/source/task-source-v3-frozen.js +0 -1
- package/dist/tasks/source/task-source-v4.js +0 -1
- package/dist/workflows/exec/child-workflow.js +2 -3
- package/dist/workflows/exec/exec-unit.js +3 -4
- package/dist/workflows/exec/run-workflow.js +19 -10
- package/dist/workflows/exec/step-work.js +35 -56
- package/dist/workflows/freeze/resolve-steps.js +19 -11
- package/dist/workflows/freeze/source-freeze.js +7 -0
- package/dist/workflows/freeze/targets/child-workflow.js +12 -18
- package/dist/workflows/freeze/targets/command.js +14 -2
- package/dist/workflows/ir/environment-v4.js +4 -2
- package/dist/workflows/ir/freeze-v4.js +2 -5
- package/dist/workflows/ir/plan-hash.js +0 -3
- package/dist/workflows/ir/schema-v4.js +14 -9
- package/dist/workflows/ir/schema.js +1 -3
- package/dist/workflows/resource-limits.js +35 -48
- package/dist/workflows/runtime/plan-classifier.js +89 -41
- package/dist/workflows/runtime/run-outputs.js +1 -21
- package/dist/workflows/runtime/runs.js +76 -150
- package/dist/workflows/source-files.js +28 -54
- package/dist/workflows/source-ir/program.js +2 -2
- package/dist/workflows/source-ir/semantics.js +5 -23
- package/docs/migration/v0.9.1-to-v0.9.2.md +20 -0
- package/docs/reference/cli.md +74 -17
- package/package.json +1 -1
- package/schemas/akm-config.json +5 -10
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,172 @@ All notable changes to this project will be documented in this file.
|
|
|
4
4
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
6
6
|
|
|
7
|
+
## [0.9.12] - 2026-09-03
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- **`akm health` probes LLM engine reachability (#914).** `default-llm-engine`
|
|
12
|
+
and `configured-engines` now send one bounded `GET` to the endpoint's
|
|
13
|
+
`/models` route (3 s timeout, one probe per distinct endpoint per
|
|
14
|
+
invocation, no cross-run cache) instead of only checking that a credential is
|
|
15
|
+
present. Any HTTP response counts as reachable, so a cold local server is
|
|
16
|
+
never asked to load a model just to be checked. An unreachable default LLM engine is a hard `fail` naming the
|
|
17
|
+
connection error; an unreachable non-default engine is a `warn`. Pass
|
|
18
|
+
`--no-probe` on an offline or air-gapped host to keep the credential-only
|
|
19
|
+
verdict; the message then says reachability was not probed.
|
|
20
|
+
- **`akm proposal extract` reports the engine it resolved (#913).** The
|
|
21
|
+
envelope carries `engine` and `engineKind` (`llm`, `sdk`, or `agent`), every
|
|
22
|
+
`sessions[]` entry carries `engine`, and the `extract_sessions_seen` ledger
|
|
23
|
+
metadata records it, so "which engine did that run actually use" is one
|
|
24
|
+
field instead of config archaeology. `akm health`'s `active-improve-strategy`
|
|
25
|
+
check names the engine each improve process resolved to, which makes a
|
|
26
|
+
strategy-level `engine` pin that shadows `defaults.llmEngine` visible.
|
|
27
|
+
- **`akm workflow run <ref>` says when it resumes, and `--new` starts fresh
|
|
28
|
+
(#919).** Resolving a ref to an already-active run in the current scope is
|
|
29
|
+
unchanged, but the `workflow-run` envelope now carries `resumed: true` and
|
|
30
|
+
the text output leads with `resuming existing run <id> for <ref>; pass --new
|
|
31
|
+
to start a fresh run`. `--new` starts a second run and leaves the active one
|
|
32
|
+
untouched. `status`, `abandon`, `resume`, and `run <id>` all accept a unique
|
|
33
|
+
run-id prefix of eight or more characters.
|
|
34
|
+
|
|
35
|
+
### Changed
|
|
36
|
+
|
|
37
|
+
- **A run that skipped every session for an infrastructure reason is visible
|
|
38
|
+
in the extract envelope (#912).** `warnings[]` gains one aggregate line per
|
|
39
|
+
infrastructure skip reason (`llm_unavailable`, `read_failed`, `exception`,
|
|
40
|
+
`locked_concurrent`), for example `25 of 25 sessions skipped: llm_unavailable
|
|
41
|
+
(engine "default")`, and a `skipReasons` count map is present whenever
|
|
42
|
+
`sessionsSkipped > 0`. `ok` keeps meaning "the command ran" and the exit code
|
|
43
|
+
does not change; that meaning is now written down on the type.
|
|
44
|
+
- **`akm health`'s `session-extraction` check reads the extraction ledger
|
|
45
|
+
(#914).** It used to read only `improve_runs`, which the hook-driven
|
|
46
|
+
`akm proposal extract --session-id` never writes, so a plugin-driven machine
|
|
47
|
+
reported "not active" as `pass` forever. It now derives its verdict from the
|
|
48
|
+
last seven days of `extract_sessions_seen`: `unknown` when nothing was
|
|
49
|
+
recorded, `warn` naming the reason and engine when every session was skipped
|
|
50
|
+
or failed for an infrastructure reason (`llm_unavailable`, `read_failed`,
|
|
51
|
+
`exception`, `locked_concurrent`), otherwise `pass` with per-outcome counts.
|
|
52
|
+
- **Every passthrough success envelope carries `ok: true` (#918).**
|
|
53
|
+
`akm config set` and `akm config unset` printed the resulting config with no
|
|
54
|
+
`ok` field while their failure envelope had `ok: false`, so a caller
|
|
55
|
+
branching on `.ok` read success as failure. The shared passthrough stamp now
|
|
56
|
+
adds `ok: true` when a result has no `ok` of its own; commands whose exit
|
|
57
|
+
code grades the outcome (`task sync`, `task sync --dry-run`, `task prune`,
|
|
58
|
+
`workflow run`, `upgrade`, and the `migrate` subcommands) set `ok` and the
|
|
59
|
+
exit code from the same value, so the two cannot disagree. `--silent` still
|
|
60
|
+
prints nothing.
|
|
61
|
+
- **The unsupported-plan error names the real situation (#919).** A frozen
|
|
62
|
+
workflow plan with an `irVersion` above the current one no longer reports
|
|
63
|
+
"pre-irVersion-5 ... after the 0.9.2 upgrade"; it says the plan was probably
|
|
64
|
+
written by a newer akm. The reported `irVersion 111` did not reproduce
|
|
65
|
+
against this tree (the column is only ever written as `5` and no path
|
|
66
|
+
rewrites it on a source edit); a regression test pins that editing a
|
|
67
|
+
workflow source leaves its in-flight run executable.
|
|
68
|
+
|
|
69
|
+
### Fixed
|
|
70
|
+
|
|
71
|
+
- **`akm proposal accept` no longer rewrites your content.** Two "repairs" ran
|
|
72
|
+
before validation: one deleted every body line matching `description:` or
|
|
73
|
+
`when_to_use:`, the other deleted every `---` in a body that had
|
|
74
|
+
frontmatter. Both fired inside fenced code blocks, so any asset documenting
|
|
75
|
+
frontmatter — a note about akm, Claude Code skills, Jekyll, Hugo — was
|
|
76
|
+
silently gutted on accept, and the rewritten bytes were saved back over the
|
|
77
|
+
original in the proposals database. Nothing was printed. Both repairs are
|
|
78
|
+
gone; the truncated-description repair, which only ever rewrote a
|
|
79
|
+
frontmatter value, stays.
|
|
80
|
+
- **Prose-quality findings no longer block `proposal accept`.** A description
|
|
81
|
+
that read like a heading, an odd number of backticks, or a reflect revision
|
|
82
|
+
outside the size ratio refused the promotion and told the user to "fix the
|
|
83
|
+
proposal payload and try again" — but there is no `akm proposal edit` and
|
|
84
|
+
`accept` has no `--force`, so the only way out was hand-editing the
|
|
85
|
+
proposals database. These findings are now reported as warnings on a
|
|
86
|
+
command a human typed. Structural defects that genuinely cannot be written
|
|
87
|
+
(empty content, an unparseable ref, malformed frontmatter, a broken
|
|
88
|
+
workflow shape) still block.
|
|
89
|
+
- **akm no longer refuses to open a state database migrated by a newer akm.**
|
|
90
|
+
Two akm versions sharing one data directory is a supported deployment — a
|
|
91
|
+
bundled CLI beside a newer global install — and the old binary was bricked
|
|
92
|
+
for every command that touches `state.db`, not degraded. It protected
|
|
93
|
+
nothing: an older binary's entire migration registry is already applied, so
|
|
94
|
+
it has no pending migration to run. It now opens, warns once naming the
|
|
95
|
+
migrations it does not know, and reads and writes the tables it knows. A
|
|
96
|
+
ledger that genuinely diverges (a migration this akm has was never applied
|
|
97
|
+
and something else was applied in its place) is still refused.
|
|
98
|
+
- **`akm agent --prompt` no longer rejects prose that looks like a template.**
|
|
99
|
+
A prompt was validated as if it were a portable command template, so any
|
|
100
|
+
prompt containing `}}` from compact JSON, a `$VAR`, a `${...}`, a `$(...)`
|
|
101
|
+
shell snippet, an `@path`, or a `` !` `` was rejected with "unsupported
|
|
102
|
+
portable template construct" before the agent started. akm substitutes
|
|
103
|
+
nothing into a prompt, so it is now sent verbatim. The template language is
|
|
104
|
+
unchanged for stored command files, which are templates.
|
|
105
|
+
|
|
106
|
+
- **The 0.9.2 `claude-code` -> `claude` harness rename left the ledgers split
|
|
107
|
+
(#915).** State migration `027-extract-sessions-seen-harness-rename` moves
|
|
108
|
+
`extract_sessions_seen` and `workflow_runs.agent_harness` rows off the old
|
|
109
|
+
key, keeping a session already recorded under `claude` as the authoritative
|
|
110
|
+
row, and empties the old key space. `PERSISTED_HARNESS_IDS` in the harness
|
|
111
|
+
registry is now pinned by a test so the next rename cannot ship without a
|
|
112
|
+
migration. Scripts querying the ledger by `claude-code` will find it empty
|
|
113
|
+
after upgrade; see the 0.9.1 -> 0.9.2 migration guide.
|
|
114
|
+
|
|
115
|
+
### Changed — refusals that now degrade
|
|
116
|
+
|
|
117
|
+
A repo-wide audit reviewed every defensive refusal in the codebase against
|
|
118
|
+
three tests: has it demonstrably helped a real user, does its failure mode cost
|
|
119
|
+
less than the hazard it guards, and is the hazard already gated behind a
|
|
120
|
+
deliberate human command. Refusals that failed those tests were removed or
|
|
121
|
+
downgraded to a warning, in that order of preference. Machinery that prevents
|
|
122
|
+
data loss or corruption — atomic writes, backups, write-path validation, path
|
|
123
|
+
containment — was explicitly out of scope and is unchanged.
|
|
124
|
+
|
|
125
|
+
The user-visible effect is that akm stops aborting on conditions it can
|
|
126
|
+
survive. Highlights:
|
|
127
|
+
|
|
128
|
+
- **Config load no longer bricks every command over one bad key.** Retired
|
|
129
|
+
vocabulary (`profiles`, `llm`, `agent`, `features`, `stashes`,
|
|
130
|
+
`modelAliases`, `bindings`, top-level `writable`) warns and passes through,
|
|
131
|
+
and the pre-`bundles` `stashDir`/`sources[]`/`installed[]` shape folds into
|
|
132
|
+
the current shape in memory. An unknown `config set` key warns and stores.
|
|
133
|
+
- **A newer or unfamiliar state.db migration ledger no longer refuses to
|
|
134
|
+
open.** A ledger carrying migrations this binary does not know warns once and
|
|
135
|
+
degrades; only a genuinely inconsistent ledger still aborts. This is what let
|
|
136
|
+
a bundled older akm keep working against a newer host's data directory.
|
|
137
|
+
A `plan_ir_version` of NULL (a row predating the column) decodes normally.
|
|
138
|
+
- **Free text is no longer validated as code.** `akm agent` accepts prompts
|
|
139
|
+
containing `{{`/`}}`; inline workflow `akm/command` content and portable
|
|
140
|
+
command templates accept `$HOME`, `@file`, `${...}` and the rest as the prose
|
|
141
|
+
they are. Only `$ARGUMENTS[N]`, which merely looks like the one placeholder
|
|
142
|
+
akm expands, still warns.
|
|
143
|
+
- **Version skew stops being treated as corruption.** A newer index is left
|
|
144
|
+
alone rather than wiped, a stale indexed workflow identity falls back, and
|
|
145
|
+
frozen-plan spine drift from a formatting change between releases warns
|
|
146
|
+
instead of marking every in-flight run corrupt.
|
|
147
|
+
- **Deliberate commands stop being second-guessed.** `--since` bypasses
|
|
148
|
+
`extract`'s per-run cap the way `--force` already did; `workflow create
|
|
149
|
+
--force` no longer also demands `--reset`; scheduler writes, `setup --dir`
|
|
150
|
+
and `bundle create --dir` under transient paths warn instead of refusing.
|
|
151
|
+
- **Symlinks are followed on read paths** (task sources, `models.json`, stash
|
|
152
|
+
meta, `akm.include` entries) with realpath containment doing the actual
|
|
153
|
+
safety work, rather than being refused outright.
|
|
154
|
+
- **Blocks that had no escape hatch got one, or got removed.** `env run` gained
|
|
155
|
+
`--allow-insecure` for the third-party dangerous-key block that previously
|
|
156
|
+
had no bypass; `registry add` accepts a credentialed URL with a warning that
|
|
157
|
+
redacts the credential; promotion lint findings on `proposal accept` report
|
|
158
|
+
instead of blocking, since there is no `proposal edit` and no `accept
|
|
159
|
+
--force` to work around them.
|
|
160
|
+
- **Limits that bounded nothing real were deleted.** Workflow plan and embedded
|
|
161
|
+
child-plan byte caps, step-evidence tombstoning, search-text truncation, and
|
|
162
|
+
the JSON-schema node budget are gone; the akm.lock acquisition budget went
|
|
163
|
+
from 300 ms to ~30 s with backoff and a "waiting for another akm process"
|
|
164
|
+
notice.
|
|
165
|
+
- **A cron line too long for vixie-cron now spills into a wrapper script**
|
|
166
|
+
instead of refusing the install. The length limit stays, because a truncated
|
|
167
|
+
cron line would execute a partial command.
|
|
168
|
+
|
|
169
|
+
Guards that passed the three tests were kept and documented, including the
|
|
170
|
+
registry-URL credential inspection limit, the third-party dangerous-key block
|
|
171
|
+
itself, `MAX_SCHTASKS_TRIGGERS`, and every path-containment check.
|
|
172
|
+
|
|
7
173
|
## [0.9.11] - 2026-09-03
|
|
8
174
|
|
|
9
175
|
### Added
|
package/STABILITY.md
CHANGED
|
@@ -219,6 +219,10 @@ enumeration of the whole `proposal` noun group.
|
|
|
219
219
|
| `4` | Health warning (`akm health` only) |
|
|
220
220
|
| `70` | Internal / unclassified |
|
|
221
221
|
| `78` | Configuration error |
|
|
222
|
+
|
|
223
|
+
**From 0.9.12**, every success envelope produced by the passthrough stamp
|
|
224
|
+
(`config`, `clone`, `models`, `task-*`, `workflow-*`, `registry-*`, …) also
|
|
225
|
+
carries `ok: true`; a command that already computes its own `ok` keeps it.
|
|
222
226
|
- **Install scripts** — `install.sh` and `install.ps1` URLs; the `--prefix`
|
|
223
227
|
/ `AKM_INSTALL_DIR` environment override.
|
|
224
228
|
- **Runtime** — the npm package requires Node.js >= 22 as its bootstrap and
|
|
@@ -439,7 +443,6 @@ on them.
|
|
|
439
443
|
| `AKM_SQLITE_JOURNAL_MODE` | SQLite journal mode (network filesystems) |
|
|
440
444
|
| `AKM_BIN` | Absolute `akm` path for scheduler registration |
|
|
441
445
|
| `AKM_INSTALL_DIR` | Install-script prefix |
|
|
442
|
-
| `AKM_FORCE_SETUP_TMP_STASH` | Documented escape hatch for intentional temp-directory bundles |
|
|
443
446
|
| `AKM_UPGRADE_SKIP_CHECKSUM` | Recovery hatch for a broken upgrade checksum |
|
|
444
447
|
|
|
445
448
|
**Internal** — no compatibility guarantee, may vanish without notice:
|
|
@@ -481,6 +484,8 @@ lives in this repo). **D3** shipped too, in the end: `akm mv` was removed in
|
|
|
481
484
|
0.9.0 (see the Renames bullet above), with `scripts/rekey-asset-ref.ts` as the
|
|
482
485
|
Internal replacement for the one capability nothing else covered.
|
|
483
486
|
|
|
487
|
+
- **0.10 — `config set`/`config unset` may drop the config dump** in favor of
|
|
488
|
+
a compact `{ok, shape, key}` result; `akm config list` remains the full read.
|
|
484
489
|
- **0.10 — migration extraction.** The migration machinery leaves the CLI for
|
|
485
490
|
a separately published `akm-migrate` package (see Internal above).
|
|
486
491
|
- **0.10 — `--auto-accept` hard error.** It is currently accepted-and-warned;
|
|
@@ -414,7 +414,7 @@ Result-envelope commands accept `--format`, `--detail`, and `--shape` flags:
|
|
|
414
414
|
- `--detail full` — includes scores, paths, timing, debug info
|
|
415
415
|
- `--shape human` (default) — standard projection
|
|
416
416
|
- `--shape agent` — agent-optimized output: strips non-actionable fields
|
|
417
|
-
- `--shape summary` — metadata only (no content/template/prompt), under 200 tokens; only
|
|
417
|
+
- `--shape summary` — metadata only (no content/template/prompt), under 200 tokens; only `akm show` has a dedicated summary projection — elsewhere it falls back to `agent` with a warning
|
|
418
418
|
|
|
419
419
|
Run `akm help <command>` or `akm <command> -h` for per-command help. Run
|
|
420
420
|
`akm --help` for the sectioned command overview.
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"processes": {
|
|
4
4
|
"reflect": { "enabled": false },
|
|
5
5
|
"distill": { "enabled": false },
|
|
6
|
-
"consolidate": { "enabled": true, "allowedTypes": ["memory"], "maxChunkSize": 25
|
|
6
|
+
"consolidate": { "enabled": true, "allowedTypes": ["memory"], "maxChunkSize": 25 },
|
|
7
7
|
"memoryInference": { "enabled": false },
|
|
8
8
|
"graphExtraction": { "enabled": false },
|
|
9
9
|
"extract": { "enabled": false },
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
"allowedTypes": ["agent", "command", "knowledge", "lesson", "memory", "skill", "workflow"]
|
|
8
8
|
},
|
|
9
9
|
"distill": { "enabled": true, "allowedTypes": ["memory"], "requirePlannedRefs": true },
|
|
10
|
-
"consolidate": { "enabled": true, "allowedTypes": ["memory"]
|
|
10
|
+
"consolidate": { "enabled": true, "allowedTypes": ["memory"] },
|
|
11
11
|
"memoryInference": { "enabled": true },
|
|
12
12
|
"graphExtraction": { "enabled": true },
|
|
13
13
|
"extract": { "enabled": false, "triage": { "enabled": true, "minScore": 2 } },
|
package/dist/cli/shared.js
CHANGED
|
@@ -150,13 +150,14 @@ export const GLOBAL_OUTPUT_ARGS = {
|
|
|
150
150
|
// R-050(c): single-sourced with the root command's own `--shape` help
|
|
151
151
|
// (`main.args.shape` in src/cli.ts, which spreads this object) so the
|
|
152
152
|
// caveat is visible from every leaf's own `--help`, not only the top-level
|
|
153
|
-
// one. `summary` outside `show`
|
|
154
|
-
//
|
|
155
|
-
//
|
|
153
|
+
// one. `summary` outside `show` falls back to `agent` with a warning
|
|
154
|
+
// (shapeForCommand in src/output/shapes.ts) — `--shape` is a global flag,
|
|
155
|
+
// so a script that passes it to a mixed batch of commands still works.
|
|
156
156
|
shape: {
|
|
157
157
|
type: "string",
|
|
158
158
|
description: "Output projection: human|agent|summary (global flag). 'agent' trims to agent-essential fields; " +
|
|
159
|
-
"'summary'
|
|
159
|
+
"'summary' only has a dedicated projection on 'akm show' — elsewhere it falls back to 'agent' with a " +
|
|
160
|
+
"warning. Default: human.",
|
|
160
161
|
},
|
|
161
162
|
output: {
|
|
162
163
|
type: "string",
|
|
@@ -269,6 +270,17 @@ export function defineGroupCommand(def) {
|
|
|
269
270
|
* document is written to that file instead of stdout (jsonl excepted — it is
|
|
270
271
|
* a line-streaming protocol and always goes to stdout).
|
|
271
272
|
*/
|
|
273
|
+
/**
|
|
274
|
+
* Emit a result whose success is graded by an exit code. `ok` on the envelope
|
|
275
|
+
* and `process.exitCode` are set from the same value here, so the two cannot
|
|
276
|
+
* disagree (#918); `exitCode` undefined or 0 means success.
|
|
277
|
+
*/
|
|
278
|
+
export function outputWithExitCode(command, result, exitCode) {
|
|
279
|
+
const failed = exitCode !== undefined && exitCode !== EXIT_CODES.SUCCESS;
|
|
280
|
+
output(command, { ...result, ok: !failed });
|
|
281
|
+
if (failed)
|
|
282
|
+
process.exitCode = exitCode;
|
|
283
|
+
}
|
|
272
284
|
export function output(command, result) {
|
|
273
285
|
const mode = getOutputMode();
|
|
274
286
|
const shaped = shapeForCommand(command, result, mode.detail, mode.shape);
|
package/dist/cli.js
CHANGED
|
@@ -57,7 +57,7 @@ process.on("uncaughtException", (err) => {
|
|
|
57
57
|
});
|
|
58
58
|
import fs from "node:fs";
|
|
59
59
|
import { defineCommand, parseArgs, renderUsage, runCommand, showUsage } from "citty";
|
|
60
|
-
import {
|
|
60
|
+
import { findCittyTopLevelCommandIndex, getParsedInvocation, parseAllFlagValues, resolveHelpMigrateVersionArg, setParsedInvocation, } from "./cli/invocation.js";
|
|
61
61
|
import { retiredCommandHint } from "./cli/retired-commands.js";
|
|
62
62
|
import { defineGroupCommand, EXIT_CODES, emitJsonError, GLOBAL_OUTPUT_ARGS, output, runWithJsonErrors, } from "./cli/shared.js";
|
|
63
63
|
import { assertKnownFlags, closestMatch } from "./cli/unknown-flags.js";
|
|
@@ -284,6 +284,18 @@ const healthCommand = defineCommand({
|
|
|
284
284
|
description: "Fetch the full report dataset: per-run rows, trend deltas vs the prior window, and the pending proposal queue. Renders as the rich report under --format md/html and as complete data under any other format.",
|
|
285
285
|
default: false,
|
|
286
286
|
},
|
|
287
|
+
// #914: citty strips any `--no-X` argument and treats it as negating `X`
|
|
288
|
+
// (see `parseArgs` in citty's dist), regardless of whether an arg literally
|
|
289
|
+
// named "no-X" is declared — declaring "no-probe" directly would silently
|
|
290
|
+
// never populate `args["no-probe"]` and the flag would do nothing. Declare
|
|
291
|
+
// the positive flag instead; `--no-probe` is citty's automatic negation of
|
|
292
|
+
// it (rendered in `--help` via `negativeDescription`).
|
|
293
|
+
probe: {
|
|
294
|
+
type: "boolean",
|
|
295
|
+
default: true,
|
|
296
|
+
description: "Probe default-llm-engine / configured-engines reachability (on by default).",
|
|
297
|
+
negativeDescription: "Skip the reachability probes (for an offline or air-gapped host).",
|
|
298
|
+
},
|
|
287
299
|
},
|
|
288
300
|
async run({ args }) {
|
|
289
301
|
let resultStatus;
|
|
@@ -316,11 +328,12 @@ const healthCommand = defineCommand({
|
|
|
316
328
|
const sinceIsDuration = args.since !== undefined && parseDuration(args.since, DURATION_UNITS) !== null;
|
|
317
329
|
const implicitCompare = explicitWindows ? undefined : ((sinceIsDuration ? args.since : undefined) ?? "24h");
|
|
318
330
|
const windowCompare = report ? (args["window-compare"] ?? implicitCompare) : args["window-compare"];
|
|
319
|
-
const base = akmHealth({
|
|
331
|
+
const base = await akmHealth({
|
|
320
332
|
since: args.since,
|
|
321
333
|
groupBy: report ? "run" : groupBy,
|
|
322
334
|
windowCompare,
|
|
323
335
|
windows,
|
|
336
|
+
probe: args.probe !== false,
|
|
324
337
|
});
|
|
325
338
|
const reportCompare = windowCompare ??
|
|
326
339
|
(explicitWindows
|
|
@@ -945,18 +958,7 @@ async function runCli() {
|
|
|
945
958
|
emitJsonError(error);
|
|
946
959
|
return;
|
|
947
960
|
}
|
|
948
|
-
// `--shape summary` is only meaningful on `akm show`. Reject it up front for
|
|
949
|
-
// every other command so a write command (e.g. `akm proposal accept …`)
|
|
950
|
-
// fails fast BEFORE performing its mutation, rather than throwing at
|
|
951
|
-
// output-shaping time after the side effect has already happened. The
|
|
952
|
-
// shape-registry gate in shapeForCommand() remains as defense-in-depth (and
|
|
953
|
-
// covers the in-process test harness, which skips this startup block).
|
|
954
961
|
const commandPath = resolveCittyCommandPath(main, process.argv.slice(2));
|
|
955
|
-
const topLevelCommand = commandPath[0] ?? findCittyTopLevelCommand(process.argv.slice(2), MAIN_TOP_LEVEL_ARGS);
|
|
956
|
-
if (getOutputMode().shape === "summary" && topLevelCommand !== "show") {
|
|
957
|
-
emitJsonError(new UsageError("'--shape summary' is only valid on 'akm show'.", "INVALID_SHAPE_VALUE"));
|
|
958
|
-
return;
|
|
959
|
-
}
|
|
960
962
|
// D7 — every command that renders through output() honours all six --format
|
|
961
963
|
// values. The declared exempt set (src/output/format-exempt.ts) does not
|
|
962
964
|
// render an envelope at all, so warn rather than pretend: silently ignoring
|
|
@@ -32,10 +32,18 @@ function rejectInvalidAgentRef(agentRef) {
|
|
|
32
32
|
return;
|
|
33
33
|
throw new UsageError(`agent expects an agent asset ref under agents/...; received ${JSON.stringify(agentRef)}.`, "INVALID_FLAG_VALUE");
|
|
34
34
|
}
|
|
35
|
+
/**
|
|
36
|
+
* Dispatch a `--prompt` / `--prompt-stdin` task through the canonical command
|
|
37
|
+
* path. The prompt is a person's free text, not a template: it is sent to the
|
|
38
|
+
* agent verbatim (`inlineContentMode: "literal"`), so prose containing `}}`
|
|
39
|
+
* from compact JSON, a `$VAR`, a shell snippet, or an `@path` reaches the
|
|
40
|
+
* agent instead of being rejected as an unsupported template construct.
|
|
41
|
+
*/
|
|
35
42
|
async function delegateCanonicalCommand(options, seams, action) {
|
|
36
43
|
const execute = seams.executeCommand ?? executeCommandInvocation;
|
|
37
44
|
const result = await execute({
|
|
38
45
|
action,
|
|
46
|
+
inlineContentMode: "literal",
|
|
39
47
|
config: options.agentConfig,
|
|
40
48
|
current: canonicalCurrent(options),
|
|
41
49
|
});
|
|
@@ -10,6 +10,7 @@ import { isWithin } from "../../core/common.js";
|
|
|
10
10
|
import { loadConfig } from "../../core/config/config.js";
|
|
11
11
|
import { bundleComponentConfig, bundlesToSourceEntries } from "../../core/config/config-sources.js";
|
|
12
12
|
import { ConfigError, NotFoundError, UsageError } from "../../core/errors.js";
|
|
13
|
+
import { warnOnce } from "../../core/warn.js";
|
|
13
14
|
import { lookupBundleRef } from "../../indexer/indexer.js";
|
|
14
15
|
import { deriveInstallations } from "../../indexer/installations.js";
|
|
15
16
|
import { resolveEntryContentDir, resolveSourceEntries } from "../../indexer/search/search-source.js";
|
|
@@ -66,10 +67,10 @@ function implicitComponentForEntry(entry, config, realRoot) {
|
|
|
66
67
|
continue;
|
|
67
68
|
const canonicalRoot = realDirectory(component.root, `Canonical implicit source root for ${entry.bundleId}`);
|
|
68
69
|
if (canonicalRoot !== realRoot) {
|
|
69
|
-
|
|
70
|
+
warnOnce(`execution-source-root-drift:${entry.itemRef}`, `[command] Indexed root for ${JSON.stringify(entry.itemRef)} no longer matches the working bundle source; re-resolving from live config. Run \`akm index --full\` to refresh it.`);
|
|
70
71
|
}
|
|
71
72
|
if (component.adapter !== entry.adapterId) {
|
|
72
|
-
|
|
73
|
+
warnOnce(`execution-source-adapter-drift:${entry.itemRef}`, `[command] Indexed adapter ${JSON.stringify(entry.adapterId)} for ${JSON.stringify(entry.itemRef)} no longer matches the working source's adapter ${JSON.stringify(component.adapter)}; re-resolving from live config. Run \`akm index --full\` to refresh it.`);
|
|
73
74
|
}
|
|
74
75
|
return Object.freeze({
|
|
75
76
|
id: installation.id,
|
|
@@ -91,31 +92,33 @@ function componentForEntry(entry, config, realRoot) {
|
|
|
91
92
|
const component = bundleComponentConfig(configured);
|
|
92
93
|
const configuredAdapter = component?.adapter;
|
|
93
94
|
if (configuredAdapter && configuredAdapter !== entry.adapterId) {
|
|
94
|
-
|
|
95
|
+
warnOnce(`execution-source-adapter-drift:${entry.itemRef}`, `[command] Indexed adapter ${JSON.stringify(entry.adapterId)} for ${JSON.stringify(entry.itemRef)} no longer matches the bundle config's adapter ${JSON.stringify(configuredAdapter)}; re-resolving from live config. Run \`akm index --full\` to refresh it.`);
|
|
95
96
|
}
|
|
96
97
|
const configuredSource = bundlesToSourceEntries(config)?.find((source) => source.name === entry.bundleId);
|
|
97
98
|
const configuredContentRoot = configuredSource ? resolveEntryContentDir(configuredSource) : undefined;
|
|
98
99
|
if (!configuredSource || !configuredContentRoot) {
|
|
99
|
-
|
|
100
|
+
warnOnce(`execution-source-unresolved:${entry.itemRef}`, `[command] Configured source for ${JSON.stringify(entry.itemRef)} no longer resolves to materialized content; dispatching from the indexed location instead. Run \`akm index --full\` after restoring or updating the bundle.`);
|
|
100
101
|
}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
102
|
+
else {
|
|
103
|
+
const lexicalSourceRoot = path.resolve(configuredContentRoot);
|
|
104
|
+
const lexicalConfiguredRoot = path.resolve(configuredContentRoot, component?.root ?? ".");
|
|
105
|
+
if (!isLexicallyWithin(lexicalConfiguredRoot, lexicalSourceRoot)) {
|
|
106
|
+
throw new ConfigError(`Configured component root for ${JSON.stringify(entry.itemRef)} resolves outside its materialized bundle source.`, "INVALID_CONFIG_FILE", "Keep component roots inside their owning bundle source, then run `akm index --full`.");
|
|
107
|
+
}
|
|
108
|
+
const configuredSourceRoot = realDirectory(lexicalSourceRoot, `Configured source root for ${entry.bundleId}`);
|
|
109
|
+
let configuredRoot;
|
|
110
|
+
try {
|
|
111
|
+
configuredRoot = realDirectory(lexicalConfiguredRoot, `Configured component root for ${entry.bundleId}`);
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
throw new ConfigError(`Configured ${JSON.stringify(configuredSource.type)} source for ${JSON.stringify(entry.itemRef)} is not materialized at its current component root.`, "INVALID_CONFIG_FILE", "Restore or update the bundle, then run `akm index --full` before dispatching this asset.");
|
|
115
|
+
}
|
|
116
|
+
if (!isWithin(configuredRoot, configuredSourceRoot)) {
|
|
117
|
+
throw new ConfigError(`Configured component root for ${JSON.stringify(entry.itemRef)} resolves outside its materialized bundle source.`, "INVALID_CONFIG_FILE", "Remove the escaping symlink or component path, then run `akm index --full`.");
|
|
118
|
+
}
|
|
119
|
+
if (configuredRoot !== realRoot) {
|
|
120
|
+
warnOnce(`execution-source-root-drift:${entry.itemRef}`, `[command] Indexed root for ${JSON.stringify(entry.itemRef)} no longer matches the ${JSON.stringify(configuredSource.type)} bundle source; re-resolving from live config. Run \`akm index --full\` to refresh it.`);
|
|
121
|
+
}
|
|
119
122
|
}
|
|
120
123
|
const writable = component?.writable ?? configured.writable ?? configured.path !== undefined;
|
|
121
124
|
return Object.freeze({
|
|
@@ -1,22 +1,9 @@
|
|
|
1
1
|
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
2
|
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
3
|
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
|
-
import {
|
|
4
|
+
import { warn } from "../../core/warn.js";
|
|
5
5
|
export const PORTABLE_ARGUMENTS_PLACEHOLDER = "$ARGUMENTS";
|
|
6
|
-
const
|
|
7
|
-
{ label: "$ARGUMENTS[N]", pattern: /\$ARGUMENTS\s*\[/u },
|
|
8
|
-
{ label: "```! ... ```", pattern: /(?:^|\r?\n)[\t ]*```!/u },
|
|
9
|
-
{ label: "!`...`", pattern: /!`/u },
|
|
10
|
-
{ label: "@file", pattern: /(?<![A-Za-z0-9._%+-])@(?:\.{0,2}\/)?[A-Za-z0-9_.-]+(?:\/[A-Za-z0-9_.-]+)*/u },
|
|
11
|
-
{ label: "$" + "{...}", pattern: /\$\{/u },
|
|
12
|
-
{ label: "$(...)", pattern: /\$\(/u },
|
|
13
|
-
{ label: "$N", pattern: /\$\d/u },
|
|
14
|
-
{ label: "$NAME", pattern: /\$[A-Za-z_][A-Za-z0-9_]*/u },
|
|
15
|
-
{ label: "{{...}}", pattern: /\{\{|\}\}/u },
|
|
16
|
-
]);
|
|
17
|
-
function unsupportedTemplate(source, label) {
|
|
18
|
-
return new UsageError(`Command ${JSON.stringify(source)} uses unsupported portable template construct ${label}.`, "INVALID_FLAG_VALUE", `AKM command execution supports only the literal ${PORTABLE_ARGUMENTS_PLACEHOLDER} placeholder. Invoke native-only templates through their owning tool instead.`);
|
|
19
|
-
}
|
|
6
|
+
const INDEXED_ARGUMENTS_PATTERN = /\$ARGUMENTS\s*\[/u;
|
|
20
7
|
/**
|
|
21
8
|
* Validate the deliberately small portable command-template language.
|
|
22
9
|
*
|
|
@@ -28,17 +15,8 @@ export function validatePortableCommandTemplate(template, source) {
|
|
|
28
15
|
throw new TypeError("command template must be a string");
|
|
29
16
|
if (typeof source !== "string" || source.length === 0)
|
|
30
17
|
throw new TypeError("command source must be a string");
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
const indexedArguments = UNSUPPORTED_TEMPLATE_CONSTRUCTS[0];
|
|
34
|
-
if (indexedArguments?.pattern.test(template))
|
|
35
|
-
throw unsupportedTemplate(source, indexedArguments.label);
|
|
36
|
-
// Mask only the exact portable token. Everything else remains visible to the
|
|
37
|
-
// unsupported-construct detectors, including `$ARGUMENTS_SUFFIX`.
|
|
38
|
-
const portableMasked = template.replace(/\$ARGUMENTS(?![A-Za-z0-9_])/gu, "");
|
|
39
|
-
for (const construct of UNSUPPORTED_TEMPLATE_CONSTRUCTS.slice(1)) {
|
|
40
|
-
if (construct.pattern.test(portableMasked))
|
|
41
|
-
throw unsupportedTemplate(source, construct.label);
|
|
18
|
+
if (INDEXED_ARGUMENTS_PATTERN.test(template)) {
|
|
19
|
+
warn(`Command ${JSON.stringify(source)} uses "$ARGUMENTS[N]", which akm does not support. Only the literal $ARGUMENTS placeholder is expanded; the indexed form is left as-is.`);
|
|
42
20
|
}
|
|
43
21
|
}
|
|
44
22
|
/**
|
|
@@ -111,7 +111,10 @@ export const configCommand = defineGroupCommand({
|
|
|
111
111
|
},
|
|
112
112
|
}),
|
|
113
113
|
set: defineJsonCommand({
|
|
114
|
-
meta: {
|
|
114
|
+
meta: {
|
|
115
|
+
name: "set",
|
|
116
|
+
description: "Set a configuration value by key; prints the resulting config with ok: true",
|
|
117
|
+
},
|
|
115
118
|
args: {
|
|
116
119
|
key: {
|
|
117
120
|
type: "positional",
|
|
@@ -124,7 +127,7 @@ export const configCommand = defineGroupCommand({
|
|
|
124
127
|
// writes don't pollute their host's output stream.
|
|
125
128
|
silent: {
|
|
126
129
|
type: "boolean",
|
|
127
|
-
description: "Suppress the post-write config dump on stdout. Use from hooks and CI scripts; the write still happens and errors still print.",
|
|
130
|
+
description: "Suppress the post-write config dump on stdout entirely (prints nothing; exit code is the status). Use from hooks and CI scripts; the write still happens and errors still print.",
|
|
128
131
|
default: false,
|
|
129
132
|
},
|
|
130
133
|
},
|
|
@@ -136,12 +139,15 @@ export const configCommand = defineGroupCommand({
|
|
|
136
139
|
},
|
|
137
140
|
}),
|
|
138
141
|
unset: defineJsonCommand({
|
|
139
|
-
meta: {
|
|
142
|
+
meta: {
|
|
143
|
+
name: "unset",
|
|
144
|
+
description: "Unset an optional configuration key or whole embedding/engine section; prints the resulting config with ok: true",
|
|
145
|
+
},
|
|
140
146
|
args: {
|
|
141
147
|
key: { type: "positional", required: true, description: "Config key to unset" },
|
|
142
148
|
silent: {
|
|
143
149
|
type: "boolean",
|
|
144
|
-
description: "Suppress the post-write config dump on stdout.",
|
|
150
|
+
description: "Suppress the post-write config dump on stdout entirely (prints nothing; exit code is the status).",
|
|
145
151
|
default: false,
|
|
146
152
|
},
|
|
147
153
|
},
|
|
@@ -84,13 +84,20 @@ export function resolveEnvBinding(target, options = {}) {
|
|
|
84
84
|
const dangerous = keys.filter(isDangerousEnvKey);
|
|
85
85
|
if (dangerous.length > 0) {
|
|
86
86
|
const detail = `Env "${envRef}" injects process-hijacking variable(s): ${dangerous.join(", ")}.`;
|
|
87
|
-
const decision = decideDangerousEnvInjection({
|
|
87
|
+
const decision = decideDangerousEnvInjection({
|
|
88
|
+
dangerousKeys: dangerous,
|
|
89
|
+
thirdParty: Boolean(source.registryId),
|
|
90
|
+
allowInsecure: options.allowInsecure,
|
|
91
|
+
});
|
|
88
92
|
if (decision === "block") {
|
|
89
93
|
throw new UsageError(`Refusing to inject env from a third-party stash. ${detail}\n` +
|
|
90
|
-
` Review the file, then copy the values into a first-party env if you trust them
|
|
94
|
+
` Review the file, then copy the values into a first-party env if you trust them, ` +
|
|
95
|
+
`or pass --allow-insecure once you have.`, "INVALID_FLAG_VALUE");
|
|
91
96
|
}
|
|
92
97
|
if (decision === "warn") {
|
|
93
|
-
warn(
|
|
98
|
+
warn(options.allowInsecure && source.registryId
|
|
99
|
+
? `${detail} Injecting anyway (--allow-insecure).`
|
|
100
|
+
: `${detail} Injecting anyway (first-party stash).`);
|
|
94
101
|
}
|
|
95
102
|
}
|
|
96
103
|
// Audit trail: keys only, never values.
|
|
@@ -255,6 +255,7 @@ async function runEnvInjected(target, opts) {
|
|
|
255
255
|
const { values: envValues } = resolveEnvBinding(target, {
|
|
256
256
|
only: opts.only,
|
|
257
257
|
except: opts.except,
|
|
258
|
+
allowInsecure: opts.allowInsecure,
|
|
258
259
|
});
|
|
259
260
|
const mergedEnv = buildChildEnv(process.env, {
|
|
260
261
|
clean: opts.clean === true,
|
|
@@ -323,6 +324,11 @@ const envRunCommand = defineJsonCommand({
|
|
|
323
324
|
type: "string",
|
|
324
325
|
description: "When used with --clean, also inherit these parent env vars (comma-separated). Ignored without --clean.",
|
|
325
326
|
},
|
|
327
|
+
"allow-insecure": {
|
|
328
|
+
type: "boolean",
|
|
329
|
+
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.",
|
|
330
|
+
default: false,
|
|
331
|
+
},
|
|
326
332
|
},
|
|
327
333
|
async run({ args }) {
|
|
328
334
|
await runEnvInjected(args.target, {
|
|
@@ -330,6 +336,7 @@ const envRunCommand = defineJsonCommand({
|
|
|
330
336
|
except: parseKeyListFlag(args.except),
|
|
331
337
|
clean: args.clean === true,
|
|
332
338
|
inherit: parseKeyListFlag(args.inherit) ?? [],
|
|
339
|
+
allowInsecure: args["allow-insecure"] === true,
|
|
333
340
|
});
|
|
334
341
|
},
|
|
335
342
|
});
|
|
@@ -32,11 +32,13 @@ import path from "node:path";
|
|
|
32
32
|
import { getParsedInvocation } from "../../cli/invocation.js";
|
|
33
33
|
import { getStringArg } from "../../cli/parse-args.js";
|
|
34
34
|
import { defineGroupCommand, defineJsonCommand, output } from "../../cli/shared.js";
|
|
35
|
+
import { decideDangerousEnvInjection } from "../../core/activation-policy.js";
|
|
35
36
|
import { deriveCanonicalAssetName } from "../../core/asset/asset-placement.js";
|
|
36
37
|
import { loadConfig } from "../../core/config/config.js";
|
|
37
38
|
import { makeSecretRef, resolveSecretPath, resolveSecretWriteTarget, withEnvSecretWrite, } from "../../core/env-secret-ref.js";
|
|
38
39
|
import { ConfigError, NotFoundError, UsageError } from "../../core/errors.js";
|
|
39
40
|
import { appendEvent } from "../../core/events.js";
|
|
41
|
+
import { warn } from "../../core/warn.js";
|
|
40
42
|
import { resolveSourceEntries } from "../../indexer/search/search-source.js";
|
|
41
43
|
import { readStdin } from "../../runtime.js";
|
|
42
44
|
import { buildChildEnv } from "./child-env.js";
|
|
@@ -176,10 +178,6 @@ const secretRunCommand = defineJsonCommand({
|
|
|
176
178
|
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(varName)) {
|
|
177
179
|
throw new UsageError(`"${varName}" is not a valid environment variable name.`, "INVALID_FLAG_VALUE");
|
|
178
180
|
}
|
|
179
|
-
const { isDangerousEnvKey } = await import("../lint/env-key-rules.js");
|
|
180
|
-
if (isDangerousEnvKey(varName)) {
|
|
181
|
-
throw new UsageError(`Refusing to inject a secret into "${varName}": it is a known process-hijacking variable (e.g. LD_PRELOAD, PATH).`, "INVALID_FLAG_VALUE");
|
|
182
|
-
}
|
|
183
181
|
const command = getParsedInvocation().passthroughArgs();
|
|
184
182
|
if (command.length === 0) {
|
|
185
183
|
throw new UsageError("Missing command. Usage: akm secret run <ref> <VAR> -- <command>");
|
|
@@ -188,6 +186,19 @@ const secretRunCommand = defineJsonCommand({
|
|
|
188
186
|
if (!fs.existsSync(absPath)) {
|
|
189
187
|
throw new NotFoundError(`Secret not found: ${makeSecretRef(name, source)}`);
|
|
190
188
|
}
|
|
189
|
+
const { isDangerousEnvKey } = await import("../lint/env-key-rules.js");
|
|
190
|
+
if (isDangerousEnvKey(varName)) {
|
|
191
|
+
const detail = `"${varName}" is a known process-hijacking variable (e.g. LD_PRELOAD, PATH).`;
|
|
192
|
+
const decision = decideDangerousEnvInjection({
|
|
193
|
+
dangerousKeys: [varName],
|
|
194
|
+
thirdParty: Boolean(source.registryId),
|
|
195
|
+
});
|
|
196
|
+
if (decision === "block") {
|
|
197
|
+
throw new UsageError(`Refusing to inject a secret from a third-party stash into ${detail}\n` +
|
|
198
|
+
" Review the secret, then copy it into a first-party secret if you trust it.", "INVALID_FLAG_VALUE");
|
|
199
|
+
}
|
|
200
|
+
warn(`Injecting a secret into ${detail} Injecting anyway (first-party stash).`);
|
|
201
|
+
}
|
|
191
202
|
const { readValue } = await import("./secret.js");
|
|
192
203
|
const mergedEnv = buildChildEnv(process.env, {
|
|
193
204
|
clean: args.clean === true,
|