akm-cli 0.10.261012-beta → 0.10.261013-beta

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.
Files changed (146) hide show
  1. package/CHANGELOG.md +216 -0
  2. package/SECURITY.md +2 -2
  3. package/STABILITY.md +2 -2
  4. package/dist/assets/hints/cli-hints-full.md +6 -7
  5. package/dist/assets/hints/cli-hints-short.md +3 -4
  6. package/dist/commands/command/execution-source-loader.js +10 -14
  7. package/dist/commands/env/env-binding.js +47 -58
  8. package/dist/commands/env/env-cli.js +20 -22
  9. package/dist/commands/env/env.js +10 -58
  10. package/dist/commands/env/secret-cli.js +9 -27
  11. package/dist/commands/env/secret.js +3 -10
  12. package/dist/commands/health/scheduler-binary.js +1 -1
  13. package/dist/commands/improve/distill.js +0 -2
  14. package/dist/commands/improve/eligibility.js +0 -2
  15. package/dist/commands/lint/env-key-rules.js +7 -6
  16. package/dist/commands/lint/index.js +1 -1
  17. package/dist/commands/proposal/validators/proposal-validators.js +1 -4
  18. package/dist/commands/read/show.js +4 -25
  19. package/dist/commands/sources/add-cli.js +8 -63
  20. package/dist/commands/sources/bundle-cli.js +8 -9
  21. package/dist/commands/sources/installed-stashes.js +117 -61
  22. package/dist/commands/sources/schema-repair.js +0 -2
  23. package/dist/commands/sources/source-clone.js +31 -7
  24. package/dist/commands/tasks/explain.js +1 -1
  25. package/dist/commands/tasks/tasks.js +38 -56
  26. package/dist/commands/ui/redact.js +4 -4
  27. package/dist/commands/ui/workspace-actions.js +1 -1
  28. package/dist/commands/workflow/plan.js +47 -8
  29. package/dist/commands/workflow-cli.js +37 -41
  30. package/dist/core/adapter/adapters/akm-adapter.js +3 -3
  31. package/dist/core/adapter/adapters/akm-lint.js +3 -4
  32. package/dist/core/adapter/adapters/akm-metadata.js +2 -2
  33. package/dist/core/adapter/adapters/akm-task-adapter.js +5 -6
  34. package/dist/core/adapter/adapters/dotenv-adapter.js +4 -3
  35. package/dist/core/asset/resolve-ref.js +5 -0
  36. package/dist/core/bundle-rename.js +12 -4
  37. package/dist/core/common.js +4 -52
  38. package/dist/core/config/config-walker.js +1 -1
  39. package/dist/core/config/config.js +55 -18
  40. package/dist/core/config/schema/primitives.js +12 -6
  41. package/dist/core/config/schema/sources-bundles.js +2 -2
  42. package/dist/core/env-secret-ref.js +84 -73
  43. package/dist/core/errors.js +1 -1
  44. package/dist/core/paths.js +1 -6
  45. package/dist/core/redaction.js +16 -3
  46. package/dist/core/ref-token.js +82 -0
  47. package/dist/core/secret-value.js +14 -0
  48. package/dist/core/write-source.js +20 -4
  49. package/dist/indexer/indexer.js +27 -36
  50. package/dist/indexer/installations.js +1 -8
  51. package/dist/indexer/lookup/adapter-concept-owner.js +40 -14
  52. package/dist/indexer/materialize-embeddings.js +1 -1
  53. package/dist/indexer/scan/drain-dir.js +3 -3
  54. package/dist/indexer/search/search-source.js +4 -2
  55. package/dist/indexer/walk/path-resolver.js +13 -68
  56. package/dist/indexer/walk/walker.js +18 -22
  57. package/dist/integrations/agent/engine-resolution.js +9 -7
  58. package/dist/integrations/agent/runner-dispatch.js +4 -5
  59. package/dist/integrations/agent/runner.js +2 -0
  60. package/dist/llm/client.js +1 -1
  61. package/dist/llm/embedders/remote.js +4 -3
  62. package/dist/output/renderers.js +3 -4
  63. package/dist/output/shapes/helpers.js +0 -2
  64. package/dist/output/text/command-format.js +12 -2
  65. package/dist/output/text/show-directives.js +16 -28
  66. package/dist/output/text/workflow-format.js +37 -7
  67. package/dist/scripts/akm-migrate-node.js +45865 -44692
  68. package/dist/scripts/akm-migrate.js +45804 -44630
  69. package/dist/setup/steps/tasks.js +1 -2
  70. package/dist/sources/providers/provider-utils.js +12 -0
  71. package/dist/sources/resolve.js +12 -16
  72. package/dist/sources/snapshot-fetchers/secret-seam.js +2 -4
  73. package/dist/sources/snapshot-fetchers/x.js +1 -1
  74. package/dist/storage/repositories/proposals-repository.js +2 -2
  75. package/dist/storage/repositories/task-history-repository.js +2 -2
  76. package/dist/storage/repositories/workflow-runs-repository.js +38 -116
  77. package/dist/tasks/embedded.js +1 -1
  78. package/dist/tasks/prepare/prepare-support.js +3 -2
  79. package/dist/tasks/prepare/prepare.js +3 -4
  80. package/dist/tasks/prepare/script-capture.js +8 -22
  81. package/dist/tasks/run/load-task.js +46 -14
  82. package/dist/tasks/run/run-native-task.js +0 -8
  83. package/dist/tasks/run/run-workflow-task.js +2 -2
  84. package/dist/tasks/scheduler-sync.js +0 -1
  85. package/dist/tasks/source/bounded-document.js +0 -41
  86. package/dist/tasks/source/parse-task-source.js +0 -1
  87. package/dist/tasks/source/task-source-v4.js +1 -4
  88. package/dist/workflows/compile.js +1 -15
  89. package/dist/workflows/exec/child-workflow.js +1 -2
  90. package/dist/workflows/exec/dispatch-redaction.js +24 -6
  91. package/dist/workflows/exec/environment.js +11 -74
  92. package/dist/workflows/exec/exec-unit.js +31 -23
  93. package/dist/workflows/exec/frozen-judge.js +17 -3
  94. package/dist/workflows/exec/native-executor.js +70 -26
  95. package/dist/workflows/exec/run-workflow.js +31 -18
  96. package/dist/workflows/exec/step-work.js +4 -3
  97. package/dist/workflows/exec/unit-dispatch.js +1 -8
  98. package/dist/workflows/freeze/environment.js +14 -119
  99. package/dist/workflows/freeze/freeze.js +0 -1
  100. package/dist/workflows/freeze/step-values.js +1 -2
  101. package/dist/workflows/freeze/targets/child-workflow.js +3 -5
  102. package/dist/workflows/freeze/targets/command.js +6 -9
  103. package/dist/workflows/freeze/targets/shell.js +0 -3
  104. package/dist/workflows/parser.js +6 -25
  105. package/dist/workflows/plan.js +6 -2
  106. package/dist/workflows/renderer.js +77 -4
  107. package/dist/workflows/runtime/run-plan.js +37 -23
  108. package/dist/workflows/runtime/runs.js +41 -84
  109. package/dist/workflows/runtime/workflow-asset-loader.js +55 -86
  110. package/dist/workflows/source-files.js +7 -19
  111. package/docs/architecture/0.10-process-rules.md +6 -6
  112. package/docs/architecture/architecture.md +6 -6
  113. package/docs/architecture/comparisons/claude-code-vs-akm-workflows-full.md +12 -18
  114. package/docs/architecture/decisions/0003-child-env-allowlist-and-provenance.md +6 -8
  115. package/docs/architecture/internals/storage-locations.md +4 -4
  116. package/docs/architecture/persisted-data-compat.md +3 -2
  117. package/docs/architecture/specs/0.9.0-public-api-issue-backlog.md +1 -1
  118. package/docs/architecture/specs/akm-format-neutral-bundle-workspace-spec.md +3 -3
  119. package/docs/architecture/testing/manual-testing-checklist.md +46 -78
  120. package/docs/architecture/testing/task-workflow-manual-tests.md +22 -40
  121. package/docs/architecture/testing/testing-workflow.md +9 -11
  122. package/docs/architecture/workflow-engine.md +41 -27
  123. package/docs/guides/author-bundles.md +14 -17
  124. package/docs/guides/author-workflows.md +43 -9
  125. package/docs/guides/bundles.md +16 -11
  126. package/docs/guides/claude-code-vs-akm-workflows.md +1 -1
  127. package/docs/guides/run-workflows.md +25 -13
  128. package/docs/guides/scheduling.md +1 -1
  129. package/docs/maintainers/README.md +2 -3
  130. package/docs/maintainers/release-checklist.md +73 -126
  131. package/docs/migration/v0.9-to-v0.10.md +5 -1
  132. package/docs/reference/cli.md +124 -112
  133. package/docs/reference/configuration.md +16 -9
  134. package/docs/reference/data-and-telemetry.md +1 -1
  135. package/docs/reference/env-and-secrets.md +66 -15
  136. package/docs/reference/registry.md +6 -11
  137. package/docs/reference/tasks.md +11 -3
  138. package/docs/reference/ui-api.md +3 -3
  139. package/docs/reference/workflow-schema.md +109 -36
  140. package/package.json +1 -1
  141. package/schemas/akm-task.json +2 -4
  142. package/dist/commands/sources/dangerous-env-audit.js +0 -134
  143. package/dist/core/activation-policy.js +0 -67
  144. package/dist/execution/directory-identity.js +0 -78
  145. package/dist/workflows/authoring/scope-key.js +0 -79
  146. package/dist/workflows/source-semantics.js +0 -110
@@ -311,12 +311,7 @@ export function getStateDir(env = process.env, platform = process.platform) {
311
311
  * its `stash_dir` column — so two stashes on one machine never collide.
312
312
  *
313
313
  * Reuses {@link shortHash} (bundle-id.ts) rather than growing a second
314
- * sha256-truncated-hex helper. (The other existing option,
315
- * `getCurrentWorkflowScopeKey`'s `dir:v1:<sha256>` in
316
- * `src/workflows/authoring/scope-key.ts`, is a state.db column value for a
317
- * different subsystem — full untruncated hex plus a `dir:v1:` prefix, which
318
- * is neither filesystem-safe as a directory segment on Windows (`:`) nor
319
- * short, so it is not a fit here.) `shortHash` trims to 8 hex chars (32
314
+ * sha256-truncated-hex helper. `shortHash` trims to 8 hex chars (32
320
315
  * bits) — fine at its original call site because a collision only ever
321
316
  * shortens a batch-unique slug suffix that a retry loop immediately
322
317
  * re-disambiguates. Here there is no such retry loop, but a machine
@@ -499,10 +499,23 @@ export function redactErrorBody(input) {
499
499
  }
500
500
  return out;
501
501
  }
502
+ /**
503
+ * The spellings a JSON encoder gives a value inside a string literal: the
504
+ * `JSON.stringify` form, and the ASCII-only form with non-ASCII as `\uXXXX`
505
+ * (what Python's `json.dumps` writes). Empty for a value that needs no escaping.
506
+ */
507
+ function jsonEscapedSpellings(value) {
508
+ const json = JSON.stringify(value).slice(1, -1);
509
+ // ASCII-only encoders (Python's ensure_ascii) also escape DEL; some (Java's) write uppercase hex.
510
+ const ascii = json.replace(/[\u007f-\uffff]/g, (char) => `\\u${char.charCodeAt(0).toString(16).padStart(4, "0")}`);
511
+ const upper = ascii.replace(/\\u([0-9a-f]{4})/g, (_match, hex) => `\\u${hex.toUpperCase()}`);
512
+ return [...new Set([json, ascii, upper])].filter((spelling) => spelling !== value);
513
+ }
502
514
  export function redactSensitiveText(text, sensitiveValues) {
503
- const values = [...new Set(sensitiveValues)]
504
- .filter((value) => value.length > 0)
505
- .sort((a, b) => b.length - a.length || a.localeCompare(b));
515
+ const exact = [...sensitiveValues].filter((value) => value.length > 0);
516
+ // A child that prints JSON writes a secret's escaped spelling (`\"`, `\\`, `\n`, `\uXXXX`),
517
+ // which the exact value never matches. Each escaped spelling is just another needle.
518
+ const values = [...new Set([...exact, ...exact.flatMap(jsonEscapedSpellings)])].sort((a, b) => b.length - a.length || a.localeCompare(b));
506
519
  if (values.length === 0)
507
520
  return text;
508
521
  const coverageDelta = new Int32Array(text.length + 1);
@@ -0,0 +1,82 @@
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
+ /**
5
+ * The one substitution token akm hands to a process or a provider: `${ref}`,
6
+ * where `ref` is an akm ref (`[bundle//]conceptId`). A `secrets/…` ref stands
7
+ * for the secret's value; any other asset ref stands for the asset's absolute
8
+ * path. A `${…}` body that is not an akm ref (`${HOME}`, `${PATH//:/ }`,
9
+ * `${v//-/_}`, `${x/a/b}`: not led by a native type and not qualified by an
10
+ * installed bundle) is plain text and is left alone.
11
+ *
12
+ * Kept free of the commands layer so config credentials and env values share
13
+ * this one function.
14
+ */
15
+ import { parseBundleRef } from "./asset/asset-ref.js";
16
+ import { typeNameFromConceptId } from "./asset/resolve-ref.js";
17
+ export const REF_TOKEN_RE = /\$\{([^}\s]+)\}/g;
18
+ /**
19
+ * Does a `${…}` body parse as an akm ref (no export fragment) that either
20
+ * leads with a native asset type (bundle-qualified or not) or is bundle-qualified
21
+ * with a bundle `isBundleInstalled` accepts? Anything else stays text, so shell
22
+ * syntax (`${v//-/_}`, `${x/a/b}`) is never read as a ref. A qualified opaque body
23
+ * follows the core ref grammar and the owner walk, whatever adapter owns it, but
24
+ * only for an installed bundle; with no predicate no bundle is installed.
25
+ */
26
+ function isRefBody(body, isBundleInstalled) {
27
+ try {
28
+ const ref = parseBundleRef(body);
29
+ if (ref.fragment !== undefined)
30
+ return false;
31
+ if (typeNameFromConceptId(ref.conceptId) !== undefined)
32
+ return true;
33
+ return ref.bundle !== undefined && isBundleInstalled?.(ref.bundle) === true;
34
+ }
35
+ catch {
36
+ return false;
37
+ }
38
+ }
39
+ /** Is the whole string exactly one `${ref}` token? */
40
+ export function isRefToken(value) {
41
+ const body = /^\$\{([^}\s]+)\}$/.exec(value)?.[1];
42
+ return body !== undefined && isRefBody(body);
43
+ }
44
+ /** Is the whole string exactly one `${ref}` token whose ref names a `secrets/` asset? */
45
+ export function isSecretRefToken(value) {
46
+ const body = /^\$\{([^}\s]+)\}$/.exec(value)?.[1];
47
+ return (body !== undefined && isRefBody(body) && typeNameFromConceptId(parseBundleRef(body).conceptId)?.type === "secret");
48
+ }
49
+ /**
50
+ * Replace every `${ref}` token in each value. Keys are never touched, and
51
+ * tokens embedded in larger strings are all substituted. `isBundleInstalled`
52
+ * lets a bundle-qualified body that is not led by a native type be a ref when
53
+ * its bundle is installed. An unresolved ref is
54
+ * collected in `missing` and its token left in place. Resolved secret values
55
+ * must never be logged or printed by callers.
56
+ */
57
+ export function substituteRefTokens(values, resolve, isBundleInstalled) {
58
+ const missing = [];
59
+ const secretValues = new Set();
60
+ const cache = new Map();
61
+ const out = {};
62
+ for (const [key, value] of Object.entries(values)) {
63
+ out[key] = value.replace(REF_TOKEN_RE, (token, body) => {
64
+ if (!isRefBody(body, isBundleInstalled))
65
+ return token;
66
+ if (!cache.has(body))
67
+ cache.set(body, resolve(body));
68
+ const resolved = cache.get(body);
69
+ if (resolved === undefined) {
70
+ if (!missing.includes(body))
71
+ missing.push(body);
72
+ return token;
73
+ }
74
+ if (resolved.kind === "path")
75
+ return resolved.path;
76
+ if (resolved.value)
77
+ secretValues.add(resolved.value);
78
+ return resolved.value;
79
+ });
80
+ }
81
+ return { values: out, missing, secretValues: [...secretValues] };
82
+ }
@@ -0,0 +1,14 @@
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 fs from "node:fs";
5
+ /**
6
+ * Read a secret's value: the file content as UTF-8 with surrounding whitespace
7
+ * trimmed. The one read every consumer uses (`secret run`, env `${ref}`
8
+ * tokens, config credentials, website fetchers), so a hand-written file's
9
+ * trailing newline never lands in an HTTP header. Callers MUST NOT write the
10
+ * returned value to stdout or any log.
11
+ */
12
+ export function readValue(secretPath) {
13
+ return fs.readFileSync(secretPath, "utf8").trim();
14
+ }
@@ -49,26 +49,42 @@ export function resolveWritable(entry) {
49
49
  return entry.writable;
50
50
  return entry.type === "filesystem";
51
51
  }
52
+ /**
53
+ * Whether a resolved source is write-activated. Only the primary stash and
54
+ * sources explicitly marked `writable: true` are writable; registry-cached
55
+ * (installed, read-only) sources are never written in place because
56
+ * `akm update` overwrites them.
57
+ */
58
+ export function isSourceWriteActivated(source) {
59
+ return source.writable === true;
60
+ }
52
61
  /** The two kinds writes are defined for; scheduler state binds only to these. */
53
62
  export function isWriteCapableSourceKind(kind) {
54
63
  return kind === "filesystem" || kind === "git";
55
64
  }
65
+ /**
66
+ * Throw when `name` is a configured bundle that is disabled. Callers run it
67
+ * after a name failed to resolve against the active set, so a disabled bundle
68
+ * is reported as disabled rather than as not configured.
69
+ */
70
+ export function assertBundleNotDisabled(akmConfig, name) {
71
+ if (resolveConfiguredSources(akmConfig).some((source) => source.name === name)) {
72
+ throw new UsageError(`Bundle "${name}" is disabled.`, "INVALID_FLAG_VALUE");
73
+ }
74
+ }
56
75
  /**
57
76
  * Resolve the destination for a write: explicit `--bundle`, then
58
77
  * `defaultWriteTarget`, then the working stash (`defaultBundle`). There is no
59
78
  * fallback to the first writable source.
60
79
  */
61
80
  export function resolveWriteTarget(akmConfig, explicitTarget, options = {}) {
62
- const allConfiguredSources = resolveConfiguredSources(akmConfig);
63
81
  const configuredSources = resolveActiveConfiguredSources(akmConfig);
64
82
  const requireWritable = options.requireWritable !== false;
65
83
  const flag = options.flag ?? "--bundle";
66
84
  if (explicitTarget) {
67
85
  const match = configuredSources.find((s) => s.name === explicitTarget);
68
86
  if (!match) {
69
- if (allConfiguredSources.some((source) => source.name === explicitTarget)) {
70
- throw new UsageError(`Bundle "${explicitTarget}" is disabled.`, "INVALID_FLAG_VALUE");
71
- }
87
+ assertBundleNotDisabled(akmConfig, explicitTarget);
72
88
  throw new UsageError(`${flag} must reference a source name from your config. No source named "${explicitTarget}" is configured. Run \`akm bundle list\` to see available sources.`, "INVALID_FLAG_VALUE");
73
89
  }
74
90
  if (requireWritable && !resolveWritable({ type: match.type, writable: match.writable })) {
@@ -25,7 +25,7 @@ import { INDEX_DB_VACUUMED_EVENT, readFreelistInfo, vacuumIfReclaimable } from "
25
25
  import { assertIndexedWorkflowSourceIdentity, WorkflowSourceIdentityError } from "../workflows/source-files.js";
26
26
  import { reclassifyIndexDbContention } from "./index-db-contention.js";
27
27
  import { deriveEntryProvenance, deriveInstallations } from "./installations.js";
28
- import { indexedPathMatchesOwner, resolveAdapterConceptOwner, } from "./lookup/adapter-concept-owner.js";
28
+ import { indexedPathMatchesOwner, resolveConceptOwner } from "./lookup/adapter-concept-owner.js";
29
29
  import { generateEmbeddingsForDb } from "./materialize-embeddings.js";
30
30
  import { canUseIncrementalSkip, computeDirFingerprint, getCachedDirState, getDirIndexState, inferZeroRowReason, } from "./passes/dir-staleness.js";
31
31
  import { isWorkflowSkipWarning, withFileSize } from "./passes/metadata.js";
@@ -669,9 +669,6 @@ function buildSourceScanPlans(db, allSourceEntries, isIncremental, reconcileMiss
669
669
  }
670
670
  const walked = walkStashFlatWithStatus(currentStashDir, {
671
671
  includeAllDirectories: component.adapter === "okf",
672
- ...(component.adapter === "akm" || component.adapter === "akm-workflow"
673
- ? { workflowSymlinkAdapter: component.adapter }
674
- : {}),
675
672
  });
676
673
  const dirGroups = groupFileContextsByDir(walked.files);
677
674
  const adapter = adapterForId(component.adapter);
@@ -1132,10 +1129,8 @@ borrowed = false) {
1132
1129
  const sources = await resolveLookupSources();
1133
1130
  if (sources.length === 0)
1134
1131
  return { entry: null };
1135
- const bundleBySourcePath = new Map(sourceComponents(sources).map((component, index) => [path.resolve(sources[index].path), component.id]));
1136
1132
  const qualified = Boolean(ref.bundle);
1137
- const candidateSources = ref.bundle ? resolveSourcesForOrigin(ref.bundle, sources) : sources;
1138
- if (candidateSources.length === 0)
1133
+ if ((ref.bundle ? resolveSourcesForOrigin(ref.bundle, sources) : sources).length === 0)
1139
1134
  return { entry: null };
1140
1135
  let db;
1141
1136
  let indexError;
@@ -1151,42 +1146,38 @@ borrowed = false) {
1151
1146
  ...(indexError === undefined ? {} : { indexError }),
1152
1147
  });
1153
1148
  try {
1154
- for (const source of candidateSources) {
1155
- const adapterId = source.adapterId ?? detectAdapterId(source.path);
1156
- const owner = resolveAdapterConceptOwner(source.path, adapterId, ref.conceptId);
1157
- const lookupConceptId = owner?.conceptId ?? ref.conceptId;
1158
- const inputRef = makeBundleRef(qualified ? ref.bundle : undefined, lookupConceptId);
1159
- const sourceBundleId = bundleBySourcePath.get(path.resolve(source.path));
1160
- const id = db && sourceBundleId ? findEntryIdByRef(db, inputRef, sourceBundleId) : undefined;
1161
- if (id !== undefined && owner && db) {
1162
- const entry = readLookupEntry(db, id, ref.conceptId, source.path);
1163
- if (entry) {
1164
- if (owner.workflowSource) {
1165
- try {
1166
- assertIndexedWorkflowSourceIdentity(inputRef, entry.filePath, owner.workflowSource);
1167
- if (entry.adapterId !== adapterId) {
1168
- throw new WorkflowSourceIdentityError(inputRef, entry.filePath, owner.path);
1169
- }
1170
- }
1171
- catch (error) {
1172
- if (!(error instanceof WorkflowSourceIdentityError))
1173
- throw error;
1174
- warn(`${error.message} Falling back to the physical owner.`);
1175
- return resolved(null, owner);
1149
+ const match = resolveConceptOwner(ref, sources);
1150
+ if (!match)
1151
+ return resolved(null);
1152
+ const { owner, source, bundleId, adapterId } = match;
1153
+ const inputRef = makeBundleRef(qualified ? ref.bundle : undefined, owner.conceptId);
1154
+ const id = db ? findEntryIdByRef(db, inputRef, bundleId) : undefined;
1155
+ if (id !== undefined && db) {
1156
+ const entry = readLookupEntry(db, id, ref.conceptId, source.path);
1157
+ if (entry) {
1158
+ if (owner.workflowSource) {
1159
+ try {
1160
+ assertIndexedWorkflowSourceIdentity(inputRef, entry.filePath, owner.workflowSource);
1161
+ if (entry.adapterId !== adapterId) {
1162
+ throw new WorkflowSourceIdentityError(inputRef, entry.filePath, owner.path);
1176
1163
  }
1177
1164
  }
1178
- else if (entry.adapterId !== adapterId || !indexedPathMatchesOwner(entry.filePath, owner)) {
1165
+ catch (error) {
1166
+ if (!(error instanceof WorkflowSourceIdentityError))
1167
+ throw error;
1168
+ warn(`${error.message} Falling back to the physical owner.`);
1179
1169
  return resolved(null, owner);
1180
1170
  }
1181
- return resolved(entry, owner);
1182
1171
  }
1172
+ else if (entry.adapterId !== adapterId || !indexedPathMatchesOwner(entry.filePath, owner)) {
1173
+ return resolved(null, owner);
1174
+ }
1175
+ return resolved(entry, owner);
1183
1176
  }
1184
- // A physical owner with a missing/incomplete index row still owns this
1185
- // unqualified concept. Stop here so a later source cannot retarget it.
1186
- if (owner)
1187
- return resolved(null, owner);
1188
1177
  }
1189
- return resolved(null);
1178
+ // A physical owner with a missing/incomplete index row still owns this
1179
+ // concept: no later source can retarget it.
1180
+ return resolved(null, owner);
1190
1181
  }
1191
1182
  finally {
1192
1183
  if (db && !borrowed)
@@ -19,12 +19,6 @@
19
19
  * spec §1.3). Slugs are made unique WITHIN a derivation batch
20
20
  * by appending a short path-hash suffix on collision, so two
21
21
  * sources sharing a basename never mint the same bundle id.
22
- * - `trusted` = `source.writable === true`. 0.9.0 ships no first-class trust
23
- * record (spec §1.3 — "no new trust machinery"); the writable
24
- * primary/config stashes are the user's own and count as
25
- * trusted, read-only registry caches do not. Nothing consumes
26
- * `trusted` in the live path yet (installation grants nothing,
27
- * History D8) — this is the Tier-A placeholder mapping.
28
22
  * - components = ONE component for the current single-root akm layout
29
23
  * (spec §1.2 rule 5): `{ id, root: source.path, adapter }`. The
30
24
  * akm layout's type dirs (`knowledge/`, `skills/`, …) are NOT
@@ -56,10 +50,10 @@
56
50
  * fallback is **`akm`** (see {@link FALLBACK_ADAPTER_ID}), preserving workspace
57
51
  * root behavior. Explicit configured ownership always wins over probing.
58
52
  */
59
- import { isSourceWriteActivated } from "../core/activation-policy.js";
60
53
  import { detectAdapterId } from "../core/adapter/detect-adapter.js";
61
54
  import { stashDirFor } from "../core/asset/asset-placement.js";
62
55
  import { deriveBundleId, deriveBundleIds, slugForPath } from "../core/bundle-id.js";
56
+ import { isSourceWriteActivated } from "../core/write-source.js";
63
57
  export { deriveBundleId, deriveBundleIds, slugForPath };
64
58
  /**
65
59
  * The no-probe-match fallback adapter id.
@@ -98,7 +92,6 @@ export function deriveInstallations(sources) {
98
92
  const adapter = source.adapterId ?? detectAdapterId(source.path, FALLBACK_ADAPTER_ID);
99
93
  installations.push({
100
94
  id,
101
- trusted: writable,
102
95
  components: [
103
96
  {
104
97
  // Single-component akm layout: the component id == the bundle id so
@@ -3,12 +3,15 @@
3
3
  // file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
4
  import fs from "node:fs";
5
5
  import path from "node:path";
6
+ import { detectAdapterId } from "../../core/adapter/detect-adapter.js";
6
7
  import { adapterForId } from "../../core/adapter/registry.js";
7
- import { compareCodePoints, hasErrnoCode, isWithin } from "../../core/common.js";
8
+ import { compareCodePoints, hasErrnoCode } from "../../core/common.js";
8
9
  import { ConfigError, UsageError } from "../../core/errors.js";
9
10
  import { canonicalizeWorkflowName } from "../../core/recognition-util.js";
11
+ import { sensitiveMarkerPath } from "../../core/sensitive-marker-path.js";
10
12
  import { warnOnce } from "../../core/warn.js";
11
13
  import { resolveUniqueWorkflowSource, workflowNameForConceptId, } from "../../workflows/source-files.js";
14
+ import { deriveInstallations } from "../installations.js";
12
15
  import { buildFileContext } from "../walk/file-context.js";
13
16
  const CONTENT_READ_REQUIRED = Symbol("adapter ownership probe requires content");
14
17
  export class AdapterConceptOwnershipError extends UsageError {
@@ -40,22 +43,18 @@ function lexicallyWithin(candidate, root) {
40
43
  return relative === "" || (!relative.startsWith(`..${path.sep}`) && relative !== ".." && !path.isAbsolute(relative));
41
44
  }
42
45
  /** Add case-only suffix spellings beside an adapter-authored read candidate. */
43
- function candidateSpellings(candidate, sourcePath, realRoot) {
46
+ function candidateSpellings(candidate, sourcePath) {
44
47
  const authored = path.resolve(candidate.path);
45
48
  if (!lexicallyWithin(authored, sourcePath))
46
49
  return [{ ...candidate, path: authored }];
47
50
  const parent = path.dirname(authored);
48
- let realParent;
49
51
  let entries;
50
52
  try {
51
- realParent = fs.realpathSync(parent);
52
53
  entries = fs.readdirSync(parent, { withFileTypes: true });
53
54
  }
54
55
  catch {
55
56
  return [{ ...candidate, path: authored }];
56
57
  }
57
- if (!isWithin(realParent, realRoot))
58
- return [{ ...candidate, path: authored }];
59
58
  const extension = path.extname(authored);
60
59
  if (!extension)
61
60
  return [{ ...candidate, path: authored }];
@@ -67,7 +66,7 @@ function candidateSpellings(candidate, sourcePath, realRoot) {
67
66
  })
68
67
  .map((entry) => ({ ...candidate, path: path.join(parent, entry.name) }));
69
68
  }
70
- function inspectCandidate(sourcePath, realRoot, adapterId, candidate) {
69
+ function inspectCandidate(sourcePath, adapterId, candidate) {
71
70
  const authoredPath = path.resolve(candidate.path);
72
71
  if (!lexicallyWithin(authoredPath, sourcePath))
73
72
  return undefined;
@@ -87,8 +86,6 @@ function inspectCandidate(sourcePath, realRoot, adapterId, candidate) {
87
86
  catch {
88
87
  return undefined;
89
88
  }
90
- if (!isWithin(realPath, realRoot))
91
- return undefined;
92
89
  try {
93
90
  if (!fs.statSync(realPath).isFile())
94
91
  return undefined;
@@ -118,6 +115,14 @@ function claimsWithoutContent(adapter, component, owner) {
118
115
  throw error;
119
116
  }
120
117
  }
118
+ /** True for an env/secret file that a sibling `.sensitive` marker hides from the index. */
119
+ function isMarkedSensitive(owner) {
120
+ const head = owner.conceptId.split("/")[0];
121
+ if (head !== "env" && head !== "secrets")
122
+ return false;
123
+ const marker = sensitiveMarkerPath(owner.path, head === "env" ? "env" : "secret");
124
+ return marker !== owner.path && fs.existsSync(marker);
125
+ }
121
126
  /**
122
127
  * Resolve one adapter/component's exact physical concept owner without reading
123
128
  * authored bytes. Native workflow arbitration is reused verbatim; every other
@@ -129,9 +134,8 @@ export function resolveAdapterConceptOwner(sourcePath, adapterId, conceptId, opt
129
134
  const normalized = normalizedConceptId(conceptId);
130
135
  if (!adapter || !normalized)
131
136
  return undefined;
132
- let realRoot;
133
137
  try {
134
- realRoot = fs.realpathSync(sourcePath);
138
+ fs.realpathSync(sourcePath);
135
139
  }
136
140
  catch (error) {
137
141
  // Genuinely absent — "no owner" is correct. Any other failure (e.g.
@@ -168,13 +172,13 @@ export function resolveAdapterConceptOwner(sourcePath, adapterId, conceptId, opt
168
172
  // — see each adapter's own doc comment for its candidate classes), and
169
173
  // `candidateSpellings` adds case-only siblings via one `readdirSync` of
170
174
  // each candidate's own parent directory.
171
- const spellings = (adapter.readCandidates?.(component, resolutionConceptId) ?? []).flatMap((candidate) => candidateSpellings(candidate, sourcePath, realRoot));
175
+ const spellings = (adapter.readCandidates?.(component, resolutionConceptId) ?? []).flatMap((candidate) => candidateSpellings(candidate, sourcePath));
172
176
  const inspected = [
173
177
  ...new Map(spellings.map((candidate) => [`${path.resolve(candidate.path)}\0${candidate.conceptId}`, candidate])).values(),
174
178
  ]
175
179
  .sort((left, right) => compareCodePoints(left.path, right.path))
176
180
  .flatMap((candidate) => {
177
- const owner = inspectCandidate(sourcePath, realRoot, adapterId, candidate);
181
+ const owner = inspectCandidate(sourcePath, adapterId, candidate);
178
182
  return owner ? [owner] : [];
179
183
  });
180
184
  for (const candidate of inspected) {
@@ -183,8 +187,10 @@ export function resolveAdapterConceptOwner(sourcePath, adapterId, conceptId, opt
183
187
  const identity = `${path.resolve(candidate.path)}\0${candidate.conceptId}`;
184
188
  if (ownersByIdentity.has(identity))
185
189
  continue;
186
- if (claimsWithoutContent(adapter, component, candidate))
190
+ if (claimsWithoutContent(adapter, component, candidate) ||
191
+ (options?.includeSensitive && isMarkedSensitive(candidate))) {
187
192
  ownersByIdentity.set(identity, candidate);
193
+ }
188
194
  }
189
195
  const owners = [...ownersByIdentity.values()]
190
196
  .filter((owner) => owner.conceptId === resolutionConceptId)
@@ -202,6 +208,26 @@ export function resolveAdapterConceptOwner(sourcePath, adapterId, conceptId, opt
202
208
  }
203
209
  return owners[0];
204
210
  }
211
+ /**
212
+ * The one "where is asset X right now?" walk. `sources` is the ordered source
213
+ * list (`resolveSourceEntries`: default bundle first, then installation
214
+ * priority). The first source with a physical owner for the ref ends the
215
+ * walk; a qualified ref only considers its own bundle. Synchronous and
216
+ * index-free: it never opens a database, so a caller may use it at dispatch.
217
+ */
218
+ export function resolveConceptOwner(ref, sources, options) {
219
+ const installations = deriveInstallations([...sources]);
220
+ for (const [index, source] of sources.entries()) {
221
+ const bundleId = installations[index]?.id;
222
+ if (!bundleId || (ref.bundle !== undefined && bundleId !== ref.bundle))
223
+ continue;
224
+ const adapterId = source.adapterId ?? detectAdapterId(source.path);
225
+ const owner = resolveAdapterConceptOwner(source.path, adapterId, ref.conceptId, options);
226
+ if (owner)
227
+ return { owner, source, bundleId, adapterId };
228
+ }
229
+ return undefined;
230
+ }
205
231
  export function indexedPathMatchesOwner(indexedPath, owner) {
206
232
  if (path.resolve(indexedPath) !== path.resolve(owner.path))
207
233
  return false;
@@ -81,7 +81,7 @@ export async function generateEmbeddingsForDb(db, config, onProgress, signal, en
81
81
  return { success: false, message: "Semantic search is disabled." };
82
82
  }
83
83
  // #953 field gap: the actionable outcome is a self-diagnosing run, not a
84
- // fix (every RemoteEmbedder path already resolves secret:// through one
84
+ // fix (every RemoteEmbedder path already resolves `${ref}` credentials through one
85
85
  // boundary — a keyless request can only mean embedding.apiKey was absent
86
86
  // from the config THIS run loaded). One default-level line, before the
87
87
  // first provider request of the phase, naming the endpoint/model/credential
@@ -98,7 +98,7 @@ export function drainDirDocuments(adapter, component, fileContexts) {
98
98
  const entry = indexDocumentToStashEntry(doc);
99
99
  // Workflow docs: drop-with-warning if broken; otherwise cache a lossless
100
100
  // runtime projection when the current executor can represent the source.
101
- const dropWarning = handleWorkflowDoc(doc, file, component.root);
101
+ const dropWarning = handleWorkflowDoc(doc, file);
102
102
  if (dropWarning !== null) {
103
103
  warnings.push(dropWarning);
104
104
  if (workflowName !== undefined)
@@ -138,13 +138,13 @@ export function recognizeStashEntries(stashRoot, files) {
138
138
  * `Skipped workflow …` drop warning when it is broken, or return `null` when
139
139
  * it compiles. Non-workflow docs return `null` immediately.
140
140
  */
141
- function handleWorkflowDoc(doc, file, workspaceRoot) {
141
+ function handleWorkflowDoc(doc, file) {
142
142
  if (doc.type !== "workflow" ||
143
143
  (doc.adapterId !== "akm" && doc.adapterId !== "akm-workflow") ||
144
144
  (docRenderer(doc) !== WORKFLOW_MD_RENDERER && doc.adapterId !== "akm-workflow")) {
145
145
  return null;
146
146
  }
147
- const result = compileWorkflowSource(file.content(), { path: file.relPath, workspaceRoot });
147
+ const result = compileWorkflowSource(file.content(), { path: file.relPath });
148
148
  if (!result.ok)
149
149
  return workflowDropWarning(file, result.errors);
150
150
  return null;
@@ -43,7 +43,9 @@ export function resolveSourceEntries(overrideStashDir, existingConfig) {
43
43
  }
44
44
  // Explicit and environment overrides stay first. Without either override,
45
45
  // the configured default enters through the validated loop below.
46
- const sources = implicitStashDir ? [{ path: implicitStashDir, writable: true, isDefault: true }] : [];
46
+ const sources = implicitStashDir
47
+ ? [{ path: implicitStashDir, writable: true, isDefault: true, type: "filesystem" }]
48
+ : [];
47
49
  const sourceIdentity = (dir) => {
48
50
  const resolved = path.resolve(dir);
49
51
  try {
@@ -65,7 +67,7 @@ export function resolveSourceEntries(overrideStashDir, existingConfig) {
65
67
  // identity-less and a user-named primary source ("name": "my-stash")
66
68
  // would validate but match zero entries when filtering.
67
69
  const existing = sources.find((source) => sourceIdentity(source.path) === identity);
68
- if (existing && existing.type === undefined) {
70
+ if (existing && existing.registryId === undefined) {
69
71
  if (registryId)
70
72
  existing.registryId = registryId;
71
73
  if (isDefault)
@@ -1,55 +1,15 @@
1
1
  // This Source Code Form is subject to the terms of the Mozilla Public
2
2
  // License, v. 2.0. If a copy of the MPL was not distributed with this
3
3
  // file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
- import fs from "node:fs";
5
- import path from "node:path";
6
- import { assetPathForName, stashDirFor } from "../../core/asset/asset-placement.js";
7
- import { parseRefInput } from "../../core/asset/resolve-ref.js";
8
- import { isWithin } from "../../core/common.js";
9
- import { resolveSourcesForOrigin } from "../../registry/origin-resolve.js";
4
+ import { conceptIdFromTypeName, parseRefInput } from "../../core/asset/resolve-ref.js";
10
5
  import { lookup } from "../indexer.js";
11
6
  import { deriveInstallations } from "../installations.js";
7
+ import { resolveConceptOwner } from "../lookup/adapter-concept-owner.js";
12
8
  import { resolveSourceEntries } from "../search/search-source.js";
13
9
  function normalizeRef(ref) {
14
10
  // String inputs use the current `[bundle//]conceptId` grammar.
15
11
  return typeof ref === "string" ? parseRefInput(ref) : ref;
16
12
  }
17
- function buildDiskCandidates(sourcePath, ref, preserveDirectNameFallback) {
18
- const placementStashDir = stashDirFor(ref.type);
19
- // Opaque adapter conceptIds are index-resolved. Core cannot infer their
20
- // adapter-owned serialization or path extension before indexing.
21
- if (placementStashDir === undefined)
22
- return [];
23
- const typeDir = path.join(sourcePath, placementStashDir);
24
- const candidates = [
25
- assetPathForName(ref.type, typeDir, ref.name),
26
- path.join(sourcePath, ref.type, `${ref.name}.md`),
27
- path.join(sourcePath, ref.type, ref.name),
28
- ];
29
- if (preserveDirectNameFallback) {
30
- candidates.push(path.join(sourcePath, `${ref.name}.md`), path.join(sourcePath, ref.name));
31
- }
32
- return candidates;
33
- }
34
- function resolveDirectoryEntry(filePath, directoryIndexNames) {
35
- let stat;
36
- try {
37
- stat = fs.statSync(filePath);
38
- }
39
- catch {
40
- return null;
41
- }
42
- if (stat.isFile())
43
- return filePath;
44
- if (!stat.isDirectory())
45
- return null;
46
- for (const indexName of directoryIndexNames) {
47
- const candidate = path.join(filePath, indexName);
48
- if (fs.existsSync(candidate) && fs.statSync(candidate).isFile())
49
- return candidate;
50
- }
51
- return null;
52
- }
53
13
  async function resolveViaIndex(ref) {
54
14
  try {
55
15
  const entry = await lookup(ref);
@@ -60,32 +20,17 @@ async function resolveViaIndex(ref) {
60
20
  }
61
21
  }
62
22
  function resolveViaDisk(ref, options) {
63
- let sources = resolveSourceEntries(options.stashDir);
64
- if (options.honorOrigin !== false) {
65
- sources = resolveSourcesForOrigin(ref.origin, sources);
66
- }
67
- const directoryIndexNames = options.directoryIndexNames ?? ["SKILL.md"];
68
- const preserveDirectNameFallback = options.preserveDirectNameFallback ?? true;
69
- const installations = deriveInstallations(sources);
70
- for (const [index, source] of sources.entries()) {
71
- const bundleId = installations[index]?.id;
72
- if (options.writableBundleIds && (!bundleId || !options.writableBundleIds.has(bundleId)))
73
- continue;
74
- const candidates = buildDiskCandidates(source.path, ref, preserveDirectNameFallback);
75
- for (const candidate of candidates) {
76
- if (!fs.existsSync(candidate))
77
- continue;
78
- const resolved = resolveDirectoryEntry(candidate, directoryIndexNames);
79
- if (!resolved)
80
- continue;
81
- const resolvedRoot = fs.realpathSync(source.path);
82
- const realTarget = fs.realpathSync(resolved);
83
- if (!isWithin(realTarget, resolvedRoot))
84
- continue;
85
- return realTarget;
86
- }
87
- }
88
- return null;
23
+ const allSources = resolveSourceEntries(options.stashDir);
24
+ const installations = deriveInstallations(allSources);
25
+ const sources = options.writableBundleIds
26
+ ? allSources.filter((_, index) => {
27
+ const bundleId = installations[index]?.id;
28
+ return bundleId !== undefined && options.writableBundleIds?.has(bundleId);
29
+ })
30
+ : allSources;
31
+ const bundle = options.honorOrigin !== false ? ref.origin : undefined;
32
+ return (resolveConceptOwner({ bundle, conceptId: conceptIdFromTypeName(ref.type, ref.name) }, sources)?.owner.realPath ??
33
+ null);
89
34
  }
90
35
  export async function resolveAssetPath(ref, options = {}) {
91
36
  const parsed = normalizeRef(ref);