akm-cli 0.9.15 → 0.9.16-alpha.2
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 +52 -0
- package/dist/assets/hints/cli-hints-full.md +13 -6
- package/dist/assets/tasks/improve/akm-improve-catchup.yml +3 -6
- package/dist/cli/retired-commands.js +0 -2
- package/dist/commands/env/env-binding.js +4 -4
- package/dist/commands/env/env-cli.js +3 -3
- package/dist/commands/improve/improve-cli.js +19 -14
- package/dist/commands/improve/reflect.js +23 -2
- package/dist/commands/lint/base-linter.js +9 -0
- package/dist/commands/lint/env-key-rules.js +2 -2
- package/dist/commands/proposal/propose.js +15 -1
- package/dist/commands/proposal/validators/proposal-quality-validators.js +40 -3
- package/dist/commands/proposal/validators/proposal-validators.js +5 -4
- package/dist/commands/read/search.js +33 -4
- package/dist/commands/read/show.js +21 -2
- package/dist/commands/registry-cli.js +5 -5
- package/dist/commands/sources/add-cli.js +59 -16
- package/dist/commands/sources/bundle-cli.js +35 -11
- package/dist/commands/sources/bundle-config-ops.js +30 -0
- package/dist/commands/sources/dangerous-env-audit.js +4 -4
- package/dist/commands/sources/installed-stashes.js +43 -28
- package/dist/commands/sources/source-add.js +33 -17
- package/dist/commands/sources/source-manage.js +34 -12
- package/dist/commands/sources/stash-skeleton.js +6 -3
- package/dist/commands/tasks/explain.js +4 -1
- package/dist/commands/tasks/tasks-cli.js +31 -9
- package/dist/commands/tasks/tasks.js +239 -194
- package/dist/commands/tasks/validate.js +20 -32
- package/dist/core/activation-policy.js +4 -4
- package/dist/core/adapter/adapters/akm-adapter.js +5 -0
- package/dist/core/adapter/execution-source.js +10 -29
- package/dist/core/config/config-schema.js +64 -8
- package/dist/core/config/config-sources.js +96 -2
- package/dist/core/config/config.js +190 -24
- package/dist/core/config/legacy-source-shape-shim.js +9 -0
- package/dist/core/config/schema/execution.js +23 -0
- package/dist/core/config/schema/experimental.js +1 -1
- package/dist/core/config/schema/scheduler.js +20 -0
- package/dist/core/config/schema/search.js +1 -1
- package/dist/core/config/schema/sources-bundles.js +32 -1
- package/dist/core/content-safety.js +52 -0
- package/dist/core/maintenance-barrier.js +6 -6
- package/dist/core/type-presentation.js +1 -1
- package/dist/core/write-source.js +13 -8
- package/dist/indexer/bundle-identity-guard.js +45 -8
- package/dist/indexer/indexer.js +1 -1
- package/dist/indexer/materialize-embeddings.js +15 -1
- package/dist/indexer/search/search-source.js +29 -11
- package/dist/integrations/agent/execution-lowering.js +3 -2
- package/dist/integrations/agent/execution-preparation.js +32 -1
- package/dist/integrations/agent/prompts.js +1 -1
- package/dist/integrations/agent/request-lowering.js +3 -2
- package/dist/llm/client.js +2 -1
- package/dist/output/shapes/passthrough.js +2 -0
- package/dist/registry/resolve.js +37 -10
- package/dist/scripts/akm-migrate-node.js +13644 -9894
- package/dist/scripts/akm-migrate.js +12626 -8876
- package/dist/setup/setup.js +3 -3
- package/dist/setup/steps/tasks.js +29 -36
- package/dist/sources/providers/git-install.js +17 -11
- package/dist/sources/providers/git-provider.js +12 -5
- package/dist/sources/providers/git-stash.js +38 -16
- package/dist/sources/snapshot-fetchers/website-ingest.js +3 -3
- package/dist/storage/repositories/index-vec-repository.js +100 -0
- package/dist/tasks/activation-config.js +90 -0
- package/dist/tasks/backends/cron.js +9 -0
- package/dist/tasks/backends/launchd.js +1 -0
- package/dist/tasks/backends/schtasks.js +2 -0
- package/dist/tasks/embedded.js +4 -5
- package/dist/tasks/scheduler-binding.js +2 -2
- package/dist/tasks/scheduler-sync-preview.js +8 -1
- package/dist/tasks/scheduler-sync.js +19 -10
- package/dist/tasks/source/parse-task-source.js +10 -113
- package/dist/tasks/source/project-v4.js +2 -2
- package/dist/tasks/source/task-source-v4.js +4 -12
- package/dist/tasks/source/task-to-v3.js +4 -12
- package/dist/tasks/source/task-to-v4.js +40 -7
- package/docs/migration/README.md +1 -0
- package/docs/migration/release-notes/0.9.16.md +72 -0
- package/docs/migration/v0.9.1-to-v0.9.2.md +6 -9
- package/docs/reference/cli.md +37 -29
- package/docs/reference/configuration.md +48 -5
- package/docs/reference/tasks.md +34 -29
- package/package.json +1 -1
- package/schemas/akm-config.json +112 -4
- package/schemas/akm-task.json +1 -2
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,58 @@ 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
|
+
## [Unreleased]
|
|
8
|
+
|
|
9
|
+
## [0.9.16-alpha.2] - 2026-09-21
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- **Scheduled execution is now granted by host-local config and bound to the
|
|
14
|
+
approved source installation.** `scheduler.enabled` records
|
|
15
|
+
`{kind, ref, sourceId}` entries written by `akm task enable`; authored task
|
|
16
|
+
frontmatter cannot enable itself. `akm migrate apply` performs the explicit,
|
|
17
|
+
backed-up conversion of older grants, and runtime config points unmigrated
|
|
18
|
+
installations to that command instead of silently inventing authority.
|
|
19
|
+
- **Executable assets now run beneath a host-owned tool ceiling.** Local
|
|
20
|
+
`execution.allowedTools` config caps asset tool requests. Workspace,
|
|
21
|
+
environment, and opaque runtime selection are no longer accepted from asset
|
|
22
|
+
frontmatter, and a transport that cannot enforce a non-empty resolved tool
|
|
23
|
+
policy fails before dispatch.
|
|
24
|
+
|
|
25
|
+
### Changed
|
|
26
|
+
|
|
27
|
+
- **Unscoped `akm task sync` reconciles every enabled bundle.** It refreshes
|
|
28
|
+
the native scheduler from the latest locally enabled task/workflow sources
|
|
29
|
+
and removes attributable bindings for bundles that have been disabled.
|
|
30
|
+
Removing a bundle also revokes its scheduler grants.
|
|
31
|
+
- **Bundle activation and identity are consistent across the CLI.** A disabled
|
|
32
|
+
bundle is inert for reads, writes, indexing, execution, and scheduling;
|
|
33
|
+
explicit lifecycle updates remain available. Physical-root identity rejects
|
|
34
|
+
duplicate or symlink-aliased bundle registrations and prevents an
|
|
35
|
+
`AKM_BUNDLE_DIR` alias from reactivating disabled content.
|
|
36
|
+
- **Shared config inheritance now separates portable policy from host
|
|
37
|
+
authority.** Source/default ownership, registries, scheduler and execution
|
|
38
|
+
grants, credentials, executable arguments/workspace, setup/experimental
|
|
39
|
+
state, reranker connections, and publication hooks stay local. Every
|
|
40
|
+
bundle-relative `extends` hop is checked by real path containment.
|
|
41
|
+
- **Unsafe overrides now name one risk each.** Use
|
|
42
|
+
`--allow-insecure-transport` for reviewed plain HTTP and
|
|
43
|
+
`--allow-dangerous-env-keys` for reviewed process-hijacking environment
|
|
44
|
+
keys; the former combined `--allow-insecure` switch is removed.
|
|
45
|
+
- **Bundle/source resolution carries explicit default and priority state.**
|
|
46
|
+
Search, show, write targeting, registry installation, and configuration
|
|
47
|
+
mutation no longer infer ownership or trust from array position.
|
|
48
|
+
|
|
49
|
+
### Fixed
|
|
50
|
+
|
|
51
|
+
- Protected generated improve content from credential echoes, redacted bodies,
|
|
52
|
+
and run-only scaffolding, while exercising the real bounded engine probe.
|
|
53
|
+
- Repaired degraded sqlite-vec mirrors, preserved scheduler intent across
|
|
54
|
+
synchronization, kept explicit setup choices and rollback serialization
|
|
55
|
+
stable, and tolerated valid sharded startup contention.
|
|
56
|
+
- Made git, website, npm, and filesystem bundle add/update/remove workflows
|
|
57
|
+
converge on the same lifecycle and dangerous-environment audit behavior.
|
|
58
|
+
|
|
7
59
|
## [0.9.15] - 2026-09-10
|
|
8
60
|
|
|
9
61
|
### Added
|
|
@@ -264,6 +264,7 @@ akm bundle add @scope/pkg # From npm (managed)
|
|
|
264
264
|
akm bundle add owner/repo # From GitHub (managed)
|
|
265
265
|
akm bundle add ./path/to/local/bundle # Local directory
|
|
266
266
|
akm bundle add git@github.com:org/repo.git --provider git --name my-skills --writable
|
|
267
|
+
akm bundle add https://github.com/org/private.git --provider git --credential '$GIT_READ_TOKEN'
|
|
267
268
|
akm registry add https://skills.sh --name skills.sh --provider skills-sh # Add the skills.sh registry
|
|
268
269
|
akm registry remove skills.sh # Remove the skills.sh registry
|
|
269
270
|
akm bundle list # List all sources
|
|
@@ -271,9 +272,13 @@ akm bundle list --kind git # Filter by provider (files
|
|
|
271
272
|
akm bundle remove <target> # Remove by id, ref, path, or name
|
|
272
273
|
akm bundle update --all # Update all managed sources
|
|
273
274
|
akm bundle update <target> --force # Force re-download
|
|
274
|
-
akm bundle update <target> --allow-
|
|
275
|
+
akm bundle update <target> --allow-dangerous-env-keys # Approve reviewed dangerous env keys
|
|
276
|
+
akm bundle update --all --skip-if-locked # Scheduled refresh; exit 0 on DB contention
|
|
275
277
|
```
|
|
276
278
|
|
|
279
|
+
Git bundles refresh only when `akm bundle update` runs. Schedule that command
|
|
280
|
+
when automatic refresh is desired.
|
|
281
|
+
|
|
277
282
|
## Registries
|
|
278
283
|
|
|
279
284
|
```sh
|
|
@@ -357,7 +362,9 @@ file plus one `sync` is a complete workflow.
|
|
|
357
362
|
```sh
|
|
358
363
|
akm task add nightly-improve --schedule "@daily" --command "akm improve --strategy default"
|
|
359
364
|
akm task add briefing --schedule "0 9 * * *" --prompt "Draft the morning briefing" # Inline command task
|
|
360
|
-
akm task
|
|
365
|
+
akm task enable <bundle>//tasks/<id> # Enable locally and sync that bundle
|
|
366
|
+
akm task disable <bundle>//tasks/<id> # Disable locally and unschedule it
|
|
367
|
+
akm task sync # Reconcile activated refs from every enabled configured bundle
|
|
361
368
|
akm task sync --rebind # Also re-pin the scheduler's akm binary/spelling
|
|
362
369
|
akm task doctor # Scheduler binding + runtime eligibility diagnosis
|
|
363
370
|
akm task history # Recent run rows (status, timing)
|
|
@@ -370,10 +377,10 @@ Task files use task source v4 (`version: 4`). There is no `akm:` options bag
|
|
|
370
377
|
or `on:` block — every control (`schedule`, `timeout`, `engine`, `model`,
|
|
371
378
|
`redact`, `maxSteps`, `maxRetries`, …) is a top-level key now. Typed
|
|
372
379
|
`inputs:` declarations and a bounded `output:` schema work like a
|
|
373
|
-
workflow's (`output:` replaces v3's `akm.outputSchema`).
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
380
|
+
workflow's (`output:` replaces v3's `akm.outputSchema`). Task source never
|
|
381
|
+
controls activation: use `akm task enable` / `disable`, which update the
|
|
382
|
+
host-local config allow-list and sync. To remove one, delete the YAML and run
|
|
383
|
+
`akm task sync` — the scheduler entry is unbound. Top-level
|
|
377
384
|
`timeout:` may be `null` (disable the invocation timer) or a duration/number
|
|
378
385
|
overriding the selected engine invocation timeout. Preview old task-v2/v3
|
|
379
386
|
conversion with `akm migrate apply --dry-run`.
|
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
version: 4
|
|
2
2
|
run: akm improve --strategy catchup --skip-if-locked --require-engines
|
|
3
3
|
description: Manual recovery — consolidation + triage drain (run on demand via `akm task run akm-improve-catchup`)
|
|
4
|
-
# Manual-recovery task:
|
|
5
|
-
#
|
|
6
|
-
|
|
7
|
-
schedule:
|
|
8
|
-
- cron: "0 4 * * *"
|
|
9
|
-
enabled: false
|
|
4
|
+
# Manual-recovery task: setup offers it disabled by default. `akm task run`
|
|
5
|
+
# still works manually; `akm task enable` opts this host into the schedule.
|
|
6
|
+
schedule: "0 4 * * *"
|
|
@@ -62,8 +62,6 @@ const RETIRED_COMMAND_HINTS = {
|
|
|
62
62
|
"workflow report": "`akm workflow report` was removed — the external-driver protocol is gone; `akm workflow run <target>` dispatches and records units itself.",
|
|
63
63
|
"config show": "`akm config show` was removed in 0.9 — use `akm config list`.",
|
|
64
64
|
"config validate": "`akm config validate` was removed in 0.9 — the config file is validated on every load.",
|
|
65
|
-
"task enable": "`akm task enable` was removed in 0.9 — set `enabled: true` in the task YAML, then `akm task sync`.",
|
|
66
|
-
"task disable": "`akm task disable` was removed in 0.9 — set `enabled: false` in the task YAML, then `akm task sync`.",
|
|
67
65
|
"task init": "`akm task init` was removed in 0.9 — `akm setup` seeds the default schedules.",
|
|
68
66
|
"task show": "there is no `task show` — task files are indexed assets; use `akm show <ref>`.",
|
|
69
67
|
"task remove": "there is no `task remove` — delete the task YAML, then run `akm task sync` to unbind it.",
|
|
@@ -87,16 +87,16 @@ export function resolveEnvBinding(target, options = {}) {
|
|
|
87
87
|
const decision = decideDangerousEnvInjection({
|
|
88
88
|
dangerousKeys: dangerous,
|
|
89
89
|
thirdParty: Boolean(source.registryId),
|
|
90
|
-
|
|
90
|
+
allowDangerousEnvKeys: options.allowDangerousEnvKeys,
|
|
91
91
|
});
|
|
92
92
|
if (decision === "block") {
|
|
93
93
|
throw new UsageError(`Refusing to inject env from a third-party stash. ${detail}\n` +
|
|
94
94
|
` Review the file, then copy the values into a first-party env if you trust them, ` +
|
|
95
|
-
`or pass --allow-
|
|
95
|
+
`or pass --allow-dangerous-env-keys once you have.`, "INVALID_FLAG_VALUE");
|
|
96
96
|
}
|
|
97
97
|
if (decision === "warn") {
|
|
98
|
-
warn(options.
|
|
99
|
-
? `${detail} Injecting anyway (--allow-
|
|
98
|
+
warn(options.allowDangerousEnvKeys && source.registryId
|
|
99
|
+
? `${detail} Injecting anyway (--allow-dangerous-env-keys).`
|
|
100
100
|
: `${detail} Injecting anyway (first-party stash).`);
|
|
101
101
|
}
|
|
102
102
|
}
|
|
@@ -214,7 +214,7 @@ async function runEnvInjected(target, opts) {
|
|
|
214
214
|
const { values: envValues } = resolveEnvBinding(target, {
|
|
215
215
|
only: opts.only,
|
|
216
216
|
except: opts.except,
|
|
217
|
-
|
|
217
|
+
allowDangerousEnvKeys: opts.allowDangerousEnvKeys,
|
|
218
218
|
});
|
|
219
219
|
const mergedEnv = buildChildEnv(process.env, {
|
|
220
220
|
clean: opts.clean === true,
|
|
@@ -283,7 +283,7 @@ const envRunCommand = defineJsonCommand({
|
|
|
283
283
|
type: "string",
|
|
284
284
|
description: "When used with --clean, also inherit these parent env vars (comma-separated). Ignored without --clean.",
|
|
285
285
|
},
|
|
286
|
-
"allow-
|
|
286
|
+
"allow-dangerous-env-keys": {
|
|
287
287
|
type: "boolean",
|
|
288
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.",
|
|
289
289
|
default: false,
|
|
@@ -295,7 +295,7 @@ const envRunCommand = defineJsonCommand({
|
|
|
295
295
|
except: parseKeyListFlag(args.except),
|
|
296
296
|
clean: args.clean === true,
|
|
297
297
|
inherit: parseKeyListFlag(args.inherit) ?? [],
|
|
298
|
-
|
|
298
|
+
allowDangerousEnvKeys: args["allow-dangerous-env-keys"] === true,
|
|
299
299
|
});
|
|
300
300
|
},
|
|
301
301
|
});
|
|
@@ -15,7 +15,7 @@ import { redactSensitiveText } from "../../core/redaction.js";
|
|
|
15
15
|
import { clearLogFile, setLogFile, warn } from "../../core/warn.js";
|
|
16
16
|
import { resolveWriteTarget } from "../../core/write-source.js";
|
|
17
17
|
import { collectEngineCredentialValues } from "../../integrations/agent/engine-resolution.js";
|
|
18
|
-
import {
|
|
18
|
+
import { probeLlmReachable } from "../../llm/client.js";
|
|
19
19
|
import { getOutputMode } from "../../output/context.js";
|
|
20
20
|
import { deliverRendered } from "../../output/html-render.js";
|
|
21
21
|
import { akmImprove, resolveImproveReadSource } from "./improve.js";
|
|
@@ -122,27 +122,32 @@ function collectRequiredEngineTargets(plan) {
|
|
|
122
122
|
* `--require-engines` field re-test (#957): the static check above only
|
|
123
123
|
* proves an engine is configured and credentialed — it cannot see a dead
|
|
124
124
|
* endpoint. A field run against an unreachable engine sat silent for
|
|
125
|
-
* minutes instead of hitting the documented exit-78 path.
|
|
126
|
-
*
|
|
127
|
-
* `
|
|
128
|
-
*
|
|
129
|
-
*
|
|
130
|
-
*
|
|
125
|
+
* minutes instead of hitting the documented exit-78 path. Exercise the real
|
|
126
|
+
* model completion path with a tiny response and a three-second bound. The
|
|
127
|
+
* `/models` endpoint used by the lightweight health check is deliberately
|
|
128
|
+
* insufficient here: a gateway can list a model while its upstream completion
|
|
129
|
+
* route is dead (#980). Deduplicate by endpoint + model, not endpoint alone,
|
|
130
|
+
* because model backends behind one gateway can fail independently.
|
|
131
131
|
*/
|
|
132
|
-
async function assertRequiredEnginesReachable(plan, probeReachable =
|
|
132
|
+
async function assertRequiredEnginesReachable(plan, probeReachable = (connection) => probeLlmReachable(connection, 3_000)) {
|
|
133
133
|
const targets = collectRequiredEngineTargets(plan);
|
|
134
134
|
if (targets.length === 0)
|
|
135
135
|
return;
|
|
136
|
-
const
|
|
137
|
-
const probed = await Promise.all(targets.map(async (target) =>
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
136
|
+
const probesByConnection = new Map();
|
|
137
|
+
const probed = await Promise.all(targets.map(async (target) => {
|
|
138
|
+
const key = `${target.connection.endpoint.replace(/\/+$/, "")}|${target.connection.model}`;
|
|
139
|
+
let pending = probesByConnection.get(key);
|
|
140
|
+
if (!pending) {
|
|
141
|
+
pending = probeReachable(target.connection);
|
|
142
|
+
probesByConnection.set(key, pending);
|
|
143
|
+
}
|
|
144
|
+
return { ...target, reach: await pending };
|
|
145
|
+
}));
|
|
141
146
|
const unreachable = probed.filter((item) => !item.reach.reachable);
|
|
142
147
|
if (unreachable.length === 0)
|
|
143
148
|
return;
|
|
144
149
|
const lines = unreachable.map((item) => ` - ${item.process} (engine "${item.engine}", ${item.connection.endpoint}): ${item.reach.error ?? "did not respond"}`);
|
|
145
|
-
throw new ConfigError(`--require-engines: ${unreachable.length} improve process${unreachable.length === 1 ? "" : "es"} cannot run because ${unreachable.length === 1 ? "its" : "their"} engine
|
|
150
|
+
throw new ConfigError(`--require-engines: ${unreachable.length} improve process${unreachable.length === 1 ? "" : "es"} cannot run because ${unreachable.length === 1 ? "its" : "their"} engine completion path is not reachable:\n${lines.join("\n")}`, "LLM_NOT_CONFIGURED");
|
|
146
151
|
}
|
|
147
152
|
/**
|
|
148
153
|
* `--show-prompt` (#952): render the composed reflect prompt for one asset ref
|
|
@@ -29,6 +29,7 @@ import { parseFrontmatter } from "../../core/asset/frontmatter.js";
|
|
|
29
29
|
import { conceptIdFromTypeName, parseRefInput } from "../../core/asset/resolve-ref.js";
|
|
30
30
|
import { DESCRIPTION_MAX_CHARS, requiresDescription } from "../../core/authoring-rules.js";
|
|
31
31
|
import { loadConfig } from "../../core/config/config.js";
|
|
32
|
+
import { generatedContentRejection, stripReflectPromptScaffolding } from "../../core/content-safety.js";
|
|
32
33
|
import { ConfigError, UsageError } from "../../core/errors.js";
|
|
33
34
|
import { appendEvent, readEvents } from "../../core/events.js";
|
|
34
35
|
import { lintLessonContent } from "../../core/lesson-lint.js";
|
|
@@ -511,7 +512,12 @@ export function sanitizeReflectPayload(payload, sourceContent, targetRef) {
|
|
|
511
512
|
mergedFm[field] = sourceFm[field];
|
|
512
513
|
}
|
|
513
514
|
}
|
|
514
|
-
const
|
|
515
|
+
const withoutAppendedFrontmatter = stripAppendedFrontmatter(rawLlmBody.replace(/^\s+/, ""));
|
|
516
|
+
const promptScaffolding = stripReflectPromptScaffolding(withoutAppendedFrontmatter);
|
|
517
|
+
const cleanedBody = promptScaffolding.content;
|
|
518
|
+
if (promptScaffolding.stripped) {
|
|
519
|
+
warnings.push('Removed echoed run-only "Avoid These Patterns" guidance from the proposed asset body (#963).');
|
|
520
|
+
}
|
|
515
521
|
// #636 — deterministic description fallback (reflect-side belt-and-suspenders).
|
|
516
522
|
// If the type requires a `description` and the merged frontmatter is still
|
|
517
523
|
// MISSING one (source had none AND the model didn't author one), derive a
|
|
@@ -1881,7 +1887,22 @@ export async function akmReflect(options = {}) {
|
|
|
1881
1887
|
// draft paths.
|
|
1882
1888
|
cleanupReflectDrafts(draftPathsToCleanup);
|
|
1883
1889
|
}
|
|
1884
|
-
|
|
1890
|
+
const unsafeContent = generatedContentRejection(payload.content, redactSensitiveText(payload.content, sensitiveValues));
|
|
1891
|
+
if (unsafeContent) {
|
|
1892
|
+
emitReflectFailed("parse_error", "parse_error", options.ref, {
|
|
1893
|
+
...(result.exitCode !== null ? { exitCode: result.exitCode } : {}),
|
|
1894
|
+
});
|
|
1895
|
+
return {
|
|
1896
|
+
schemaVersion: 2,
|
|
1897
|
+
ok: false,
|
|
1898
|
+
reason: "parse_error",
|
|
1899
|
+
error: unsafeContent,
|
|
1900
|
+
...(options.ref ? { ref: options.ref } : {}),
|
|
1901
|
+
engine: engineName,
|
|
1902
|
+
exitCode: result.exitCode,
|
|
1903
|
+
...reflectNoticeFields(executionNotices),
|
|
1904
|
+
};
|
|
1905
|
+
}
|
|
1885
1906
|
const refFailure = validateReflectPayloadRef({
|
|
1886
1907
|
payload,
|
|
1887
1908
|
result,
|
|
@@ -41,6 +41,7 @@ import { checkUnquotedDescriptionColon } from "../../core/asset/frontmatter-lint
|
|
|
41
41
|
import { isArchivedRelPath } from "../../core/asset/memory-archive.js";
|
|
42
42
|
import { conceptIdFromTypeName, typeNameFromConceptId } from "../../core/asset/resolve-ref.js";
|
|
43
43
|
import { localDateStamp } from "../../core/common.js";
|
|
44
|
+
import { containsRedactedContent, REDACTED_CONTENT_MARKER } from "../../core/content-safety.js";
|
|
44
45
|
import { findFenceRegions } from "./markdown-insertion.js";
|
|
45
46
|
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
46
47
|
/** Fold physically wrapped prose the same way a YAML plain scalar does. */
|
|
@@ -603,6 +604,14 @@ export function runBaseChecks(ctx) {
|
|
|
603
604
|
.map((v) => String(v).trim())
|
|
604
605
|
.filter(Boolean);
|
|
605
606
|
const shouldRun = (issueType) => !lintSkip.includes(issueType);
|
|
607
|
+
if (shouldRun("redacted-content") && containsRedactedContent(currentRaw)) {
|
|
608
|
+
issues.push({
|
|
609
|
+
file: ctx.relPath,
|
|
610
|
+
issue: "redacted-content",
|
|
611
|
+
detail: `asset contains ${REDACTED_CONTENT_MARKER}; restore the original non-secret prose from version history`,
|
|
612
|
+
fixed: false,
|
|
613
|
+
});
|
|
614
|
+
}
|
|
606
615
|
// ── 1. unquoted-colon ──────────────────────────────────────────────────
|
|
607
616
|
if (shouldRun("unquoted-colon")) {
|
|
608
617
|
const unquotedColonDetail = checkUnquotedDescriptionColon(ctx.frontmatter);
|
|
@@ -11,7 +11,7 @@
|
|
|
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-
|
|
14
|
+
* - `akm bundle add` BLOCKS install unless `--allow-dangerous-env-keys` is set (or, on TTY,
|
|
15
15
|
* the user explicitly confirms at the prompt).
|
|
16
16
|
* - Local env writes do NOT consult this list — by design, the operator may
|
|
17
17
|
* legitimately store any key locally. The gate exists only for third-party
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
* invoked by many interactive tools and are a documented RCE vector when
|
|
23
23
|
* sourced from untrusted environments. They will also flag on benign files
|
|
24
24
|
* where the operator legitimately wants to set their editor — accept the
|
|
25
|
-
* FP and bypass with `--allow-
|
|
25
|
+
* FP and bypass with `--allow-dangerous-env-keys` after review.
|
|
26
26
|
*/
|
|
27
27
|
import fs from "node:fs";
|
|
28
28
|
import { listKeys } from "../env/env.js";
|
|
@@ -16,6 +16,7 @@ import fs from "node:fs";
|
|
|
16
16
|
import { placementTypes, stashDirFor } from "../../core/asset/asset-placement.js";
|
|
17
17
|
import { parseRefInput } from "../../core/asset/resolve-ref.js";
|
|
18
18
|
import { resolveStashDir } from "../../core/common.js";
|
|
19
|
+
import { generatedContentRejection } from "../../core/content-safety.js";
|
|
19
20
|
import { UsageError } from "../../core/errors.js";
|
|
20
21
|
import { appendEvent } from "../../core/events.js";
|
|
21
22
|
import { resolveStandardsContext } from "../../core/standards/resolve-standards-context.js";
|
|
@@ -217,7 +218,20 @@ export async function akmPropose(options) {
|
|
|
217
218
|
};
|
|
218
219
|
}
|
|
219
220
|
}
|
|
220
|
-
|
|
221
|
+
const unsafeContent = generatedContentRejection(payload.content, redactWithLoweredExecutionDispatchLease(lease, payload.content));
|
|
222
|
+
if (unsafeContent) {
|
|
223
|
+
return {
|
|
224
|
+
schemaVersion: 2,
|
|
225
|
+
ok: false,
|
|
226
|
+
reason: "parse_error",
|
|
227
|
+
error: unsafeContent,
|
|
228
|
+
type: options.type,
|
|
229
|
+
name: options.name,
|
|
230
|
+
engine: engineName,
|
|
231
|
+
exitCode: result.exitCode,
|
|
232
|
+
...noticeFields(notices),
|
|
233
|
+
};
|
|
234
|
+
}
|
|
221
235
|
// 6. Insert the proposal. Note: we allow the agent's `ref` to normalise the
|
|
222
236
|
// asset name (e.g. path-cleanup), but only after validating that the ref is
|
|
223
237
|
// well-formed and the type still matches the requested type.
|
|
@@ -61,6 +61,7 @@
|
|
|
61
61
|
import { parseFrontmatter } from "../../../core/asset/frontmatter.js";
|
|
62
62
|
import { parseRefInput } from "../../../core/asset/resolve-ref.js";
|
|
63
63
|
import { DESCRIPTION_MAX_CHARS, DESCRIPTION_MIN_CHARS, WHEN_TO_USE_MAX_CHARS, WHEN_TO_USE_MIN_CHARS, } from "../../../core/authoring-rules.js";
|
|
64
|
+
import { containsRedactedContent, containsReflectPromptScaffolding, REDACTED_CONTENT_MARKER, REFLECT_AVOID_PATTERNS_HEADING, } from "../../../core/content-safety.js";
|
|
64
65
|
import { proposalContent } from "../../../core/file-change.js";
|
|
65
66
|
/**
|
|
66
67
|
* The canonical asset NAME an inputRef names, lower-cased — the tail the
|
|
@@ -399,6 +400,40 @@ const reflectTruncationMarkerValidator = {
|
|
|
399
400
|
];
|
|
400
401
|
},
|
|
401
402
|
};
|
|
403
|
+
/** Never promote proposal text that already contains an output-redaction marker (#962). */
|
|
404
|
+
const redactedContentValidator = {
|
|
405
|
+
name: "redacted-content",
|
|
406
|
+
appliesTo(proposal) {
|
|
407
|
+
return typeof proposal.payload?.content === "string";
|
|
408
|
+
},
|
|
409
|
+
validate(proposal) {
|
|
410
|
+
if (!containsRedactedContent(proposalContent(proposal)))
|
|
411
|
+
return [];
|
|
412
|
+
return [
|
|
413
|
+
{
|
|
414
|
+
kind: "redacted-content",
|
|
415
|
+
message: `Proposal ${proposal.id} (${proposal.ref}) contains ${REDACTED_CONTENT_MARKER}. Restore the original non-secret prose and create a clean proposal; redacted output cannot be promoted.`,
|
|
416
|
+
},
|
|
417
|
+
];
|
|
418
|
+
},
|
|
419
|
+
};
|
|
420
|
+
/** Defense in depth when a reflect proposal bypasses creation-time sanitization (#963). */
|
|
421
|
+
const reflectPromptScaffoldingValidator = {
|
|
422
|
+
name: "reflect-prompt-scaffolding",
|
|
423
|
+
appliesTo(proposal) {
|
|
424
|
+
return proposal.source === "reflect" && typeof proposal.payload?.content === "string";
|
|
425
|
+
},
|
|
426
|
+
validate(proposal) {
|
|
427
|
+
if (!containsReflectPromptScaffolding(proposalContent(proposal)))
|
|
428
|
+
return [];
|
|
429
|
+
return [
|
|
430
|
+
{
|
|
431
|
+
kind: "reflect-prompt-scaffolding",
|
|
432
|
+
message: `Proposal ${proposal.id} (${proposal.ref}) still contains the run-only "${REFLECT_AVOID_PATTERNS_HEADING}" prompt section. Reflect the asset again before promotion.`,
|
|
433
|
+
},
|
|
434
|
+
];
|
|
435
|
+
},
|
|
436
|
+
};
|
|
402
437
|
/**
|
|
403
438
|
* Report a validator's findings as advisory.
|
|
404
439
|
*
|
|
@@ -424,9 +459,9 @@ function advisory(validator) {
|
|
|
424
459
|
* Full set of quality validators in registration order. Appended onto
|
|
425
460
|
* {@link defaultProposalValidators} so they run inside `validateProposal` on
|
|
426
461
|
* `proposal accept` automatically. All prose-quality checks report without
|
|
427
|
-
* blocking (see {@link advisory})
|
|
428
|
-
*
|
|
429
|
-
* prose quality.
|
|
462
|
+
* blocking (see {@link advisory}). The truncation-marker, redacted-content,
|
|
463
|
+
* and reflected-prompt-scaffolding validators block because they protect
|
|
464
|
+
* durable content rather than judging prose quality.
|
|
430
465
|
*/
|
|
431
466
|
export const defaultProposalQualityValidators = [
|
|
432
467
|
...[
|
|
@@ -436,4 +471,6 @@ export const defaultProposalQualityValidators = [
|
|
|
436
471
|
reflectSizeGuardValidator,
|
|
437
472
|
].map(advisory),
|
|
438
473
|
reflectTruncationMarkerValidator,
|
|
474
|
+
redactedContentValidator,
|
|
475
|
+
reflectPromptScaffoldingValidator,
|
|
439
476
|
];
|
|
@@ -131,14 +131,15 @@ export const defaultProposalValidators = [
|
|
|
131
131
|
* and was previously safe to run in full there because every quality
|
|
132
132
|
* validator was advisory (`advisory()` downgrades findings to `severity:
|
|
133
133
|
* "warn"`, which {@link runProposalValidators}'s `ok` never treats as
|
|
134
|
-
* failing). #952
|
|
135
|
-
*
|
|
136
|
-
*
|
|
134
|
+
* failing). Blocking durable-content validators (the #952 truncation marker,
|
|
135
|
+
* #962 redaction marker, and #963 reflected prompt scaffolding) deliberately
|
|
136
|
+
* remain outside this subset, so running the full
|
|
137
|
+
* {@link defaultProposalValidators} list at mint time could throw
|
|
137
138
|
* `invalid_canonical_structure` for any lesson/task/workflow reflect
|
|
138
139
|
* proposal whose body leaks the truncation marker — instead of letting
|
|
139
140
|
* `sanitizeReflectPayload` mint the proposal and defer it with
|
|
140
141
|
* `reflect-truncation-leak`, per the #952 design. Quality validators (prose
|
|
141
|
-
* shape, reflect size ratio,
|
|
142
|
+
* shape, reflect size ratio, and durable-content guards) belong at
|
|
142
143
|
* `proposal accept` / drain-promotion time, which already calls
|
|
143
144
|
* {@link validateProposal} (the full list) via `preflightProposalPromotion`
|
|
144
145
|
* / `promoteProposalWithLease`.
|
|
@@ -26,6 +26,24 @@ import { insertUsageEvent } from "../../indexer/usage/usage-events.js";
|
|
|
26
26
|
import { TELEMETRY_BUSY_TIMEOUT_MS, withIndexDb } from "../../storage/repositories/index-db.js";
|
|
27
27
|
import { searchRegistry } from "./registry-search.js";
|
|
28
28
|
const DEFAULT_LIMIT = 20;
|
|
29
|
+
function duplicateConceptWarnings(hits, defaultBundle) {
|
|
30
|
+
const ownersByConcept = new Map();
|
|
31
|
+
for (const hit of hits) {
|
|
32
|
+
const displayRef = (hit.parentRef ?? hit.ref).split("#", 1)[0] ?? hit.ref;
|
|
33
|
+
const boundary = displayRef.indexOf("//");
|
|
34
|
+
const conceptId = boundary >= 0 ? displayRef.slice(boundary + 2) : displayRef;
|
|
35
|
+
const owner = hit.origin ?? defaultBundle ?? "working-bundle";
|
|
36
|
+
const owners = ownersByConcept.get(conceptId) ?? [];
|
|
37
|
+
if (!owners.includes(owner))
|
|
38
|
+
owners.push(owner);
|
|
39
|
+
ownersByConcept.set(conceptId, owners);
|
|
40
|
+
}
|
|
41
|
+
return [...ownersByConcept]
|
|
42
|
+
.filter(([, owners]) => owners.length > 1)
|
|
43
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
44
|
+
.map(([conceptId, owners]) => `Multiple bundles provide "${conceptId}": ${owners.join(", ")}. ` +
|
|
45
|
+
"Unqualified refs resolve by configured bundle priority; use a bundle-qualified ref to select explicitly.");
|
|
46
|
+
}
|
|
29
47
|
export async function akmSearch(input) {
|
|
30
48
|
const t0 = Date.now();
|
|
31
49
|
const query = input.query.trim();
|
|
@@ -116,6 +134,7 @@ export async function akmSearch(input) {
|
|
|
116
134
|
: await searchRegistry(query, { limit, includeAssets: input.assets === true, registries: config.registries });
|
|
117
135
|
if (source === "local") {
|
|
118
136
|
const localHits = localResult?.hits ?? [];
|
|
137
|
+
const warnings = [...(localResult?.warnings ?? []), ...duplicateConceptWarnings(localHits, config.defaultBundle)];
|
|
119
138
|
const hasResults = localHits.length > 0;
|
|
120
139
|
const response = {
|
|
121
140
|
schemaVersion: 1,
|
|
@@ -123,7 +142,7 @@ export async function akmSearch(input) {
|
|
|
123
142
|
source,
|
|
124
143
|
hits: localHits,
|
|
125
144
|
tip: hasResults ? undefined : localResult?.tip,
|
|
126
|
-
warnings:
|
|
145
|
+
warnings: warnings.length ? warnings : undefined,
|
|
127
146
|
searchMode: localResult?.mode ?? "keyword",
|
|
128
147
|
timing: { totalMs: Date.now() - t0, rankMs: localResult?.rankMs, embedMs: localResult?.embedMs },
|
|
129
148
|
};
|
|
@@ -162,7 +181,11 @@ export async function akmSearch(input) {
|
|
|
162
181
|
}
|
|
163
182
|
// source === "all"
|
|
164
183
|
const allStashHits = (localResult?.hits ?? []).slice(0, limit);
|
|
165
|
-
const warnings = [
|
|
184
|
+
const warnings = [
|
|
185
|
+
...(localResult?.warnings ?? []),
|
|
186
|
+
...duplicateConceptWarnings(allStashHits, config.defaultBundle),
|
|
187
|
+
...(registryResult?.warnings ?? []),
|
|
188
|
+
];
|
|
166
189
|
const hasResults = allStashHits.length > 0 || registryHits.length > 0;
|
|
167
190
|
const response = {
|
|
168
191
|
schemaVersion: 1,
|
|
@@ -301,9 +324,15 @@ function logSearchEvent(query, response, mode = "keyword", eventSource = "user",
|
|
|
301
324
|
*/
|
|
302
325
|
function assertNamedSourceExists(config, namedSourceName) {
|
|
303
326
|
const configSources = getSources(config);
|
|
304
|
-
const foundInConfig = configSources.
|
|
327
|
+
const foundInConfig = configSources.find((source) => source.name === namedSourceName || source.path === namedSourceName);
|
|
328
|
+
if (foundInConfig?.enabled === false) {
|
|
329
|
+
throw new UsageError(`Source "${namedSourceName}" is disabled.`, "INVALID_SOURCE_VALUE");
|
|
330
|
+
}
|
|
305
331
|
if (!foundInConfig) {
|
|
306
|
-
const validNames = configSources
|
|
332
|
+
const validNames = configSources
|
|
333
|
+
.filter((source) => source.enabled !== false)
|
|
334
|
+
.map((source) => source.name)
|
|
335
|
+
.filter((name) => Boolean(name));
|
|
307
336
|
const hint = validNames.length > 0
|
|
308
337
|
? `Known source names: ${validNames.join(", ")}`
|
|
309
338
|
: "No named sources are configured. Run `akm bundle list` to see installed bundles.";
|
|
@@ -75,6 +75,11 @@ export async function akmShowUnified(input) {
|
|
|
75
75
|
if (metaRef)
|
|
76
76
|
return showStashMeta(metaRef);
|
|
77
77
|
}
|
|
78
|
+
const legacyReplacement = legacyColonRefReplacement(ref);
|
|
79
|
+
if (legacyReplacement) {
|
|
80
|
+
throw new NotFoundError(`The legacy colon ref "${ref}" was removed in 0.9.0. Use the slash form instead: ` +
|
|
81
|
+
`akm show ${legacyReplacement}`);
|
|
82
|
+
}
|
|
78
83
|
// Env/secret bodies have no safe fragment surface, and a fragment cannot
|
|
79
84
|
// widen what the env/secret renderers expose: both always omit the body
|
|
80
85
|
// (env — key names only; secret — never rendered), fragment or not. Warn
|
|
@@ -110,6 +115,20 @@ export async function akmShowUnified(input) {
|
|
|
110
115
|
}
|
|
111
116
|
return result;
|
|
112
117
|
}
|
|
118
|
+
/** Actionable migration guidance for the retired `[bundle//]type:name` spelling. */
|
|
119
|
+
function legacyColonRefReplacement(ref) {
|
|
120
|
+
const match = /^(?:(?<bundle>[^/#]+)\/\/)?(?<type>[a-z][a-z0-9-]*):(?<name>[^#]+)(?<fragment>#.*)?$/i.exec(ref);
|
|
121
|
+
const type = match?.groups?.type?.toLowerCase();
|
|
122
|
+
const name = match?.groups?.name;
|
|
123
|
+
if (!type || !name)
|
|
124
|
+
return undefined;
|
|
125
|
+
const stashDir = stashDirFor(type);
|
|
126
|
+
if (!stashDir)
|
|
127
|
+
return undefined;
|
|
128
|
+
const bundle = match?.groups?.bundle;
|
|
129
|
+
const fragment = match?.groups?.fragment ?? "";
|
|
130
|
+
return `${bundle ? `${bundle}//` : ""}${stashDir}/${name}${fragment}`;
|
|
131
|
+
}
|
|
113
132
|
/**
|
|
114
133
|
* Resolve a stash `.meta/` doc and return it as a lightweight ShowResponse.
|
|
115
134
|
*
|
|
@@ -288,7 +307,7 @@ export async function showLocal(input) {
|
|
|
288
307
|
throw new UsageError(`Renderer "${match.renderer}" not found for asset: ${makeBundleRef(parsed.bundle, parsed.conceptId)}`);
|
|
289
308
|
}
|
|
290
309
|
const renderBundle = indexedEntry.bundleId;
|
|
291
|
-
const renderDefaultBundle = config.defaultBundle ?? (source?.
|
|
310
|
+
const renderDefaultBundle = config.defaultBundle ?? (source?.isDefault === true ? renderBundle : undefined);
|
|
292
311
|
const renderCtx = buildRenderContext(fileCtx, match, allSourceDirs, renderBundle, renderDefaultBundle);
|
|
293
312
|
response = renderer.buildShowResponse(renderCtx);
|
|
294
313
|
if (parsed.fragment !== undefined) {
|
|
@@ -306,7 +325,7 @@ export async function showLocal(input) {
|
|
|
306
325
|
}
|
|
307
326
|
response.type = indexedEntry.type;
|
|
308
327
|
response.name = indexedEntry.name;
|
|
309
|
-
const isPrimaryStash = source
|
|
328
|
+
const isPrimaryStash = source?.isDefault === true;
|
|
310
329
|
const canonicalRef = displayRef({
|
|
311
330
|
type: indexedEntry.type,
|
|
312
331
|
name: presentedName,
|
|
@@ -25,7 +25,7 @@ export const registryCommand = defineGroupCommand({
|
|
|
25
25
|
name: { type: "string", description: "Human-friendly name for the registry" },
|
|
26
26
|
provider: { type: "string", description: "Provider type (e.g. static-index, skills-sh)" },
|
|
27
27
|
options: { type: "string", description: "Provider-specific options as JSON." },
|
|
28
|
-
"allow-insecure": {
|
|
28
|
+
"allow-insecure-transport": {
|
|
29
29
|
type: "boolean",
|
|
30
30
|
description: "Allow a plain HTTP registry URL (otherwise rejected)",
|
|
31
31
|
default: false,
|
|
@@ -41,12 +41,12 @@ export const registryCommand = defineGroupCommand({
|
|
|
41
41
|
throw new UsageError("Registry URL must start with http:// or https://");
|
|
42
42
|
}
|
|
43
43
|
if (args.url.startsWith("http://")) {
|
|
44
|
-
const
|
|
45
|
-
if (!
|
|
44
|
+
const allowInsecureTransport = args["allow-insecure-transport"];
|
|
45
|
+
if (!allowInsecureTransport) {
|
|
46
46
|
throw new UsageError("Registry URL uses plain HTTP (not HTTPS). An on-path attacker could substitute a malicious index. " +
|
|
47
|
-
"Use https:// or pass --allow-insecure if you have explicitly accepted the risk.");
|
|
47
|
+
"Use https:// or pass --allow-insecure-transport if you have explicitly accepted the risk.");
|
|
48
48
|
}
|
|
49
|
-
warn("Warning: registry URL uses plain HTTP (not HTTPS). --allow-insecure was set; an on-path attacker could substitute a malicious index.");
|
|
49
|
+
warn("Warning: registry URL uses plain HTTP (not HTTPS). --allow-insecure-transport was set; an on-path attacker could substitute a malicious index.");
|
|
50
50
|
}
|
|
51
51
|
const entry = { url: args.url };
|
|
52
52
|
if (args.name)
|