akm-cli 0.9.14 → 0.9.15-beta.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 +559 -0
- package/STABILITY.md +6 -3
- package/dist/akm +54 -1
- package/dist/akm-migrate +34 -1
- package/dist/assets/prompts/reflect-feedback-framing.md +1 -0
- package/dist/assets/prompts/reflect-llm-framed-contract.md +2 -0
- package/dist/assets/prompts/reflect-llm-schema-contract.md +2 -0
- package/dist/assets/tasks/core/improve.yml +1 -1
- package/dist/assets/tasks/core/index-refresh.yml +1 -1
- package/dist/assets/tasks/improve/akm-graph-refresh-weekly.yml +1 -1
- package/dist/assets/tasks/improve/akm-improve-catchup.yml +1 -1
- package/dist/assets/tasks/improve/akm-improve-consolidate.yml +1 -1
- package/dist/assets/tasks/improve/akm-improve-frequent.yml +1 -1
- package/dist/assets/tasks/improve/akm-improve-nightly.yml +1 -1
- package/dist/cli/retired-commands.js +0 -1
- package/dist/cli/shared.js +9 -0
- package/dist/cli/unknown-flags.js +1 -0
- package/dist/cli.js +40 -3
- package/dist/commands/config-cli.js +85 -3
- package/dist/commands/env/env-cli.js +1 -42
- package/dist/commands/env/env.js +1 -1
- package/dist/commands/env/secret-cli.js +1 -2
- package/dist/commands/health/checks.js +357 -63
- package/dist/commands/health/engine-usage.js +45 -0
- package/dist/commands/health/improve-metrics.js +18 -0
- package/dist/commands/health/llm-usage.js +41 -1
- package/dist/commands/health/plugin-staleness.js +7 -3
- package/dist/commands/health/version-drift.js +93 -0
- package/dist/commands/health/windows.js +3 -1
- package/dist/commands/health.js +44 -9
- package/dist/commands/improve/consolidate/chunking.js +4 -2
- package/dist/commands/improve/improve-cli.js +99 -5
- package/dist/commands/improve/improve-report.js +154 -0
- package/dist/commands/improve/improve-result-file.js +45 -33
- package/dist/commands/improve/improve-strategies.js +133 -3
- package/dist/commands/improve/improve-usage-report.js +182 -0
- package/dist/commands/improve/improve.js +40 -3
- package/dist/commands/improve/locks.js +28 -78
- package/dist/commands/improve/planner.js +1 -0
- package/dist/commands/improve/preparation.js +9 -1
- package/dist/commands/improve/reflect.js +44 -4
- package/dist/commands/models-cli.js +50 -1
- package/dist/commands/proposal/repository.js +8 -3
- package/dist/commands/proposal/validators/proposal-quality-validators.js +41 -6
- package/dist/commands/proposal/validators/proposal-validators.js +24 -0
- package/dist/commands/read/search-cli.js +38 -2
- package/dist/commands/read/show.js +103 -4
- package/dist/commands/sources/info.js +5 -1
- package/dist/commands/sources/installed-stashes.js +58 -16
- package/dist/commands/sources/self-update.js +2 -2
- package/dist/commands/sources/stash-cli.js +48 -0
- package/dist/commands/tasks/tasks-cli.js +49 -2
- package/dist/commands/workflow-cli.js +86 -12
- package/dist/core/asset/markdown-fragments.js +35 -0
- package/dist/core/config/config-schema.js +14 -0
- package/dist/core/config/config.js +302 -24
- package/dist/core/config/schema/embedding.js +41 -0
- package/dist/core/env-secret-ref.js +58 -5
- package/dist/core/errors.js +30 -0
- package/dist/core/file-lock.js +49 -15
- package/dist/core/improve-result.js +51 -0
- package/dist/core/loopback.js +17 -0
- package/dist/core/parent-watchdog.js +64 -0
- package/dist/core/paths.js +11 -0
- package/dist/core/run-lock.js +107 -0
- package/dist/core/sensitive-marker-path.js +19 -0
- package/dist/core/state-db.js +74 -14
- package/dist/indexer/index-rebuild-lock.js +73 -0
- package/dist/indexer/index-writer-lock.js +40 -1
- package/dist/indexer/index-written-assets.js +29 -1
- package/dist/indexer/indexer.js +93 -29
- package/dist/indexer/materialize-embeddings.js +564 -48
- package/dist/indexer/search/db-search.js +49 -2
- package/dist/indexer/search/search-source.js +23 -1
- package/dist/integrations/agent/engine-resolution.js +96 -6
- package/dist/integrations/agent/execution-definitions.js +6 -15
- package/dist/integrations/agent/execution-lowering.js +6 -1
- package/dist/integrations/agent/execution-preparation.js +1 -1
- package/dist/integrations/agent/model-map.js +123 -20
- package/dist/integrations/agent/prompts.js +40 -8
- package/dist/integrations/agent/runner-dispatch.js +9 -3
- package/dist/integrations/agent/runner.js +2 -0
- package/dist/llm/client.js +8 -3
- package/dist/llm/embedder.js +20 -8
- package/dist/llm/embedders/local.js +10 -2
- package/dist/llm/embedders/remote.js +497 -32
- package/dist/output/shapes/helpers.js +38 -2
- package/dist/output/shapes/models-list.js +16 -0
- package/dist/output/shapes/passthrough.js +2 -0
- package/dist/output/shapes.js +4 -0
- package/dist/output/text/command-format.js +29 -0
- package/dist/output/text/helpers.js +1 -1
- package/dist/output/text/improve-report.js +27 -0
- package/dist/{commands/env/marker-path.js → output/text/models.js} +4 -3
- package/dist/output/text/show-format.js +4 -0
- package/dist/output/text.js +4 -0
- package/dist/scripts/akm-migrate-node.js +25146 -21759
- package/dist/scripts/akm-migrate.js +24271 -20885
- package/dist/storage/repositories/embedding-salvage-repository.js +184 -0
- package/dist/storage/repositories/improve-runs-repository.js +34 -0
- package/dist/storage/repositories/index-fts-repository.js +49 -6
- package/dist/storage/repositories/index-schema.js +16 -0
- package/dist/storage/repositories/index-vec-repository.js +30 -0
- package/dist/storage/repositories/workflow-runs-repository.js +55 -18
- package/dist/tasks/backends/cron.js +14 -7
- package/dist/tasks/run/run-native-task.js +23 -1
- package/dist/tasks/run/run-workflow-task.js +16 -0
- package/dist/workflows/exec/child-workflow.js +2 -2
- package/dist/workflows/exec/dispatch-redaction.js +21 -9
- package/dist/workflows/exec/run-workflow.js +6 -5
- package/dist/workflows/runtime/runs.js +33 -5
- package/docs/migration/release-notes/0.9.15.md +133 -0
- package/docs/migration/release-notes/README.md +5 -0
- package/docs/reference/cli.md +271 -30
- package/docs/reference/configuration.md +234 -21
- package/docs/reference/data-and-telemetry.md +8 -0
- package/docs/reference/tasks.md +16 -1
- package/docs/reference/workflow-schema.md +5 -1
- package/package.json +1 -1
- package/schemas/akm-config.json +47 -0
|
@@ -25,7 +25,7 @@ import { allowsFragmentRef, defaultRendererRegistry } from "../../core/type-pres
|
|
|
25
25
|
import { normalizeEmbeddingEndpoint } from "../../llm/embedders/remote.js";
|
|
26
26
|
import { assertIndexPathReadable, closeDatabase, openExistingDatabase, } from "../../storage/repositories/index-connection.js";
|
|
27
27
|
import { getAllEntries, getBaseBeliefStatesForDerivedTwins, getEntryById, getEntryCount, getPositiveFeedbackCountsByIds, } from "../../storage/repositories/index-entries-repository.js";
|
|
28
|
-
import { searchFts } from "../../storage/repositories/index-fts-repository.js";
|
|
28
|
+
import { getIndexedMarkdownFragment, getIndexedMarkdownFragments, searchFts, } from "../../storage/repositories/index-fts-repository.js";
|
|
29
29
|
import { getMeta } from "../../storage/repositories/index-meta-repository.js";
|
|
30
30
|
import { getEmbeddingCount, searchVec } from "../../storage/repositories/index-vec-repository.js";
|
|
31
31
|
import { getCurrentWorkflowScopeKey } from "../../workflows/authoring/scope-key.js";
|
|
@@ -451,6 +451,14 @@ async function searchDatabase(db, query, searchType, limit, stashDir, allSourceD
|
|
|
451
451
|
});
|
|
452
452
|
const rankMs = Date.now() - tRank0;
|
|
453
453
|
const selected = beliefFiltered.slice(0, limit);
|
|
454
|
+
const fragmentSelections = selected.flatMap((ranked) => ranked.fragmentId && allowsFragmentRef(ranked.entry.type) && hasIndexedProvenance(ranked)
|
|
455
|
+
? [{ entryId: ranked.id, itemRef: ranked.itemRef, fragmentId: ranked.fragmentId }]
|
|
456
|
+
: []);
|
|
457
|
+
const selectedFragments = getIndexedMarkdownFragments(db, fragmentSelections);
|
|
458
|
+
const selectedFragmentByEntryId = new Map();
|
|
459
|
+
fragmentSelections.forEach((selection, index) => {
|
|
460
|
+
selectedFragmentByEntryId.set(selection.entryId, selectedFragments[index]);
|
|
461
|
+
});
|
|
454
462
|
const hits = await Promise.all(selected.map((ranked) => {
|
|
455
463
|
const { entry, filePath, score, rankingMode, utilityBoosted } = ranked;
|
|
456
464
|
// CLAUDE.md locks SearchHit.score in [0,1]. The boost loop deliberately
|
|
@@ -467,6 +475,7 @@ async function searchDatabase(db, query, searchType, limit, stashDir, allSourceD
|
|
|
467
475
|
rankingMode,
|
|
468
476
|
lexicalMatch: ranked.lexicalMatch,
|
|
469
477
|
fragmentId: ranked.fragmentId,
|
|
478
|
+
indexedFragment: ranked.fragmentId ? (selectedFragmentByEntryId.get(ranked.id) ?? null) : undefined,
|
|
470
479
|
defaultStashDir: stashDir,
|
|
471
480
|
allSourceDirs,
|
|
472
481
|
sources,
|
|
@@ -788,7 +797,19 @@ export async function buildDbHit(input) {
|
|
|
788
797
|
// The central type-presentation contract opts those types out explicitly.
|
|
789
798
|
const ref = input.fragmentId && allowsFragmentRef(input.entry.type) ? `${parentRef}#${input.fragmentId}` : parentRef;
|
|
790
799
|
const editable = isEditable(absolutePath, input.config, input.sources);
|
|
791
|
-
const
|
|
800
|
+
const indexedFragment = input.indexedFragment === undefined
|
|
801
|
+
? input.fragmentId && input.db
|
|
802
|
+
? getIndexedMarkdownFragment(input.db, input.itemRef, input.fragmentId)
|
|
803
|
+
: undefined
|
|
804
|
+
: (input.indexedFragment ?? undefined);
|
|
805
|
+
const selectedRef = input.fragmentId && ref !== parentRef ? `${parentRef}#${input.fragmentId}` : undefined;
|
|
806
|
+
const parentEstimatedTokens = typeof input.entry.fileSize === "number"
|
|
807
|
+
? Math.round(input.entry.fileSize / 4)
|
|
808
|
+
: indexedFragment
|
|
809
|
+
? Math.round(indexedFragment.parentChars / 4)
|
|
810
|
+
: undefined;
|
|
811
|
+
const fragmentEstimatedTokens = indexedFragment ? Math.round(indexedFragment.fragmentChars / 4) : undefined;
|
|
812
|
+
const estimatedTokens = selectedRef === ref && fragmentEstimatedTokens !== undefined ? fragmentEstimatedTokens : parentEstimatedTokens;
|
|
792
813
|
const hit = {
|
|
793
814
|
type: input.entry.type,
|
|
794
815
|
name: input.entry.name,
|
|
@@ -804,6 +825,32 @@ export async function buildDbHit(input) {
|
|
|
804
825
|
score,
|
|
805
826
|
whyMatched,
|
|
806
827
|
...(estimatedTokens !== undefined ? { estimatedTokens } : {}),
|
|
828
|
+
...(selectedRef
|
|
829
|
+
? {
|
|
830
|
+
selectedRef,
|
|
831
|
+
parentRef,
|
|
832
|
+
...(indexedFragment
|
|
833
|
+
? {
|
|
834
|
+
fragmentOrdinal: indexedFragment.ordinal + 1,
|
|
835
|
+
fragmentCount: indexedFragment.count,
|
|
836
|
+
startLine: indexedFragment.startLine,
|
|
837
|
+
endLine: indexedFragment.endLine,
|
|
838
|
+
...(indexedFragment.previousFragmentId
|
|
839
|
+
? { previousRef: `${parentRef}#${indexedFragment.previousFragmentId}` }
|
|
840
|
+
: {}),
|
|
841
|
+
...(indexedFragment.nextFragmentId
|
|
842
|
+
? { nextRef: `${parentRef}#${indexedFragment.nextFragmentId}` }
|
|
843
|
+
: {}),
|
|
844
|
+
fragmentChars: indexedFragment.fragmentChars,
|
|
845
|
+
fragmentEstimatedTokens,
|
|
846
|
+
parentChars: indexedFragment.parentChars,
|
|
847
|
+
...(parentEstimatedTokens !== undefined ? { parentEstimatedTokens } : {}),
|
|
848
|
+
}
|
|
849
|
+
: parentEstimatedTokens !== undefined
|
|
850
|
+
? { parentEstimatedTokens }
|
|
851
|
+
: {}),
|
|
852
|
+
}
|
|
853
|
+
: {}),
|
|
807
854
|
// Surface optional quality (v1 spec §4.2). Omitted when entry has
|
|
808
855
|
// no `quality` field so payloads stay compact for the common case.
|
|
809
856
|
...(input.entry.quality ? { quality: input.entry.quality } : {}),
|
|
@@ -238,6 +238,7 @@ export async function ensureSourceCaches(config, options) {
|
|
|
238
238
|
const cfg = config ?? loadConfig();
|
|
239
239
|
const force = options?.force === true;
|
|
240
240
|
const materialize = options?.materialize !== false;
|
|
241
|
+
const onProgress = options?.onProgress ?? (() => { });
|
|
241
242
|
// Polymorphic refresh: walk every enabled source through its registered
|
|
242
243
|
// provider and call `sync()`. Every cache-backed kind (git, website, npm)
|
|
243
244
|
// refreshes the same way — a bad source warns and is skipped without
|
|
@@ -249,6 +250,12 @@ export async function ensureSourceCaches(config, options) {
|
|
|
249
250
|
// content ENDS UP; the lock's `localRoot` merely records the result. So this
|
|
250
251
|
// path correctly uses the provider, not the shared `lockContentRootFor`
|
|
251
252
|
// resolver that reads/writes use to agree on where content already IS.
|
|
253
|
+
//
|
|
254
|
+
// Two passes: first resolve which sources will actually sync (constructing
|
|
255
|
+
// each provider once, exactly as before), so the progress count ("i/n")
|
|
256
|
+
// reflects real syncs rather than every configured entry including managed/
|
|
257
|
+
// unsyncable ones; then sync them in order, reporting progress per source.
|
|
258
|
+
const toSync = [];
|
|
252
259
|
for (const entry of getSources(cfg)) {
|
|
253
260
|
if (entry.enabled === false)
|
|
254
261
|
continue;
|
|
@@ -282,12 +289,27 @@ export async function ensureSourceCaches(config, options) {
|
|
|
282
289
|
warnIfSourceUnavailableForRead(entry, provider.name);
|
|
283
290
|
continue;
|
|
284
291
|
}
|
|
292
|
+
toSync.push(provider);
|
|
293
|
+
}
|
|
294
|
+
for (const [i, provider] of toSync.entries()) {
|
|
295
|
+
onProgress(`Hydrating source ${i + 1}/${toSync.length}: ${provider.name}`);
|
|
296
|
+
let heartbeat;
|
|
285
297
|
try {
|
|
286
|
-
|
|
298
|
+
heartbeat = setInterval(() => {
|
|
299
|
+
onProgress(`Still hydrating source ${i + 1}/${toSync.length}: ${provider.name}...`);
|
|
300
|
+
}, 15000);
|
|
301
|
+
// `toSync` only ever holds providers whose `.sync` passed the check
|
|
302
|
+
// above; the optional-chain here is just to satisfy the type (the
|
|
303
|
+
// narrowing does not survive the array round-trip).
|
|
304
|
+
await provider.sync?.({ force, secrets: options?.secrets, ensureWebsiteMirror });
|
|
287
305
|
}
|
|
288
306
|
catch (err) {
|
|
289
307
|
warn(`Warning: failed to refresh ${provider.kind} source "${provider.name}": ${err instanceof Error ? err.message : String(err)}`);
|
|
290
308
|
}
|
|
309
|
+
finally {
|
|
310
|
+
if (heartbeat)
|
|
311
|
+
clearInterval(heartbeat);
|
|
312
|
+
}
|
|
291
313
|
}
|
|
292
314
|
}
|
|
293
315
|
/**
|
|
@@ -4,11 +4,14 @@
|
|
|
4
4
|
import fs from "node:fs";
|
|
5
5
|
import os from "node:os";
|
|
6
6
|
import path from "node:path";
|
|
7
|
+
import { resolveSecret } from "../../core/config/config.js";
|
|
7
8
|
import { deepMergeConfig } from "../../core/config/deep-merge.js";
|
|
9
|
+
import { SECRET_STORE_REFERENCE_PATTERN } from "../../core/config/schema/primitives.js";
|
|
8
10
|
import { ConfigError } from "../../core/errors.js";
|
|
9
11
|
import { formatExtraParamsIssue, validateExtraParams } from "../../core/extra-params.js";
|
|
10
12
|
import { collectSensitiveValues } from "../../core/redaction.js";
|
|
11
13
|
import { warn } from "../../core/warn.js";
|
|
14
|
+
import { resolveSecretFromStore } from "../../sources/snapshot-fetchers/secret-seam.js";
|
|
12
15
|
import { getHarness } from "../harnesses/index.js";
|
|
13
16
|
import { DEFAULT_AGENT_TIMEOUT_MS, DEFAULT_LLM_TIMEOUT_MS } from "./config.js";
|
|
14
17
|
import { getBuiltinAgentProfile } from "./profiles.js";
|
|
@@ -70,6 +73,21 @@ export function lookupApiKeyFileValue(filePath) {
|
|
|
70
73
|
return undefined;
|
|
71
74
|
}
|
|
72
75
|
}
|
|
76
|
+
/**
|
|
77
|
+
* Best-effort, non-throwing read of a secret-store-backed credential's
|
|
78
|
+
* current value (#953), for redaction inventories and health probes that
|
|
79
|
+
* must never fail just because a value collector ran ahead of the real
|
|
80
|
+
* dispatch — an unresolvable reference is reported by
|
|
81
|
+
* {@link resolveLlmCredentialValue} at the actual call.
|
|
82
|
+
*/
|
|
83
|
+
export function lookupApiKeySecretRefValue(ref) {
|
|
84
|
+
try {
|
|
85
|
+
return resolveSecret(ref, resolveSecretFromStore);
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
return undefined;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
73
91
|
function selectedEngineName(config, layers, llmOnly) {
|
|
74
92
|
for (let index = layers.length - 1; index >= 0; index--) {
|
|
75
93
|
const layer = layers[index];
|
|
@@ -94,9 +112,14 @@ function resolveCredential(name, engine, config) {
|
|
|
94
112
|
const apiKey = ownValue(engine, "apiKey");
|
|
95
113
|
if (apiKey !== undefined) {
|
|
96
114
|
const explicit = envName(apiKey);
|
|
97
|
-
if (
|
|
98
|
-
|
|
99
|
-
|
|
115
|
+
if (explicit)
|
|
116
|
+
return { names: [explicit], required: true };
|
|
117
|
+
// #953: a secret-store reference has no env descriptor — resolved
|
|
118
|
+
// separately onto `ResolvedLlmUse.apiKeySecretRef` — mirroring how
|
|
119
|
+
// apiKeyFile above is its own credential source.
|
|
120
|
+
if (SECRET_STORE_REFERENCE_PATTERN.test(apiKey))
|
|
121
|
+
return undefined;
|
|
122
|
+
throw new ConfigError(`Engine "${name}" has an invalid symbolic apiKey reference.`, "INVALID_CONFIG_FILE");
|
|
100
123
|
}
|
|
101
124
|
// #905: an explicit apiKeyFile is its own credential source — resolved
|
|
102
125
|
// separately onto `ResolvedLlmUse.apiKeyFile` — so it does not also fall
|
|
@@ -142,11 +165,66 @@ export function resolveCredentialFromEnv(credential, envSource = process.env) {
|
|
|
142
165
|
* so a whole operation observes one stable credential value instead of
|
|
143
166
|
* re-reading the file on every dispatch within it.
|
|
144
167
|
*/
|
|
145
|
-
export function resolveLlmCredentialValue(engine, credential, apiKeyFile, envSource = process.env) {
|
|
168
|
+
export function resolveLlmCredentialValue(engine, credential, apiKeyFile, apiKeySecretRef, envSource = process.env) {
|
|
146
169
|
const envValue = resolveCredentialFromEnv(credential, envSource);
|
|
147
170
|
if (envValue !== undefined)
|
|
148
171
|
return envValue;
|
|
149
|
-
|
|
172
|
+
if (apiKeyFile !== undefined)
|
|
173
|
+
return readApiKeyFile(engine, apiKeyFile);
|
|
174
|
+
// #953: a secret-store reference is the last fallback tier, reusing the
|
|
175
|
+
// same resolveSecret() helper llm/client.ts and embedders/remote.ts call
|
|
176
|
+
// directly — throws SECRET_REFERENCE_UNRESOLVED naming only the reference.
|
|
177
|
+
return apiKeySecretRef !== undefined ? resolveSecret(apiKeySecretRef, resolveSecretFromStore) : undefined;
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Non-throwing credential-presence check for `akm health`, the
|
|
181
|
+
* improve-strategy probe (#953), and improve's own plan builder (#957): an env
|
|
182
|
+
* value is present, or a file-backed credential is readable and non-empty, or
|
|
183
|
+
* a secret-store reference resolves. Never reads env/disk/store speculatively
|
|
184
|
+
* beyond what's needed to answer "is something here", and never throws on a
|
|
185
|
+
* broken/missing source — that is reported by {@link resolveLlmCredentialValue}
|
|
186
|
+
* at the real dispatch.
|
|
187
|
+
*
|
|
188
|
+
* On failure, `reference` and `reason` name WHICH env var / file / secret
|
|
189
|
+
* reference is missing (never its value) — the operator's own shell often
|
|
190
|
+
* passes the same check a scheduler's stripped-down environment fails, so the
|
|
191
|
+
* caller needs to say which reference is the problem. Callers that must never
|
|
192
|
+
* name the reference (`akm health`'s evidence/message) use only `.available`.
|
|
193
|
+
*/
|
|
194
|
+
export function describeLlmCredentialAvailability(resolved, env = process.env) {
|
|
195
|
+
if (resolved.credential?.required) {
|
|
196
|
+
if (resolved.credential.names.some((name) => Boolean(env[name]?.trim())))
|
|
197
|
+
return { available: true };
|
|
198
|
+
const reference = `$${resolved.credential.names[0]}`;
|
|
199
|
+
return { available: false, reference, reason: `${reference} is not set in this environment` };
|
|
200
|
+
}
|
|
201
|
+
if (resolved.apiKeyFile !== undefined) {
|
|
202
|
+
if (lookupApiKeyFileValue(resolved.apiKeyFile) !== undefined)
|
|
203
|
+
return { available: true };
|
|
204
|
+
return {
|
|
205
|
+
available: false,
|
|
206
|
+
reference: resolved.apiKeyFile,
|
|
207
|
+
reason: `apiKeyFile ${resolved.apiKeyFile} is missing or empty`,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
if (resolved.apiKeySecretRef !== undefined) {
|
|
211
|
+
if (lookupApiKeySecretRefValue(resolved.apiKeySecretRef) !== undefined)
|
|
212
|
+
return { available: true };
|
|
213
|
+
return {
|
|
214
|
+
available: false,
|
|
215
|
+
reference: resolved.apiKeySecretRef,
|
|
216
|
+
reason: `${resolved.apiKeySecretRef} did not resolve from the secret store`,
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
return { available: true };
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Thin boolean wrapper over {@link describeLlmCredentialAvailability} for
|
|
223
|
+
* callers (`akm health`'s engine-reachability probes) that only need a
|
|
224
|
+
* yes/no answer and never surface the reference.
|
|
225
|
+
*/
|
|
226
|
+
export function isLlmCredentialAvailable(resolved, env = process.env) {
|
|
227
|
+
return describeLlmCredentialAvailability(resolved, env).available;
|
|
150
228
|
}
|
|
151
229
|
/** Collect materialized engine credentials for output and persistence redaction. */
|
|
152
230
|
export function collectEngineCredentialValues(config, envSource = process.env) {
|
|
@@ -168,6 +246,13 @@ export function collectEngineCredentialValues(config, envSource = process.env) {
|
|
|
168
246
|
if (value)
|
|
169
247
|
values.add(value);
|
|
170
248
|
}
|
|
249
|
+
// #953: secret-store-backed credential — best-effort for the same reason.
|
|
250
|
+
const apiKey = ownValue(engine, "apiKey");
|
|
251
|
+
if (apiKey !== undefined && SECRET_STORE_REFERENCE_PATTERN.test(apiKey)) {
|
|
252
|
+
const value = lookupApiKeySecretRefValue(apiKey);
|
|
253
|
+
if (value)
|
|
254
|
+
values.add(value);
|
|
255
|
+
}
|
|
171
256
|
}
|
|
172
257
|
return collectSensitiveValues(values);
|
|
173
258
|
}
|
|
@@ -235,11 +320,14 @@ export function resolveLlmEngineUse(config, layers, options = {}) {
|
|
|
235
320
|
delete connection[key];
|
|
236
321
|
}
|
|
237
322
|
const apiKeyFile = ownValue(engine, "apiKeyFile");
|
|
323
|
+
const apiKeyRaw = ownValue(engine, "apiKey");
|
|
324
|
+
const apiKeySecretRef = apiKeyRaw !== undefined && SECRET_STORE_REFERENCE_PATTERN.test(apiKeyRaw) ? apiKeyRaw : undefined;
|
|
238
325
|
return {
|
|
239
326
|
engine: name,
|
|
240
327
|
connection: sterileRecord(connection),
|
|
241
328
|
credential: resolveCredential(name, engine, config),
|
|
242
329
|
...(apiKeyFile !== undefined ? { apiKeyFile: expandHomePath(apiKeyFile) } : {}),
|
|
330
|
+
...(apiKeySecretRef !== undefined ? { apiKeySecretRef } : {}),
|
|
243
331
|
timeoutMs: effectiveTimeout(engine, layers, DEFAULT_LLM_TIMEOUT_MS),
|
|
244
332
|
};
|
|
245
333
|
}
|
|
@@ -270,7 +358,7 @@ export function materializeLlmConnectionWithCredential(resolved, credentialValue
|
|
|
270
358
|
* engine has no env descriptor.
|
|
271
359
|
*/
|
|
272
360
|
export function materializeLlmConnection(resolved, envSource = process.env) {
|
|
273
|
-
return materializeLlmConnectionWithCredential(resolved, resolveLlmCredentialValue(resolved.engine, resolved.credential, resolved.apiKeyFile, envSource));
|
|
361
|
+
return materializeLlmConnectionWithCredential(resolved, resolveLlmCredentialValue(resolved.engine, resolved.credential, resolved.apiKeyFile, resolved.apiKeySecretRef, envSource));
|
|
274
362
|
}
|
|
275
363
|
function lowerAgentEngine(name, engine, config) {
|
|
276
364
|
const harness = getHarness(engine.platform);
|
|
@@ -319,6 +407,7 @@ function lowerAgentEngine(name, engine, config) {
|
|
|
319
407
|
fallbackConnection: fallback.connection,
|
|
320
408
|
...(fallback.credential ? { fallbackCredential: fallback.credential } : {}),
|
|
321
409
|
...(fallback.apiKeyFile ? { fallbackApiKeyFile: fallback.apiKeyFile } : {}),
|
|
410
|
+
...(fallback.apiKeySecretRef ? { fallbackApiKeySecretRef: fallback.apiKeySecretRef } : {}),
|
|
322
411
|
fallbackTimeoutMs: fallback.timeoutMs,
|
|
323
412
|
}
|
|
324
413
|
: {}),
|
|
@@ -340,6 +429,7 @@ export function resolveEngine(name, config) {
|
|
|
340
429
|
connection: resolved.connection,
|
|
341
430
|
...(resolved.credential ? { credential: resolved.credential } : {}),
|
|
342
431
|
...(resolved.apiKeyFile ? { apiKeyFile: resolved.apiKeyFile } : {}),
|
|
432
|
+
...(resolved.apiKeySecretRef ? { apiKeySecretRef: resolved.apiKeySecretRef } : {}),
|
|
343
433
|
timeoutMs: resolved.timeoutMs,
|
|
344
434
|
};
|
|
345
435
|
}
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
4
|
import { cloneExecutionJsonObject } from "../../execution/json.js";
|
|
5
5
|
import { DEFAULT_AGENT_TIMEOUT_MS, DEFAULT_LLM_TIMEOUT_MS } from "./config.js";
|
|
6
|
+
import { engineModelAndInference } from "./model-map.js";
|
|
6
7
|
import { OPENCODE_SDK_SERVER_BIN } from "./profiles.js";
|
|
7
8
|
/**
|
|
8
9
|
* Canonical engine-setting marker: an SDK without its own model selected the
|
|
@@ -22,25 +23,15 @@ function withoutUndefined(value) {
|
|
|
22
23
|
}
|
|
23
24
|
function engineDefaults(engine) {
|
|
24
25
|
const defaults = {};
|
|
25
|
-
|
|
26
|
-
|
|
26
|
+
const { model, inference } = engineModelAndInference(engine);
|
|
27
|
+
if (model !== undefined)
|
|
28
|
+
defaults.model = model;
|
|
27
29
|
if (own(engine, "timeoutMs"))
|
|
28
30
|
defaults.timeout = engine.timeoutMs;
|
|
29
31
|
if (engine.kind === "agent" && own(engine, "workspace"))
|
|
30
32
|
defaults.workspace = engine.workspace;
|
|
31
|
-
if (
|
|
32
|
-
|
|
33
|
-
temperature: ownValue(engine, "temperature"),
|
|
34
|
-
maxTokens: ownValue(engine, "maxTokens"),
|
|
35
|
-
supportsJsonSchema: ownValue(engine, "supportsJsonSchema"),
|
|
36
|
-
extraParams: ownValue(engine, "extraParams"),
|
|
37
|
-
contextLength: ownValue(engine, "contextLength"),
|
|
38
|
-
enableThinking: ownValue(engine, "enableThinking"),
|
|
39
|
-
reasoningEffort: ownValue(engine, "reasoningEffort"),
|
|
40
|
-
});
|
|
41
|
-
if (Object.keys(inference).length > 0)
|
|
42
|
-
defaults.inference = inference;
|
|
43
|
-
}
|
|
33
|
+
if (inference !== undefined)
|
|
34
|
+
defaults.inference = inference;
|
|
44
35
|
return Object.freeze(defaults);
|
|
45
36
|
}
|
|
46
37
|
function configuredSdkFallback(engine, config) {
|
|
@@ -210,13 +210,16 @@ function snapshotRunnerSpec(input, options = {}) {
|
|
|
210
210
|
if (own(cloned, "timeoutMs"))
|
|
211
211
|
validateTimeout(cloned.timeoutMs, "execution runner material.timeoutMs");
|
|
212
212
|
if (kind === "llm") {
|
|
213
|
-
assertKeys(cloned, ["kind", "engine", "connection", "credential", "apiKeyFile", "timeoutMs"], "execution runner material");
|
|
213
|
+
assertKeys(cloned, ["kind", "engine", "connection", "credential", "apiKeyFile", "apiKeySecretRef", "timeoutMs"], "execution runner material");
|
|
214
214
|
validateConnection(cloned.connection, "execution runner material.connection", !options.allowMissingLlmModel);
|
|
215
215
|
if (own(cloned, "credential"))
|
|
216
216
|
validateCredential(cloned.credential, "execution runner material.credential");
|
|
217
217
|
// #905: a path, not the secret itself — as safe to freeze as `credential`'s
|
|
218
218
|
// env-var name.
|
|
219
219
|
validateOptionalString(cloned, "apiKeyFile", "execution runner material");
|
|
220
|
+
// #953: a `secret://<name>` reference, not the secret itself — same
|
|
221
|
+
// freeze-safety rationale as apiKeyFile above.
|
|
222
|
+
validateOptionalString(cloned, "apiKeySecretRef", "execution runner material");
|
|
220
223
|
}
|
|
221
224
|
else if (kind === "agent") {
|
|
222
225
|
assertKeys(cloned, ["kind", "engine", "profile", "timeoutMs"], "execution runner material");
|
|
@@ -230,6 +233,7 @@ function snapshotRunnerSpec(input, options = {}) {
|
|
|
230
233
|
"fallbackConnection",
|
|
231
234
|
"fallbackCredential",
|
|
232
235
|
"fallbackApiKeyFile",
|
|
236
|
+
"fallbackApiKeySecretRef",
|
|
233
237
|
"fallbackTimeoutMs",
|
|
234
238
|
"timeoutMs",
|
|
235
239
|
], "execution runner material");
|
|
@@ -241,6 +245,7 @@ function snapshotRunnerSpec(input, options = {}) {
|
|
|
241
245
|
validateCredential(cloned.fallbackCredential, "execution runner material.fallbackCredential");
|
|
242
246
|
}
|
|
243
247
|
validateOptionalString(cloned, "fallbackApiKeyFile", "execution runner material");
|
|
248
|
+
validateOptionalString(cloned, "fallbackApiKeySecretRef", "execution runner material");
|
|
244
249
|
if (own(cloned, "fallbackTimeoutMs")) {
|
|
245
250
|
validateTimeout(cloned.fallbackTimeoutMs, "execution runner material.fallbackTimeoutMs");
|
|
246
251
|
}
|
|
@@ -54,7 +54,7 @@ export function prepareResolvedExecution(options) {
|
|
|
54
54
|
...(options.invocationDefaults ? { invocationDefaults: options.invocationDefaults } : {}),
|
|
55
55
|
...(options.current ? { current: options.current } : {}),
|
|
56
56
|
engines: executionEngineDefinitionsFromConfig(config),
|
|
57
|
-
modelMap: options.modelMap ?? loadModelMap().map,
|
|
57
|
+
modelMap: options.modelMap ?? loadModelMap({ engines: config.engines }).map,
|
|
58
58
|
invocationKind: options.invocationKind,
|
|
59
59
|
...(options.authorizeTools ? { authorizeTools: options.authorizeTools } : {}),
|
|
60
60
|
});
|
|
@@ -69,13 +69,25 @@ function parseProfileLayer(value, source, jsonPath) {
|
|
|
69
69
|
if (typeof value === "string")
|
|
70
70
|
return requireNonemptyString(value, source, jsonPath);
|
|
71
71
|
const record = requireRecord(value, source, jsonPath);
|
|
72
|
-
assertOnlyKeys(record, ["model", "inference"], source, jsonPath);
|
|
73
|
-
if (!Object.hasOwn(record, "model") && !Object.hasOwn(record, "inference")) {
|
|
74
|
-
invalid(source, jsonPath, "structured profile must contain model and/or
|
|
72
|
+
assertOnlyKeys(record, ["model", "inference", "engine"], source, jsonPath);
|
|
73
|
+
if (!Object.hasOwn(record, "model") && !Object.hasOwn(record, "inference") && !Object.hasOwn(record, "engine")) {
|
|
74
|
+
invalid(source, jsonPath, "structured profile must contain model, inference, and/or engine");
|
|
75
|
+
}
|
|
76
|
+
if (Object.hasOwn(record, "model") && Object.hasOwn(record, "engine")) {
|
|
77
|
+
invalid(source, jsonPath, "model and engine cannot both be set; engine is an indirection for the model value");
|
|
75
78
|
}
|
|
76
79
|
const out = {};
|
|
77
80
|
if (Object.hasOwn(record, "model"))
|
|
78
81
|
out.model = requireNonemptyString(record.model, source, `${jsonPath}.model`);
|
|
82
|
+
if (Object.hasOwn(record, "engine")) {
|
|
83
|
+
const rawEngine = requireNonemptyString(record.engine, source, `${jsonPath}.engine`);
|
|
84
|
+
const engine = rawEngine.toLowerCase();
|
|
85
|
+
assertSafeMapKey(engine, source, `${jsonPath}.engine`);
|
|
86
|
+
if (!ENGINE_KEY_PATTERN.test(engine)) {
|
|
87
|
+
invalid(source, `${jsonPath}.engine`, "engine reference must be lowercase kebab-case");
|
|
88
|
+
}
|
|
89
|
+
out.engine = engine;
|
|
90
|
+
}
|
|
79
91
|
if (Object.hasOwn(record, "inference")) {
|
|
80
92
|
if (record.inference === null)
|
|
81
93
|
out.inference = null;
|
|
@@ -161,10 +173,21 @@ function mergeProfiles(base, overlay) {
|
|
|
161
173
|
const out = {};
|
|
162
174
|
if (base?.model !== undefined)
|
|
163
175
|
out.model = base.model;
|
|
176
|
+
if (base?.engine !== undefined)
|
|
177
|
+
out.engine = base.engine;
|
|
164
178
|
if (base && Object.hasOwn(base, "inference"))
|
|
165
179
|
out.inference = base.inference;
|
|
166
|
-
|
|
180
|
+
// A literal `model` and an `engine` indirection are mutually exclusive within
|
|
181
|
+
// one layer (enforced at parse time); across layers, whichever the nearer
|
|
182
|
+
// layer sets replaces the other so the merged profile keeps that invariant.
|
|
183
|
+
if (next.model !== undefined) {
|
|
167
184
|
out.model = next.model;
|
|
185
|
+
delete out.engine;
|
|
186
|
+
}
|
|
187
|
+
if (next.engine !== undefined) {
|
|
188
|
+
out.engine = next.engine;
|
|
189
|
+
delete out.model;
|
|
190
|
+
}
|
|
168
191
|
if (Object.hasOwn(next, "inference")) {
|
|
169
192
|
out.inference =
|
|
170
193
|
next.inference !== null && next.inference !== undefined && isJsonObject(base?.inference)
|
|
@@ -173,14 +196,50 @@ function mergeProfiles(base, overlay) {
|
|
|
173
196
|
}
|
|
174
197
|
return Object.freeze(out);
|
|
175
198
|
}
|
|
176
|
-
/**
|
|
177
|
-
|
|
199
|
+
/**
|
|
200
|
+
* Derive the `{ model, inference }` a model-map profile borrows from a
|
|
201
|
+
* configured engine (#946). Shared verbatim by
|
|
202
|
+
* `execution-definitions.ts`'s own execution-defaults derivation
|
|
203
|
+
* (`engineDefaults`) so the two paths cannot silently diverge. `model` is
|
|
204
|
+
* copied verbatim from the engine's own config value; it must already be
|
|
205
|
+
* meaningful for the model-map column's platform (akm does not translate
|
|
206
|
+
* between an engine's connection and an agent platform's own provider
|
|
207
|
+
* registry). Only `kind: "llm"` engines contribute inference defaults — an
|
|
208
|
+
* agent-kind engine's schema carries no temperature/thinking fields.
|
|
209
|
+
*/
|
|
210
|
+
export function engineModelAndInference(engine) {
|
|
211
|
+
const out = {};
|
|
212
|
+
if (Object.hasOwn(engine, "model") && engine.model !== undefined)
|
|
213
|
+
out.model = engine.model;
|
|
214
|
+
if (engine.kind === "llm") {
|
|
215
|
+
const inference = {};
|
|
216
|
+
if (Object.hasOwn(engine, "temperature"))
|
|
217
|
+
inference.temperature = engine.temperature;
|
|
218
|
+
if (Object.hasOwn(engine, "maxTokens"))
|
|
219
|
+
inference.maxTokens = engine.maxTokens;
|
|
220
|
+
if (Object.hasOwn(engine, "supportsJsonSchema"))
|
|
221
|
+
inference.supportsJsonSchema = engine.supportsJsonSchema;
|
|
222
|
+
if (Object.hasOwn(engine, "extraParams"))
|
|
223
|
+
inference.extraParams = engine.extraParams;
|
|
224
|
+
if (Object.hasOwn(engine, "contextLength"))
|
|
225
|
+
inference.contextLength = engine.contextLength;
|
|
226
|
+
if (Object.hasOwn(engine, "enableThinking"))
|
|
227
|
+
inference.enableThinking = engine.enableThinking;
|
|
228
|
+
if (Object.hasOwn(engine, "reasoningEffort"))
|
|
229
|
+
inference.reasoningEffort = engine.reasoningEffort;
|
|
230
|
+
if (Object.keys(inference).length > 0)
|
|
231
|
+
out.inference = inference;
|
|
232
|
+
}
|
|
233
|
+
return Object.freeze(out);
|
|
234
|
+
}
|
|
235
|
+
/** Overlay user fields over installed fields, per (alias, column), without resolving `engine` indirection. */
|
|
236
|
+
function mergeRawProfileLayers(installed, user) {
|
|
178
237
|
const aliases = new Map();
|
|
179
238
|
const apply = (layer) => {
|
|
180
|
-
for (const [alias,
|
|
239
|
+
for (const [alias, layerEngines] of Object.entries(layer.aliases)) {
|
|
181
240
|
const mergedEngines = aliases.get(alias) ?? new Map();
|
|
182
|
-
for (const [
|
|
183
|
-
mergedEngines.set(
|
|
241
|
+
for (const [engineKey, profile] of Object.entries(layerEngines)) {
|
|
242
|
+
mergedEngines.set(engineKey, mergeProfiles(mergedEngines.get(engineKey), profile));
|
|
184
243
|
}
|
|
185
244
|
aliases.set(alias, mergedEngines);
|
|
186
245
|
}
|
|
@@ -188,14 +247,55 @@ export function mergeModelMapLayers(installed, user) {
|
|
|
188
247
|
apply(installed);
|
|
189
248
|
if (user)
|
|
190
249
|
apply(user);
|
|
250
|
+
return aliases;
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* The overlaid-but-unresolved profile per (alias, column), before any
|
|
254
|
+
* `engine` indirection is expanded. `akm models list` (#946) uses this to
|
|
255
|
+
* report whether a column resolves through a literal `model` or an `engine`
|
|
256
|
+
* reference — information {@link mergeModelMapLayers} collapses away once it
|
|
257
|
+
* performs the final resolution.
|
|
258
|
+
*/
|
|
259
|
+
export function mergedModelMapProfiles(installed, user) {
|
|
260
|
+
const aliases = mergeRawProfileLayers(installed, user);
|
|
261
|
+
const out = [];
|
|
262
|
+
for (const [alias, engineProfiles] of aliases)
|
|
263
|
+
out.push([alias, freezeRecord(engineProfiles)]);
|
|
264
|
+
return freezeRecord(out);
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Overlay user fields over installed fields, resolve any `engine` indirection
|
|
268
|
+
* against configured engines, then enforce usable merged profiles.
|
|
269
|
+
*/
|
|
270
|
+
export function mergeModelMapLayers(installed, user, engines) {
|
|
271
|
+
const aliases = mergeRawProfileLayers(installed, user);
|
|
191
272
|
const resolvedAliases = [];
|
|
192
|
-
for (const [alias,
|
|
273
|
+
for (const [alias, engineProfiles] of aliases) {
|
|
193
274
|
const resolvedEngines = [];
|
|
194
|
-
for (const [
|
|
195
|
-
|
|
196
|
-
|
|
275
|
+
for (const [engineKey, profile] of engineProfiles) {
|
|
276
|
+
let model = profile.model;
|
|
277
|
+
let inference = Object.hasOwn(profile, "inference") ? profile.inference : undefined;
|
|
278
|
+
if (model === undefined && profile.engine !== undefined) {
|
|
279
|
+
const target = ownValue(engines, profile.engine);
|
|
280
|
+
if (target === undefined) {
|
|
281
|
+
invalid("merged models.json", `$.aliases.${alias}.${engineKey}.engine`, `references unknown engine ${JSON.stringify(profile.engine)}; a usable model is required after overlay`);
|
|
282
|
+
}
|
|
283
|
+
const expansion = engineModelAndInference(target);
|
|
284
|
+
if (expansion.model === undefined) {
|
|
285
|
+
invalid("merged models.json", `$.aliases.${alias}.${engineKey}.engine`, `engine ${JSON.stringify(profile.engine)} has no usable model; a usable model is required after overlay`);
|
|
286
|
+
}
|
|
287
|
+
model = expansion.model;
|
|
288
|
+
inference =
|
|
289
|
+
inference === undefined
|
|
290
|
+
? expansion.inference
|
|
291
|
+
: inference !== null && isJsonObject(expansion.inference)
|
|
292
|
+
? mergeJsonValue(expansion.inference, inference)
|
|
293
|
+
: inference;
|
|
197
294
|
}
|
|
198
|
-
|
|
295
|
+
if (model === undefined) {
|
|
296
|
+
invalid("merged models.json", `$.aliases.${alias}.${engineKey}.model`, "a usable model is required after overlay");
|
|
297
|
+
}
|
|
298
|
+
resolvedEngines.push([engineKey, Object.freeze(inference !== undefined ? { model, inference } : { model })]);
|
|
199
299
|
}
|
|
200
300
|
resolvedAliases.push([alias, freezeRecord(resolvedEngines)]);
|
|
201
301
|
}
|
|
@@ -314,15 +414,18 @@ export function readInstalledModelMapText(options = {}) {
|
|
|
314
414
|
}
|
|
315
415
|
throw new ConfigError(`Installed models.json is missing from this AKM installation (checked: ${candidates.join(", ")}).`, "INVALID_CONFIG_FILE", "Reinstall AKM so its dist/assets/models.json package asset is restored.");
|
|
316
416
|
}
|
|
317
|
-
/**
|
|
318
|
-
export function
|
|
417
|
+
/** Read and parse the installed authority plus the optional operator overlay, without resolving them. */
|
|
418
|
+
export function loadModelMapLayers(options = {}) {
|
|
319
419
|
const installed = parseModelMapLayer(readInstalledModelMapText(options), "installed models.json");
|
|
320
420
|
const userPath = userModelMapPath(options.env);
|
|
321
421
|
const userText = readModelMapFile(userPath, "User models.json", true);
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
422
|
+
const user = userText !== undefined ? parseModelMapLayer(userText, `user models.json (${userPath})`) : undefined;
|
|
423
|
+
return Object.freeze({ installed, ...(user ? { user } : {}), userPath, userStatus: user ? "loaded" : "absent" });
|
|
424
|
+
}
|
|
425
|
+
/** Load the installed authority plus the optional operator overlay. */
|
|
426
|
+
export function loadModelMap(options = {}) {
|
|
427
|
+
const { installed, user, userPath, userStatus } = loadModelMapLayers(options);
|
|
428
|
+
return Object.freeze({ map: mergeModelMapLayers(installed, user, options.engines), userPath, userStatus });
|
|
326
429
|
}
|
|
327
430
|
function targetExistsError(target, detail) {
|
|
328
431
|
return new UsageError(`${detail}: ${target}`, "RESOURCE_ALREADY_EXISTS", "Move the existing target aside, then retry. Use --overwrite only for a stable regular file you intend to replace.");
|