@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,217 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Health checks for a project's secrets configuration.
4
+ *
5
+ * The most valuable check here is the last one, and it is the reason this file
6
+ * exists. A value present in both the provider and a local copy is **not a
7
+ * duplicate** — it is two live credentials, one of which is untracked. Both
8
+ * authenticate, so the difference is invisible from either side, and "tidying
9
+ * up the duplicate" deletes a working credential that no record accounts for.
10
+ * That is not hypothetical; it is what prompted the single-store rule.
11
+ *
12
+ * Every check reports without printing a value. Where two copies must be
13
+ * compared, they are compared by digest.
14
+ *
15
+ * Usage:
16
+ * doctor-secrets.mjs
17
+ * @module doctor-secrets
18
+ */
19
+
20
+ import { createHash } from "node:crypto";
21
+
22
+ import { fetchAll } from "./providers.mjs";
23
+ import { readMaterialized } from "./resolve-secret.mjs";
24
+ import { readConfig } from "./surfaces.mjs";
25
+
26
+ /**
27
+ * Build a collector so each run owns its findings.
28
+ *
29
+ * Module-level state would make the checks untestable in isolation and would
30
+ * leak findings between runs in any process that calls them twice.
31
+ * @returns {{findings: object[], report: Function}} A collector.
32
+ */
33
+ export function collector() {
34
+ const findings = [];
35
+ return {
36
+ findings,
37
+ report: (level, name, message) => findings.push({ level, name, message }),
38
+ };
39
+ }
40
+
41
+ /**
42
+ * Digest a value so two copies can be compared without exposing either.
43
+ * @param {string} value Secret value.
44
+ * @returns {string} A short digest.
45
+ */
46
+ function fingerprint(value) {
47
+ return createHash("sha256").update(value).digest("hex").slice(0, 12);
48
+ }
49
+
50
+ /**
51
+ * Assert every declared name actually resolves.
52
+ * @param {object} cfg Resolved configuration.
53
+ * @param {Map<string, object>} provider Provider view.
54
+ * @param {Map<string, string>} file Materialized view.
55
+ * @param {Function} report Finding collector.
56
+ */
57
+ export function checkRequired(cfg, provider, file, report) {
58
+ for (const name of cfg.require ?? []) {
59
+ const resolves =
60
+ (process.env[name] ?? "").trim() ||
61
+ file.get(name) ||
62
+ provider.get(name)?.value;
63
+ if (resolves) report("ok", name, "resolves");
64
+ else
65
+ report(
66
+ "error",
67
+ name,
68
+ "declared in secrets.require but resolves nowhere — a startup error, " +
69
+ "not a late surprise"
70
+ );
71
+ }
72
+ }
73
+
74
+ /**
75
+ * Assert every key is an exact environment-variable name.
76
+ * @param {Map<string, object>} provider Provider view.
77
+ * @param {Function} report Finding collector.
78
+ */
79
+ export function checkNaming(provider, report) {
80
+ for (const name of provider.keys()) {
81
+ if (!/^[A-Z][A-Z0-9_]*$/.test(name)) {
82
+ report(
83
+ "warn",
84
+ name,
85
+ "key is not UPPER_SNAKE_CASE, so it will not resolve for the " +
86
+ "environment variable of that name — lookup is exact, never fuzzy"
87
+ );
88
+ }
89
+ }
90
+ }
91
+
92
+ /**
93
+ * Assert every secret carries a usage note.
94
+ * @param {Map<string, object>} provider Provider view.
95
+ * @param {Function} report Finding collector.
96
+ */
97
+ export function checkNotes(provider, report) {
98
+ for (const [name, entry] of provider) {
99
+ if (!entry.note?.trim()) {
100
+ report(
101
+ "warn",
102
+ name,
103
+ "has no usage note. An agent cannot learn this credential's scope " +
104
+ "without one, and inferring it from the name is exactly the guess " +
105
+ "that writes to the wrong system"
106
+ );
107
+ }
108
+ }
109
+ }
110
+
111
+ /**
112
+ * Assert every rotating credential could persist its replacement.
113
+ * @param {object} cfg Resolved configuration.
114
+ * @param {Map<string, object>} provider Provider view.
115
+ * @param {Function} report Finding collector.
116
+ */
117
+ export function checkRotating(cfg, provider, report) {
118
+ for (const name of cfg.rotating ?? []) {
119
+ if (!cfg.bootstrap?.key) {
120
+ report(
121
+ "error",
122
+ name,
123
+ "is declared rotating but the lane has no bootstrap, so a replacement " +
124
+ "could not be written back. Using it once would strand it"
125
+ );
126
+ continue;
127
+ }
128
+ if (!provider.get(name)?.id) {
129
+ report(
130
+ "error",
131
+ name,
132
+ "is declared rotating but has no provider identifier to write back to"
133
+ );
134
+ continue;
135
+ }
136
+ report("ok", name, "rotating, with a write path");
137
+ }
138
+ }
139
+
140
+ /**
141
+ * Detect the same credential living in more than one store.
142
+ *
143
+ * Two different values under one name are two live credentials, not a stale
144
+ * copy — treat it as stop-and-ask rather than something to adjudicate. Deleting
145
+ * the one you cannot verify leaves a working credential recorded nowhere.
146
+ * @param {Map<string, object>} provider Provider view.
147
+ * @param {Map<string, string>} file Materialized view.
148
+ * @param {Function} report Finding collector.
149
+ */
150
+ export function checkTwoStores(provider, file, report) {
151
+ for (const [name, value] of file) {
152
+ const fromProvider = provider.get(name)?.value;
153
+ if (!fromProvider) continue;
154
+ if (fromProvider === value) {
155
+ report(
156
+ "warn",
157
+ name,
158
+ "exists in both the provider and a local copy. Same value today, but " +
159
+ "two stores drift; the local copy should be re-materialized, never edited"
160
+ );
161
+ continue;
162
+ }
163
+ report(
164
+ "error",
165
+ name,
166
+ `differs between the provider (${fingerprint(fromProvider)}) and the ` +
167
+ `local copy (${fingerprint(value)}). These are TWO LIVE CREDENTIALS, ` +
168
+ `not a duplicate. Stop and ask — deleting the one you cannot verify ` +
169
+ `leaves a working credential that no record accounts for`
170
+ );
171
+ }
172
+ }
173
+
174
+ function main() {
175
+ const cfg = readConfig();
176
+
177
+ if (cfg.provider === "env") {
178
+ console.log(
179
+ "secrets: no credentials manager configured (provider: env).\n" +
180
+ " This is supported — the environment is the provider. A manager is the\n" +
181
+ " preferred path because it gives one store, rotation, and an audit trail,\n" +
182
+ " but it is never required. Configure one with secrets.provider when ready."
183
+ );
184
+ return;
185
+ }
186
+
187
+ const provider = fetchAll(cfg);
188
+ const file = readMaterialized(cfg);
189
+
190
+ console.log(`secrets: provider ${cfg.provider}, surface ${cfg.surface}`);
191
+ const { findings, report } = collector();
192
+ checkRequired(cfg, provider, file, report);
193
+ checkNaming(provider, report);
194
+ checkNotes(provider, report);
195
+ checkRotating(cfg, provider, report);
196
+ checkTwoStores(provider, file, report);
197
+
198
+ const order = { error: 0, warn: 1, ok: 2 };
199
+ findings.sort((a, b) => order[a.level] - order[b.level]);
200
+ for (const f of findings) {
201
+ console.log(
202
+ ` ${f.level.toUpperCase().padEnd(5)} ${f.name.padEnd(28)} ${f.message}`
203
+ );
204
+ }
205
+
206
+ const errors = findings.filter(f => f.level === "error").length;
207
+ if (errors) throw new Error(`${errors} secrets configuration error(s)`);
208
+ }
209
+
210
+ if (import.meta.url === `file://${process.argv[1]}`) {
211
+ try {
212
+ main();
213
+ } catch (err) {
214
+ console.error(err.message);
215
+ process.exit(1);
216
+ }
217
+ }
@@ -0,0 +1,78 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * The `secrets.env` format: writer and reader, kept together.
4
+ *
5
+ * The file has two consumers with different needs. A shell sources it, so it
6
+ * must be valid POSIX with every value quoted. The resolver reads it back, so
7
+ * the quoting must be exactly reversible. Those two requirements only stay
8
+ * consistent if the pair lives in one module — a writer and a parser that drift
9
+ * apart corrupt any value containing a quote, and do it silently.
10
+ * @module envfile
11
+ */
12
+
13
+ /**
14
+ * POSIX single-quote a value so a shell reproduces it byte for byte.
15
+ *
16
+ * Single quotes suppress every expansion, so the only character needing care is
17
+ * the quote itself: close the string, emit an escaped quote, reopen.
18
+ * @param {string} value Raw value.
19
+ * @returns {string} A single-quoted shell token.
20
+ */
21
+ export function quote(value) {
22
+ return `'${String(value).replaceAll("'", `'\\''`)}'`;
23
+ }
24
+
25
+ /**
26
+ * Render selected secrets as a sourceable environment file.
27
+ * @param {Map<string, {value: string}>} selected Secrets by exact name.
28
+ * @returns {string} File contents, newline-terminated.
29
+ */
30
+ export function renderEnv(selected) {
31
+ const lines = [
32
+ "# Generated by lisa-secrets-access. Contains secret values.",
33
+ "# Do not print, inspect, summarize, commit, or copy this file.",
34
+ ];
35
+ for (const key of [...selected.keys()].sort()) {
36
+ lines.push(`export ${key}=${quote(selected.get(key).value)}`);
37
+ }
38
+ return `${lines.join("\n")}\n`;
39
+ }
40
+
41
+ /**
42
+ * Parse a file produced by {@link renderEnv} back into values.
43
+ *
44
+ * Deliberately strict: it understands only the shape this module writes. A line
45
+ * it does not recognise is skipped rather than guessed at, because a partial
46
+ * parse that yields a truncated secret is worse than one that yields nothing.
47
+ * @param {string} text File contents.
48
+ * @returns {Map<string, string>} Values by exact name.
49
+ */
50
+ export function parseEnv(text) {
51
+ const out = new Map();
52
+ for (const line of text.split("\n")) {
53
+ const match = /^export ([A-Za-z_][A-Za-z0-9_]*)='(.*)'$/.exec(line);
54
+ if (!match) continue;
55
+ out.set(match[1], match[2].replaceAll(`'\\''`, "'"));
56
+ }
57
+ return out;
58
+ }
59
+
60
+ /**
61
+ * Render usage notes with every value intentionally omitted.
62
+ *
63
+ * The separation is the point: an agent reading a note must have no path to a
64
+ * value, so the note file is a different file with a different reader.
65
+ * @param {Map<string, {note: string}>} selected Secrets by exact name.
66
+ * @returns {string} JSON contents, newline-terminated.
67
+ */
68
+ export function renderNotes(selected) {
69
+ const secrets = {};
70
+ for (const key of [...selected.keys()].sort()) {
71
+ const note = selected.get(key).note ?? "";
72
+ if (typeof note !== "string") {
73
+ throw new Error(`secret note is not a string: ${key}`);
74
+ }
75
+ secrets[key] = note;
76
+ }
77
+ return `${JSON.stringify({ schemaVersion: 1, secrets }, null, 2)}\n`;
78
+ }
@@ -0,0 +1,105 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Write the provider's current view to the two files a materializing surface
4
+ * reads.
5
+ *
6
+ * Only surfaces that cannot read through live use this. A remote agent
7
+ * container prepares itself during setup — before any task exists, and often
8
+ * before network policy would permit a provider call from the task itself — so
9
+ * files written at that moment are the only channel available. Everywhere else
10
+ * this is forbidden, because a value on disk is a copy that can drift and leak.
11
+ *
12
+ * Both files are written atomically from one provider response, so values and
13
+ * notes always describe the same revision. A rename within the destination
14
+ * directory is atomic on the same filesystem; a temporary file elsewhere would
15
+ * not be.
16
+ *
17
+ * Usage:
18
+ * materialize-secrets.mjs [--dry-run]
19
+ * @module materialize-secrets
20
+ */
21
+
22
+ import {
23
+ chmodSync,
24
+ mkdirSync,
25
+ renameSync,
26
+ rmSync,
27
+ writeFileSync,
28
+ } from "node:fs";
29
+
30
+ import { renderEnv, renderNotes } from "./envfile.mjs";
31
+ import { fetchAll } from "./providers.mjs";
32
+ import { materializedPaths, readConfig } from "./surfaces.mjs";
33
+
34
+ /**
35
+ * Write one file to a temporary sibling, then move it into place.
36
+ *
37
+ * The mode is set before the rename so the file is never briefly readable by
38
+ * anyone else, and the temporary sibling is removed on any failure so a partial
39
+ * write cannot be mistaken for a complete one.
40
+ * @param {string} destination Final path.
41
+ * @param {string} contents File body.
42
+ */
43
+ function writeAtomic(destination, contents) {
44
+ const temporary = `${destination}.tmp-${process.pid}`;
45
+ try {
46
+ writeFileSync(temporary, contents, { mode: 0o600 });
47
+ chmodSync(temporary, 0o600);
48
+ renameSync(temporary, destination);
49
+ } catch (err) {
50
+ rmSync(temporary, { force: true });
51
+ throw err;
52
+ }
53
+ }
54
+
55
+ /**
56
+ * Materialize the configured provider view for the current surface.
57
+ * @param {object} [cfg] Resolved configuration.
58
+ * @returns {{count: number, dir: string}} What was written, and where.
59
+ */
60
+ export function materialize(cfg = readConfig()) {
61
+ if (!cfg.capabilities.mayWriteValues) {
62
+ throw new Error(
63
+ `surface "${cfg.surface}" may not write secret values to disk.\n` +
64
+ `It can read through to the provider, so a copy on disk would add drift ` +
65
+ `and exposure without adding capability.`
66
+ );
67
+ }
68
+
69
+ const selected = fetchAll(cfg);
70
+ if (!selected.size) {
71
+ throw new Error(
72
+ "no exportable secrets matched the configured boundary.\n" +
73
+ "Check the machine account's project grants and any narrowing filters."
74
+ );
75
+ }
76
+
77
+ const { dir, valuesFile, notesFile } = materializedPaths(cfg.namespace);
78
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
79
+ chmodSync(dir, 0o700);
80
+ writeAtomic(valuesFile, renderEnv(selected));
81
+ writeAtomic(notesFile, renderNotes(selected));
82
+ return { count: selected.size, dir };
83
+ }
84
+
85
+ function main() {
86
+ const cfg = readConfig();
87
+ if (process.argv.includes("--dry-run")) {
88
+ const selected = fetchAll(cfg);
89
+ const { dir } = materializedPaths(cfg.namespace);
90
+ console.log(`would write ${selected.size} secret(s) to ${dir}`);
91
+ console.log([...selected.keys()].sort().join("\n"));
92
+ return;
93
+ }
94
+ const { count, dir } = materialize(cfg);
95
+ console.log(`materialized ${count} secret(s) and their notes into ${dir}`);
96
+ }
97
+
98
+ if (import.meta.url === `file://${process.argv[1]}`) {
99
+ try {
100
+ main();
101
+ } catch (err) {
102
+ console.error(err.message);
103
+ process.exit(1);
104
+ }
105
+ }
@@ -0,0 +1,240 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Provider reads and boundary enforcement for `lisa-secrets-access`.
4
+ *
5
+ * Retrieval and selection are separated deliberately. `fetchRaw` is the only
6
+ * function that touches a provider; everything downstream operates on plain
7
+ * rows, which is what makes the boundary rules testable with synthetic data and
8
+ * without granting a test process access to any real secret.
9
+ * @module providers
10
+ */
11
+
12
+ import { execFileSync } from "node:child_process";
13
+
14
+ /**
15
+ * A provider key becomes a shell variable name on materializing surfaces, so
16
+ * only names valid in every POSIX-like shell are accepted. Anything else stays
17
+ * in the provider and is intentionally not exported.
18
+ */
19
+ export const ENV_KEY = /^[A-Za-z_][A-Za-z0-9_]*$/;
20
+
21
+ /**
22
+ * The provider entry holding rotation leases.
23
+ *
24
+ * Leases live in the provider because it is the only substrate every surface
25
+ * shares — a CI concurrency group and a laptop lockfile cannot see each other,
26
+ * but both can see this. It is coordination state rather than a credential, so
27
+ * it is excluded from every selection: nothing should resolve or materialize it.
28
+ */
29
+ export const LEASE_KEY = "LISA_ROTATION_LEASES";
30
+
31
+ /**
32
+ * Obtain the one credential that unlocks the provider.
33
+ *
34
+ * Walks `sources` in order, environment first, so a CI run where the pipeline
35
+ * injects the bootstrap never reaches for a local store. This is the only
36
+ * credential permitted in an OS keychain — it is a bootstrap, not a cached copy
37
+ * of anything downstream.
38
+ * @param {{sources: string[], key: string|null}} bootstrap Bootstrap config.
39
+ * @returns {string|null} The token, or null when the provider needs none.
40
+ */
41
+ export function bootstrapToken(bootstrap) {
42
+ if (!bootstrap.key) return null;
43
+ for (const source of bootstrap.sources) {
44
+ const found =
45
+ source === "env"
46
+ ? (process.env[bootstrap.key] ?? "").trim()
47
+ : source === "keychain"
48
+ ? fromKeychain(bootstrap.key)
49
+ : "";
50
+ if (found) return found;
51
+ }
52
+ throw new Error(
53
+ `${bootstrap.key} not found in: ${bootstrap.sources.join(", ")}.\n` +
54
+ `It is the bootstrap credential — without it no other secret can be read.`
55
+ );
56
+ }
57
+
58
+ /**
59
+ * Read one value from the macOS keychain, treating absence as empty.
60
+ * @param {string} key Keychain service name.
61
+ * @returns {string} The value, or an empty string when unavailable.
62
+ */
63
+ function fromKeychain(key) {
64
+ if (process.platform !== "darwin") return "";
65
+ try {
66
+ return execFileSync(
67
+ "security",
68
+ ["find-generic-password", "-s", key, "-a", process.env.USER ?? "", "-w"],
69
+ { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }
70
+ ).trim();
71
+ } catch {
72
+ return "";
73
+ }
74
+ }
75
+
76
+ /**
77
+ * Read every secret the provider grants, as unfiltered rows.
78
+ *
79
+ * Deliberately reads all rather than one: the provider already scopes what this
80
+ * caller may see, so the set it returns *is* the permitted set. Restating that
81
+ * as a list in config would duplicate a boundary the provider already enforces.
82
+ * @param {object} cfg Resolved configuration.
83
+ * @returns {Array<{key: string, value: string, note: string, projectId: string|null}>} Rows.
84
+ */
85
+ export function fetchRaw(cfg) {
86
+ const env = { ...process.env };
87
+ const token = bootstrapToken(cfg.bootstrap);
88
+ if (cfg.bootstrap.key && token) env[cfg.bootstrap.key] = token;
89
+
90
+ if (cfg.provider === "env") {
91
+ return Object.entries(process.env)
92
+ .filter(([k]) => /^[A-Z][A-Z0-9_]*$/.test(k))
93
+ .map(([key, value]) => ({
94
+ key,
95
+ value: value ?? "",
96
+ note: "",
97
+ projectId: null,
98
+ }));
99
+ }
100
+
101
+ if (cfg.provider === "bitwarden") {
102
+ const raw = run("bws", ["secret", "list", "--output", "json"], env);
103
+ return JSON.parse(raw || "[]").map(s => ({
104
+ key: s.key,
105
+ value: s.value,
106
+ note: s.note ?? "",
107
+ projectId: s.projectId ?? null,
108
+ id: s.id ?? null,
109
+ }));
110
+ }
111
+
112
+ if (cfg.provider === "doppler") {
113
+ const args = ["secrets", "download", "--no-file", "--format", "json"];
114
+ const raw = run("doppler", args, env);
115
+ return Object.entries(JSON.parse(raw || "{}")).map(([key, value]) => ({
116
+ key,
117
+ value: String(value),
118
+ note: "",
119
+ projectId: null,
120
+ }));
121
+ }
122
+
123
+ throw new Error(
124
+ `provider "${cfg.provider}" has no bulk read implemented yet.\n` +
125
+ `Add one in providers.mjs — see the dispatch table in SKILL.md.`
126
+ );
127
+ }
128
+
129
+ /**
130
+ * Run a provider CLI, keeping its output off any shared stream.
131
+ * @param {string} bin Executable name.
132
+ * @param {string[]} args Arguments.
133
+ * @param {NodeJS.ProcessEnv} env Environment for the child.
134
+ * @returns {string} Captured stdout.
135
+ */
136
+ function run(bin, args, env) {
137
+ return execFileSync(bin, args, {
138
+ encoding: "utf8",
139
+ env,
140
+ stdio: ["ignore", "pipe", "pipe"],
141
+ });
142
+ }
143
+
144
+ /**
145
+ * Apply the exposure boundary and reject an ambiguous set.
146
+ *
147
+ * The provider's own scoping is the default allowlist. The optional controls
148
+ * may only *narrow* it — there is intentionally no way to widen access from
149
+ * config, because that boundary belongs to the provider grant.
150
+ *
151
+ * A duplicate exact-name key is fatal rather than last-wins. Silently choosing
152
+ * one would make which credential gets used depend on provider response order,
153
+ * which is neither stable nor visible at the call site.
154
+ * @param {Array<object>} rows Raw provider rows.
155
+ * @param {{projectIds: string[], excludeKeys: string[]}} narrow Narrowing controls.
156
+ * @returns {Map<string, {value: string, note: string, id: string|null}>} Selected secrets by name.
157
+ */
158
+ export function normalizeRows(
159
+ rows,
160
+ narrow = { projectIds: [], excludeKeys: [] }
161
+ ) {
162
+ const projects = new Set(narrow.projectIds ?? []);
163
+ const excluded = new Set([...(narrow.excludeKeys ?? []), LEASE_KEY]);
164
+ const out = new Map();
165
+
166
+ for (const row of rows) {
167
+ if (projects.size && !projects.has(row.projectId)) continue;
168
+ if (typeof row.key !== "string" || !ENV_KEY.test(row.key)) continue;
169
+ if (excluded.has(row.key)) continue;
170
+ if (out.has(row.key)) {
171
+ throw new Error(
172
+ `duplicate secret key across visible projects: ${row.key}.\n` +
173
+ `Resolve it at the provider — choosing one here would make credential ` +
174
+ `use depend on response order.`
175
+ );
176
+ }
177
+ if (typeof row.value !== "string") {
178
+ throw new Error(`secret value is not a string: ${row.key}`);
179
+ }
180
+ out.set(row.key, {
181
+ value: row.value,
182
+ note: row.note ?? "",
183
+ id: row.id ?? null,
184
+ });
185
+ }
186
+ return out;
187
+ }
188
+
189
+ /**
190
+ * Read and select in one step — the normal entry point for consumers.
191
+ * @param {object} cfg Resolved configuration.
192
+ * @returns {Map<string, {value: string, note: string, id: string|null}>} Selected secrets.
193
+ */
194
+ export function fetchAll(cfg) {
195
+ return normalizeRows(fetchRaw(cfg), cfg.narrow);
196
+ }
197
+
198
+ /**
199
+ * Find one raw row by exact key, bypassing the exposure boundary.
200
+ *
201
+ * Only the rotation path uses this, and only to reach the lease record, which
202
+ * is deliberately excluded from every normal selection. Reading a *credential*
203
+ * must always go through {@link fetchAll} so the boundary applies.
204
+ * @param {object} cfg Resolved configuration.
205
+ * @param {string} key Exact key name.
206
+ * @returns {object|null} The raw row, or null when absent.
207
+ */
208
+ export function rawByKey(cfg, key) {
209
+ const matches = fetchRaw(cfg).filter(row => row.key === key);
210
+ if (matches.length > 1) throw new Error(`duplicate secret key: ${key}`);
211
+ return matches[0] ?? null;
212
+ }
213
+
214
+ /**
215
+ * Write a replacement value back to the provider.
216
+ *
217
+ * Passing a value as an argument is normally forbidden, because process
218
+ * arguments are visible to anyone who can run `ps` on the same host. The
219
+ * Bitwarden CLI exposes no stdin path for an edit, so this is a documented
220
+ * exception rather than an oversight: it is confined to the one operation that
221
+ * cannot be expressed otherwise, and `execFileSync` at least keeps the value
222
+ * out of a shell and therefore out of shell history.
223
+ * @param {object} cfg Resolved configuration.
224
+ * @param {string} id Provider-side identifier for the secret.
225
+ * @param {string} value Replacement value.
226
+ */
227
+ export function writeSecret(cfg, id, value) {
228
+ const env = { ...process.env };
229
+ const token = bootstrapToken(cfg.bootstrap);
230
+ if (cfg.bootstrap.key && token) env[cfg.bootstrap.key] = token;
231
+
232
+ if (cfg.provider === "bitwarden") {
233
+ run("bws", ["secret", "edit", id, "--value", value], env);
234
+ return;
235
+ }
236
+ throw new Error(
237
+ `provider "${cfg.provider}" has no write implemented.\n` +
238
+ `Rotation requires a proven write path; add one in providers.mjs.`
239
+ );
240
+ }