akm-cli 0.9.11 → 0.9.13
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 +227 -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/assets/workflows/workflow-template.md +4 -0
- 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 +14 -0
- package/dist/commands/improve/preparation.js +9 -6
- package/dist/commands/improve/reflect.js +61 -77
- package/dist/commands/lint/base-linter.js +10 -0
- 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/drain.js +48 -6
- package/dist/commands/proposal/proposal-cli.js +1 -0
- 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 +56 -6
- package/dist/core/config/config.js +42 -17
- 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 +27 -10
- package/dist/core/config/schema/sources-bundles.js +1 -6
- package/dist/core/errors.js +4 -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 +28 -8
- package/dist/llm/embedders/remote.js +3 -2
- package/dist/llm/index-passes.js +3 -2
- package/dist/output/shapes/passthrough.js +9 -3
- package/dist/output/shapes.js +50 -3
- package/dist/output/text/proposal-format.js +5 -0
- package/dist/output/text/workflow-format.js +8 -1
- package/dist/scripts/akm-migrate-node.js +1737 -1392
- package/dist/scripts/akm-migrate.js +1736 -1391
- 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 +133 -11
- 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 +20 -11
- package/dist/workflows/exec/step-work.js +76 -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/parser.js +1 -1
- 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 +104 -154
- 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 +92 -17
- package/package.json +1 -1
- package/schemas/akm-config.json +5 -10
|
@@ -6,6 +6,8 @@ import { isMap, isScalar, parseDocument, visit } from "yaml";
|
|
|
6
6
|
import { cloneExecutionJson, cloneExecutionJsonObject, } from "../../execution/json.js";
|
|
7
7
|
import { assertSnapshotKeys, snapshotStrictRecord } from "../../execution/record.js";
|
|
8
8
|
import { cloneToolSelection, createAdapterRenderedExecutionSource, } from "../../execution/source.js";
|
|
9
|
+
import { UsageError } from "../errors.js";
|
|
10
|
+
import { warnOnce } from "../warn.js";
|
|
9
11
|
function nextLine(text, start) {
|
|
10
12
|
const lf = text.indexOf("\n", start);
|
|
11
13
|
if (lf < 0)
|
|
@@ -13,16 +15,39 @@ function nextLine(text, start) {
|
|
|
13
15
|
const end = lf > start && text[lf - 1] === "\r" ? lf - 1 : lf;
|
|
14
16
|
return { line: text.slice(start, end), next: lf + 1 };
|
|
15
17
|
}
|
|
16
|
-
/**
|
|
17
|
-
|
|
18
|
+
/**
|
|
19
|
+
* Strict execution-only frontmatter parser; indexing's tolerant parser is
|
|
20
|
+
* deliberately not reused.
|
|
21
|
+
*
|
|
22
|
+
* `filePath`, when given, names the offending asset in a thrown error so an
|
|
23
|
+
* operator (or CI) sees which file to fix instead of a bare message.
|
|
24
|
+
*
|
|
25
|
+
* A third party's markdown that genuinely cannot be parsed into a mapping
|
|
26
|
+
* (unterminated frontmatter, broken YAML syntax, a non-mapping document)
|
|
27
|
+
* throws {@link UsageError} \u2014 exit 2, an actionable usage problem, not exit
|
|
28
|
+
* 70 (`TypeError`'s "unclassified internal error" code, which is what a
|
|
29
|
+
* `throw new TypeError(...)` here used to surface as). Anchors, explicit
|
|
30
|
+
* tags, and non-string mapping keys are recognized-but-unsupported
|
|
31
|
+
* constructs rather than parse failures on their own \u2014 an anchor nobody
|
|
32
|
+
* aliases, or a tag `toJS` converts to a plain value, is harmless \u2014 so they
|
|
33
|
+
* warn once (naming the file) and fall through to conversion instead of
|
|
34
|
+
* being pre-emptively rejected by a second, stricter gate in front of it.
|
|
35
|
+
* `toJS({ maxAliasCount: 0 })` is the real, UNCHANGED safety bound against
|
|
36
|
+
* alias-expansion abuse \u2014 it disables alias RESOLUTION outright, so a value
|
|
37
|
+
* that actually references an anchor via `*name` still fails, now as a
|
|
38
|
+
* {@link UsageError} from this same conversion step rather than the removed
|
|
39
|
+
* duplicate pre-check.
|
|
40
|
+
*/
|
|
41
|
+
export function parseExecutionMarkdown(raw, filePath) {
|
|
18
42
|
if (typeof raw !== "string")
|
|
19
43
|
throw new TypeError("execution source raw content must be a string");
|
|
44
|
+
const where = filePath ? ` (${filePath})` : "";
|
|
20
45
|
const withoutBom = raw.startsWith("\uFEFF") ? raw.slice(1) : raw;
|
|
21
46
|
const opening = nextLine(withoutBom, 0);
|
|
22
47
|
if (opening.line !== "---")
|
|
23
48
|
return { content: withoutBom, data: Object.freeze({}) };
|
|
24
49
|
if (opening.next === withoutBom.length) {
|
|
25
|
-
throw new
|
|
50
|
+
throw new UsageError(`execution source${where} has unterminated frontmatter`);
|
|
26
51
|
}
|
|
27
52
|
let cursor = opening.next;
|
|
28
53
|
let frontmatterEnd = -1;
|
|
@@ -40,18 +65,18 @@ export function parseExecutionMarkdown(raw) {
|
|
|
40
65
|
cursor = current.next;
|
|
41
66
|
}
|
|
42
67
|
if (frontmatterEnd < 0 || bodyStart < 0) {
|
|
43
|
-
throw new
|
|
68
|
+
throw new UsageError(`execution source${where} has unterminated frontmatter`);
|
|
44
69
|
}
|
|
45
70
|
const yaml = withoutBom.slice(opening.next, frontmatterEnd);
|
|
46
71
|
const document = parseDocument(yaml, { uniqueKeys: true });
|
|
47
72
|
if (document.errors.length > 0) {
|
|
48
|
-
throw new
|
|
73
|
+
throw new UsageError(`execution source${where} has invalid YAML frontmatter: ${document.errors[0]?.message ?? "parse error"}`);
|
|
49
74
|
}
|
|
50
75
|
if (document.warnings.length > 0) {
|
|
51
|
-
|
|
76
|
+
warnOnce(`execution-source:yaml-warning:${filePath ?? "<inline>"}`, `execution source${where} YAML frontmatter uses an unsupported tag or construct: ${document.warnings[0]?.message}. Using it as parsed.`);
|
|
52
77
|
}
|
|
53
78
|
if (!isMap(document.contents)) {
|
|
54
|
-
throw new
|
|
79
|
+
throw new UsageError(`execution source${where} YAML frontmatter must be a mapping`);
|
|
55
80
|
}
|
|
56
81
|
let unsupported;
|
|
57
82
|
visit(document, {
|
|
@@ -69,14 +94,16 @@ export function parseExecutionMarkdown(raw) {
|
|
|
69
94
|
unsupported ??= "non-string mapping keys";
|
|
70
95
|
},
|
|
71
96
|
});
|
|
72
|
-
if (unsupported)
|
|
73
|
-
|
|
97
|
+
if (unsupported) {
|
|
98
|
+
warnOnce(`execution-source:unsupported:${filePath ?? "<inline>"}`, `execution source${where} YAML frontmatter uses ${unsupported}, which akm does not fully support; using the bounded conversion's result as-is.`);
|
|
99
|
+
}
|
|
74
100
|
let root;
|
|
75
101
|
try {
|
|
76
102
|
root = document.toJS({ maxAliasCount: 0 });
|
|
77
103
|
}
|
|
78
104
|
catch (cause) {
|
|
79
|
-
|
|
105
|
+
const detail = cause instanceof Error ? cause.message : String(cause);
|
|
106
|
+
throw new UsageError(`execution source${where} YAML frontmatter could not be converted safely: ${detail}`);
|
|
80
107
|
}
|
|
81
108
|
const data = cloneExecutionJsonObject(root, "execution source YAML frontmatter");
|
|
82
109
|
return { content: withoutBom.slice(bodyStart), data: Object.freeze(data) };
|
|
@@ -279,7 +306,8 @@ export function renderMarkdownExecutionSource(input) {
|
|
|
279
306
|
const kind = snapshots.input.kind;
|
|
280
307
|
if (kind !== "command" && kind !== "persona")
|
|
281
308
|
throw new TypeError("adapter execution source.kind is invalid");
|
|
282
|
-
const
|
|
309
|
+
const identityFile = snapshots.identity.file;
|
|
310
|
+
const parsed = parseExecutionMarkdown(raw, typeof identityFile === "string" ? identityFile : undefined);
|
|
283
311
|
const hasDefaults = Object.hasOwn(snapshots.input, "defaults");
|
|
284
312
|
const defaultsProjection = snapshots.input.defaults;
|
|
285
313
|
const defaults = typeof defaultsProjection === "function" ? defaultsProjection(parsed.data) : defaultsProjection;
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
*/
|
|
19
19
|
import fs from "node:fs";
|
|
20
20
|
import path from "node:path";
|
|
21
|
+
import { isWithin } from "../common.js";
|
|
21
22
|
import { UsageError } from "../errors.js";
|
|
22
23
|
/** Root-relative directory holding a stash's meta docs. */
|
|
23
24
|
export const META_DIR = ".meta";
|
|
@@ -113,7 +114,7 @@ export function readMetaFile(sourceRoot, name) {
|
|
|
113
114
|
return null;
|
|
114
115
|
let fd;
|
|
115
116
|
try {
|
|
116
|
-
fd = fs.openSync(filePath, fs.constants.O_RDONLY
|
|
117
|
+
fd = fs.openSync(filePath, fs.constants.O_RDONLY);
|
|
117
118
|
const opened = fs.fstatSync(fd);
|
|
118
119
|
if (!opened.isFile())
|
|
119
120
|
throw metaPathEscape();
|
|
@@ -141,60 +142,25 @@ export function readMetaFile(sourceRoot, name) {
|
|
|
141
142
|
}
|
|
142
143
|
}
|
|
143
144
|
function isSafeRegularMetaFile(sourceRoot, metaRoot, filePath) {
|
|
144
|
-
const sourceRootReal = realPathOrNull(sourceRoot);
|
|
145
|
-
if (!sourceRootReal)
|
|
146
|
-
return false;
|
|
147
|
-
const metaStat = lstatOrNull(metaRoot);
|
|
148
|
-
if (!metaStat)
|
|
149
|
-
return false;
|
|
150
|
-
if (metaStat.isSymbolicLink())
|
|
151
|
-
throw metaPathEscape();
|
|
152
|
-
if (!metaStat.isDirectory())
|
|
153
|
-
return false;
|
|
154
145
|
const relative = path.relative(metaRoot, filePath);
|
|
155
146
|
if (relative === "" || relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
|
|
156
147
|
throw metaPathEscape();
|
|
157
148
|
}
|
|
158
|
-
|
|
159
|
-
for (const segment of relative.split(path.sep)) {
|
|
160
|
-
current = path.join(current, segment);
|
|
161
|
-
const stat = lstatOrNull(current);
|
|
162
|
-
if (!stat)
|
|
163
|
-
return false;
|
|
164
|
-
if (stat.isSymbolicLink())
|
|
165
|
-
throw metaPathEscape();
|
|
166
|
-
}
|
|
167
|
-
const finalStat = lstatOrNull(filePath);
|
|
149
|
+
const finalStat = statOrNull(filePath);
|
|
168
150
|
if (!finalStat?.isFile())
|
|
169
151
|
return false;
|
|
170
|
-
|
|
171
|
-
const fileReal = realPathOrNull(filePath);
|
|
172
|
-
if (!metaRootReal || !fileReal)
|
|
173
|
-
return false;
|
|
174
|
-
if (!isWithin(sourceRootReal, metaRootReal) || !isWithin(metaRootReal, fileReal))
|
|
152
|
+
if (!isWithin(filePath, sourceRoot))
|
|
175
153
|
throw metaPathEscape();
|
|
176
154
|
return true;
|
|
177
155
|
}
|
|
178
|
-
function
|
|
156
|
+
function statOrNull(filePath) {
|
|
179
157
|
try {
|
|
180
|
-
return fs.
|
|
158
|
+
return fs.statSync(filePath);
|
|
181
159
|
}
|
|
182
160
|
catch {
|
|
183
161
|
return null;
|
|
184
162
|
}
|
|
185
163
|
}
|
|
186
|
-
function realPathOrNull(filePath) {
|
|
187
|
-
try {
|
|
188
|
-
return fs.realpathSync(filePath);
|
|
189
|
-
}
|
|
190
|
-
catch {
|
|
191
|
-
return null;
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
function isWithin(root, candidate) {
|
|
195
|
-
const relative = path.relative(root, candidate);
|
|
196
|
-
return relative === "" || (relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative));
|
|
197
|
-
}
|
|
198
164
|
function metaPathEscape() {
|
|
199
|
-
return new UsageError("Meta doc must
|
|
165
|
+
return new UsageError("Meta doc must resolve to a regular file inside the stash .meta directory, not one that escapes it.", "PATH_ESCAPE_VIOLATION");
|
|
200
166
|
}
|
package/dist/core/common.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import crypto from "node:crypto";
|
|
5
5
|
import fs from "node:fs";
|
|
6
6
|
import path from "node:path";
|
|
7
|
+
import { migrateLegacySourceShape } from "./config/legacy-source-shape-shim.js";
|
|
7
8
|
import { ConfigError } from "./errors.js";
|
|
8
9
|
import { getConfigPath, getDefaultStashDir, getRegistryCacheDir, getRegistryIndexCacheDir } from "./paths.js";
|
|
9
10
|
// ── Constants ───────────────────────────────────────────────────────────────
|
|
@@ -302,23 +303,20 @@ function readStashDirFromConfig() {
|
|
|
302
303
|
// in use. Parsing it raw here threw, the catch swallowed it, and every
|
|
303
304
|
// caller silently fell back — operating on the wrong bundle or failing with
|
|
304
305
|
// STASH_DIR_NOT_FOUND despite a perfectly good config.
|
|
305
|
-
const
|
|
306
|
-
if (typeof
|
|
306
|
+
const parsed = JSON.parse(stripJsonComments(text));
|
|
307
|
+
if (typeof parsed !== "object" || parsed === null)
|
|
307
308
|
return undefined;
|
|
309
|
+
const raw = migrateLegacySourceShape(parsed, configPath);
|
|
308
310
|
// 0.9.0 config-shape cutover (spec §10.1): the primary stash is the
|
|
309
311
|
// `defaultBundle`'s filesystem `path`. Read it directly (no config module
|
|
310
312
|
// import) so the primary-stash location survives the stashDir → bundles
|
|
311
313
|
// migration without a runtime rewire.
|
|
312
314
|
const bundles = raw.bundles;
|
|
313
315
|
const defaultBundle = raw.defaultBundle;
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
typeof bundles[defaultBundle] === "object" &&
|
|
319
|
-
typeof bundles[defaultBundle].path === "string" &&
|
|
320
|
-
bundles[defaultBundle].path.trim()) {
|
|
321
|
-
const bundle = bundles[defaultBundle];
|
|
316
|
+
const selectedBundle = typeof defaultBundle === "string" ? bundles?.[defaultBundle] : undefined;
|
|
317
|
+
const selectedBundlePath = typeof selectedBundle?.path === "string" ? selectedBundle.path : undefined;
|
|
318
|
+
if (bundles && typeof bundles === "object" && typeof defaultBundle === "string" && selectedBundlePath?.trim()) {
|
|
319
|
+
const bundle = selectedBundle;
|
|
322
320
|
const bundlePath = bundle.path.trim();
|
|
323
321
|
if (bundle.components !== undefined) {
|
|
324
322
|
if (typeof bundle.components !== "object" || bundle.components === null) {
|
|
@@ -343,13 +341,6 @@ function readStashDirFromConfig() {
|
|
|
343
341
|
}
|
|
344
342
|
return bundlePath;
|
|
345
343
|
}
|
|
346
|
-
// Retired pre-cutover shapes are not runtime inputs. Refuse them instead
|
|
347
|
-
// of silently resolving old keys.
|
|
348
|
-
for (const key of ["stashDir", "sources", "installed"]) {
|
|
349
|
-
if (key in raw && raw[key] !== undefined) {
|
|
350
|
-
throw new ConfigError(`${key} is not supported; configure the current bundles shape`, "INVALID_CONFIG_FILE");
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
344
|
}
|
|
354
345
|
catch (err) {
|
|
355
346
|
// An unsupported-shape refusal must reach the caller; genuine missing/invalid
|
|
@@ -41,6 +41,7 @@
|
|
|
41
41
|
* enforced at save time via `superRefine` on the top-level schema.
|
|
42
42
|
*/
|
|
43
43
|
import { z } from "zod";
|
|
44
|
+
import { warnOnce } from "../warn.js";
|
|
44
45
|
import { BUILTIN_IMPROVE_STRATEGY_NAMES, IMPROVE_PROCESS_ENGINE_CAPABILITIES } from "./engine-semantics.js";
|
|
45
46
|
import { EmbeddingConnectionConfigSchema } from "./schema/embedding.js";
|
|
46
47
|
import { EnginesSchema } from "./schema/engines.js";
|
|
@@ -121,25 +122,11 @@ const RETIRED_SOURCE_SHAPE_KEY_MESSAGES = {
|
|
|
121
122
|
};
|
|
122
123
|
export const AkmConfigSchema = AkmConfigBaseSchema.superRefine((config, ctx) => {
|
|
123
124
|
const raw = config;
|
|
124
|
-
for (const key of ["profiles", "llm", "agent", "features", "stashes", "modelAliases"]) {
|
|
125
|
+
for (const key of ["profiles", "llm", "agent", "features", "stashes", "modelAliases", "bindings", "writable"]) {
|
|
125
126
|
if (key in raw) {
|
|
126
|
-
|
|
127
|
-
code: z.ZodIssueCode.custom,
|
|
128
|
-
path: [key],
|
|
129
|
-
message: `${key} is retired in 0.9; configure engines and improve.strategies instead`,
|
|
130
|
-
});
|
|
127
|
+
warnOnce(`config:retired-key:${key}`, `Config key "${key}" is retired in 0.9 and is ignored; configure engines/improve.strategies/bundles.<id> instead.`);
|
|
131
128
|
}
|
|
132
129
|
}
|
|
133
|
-
// `bindings` (spec §10.1) is Tier B — never emitted, never accepted. The
|
|
134
|
-
// top-level schema is `.passthrough()`, so without this it would round-trip
|
|
135
|
-
// silently; reject it loudly so a stray/hand-written bindings block is caught.
|
|
136
|
-
if ("bindings" in raw) {
|
|
137
|
-
ctx.addIssue({
|
|
138
|
-
code: z.ZodIssueCode.custom,
|
|
139
|
-
path: ["bindings"],
|
|
140
|
-
message: "bindings is not supported in 0.9.0 (Tier B); it is neither emitted nor accepted",
|
|
141
|
-
});
|
|
142
|
-
}
|
|
143
130
|
// Only the current source shape enters the runtime. There is no config
|
|
144
131
|
// compatibility path; `bundles` + `defaultBundle` fully supersede these keys.
|
|
145
132
|
for (const key of ["stashDir", "sources", "installed"]) {
|
|
@@ -151,13 +138,6 @@ export const AkmConfigSchema = AkmConfigBaseSchema.superRefine((config, ctx) =>
|
|
|
151
138
|
});
|
|
152
139
|
}
|
|
153
140
|
}
|
|
154
|
-
if ("writable" in raw) {
|
|
155
|
-
ctx.addIssue({
|
|
156
|
-
code: z.ZodIssueCode.custom,
|
|
157
|
-
path: ["writable"],
|
|
158
|
-
message: "top-level writable is not supported; configure bundles.<id>.writable instead",
|
|
159
|
-
});
|
|
160
|
-
}
|
|
161
141
|
// `defaultBundle`, when present, must name a configured bundle.
|
|
162
142
|
if (config.defaultBundle !== undefined) {
|
|
163
143
|
if (config.bundles === undefined) {
|
|
@@ -23,8 +23,11 @@
|
|
|
23
23
|
import { z } from "zod";
|
|
24
24
|
import { isRecord } from "../common.js";
|
|
25
25
|
import { UsageError } from "../errors.js";
|
|
26
|
+
import { hasRegistryUrlCredentials, REGISTRY_CREDENTIALS_UNSUPPORTED } from "../registry-url.js";
|
|
27
|
+
import { warnOnce } from "../warn.js";
|
|
26
28
|
import { AkmConfigBaseSchema, EngineConfigSchema, listTopLevelConfigKeys } from "./config-schema.js";
|
|
27
29
|
import { deepMergeConfig } from "./deep-merge.js";
|
|
30
|
+
import { isApiKeyReference } from "./schema/primitives.js";
|
|
28
31
|
/**
|
|
29
32
|
* Parse a dotted path into segments. Empty segments are rejected. Bracket
|
|
30
33
|
* notation (e.g. `sources[0]`) is NOT supported — arrays are set as JSON.
|
|
@@ -175,11 +178,13 @@ export function configSet(config, dotted, raw) {
|
|
|
175
178
|
}
|
|
176
179
|
// #454: apiKey paths are not persistable. Throw at set time.
|
|
177
180
|
rejectApiKeyPath(path, dotted);
|
|
181
|
+
rejectLiteralApiKeyInWholeObjectSet(path, raw, dotted);
|
|
178
182
|
const schema = resolveSchemaAt(path, config, raw);
|
|
179
183
|
const symbolicApiKey = (path[0] === "engines" && path[2] === "apiKey") ||
|
|
180
184
|
(path[0] === "embedding" && path.length === 2 && path[1] === "apiKey");
|
|
181
|
-
|
|
182
|
-
|
|
185
|
+
const isUnknownKey = !schema && !symbolicApiKey;
|
|
186
|
+
if (isUnknownKey) {
|
|
187
|
+
warnOnce(`config-set:unknown-key:${dotted}`, `"${dotted}" is not a known config key; storing it anyway. Run \`akm config get ${dotted}\` to confirm it round-trips as expected.`);
|
|
183
188
|
}
|
|
184
189
|
const judgmentObjectPath = isTriageJudgmentPath(path);
|
|
185
190
|
const value = judgmentObjectPath && raw === "null"
|
|
@@ -188,7 +193,11 @@ export function configSet(config, dotted, raw) {
|
|
|
188
193
|
? parseObjectPatch(raw, dotted)
|
|
189
194
|
: symbolicApiKey
|
|
190
195
|
? raw
|
|
191
|
-
:
|
|
196
|
+
: isUnknownKey
|
|
197
|
+
? bestEffortJsonValue(raw)
|
|
198
|
+
: coerceForSchema(schema, raw, dotted);
|
|
199
|
+
if (path[0] === "registries")
|
|
200
|
+
rejectRegistryCredentialValue(value, dotted);
|
|
192
201
|
const existing = path.reduce((value, key) => {
|
|
193
202
|
if (value && typeof value === "object")
|
|
194
203
|
return value[key];
|
|
@@ -206,10 +215,15 @@ export function configSet(config, dotted, raw) {
|
|
|
206
215
|
const parsed = path[0] === "engines" && path.length === 2
|
|
207
216
|
? EngineConfigSchema.safeParse(value)
|
|
208
217
|
: symbolicApiKey
|
|
209
|
-
?
|
|
218
|
+
? isApiKeyReference(raw)
|
|
219
|
+
? { success: true, data: value }
|
|
220
|
+
: {
|
|
221
|
+
success: false,
|
|
222
|
+
error: { issues: [{ path: [], message: "apiKey must be $VAR, ${VAR}, or secret://<name>" }] },
|
|
223
|
+
}
|
|
224
|
+
: isUnknownKey
|
|
210
225
|
? { success: true, data: value }
|
|
211
|
-
:
|
|
212
|
-
: schema.safeParse(candidate);
|
|
226
|
+
: schema.safeParse(candidate);
|
|
213
227
|
if (!parsed.success) {
|
|
214
228
|
const lines = parsed.error.issues
|
|
215
229
|
.map((i) => {
|
|
@@ -273,6 +287,42 @@ function rejectApiKeyPath(path, dotted) {
|
|
|
273
287
|
throw new UsageError(`apiKey cannot be persisted in config; export ${recipe} instead. (key: ${dotted})`, "INVALID_FLAG_VALUE", "Storing API keys in config.json leaks them through backups, logs, and version control. " +
|
|
274
288
|
"Use the corresponding environment variable. AKM reads it at request time.");
|
|
275
289
|
}
|
|
290
|
+
function rejectLiteralApiKeyInWholeObjectSet(path, raw, dotted) {
|
|
291
|
+
const isWholeEngineSet = path[0] === "engines" && path.length === 2;
|
|
292
|
+
const isWholeEmbeddingSet = path.length === 1 && path[0] === "embedding";
|
|
293
|
+
if (!isWholeEngineSet && !isWholeEmbeddingSet)
|
|
294
|
+
return;
|
|
295
|
+
let parsed;
|
|
296
|
+
try {
|
|
297
|
+
parsed = JSON.parse(raw);
|
|
298
|
+
}
|
|
299
|
+
catch {
|
|
300
|
+
return; // Malformed JSON — the caller's own parse/coercion reports this.
|
|
301
|
+
}
|
|
302
|
+
if (!isRecord(parsed) || typeof parsed.apiKey !== "string")
|
|
303
|
+
return;
|
|
304
|
+
if (isApiKeyReference(parsed.apiKey))
|
|
305
|
+
return;
|
|
306
|
+
throw new UsageError(`apiKey cannot be persisted in config; export ${recipeForApiKey([...path, "apiKey"], `${dotted}.apiKey`)} instead. (key: ${dotted}.apiKey)`, "INVALID_FLAG_VALUE", "Storing API keys in config.json leaks them through backups, logs, and version control. " +
|
|
307
|
+
"Use the corresponding environment variable. AKM reads it at request time.");
|
|
308
|
+
}
|
|
309
|
+
function rejectRegistryCredentialValue(value, dotted) {
|
|
310
|
+
const entries = Array.isArray(value) ? value : [value];
|
|
311
|
+
for (const entry of entries) {
|
|
312
|
+
const url = isRecord(entry) && typeof entry.url === "string" ? entry.url : undefined;
|
|
313
|
+
if (url && hasRegistryUrlCredentials(url)) {
|
|
314
|
+
throw new UsageError(`${REGISTRY_CREDENTIALS_UNSUPPORTED} (key: ${dotted})`, "INVALID_FLAG_VALUE", REGISTRY_CREDENTIALS_UNSUPPORTED);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
function bestEffortJsonValue(raw) {
|
|
319
|
+
try {
|
|
320
|
+
return JSON.parse(raw);
|
|
321
|
+
}
|
|
322
|
+
catch {
|
|
323
|
+
return raw;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
276
326
|
function parseObjectPatch(raw, key) {
|
|
277
327
|
try {
|
|
278
328
|
const value = JSON.parse(raw);
|
|
@@ -5,14 +5,17 @@ import fs from "node:fs";
|
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import { ConfigError } from "../errors.js";
|
|
7
7
|
import { liftLegacyEngineExtraParams } from "../extra-params.js";
|
|
8
|
+
import { formatRegistryLabel, hasRegistryUrlCredentials } from "../registry-url.js";
|
|
8
9
|
import { acquireConfigLock, backupExistingConfig, parseConfigText, readConfigText, withConfigLock, writeConfigAtomic, } from "./config-io.js";
|
|
9
10
|
import { AkmConfigSchema, CURRENT_CONFIG_VERSION } from "./config-schema.js";
|
|
10
11
|
import { bundlesToSourceEntries } from "./config-sources.js";
|
|
11
12
|
import { upgradeConfigVersion } from "./config-version-shim.js";
|
|
12
13
|
import { deepMergeConfig } from "./deep-merge.js";
|
|
14
|
+
import { migrateLegacySourceShape } from "./legacy-source-shape-shim.js";
|
|
15
|
+
import { isApiKeyReference, SECRET_STORE_REFERENCE_PATTERN } from "./schema/primitives.js";
|
|
13
16
|
export { stripJsonComments } from "./config-io.js";
|
|
14
17
|
import { getConfigPath } from "../paths.js";
|
|
15
|
-
import { warn } from "../warn.js";
|
|
18
|
+
import { warn, warnOnce } from "../warn.js";
|
|
16
19
|
// Canonical harness-id source of truth (#565) — runtime value re-export.
|
|
17
20
|
export { VALID_HARNESS_IDS } from "./config-types.js";
|
|
18
21
|
// ── Feedback failure-mode constants (F-3 / #384) ────────────────────────────
|
|
@@ -145,7 +148,8 @@ export function acquireConfigReadFence() {
|
|
|
145
148
|
* is validated.
|
|
146
149
|
*/
|
|
147
150
|
export function parseAndValidateConfigText(text, sourcePath) {
|
|
148
|
-
const
|
|
151
|
+
const versioned = upgradeConfigVersion(parseConfigText(text, sourcePath), sourcePath);
|
|
152
|
+
const parsedRaw = migrateLegacySourceShape(versioned, sourcePath);
|
|
149
153
|
// #852 (following #815): a config still using legacy `extraParams` keys —
|
|
150
154
|
// e.g. `reasoning_effort`, a documented 0.9.1 workaround — needs to be
|
|
151
155
|
// rewritten onto the first-class engine field they now shadow. This used
|
|
@@ -155,7 +159,7 @@ export function parseAndValidateConfigText(text, sourcePath) {
|
|
|
155
159
|
// config that has not been migrated yet fails closed here instead of
|
|
156
160
|
// silently drifting from what's on disk.
|
|
157
161
|
const where = sourcePath ? ` at ${sourcePath}` : "";
|
|
158
|
-
const { lifted, conflicts } = liftLegacyEngineExtraParams(parsedRaw);
|
|
162
|
+
const { config: liftedConfig, lifted, conflicts } = liftLegacyEngineExtraParams(parsedRaw);
|
|
159
163
|
if (conflicts.length > 0) {
|
|
160
164
|
const lines = conflicts
|
|
161
165
|
.map((c) => ` - engines.${c.engine}.extraParams.${c.key} (${JSON.stringify(c.extraParamsValue)}) conflicts with engines.${c.engine}.${c.field} (${JSON.stringify(c.fieldValue)})`)
|
|
@@ -163,9 +167,9 @@ export function parseAndValidateConfigText(text, sourcePath) {
|
|
|
163
167
|
throw new ConfigError(`Invalid config${where}: extraParams and the first-class field disagree:\n${lines}\n\nEach extraParams key above has a first-class equivalent and akm will not guess which value you meant — remove the extraParams entry once the field carries the value you want.`, "INVALID_CONFIG_FILE");
|
|
164
168
|
}
|
|
165
169
|
if (lifted.length > 0) {
|
|
166
|
-
|
|
170
|
+
warnOnce(`config:extra-params-lift${sourcePath ? `:${sourcePath}` : ""}`, `Config${where} uses deprecated extraParams keys with first-class equivalents — auto-lifted in memory:\n - ${lifted.join("\n - ")}\n\nRun \`akm migrate apply\` to rewrite the config file and silence this warning.`);
|
|
167
171
|
}
|
|
168
|
-
const parsed = AkmConfigSchema.safeParse(
|
|
172
|
+
const parsed = AkmConfigSchema.safeParse(liftedConfig);
|
|
169
173
|
if (!parsed.success) {
|
|
170
174
|
const lines = parsed.error.issues.map((i) => ` - ${i.path.join(".") || "(root)"}: ${i.message}`).join("\n");
|
|
171
175
|
throw new ConfigError(`Invalid config${where}:\n${lines}`, "INVALID_CONFIG_FILE");
|
|
@@ -296,7 +300,7 @@ export function sanitizeConfigForWrite(config) {
|
|
|
296
300
|
const stripped = [];
|
|
297
301
|
if (config.embedding?.apiKey !== undefined) {
|
|
298
302
|
const apiKey = config.embedding.apiKey;
|
|
299
|
-
if (
|
|
303
|
+
if (isApiKeyReference(apiKey)) {
|
|
300
304
|
// Preserve reference verbatim — not a secret.
|
|
301
305
|
sanitized.embedding = { ...config.embedding };
|
|
302
306
|
}
|
|
@@ -313,7 +317,7 @@ export function sanitizeConfigForWrite(config) {
|
|
|
313
317
|
if (config.engines) {
|
|
314
318
|
const engines = {};
|
|
315
319
|
for (const [name, engine] of Object.entries(config.engines)) {
|
|
316
|
-
if (engine.kind !== "llm" || engine.apiKey === undefined ||
|
|
320
|
+
if (engine.kind !== "llm" || engine.apiKey === undefined || isApiKeyReference(engine.apiKey)) {
|
|
317
321
|
engines[name] = { ...engine };
|
|
318
322
|
continue;
|
|
319
323
|
}
|
|
@@ -327,21 +331,30 @@ export function sanitizeConfigForWrite(config) {
|
|
|
327
331
|
if (stripped.length > 0) {
|
|
328
332
|
warn(`Config sanitizer dropped API key(s) before writing to disk:\n - ${stripped.join("\n - ")}\n\nakm does not persist API keys to config.json. Set the listed environment variables to provide them at runtime, or use \`\${VAR}\` references in your config to defer lookup. See docs/reference/data-and-telemetry.md.`);
|
|
329
333
|
}
|
|
334
|
+
if (config.registries) {
|
|
335
|
+
const droppedRegistries = [];
|
|
336
|
+
const registries = config.registries.filter((entry) => {
|
|
337
|
+
if (!hasRegistryUrlCredentials(entry.url))
|
|
338
|
+
return true;
|
|
339
|
+
droppedRegistries.push(formatRegistryLabel(entry));
|
|
340
|
+
return false;
|
|
341
|
+
});
|
|
342
|
+
if (droppedRegistries.length > 0) {
|
|
343
|
+
sanitized.registries = registries;
|
|
344
|
+
warn(`Config sanitizer dropped registry entr${droppedRegistries.length === 1 ? "y" : "ies"} with URL credentials before writing to disk:\n - ${droppedRegistries.join("\n - ")}\n\nRegistry URLs must be credential-free; configure a credential-free HTTPS endpoint.`);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
330
347
|
return sanitized;
|
|
331
348
|
}
|
|
332
|
-
/** Matches the only 0.9 symbolic secret forms: `${VAR}` or `$VAR`. */
|
|
333
|
-
function isEnvReference(value) {
|
|
334
|
-
return /^\$\{[A-Za-z_][A-Za-z0-9_]*\}$|^\$[A-Za-z_][A-Za-z0-9_]*$/.test(value);
|
|
335
|
-
}
|
|
336
349
|
export function updateConfig(partial) {
|
|
337
350
|
return mutateConfig((current) => deepMergeConfig(current, partial)).config;
|
|
338
351
|
}
|
|
339
|
-
// ── Helpers ─────────────────────────────────────────────────────────────────
|
|
340
352
|
/**
|
|
341
|
-
* Resolve a single secret value
|
|
342
|
-
*
|
|
343
|
-
* authorization-header consumption sites (LLM client, embedder,
|
|
344
|
-
* runner) — NOT on the load path. Non-string inputs pass through
|
|
353
|
+
* Resolve a single secret value: expand `${VAR}` / `$VAR` against
|
|
354
|
+
* `process.env`, or look up `secret://<name>` via `resolveFromStore`. Use this
|
|
355
|
+
* at apiKey / authorization-header consumption sites (LLM client, embedder,
|
|
356
|
+
* agent SDK runner) — NOT on the load path. Non-string inputs pass through
|
|
357
|
+
* unchanged.
|
|
345
358
|
*
|
|
346
359
|
* Returns the input unchanged when no substitution markers are present, so
|
|
347
360
|
* literal API key strings (already-resolved secrets) are zero-cost.
|
|
@@ -349,12 +362,24 @@ export function updateConfig(partial) {
|
|
|
349
362
|
* Other config string values (URLs, endpoints, model names, prompts) are
|
|
350
363
|
* preserved verbatim on read — only fields explicitly routed through this
|
|
351
364
|
* helper are expanded.
|
|
365
|
+
*
|
|
366
|
+
* A `secret://<name>` value that fails to resolve throws `ConfigError`
|
|
367
|
+
* (naming the ref, never the secret) rather than silently sending an unusable
|
|
368
|
+
* credential.
|
|
352
369
|
*/
|
|
353
|
-
export function resolveSecret(value) {
|
|
370
|
+
export function resolveSecret(value, resolveFromStore) {
|
|
354
371
|
if (value === undefined)
|
|
355
372
|
return undefined;
|
|
356
373
|
if (typeof value !== "string")
|
|
357
374
|
return value;
|
|
375
|
+
const storeRef = SECRET_STORE_REFERENCE_PATTERN.exec(value)?.[1];
|
|
376
|
+
if (storeRef !== undefined) {
|
|
377
|
+
const resolved = resolveFromStore?.(storeRef) ?? null;
|
|
378
|
+
if (resolved === null) {
|
|
379
|
+
throw new ConfigError(`Secret store reference "${value}" did not resolve to a stored value.`, "SECRET_REFERENCE_UNRESOLVED");
|
|
380
|
+
}
|
|
381
|
+
return resolved;
|
|
382
|
+
}
|
|
358
383
|
if (!value.includes("$"))
|
|
359
384
|
return value;
|
|
360
385
|
return value.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)\}|\$([A-Za-z_][A-Za-z0-9_]*)/g, (_match, braced, bare) => {
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { isBundleSlug } from "../asset/asset-ref.js";
|
|
2
|
+
import { warnOnce } from "../warn.js";
|
|
3
|
+
function isPlainRecord(value) {
|
|
4
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
5
|
+
}
|
|
6
|
+
const DEFAULT_WRITABLE_BY_TYPE = {
|
|
7
|
+
filesystem: true,
|
|
8
|
+
git: false,
|
|
9
|
+
website: false,
|
|
10
|
+
npm: false,
|
|
11
|
+
};
|
|
12
|
+
function bundleFromLegacySource(entry, index) {
|
|
13
|
+
if (!isPlainRecord(entry))
|
|
14
|
+
return undefined;
|
|
15
|
+
const type = typeof entry.type === "string" ? entry.type : undefined;
|
|
16
|
+
const bundle = {};
|
|
17
|
+
switch (type) {
|
|
18
|
+
case "filesystem":
|
|
19
|
+
if (typeof entry.path !== "string" || !entry.path)
|
|
20
|
+
return undefined;
|
|
21
|
+
bundle.path = entry.path;
|
|
22
|
+
break;
|
|
23
|
+
case "git":
|
|
24
|
+
if (typeof entry.url !== "string" || !entry.url)
|
|
25
|
+
return undefined;
|
|
26
|
+
bundle.git = entry.url;
|
|
27
|
+
break;
|
|
28
|
+
case "website":
|
|
29
|
+
if (typeof entry.url !== "string" || !entry.url)
|
|
30
|
+
return undefined;
|
|
31
|
+
bundle.website = { url: entry.url };
|
|
32
|
+
break;
|
|
33
|
+
case "npm": {
|
|
34
|
+
const spec = typeof entry.url === "string" && entry.url ? entry.url : entry.path;
|
|
35
|
+
if (typeof spec !== "string" || !spec)
|
|
36
|
+
return undefined;
|
|
37
|
+
bundle.npm = spec;
|
|
38
|
+
break;
|
|
39
|
+
}
|
|
40
|
+
default:
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
43
|
+
const writable = typeof entry.writable === "boolean" ? entry.writable : DEFAULT_WRITABLE_BY_TYPE[type ?? ""];
|
|
44
|
+
if (writable !== undefined)
|
|
45
|
+
bundle.writable = writable;
|
|
46
|
+
if (typeof entry.enabled === "boolean")
|
|
47
|
+
bundle.enabled = entry.enabled;
|
|
48
|
+
const name = typeof entry.name === "string" ? entry.name : undefined;
|
|
49
|
+
const key = name && isBundleSlug(name) ? name : `source-${index + 1}`;
|
|
50
|
+
return [key, bundle];
|
|
51
|
+
}
|
|
52
|
+
export function migrateLegacySourceShape(raw, sourcePath) {
|
|
53
|
+
const hasStashDir = typeof raw.stashDir === "string" && raw.stashDir.trim().length > 0;
|
|
54
|
+
const hasSources = Array.isArray(raw.sources) && raw.sources.length > 0;
|
|
55
|
+
const hasInstalled = "installed" in raw && raw.installed !== undefined;
|
|
56
|
+
if (!hasStashDir && !hasSources && !hasInstalled)
|
|
57
|
+
return raw;
|
|
58
|
+
const { stashDir: _stashDir, sources: _sources, installed: _installed, ...rest } = raw;
|
|
59
|
+
const bundles = isPlainRecord(rest.bundles) ? { ...rest.bundles } : {};
|
|
60
|
+
let defaultBundle = typeof rest.defaultBundle === "string" ? rest.defaultBundle : undefined;
|
|
61
|
+
if (hasStashDir) {
|
|
62
|
+
bundles.stash = { path: raw.stashDir, writable: true };
|
|
63
|
+
defaultBundle ??= "stash";
|
|
64
|
+
}
|
|
65
|
+
if (hasSources) {
|
|
66
|
+
raw.sources.forEach((entry, index) => {
|
|
67
|
+
const converted = bundleFromLegacySource(entry, index);
|
|
68
|
+
if (!converted)
|
|
69
|
+
return;
|
|
70
|
+
const [key, bundle] = converted;
|
|
71
|
+
bundles[key] = bundle;
|
|
72
|
+
defaultBundle ??= key;
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
const droppedKeys = [hasStashDir && "stashDir", hasSources && "sources", hasInstalled && "installed"].filter(Boolean);
|
|
76
|
+
const where = sourcePath ? ` at ${sourcePath}` : "";
|
|
77
|
+
warnOnce(`legacy-source-shape${sourcePath ? `:${sourcePath}` : ""}`, `Config${where} uses the retired ${droppedKeys.join("/")} shape — auto-migrated in memory to \`bundles\`/\`defaultBundle\`. Run \`akm migrate apply\` to rewrite the config file and silence this warning.`);
|
|
78
|
+
return { ...rest, bundles, ...(defaultBundle !== undefined ? { defaultBundle } : {}) };
|
|
79
|
+
}
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* `config-schema.ts` monolith — no behavior change.
|
|
7
7
|
*/
|
|
8
8
|
import { z } from "zod";
|
|
9
|
-
import {
|
|
9
|
+
import { positiveInt, symbolicOrWarnApiKey } from "./primitives.js";
|
|
10
10
|
const EmbeddingOllamaOptionsSchema = z
|
|
11
11
|
.object({
|
|
12
12
|
num_ctx: positiveInt.optional(),
|
|
@@ -26,7 +26,7 @@ export const EmbeddingConnectionConfigSchema = z
|
|
|
26
26
|
provider: z.string().optional(),
|
|
27
27
|
endpoint: z.string().optional(),
|
|
28
28
|
model: z.string().optional(),
|
|
29
|
-
apiKey:
|
|
29
|
+
apiKey: symbolicOrWarnApiKey("embedding.apiKey").optional(),
|
|
30
30
|
// Bounded to the index schema's own vec-table guard (1–4096,
|
|
31
31
|
// storage/repositories/index-schema.ts) so an out-of-range dimension
|
|
32
32
|
// fails at config validation with a clear message instead of crashing
|
|
@@ -15,7 +15,7 @@ import { z } from "zod";
|
|
|
15
15
|
// a config-schema ↔ config-types type cycle that collapses inference.
|
|
16
16
|
import { HARNESS_AGENT_DISPATCH_IDS, VALID_HARNESS_IDS } from "../../../integrations/harnesses/ids.js";
|
|
17
17
|
import { WORKFLOW_MAX_TIMEOUT_MS } from "../../../workflows/resource-limits.js";
|
|
18
|
-
import { chatCompletionsEndpoint,
|
|
18
|
+
import { chatCompletionsEndpoint, ExtraParamsSchema, engineName, nonEmptyString, positiveInt, symbolicOrWarnApiKey, } from "./primitives.js";
|
|
19
19
|
/**
|
|
20
20
|
* Engine-config timeouts share the workflow ceiling.
|
|
21
21
|
*
|
|
@@ -67,7 +67,7 @@ const LlmEngineSchema = z
|
|
|
67
67
|
provider: z.string().optional(),
|
|
68
68
|
endpoint: chatCompletionsEndpoint,
|
|
69
69
|
model: nonEmptyString,
|
|
70
|
-
apiKey:
|
|
70
|
+
apiKey: symbolicOrWarnApiKey("engines.<name>.apiKey").optional(),
|
|
71
71
|
// #905: file-backed alternative to `apiKey` for hosts that refuse
|
|
72
72
|
// secrets in the process environment. A plain filesystem path (`~`
|
|
73
73
|
// expanded, read at dispatch) — see resolveLlmEngineUse/
|