@codyswann/lisa 2.316.2 → 2.317.0

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 (212) hide show
  1. package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
  2. package/dist/core/upstream-evidence-manifest.js +139 -9
  3. package/dist/core/upstream-evidence-manifest.js.map +1 -1
  4. package/package.json +1 -1
  5. package/plugins/lisa/.claude-plugin/plugin.json +1 -1
  6. package/plugins/lisa/.codex-plugin/plugin.json +1 -1
  7. package/plugins/lisa/.codex-plugin/skills/lisa-atlassian-access/SKILL.md +17 -0
  8. package/plugins/lisa/.codex-plugin/skills/lisa-automation-status/SKILL.md +23 -0
  9. package/plugins/lisa/.codex-plugin/skills/lisa-doctor/SKILL.md +33 -0
  10. package/plugins/lisa/.codex-plugin/skills/lisa-implement/SKILL.md +17 -0
  11. package/plugins/lisa/.codex-plugin/skills/lisa-notion-access/SKILL.md +17 -0
  12. package/plugins/lisa/.codex-plugin/skills/lisa-remote-dispatch/SKILL.md +99 -0
  13. package/plugins/lisa/.codex-plugin/skills/lisa-remote-dispatch/agents/openai.yaml +4 -0
  14. package/plugins/lisa/.codex-plugin/skills/lisa-remote-dispatch/scripts/dispatch.mjs +264 -0
  15. package/plugins/lisa/.codex-plugin/skills/lisa-secrets-access/SKILL.md +137 -51
  16. package/plugins/lisa/.codex-plugin/skills/lisa-secrets-access/scripts/doctor-secrets.mjs +217 -0
  17. package/plugins/lisa/.codex-plugin/skills/lisa-secrets-access/scripts/envfile.mjs +78 -0
  18. package/plugins/lisa/.codex-plugin/skills/lisa-secrets-access/scripts/materialize-secrets.mjs +105 -0
  19. package/plugins/lisa/.codex-plugin/skills/lisa-secrets-access/scripts/providers.mjs +240 -0
  20. package/plugins/lisa/.codex-plugin/skills/lisa-secrets-access/scripts/read-secret-note.mjs +82 -0
  21. package/plugins/lisa/.codex-plugin/skills/lisa-secrets-access/scripts/resolve-secret.mjs +151 -176
  22. package/plugins/lisa/.codex-plugin/skills/lisa-secrets-access/scripts/rotate-secret.mjs +266 -0
  23. package/plugins/lisa/.codex-plugin/skills/lisa-secrets-access/scripts/surfaces.mjs +147 -0
  24. package/plugins/lisa/.codex-plugin/skills/lisa-secrets-access/scripts/validate-config.mjs +224 -0
  25. package/plugins/lisa/.codex-plugin/skills/lisa-setup-automations/SKILL.md +69 -0
  26. package/plugins/lisa/.codex-plugin/skills/lisa-setup-automations/scripts/generate-workflow.mjs +185 -0
  27. package/plugins/lisa/.codex-plugin/skills/lisa-setup-remote-env/SKILL.md +150 -0
  28. package/plugins/lisa/.codex-plugin/skills/lisa-setup-remote-env/agents/openai.yaml +4 -0
  29. package/plugins/lisa/.codex-plugin/skills/lisa-setup-remote-env/assets/setup.sh +45 -0
  30. package/plugins/lisa/.codex-plugin/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +227 -0
  31. package/plugins/lisa/.codex-plugin/skills/lisa-setup-remote-env/scripts/toolchain.mjs +169 -0
  32. package/plugins/lisa/.codex-plugin/skills/lisa-setup-remote-env/scripts/verify-remote-env.mjs +186 -0
  33. package/plugins/lisa/commands/implement.md +5 -3
  34. package/plugins/lisa/commands/setup/remote-env.md +7 -0
  35. package/plugins/lisa/skills/lisa-atlassian-access/SKILL.md +17 -0
  36. package/plugins/lisa/skills/lisa-automation-status/SKILL.md +23 -0
  37. package/plugins/lisa/skills/lisa-doctor/SKILL.md +33 -0
  38. package/plugins/lisa/skills/lisa-implement/SKILL.md +17 -0
  39. package/plugins/lisa/skills/lisa-notion-access/SKILL.md +17 -0
  40. package/plugins/lisa/skills/lisa-remote-dispatch/SKILL.md +99 -0
  41. package/plugins/lisa/skills/lisa-remote-dispatch/agents/openai.yaml +4 -0
  42. package/plugins/lisa/skills/lisa-remote-dispatch/scripts/dispatch.mjs +264 -0
  43. package/plugins/lisa/skills/lisa-secrets-access/SKILL.md +138 -52
  44. package/plugins/lisa/skills/lisa-secrets-access/scripts/doctor-secrets.mjs +217 -0
  45. package/plugins/lisa/skills/lisa-secrets-access/scripts/envfile.mjs +78 -0
  46. package/plugins/lisa/skills/lisa-secrets-access/scripts/materialize-secrets.mjs +105 -0
  47. package/plugins/lisa/skills/lisa-secrets-access/scripts/providers.mjs +240 -0
  48. package/plugins/lisa/skills/lisa-secrets-access/scripts/read-secret-note.mjs +82 -0
  49. package/plugins/lisa/skills/lisa-secrets-access/scripts/resolve-secret.mjs +151 -176
  50. package/plugins/lisa/skills/lisa-secrets-access/scripts/rotate-secret.mjs +266 -0
  51. package/plugins/lisa/skills/lisa-secrets-access/scripts/surfaces.mjs +147 -0
  52. package/plugins/lisa/skills/lisa-secrets-access/scripts/validate-config.mjs +224 -0
  53. package/plugins/lisa/skills/lisa-setup-automations/SKILL.md +69 -0
  54. package/plugins/lisa/skills/lisa-setup-automations/scripts/generate-workflow.mjs +185 -0
  55. package/plugins/lisa/skills/lisa-setup-remote-env/SKILL.md +150 -0
  56. package/plugins/lisa/skills/lisa-setup-remote-env/agents/openai.yaml +4 -0
  57. package/plugins/lisa/skills/lisa-setup-remote-env/assets/setup.sh +45 -0
  58. package/plugins/lisa/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +227 -0
  59. package/plugins/lisa/skills/lisa-setup-remote-env/scripts/toolchain.mjs +169 -0
  60. package/plugins/lisa/skills/lisa-setup-remote-env/scripts/verify-remote-env.mjs +186 -0
  61. package/plugins/lisa-agy/commands/lisa/implement.md +5 -3
  62. package/plugins/lisa-agy/commands/lisa/setup/remote-env.md +7 -0
  63. package/plugins/lisa-agy/plugin.json +1 -1
  64. package/plugins/lisa-agy/skills/lisa-atlassian-access/SKILL.md +17 -0
  65. package/plugins/lisa-agy/skills/lisa-automation-status/SKILL.md +23 -0
  66. package/plugins/lisa-agy/skills/lisa-doctor/SKILL.md +33 -0
  67. package/plugins/lisa-agy/skills/lisa-implement/SKILL.md +17 -0
  68. package/plugins/lisa-agy/skills/lisa-notion-access/SKILL.md +17 -0
  69. package/plugins/lisa-agy/skills/lisa-remote-dispatch/SKILL.md +99 -0
  70. package/plugins/lisa-agy/skills/lisa-remote-dispatch/scripts/dispatch.mjs +264 -0
  71. package/plugins/lisa-agy/skills/lisa-secrets-access/SKILL.md +138 -52
  72. package/plugins/lisa-agy/skills/lisa-secrets-access/scripts/doctor-secrets.mjs +217 -0
  73. package/plugins/lisa-agy/skills/lisa-secrets-access/scripts/envfile.mjs +78 -0
  74. package/plugins/lisa-agy/skills/lisa-secrets-access/scripts/materialize-secrets.mjs +105 -0
  75. package/plugins/lisa-agy/skills/lisa-secrets-access/scripts/providers.mjs +240 -0
  76. package/plugins/lisa-agy/skills/lisa-secrets-access/scripts/read-secret-note.mjs +82 -0
  77. package/plugins/lisa-agy/skills/lisa-secrets-access/scripts/resolve-secret.mjs +151 -176
  78. package/plugins/lisa-agy/skills/lisa-secrets-access/scripts/rotate-secret.mjs +266 -0
  79. package/plugins/lisa-agy/skills/lisa-secrets-access/scripts/surfaces.mjs +147 -0
  80. package/plugins/lisa-agy/skills/lisa-secrets-access/scripts/validate-config.mjs +224 -0
  81. package/plugins/lisa-agy/skills/lisa-setup-automations/SKILL.md +69 -0
  82. package/plugins/lisa-agy/skills/lisa-setup-automations/scripts/generate-workflow.mjs +185 -0
  83. package/plugins/lisa-agy/skills/lisa-setup-remote-env/SKILL.md +150 -0
  84. package/plugins/lisa-agy/skills/lisa-setup-remote-env/assets/setup.sh +45 -0
  85. package/plugins/lisa-agy/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +227 -0
  86. package/plugins/lisa-agy/skills/lisa-setup-remote-env/scripts/toolchain.mjs +169 -0
  87. package/plugins/lisa-agy/skills/lisa-setup-remote-env/scripts/verify-remote-env.mjs +186 -0
  88. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  89. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  90. package/plugins/lisa-cdk-agy/plugin.json +1 -1
  91. package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
  92. package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
  93. package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
  94. package/plugins/lisa-copilot/commands/lisa/implement.md +5 -3
  95. package/plugins/lisa-copilot/commands/lisa/setup/remote-env.md +7 -0
  96. package/plugins/lisa-copilot/skills/lisa-atlassian-access/SKILL.md +17 -0
  97. package/plugins/lisa-copilot/skills/lisa-automation-status/SKILL.md +23 -0
  98. package/plugins/lisa-copilot/skills/lisa-doctor/SKILL.md +33 -0
  99. package/plugins/lisa-copilot/skills/lisa-implement/SKILL.md +17 -0
  100. package/plugins/lisa-copilot/skills/lisa-notion-access/SKILL.md +17 -0
  101. package/plugins/lisa-copilot/skills/lisa-remote-dispatch/SKILL.md +99 -0
  102. package/plugins/lisa-copilot/skills/lisa-remote-dispatch/scripts/dispatch.mjs +264 -0
  103. package/plugins/lisa-copilot/skills/lisa-secrets-access/SKILL.md +138 -52
  104. package/plugins/lisa-copilot/skills/lisa-secrets-access/scripts/doctor-secrets.mjs +217 -0
  105. package/plugins/lisa-copilot/skills/lisa-secrets-access/scripts/envfile.mjs +78 -0
  106. package/plugins/lisa-copilot/skills/lisa-secrets-access/scripts/materialize-secrets.mjs +105 -0
  107. package/plugins/lisa-copilot/skills/lisa-secrets-access/scripts/providers.mjs +240 -0
  108. package/plugins/lisa-copilot/skills/lisa-secrets-access/scripts/read-secret-note.mjs +82 -0
  109. package/plugins/lisa-copilot/skills/lisa-secrets-access/scripts/resolve-secret.mjs +151 -176
  110. package/plugins/lisa-copilot/skills/lisa-secrets-access/scripts/rotate-secret.mjs +266 -0
  111. package/plugins/lisa-copilot/skills/lisa-secrets-access/scripts/surfaces.mjs +147 -0
  112. package/plugins/lisa-copilot/skills/lisa-secrets-access/scripts/validate-config.mjs +224 -0
  113. package/plugins/lisa-copilot/skills/lisa-setup-automations/SKILL.md +69 -0
  114. package/plugins/lisa-copilot/skills/lisa-setup-automations/scripts/generate-workflow.mjs +185 -0
  115. package/plugins/lisa-copilot/skills/lisa-setup-remote-env/SKILL.md +150 -0
  116. package/plugins/lisa-copilot/skills/lisa-setup-remote-env/assets/setup.sh +45 -0
  117. package/plugins/lisa-copilot/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +227 -0
  118. package/plugins/lisa-copilot/skills/lisa-setup-remote-env/scripts/toolchain.mjs +169 -0
  119. package/plugins/lisa-copilot/skills/lisa-setup-remote-env/scripts/verify-remote-env.mjs +186 -0
  120. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
  121. package/plugins/lisa-cursor/commands/lisa/implement.md +5 -3
  122. package/plugins/lisa-cursor/commands/lisa/setup/remote-env.md +7 -0
  123. package/plugins/lisa-cursor/skills/lisa-atlassian-access/SKILL.md +17 -0
  124. package/plugins/lisa-cursor/skills/lisa-automation-status/SKILL.md +23 -0
  125. package/plugins/lisa-cursor/skills/lisa-doctor/SKILL.md +33 -0
  126. package/plugins/lisa-cursor/skills/lisa-implement/SKILL.md +17 -0
  127. package/plugins/lisa-cursor/skills/lisa-notion-access/SKILL.md +17 -0
  128. package/plugins/lisa-cursor/skills/lisa-remote-dispatch/SKILL.md +99 -0
  129. package/plugins/lisa-cursor/skills/lisa-remote-dispatch/scripts/dispatch.mjs +264 -0
  130. package/plugins/lisa-cursor/skills/lisa-secrets-access/SKILL.md +138 -52
  131. package/plugins/lisa-cursor/skills/lisa-secrets-access/scripts/doctor-secrets.mjs +217 -0
  132. package/plugins/lisa-cursor/skills/lisa-secrets-access/scripts/envfile.mjs +78 -0
  133. package/plugins/lisa-cursor/skills/lisa-secrets-access/scripts/materialize-secrets.mjs +105 -0
  134. package/plugins/lisa-cursor/skills/lisa-secrets-access/scripts/providers.mjs +240 -0
  135. package/plugins/lisa-cursor/skills/lisa-secrets-access/scripts/read-secret-note.mjs +82 -0
  136. package/plugins/lisa-cursor/skills/lisa-secrets-access/scripts/resolve-secret.mjs +151 -176
  137. package/plugins/lisa-cursor/skills/lisa-secrets-access/scripts/rotate-secret.mjs +266 -0
  138. package/plugins/lisa-cursor/skills/lisa-secrets-access/scripts/surfaces.mjs +147 -0
  139. package/plugins/lisa-cursor/skills/lisa-secrets-access/scripts/validate-config.mjs +224 -0
  140. package/plugins/lisa-cursor/skills/lisa-setup-automations/SKILL.md +69 -0
  141. package/plugins/lisa-cursor/skills/lisa-setup-automations/scripts/generate-workflow.mjs +185 -0
  142. package/plugins/lisa-cursor/skills/lisa-setup-remote-env/SKILL.md +150 -0
  143. package/plugins/lisa-cursor/skills/lisa-setup-remote-env/assets/setup.sh +45 -0
  144. package/plugins/lisa-cursor/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +227 -0
  145. package/plugins/lisa-cursor/skills/lisa-setup-remote-env/scripts/toolchain.mjs +169 -0
  146. package/plugins/lisa-cursor/skills/lisa-setup-remote-env/scripts/verify-remote-env.mjs +186 -0
  147. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  148. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  149. package/plugins/lisa-expo-agy/plugin.json +1 -1
  150. package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
  151. package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
  152. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  153. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  154. package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
  155. package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
  156. package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
  157. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  158. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  159. package/plugins/lisa-nestjs-agy/plugin.json +1 -1
  160. package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
  161. package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
  162. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  163. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  164. package/plugins/lisa-openclaw-agy/plugin.json +1 -1
  165. package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
  166. package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
  167. package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
  168. package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
  169. package/plugins/lisa-phaser-agy/plugin.json +1 -1
  170. package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
  171. package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
  172. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  173. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  174. package/plugins/lisa-rails-agy/plugin.json +1 -1
  175. package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
  176. package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
  177. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  178. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  179. package/plugins/lisa-typescript-agy/plugin.json +1 -1
  180. package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
  181. package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
  182. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  183. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  184. package/plugins/lisa-wiki-agy/plugin.json +1 -1
  185. package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
  186. package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
  187. package/plugins/src/base/commands/implement.md +5 -3
  188. package/plugins/src/base/commands/setup/remote-env.md +7 -0
  189. package/plugins/src/base/skills/lisa-atlassian-access/SKILL.md +17 -0
  190. package/plugins/src/base/skills/lisa-automation-status/SKILL.md +23 -0
  191. package/plugins/src/base/skills/lisa-doctor/SKILL.md +33 -0
  192. package/plugins/src/base/skills/lisa-implement/SKILL.md +17 -0
  193. package/plugins/src/base/skills/lisa-notion-access/SKILL.md +17 -0
  194. package/plugins/src/base/skills/lisa-remote-dispatch/SKILL.md +99 -0
  195. package/plugins/src/base/skills/lisa-remote-dispatch/scripts/dispatch.mjs +264 -0
  196. package/plugins/src/base/skills/lisa-secrets-access/SKILL.md +138 -52
  197. package/plugins/src/base/skills/lisa-secrets-access/scripts/doctor-secrets.mjs +217 -0
  198. package/plugins/src/base/skills/lisa-secrets-access/scripts/envfile.mjs +78 -0
  199. package/plugins/src/base/skills/lisa-secrets-access/scripts/materialize-secrets.mjs +105 -0
  200. package/plugins/src/base/skills/lisa-secrets-access/scripts/providers.mjs +240 -0
  201. package/plugins/src/base/skills/lisa-secrets-access/scripts/read-secret-note.mjs +82 -0
  202. package/plugins/src/base/skills/lisa-secrets-access/scripts/resolve-secret.mjs +151 -176
  203. package/plugins/src/base/skills/lisa-secrets-access/scripts/rotate-secret.mjs +266 -0
  204. package/plugins/src/base/skills/lisa-secrets-access/scripts/surfaces.mjs +147 -0
  205. package/plugins/src/base/skills/lisa-secrets-access/scripts/validate-config.mjs +224 -0
  206. package/plugins/src/base/skills/lisa-setup-automations/SKILL.md +69 -0
  207. package/plugins/src/base/skills/lisa-setup-automations/scripts/generate-workflow.mjs +185 -0
  208. package/plugins/src/base/skills/lisa-setup-remote-env/SKILL.md +150 -0
  209. package/plugins/src/base/skills/lisa-setup-remote-env/assets/setup.sh +45 -0
  210. package/plugins/src/base/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +227 -0
  211. package/plugins/src/base/skills/lisa-setup-remote-env/scripts/toolchain.mjs +169 -0
  212. package/plugins/src/base/skills/lisa-setup-remote-env/scripts/verify-remote-env.mjs +186 -0
@@ -0,0 +1,82 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Print one credential's usage note without opening the values file.
4
+ *
5
+ * This is a separate program from the resolver on purpose. An agent about to
6
+ * use a credential needs to know its blast radius — scope, owner, rotation,
7
+ * cautions — and should be able to learn that without any code path that could
8
+ * serialize a secret value. Values never enter this process, so its output
9
+ * cannot leak one even if it is logged.
10
+ *
11
+ * Usage:
12
+ * read-secret-note.mjs NAME
13
+ * @module read-secret-note
14
+ */
15
+
16
+ import { existsSync, readFileSync } from "node:fs";
17
+
18
+ import { materializedPaths, readConfig } from "./surfaces.mjs";
19
+
20
+ /** A key must be an exact environment-variable name; see the naming rule. */
21
+ const ENV_KEY = /^[A-Za-z_][A-Za-z0-9_]*$/;
22
+
23
+ /**
24
+ * Load the notes manifest, rejecting a shape this version does not understand.
25
+ * @param {string} notesFile Path to `secret-notes.json`.
26
+ * @returns {Record<string, string>} Notes by exact key name.
27
+ */
28
+ export function loadNotes(notesFile) {
29
+ if (!existsSync(notesFile)) {
30
+ throw new Error(
31
+ `secret notes are unavailable at ${notesFile}.\n` +
32
+ `Run the remote environment bootstrap before using a credential.`
33
+ );
34
+ }
35
+ const payload = JSON.parse(readFileSync(notesFile, "utf8"));
36
+ if (payload.schemaVersion !== 1 || typeof payload.secrets !== "object") {
37
+ throw new Error("unsupported or malformed secret-notes manifest");
38
+ }
39
+ return payload.secrets;
40
+ }
41
+
42
+ /**
43
+ * Resolve one note, treating absence as a stop condition rather than a default.
44
+ *
45
+ * A missing note is deliberately fatal for the credential that lacks it. The
46
+ * alternative — carrying on with an inferred purpose — is how a token scoped to
47
+ * one repository gets used against another, and the failure is silent.
48
+ * @param {string} name Exact environment-variable name.
49
+ * @param {Record<string, string>} notes Loaded manifest entries.
50
+ * @returns {string} The note text.
51
+ */
52
+ export function noteFor(name, notes) {
53
+ if (!ENV_KEY.test(name)) {
54
+ throw new Error("secret key must be an exact environment-variable name");
55
+ }
56
+ const note = notes[name];
57
+ if (typeof note !== "string" || !note.trim()) {
58
+ throw new Error(
59
+ `no usable note exists for ${name}.\n` +
60
+ `A missing or ambiguous note is a stop condition for that credential — ` +
61
+ `do not infer its purpose from its name.`
62
+ );
63
+ }
64
+ return note;
65
+ }
66
+
67
+ function main() {
68
+ const [name] = process.argv.slice(2);
69
+ if (!name) throw new Error("usage: read-secret-note.mjs NAME");
70
+ const cfg = readConfig();
71
+ const { notesFile } = materializedPaths(cfg.namespace);
72
+ console.log(noteFor(name, loadNotes(notesFile)));
73
+ }
74
+
75
+ if (import.meta.url === `file://${process.argv[1]}`) {
76
+ try {
77
+ main();
78
+ } catch (err) {
79
+ console.error(err.message);
80
+ process.exit(1);
81
+ }
82
+ }
@@ -1,15 +1,19 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * Resolve a secret through the configured provider.
3
+ * Resolve a secret through the configured provider and surface.
4
4
  *
5
5
  * The executable half of `lisa-secrets-access`. It exists as a CLI rather than
6
6
  * a library because the callers are polyglot — Python scripts, shell, and
7
7
  * TypeScript all need the same answer, and a subprocess is the only interface
8
8
  * all three share without duplicating the logic three times.
9
9
  *
10
- * Resolution order is environment, then provider. The environment comes first
11
- * so a CI run — where secrets are injected by the pipeline — never reaches for
12
- * a provider or a local store at all.
10
+ * The resolution ladder is one rule with one order:
11
+ *
12
+ * environment -> materialized file (surfaces that have one) -> provider
13
+ *
14
+ * Environment comes first so a CI run, where the pipeline injects secrets,
15
+ * never reaches for a provider or a local store at all. The middle rung exists
16
+ * only on surfaces whose bootstrap runs before the consuming process does.
13
17
  *
14
18
  * Values are written to stdout with no trailing newline and nothing else, so
15
19
  * `$(resolve-secret.mjs get NAME)` is safe. Diagnostics go to stderr.
@@ -19,160 +23,65 @@
19
23
  * resolve-secret.mjs list
20
24
  * resolve-secret.mjs describe NAME
21
25
  * resolve-secret.mjs verify
26
+ * resolve-secret.mjs surface
27
+ * @module resolve-secret
22
28
  */
23
29
 
24
- import { execFileSync } from "node:child_process";
25
30
  import { existsSync, readFileSync } from "node:fs";
26
- import { join } from "node:path";
27
31
 
28
- /** Config defaults when `.lisa.config.json` carries no `secrets` block. */
29
- const DEFAULTS = {
30
- provider: "env",
31
- bootstrap: { sources: ["env"], key: null },
32
- require: null,
33
- };
32
+ import { parseEnv } from "./envfile.mjs";
33
+ import { ENV_KEY, fetchAll } from "./providers.mjs";
34
+ import { materializedPaths, readConfig } from "./surfaces.mjs";
34
35
 
35
- /**
36
- * Read the `secrets` block from `.lisa.config.json`.
37
- * @param {string} cwd Directory to look in.
38
- * @returns {{provider: string, bootstrap: {sources: string[], key: string|null}, require: string[]|null}}
39
- */
40
- export function readConfig(cwd = process.cwd()) {
41
- const path = join(cwd, ".lisa.config.json");
42
- if (!existsSync(path)) return DEFAULTS;
43
- try {
44
- const cfg = JSON.parse(readFileSync(path, "utf8")).secrets;
45
- if (!cfg) return DEFAULTS;
46
- return {
47
- provider: cfg.provider ?? DEFAULTS.provider,
48
- bootstrap: { ...DEFAULTS.bootstrap, ...(cfg.bootstrap ?? {}) },
49
- require: cfg.require ?? null,
50
- };
51
- } catch (err) {
52
- throw new Error(`.lisa.config.json is not readable: ${err.message}`);
53
- }
54
- }
36
+ export { readConfig };
55
37
 
56
38
  /**
57
- * Obtain the one credential that unlocks the provider.
39
+ * Read the materialized values file, if this surface has one.
58
40
  *
59
- * Walks `sources` in order. This is the only credential permitted in an OS
60
- * keychain it is a bootstrap, not a cached copy of anything.
61
- * @param {{sources: string[], key: string|null}} bootstrap Bootstrap config.
62
- * @returns {string|null} The token, or null when the provider needs none.
41
+ * A surface without the capability returns empty rather than throwing: the
42
+ * absence of a middle rung is normal, not an error.
43
+ * @param {object} cfg Resolved configuration.
44
+ * @returns {Map<string, string>} Values by exact name.
63
45
  */
64
- function bootstrapToken(bootstrap) {
65
- if (!bootstrap.key) return null;
66
- for (const source of bootstrap.sources) {
67
- if (source === "env") {
68
- const v = (process.env[bootstrap.key] ?? "").trim();
69
- if (v) return v;
70
- }
71
- if (source === "keychain" && process.platform === "darwin") {
72
- try {
73
- const v = execFileSync(
74
- "security",
75
- [
76
- "find-generic-password",
77
- "-s",
78
- bootstrap.key,
79
- "-a",
80
- process.env.USER ?? "",
81
- "-w",
82
- ],
83
- { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }
84
- ).trim();
85
- if (v) return v;
86
- } catch {
87
- /* not present in the keychain; try the next source */
88
- }
89
- }
90
- }
91
- throw new Error(
92
- `${bootstrap.key} not found in: ${bootstrap.sources.join(", ")}.\n` +
93
- `It is the bootstrap credential — without it no other secret can be read.`
94
- );
46
+ export function readMaterialized(cfg) {
47
+ if (!cfg.capabilities.materialized) return new Map();
48
+ const { valuesFile } = materializedPaths(cfg.namespace);
49
+ if (!existsSync(valuesFile)) return new Map();
50
+ return parseEnv(readFileSync(valuesFile, "utf8"));
95
51
  }
96
52
 
97
53
  /**
98
- * Fetch every secret the provider grants, keyed by name.
54
+ * Assert a name is declared when the project narrows itself with `require`.
99
55
  *
100
- * Deliberately fetches all rather than one: the provider already scopes what
101
- * this caller may see, so the set it returns *is* the permitted set, and
102
- * re-stating that as a list in config would duplicate a boundary the provider
103
- * already enforces.
104
- * @param {ReturnType<typeof readConfig>} cfg Resolved config.
105
- * @returns {Map<string, {value: string, note: string}>} Secrets by name.
56
+ * A `require` list is an assertion, not merely a filter: naming a secret
57
+ * declares the project needs it, so asking for one outside the list is a
58
+ * configuration error rather than a lookup miss.
59
+ * @param {string} name Requested name.
60
+ * @param {object} cfg Resolved configuration.
106
61
  */
107
- function fetchAll(cfg) {
108
- const out = new Map();
109
- const token = bootstrapToken(cfg.bootstrap);
110
- const env = { ...process.env };
111
- if (cfg.bootstrap.key && token) env[cfg.bootstrap.key] = token;
112
-
113
- switch (cfg.provider) {
114
- case "env":
115
- for (const [k, v] of Object.entries(process.env)) {
116
- if (/^[A-Z][A-Z0-9_]*$/.test(k))
117
- out.set(k, { value: v ?? "", note: "" });
118
- }
119
- return out;
120
-
121
- case "bitwarden": {
122
- const raw = execFileSync("bws", ["secret", "list", "--output", "json"], {
123
- encoding: "utf8",
124
- env,
125
- stdio: ["ignore", "pipe", "pipe"],
126
- });
127
- for (const s of JSON.parse(raw || "[]")) {
128
- out.set(s.key, { value: s.value, note: s.note ?? "" });
129
- }
130
- return out;
131
- }
132
-
133
- case "doppler": {
134
- const raw = execFileSync(
135
- "doppler",
136
- ["secrets", "download", "--no-file", "--format", "json"],
137
- {
138
- encoding: "utf8",
139
- env,
140
- stdio: ["ignore", "pipe", "pipe"],
141
- }
142
- );
143
- for (const [k, v] of Object.entries(JSON.parse(raw || "{}"))) {
144
- out.set(k, { value: String(v), note: "" });
145
- }
146
- return out;
147
- }
148
-
149
- default:
150
- throw new Error(
151
- `provider "${cfg.provider}" has no bulk read implemented yet.\n` +
152
- `Add one in resolve-secret.mjs — see the dispatch table in SKILL.md.`
153
- );
62
+ function assertDeclared(name, cfg) {
63
+ if (cfg.require && !cfg.require.includes(name)) {
64
+ throw new Error(
65
+ `${name} is not declared in secrets.require.\n` +
66
+ `Declared: ${cfg.require.join(", ") || "(none)"}`
67
+ );
154
68
  }
155
69
  }
156
70
 
157
71
  /**
158
- * Resolve one secret: environment first, then the provider.
72
+ * Resolve one secret down the ladder.
159
73
  * @param {string} name Environment-variable-style key.
160
- * @param {ReturnType<typeof readConfig>} [cfg] Resolved config.
74
+ * @param {object} [cfg] Resolved configuration.
161
75
  * @returns {string} The secret value.
162
76
  */
163
77
  export function get(name, cfg = readConfig()) {
164
78
  const fromEnv = (process.env[name] ?? "").trim();
165
79
  if (fromEnv) return fromEnv;
166
80
 
167
- // A `require` list is an assertion, not just a filter: naming a secret
168
- // declares that this project needs it, so asking for one outside the list is
169
- // a configuration error rather than a lookup miss.
170
- if (cfg.require && !cfg.require.includes(name)) {
171
- throw new Error(
172
- `${name} is not declared in secrets.require.\n` +
173
- `Declared: ${cfg.require.join(", ") || "(none)"}`
174
- );
175
- }
81
+ assertDeclared(name, cfg);
82
+
83
+ const fromFile = readMaterialized(cfg).get(name);
84
+ if (fromFile) return fromFile;
176
85
 
177
86
  const all = fetchAll(cfg);
178
87
  const hit = all.get(name);
@@ -186,10 +95,108 @@ export function get(name, cfg = readConfig()) {
186
95
  return hit.value;
187
96
  }
188
97
 
98
+ /**
99
+ * Report which rung of the ladder answers for a name, without revealing values.
100
+ * @param {string} name Requested name.
101
+ * @param {Map<string, object>} provider Provider view.
102
+ * @param {Map<string, string>} file Materialized view.
103
+ * @returns {string} A fixed-width source label.
104
+ */
105
+ function sourceOf(name, provider, file) {
106
+ if ((process.env[name] ?? "").trim()) return "env ";
107
+ if (file.get(name)) return "file ";
108
+ if (provider.get(name)?.value) return "provider ";
109
+ return " ";
110
+ }
111
+
112
+ /**
113
+ * Verify every declared secret, mirroring the ladder's own order.
114
+ *
115
+ * Checking only the provider would report MISSING for every secret in CI, where
116
+ * the pipeline injects them as environment variables — a false negative exactly
117
+ * where verification matters most.
118
+ *
119
+ * This command is read-only. Proving that a rotating credential can actually be
120
+ * written back requires a write, so that check lives in the rotation path
121
+ * rather than here.
122
+ * @param {object} cfg Resolved configuration.
123
+ * @returns {number} Count of secrets that failed.
124
+ */
125
+ function verify(cfg) {
126
+ const provider = fetchAll(cfg);
127
+ const file = readMaterialized(cfg);
128
+ const names = cfg.require ?? [
129
+ ...new Set([...provider.keys(), ...file.keys()]),
130
+ ];
131
+ const rotating = new Set(cfg.rotating ?? []);
132
+ let bad = 0;
133
+
134
+ console.log(`surface: ${cfg.surface} provider: ${cfg.provider}`);
135
+ for (const name of names) {
136
+ const resolves = Boolean(
137
+ (process.env[name] ?? "").trim() ||
138
+ file.get(name) ||
139
+ provider.get(name)?.value
140
+ );
141
+ const named = ENV_KEY.test(name);
142
+ const noted = Boolean(provider.get(name)?.note);
143
+ if (!resolves || !named || !noted) bad += 1;
144
+ console.log(
145
+ ` ${name.padEnd(30)} ${resolves ? "resolves" : "MISSING "} ` +
146
+ `${sourceOf(name, provider, file)} ` +
147
+ `${named ? "name ok" : "NAME NOT UPPER_SNAKE"} ` +
148
+ `${noted ? "noted" : "NO NOTE"}` +
149
+ `${rotating.has(name) ? " rotating" : ""}`
150
+ );
151
+ }
152
+
153
+ for (const name of rotating) {
154
+ if (!cfg.bootstrap.key) {
155
+ console.log(
156
+ ` ${name.padEnd(30)} ROTATING BUT NO BOOTSTRAP — the replacement could ` +
157
+ `not be written back, which strands the credential on first use`
158
+ );
159
+ bad += 1;
160
+ }
161
+ }
162
+ return bad;
163
+ }
164
+
165
+ /**
166
+ * Describe a secret's usage note without ever printing its value.
167
+ * @param {string} name Requested name.
168
+ * @param {object} cfg Resolved configuration.
169
+ */
170
+ function describe(name, cfg) {
171
+ const hit = fetchAll(cfg).get(name);
172
+ if (hit) {
173
+ // Infer *and* warn, never instead of. A silent fallback that works well
174
+ // enough is what guarantees the notes stay empty forever.
175
+ console.log(
176
+ hit.note ||
177
+ `(no note — purpose inferred from the name: ${name}. ` +
178
+ `An inferred mapping must never authorise a write.)`
179
+ );
180
+ return;
181
+ }
182
+ // Environment-only is a legitimate state, not an error: CI injects secrets
183
+ // that never appear in a provider listing. Say so rather than claiming the
184
+ // secret does not exist, and never print the value.
185
+ if ((process.env[name] ?? "").trim()) {
186
+ console.log("(set in the environment; no provider entry, so no note)");
187
+ return;
188
+ }
189
+ throw new Error(`${name} not found in the environment or the provider`);
190
+ }
191
+
189
192
  function main() {
190
193
  const [op, name] = process.argv.slice(2);
191
194
  const cfg = readConfig();
192
195
 
196
+ if (op === "surface") {
197
+ console.log(cfg.surface);
198
+ return;
199
+ }
193
200
  if (op === "get") {
194
201
  if (!name) throw new Error("usage: resolve-secret.mjs get NAME");
195
202
  process.stdout.write(get(name, cfg));
@@ -197,58 +204,26 @@ function main() {
197
204
  }
198
205
  if (op === "list") {
199
206
  // Names only. This command must never be able to leak a value.
200
- console.log([...fetchAll(cfg).keys()].sort().join("\n"));
207
+ const names = new Set([
208
+ ...fetchAll(cfg).keys(),
209
+ ...readMaterialized(cfg).keys(),
210
+ ]);
211
+ console.log([...names].sort().join("\n"));
201
212
  return;
202
213
  }
203
214
  if (op === "describe") {
204
215
  if (!name) throw new Error("usage: resolve-secret.mjs describe NAME");
205
- const hit = fetchAll(cfg).get(name);
206
- if (hit) {
207
- console.log(
208
- hit.note || `(no note — purpose inferred from the name: ${name})`
209
- );
210
- return;
211
- }
212
- // Environment-only is a legitimate state, not an error: CI injects secrets
213
- // that never appear in the provider listing. Say so rather than claiming
214
- // the secret does not exist, and never print the value.
215
- if ((process.env[name] ?? "").trim()) {
216
- console.log(
217
- `(set in the environment; no provider entry, so no note is available)`
218
- );
219
- return;
220
- }
221
- throw new Error(`${name} not found in the environment or the provider`);
216
+ describe(name, cfg);
217
+ return;
222
218
  }
223
219
  if (op === "verify") {
224
- const all = fetchAll(cfg);
225
- const names = cfg.require ?? [...all.keys()];
226
- let bad = 0;
227
- for (const n of names) {
228
- // Mirror get()'s environment-first order. Checking only the provider
229
- // reports MISSING for every secret in CI, where the pipeline injects
230
- // them as environment variables — a false negative precisely where
231
- // verification matters most.
232
- const fromEnv = Boolean((process.env[n] ?? "").trim());
233
- const entry = all.get(n);
234
- const ok = fromEnv || Boolean(entry?.value);
235
- const named = /^[A-Z][A-Z0-9_]*$/.test(n);
236
- const noted = Boolean(entry?.note);
237
- if (!ok || !named) bad += 1;
238
- const source = fromEnv
239
- ? "env "
240
- : entry?.value
241
- ? "provider"
242
- : " ";
243
- console.log(
244
- ` ${n.padEnd(30)} ${ok ? "resolves" : "MISSING "} ${source} ` +
245
- `${named ? "name ok" : "NAME NOT UPPER_SNAKE"} ${noted ? "noted" : "no note"}`
246
- );
247
- }
220
+ const bad = verify(cfg);
248
221
  if (bad) throw new Error(`${bad} secret(s) failed verification`);
249
222
  return;
250
223
  }
251
- throw new Error("usage: resolve-secret.mjs get|list|describe|verify [NAME]");
224
+ throw new Error(
225
+ "usage: resolve-secret.mjs get|list|describe|verify|surface [NAME]"
226
+ );
252
227
  }
253
228
 
254
229
  if (import.meta.url === `file://${process.argv[1]}`) {