akm-cli 0.9.16-alpha.1 → 0.9.16
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 +56 -132
- package/dist/assets/hints/cli-hints-full.md +13 -6
- package/dist/assets/tasks/core/index-refresh.yml +1 -1
- package/dist/assets/tasks/improve/akm-improve-catchup.yml +3 -6
- package/dist/cli/retired-commands.js +0 -4
- package/dist/cli/unknown-flags.js +3 -36
- package/dist/commands/env/env-binding.js +4 -4
- package/dist/commands/env/env-cli.js +3 -3
- package/dist/commands/improve/collapse-detector.js +2 -2
- package/dist/commands/improve/consolidate.js +4 -6
- package/dist/commands/improve/improve-cli.js +20 -15
- 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/repository.js +3 -12
- 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/curate.js +44 -34
- package/dist/commands/read/search.js +35 -54
- 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/info.js +8 -8
- package/dist/commands/sources/installed-stashes.js +55 -61
- package/dist/commands/sources/source-add.js +39 -38
- package/dist/commands/sources/source-manage.js +34 -12
- package/dist/commands/sources/stash-cli.js +111 -119
- 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 +8 -35
- package/dist/core/adapter/adapters/akm-metadata.js +1 -11
- package/dist/core/adapter/execution-source.js +10 -29
- package/dist/core/asset/asset-placement.js +0 -35
- 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/embedding.js +30 -7
- 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 +10 -12
- package/dist/core/config/schema/sources-bundles.js +32 -1
- package/dist/core/content-safety.js +52 -0
- package/dist/core/errors.js +2 -5
- package/dist/core/maintenance-barrier.js +11 -13
- package/dist/core/paths.js +11 -0
- package/dist/core/run-lock.js +2 -5
- package/dist/core/state/migrations.js +1 -26
- package/dist/core/state-db.js +27 -63
- 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/ensure-index.js +0 -5
- package/dist/indexer/index-db-contention.js +56 -0
- package/dist/indexer/index-rebuild-lock.js +73 -0
- package/dist/indexer/index-written-assets.js +171 -133
- package/dist/indexer/indexer.js +1621 -458
- package/dist/indexer/lookup/adapter-concept-owner.js +5 -19
- package/dist/indexer/materialize-embeddings.js +785 -0
- package/dist/indexer/passes/dir-staleness.js +161 -0
- package/dist/indexer/passes/metadata.js +1 -18
- package/dist/indexer/scan/drain-dir.js +70 -27
- package/dist/indexer/search/db-search.js +89 -373
- package/dist/indexer/search/ranking-contributors.js +16 -21
- package/dist/indexer/search/ranking.js +57 -135
- 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 +3 -11
- package/dist/llm/embedder.js +3 -10
- package/dist/llm/embedders/remote.js +104 -133
- package/dist/llm/feature-gate.js +2 -4
- package/dist/llm/rerank-client.js +3 -3
- package/dist/output/html-render.js +2 -1
- package/dist/output/shapes/passthrough.js +2 -1
- package/dist/output/stdout.js +24 -0
- package/dist/output/text/command-format.js +13 -19
- package/dist/output/text/helpers.js +1 -1
- package/dist/output/text/index.js +2 -5
- package/dist/output/text.js +4 -3
- package/dist/registry/resolve.js +37 -10
- package/dist/scripts/akm-migrate-node.js +15197 -11351
- package/dist/scripts/akm-migrate.js +15514 -11668
- package/dist/setup/semantic-assets.js +2 -2
- package/dist/setup/setup.js +3 -3
- package/dist/setup/steps/connection.js +2 -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/embedding-salvage-repository.js +184 -0
- package/dist/storage/repositories/index-connection.js +3 -1
- package/dist/storage/repositories/index-entries-repository.js +68 -77
- package/dist/storage/repositories/index-entry-schema.js +25 -16
- package/dist/storage/repositories/index-fts-repository.js +263 -29
- package/dist/storage/repositories/index-meta-repository.js +29 -0
- package/dist/storage/repositories/index-schema.js +122 -115
- package/dist/storage/repositories/index-utility-repository.js +1 -1
- package/dist/storage/repositories/index-vec-repository.js +435 -22
- 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.15.md +36 -34
- package/docs/migration/release-notes/0.9.16.md +60 -98
- package/docs/migration/release-notes/README.md +0 -5
- package/docs/migration/v0.9.1-to-v0.9.2.md +6 -9
- package/docs/reference/cli.md +124 -122
- package/docs/reference/configuration.md +137 -133
- package/docs/reference/data-and-telemetry.md +1 -2
- package/docs/reference/tasks.md +34 -29
- package/package.json +1 -1
- package/schemas/akm-config.json +170 -6
- package/schemas/akm-task.json +1 -2
- package/dist/commands/sources/index-status.js +0 -99
- package/dist/core/hash.js +0 -18
- package/dist/indexer/drain.js +0 -306
- package/dist/indexer/embedding-identity.js +0 -20
- package/dist/indexer/enrich.js +0 -260
- package/dist/indexer/reconcile.js +0 -890
- package/dist/indexer/scan/parse-file.js +0 -66
- package/dist/indexer/units/unit.js +0 -159
- package/dist/llm/embedders/provider-limits.js +0 -288
- package/dist/storage/repositories/files-repository.js +0 -181
- package/dist/storage/repositories/units-repository.js +0 -510
|
@@ -11,8 +11,8 @@ import { ConfigError } from "../errors.js";
|
|
|
11
11
|
import { liftLegacyEngineExtraParams } from "../extra-params.js";
|
|
12
12
|
import { formatRegistryLabel, hasRegistryUrlCredentials } from "../registry-url.js";
|
|
13
13
|
import { acquireConfigLock, backupExistingConfig, parseConfigText, readConfigText, withConfigLock, writeConfigAtomic, } from "./config-io.js";
|
|
14
|
-
import { AkmConfigSchema, CURRENT_CONFIG_VERSION } from "./config-schema.js";
|
|
15
|
-
import { bundleComponentConfig, bundleContentRoot, bundlesToSourceEntries } from "./config-sources.js";
|
|
14
|
+
import { AkmConfigSchema, CURRENT_CONFIG_VERSION, listTopLevelConfigKeys } from "./config-schema.js";
|
|
15
|
+
import { bundleComponentConfig, bundleContentRoot, bundleContentRoots, bundlesToSourceEntries } from "./config-sources.js";
|
|
16
16
|
import { upgradeConfigVersion } from "./config-version-shim.js";
|
|
17
17
|
import { deepMergeConfig, isPlainObject } from "./deep-merge.js";
|
|
18
18
|
import { migrateLegacySourceShape } from "./legacy-source-shape-shim.js";
|
|
@@ -192,21 +192,60 @@ function liftExtraParamsOrThrow(parsedRaw, sourcePath) {
|
|
|
192
192
|
* keeps around, instead of only getting the final merged `AkmConfig` back.
|
|
193
193
|
*/
|
|
194
194
|
function buildEffectiveConfig(liftedLocalRaw, sourcePath) {
|
|
195
|
+
warnUnknownTopLevelConfigKeys(liftedLocalRaw, sourcePath);
|
|
195
196
|
const withExtends = resolveExtendsChain(liftedLocalRaw, sourcePath);
|
|
196
197
|
const where = sourcePath ? ` at ${sourcePath}` : "";
|
|
197
198
|
const parsed = AkmConfigSchema.safeParse(withExtends);
|
|
198
199
|
if (!parsed.success) {
|
|
199
200
|
const lines = parsed.error.issues.map((i) => ` - ${i.path.join(".") || "(root)"}: ${i.message}`).join("\n");
|
|
200
|
-
|
|
201
|
+
const needsSchedulerMigration = parsed.error.issues.some((issue) => issue.path[0] === "scheduler" && issue.path.at(-1) === "sourceId");
|
|
202
|
+
throw new ConfigError(`Invalid config${where}:\n${lines}`, "INVALID_CONFIG_FILE", needsSchedulerMigration
|
|
203
|
+
? "Run `akm migrate apply` to bind existing scheduler grants to their source."
|
|
204
|
+
: undefined);
|
|
201
205
|
}
|
|
202
206
|
const merged = deepMergeConfig(DEFAULT_CONFIG, parsed.data);
|
|
203
207
|
const finalResult = AkmConfigSchema.safeParse(merged);
|
|
204
208
|
if (!finalResult.success) {
|
|
205
209
|
const lines = finalResult.error.issues.map((i) => ` - ${i.path.join(".") || "(root)"}: ${i.message}`).join("\n");
|
|
206
|
-
|
|
210
|
+
const needsSchedulerMigration = finalResult.error.issues.some((issue) => issue.path[0] === "scheduler" && issue.path.at(-1) === "sourceId");
|
|
211
|
+
throw new ConfigError(`Invalid merged config${sourcePath ? ` at ${sourcePath}` : ""}:\n${lines}`, "INVALID_CONFIG_FILE", needsSchedulerMigration
|
|
212
|
+
? "Run `akm migrate apply` to bind existing scheduler grants to their source."
|
|
213
|
+
: undefined);
|
|
207
214
|
}
|
|
215
|
+
assertUniquePhysicalBundleRoots(finalResult.data, sourcePath);
|
|
208
216
|
return finalResult.data;
|
|
209
217
|
}
|
|
218
|
+
const RETIRED_TOP_LEVEL_CONFIG_KEYS = new Set([
|
|
219
|
+
"agent",
|
|
220
|
+
"bindings",
|
|
221
|
+
"features",
|
|
222
|
+
"installed",
|
|
223
|
+
"llm",
|
|
224
|
+
"modelAliases",
|
|
225
|
+
"profiles",
|
|
226
|
+
"sources",
|
|
227
|
+
"stashDir",
|
|
228
|
+
"stashes",
|
|
229
|
+
"writable",
|
|
230
|
+
]);
|
|
231
|
+
function warnUnknownTopLevelConfigKeys(raw, sourcePath) {
|
|
232
|
+
const known = new Set(listTopLevelConfigKeys());
|
|
233
|
+
for (const key of Object.keys(raw).sort()) {
|
|
234
|
+
if (known.has(key) || RETIRED_TOP_LEVEL_CONFIG_KEYS.has(key))
|
|
235
|
+
continue;
|
|
236
|
+
warnOnce(`config:unknown-key:${sourcePath ?? "inline"}:${key}`, `Unknown config key ${JSON.stringify(key)}${sourcePath ? ` at ${sourcePath}` : ""} has no defined akm behavior. Check the spelling or remove it.`);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
function assertUniquePhysicalBundleRoots(config, sourcePath) {
|
|
240
|
+
const owners = new Map();
|
|
241
|
+
for (const { id, contentRoot } of bundleContentRoots(config)) {
|
|
242
|
+
const prior = owners.get(contentRoot);
|
|
243
|
+
if (prior !== undefined) {
|
|
244
|
+
throw new ConfigError(`Invalid config${sourcePath ? ` at ${sourcePath}` : ""}: bundles ${JSON.stringify(prior)} and ${JSON.stringify(id)} resolve to the same physical content root ${contentRoot}.`, "INVALID_CONFIG_FILE", "Configure one bundle id per physical source root; symbolic-link aliases are not separate bundles.");
|
|
245
|
+
}
|
|
246
|
+
owners.set(contentRoot, id);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
210
249
|
/**
|
|
211
250
|
* Parse raw config text and validate via Zod.
|
|
212
251
|
* ({@link AkmConfigSchema}). Returns the merged-with-defaults AkmConfig.
|
|
@@ -233,6 +272,7 @@ function collectExtendsLayers(localRaw, configPath) {
|
|
|
233
272
|
const visited = new Set(configPath ? [path.resolve(configPath)] : []);
|
|
234
273
|
let current = localRaw;
|
|
235
274
|
let currentPath = configPath;
|
|
275
|
+
let containmentRoot;
|
|
236
276
|
while (true) {
|
|
237
277
|
const ref = current.extends;
|
|
238
278
|
if (ref === undefined)
|
|
@@ -240,15 +280,18 @@ function collectExtendsLayers(localRaw, configPath) {
|
|
|
240
280
|
if (typeof ref !== "string" || !ref.trim()) {
|
|
241
281
|
throw new ConfigError(`Invalid "extends"${currentPath ? ` at ${currentPath}` : ""}: expected a non-empty string (a file path or bundle//path ref), got ${JSON.stringify(ref)}.`, "INVALID_CONFIG_FILE");
|
|
242
282
|
}
|
|
243
|
-
const
|
|
283
|
+
const resolved = resolveConfigRefSource(ref, current, currentPath, containmentRoot);
|
|
284
|
+
const { text, resolvedPath } = resolved;
|
|
244
285
|
if (visited.has(resolvedPath)) {
|
|
245
286
|
throw new ConfigError(`Config "extends" cycle detected: "${ref}"${currentPath ? ` (from ${currentPath})` : ""} resolves back to an already-visited config at ${resolvedPath}.`, "INVALID_CONFIG_FILE");
|
|
246
287
|
}
|
|
247
288
|
visited.add(resolvedPath);
|
|
248
289
|
const baseRaw = runConfigFilePipeline(text, resolvedPath);
|
|
290
|
+
warnUnknownTopLevelConfigKeys(baseRaw, resolvedPath);
|
|
249
291
|
layers.push({ ref, raw: baseRaw });
|
|
250
292
|
current = baseRaw;
|
|
251
293
|
currentPath = resolvedPath;
|
|
294
|
+
containmentRoot = resolved.containmentRoot;
|
|
252
295
|
}
|
|
253
296
|
}
|
|
254
297
|
/**
|
|
@@ -274,10 +317,84 @@ function resolveExtendsChain(localRaw, configPath) {
|
|
|
274
317
|
const layers = collectExtendsLayers(localRaw, configPath);
|
|
275
318
|
let merged = {};
|
|
276
319
|
for (let i = layers.length - 1; i >= 0; i--) {
|
|
277
|
-
|
|
320
|
+
const layer = layers[i];
|
|
321
|
+
merged = deepMergeConfig(merged, i > 0 ? sanitizeInheritedConfig(layer.raw, layer.ref ?? String(i)) : layer.raw);
|
|
278
322
|
}
|
|
279
323
|
return merged;
|
|
280
324
|
}
|
|
325
|
+
const HOST_LOCAL_CONFIG_KEYS = new Set([
|
|
326
|
+
"bundles",
|
|
327
|
+
"defaultBundle",
|
|
328
|
+
"defaultWriteTarget",
|
|
329
|
+
"embedding",
|
|
330
|
+
"execution",
|
|
331
|
+
"experimental",
|
|
332
|
+
"registries",
|
|
333
|
+
"scheduler",
|
|
334
|
+
"setup",
|
|
335
|
+
]);
|
|
336
|
+
/**
|
|
337
|
+
* Shared config contributes portable behavior only. Source ownership,
|
|
338
|
+
* credentials, executable paths/arguments, and activation remain in the
|
|
339
|
+
* host's top-level config even when a bundle supplies the inherited file.
|
|
340
|
+
* LLM endpoints and model selection remain portable; their credentials never
|
|
341
|
+
* do.
|
|
342
|
+
*/
|
|
343
|
+
function sanitizeInheritedConfig(raw, label) {
|
|
344
|
+
const inherited = { ...raw };
|
|
345
|
+
for (const key of HOST_LOCAL_CONFIG_KEYS) {
|
|
346
|
+
if (!Object.hasOwn(inherited, key))
|
|
347
|
+
continue;
|
|
348
|
+
delete inherited[key];
|
|
349
|
+
warnOnce(`config:inherited-host-local:${label}:${key}`, `Ignoring inherited config key ${JSON.stringify(key)} from ${label}; it is host-local and must be declared in the top-level config file.`);
|
|
350
|
+
}
|
|
351
|
+
if (isPlainObject(inherited.engines)) {
|
|
352
|
+
const engines = {};
|
|
353
|
+
let strippedAuthority = false;
|
|
354
|
+
for (const [name, engine] of Object.entries(inherited.engines)) {
|
|
355
|
+
if (!isPlainObject(engine)) {
|
|
356
|
+
engines[name] = engine;
|
|
357
|
+
continue;
|
|
358
|
+
}
|
|
359
|
+
const portable = { ...engine };
|
|
360
|
+
for (const key of ["apiKey", "apiKeyFile", "bin", "args", "workspace"]) {
|
|
361
|
+
if (!Object.hasOwn(portable, key))
|
|
362
|
+
continue;
|
|
363
|
+
delete portable[key];
|
|
364
|
+
strippedAuthority = true;
|
|
365
|
+
}
|
|
366
|
+
engines[name] = portable;
|
|
367
|
+
}
|
|
368
|
+
inherited.engines = engines;
|
|
369
|
+
if (strippedAuthority) {
|
|
370
|
+
warnOnce(`config:inherited-host-local:${label}:engines-authority`, `Ignoring inherited engine credentials, executable arguments, or workspace from ${label}; those fields are host-local.`);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
if (isPlainObject(inherited.search) && Object.hasOwn(inherited.search, "curateRerank")) {
|
|
374
|
+
const { curateRerank: _curateRerank, ...portableSearch } = inherited.search;
|
|
375
|
+
inherited.search = portableSearch;
|
|
376
|
+
warnOnce(`config:inherited-host-local:${label}:search.curateRerank`, `Ignoring inherited config key "search.curateRerank" from ${label}; network endpoints and credentials are host-local.`);
|
|
377
|
+
}
|
|
378
|
+
if (isPlainObject(inherited.improve) && isPlainObject(inherited.improve.strategies)) {
|
|
379
|
+
const strategies = {};
|
|
380
|
+
let strippedSync = false;
|
|
381
|
+
for (const [name, profile] of Object.entries(inherited.improve.strategies)) {
|
|
382
|
+
if (isPlainObject(profile) && Object.hasOwn(profile, "sync")) {
|
|
383
|
+
const { sync: _sync, ...portableProfile } = profile;
|
|
384
|
+
strategies[name] = portableProfile;
|
|
385
|
+
strippedSync = true;
|
|
386
|
+
}
|
|
387
|
+
else {
|
|
388
|
+
strategies[name] = profile;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
inherited.improve = { ...inherited.improve, strategies };
|
|
392
|
+
if (strippedSync) {
|
|
393
|
+
warnOnce(`config:inherited-host-local:${label}:improve.sync`, `Ignoring inherited improve strategy sync policy from ${label}; publication policy is host-local.`);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
return inherited;
|
|
397
|
+
}
|
|
281
398
|
/** `~` expands to the home directory, mirroring `apiKeyFile`'s resolution (engine-resolution.ts). */
|
|
282
399
|
function expandExtendsHomePath(p) {
|
|
283
400
|
return p.startsWith("~") ? path.join(os.homedir(), p.slice(1)) : p;
|
|
@@ -299,12 +416,12 @@ function looksLikeBundleAssetRef(ref) {
|
|
|
299
416
|
* a missing file/bundle throws {@link ConfigError} naming the ref, as does an
|
|
300
417
|
* empty, absolute, or content-root-escaping path after `//`.
|
|
301
418
|
*/
|
|
302
|
-
function resolveConfigRefSource(ref, context, fromConfigPath) {
|
|
419
|
+
function resolveConfigRefSource(ref, context, fromConfigPath, containmentRoot) {
|
|
303
420
|
return looksLikeBundleAssetRef(ref)
|
|
304
|
-
? resolveConfigBundleRefSource(ref, context)
|
|
305
|
-
: resolveConfigFileRefSource(ref, fromConfigPath);
|
|
421
|
+
? resolveConfigBundleRefSource(ref, context, containmentRoot)
|
|
422
|
+
: resolveConfigFileRefSource(ref, fromConfigPath, containmentRoot);
|
|
306
423
|
}
|
|
307
|
-
function resolveConfigFileRefSource(ref, fromConfigPath) {
|
|
424
|
+
function resolveConfigFileRefSource(ref, fromConfigPath, containmentRoot) {
|
|
308
425
|
const expanded = expandExtendsHomePath(ref);
|
|
309
426
|
let resolvedPath;
|
|
310
427
|
if (path.isAbsolute(expanded)) {
|
|
@@ -316,13 +433,15 @@ function resolveConfigFileRefSource(ref, fromConfigPath) {
|
|
|
316
433
|
else {
|
|
317
434
|
throw new ConfigError(`extends "${ref}" is a relative path, but the current config has no known file location to resolve it against.`, "INVALID_CONFIG_FILE");
|
|
318
435
|
}
|
|
436
|
+
if (containmentRoot !== undefined)
|
|
437
|
+
assertConfigExtendsPhysicalContainment(containmentRoot, resolvedPath, ref);
|
|
319
438
|
const text = readConfigText(resolvedPath);
|
|
320
439
|
if (text === undefined) {
|
|
321
440
|
throw new ConfigError(`extends "${ref}" resolves to ${resolvedPath}, which does not exist. Create the file first, or point "extends" at an existing config.`, "INVALID_CONFIG_FILE");
|
|
322
441
|
}
|
|
323
|
-
return { text, resolvedPath };
|
|
442
|
+
return { text, resolvedPath, ...(containmentRoot ? { containmentRoot } : {}) };
|
|
324
443
|
}
|
|
325
|
-
function resolveConfigBundleRefSource(ref, context) {
|
|
444
|
+
function resolveConfigBundleRefSource(ref, context, outerContainmentRoot) {
|
|
326
445
|
// Split by hand rather than through `parseBundleRef`: the part after `//`
|
|
327
446
|
// is a plain file path here, not an asset conceptId, so it must not be run
|
|
328
447
|
// through conceptId validation (which, for instance, rejects every `..`
|
|
@@ -360,11 +479,31 @@ function resolveConfigBundleRefSource(ref, context) {
|
|
|
360
479
|
if (relativeToRoot === ".." || relativeToRoot.startsWith(`..${path.sep}`) || path.isAbsolute(relativeToRoot)) {
|
|
361
480
|
throw new ConfigError(`extends "${ref}" escapes bundle "${bundleId}"'s content root.`, "INVALID_CONFIG_FILE");
|
|
362
481
|
}
|
|
482
|
+
const containmentRoot = fs.realpathSync.native(bundleRoot);
|
|
483
|
+
assertConfigExtendsPhysicalContainment(containmentRoot, resolvedPath, ref);
|
|
484
|
+
if (outerContainmentRoot !== undefined) {
|
|
485
|
+
assertConfigExtendsPhysicalContainment(outerContainmentRoot, resolvedPath, ref);
|
|
486
|
+
}
|
|
363
487
|
const text = readConfigText(resolvedPath);
|
|
364
488
|
if (text === undefined) {
|
|
365
489
|
throw new ConfigError(`extends "${ref}" resolves to ${resolvedPath}, which does not exist locally. Sync the bundle first, or point "extends" at an existing file.`, "INVALID_CONFIG_FILE");
|
|
366
490
|
}
|
|
367
|
-
return { text, resolvedPath };
|
|
491
|
+
return { text, resolvedPath, containmentRoot: outerContainmentRoot ?? containmentRoot };
|
|
492
|
+
}
|
|
493
|
+
function assertConfigExtendsPhysicalContainment(root, candidate, ref) {
|
|
494
|
+
let physicalRoot;
|
|
495
|
+
let physicalCandidate;
|
|
496
|
+
try {
|
|
497
|
+
physicalRoot = fs.realpathSync.native(root);
|
|
498
|
+
physicalCandidate = fs.realpathSync.native(candidate);
|
|
499
|
+
}
|
|
500
|
+
catch (cause) {
|
|
501
|
+
throw new ConfigError(`Unable to verify physical containment for extends ${JSON.stringify(ref)}: ${cause instanceof Error ? cause.message : String(cause)}`, "INVALID_CONFIG_FILE");
|
|
502
|
+
}
|
|
503
|
+
const relative = path.relative(physicalRoot, physicalCandidate);
|
|
504
|
+
if (relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
|
|
505
|
+
throw new ConfigError(`extends ${JSON.stringify(ref)} resolves through a symbolic link outside its bundle content root.`, "INVALID_CONFIG_FILE");
|
|
506
|
+
}
|
|
368
507
|
}
|
|
369
508
|
/**
|
|
370
509
|
* `akm config get --show-source` (#945): which raw layer the dotted path's
|
|
@@ -382,7 +521,8 @@ export function getConfigValueSource(dotted) {
|
|
|
382
521
|
const liftedConfig = runConfigFilePipeline(text, configPath);
|
|
383
522
|
const segments = dotted.split(".").filter((s) => s.length > 0);
|
|
384
523
|
for (const layer of collectExtendsLayers(liftedConfig, configPath)) {
|
|
385
|
-
|
|
524
|
+
const effectiveLayer = layer.ref === undefined ? layer.raw : sanitizeInheritedConfig(layer.raw, layer.ref);
|
|
525
|
+
if (hasRawPath(effectiveLayer, segments)) {
|
|
386
526
|
return layer.ref === undefined ? "local" : `extends:${layer.ref}`;
|
|
387
527
|
}
|
|
388
528
|
}
|
|
@@ -487,15 +627,41 @@ function pruneUnchangedInheritedFields(before, after, localRaw) {
|
|
|
487
627
|
}
|
|
488
628
|
/**
|
|
489
629
|
* What to persist for a `mutateConfig`/`mutateConfigWithPrecommit` write:
|
|
490
|
-
*
|
|
491
|
-
*
|
|
492
|
-
* (#
|
|
630
|
+
* only the changed-or-already-local fields. This applies both to inherited
|
|
631
|
+
* configs and ordinary configs: lifecycle mutations must not serialize every
|
|
632
|
+
* schema default merely because validation materialized it in memory (#972).
|
|
493
633
|
*/
|
|
494
|
-
function configWriteBody(localRaw, current, next) {
|
|
495
|
-
|
|
496
|
-
|
|
634
|
+
function configWriteBody(localRaw, current, next, persistTopLevelKeys = []) {
|
|
635
|
+
// A first write keeps the established full-default scaffold. Besides being
|
|
636
|
+
// useful to a new user, the add -> rejected-install -> remove lifecycle
|
|
637
|
+
// relies on that symmetry to return a pristine install to DEFAULT_CONFIG.
|
|
638
|
+
if (localRaw === undefined)
|
|
497
639
|
return next;
|
|
498
|
-
|
|
640
|
+
const pruned = pruneUnchangedInheritedFields(current, next, localRaw);
|
|
641
|
+
// A caller may need to persist an explicit user choice even when it equals
|
|
642
|
+
// the schema default. Interactive setup uses this for semanticSearchMode:
|
|
643
|
+
// "off" means the user declined the opt-in, not an incidental default that
|
|
644
|
+
// should disappear from the saved configuration.
|
|
645
|
+
for (const key of persistTopLevelKeys) {
|
|
646
|
+
if (Object.hasOwn(next, key))
|
|
647
|
+
pruned[key] = next[key];
|
|
648
|
+
}
|
|
649
|
+
pruned.configVersion = CURRENT_CONFIG_VERSION;
|
|
650
|
+
// Keep existing top-level keys in their authored order. Lifecycle rollback
|
|
651
|
+
// writes the same logical object twice (add, then remove); reordering those
|
|
652
|
+
// surviving keys would violate its byte-parity guarantee even though the
|
|
653
|
+
// parsed JSON is equivalent. Newly changed keys follow in `next` order,
|
|
654
|
+
// while nested maps (notably `bundles`) retain mutation-selected ordering.
|
|
655
|
+
const ordered = {};
|
|
656
|
+
for (const key of Object.keys(localRaw)) {
|
|
657
|
+
if (Object.hasOwn(pruned, key))
|
|
658
|
+
ordered[key] = pruned[key];
|
|
659
|
+
}
|
|
660
|
+
for (const [key, value] of Object.entries(pruned)) {
|
|
661
|
+
if (!Object.hasOwn(ordered, key))
|
|
662
|
+
ordered[key] = value;
|
|
663
|
+
}
|
|
664
|
+
return ordered;
|
|
499
665
|
}
|
|
500
666
|
/**
|
|
501
667
|
* Mutate config under one fail-closed lock spanning read, merge, validation,
|
|
@@ -527,7 +693,7 @@ export function mutateConfig(mutate, options) {
|
|
|
527
693
|
* side effect. Setup uses this to reject a three-way conflict before creating
|
|
528
694
|
* its stash, while preventing another config writer from racing the final save.
|
|
529
695
|
*/
|
|
530
|
-
export async function mutateConfigWithPrecommit(mutate, precommit) {
|
|
696
|
+
export async function mutateConfigWithPrecommit(mutate, precommit, options) {
|
|
531
697
|
cachedConfig = undefined;
|
|
532
698
|
const configPath = getConfigPath();
|
|
533
699
|
const release = acquireConfigLock();
|
|
@@ -543,7 +709,7 @@ export async function mutateConfigWithPrecommit(mutate, precommit) {
|
|
|
543
709
|
if (mutated === current)
|
|
544
710
|
return { config: current, written: false, precommit: precommitResult };
|
|
545
711
|
fs.mkdirSync(path.dirname(configPath), { recursive: true });
|
|
546
|
-
writeConfigAtomic(configPath, sanitizeConfigForWrite(configWriteBody(localRaw, current, next)));
|
|
712
|
+
writeConfigAtomic(configPath, sanitizeConfigForWrite(configWriteBody(localRaw, current, next, options?.persistTopLevelKeys)));
|
|
547
713
|
return { config: next, written: true, precommit: precommitResult };
|
|
548
714
|
}
|
|
549
715
|
finally {
|
|
@@ -682,7 +848,7 @@ export function getIndexPassConfig(config, passName) {
|
|
|
682
848
|
return entry;
|
|
683
849
|
}
|
|
684
850
|
// Re-export source runtime helpers — implementation lives in config-sources.ts.
|
|
685
|
-
export { bundleComponentConfig, bundleContentRoot, bundleContentRoots, bundleEntryToSourceEntry, bundleKeyForContentRoot, bundlesToSourceEntries, installedSourceDescriptor, parseSourceSpec, primaryBundlePath, resolveConfiguredSources, } from "./config-sources.js";
|
|
851
|
+
export { bundleComponentConfig, bundleContentRoot, bundleContentRoots, bundleEntryToSourceEntry, bundleKeyForContentRoot, bundlePhysicalContentRoot, bundleSourceId, bundlesToSourceEntries, installedSourceDescriptor, isBundleEnabled, parseSourceSpec, primaryBundlePath, resolveActiveConfiguredSources, resolveConfiguredSources, } from "./config-sources.js";
|
|
686
852
|
/**
|
|
687
853
|
* Merge a partial user-config override onto a base config. Used by
|
|
688
854
|
* {@link loadUserConfig} (DEFAULT_CONFIG + on-disk) and {@link updateConfig}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import path from "node:path";
|
|
1
2
|
import { isBundleSlug } from "../asset/asset-ref.js";
|
|
2
3
|
import { warnOnce } from "../warn.js";
|
|
3
4
|
function isPlainRecord(value) {
|
|
@@ -49,6 +50,11 @@ function bundleFromLegacySource(entry, index) {
|
|
|
49
50
|
const key = name && isBundleSlug(name) ? name : `source-${index + 1}`;
|
|
50
51
|
return [key, bundle];
|
|
51
52
|
}
|
|
53
|
+
function filesystemLocator(bundle) {
|
|
54
|
+
if (!isPlainRecord(bundle) || typeof bundle.path !== "string" || bundle.path.length === 0)
|
|
55
|
+
return undefined;
|
|
56
|
+
return path.resolve(bundle.path);
|
|
57
|
+
}
|
|
52
58
|
export function migrateLegacySourceShape(raw, sourcePath) {
|
|
53
59
|
const hasStashDir = typeof raw.stashDir === "string" && raw.stashDir.trim().length > 0;
|
|
54
60
|
const hasSources = Array.isArray(raw.sources) && raw.sources.length > 0;
|
|
@@ -68,6 +74,9 @@ export function migrateLegacySourceShape(raw, sourcePath) {
|
|
|
68
74
|
if (!converted)
|
|
69
75
|
return;
|
|
70
76
|
const [key, bundle] = converted;
|
|
77
|
+
const locator = filesystemLocator(bundle);
|
|
78
|
+
if (locator && Object.values(bundles).some((candidate) => filesystemLocator(candidate) === locator))
|
|
79
|
+
return;
|
|
71
80
|
bundles[key] = bundle;
|
|
72
81
|
defaultBundle ??= key;
|
|
73
82
|
});
|
|
@@ -33,6 +33,32 @@ export const EmbeddingConnectionConfigSchema = z
|
|
|
33
33
|
// `akm index` when ensureSchema rejects it (§24.2 "Semantic" gate).
|
|
34
34
|
dimension: positiveInt.max(4096).optional(),
|
|
35
35
|
localModel: z.string().min(1).optional(),
|
|
36
|
+
/**
|
|
37
|
+
* Per-document token cap applied BEFORE batching (default 512,
|
|
38
|
+
* `DEFAULT_MAX_INPUT_TOKENS` in `src/llm/embedders/remote.ts`, #956).
|
|
39
|
+
* The materializer truncates a document's embedded text to
|
|
40
|
+
* this cap (head only, unicode-safe) instead of skipping it outright, so
|
|
41
|
+
* one oversized entry can no longer fail a whole batch. Distinct from
|
|
42
|
+
* `maxTokens` below, which bounds a whole HTTP REQUEST (many documents);
|
|
43
|
+
* this bounds one DOCUMENT.
|
|
44
|
+
*/
|
|
45
|
+
maxInputTokens: positiveInt.optional(),
|
|
46
|
+
/**
|
|
47
|
+
* Client-side per-request token budget — how many documents' estimated
|
|
48
|
+
* tokens fit in one HTTP request (default `DEFAULT_TOKEN_BUDGET` = 6000
|
|
49
|
+
* in `src/llm/embedders/remote.ts`). With the 512-token `maxInputTokens`
|
|
50
|
+
* cap above, a request carries about 11 documents by default.
|
|
51
|
+
*/
|
|
52
|
+
maxTokens: positiveInt.optional(),
|
|
53
|
+
batchSize: positiveInt.optional(),
|
|
54
|
+
/**
|
|
55
|
+
* Ollama's `num_ctx` ONLY (#956) — sent verbatim as
|
|
56
|
+
* `options.num_ctx` on the native `/api/embed` request. It no longer also
|
|
57
|
+
* feeds the client-side request token budget (`maxTokens` above): the two
|
|
58
|
+
* used to share this one field, so setting it for the server's context
|
|
59
|
+
* window silently changed request batching too.
|
|
60
|
+
*/
|
|
61
|
+
contextLength: positiveInt.optional(),
|
|
36
62
|
ollamaOptions: EmbeddingOllamaOptionsSchema.optional(),
|
|
37
63
|
/**
|
|
38
64
|
* Per-request timeout in milliseconds for a remote embedding request
|
|
@@ -46,13 +72,10 @@ export const EmbeddingConnectionConfigSchema = z
|
|
|
46
72
|
* Overrides the fixed in-flight request window (#954, added after field
|
|
47
73
|
* evidence from multi-slot local servers). Bounded 1-16. Unset keeps
|
|
48
74
|
* today's default: 1 for a loopback endpoint, 2 for a remote one
|
|
49
|
-
* (`resolveEmbeddingConcurrency`, `src/llm/embedders/remote.ts`)
|
|
50
|
-
*
|
|
51
|
-
* (`
|
|
52
|
-
* `
|
|
53
|
-
* serves parallel requests (llama.cpp `--parallel N`, vLLM) — request
|
|
54
|
-
* SIZE, packed against the provider's own probed context window, remains
|
|
55
|
-
* the first throughput lever.
|
|
75
|
+
* (`resolveEmbeddingConcurrency`, `src/llm/embedders/remote.ts`). Set it
|
|
76
|
+
* only for an endpoint that genuinely serves parallel requests (llama.cpp
|
|
77
|
+
* `--parallel N`, vLLM) — request SIZE (`batchSize`, `maxTokens`/
|
|
78
|
+
* `contextLength`) remains the first throughput lever.
|
|
56
79
|
*/
|
|
57
80
|
concurrency: positiveInt.max(16).optional(),
|
|
58
81
|
})
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
const toolName = z.string().trim().min(1, "expected a non-empty tool name");
|
|
6
|
+
/** Host-owned ceiling for capabilities requested by executable assets. */
|
|
7
|
+
export const ExecutionPolicyConfigSchema = z
|
|
8
|
+
.object({
|
|
9
|
+
/** Exact tool names an asset may request. `*` is an explicit allow-all ceiling. */
|
|
10
|
+
allowedTools: z
|
|
11
|
+
.array(toolName)
|
|
12
|
+
.default([])
|
|
13
|
+
.superRefine((tools, ctx) => {
|
|
14
|
+
const seen = new Set();
|
|
15
|
+
for (const [index, tool] of tools.entries()) {
|
|
16
|
+
if (seen.has(tool)) {
|
|
17
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, path: [index], message: "duplicates an earlier tool name" });
|
|
18
|
+
}
|
|
19
|
+
seen.add(tool);
|
|
20
|
+
}
|
|
21
|
+
}),
|
|
22
|
+
})
|
|
23
|
+
.strict();
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { nonEmptyString } from "./primitives.js";
|
|
6
|
+
/** One host-local grant allowing an authored asset to create scheduler bindings. */
|
|
7
|
+
export const SchedulerActivationSchema = z
|
|
8
|
+
.object({
|
|
9
|
+
kind: z.enum(["task", "workflow"]),
|
|
10
|
+
ref: nonEmptyString,
|
|
11
|
+
/** Identity of the configured source this host approved, not merely its mutable bundle name. */
|
|
12
|
+
sourceId: z.string().regex(/^sha256:[0-9a-f]{64}$/),
|
|
13
|
+
})
|
|
14
|
+
.strict();
|
|
15
|
+
/** Absence from this allow-list means disabled. */
|
|
16
|
+
export const SchedulerConfigSchema = z
|
|
17
|
+
.object({
|
|
18
|
+
enabled: z.array(SchedulerActivationSchema).default([]),
|
|
19
|
+
})
|
|
20
|
+
.strict();
|
|
@@ -23,9 +23,8 @@ const SearchGraphBoostSchema = z
|
|
|
23
23
|
})
|
|
24
24
|
.passthrough();
|
|
25
25
|
/**
|
|
26
|
-
* `search.
|
|
27
|
-
*
|
|
28
|
-
* cross-encoder rerank pass over search's already-ranked LOCAL stash hits.
|
|
26
|
+
* `search.curateRerank` (#951) — an optional cross-encoder rerank pass over
|
|
27
|
+
* `akm curate`'s already-selected candidates.
|
|
29
28
|
*
|
|
30
29
|
* Deliberately its own small config arm rather than a third member of the
|
|
31
30
|
* `engines` map (`EngineConfigSchema` in ./engines.ts): that union's "llm" /
|
|
@@ -38,27 +37,26 @@ const SearchGraphBoostSchema = z
|
|
|
38
37
|
* connection shape as an LLM engine without inheriting that machinery.
|
|
39
38
|
*
|
|
40
39
|
* `curate_rerank` was removed as a dead `llm.features.*` key in 0.8.0 (no
|
|
41
|
-
* implementation ever sent a request);
|
|
42
|
-
*
|
|
43
|
-
* moves it to search and renames the key (no compatibility alias — the old
|
|
44
|
-
* key shipped hours earlier, default-off, so nobody has it meaningfully set).
|
|
40
|
+
* implementation ever sent a request); this is a new, real implementation,
|
|
41
|
+
* disabled by default.
|
|
45
42
|
*/
|
|
46
|
-
export const
|
|
43
|
+
export const CurateRerankConfigSchema = z
|
|
47
44
|
.object({
|
|
48
45
|
enabled: z.boolean().optional(),
|
|
49
46
|
/** Full URL of the reranker's rerank endpoint, e.g. `http://host:port/rerank`. */
|
|
50
47
|
endpoint: httpUrl.optional(),
|
|
51
48
|
model: nonEmptyString.optional(),
|
|
52
|
-
apiKey: symbolicOrWarnApiKey("search.
|
|
49
|
+
apiKey: symbolicOrWarnApiKey("search.curateRerank.apiKey").optional(),
|
|
53
50
|
timeoutMs: positiveInt.optional(),
|
|
54
|
-
/** How many of
|
|
51
|
+
/** How many of curate's already-ranked candidates to send to the reranker. Default 8. */
|
|
55
52
|
topN: positiveInt.max(50).optional(),
|
|
56
53
|
})
|
|
57
|
-
.
|
|
54
|
+
.strict();
|
|
58
55
|
export const SearchConfigSchema = z
|
|
59
56
|
.object({
|
|
57
|
+
minScore: nonNegativeNumber.optional(),
|
|
60
58
|
defaultExcludeTypes: z.array(nonEmptyString).optional(),
|
|
61
59
|
graphBoost: SearchGraphBoostSchema.optional(),
|
|
62
|
-
|
|
60
|
+
curateRerank: CurateRerankConfigSchema.optional(),
|
|
63
61
|
})
|
|
64
62
|
.passthrough();
|
|
@@ -13,7 +13,7 @@ import { z } from "zod";
|
|
|
13
13
|
// and, transitively, the indexer modules they delegate to).
|
|
14
14
|
import { VALID_ADAPTER_IDS } from "../../adapter/adapter-ids.js";
|
|
15
15
|
import { isBundleSlug } from "../../asset/asset-ref.js";
|
|
16
|
-
import { httpUrl, nonEmptyString, positiveInt } from "./primitives.js";
|
|
16
|
+
import { httpUrl, isApiKeyReference, nonEmptyString, positiveInt } from "./primitives.js";
|
|
17
17
|
const VALID_ADAPTER_IDS_SET = new Set(VALID_ADAPTER_IDS);
|
|
18
18
|
// ── Sources / registries / installed ────────────────────────────────────────
|
|
19
19
|
const SourceConfigEntryOptionsSchema = z.record(z.unknown());
|
|
@@ -25,6 +25,7 @@ export const SourceConfigEntrySchema = z
|
|
|
25
25
|
name: z.string().min(1).optional(),
|
|
26
26
|
enabled: z.boolean().optional(),
|
|
27
27
|
writable: z.boolean().optional(),
|
|
28
|
+
credential: z.string().min(1).optional(),
|
|
28
29
|
primary: z.boolean().optional(),
|
|
29
30
|
options: SourceConfigEntryOptionsSchema.optional(),
|
|
30
31
|
})
|
|
@@ -52,6 +53,20 @@ export const SourceConfigEntrySchema = z
|
|
|
52
53
|
").",
|
|
53
54
|
});
|
|
54
55
|
}
|
|
56
|
+
if (entry.credential !== undefined && entry.type !== "git") {
|
|
57
|
+
ctx.addIssue({
|
|
58
|
+
code: z.ZodIssueCode.custom,
|
|
59
|
+
path: ["credential"],
|
|
60
|
+
message: "credential is only supported on git sources",
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
if (entry.credential !== undefined && !isApiKeyReference(entry.credential)) {
|
|
64
|
+
ctx.addIssue({
|
|
65
|
+
code: z.ZodIssueCode.custom,
|
|
66
|
+
path: ["credential"],
|
|
67
|
+
message: "git credential must be a $VAR or secret://<name> reference",
|
|
68
|
+
});
|
|
69
|
+
}
|
|
55
70
|
});
|
|
56
71
|
export const RegistryConfigEntrySchema = z
|
|
57
72
|
.object({
|
|
@@ -110,6 +125,8 @@ export const BundleConfigEntrySchema = z
|
|
|
110
125
|
git: z.string().min(1).optional(),
|
|
111
126
|
website: BundleWebsiteDescriptorSchema.optional(),
|
|
112
127
|
npm: z.string().min(1).optional(),
|
|
128
|
+
/** Symbolic HTTPS bearer credential; resolved only at the Git subprocess boundary. */
|
|
129
|
+
credential: z.string().min(1).optional(),
|
|
113
130
|
writable: z.boolean().optional(),
|
|
114
131
|
// Opt a bundle out of indexing, search, refresh, and write targeting
|
|
115
132
|
// without deleting it. The runtime honors the derived value in write and
|
|
@@ -150,6 +167,20 @@ export const BundleConfigEntrySchema = z
|
|
|
150
167
|
message: "writable: true is only supported on path and git bundle sources",
|
|
151
168
|
});
|
|
152
169
|
}
|
|
170
|
+
if (entry.credential !== undefined && entry.git === undefined) {
|
|
171
|
+
ctx.addIssue({
|
|
172
|
+
code: z.ZodIssueCode.custom,
|
|
173
|
+
path: ["credential"],
|
|
174
|
+
message: "credential is only supported on git bundle sources",
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
if (entry.credential !== undefined && !isApiKeyReference(entry.credential)) {
|
|
178
|
+
ctx.addIssue({
|
|
179
|
+
code: z.ZodIssueCode.custom,
|
|
180
|
+
path: ["credential"],
|
|
181
|
+
message: "git credential must be a $VAR or secret://<name> reference",
|
|
182
|
+
});
|
|
183
|
+
}
|
|
153
184
|
const componentEntries = entry.components ? Object.entries(entry.components) : [];
|
|
154
185
|
if (entry.components !== undefined && componentEntries.length !== 1) {
|
|
155
186
|
ctx.addIssue({
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
|
+
/** Marker emitted by output redaction. It must never become durable asset content. */
|
|
5
|
+
export const REDACTED_CONTENT_MARKER = "[REDACTED]";
|
|
6
|
+
/** Reflect prompt section that contains run diagnostics, not proposed asset content. */
|
|
7
|
+
export const REFLECT_AVOID_PATTERNS_HEADING = "Avoid These Patterns";
|
|
8
|
+
const REFLECT_AVOID_PATTERNS_RE = /^##[ \t]+Avoid These Patterns[ \t]*$/i;
|
|
9
|
+
const SECTION_BOUNDARY_RE = /^#{1,2}(?:[ \t]+|$)/;
|
|
10
|
+
export function containsRedactedContent(content) {
|
|
11
|
+
return content.includes(REDACTED_CONTENT_MARKER);
|
|
12
|
+
}
|
|
13
|
+
export function containsReflectPromptScaffolding(content) {
|
|
14
|
+
return content.split(/\r?\n/).some((line) => REFLECT_AVOID_PATTERNS_RE.test(line));
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Remove every echoed run-only "Avoid These Patterns" section while preserving
|
|
18
|
+
* the next peer/top-level section. Reflect alone calls this sanitizer; authored
|
|
19
|
+
* source assets are never rewritten by this helper.
|
|
20
|
+
*/
|
|
21
|
+
export function stripReflectPromptScaffolding(content) {
|
|
22
|
+
const newline = content.includes("\r\n") ? "\r\n" : "\n";
|
|
23
|
+
const lines = content.split(/\r?\n/);
|
|
24
|
+
const kept = [];
|
|
25
|
+
let stripped = false;
|
|
26
|
+
for (let index = 0; index < lines.length;) {
|
|
27
|
+
if (!REFLECT_AVOID_PATTERNS_RE.test(lines[index] ?? "")) {
|
|
28
|
+
kept.push(lines[index] ?? "");
|
|
29
|
+
index += 1;
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
stripped = true;
|
|
33
|
+
index += 1;
|
|
34
|
+
while (index < lines.length && !SECTION_BOUNDARY_RE.test(lines[index] ?? ""))
|
|
35
|
+
index += 1;
|
|
36
|
+
}
|
|
37
|
+
return { content: kept.join(newline).replace(/(?:\r?\n){3,}/g, `${newline}${newline}`), stripped };
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Return a secret-free rejection reason when generated content is unsafe to
|
|
41
|
+
* persist. `redactedContent` is the same body after applying the dispatch
|
|
42
|
+
* lease's sensitive-value inventory; comparing it avoids exposing the value.
|
|
43
|
+
*/
|
|
44
|
+
export function generatedContentRejection(content, redactedContent) {
|
|
45
|
+
if (containsRedactedContent(content)) {
|
|
46
|
+
return `Agent proposal content contains ${REDACTED_CONTENT_MARKER}; refusing to persist already-redacted text.`;
|
|
47
|
+
}
|
|
48
|
+
if (redactedContent !== content) {
|
|
49
|
+
return "Agent proposal content echoed a configured credential; refusing to persist either the secret or a redacted replacement.";
|
|
50
|
+
}
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|