akm-cli 0.9.15 → 0.9.16-alpha.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +52 -0
- package/dist/assets/hints/cli-hints-full.md +13 -6
- package/dist/assets/tasks/improve/akm-improve-catchup.yml +3 -6
- package/dist/cli/retired-commands.js +0 -2
- package/dist/commands/env/env-binding.js +4 -4
- package/dist/commands/env/env-cli.js +3 -3
- package/dist/commands/improve/improve-cli.js +19 -14
- package/dist/commands/improve/reflect.js +23 -2
- package/dist/commands/lint/base-linter.js +9 -0
- package/dist/commands/lint/env-key-rules.js +2 -2
- package/dist/commands/proposal/propose.js +15 -1
- package/dist/commands/proposal/validators/proposal-quality-validators.js +40 -3
- package/dist/commands/proposal/validators/proposal-validators.js +5 -4
- package/dist/commands/read/search.js +33 -4
- package/dist/commands/read/show.js +21 -2
- package/dist/commands/registry-cli.js +5 -5
- package/dist/commands/sources/add-cli.js +59 -16
- package/dist/commands/sources/bundle-cli.js +35 -11
- package/dist/commands/sources/bundle-config-ops.js +30 -0
- package/dist/commands/sources/dangerous-env-audit.js +4 -4
- package/dist/commands/sources/installed-stashes.js +43 -28
- package/dist/commands/sources/source-add.js +33 -17
- package/dist/commands/sources/source-manage.js +34 -12
- package/dist/commands/sources/stash-skeleton.js +6 -3
- package/dist/commands/tasks/explain.js +4 -1
- package/dist/commands/tasks/tasks-cli.js +31 -9
- package/dist/commands/tasks/tasks.js +239 -194
- package/dist/commands/tasks/validate.js +20 -32
- package/dist/core/activation-policy.js +4 -4
- package/dist/core/adapter/adapters/akm-adapter.js +5 -0
- package/dist/core/adapter/execution-source.js +10 -29
- package/dist/core/config/config-schema.js +64 -8
- package/dist/core/config/config-sources.js +96 -2
- package/dist/core/config/config.js +190 -24
- package/dist/core/config/legacy-source-shape-shim.js +9 -0
- package/dist/core/config/schema/execution.js +23 -0
- package/dist/core/config/schema/experimental.js +1 -1
- package/dist/core/config/schema/scheduler.js +20 -0
- package/dist/core/config/schema/search.js +1 -1
- package/dist/core/config/schema/sources-bundles.js +32 -1
- package/dist/core/content-safety.js +52 -0
- package/dist/core/maintenance-barrier.js +6 -6
- package/dist/core/type-presentation.js +1 -1
- package/dist/core/write-source.js +13 -8
- package/dist/indexer/bundle-identity-guard.js +45 -8
- package/dist/indexer/indexer.js +1 -1
- package/dist/indexer/materialize-embeddings.js +15 -1
- package/dist/indexer/search/search-source.js +29 -11
- package/dist/integrations/agent/execution-lowering.js +3 -2
- package/dist/integrations/agent/execution-preparation.js +32 -1
- package/dist/integrations/agent/prompts.js +1 -1
- package/dist/integrations/agent/request-lowering.js +3 -2
- package/dist/llm/client.js +2 -1
- package/dist/output/shapes/passthrough.js +2 -0
- package/dist/registry/resolve.js +37 -10
- package/dist/scripts/akm-migrate-node.js +13644 -9894
- package/dist/scripts/akm-migrate.js +12626 -8876
- package/dist/setup/setup.js +3 -3
- package/dist/setup/steps/tasks.js +29 -36
- package/dist/sources/providers/git-install.js +17 -11
- package/dist/sources/providers/git-provider.js +12 -5
- package/dist/sources/providers/git-stash.js +38 -16
- package/dist/sources/snapshot-fetchers/website-ingest.js +3 -3
- package/dist/storage/repositories/index-vec-repository.js +100 -0
- package/dist/tasks/activation-config.js +90 -0
- package/dist/tasks/backends/cron.js +9 -0
- package/dist/tasks/backends/launchd.js +1 -0
- package/dist/tasks/backends/schtasks.js +2 -0
- package/dist/tasks/embedded.js +4 -5
- package/dist/tasks/scheduler-binding.js +2 -2
- package/dist/tasks/scheduler-sync-preview.js +8 -1
- package/dist/tasks/scheduler-sync.js +19 -10
- package/dist/tasks/source/parse-task-source.js +10 -113
- package/dist/tasks/source/project-v4.js +2 -2
- package/dist/tasks/source/task-source-v4.js +4 -12
- package/dist/tasks/source/task-to-v3.js +4 -12
- package/dist/tasks/source/task-to-v4.js +40 -7
- package/docs/migration/README.md +1 -0
- package/docs/migration/release-notes/0.9.16.md +72 -0
- package/docs/migration/v0.9.1-to-v0.9.2.md +6 -9
- package/docs/reference/cli.md +37 -29
- package/docs/reference/configuration.md +48 -5
- package/docs/reference/tasks.md +34 -29
- package/package.json +1 -1
- package/schemas/akm-config.json +112 -4
- package/schemas/akm-task.json +1 -2
|
@@ -9,23 +9,18 @@
|
|
|
9
9
|
* a bundle/adapter/concept id at all — it reads exactly the path it was
|
|
10
10
|
* given and classifies it.
|
|
11
11
|
*
|
|
12
|
-
* Reuses the exact
|
|
13
|
-
* (`src/tasks/source/parse-task-source.ts`)
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* the SAME front-end helpers that shim itself calls first; they are used
|
|
18
|
-
* here only to recover the file's ORIGINALLY DECLARED schema version for
|
|
19
|
-
* the report, because `parseTaskSource`'s own `ParsedTaskSource.version` is
|
|
20
|
-
* always `4` post-shim — it cannot answer "was this a v2/v3/v4 file?" on
|
|
21
|
-
* its own once a v2/v3 source has been converted in memory.
|
|
12
|
+
* Reuses the exact current-schema router `parseTaskSource`
|
|
13
|
+
* (`src/tasks/source/parse-task-source.ts`) used by every other task-source
|
|
14
|
+
* reader. Historical v2/v3 documents are deliberately rejected here and
|
|
15
|
+
* point to `akm migrate apply`; validation does not embed a second legacy
|
|
16
|
+
* parser or perform an in-memory migration.
|
|
22
17
|
*
|
|
23
18
|
* Beyond parsing, this module also runs the SAME two per-source gates
|
|
24
19
|
* `akm task sync`'s `compileTaskSources` runs before it ever installs a
|
|
25
20
|
* schedule — `assertTaskScheduleInputsSatisfyContract` and
|
|
26
21
|
* `assertTaskScheduleCronValid` (both extracted from `scheduler-sync.ts` for
|
|
27
22
|
* exactly this reuse) — so a file `sync` would reject can never
|
|
28
|
-
* be reported `valid
|
|
23
|
+
* be reported `valid` here. Cron dialect is checked against
|
|
29
24
|
* `backendNameForPlatform()`, the same platform default `sync` falls back
|
|
30
25
|
* to whenever it has no injected/native-inspected backend to hand (see
|
|
31
26
|
* `akmTasksAdd`, `src/commands/tasks/tasks.ts`); a bare file was never
|
|
@@ -48,19 +43,12 @@
|
|
|
48
43
|
* table in its header, extended for the two gates above):
|
|
49
44
|
* - `valid` — parses as task source v4 directly (declared `version: 4`)
|
|
50
45
|
* and passes both sync gates.
|
|
51
|
-
* - `
|
|
52
|
-
*
|
|
53
|
-
* passes both sync gates.
|
|
54
|
-
* - `blocked` — declared `version: 2` or `3`; the migrator itself
|
|
55
|
-
* could not convert it (an ambiguous/unmigratable
|
|
56
|
-
* shape) — the ONLY way `parseTaskSource` ever throws
|
|
57
|
-
* for those two version numbers, so no message-text
|
|
58
|
-
* sniffing is needed to tell this apart from `invalid`.
|
|
46
|
+
* - `blocked` — declared `version: 2` or `3`; runtime does not accept
|
|
47
|
+
* it and the explicit migrator must rewrite it first.
|
|
59
48
|
* - `invalid` — the document declares SOME version (`4`, or anything
|
|
60
49
|
* other than 2/3/4) but fails to parse/validate, OR it
|
|
61
|
-
* parsed
|
|
62
|
-
*
|
|
63
|
-
* above, OR the YAML itself does not parse at all
|
|
50
|
+
* parsed but fails one of the two sync gates above, OR
|
|
51
|
+
* the YAML itself does not parse at all
|
|
64
52
|
* (a genuine syntax error, not merely a non-task
|
|
65
53
|
* shape) — reported with the parser's own reason.
|
|
66
54
|
* - `not-a-task` — the document parses as YAML but never declares a
|
|
@@ -138,11 +126,14 @@ export async function akmTaskValidate(filePath) {
|
|
|
138
126
|
if (!(cause instanceof UsageError))
|
|
139
127
|
throw cause;
|
|
140
128
|
const reason = cause.message;
|
|
141
|
-
// `parseTaskSource` only ever throws for a declared version 2/3 via the
|
|
142
|
-
// unmigratable-conversion branch (see this file's header) — no separate
|
|
143
|
-
// message check needed to recognize "blocked" here.
|
|
144
129
|
if (declaredVersion === 2 || declaredVersion === 3) {
|
|
145
|
-
return {
|
|
130
|
+
return {
|
|
131
|
+
ok: false,
|
|
132
|
+
path: resolvedPath,
|
|
133
|
+
sourceVersion: declaredVersion,
|
|
134
|
+
outcome: "blocked",
|
|
135
|
+
reason: `${reason} Run \`akm migrate apply --dry-run\`, review the plan, then run \`akm migrate apply\`.`,
|
|
136
|
+
};
|
|
146
137
|
}
|
|
147
138
|
if (peekFailed) {
|
|
148
139
|
return { ok: false, path: resolvedPath, outcome: "invalid", reason };
|
|
@@ -161,11 +152,8 @@ export async function akmTaskValidate(filePath) {
|
|
|
161
152
|
// Success is unreachable from any path that leaves `declaredVersion`
|
|
162
153
|
// undefined — the router requires a numeric 2/3/4 version to reach here.
|
|
163
154
|
const sourceVersion = declaredVersion ?? 4;
|
|
164
|
-
// The document
|
|
165
|
-
//
|
|
166
|
-
// accepting it. A violation here is `invalid`, never `blocked`: the
|
|
167
|
-
// migrator already succeeded, so this is the same kind of defect a
|
|
168
|
-
// native v4 document with the identical schedule would have.
|
|
155
|
+
// The current document parsed; now run the two gates `compileTaskSources`
|
|
156
|
+
// applies before accepting it.
|
|
169
157
|
try {
|
|
170
158
|
assertTaskScheduleInputsSatisfyContract(parsed.v4, resolvedPath);
|
|
171
159
|
assertTaskScheduleCronValid(parsed.v4, backend);
|
|
@@ -180,7 +168,7 @@ export async function akmTaskValidate(filePath) {
|
|
|
180
168
|
ok: true,
|
|
181
169
|
path: resolvedPath,
|
|
182
170
|
sourceVersion,
|
|
183
|
-
outcome:
|
|
171
|
+
outcome: "valid",
|
|
184
172
|
resolved: buildResolved(id, parsed.v4),
|
|
185
173
|
};
|
|
186
174
|
}
|
|
@@ -22,7 +22,7 @@ const INTERACTIVE_TOOL_ENV_KEYS = new Set(["EDITOR", "VISUAL", "PAGER"]);
|
|
|
22
22
|
* genuine RCE-class key; first-party stashes warn. The interactive-tool
|
|
23
23
|
* group (see {@link INTERACTIVE_TOOL_ENV_KEYS}) only ever warns, since akm's
|
|
24
24
|
* own env-injection path never invokes those keys as a command. An explicit
|
|
25
|
-
* `--allow-
|
|
25
|
+
* `--allow-dangerous-env-keys` (threaded through by the caller, same override
|
|
26
26
|
* `decideDangerousKeyInstall`'s `"warn-allow"` already honors for rule 2)
|
|
27
27
|
* downgrades a remaining block to a warning too — the operator is not racing
|
|
28
28
|
* themselves. See rule 1 above.
|
|
@@ -31,7 +31,7 @@ const INTERACTIVE_TOOL_ENV_KEYS = new Set(["EDITOR", "VISUAL", "PAGER"]);
|
|
|
31
31
|
* (already filtered by the caller via `isDangerousEnvKey`).
|
|
32
32
|
* @param thirdParty `true` when the env's source is a third-party stash — i.e.
|
|
33
33
|
* its origin carries a `registryId`.
|
|
34
|
-
* @param
|
|
34
|
+
* @param allowDangerousEnvKeys `true` when the operator passed `--allow-dangerous-env-keys` (or
|
|
35
35
|
* its equivalent) for this injection. Defaults to `false`.
|
|
36
36
|
*/
|
|
37
37
|
export function decideDangerousEnvInjection(input) {
|
|
@@ -39,7 +39,7 @@ export function decideDangerousEnvInjection(input) {
|
|
|
39
39
|
return "allow";
|
|
40
40
|
if (!input.thirdParty)
|
|
41
41
|
return "warn";
|
|
42
|
-
if (input.
|
|
42
|
+
if (input.allowDangerousEnvKeys)
|
|
43
43
|
return "warn";
|
|
44
44
|
const onlyInteractiveTool = input.dangerousKeys.every((key) => INTERACTIVE_TOOL_ENV_KEYS.has(key));
|
|
45
45
|
return onlyInteractiveTool ? "warn" : "block";
|
|
@@ -53,7 +53,7 @@ export function decideDangerousEnvInjection(input) {
|
|
|
53
53
|
export function decideDangerousKeyInstall(input) {
|
|
54
54
|
if (!input.findingsPresent)
|
|
55
55
|
return "allow";
|
|
56
|
-
return input.
|
|
56
|
+
return input.allowDangerousEnvKeys ? "warn-allow" : "gate";
|
|
57
57
|
}
|
|
58
58
|
// ── Rule 3: write activation (search-source.ts, installations.ts) ────────────
|
|
59
59
|
/**
|
|
@@ -130,6 +130,11 @@ function akmStashAbstains(root, absPath) {
|
|
|
130
130
|
const segments = relPath.split(/[\\/]+/).filter(Boolean);
|
|
131
131
|
if (segments.length === 0)
|
|
132
132
|
return false;
|
|
133
|
+
// A bundle's root README describes the bundle itself; it is not an AKM
|
|
134
|
+
// knowledge asset. Nested README files remain adapter-owned documentation
|
|
135
|
+
// and are handled by the normal typed-directory matcher policy.
|
|
136
|
+
if (segments.length === 1 && segments[0]?.toLowerCase() === "readme.md")
|
|
137
|
+
return true;
|
|
133
138
|
// Skip env `.env` files that have a sibling `.sensitive` marker file.
|
|
134
139
|
if (segments[0] === "env" && (absPath.endsWith(".env") || path.basename(absPath) === ".env")) {
|
|
135
140
|
if (fs.existsSync(absPath.replace(/\.env$/, ".sensitive")))
|
|
@@ -127,15 +127,6 @@ function nullableString(value, path) {
|
|
|
127
127
|
function nullableObject(value, path) {
|
|
128
128
|
return value === null ? null : cloneExecutionJsonObject(value, path);
|
|
129
129
|
}
|
|
130
|
-
function nullableEnvironment(value, path) {
|
|
131
|
-
if (value === null)
|
|
132
|
-
return null;
|
|
133
|
-
const environment = cloneExecutionJsonObject(value, path);
|
|
134
|
-
if (Object.values(environment).some((entry) => typeof entry !== "string")) {
|
|
135
|
-
throw new TypeError(`${path} values must be strings`);
|
|
136
|
-
}
|
|
137
|
-
return environment;
|
|
138
|
-
}
|
|
139
130
|
function nullableTimeout(value, path) {
|
|
140
131
|
const timeout = cloneExecutionJson(value, path);
|
|
141
132
|
if (timeout !== null && typeof timeout !== "string" && typeof timeout !== "number") {
|
|
@@ -167,6 +158,16 @@ export function executionDefaultsFromFrontmatter(data, options) {
|
|
|
167
158
|
const namespace = own(frontmatter, "akm")
|
|
168
159
|
? requireMetadataMapping(frontmatter.akm, "frontmatter.akm")
|
|
169
160
|
: snapshotStrictRecord({}, "frontmatter.akm");
|
|
161
|
+
for (const key of ["workspace", "environment", "runtime"]) {
|
|
162
|
+
const location = own(frontmatter, key)
|
|
163
|
+
? `frontmatter.${key}`
|
|
164
|
+
: own(namespace, key)
|
|
165
|
+
? `frontmatter.akm.${key}`
|
|
166
|
+
: undefined;
|
|
167
|
+
if (location) {
|
|
168
|
+
throw new TypeError(`${location} is host-controlled and cannot be declared by an executable asset; configure the selected engine or workflow execution environment instead`);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
170
171
|
const out = Object.create(null);
|
|
171
172
|
if (kind === "command" && own(frontmatter, "agent")) {
|
|
172
173
|
out.agent = nullableString(frontmatter.agent, "frontmatter.agent");
|
|
@@ -269,26 +270,6 @@ export function executionDefaultsFromFrontmatter(data, options) {
|
|
|
269
270
|
? topLevelTimeouts.get(selectedTimeoutKey)
|
|
270
271
|
: namespacedTimeouts.get(selectedTimeoutKey);
|
|
271
272
|
}
|
|
272
|
-
for (const key of ["workspace", "environment", "runtime"]) {
|
|
273
|
-
const namespaced = own(namespace, key)
|
|
274
|
-
? key === "workspace"
|
|
275
|
-
? nullableString(namespace[key], `frontmatter.akm.${key}`)
|
|
276
|
-
: key === "environment"
|
|
277
|
-
? nullableEnvironment(namespace[key], `frontmatter.akm.${key}`)
|
|
278
|
-
: nullableObject(namespace[key], `frontmatter.akm.${key}`)
|
|
279
|
-
: undefined;
|
|
280
|
-
const topLevel = allowTopLevelEngine && own(frontmatter, key)
|
|
281
|
-
? key === "workspace"
|
|
282
|
-
? nullableString(frontmatter[key], `frontmatter.${key}`)
|
|
283
|
-
: key === "environment"
|
|
284
|
-
? nullableEnvironment(frontmatter[key], `frontmatter.${key}`)
|
|
285
|
-
: nullableObject(frontmatter[key], `frontmatter.${key}`)
|
|
286
|
-
: undefined;
|
|
287
|
-
if (topLevel !== undefined)
|
|
288
|
-
out[key] = topLevel;
|
|
289
|
-
else if (namespaced !== undefined)
|
|
290
|
-
out[key] = namespaced;
|
|
291
|
-
}
|
|
292
273
|
return out;
|
|
293
274
|
}
|
|
294
275
|
function snapshotRendererInput(input) {
|
|
@@ -29,28 +29,29 @@
|
|
|
29
29
|
* typo in an optional section.
|
|
30
30
|
* - Unsupported top-level source shapes and provider kinds are hard-rejected;
|
|
31
31
|
* silently dropping them would mask user data loss.
|
|
32
|
-
* - UNKNOWN-KEY POLICY:
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
* load→save round trip so an older reader never strips a newer writer's
|
|
39
|
-
* settings. (Replaced the prior strict-mode object walls.)
|
|
32
|
+
* - UNKNOWN-KEY POLICY: portable descriptive sections generally use
|
|
33
|
+
* passthrough so version skew round-trips newer settings. Small finite
|
|
34
|
+
* authority/toggle sections (`scheduler`, `execution`, `experimental`, and
|
|
35
|
+
* reranker policy) are strict: a misspelling there must not silently turn a
|
|
36
|
+
* safety decision off. The top level remains passthrough and warns on
|
|
37
|
+
* unknown keys.
|
|
40
38
|
* - `defaultWriteTarget` resolution and similar cross-field invariants are
|
|
41
39
|
* enforced at save time via `superRefine` on the top-level schema.
|
|
42
40
|
*/
|
|
43
41
|
import { z } from "zod";
|
|
42
|
+
import { bundleRefToString, parseBundleRef } from "../asset/asset-ref.js";
|
|
44
43
|
import { warnOnce } from "../warn.js";
|
|
45
44
|
import { BUILTIN_IMPROVE_STRATEGY_NAMES, IMPROVE_PROCESS_ENGINE_CAPABILITIES } from "./engine-semantics.js";
|
|
46
45
|
import { EmbeddingConnectionConfigSchema } from "./schema/embedding.js";
|
|
47
46
|
import { EnginesSchema } from "./schema/engines.js";
|
|
47
|
+
import { ExecutionPolicyConfigSchema } from "./schema/execution.js";
|
|
48
48
|
import { ExperimentalConfigSchema } from "./schema/experimental.js";
|
|
49
49
|
import { FeedbackConfigSchema } from "./schema/feedback.js";
|
|
50
50
|
import { ImproveConfigSchema } from "./schema/improve.js";
|
|
51
51
|
import { IndexConfigSchema } from "./schema/index-config.js";
|
|
52
52
|
import { OutputConfigSchema } from "./schema/output.js";
|
|
53
53
|
import { CURRENT_CONFIG_VERSION, engineName, nonEmptyString, nonNegativeNumber } from "./schema/primitives.js";
|
|
54
|
+
import { SchedulerConfigSchema } from "./schema/scheduler.js";
|
|
54
55
|
import { SearchConfigSchema } from "./schema/search.js";
|
|
55
56
|
import { SetupConfigSchema } from "./schema/setup.js";
|
|
56
57
|
import { BundlesConfigSchema, RegistryConfigEntrySchema } from "./schema/sources-bundles.js";
|
|
@@ -65,6 +66,7 @@ export { ConsolidateProcessConfigSchema, DistillProcessConfigSchema, ExtractProc
|
|
|
65
66
|
export { IndexConfigSchema, IndexPassConfigSchema } from "./schema/index-config.js";
|
|
66
67
|
export { OutputConfigSchema } from "./schema/output.js";
|
|
67
68
|
export { CURRENT_CONFIG_VERSION, LlmInvocationOverridesSchema } from "./schema/primitives.js";
|
|
69
|
+
export { SchedulerActivationSchema, SchedulerConfigSchema } from "./schema/scheduler.js";
|
|
68
70
|
export { SearchConfigSchema } from "./schema/search.js";
|
|
69
71
|
export { SetupConfigSchema } from "./schema/setup.js";
|
|
70
72
|
export { BundleConfigEntrySchema, BundlesConfigSchema, RegistryConfigEntrySchema, SourceConfigEntrySchema, } from "./schema/sources-bundles.js";
|
|
@@ -107,6 +109,8 @@ export const AkmConfigShape = {
|
|
|
107
109
|
defaults: DefaultsSchema.optional(),
|
|
108
110
|
semanticSearchMode: z.enum(["off", "auto"]).default("off"),
|
|
109
111
|
embedding: EmbeddingConnectionConfigSchema.optional(),
|
|
112
|
+
// Host-local execution authority. Inherited layers are stripped.
|
|
113
|
+
execution: ExecutionPolicyConfigSchema.optional(),
|
|
110
114
|
index: IndexConfigSchema.optional(),
|
|
111
115
|
registries: z.array(RegistryConfigEntrySchema).optional(),
|
|
112
116
|
// `bundles` + `defaultBundle` are the only source configuration shape. The
|
|
@@ -122,6 +126,9 @@ export const AkmConfigShape = {
|
|
|
122
126
|
archiveRetentionDays: nonNegativeNumber.optional(),
|
|
123
127
|
improve: ImproveConfigSchema.optional(),
|
|
124
128
|
workflow: WorkflowConfigSchema.optional(),
|
|
129
|
+
// Host-local scheduler grants. Inherited layers are stripped by
|
|
130
|
+
// `resolveExtendsChain`; bundle-provided config never activates code.
|
|
131
|
+
scheduler: SchedulerConfigSchema.optional(),
|
|
125
132
|
setup: SetupConfigSchema.optional(),
|
|
126
133
|
// D8 — explicit opt-ins for behaviour outside the stability contract. Every
|
|
127
134
|
// key defaults to OFF; see `src/core/config/experimental.ts` for the readers.
|
|
@@ -168,6 +175,55 @@ export const AkmConfigSchema = AkmConfigBaseSchema.superRefine((config, ctx) =>
|
|
|
168
175
|
message: `defaultBundle "${config.defaultBundle}" does not name a configured bundle`,
|
|
169
176
|
});
|
|
170
177
|
}
|
|
178
|
+
else if (config.bundles[config.defaultBundle]?.enabled === false) {
|
|
179
|
+
ctx.addIssue({
|
|
180
|
+
code: z.ZodIssueCode.custom,
|
|
181
|
+
path: ["defaultBundle"],
|
|
182
|
+
message: `defaultBundle "${config.defaultBundle}" is disabled`,
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
if (config.defaultWriteTarget !== undefined) {
|
|
187
|
+
const target = config.bundles?.[config.defaultWriteTarget];
|
|
188
|
+
if (!target) {
|
|
189
|
+
ctx.addIssue({
|
|
190
|
+
code: z.ZodIssueCode.custom,
|
|
191
|
+
path: ["defaultWriteTarget"],
|
|
192
|
+
message: `defaultWriteTarget "${config.defaultWriteTarget}" does not name a configured bundle`,
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
else if (target.enabled === false) {
|
|
196
|
+
ctx.addIssue({
|
|
197
|
+
code: z.ZodIssueCode.custom,
|
|
198
|
+
path: ["defaultWriteTarget"],
|
|
199
|
+
message: `defaultWriteTarget "${config.defaultWriteTarget}" is disabled`,
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
const activationKeys = new Set();
|
|
204
|
+
for (const [index, activation] of (config.scheduler?.enabled ?? []).entries()) {
|
|
205
|
+
try {
|
|
206
|
+
const parsed = parseBundleRef(activation.ref);
|
|
207
|
+
if (!parsed.bundle || parsed.fragment !== undefined || bundleRefToString(parsed) !== activation.ref) {
|
|
208
|
+
throw new Error("not canonical");
|
|
209
|
+
}
|
|
210
|
+
const key = `${activation.kind}\0${activation.ref}`;
|
|
211
|
+
if (activationKeys.has(key)) {
|
|
212
|
+
ctx.addIssue({
|
|
213
|
+
code: z.ZodIssueCode.custom,
|
|
214
|
+
path: ["scheduler", "enabled", index],
|
|
215
|
+
message: "duplicates an earlier scheduler activation",
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
activationKeys.add(key);
|
|
219
|
+
}
|
|
220
|
+
catch {
|
|
221
|
+
ctx.addIssue({
|
|
222
|
+
code: z.ZodIssueCode.custom,
|
|
223
|
+
path: ["scheduler", "enabled", index, "ref"],
|
|
224
|
+
message: "must be one canonical fully-qualified ref without a fragment",
|
|
225
|
+
});
|
|
226
|
+
}
|
|
171
227
|
}
|
|
172
228
|
for (const key of ["llm", "agent", "improve"]) {
|
|
173
229
|
if (config.defaults && key in config.defaults) {
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* values from the current bundle map in an {@link AkmConfig}.
|
|
7
7
|
*/
|
|
8
8
|
import { createHash } from "node:crypto";
|
|
9
|
+
import fs from "node:fs";
|
|
9
10
|
import path from "node:path";
|
|
10
11
|
import { ConfigError } from "../errors.js";
|
|
11
12
|
/** The current one-component-per-bundle configuration entry, if present. */
|
|
@@ -33,6 +34,87 @@ export function bundleComponentConfig(bundle) {
|
|
|
33
34
|
export function bundleContentRoot(entryPath, componentRoot) {
|
|
34
35
|
return path.resolve(entryPath, componentRoot ?? ".");
|
|
35
36
|
}
|
|
37
|
+
/**
|
|
38
|
+
* Physical identity for an already materialized filesystem bundle.
|
|
39
|
+
*
|
|
40
|
+
* Configuration may spell the same directory through relative paths or
|
|
41
|
+
* symbolic links. Those spellings are not distinct sources: treating them as
|
|
42
|
+
* distinct would make ownership, default-source trust, and scheduler grants
|
|
43
|
+
* depend on syntax instead of the directory the OS will actually read.
|
|
44
|
+
*/
|
|
45
|
+
export function bundlePhysicalContentRoot(entryPath, componentRoot) {
|
|
46
|
+
const resolved = bundleContentRoot(entryPath, componentRoot);
|
|
47
|
+
try {
|
|
48
|
+
return fs.realpathSync.native(resolved);
|
|
49
|
+
}
|
|
50
|
+
catch (cause) {
|
|
51
|
+
// Cache-backed sources can be configured before they are materialized.
|
|
52
|
+
// Their source descriptor remains the identity until a real path exists.
|
|
53
|
+
if (cause.code === "ENOENT")
|
|
54
|
+
return resolved;
|
|
55
|
+
throw new ConfigError(`Unable to resolve physical bundle root ${JSON.stringify(resolved)}: ${cause instanceof Error ? cause.message : String(cause)}`, "INVALID_CONFIG_FILE");
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
/** Whether a configured bundle participates in reads, writes, and execution. */
|
|
59
|
+
export function isBundleEnabled(config, bundleId) {
|
|
60
|
+
const bundle = config.bundles?.[bundleId];
|
|
61
|
+
return bundle !== undefined && bundle.enabled !== false;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Stable identity of the origin currently installed under a bundle id.
|
|
65
|
+
*
|
|
66
|
+
* This deliberately excludes mutable policy (`enabled`, `writable`,
|
|
67
|
+
* credentials, adapter selection) and bundle contents. Ordinary updates and
|
|
68
|
+
* adapter auto-detection for the same origin keep their grant; changing the
|
|
69
|
+
* locator or component root does not.
|
|
70
|
+
*/
|
|
71
|
+
export function bundleSourceId(config, bundleId) {
|
|
72
|
+
const bundle = config.bundles?.[bundleId];
|
|
73
|
+
if (!bundle) {
|
|
74
|
+
throw new ConfigError(`Bundle ${JSON.stringify(bundleId)} is not configured.`, "INVALID_CONFIG_FILE");
|
|
75
|
+
}
|
|
76
|
+
const component = bundleComponentConfig(bundle);
|
|
77
|
+
let source;
|
|
78
|
+
if (bundle.path !== undefined) {
|
|
79
|
+
source = { kind: "filesystem", locator: bundlePhysicalContentRoot(bundle.path, component?.root) };
|
|
80
|
+
}
|
|
81
|
+
else if (bundle.git !== undefined) {
|
|
82
|
+
source = { kind: "git", locator: normalizeInstalledGitRef("git", bundle.git) };
|
|
83
|
+
}
|
|
84
|
+
else if (bundle.website !== undefined) {
|
|
85
|
+
// Crawl/refresh settings are mutable fetch policy, not source identity.
|
|
86
|
+
// Changing them must not silently revoke a user's scheduling decision for
|
|
87
|
+
// the same website origin.
|
|
88
|
+
source = { kind: "website", url: bundle.website.url };
|
|
89
|
+
}
|
|
90
|
+
else if (bundle.npm !== undefined) {
|
|
91
|
+
source = { kind: "npm", locator: bundle.npm };
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
throw new ConfigError(`Bundle ${JSON.stringify(bundleId)} has no source descriptor.`, "INVALID_CONFIG_FILE");
|
|
95
|
+
}
|
|
96
|
+
return hashBundleSourceIdentity({
|
|
97
|
+
version: 1,
|
|
98
|
+
source,
|
|
99
|
+
registryId: bundle.registryId ?? null,
|
|
100
|
+
componentRoot: component?.root ?? ".",
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
/** Identity for an environment-only filesystem bundle not persisted in config. */
|
|
104
|
+
export function filesystemBundleSourceId(contentRoot) {
|
|
105
|
+
return hashBundleSourceIdentity({
|
|
106
|
+
version: 1,
|
|
107
|
+
source: { kind: "filesystem", locator: bundlePhysicalContentRoot(contentRoot) },
|
|
108
|
+
registryId: null,
|
|
109
|
+
componentRoot: ".",
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
function hashBundleSourceIdentity(identity) {
|
|
113
|
+
return `sha256:${createHash("sha256")
|
|
114
|
+
.update("akm.bundle-source\0v1\0")
|
|
115
|
+
.update(JSON.stringify(identity))
|
|
116
|
+
.digest("hex")}`;
|
|
117
|
+
}
|
|
36
118
|
/**
|
|
37
119
|
* The resolved primary stash path — the `defaultBundle`'s filesystem `path`
|
|
38
120
|
* (spec §10.1) — or `undefined` when no filesystem primary is configured.
|
|
@@ -68,13 +150,20 @@ export function bundleContentRoots(config) {
|
|
|
68
150
|
for (const [id, entry] of Object.entries(bundles)) {
|
|
69
151
|
if (typeof entry.path !== "string" || entry.path.length === 0)
|
|
70
152
|
continue;
|
|
71
|
-
out.push({ id, contentRoot:
|
|
153
|
+
out.push({ id, contentRoot: bundlePhysicalContentRoot(entry.path, bundleComponentConfig(entry)?.root) });
|
|
72
154
|
}
|
|
73
155
|
return out;
|
|
74
156
|
}
|
|
75
157
|
/** The bundle id whose resolved content root already matches `resolvedContentRoot`, if any. */
|
|
76
158
|
export function bundleKeyForContentRoot(config, resolvedContentRoot) {
|
|
77
|
-
|
|
159
|
+
let physical = path.resolve(resolvedContentRoot);
|
|
160
|
+
try {
|
|
161
|
+
physical = fs.realpathSync.native(physical);
|
|
162
|
+
}
|
|
163
|
+
catch {
|
|
164
|
+
// Compare the unresolved absolute locator when the candidate is not yet materialized.
|
|
165
|
+
}
|
|
166
|
+
return bundleContentRoots(config).find((entry) => entry.contentRoot === physical)?.id;
|
|
78
167
|
}
|
|
79
168
|
export function bundlesToSourceEntries(config) {
|
|
80
169
|
const bundles = config.bundles;
|
|
@@ -96,6 +185,7 @@ export function bundleEntryToSourceEntry(key, bundle, isPrimary = false) {
|
|
|
96
185
|
const base = {
|
|
97
186
|
name: key,
|
|
98
187
|
...(bundle.writable !== undefined ? { writable: bundle.writable } : {}),
|
|
188
|
+
...(bundle.credential !== undefined ? { credential: bundle.credential } : {}),
|
|
99
189
|
...(bundle.enabled !== undefined ? { enabled: bundle.enabled } : {}),
|
|
100
190
|
...(isPrimary ? { primary: true } : {}),
|
|
101
191
|
};
|
|
@@ -235,6 +325,10 @@ export function resolveConfiguredSources(config) {
|
|
|
235
325
|
}
|
|
236
326
|
return out;
|
|
237
327
|
}
|
|
328
|
+
/** Active sources in the same deterministic order as `resolveConfiguredSources`. */
|
|
329
|
+
export function resolveActiveConfiguredSources(config) {
|
|
330
|
+
return resolveConfiguredSources(config).filter((source) => source.enabled !== false);
|
|
331
|
+
}
|
|
238
332
|
function toConfiguredSource(persisted, isPrimary) {
|
|
239
333
|
const source = parseSourceSpec(persisted);
|
|
240
334
|
if (!source)
|