@codyswann/lisa 2.337.2 → 2.338.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 (96) hide show
  1. package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
  2. package/dist/core/upstream-evidence-manifest.js +16 -6
  3. package/dist/core/upstream-evidence-manifest.js.map +1 -1
  4. package/package.json +6 -3
  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-secrets-access/SKILL.md +16 -0
  8. package/plugins/lisa/.codex-plugin/skills/lisa-secrets-access/scripts/tools-from-notes.mjs +59 -0
  9. package/plugins/lisa/.codex-plugin/skills/lisa-setup-remote-env/SKILL.md +1 -1
  10. package/plugins/lisa/.codex-plugin/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +65 -4
  11. package/plugins/lisa/.codex-plugin/skills/lisa-setup-workstation/scripts/cli.mjs +19 -1
  12. package/plugins/lisa/.codex-plugin/skills/lisa-setup-workstation/scripts/workstation.mjs +11 -1
  13. package/plugins/lisa/skills/lisa-secrets-access/SKILL.md +16 -0
  14. package/plugins/lisa/skills/lisa-secrets-access/scripts/tools-from-notes.mjs +59 -0
  15. package/plugins/lisa/skills/lisa-setup-remote-env/SKILL.md +1 -1
  16. package/plugins/lisa/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +65 -4
  17. package/plugins/lisa/skills/lisa-setup-workstation/scripts/cli.mjs +19 -1
  18. package/plugins/lisa/skills/lisa-setup-workstation/scripts/workstation.mjs +11 -1
  19. package/plugins/lisa-agy/plugin.json +1 -1
  20. package/plugins/lisa-agy/skills/lisa-secrets-access/SKILL.md +16 -0
  21. package/plugins/lisa-agy/skills/lisa-secrets-access/scripts/tools-from-notes.mjs +59 -0
  22. package/plugins/lisa-agy/skills/lisa-setup-remote-env/SKILL.md +1 -1
  23. package/plugins/lisa-agy/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +65 -4
  24. package/plugins/lisa-agy/skills/lisa-setup-workstation/scripts/cli.mjs +19 -1
  25. package/plugins/lisa-agy/skills/lisa-setup-workstation/scripts/workstation.mjs +11 -1
  26. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  27. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  28. package/plugins/lisa-cdk-agy/plugin.json +1 -1
  29. package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
  30. package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
  31. package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
  32. package/plugins/lisa-copilot/skills/lisa-secrets-access/SKILL.md +16 -0
  33. package/plugins/lisa-copilot/skills/lisa-secrets-access/scripts/tools-from-notes.mjs +59 -0
  34. package/plugins/lisa-copilot/skills/lisa-setup-remote-env/SKILL.md +1 -1
  35. package/plugins/lisa-copilot/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +65 -4
  36. package/plugins/lisa-copilot/skills/lisa-setup-workstation/scripts/cli.mjs +19 -1
  37. package/plugins/lisa-copilot/skills/lisa-setup-workstation/scripts/workstation.mjs +11 -1
  38. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
  39. package/plugins/lisa-cursor/skills/lisa-secrets-access/SKILL.md +16 -0
  40. package/plugins/lisa-cursor/skills/lisa-secrets-access/scripts/tools-from-notes.mjs +59 -0
  41. package/plugins/lisa-cursor/skills/lisa-setup-remote-env/SKILL.md +1 -1
  42. package/plugins/lisa-cursor/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +65 -4
  43. package/plugins/lisa-cursor/skills/lisa-setup-workstation/scripts/cli.mjs +19 -1
  44. package/plugins/lisa-cursor/skills/lisa-setup-workstation/scripts/workstation.mjs +11 -1
  45. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  46. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  47. package/plugins/lisa-expo/.codex-plugin/skills/expo-cicd-workflows/scripts/package.json +1 -1
  48. package/plugins/lisa-expo/skills/expo-cicd-workflows/scripts/package.json +1 -1
  49. package/plugins/lisa-expo-agy/plugin.json +1 -1
  50. package/plugins/lisa-expo-agy/skills/expo-cicd-workflows/scripts/package.json +1 -1
  51. package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
  52. package/plugins/lisa-expo-copilot/skills/expo-cicd-workflows/scripts/package.json +1 -1
  53. package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
  54. package/plugins/lisa-expo-cursor/skills/expo-cicd-workflows/scripts/package.json +1 -1
  55. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  56. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  57. package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
  58. package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
  59. package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
  60. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  61. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  62. package/plugins/lisa-nestjs-agy/plugin.json +1 -1
  63. package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
  64. package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
  65. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  66. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  67. package/plugins/lisa-openclaw-agy/plugin.json +1 -1
  68. package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
  69. package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
  70. package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
  71. package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
  72. package/plugins/lisa-phaser-agy/plugin.json +1 -1
  73. package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
  74. package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
  75. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  76. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  77. package/plugins/lisa-rails-agy/plugin.json +1 -1
  78. package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
  79. package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
  80. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  81. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  82. package/plugins/lisa-typescript-agy/plugin.json +1 -1
  83. package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
  84. package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
  85. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  86. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  87. package/plugins/lisa-wiki-agy/plugin.json +1 -1
  88. package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
  89. package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
  90. package/plugins/src/base/skills/lisa-secrets-access/SKILL.md +16 -0
  91. package/plugins/src/base/skills/lisa-secrets-access/scripts/tools-from-notes.mjs +59 -0
  92. package/plugins/src/base/skills/lisa-setup-remote-env/SKILL.md +1 -1
  93. package/plugins/src/base/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +65 -4
  94. package/plugins/src/base/skills/lisa-setup-workstation/scripts/cli.mjs +19 -1
  95. package/plugins/src/base/skills/lisa-setup-workstation/scripts/workstation.mjs +11 -1
  96. package/plugins/src/expo/skills/expo-cicd-workflows/scripts/package.json +1 -1
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Read which CLIs a secret set implies, from the notes stored beside it.
3
+ *
4
+ * A session with no checkout has no `remoteEnv.tools` to consult, so the
5
+ * toolchain fell back to "install the whole catalogue" — which spends a
6
+ * setup-script time budget on CLIs the container may never use, and on a
7
+ * five-minute budget that is the difference between a session and no session.
8
+ *
9
+ * The vault already knows. A credential and the CLI that consumes it belong
10
+ * together: `SONARQUBE_CLI_TOKEN` is only useful with `sonar`, and a machine
11
+ * account that cannot see the token has no business installing the CLI. Using
12
+ * the notes makes the provider grant the single boundary for both, instead of a
13
+ * second list to keep in step.
14
+ *
15
+ * Notes are free text with a loose `key: value` convention already in use
16
+ * (`scope:`, `owner:`, `consumers:`), so this reads a `tool:` or `tools:` line
17
+ * in the same shape rather than inventing a format.
18
+ *
19
+ * **Names are matched against a catalogue, never executed.** A note is
20
+ * remote-influenced input: anyone who can edit a secret in the vault can edit
21
+ * its note. Selecting from a known set means the worst a hostile note can do is
22
+ * ask for a CLI Lisa already ships a pinned, checksummed entry for — it can
23
+ * never introduce a URL, a version, or a command.
24
+ * @module tools-from-notes
25
+ */
26
+
27
+ /** Matches `tool: name` or `tools: a, b`, case-insensitively, one per line. */
28
+ const TOOL_LINE = /^[ \t]*tools?[ \t]*:[ \t]*(.+)$/gim;
29
+
30
+ /**
31
+ * Collect the tool names a set of notes asks for.
32
+ *
33
+ * Unknown names are dropped rather than reported as an error: a note naming a
34
+ * CLI this version of Lisa does not ship is a request from the future, not a
35
+ * broken environment, and failing setup over it would be a poor trade.
36
+ * @param {Record<string, string>} notes Note text by secret name.
37
+ * @param {string[]} known Tool names Lisa can install.
38
+ * @returns {string[]} Known tool names, deduplicated, in catalogue order.
39
+ */
40
+ export function toolsFromNotes(notes, known) {
41
+ const allowed = new Set(known);
42
+ const wanted = new Set();
43
+
44
+ for (const note of Object.values(notes ?? {})) {
45
+ if (typeof note !== "string") continue;
46
+ // `matchAll` rather than `exec` in a loop: the regex is global, and a shared
47
+ // lastIndex across secrets would skip matches in every note but the first.
48
+ for (const [, list] of note.matchAll(TOOL_LINE)) {
49
+ for (const raw of list.split(",")) {
50
+ const name = raw.trim().toLowerCase();
51
+ if (allowed.has(name)) wanted.add(name);
52
+ }
53
+ }
54
+ }
55
+
56
+ // Catalogue order, not note order, so the plan reads the same however the
57
+ // vault happens to be arranged.
58
+ return known.filter(name => wanted.has(name));
59
+ }
@@ -13,7 +13,7 @@ Prepare a remote surface so a host project can execute there. Today that means *
13
13
  The remote environment's own configuration fields stay **one line into the repository**. Nothing else is pasted into a vendor UI.
14
14
 
15
15
  ```text
16
- setup: n=0; rc=0; seen=""; for f in scripts/lisa-remote-env/setup.sh */scripts/lisa-remote-env/setup.sh "$HOME"/scripts/lisa-remote-env/setup.sh "$HOME"/*/scripts/lisa-remote-env/setup.sh /workspace/scripts/lisa-remote-env/setup.sh /workspace/*/scripts/lisa-remote-env/setup.sh; do [ -f "$f" ] || continue; d=$(cd "$(dirname "$f")" && pwd -P); case " $seen " in *" $d "*) continue;; esac; seen="$seen $d"; n=$((n+1)); bash "$f" || rc=$?; done; [ "$n" -gt 0 ] && exit "$rc"; g=0; for c in ./.git ./*/.git "$HOME"/*/.git /workspace/*/.git; do [ -e "$c" ] && g=1; done; if [ "$g" -eq 1 ]; then echo "lisa-remote-env entrypoint not found, but a checkout is present. PWD=$PWD HOME=$HOME" >&2; ls -1 . "$HOME" /workspace 2>&1 | head -40 >&2; exit 1; fi; ten="${LISA_TENANT:-${LISA_SECRETS_NAMESPACE:-}}"; if [ -n "$ten" ]; then echo "No checkout; preparing tools and credentials for $ten."; tw=0; ts=0; npx -y @codyswann/lisa@2.337.1 workstation --install --agents=none --provider="${LISA_PROVIDER:-${LISA_SECRETS_PROVIDER:-bitwarden}}" || tw=$?; export PATH="$HOME/.local/bin:$PATH"; npx -y @codyswann/lisa@2.337.1 remote-env --phase=secrets || ts=$?; [ "$tw" -eq 0 ] || echo "SETUP INCOMPLETE: tool install failed (exit $tw). The session will start WITHOUT the pinned tools." >&2; [ "$ts" -eq 0 ] || echo "SETUP INCOMPLETE: secrets did not materialize (exit $ts). The session will start WITHOUT credentials." >&2; else echo "No checkout and no tenant configured; nothing to prepare."; fi; exit 0
16
+ setup: n=0; rc=0; seen=""; for f in scripts/lisa-remote-env/setup.sh */scripts/lisa-remote-env/setup.sh "$HOME"/scripts/lisa-remote-env/setup.sh "$HOME"/*/scripts/lisa-remote-env/setup.sh /workspace/scripts/lisa-remote-env/setup.sh /workspace/*/scripts/lisa-remote-env/setup.sh; do [ -f "$f" ] || continue; d=$(cd "$(dirname "$f")" && pwd -P); case " $seen " in *" $d "*) continue;; esac; seen="$seen $d"; n=$((n+1)); bash "$f" || rc=$?; done; [ "$n" -gt 0 ] && exit "$rc"; g=0; for c in ./.git ./*/.git "$HOME"/*/.git /workspace/*/.git; do [ -e "$c" ] && g=1; done; if [ "$g" -eq 1 ]; then echo "lisa-remote-env entrypoint not found, but a checkout is present. PWD=$PWD HOME=$HOME" >&2; ls -1 . "$HOME" /workspace 2>&1 | head -40 >&2; exit 1; fi; ten="${LISA_TENANT:-${LISA_SECRETS_NAMESPACE:-}}"; if [ -n "$ten" ]; then echo "No checkout; preparing tools and credentials for $ten."; tw=0; tt=0; ts=0; tp=0; npx -y @codyswann/lisa@2.337.3 workstation --install --agents=none --tools=none --provider="${LISA_PROVIDER:-${LISA_SECRETS_PROVIDER:-bitwarden}}" || tw=$?; export PATH="$HOME/.local/bin:$PATH"; npx -y @codyswann/lisa@2.337.3 remote-env --phase=secrets || ts=$?; w=$(npx -y @codyswann/lisa@2.337.3 remote-env --print-tools 2>/dev/null) || tp=$?; w=$(printf "%s\n" "$w" | tail -1); [ "$tp" -eq 0 ] || echo "SETUP INCOMPLETE: could not read the tool list from the vault (exit $tp). Installing the full catalogue." >&2; npx -y @codyswann/lisa@2.337.3 workstation --install --agents=none --tools="$w" --provider="${LISA_PROVIDER:-${LISA_SECRETS_PROVIDER:-bitwarden}}" || tt=$?; [ "$tw" -eq 0 ] && [ "$tt" -eq 0 ] || echo "SETUP INCOMPLETE: tool install failed (exit $tw/$tt). The session will start WITHOUT the pinned tools." >&2; [ "$ts" -eq 0 ] || echo "SETUP INCOMPLETE: secrets did not materialize (exit $ts). The session will start WITHOUT credentials." >&2; else echo "No checkout and no tenant configured; nothing to prepare."; fi; exit 0
17
17
  maintenance: n=0; rc=0; seen=""; for f in scripts/lisa-remote-env/setup.sh */scripts/lisa-remote-env/setup.sh "$HOME"/scripts/lisa-remote-env/setup.sh "$HOME"/*/scripts/lisa-remote-env/setup.sh /workspace/scripts/lisa-remote-env/setup.sh /workspace/*/scripts/lisa-remote-env/setup.sh; do [ -f "$f" ] || continue; d=$(cd "$(dirname "$f")" && pwd -P); case " $seen " in *" $d "*) continue;; esac; seen="$seen $d"; n=$((n+1)); bash "$f" || rc=$?; done; [ "$n" -gt 0 ] || { echo "lisa-remote-env entrypoint not found. PWD=$PWD HOME=$HOME" >&2; ls -1 . "$HOME" /workspace 2>&1 | head -40 >&2; exit 1; }; exit "$rc"
18
18
  ```
19
19
 
@@ -644,7 +644,7 @@ export const SETUP_FIELD =
644
644
  'ten="${LISA_TENANT:-${LISA_SECRETS_NAMESPACE:-}}"; ' +
645
645
  'if [ -n "$ten" ]; then ' +
646
646
  'echo "No checkout; preparing tools and credentials for $ten."; ' +
647
- "tw=0; ts=0; " +
647
+ "tw=0; tt=0; ts=0; tp=0; " +
648
648
  // `--agents=none` is load-bearing, not tidiness.
649
649
  //
650
650
  // The bootstrap's default is every coding agent it knows: claude, codex,
@@ -657,15 +657,39 @@ export const SETUP_FIELD =
657
657
  // A remote container needs the provider CLI and the tools; it does not need
658
658
  // agents, because it IS one. No AGENTS entry is named "none", so the filter
659
659
  // selects nothing — pinned by a test, since it reads like a magic word.
660
- `npx -y ${SELF_SPEC} workstation --install --agents=none ` +
660
+ // Two passes, credentials in between, because the vault is what knows which
661
+ // CLIs this container needs — and it cannot be asked until it has been read.
662
+ //
663
+ // The first pass installs only the provider CLI (`--tools=none`, the same
664
+ // idiom as `--agents=none`), because that is all materialization requires.
665
+ `npx -y ${SELF_SPEC} workstation --install --agents=none --tools=none ` +
661
666
  '--provider="${LISA_PROVIDER:-${LISA_SECRETS_PROVIDER:-bitwarden}}" || tw=$?; ' +
662
667
  // Exported BEFORE the secrets phase, not after: the toolchain installs into
663
668
  // ~/.local/bin, and materialization spawns the provider CLI by name. Ordered
664
669
  // the other way it gets ENOENT on a binary that is sitting right there.
665
670
  'export PATH="$HOME/.local/bin:$PATH"; ' +
666
671
  `npx -y ${SELF_SPEC} remote-env --phase=secrets || ts=$?; ` +
667
- '[ "$tw" -eq 0 ] || echo "SETUP INCOMPLETE: tool install failed ' +
668
- '(exit $tw). The session will start WITHOUT the pinned tools." >&2; ' +
672
+ // Now the notes can answer it. A vault that names nothing yields an empty
673
+ // string, and `--tools=` with an empty value selects the whole catalogue
674
+ // so annotating the notes NARROWS what gets installed, and not annotating
675
+ // them leaves this field behaving exactly as it did before.
676
+ //
677
+ // stderr is dropped and only the last line kept: npx narrates to stderr, and
678
+ // this is a command substitution whose output becomes a flag value.
679
+ //
680
+ // The status is captured BEFORE the pipe, because `tail` exits 0 whatever
681
+ // happened upstream. A failed lookup still falls through to the whole
682
+ // catalogue — installing too much is the safe direction, and it is what this
683
+ // field did before the vault had any say — but it says so, rather than being
684
+ // indistinguishable from a vault that simply names nothing.
685
+ `w=$(npx -y ${SELF_SPEC} remote-env --print-tools 2>/dev/null) || tp=$?; ` +
686
+ 'w=$(printf "%s\\n" "$w" | tail -1); ' +
687
+ '[ "$tp" -eq 0 ] || echo "SETUP INCOMPLETE: could not read the tool list ' +
688
+ 'from the vault (exit $tp). Installing the full catalogue." >&2; ' +
689
+ `npx -y ${SELF_SPEC} workstation --install --agents=none --tools="$w" ` +
690
+ '--provider="${LISA_PROVIDER:-${LISA_SECRETS_PROVIDER:-bitwarden}}" || tt=$?; ' +
691
+ '[ "$tw" -eq 0 ] && [ "$tt" -eq 0 ] || echo "SETUP INCOMPLETE: tool install ' +
692
+ 'failed (exit $tw/$tt). The session will start WITHOUT the pinned tools." >&2; ' +
669
693
  '[ "$ts" -eq 0 ] || echo "SETUP INCOMPLETE: secrets did not materialize ' +
670
694
  '(exit $ts). The session will start WITHOUT credentials." >&2; ' +
671
695
  'else echo "No checkout and no tenant configured; nothing to prepare."; fi; ' +
@@ -1023,6 +1047,43 @@ function readBootstrapKey(cwd = process.cwd()) {
1023
1047
  }
1024
1048
 
1025
1049
  async function main() {
1050
+ // `--print-tools` answers "which CLIs does this secret set imply", reading
1051
+ // the notes materialized alongside the values.
1052
+ //
1053
+ // It exists so a repo-less setup can install what the container actually
1054
+ // needs. Without a checkout there is no `remoteEnv.tools`, and the fallback
1055
+ // was the whole catalogue — which spends a five-minute setup budget on CLIs
1056
+ // that may never be used, and a blown budget is a session that never starts.
1057
+ //
1058
+ // Printing rather than installing keeps the two skills decoupled: the secrets
1059
+ // side knows what the vault says, the workstation side knows how to install,
1060
+ // and neither grows a dependency on the other.
1061
+ if (process.argv.includes("--print-tools")) {
1062
+ const { readConfig, materializedPaths } = await import(
1063
+ pathToFileURL(siblingScript("lisa-secrets-access", "surfaces.mjs")).href
1064
+ );
1065
+ const { toolsFromNotes } = await import(
1066
+ pathToFileURL(
1067
+ siblingScript("lisa-secrets-access", "tools-from-notes.mjs")
1068
+ ).href
1069
+ );
1070
+ const { TOOLS } = await import(
1071
+ pathToFileURL(siblingScript("lisa-setup-workstation", "catalogue.mjs"))
1072
+ .href
1073
+ );
1074
+
1075
+ const { notesFile } = materializedPaths(readConfig().namespace);
1076
+ // Silence rather than failure when nothing has been materialized: the
1077
+ // caller is a shell substitution in a setup script, and an error there
1078
+ // would abort provisioning over a question that simply has no answer yet.
1079
+ if (!existsSync(notesFile)) return;
1080
+
1081
+ const notes = JSON.parse(readFileSync(notesFile, "utf8")).secrets ?? {};
1082
+ const known = TOOLS.filter(t => t.kind !== "required").map(t => t.name);
1083
+ console.log(toolsFromNotes(notes, known).join(","));
1084
+ return;
1085
+ }
1086
+
1026
1087
  const dryRun = process.argv.includes("--dry-run");
1027
1088
  const emit = process.argv
1028
1089
  .find(arg => arg.startsWith("--emit="))
@@ -10,6 +10,9 @@
10
10
  * cli.mjs report present vs missing
11
11
  * cli.mjs --install provision what is missing
12
12
  * cli.mjs --install --agents=claude,codex only these agents
13
+ * cli.mjs --install --tools=aws,sonar only these OPTIONAL tools; required
14
+ * ones are always kept, and an empty
15
+ * value selects the whole catalogue
13
16
  * cli.mjs --provider=bitwarden credential manager (asked if a TTY)
14
17
  * cli.mjs --json machine-readable plan
15
18
  * cli.mjs --print-dockerfile an image that runs this same script
@@ -177,6 +180,21 @@ export async function run(argv, io = {}) {
177
180
  .filter(Boolean)
178
181
  : null;
179
182
 
183
+ // `--tools=` with nothing after it selects everything, deliberately.
184
+ //
185
+ // Its caller is a shell substitution — a repo-less setup script asking the
186
+ // vault which CLIs its secrets imply — and a vault that names none must leave
187
+ // the container exactly as it was before this flag existed. Reading an empty
188
+ // value as "install nothing" would make adopting the convention a silent
189
+ // regression for every environment that has not annotated its notes yet.
190
+ const requested = value("tools");
191
+ const tools = requested
192
+ ? requested
193
+ .split(",")
194
+ .map(s => s.trim())
195
+ .filter(Boolean)
196
+ : null;
197
+
180
198
  const provider = chooseProvider(value("provider"), io);
181
199
 
182
200
  if (flag("print-dockerfile")) {
@@ -186,7 +204,7 @@ export async function run(argv, io = {}) {
186
204
 
187
205
  let plan;
188
206
  try {
189
- plan = planWorkstation({ agents, provider, ...(io.probes ?? {}) });
207
+ plan = planWorkstation({ agents, tools, provider, ...(io.probes ?? {}) });
190
208
  } catch (error) {
191
209
  // A misspelled provider stops here rather than silently provisioning the
192
210
  // wrong credential manager.
@@ -168,7 +168,17 @@ export function planWorkstation(options = {}) {
168
168
  group: "agent",
169
169
  })
170
170
  );
171
- const tools = TOOLS.map(t => ({ ...planEntry(t, options), group: "tool" }));
171
+ // Tools are selectable for the same reason agents are, and it matters more
172
+ // here: a remote container has a setup-script time budget, and installing a
173
+ // CLI nothing in that container uses spends it for nothing.
174
+ //
175
+ // A `required` tool is never filtered out. Those are not installs — they are
176
+ // assertions about the machine (git, node), and hiding them would turn a
177
+ // missing prerequisite into a silent one.
178
+ const wantedTools = options.tools ?? null;
179
+ const tools = TOOLS.filter(
180
+ t => t.kind === "required" || !wantedTools || wantedTools.includes(t.name)
181
+ ).map(t => ({ ...planEntry(t, options), group: "tool" }));
172
182
 
173
183
  // Only the SELECTED provider is planned. Installing every credential CLI
174
184
  // would leave four unused ones on the machine, each needing patching for no
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-cdk",
3
- "version": "2.337.2",
3
+ "version": "2.338.0",
4
4
  "description": "AWS CDK-specific plugin",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-cdk",
3
- "version": "2.337.2",
3
+ "version": "2.338.0",
4
4
  "description": "AWS CDK-specific Lisa plugin.",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-cdk",
3
- "version": "2.337.2",
3
+ "version": "2.338.0",
4
4
  "description": "AWS CDK-specific plugin",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-cdk",
3
- "version": "2.337.2",
3
+ "version": "2.338.0",
4
4
  "description": "AWS CDK-specific plugin",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-cdk",
3
- "version": "2.337.2",
3
+ "version": "2.338.0",
4
4
  "description": "AWS CDK-specific plugin",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa",
3
- "version": "2.337.2",
3
+ "version": "2.338.0",
4
4
  "description": "Universal governance — agents, skills, commands, hooks, and rules for all projects",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -129,6 +129,22 @@ ci: yes - injected by <mechanism>
129
129
  docs: <path>
130
130
  ```
131
131
 
132
+ ### `tool:` — the CLI a secret implies
133
+
134
+ One key is read by more than a human. A credential and the CLI that consumes it belong together — `SONARQUBE_CLI_TOKEN` is only useful with `sonar` — so a `tool:` (or `tools: a, b`) line declares that pairing where it cannot drift from the secret:
135
+
136
+ ```text
137
+ SonarCloud token, in the variable name the SonarQube CLI reads.
138
+ owner: <name>
139
+ tool: sonar
140
+ ```
141
+
142
+ `lisa remote-env --print-tools` reads these from the materialized notes and prints the names, which is how a **repo-less** session decides what to install: with no checkout there is no `remoteEnv.tools`, and installing the whole catalogue can exhaust a cloud surface's setup-script time budget — a blown budget is a session that never starts. The machine account's grant then scopes the credentials and the CLIs together, instead of leaving a second list to maintain.
143
+
144
+ Annotating **narrows**; annotating nothing changes nothing. A vault where no note carries the key yields an empty list, and an empty list means the full catalogue.
145
+
146
+ Names are matched against Lisa's catalogue and **never executed**. A note is remote-influenced input — anyone who can edit a secret can edit its note — so the worst a hostile one can ask for is a CLI Lisa already ships a pinned, checksummed entry for. A name Lisa cannot install is ignored rather than treated as an error: it is a request from a future version, not a broken environment.
147
+
132
148
  `describe` returns this. When a note is empty, **infer purpose from the name, mark it inferred, and report the gap** — infer *and* warn, never instead of. A silent fallback that works well enough guarantees the notes stay empty forever.
133
149
 
134
150
  **An inferred mapping must never authorise a write.** It orients a reader; it does not pick which credential calls a production API. `ATTIO_API_KEY` versus `ATTIO_API_KEY_STAGING` is exactly the guess that silently writes to the wrong system.
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Read which CLIs a secret set implies, from the notes stored beside it.
3
+ *
4
+ * A session with no checkout has no `remoteEnv.tools` to consult, so the
5
+ * toolchain fell back to "install the whole catalogue" — which spends a
6
+ * setup-script time budget on CLIs the container may never use, and on a
7
+ * five-minute budget that is the difference between a session and no session.
8
+ *
9
+ * The vault already knows. A credential and the CLI that consumes it belong
10
+ * together: `SONARQUBE_CLI_TOKEN` is only useful with `sonar`, and a machine
11
+ * account that cannot see the token has no business installing the CLI. Using
12
+ * the notes makes the provider grant the single boundary for both, instead of a
13
+ * second list to keep in step.
14
+ *
15
+ * Notes are free text with a loose `key: value` convention already in use
16
+ * (`scope:`, `owner:`, `consumers:`), so this reads a `tool:` or `tools:` line
17
+ * in the same shape rather than inventing a format.
18
+ *
19
+ * **Names are matched against a catalogue, never executed.** A note is
20
+ * remote-influenced input: anyone who can edit a secret in the vault can edit
21
+ * its note. Selecting from a known set means the worst a hostile note can do is
22
+ * ask for a CLI Lisa already ships a pinned, checksummed entry for — it can
23
+ * never introduce a URL, a version, or a command.
24
+ * @module tools-from-notes
25
+ */
26
+
27
+ /** Matches `tool: name` or `tools: a, b`, case-insensitively, one per line. */
28
+ const TOOL_LINE = /^[ \t]*tools?[ \t]*:[ \t]*(.+)$/gim;
29
+
30
+ /**
31
+ * Collect the tool names a set of notes asks for.
32
+ *
33
+ * Unknown names are dropped rather than reported as an error: a note naming a
34
+ * CLI this version of Lisa does not ship is a request from the future, not a
35
+ * broken environment, and failing setup over it would be a poor trade.
36
+ * @param {Record<string, string>} notes Note text by secret name.
37
+ * @param {string[]} known Tool names Lisa can install.
38
+ * @returns {string[]} Known tool names, deduplicated, in catalogue order.
39
+ */
40
+ export function toolsFromNotes(notes, known) {
41
+ const allowed = new Set(known);
42
+ const wanted = new Set();
43
+
44
+ for (const note of Object.values(notes ?? {})) {
45
+ if (typeof note !== "string") continue;
46
+ // `matchAll` rather than `exec` in a loop: the regex is global, and a shared
47
+ // lastIndex across secrets would skip matches in every note but the first.
48
+ for (const [, list] of note.matchAll(TOOL_LINE)) {
49
+ for (const raw of list.split(",")) {
50
+ const name = raw.trim().toLowerCase();
51
+ if (allowed.has(name)) wanted.add(name);
52
+ }
53
+ }
54
+ }
55
+
56
+ // Catalogue order, not note order, so the plan reads the same however the
57
+ // vault happens to be arranged.
58
+ return known.filter(name => wanted.has(name));
59
+ }
@@ -13,7 +13,7 @@ Prepare a remote surface so a host project can execute there. Today that means *
13
13
  The remote environment's own configuration fields stay **one line into the repository**. Nothing else is pasted into a vendor UI.
14
14
 
15
15
  ```text
16
- setup: n=0; rc=0; seen=""; for f in scripts/lisa-remote-env/setup.sh */scripts/lisa-remote-env/setup.sh "$HOME"/scripts/lisa-remote-env/setup.sh "$HOME"/*/scripts/lisa-remote-env/setup.sh /workspace/scripts/lisa-remote-env/setup.sh /workspace/*/scripts/lisa-remote-env/setup.sh; do [ -f "$f" ] || continue; d=$(cd "$(dirname "$f")" && pwd -P); case " $seen " in *" $d "*) continue;; esac; seen="$seen $d"; n=$((n+1)); bash "$f" || rc=$?; done; [ "$n" -gt 0 ] && exit "$rc"; g=0; for c in ./.git ./*/.git "$HOME"/*/.git /workspace/*/.git; do [ -e "$c" ] && g=1; done; if [ "$g" -eq 1 ]; then echo "lisa-remote-env entrypoint not found, but a checkout is present. PWD=$PWD HOME=$HOME" >&2; ls -1 . "$HOME" /workspace 2>&1 | head -40 >&2; exit 1; fi; ten="${LISA_TENANT:-${LISA_SECRETS_NAMESPACE:-}}"; if [ -n "$ten" ]; then echo "No checkout; preparing tools and credentials for $ten."; tw=0; ts=0; npx -y @codyswann/lisa@2.337.1 workstation --install --agents=none --provider="${LISA_PROVIDER:-${LISA_SECRETS_PROVIDER:-bitwarden}}" || tw=$?; export PATH="$HOME/.local/bin:$PATH"; npx -y @codyswann/lisa@2.337.1 remote-env --phase=secrets || ts=$?; [ "$tw" -eq 0 ] || echo "SETUP INCOMPLETE: tool install failed (exit $tw). The session will start WITHOUT the pinned tools." >&2; [ "$ts" -eq 0 ] || echo "SETUP INCOMPLETE: secrets did not materialize (exit $ts). The session will start WITHOUT credentials." >&2; else echo "No checkout and no tenant configured; nothing to prepare."; fi; exit 0
16
+ setup: n=0; rc=0; seen=""; for f in scripts/lisa-remote-env/setup.sh */scripts/lisa-remote-env/setup.sh "$HOME"/scripts/lisa-remote-env/setup.sh "$HOME"/*/scripts/lisa-remote-env/setup.sh /workspace/scripts/lisa-remote-env/setup.sh /workspace/*/scripts/lisa-remote-env/setup.sh; do [ -f "$f" ] || continue; d=$(cd "$(dirname "$f")" && pwd -P); case " $seen " in *" $d "*) continue;; esac; seen="$seen $d"; n=$((n+1)); bash "$f" || rc=$?; done; [ "$n" -gt 0 ] && exit "$rc"; g=0; for c in ./.git ./*/.git "$HOME"/*/.git /workspace/*/.git; do [ -e "$c" ] && g=1; done; if [ "$g" -eq 1 ]; then echo "lisa-remote-env entrypoint not found, but a checkout is present. PWD=$PWD HOME=$HOME" >&2; ls -1 . "$HOME" /workspace 2>&1 | head -40 >&2; exit 1; fi; ten="${LISA_TENANT:-${LISA_SECRETS_NAMESPACE:-}}"; if [ -n "$ten" ]; then echo "No checkout; preparing tools and credentials for $ten."; tw=0; tt=0; ts=0; tp=0; npx -y @codyswann/lisa@2.337.3 workstation --install --agents=none --tools=none --provider="${LISA_PROVIDER:-${LISA_SECRETS_PROVIDER:-bitwarden}}" || tw=$?; export PATH="$HOME/.local/bin:$PATH"; npx -y @codyswann/lisa@2.337.3 remote-env --phase=secrets || ts=$?; w=$(npx -y @codyswann/lisa@2.337.3 remote-env --print-tools 2>/dev/null) || tp=$?; w=$(printf "%s\n" "$w" | tail -1); [ "$tp" -eq 0 ] || echo "SETUP INCOMPLETE: could not read the tool list from the vault (exit $tp). Installing the full catalogue." >&2; npx -y @codyswann/lisa@2.337.3 workstation --install --agents=none --tools="$w" --provider="${LISA_PROVIDER:-${LISA_SECRETS_PROVIDER:-bitwarden}}" || tt=$?; [ "$tw" -eq 0 ] && [ "$tt" -eq 0 ] || echo "SETUP INCOMPLETE: tool install failed (exit $tw/$tt). The session will start WITHOUT the pinned tools." >&2; [ "$ts" -eq 0 ] || echo "SETUP INCOMPLETE: secrets did not materialize (exit $ts). The session will start WITHOUT credentials." >&2; else echo "No checkout and no tenant configured; nothing to prepare."; fi; exit 0
17
17
  maintenance: n=0; rc=0; seen=""; for f in scripts/lisa-remote-env/setup.sh */scripts/lisa-remote-env/setup.sh "$HOME"/scripts/lisa-remote-env/setup.sh "$HOME"/*/scripts/lisa-remote-env/setup.sh /workspace/scripts/lisa-remote-env/setup.sh /workspace/*/scripts/lisa-remote-env/setup.sh; do [ -f "$f" ] || continue; d=$(cd "$(dirname "$f")" && pwd -P); case " $seen " in *" $d "*) continue;; esac; seen="$seen $d"; n=$((n+1)); bash "$f" || rc=$?; done; [ "$n" -gt 0 ] || { echo "lisa-remote-env entrypoint not found. PWD=$PWD HOME=$HOME" >&2; ls -1 . "$HOME" /workspace 2>&1 | head -40 >&2; exit 1; }; exit "$rc"
18
18
  ```
19
19
 
@@ -644,7 +644,7 @@ export const SETUP_FIELD =
644
644
  'ten="${LISA_TENANT:-${LISA_SECRETS_NAMESPACE:-}}"; ' +
645
645
  'if [ -n "$ten" ]; then ' +
646
646
  'echo "No checkout; preparing tools and credentials for $ten."; ' +
647
- "tw=0; ts=0; " +
647
+ "tw=0; tt=0; ts=0; tp=0; " +
648
648
  // `--agents=none` is load-bearing, not tidiness.
649
649
  //
650
650
  // The bootstrap's default is every coding agent it knows: claude, codex,
@@ -657,15 +657,39 @@ export const SETUP_FIELD =
657
657
  // A remote container needs the provider CLI and the tools; it does not need
658
658
  // agents, because it IS one. No AGENTS entry is named "none", so the filter
659
659
  // selects nothing — pinned by a test, since it reads like a magic word.
660
- `npx -y ${SELF_SPEC} workstation --install --agents=none ` +
660
+ // Two passes, credentials in between, because the vault is what knows which
661
+ // CLIs this container needs — and it cannot be asked until it has been read.
662
+ //
663
+ // The first pass installs only the provider CLI (`--tools=none`, the same
664
+ // idiom as `--agents=none`), because that is all materialization requires.
665
+ `npx -y ${SELF_SPEC} workstation --install --agents=none --tools=none ` +
661
666
  '--provider="${LISA_PROVIDER:-${LISA_SECRETS_PROVIDER:-bitwarden}}" || tw=$?; ' +
662
667
  // Exported BEFORE the secrets phase, not after: the toolchain installs into
663
668
  // ~/.local/bin, and materialization spawns the provider CLI by name. Ordered
664
669
  // the other way it gets ENOENT on a binary that is sitting right there.
665
670
  'export PATH="$HOME/.local/bin:$PATH"; ' +
666
671
  `npx -y ${SELF_SPEC} remote-env --phase=secrets || ts=$?; ` +
667
- '[ "$tw" -eq 0 ] || echo "SETUP INCOMPLETE: tool install failed ' +
668
- '(exit $tw). The session will start WITHOUT the pinned tools." >&2; ' +
672
+ // Now the notes can answer it. A vault that names nothing yields an empty
673
+ // string, and `--tools=` with an empty value selects the whole catalogue
674
+ // so annotating the notes NARROWS what gets installed, and not annotating
675
+ // them leaves this field behaving exactly as it did before.
676
+ //
677
+ // stderr is dropped and only the last line kept: npx narrates to stderr, and
678
+ // this is a command substitution whose output becomes a flag value.
679
+ //
680
+ // The status is captured BEFORE the pipe, because `tail` exits 0 whatever
681
+ // happened upstream. A failed lookup still falls through to the whole
682
+ // catalogue — installing too much is the safe direction, and it is what this
683
+ // field did before the vault had any say — but it says so, rather than being
684
+ // indistinguishable from a vault that simply names nothing.
685
+ `w=$(npx -y ${SELF_SPEC} remote-env --print-tools 2>/dev/null) || tp=$?; ` +
686
+ 'w=$(printf "%s\\n" "$w" | tail -1); ' +
687
+ '[ "$tp" -eq 0 ] || echo "SETUP INCOMPLETE: could not read the tool list ' +
688
+ 'from the vault (exit $tp). Installing the full catalogue." >&2; ' +
689
+ `npx -y ${SELF_SPEC} workstation --install --agents=none --tools="$w" ` +
690
+ '--provider="${LISA_PROVIDER:-${LISA_SECRETS_PROVIDER:-bitwarden}}" || tt=$?; ' +
691
+ '[ "$tw" -eq 0 ] && [ "$tt" -eq 0 ] || echo "SETUP INCOMPLETE: tool install ' +
692
+ 'failed (exit $tw/$tt). The session will start WITHOUT the pinned tools." >&2; ' +
669
693
  '[ "$ts" -eq 0 ] || echo "SETUP INCOMPLETE: secrets did not materialize ' +
670
694
  '(exit $ts). The session will start WITHOUT credentials." >&2; ' +
671
695
  'else echo "No checkout and no tenant configured; nothing to prepare."; fi; ' +
@@ -1023,6 +1047,43 @@ function readBootstrapKey(cwd = process.cwd()) {
1023
1047
  }
1024
1048
 
1025
1049
  async function main() {
1050
+ // `--print-tools` answers "which CLIs does this secret set imply", reading
1051
+ // the notes materialized alongside the values.
1052
+ //
1053
+ // It exists so a repo-less setup can install what the container actually
1054
+ // needs. Without a checkout there is no `remoteEnv.tools`, and the fallback
1055
+ // was the whole catalogue — which spends a five-minute setup budget on CLIs
1056
+ // that may never be used, and a blown budget is a session that never starts.
1057
+ //
1058
+ // Printing rather than installing keeps the two skills decoupled: the secrets
1059
+ // side knows what the vault says, the workstation side knows how to install,
1060
+ // and neither grows a dependency on the other.
1061
+ if (process.argv.includes("--print-tools")) {
1062
+ const { readConfig, materializedPaths } = await import(
1063
+ pathToFileURL(siblingScript("lisa-secrets-access", "surfaces.mjs")).href
1064
+ );
1065
+ const { toolsFromNotes } = await import(
1066
+ pathToFileURL(
1067
+ siblingScript("lisa-secrets-access", "tools-from-notes.mjs")
1068
+ ).href
1069
+ );
1070
+ const { TOOLS } = await import(
1071
+ pathToFileURL(siblingScript("lisa-setup-workstation", "catalogue.mjs"))
1072
+ .href
1073
+ );
1074
+
1075
+ const { notesFile } = materializedPaths(readConfig().namespace);
1076
+ // Silence rather than failure when nothing has been materialized: the
1077
+ // caller is a shell substitution in a setup script, and an error there
1078
+ // would abort provisioning over a question that simply has no answer yet.
1079
+ if (!existsSync(notesFile)) return;
1080
+
1081
+ const notes = JSON.parse(readFileSync(notesFile, "utf8")).secrets ?? {};
1082
+ const known = TOOLS.filter(t => t.kind !== "required").map(t => t.name);
1083
+ console.log(toolsFromNotes(notes, known).join(","));
1084
+ return;
1085
+ }
1086
+
1026
1087
  const dryRun = process.argv.includes("--dry-run");
1027
1088
  const emit = process.argv
1028
1089
  .find(arg => arg.startsWith("--emit="))
@@ -10,6 +10,9 @@
10
10
  * cli.mjs report present vs missing
11
11
  * cli.mjs --install provision what is missing
12
12
  * cli.mjs --install --agents=claude,codex only these agents
13
+ * cli.mjs --install --tools=aws,sonar only these OPTIONAL tools; required
14
+ * ones are always kept, and an empty
15
+ * value selects the whole catalogue
13
16
  * cli.mjs --provider=bitwarden credential manager (asked if a TTY)
14
17
  * cli.mjs --json machine-readable plan
15
18
  * cli.mjs --print-dockerfile an image that runs this same script
@@ -177,6 +180,21 @@ export async function run(argv, io = {}) {
177
180
  .filter(Boolean)
178
181
  : null;
179
182
 
183
+ // `--tools=` with nothing after it selects everything, deliberately.
184
+ //
185
+ // Its caller is a shell substitution — a repo-less setup script asking the
186
+ // vault which CLIs its secrets imply — and a vault that names none must leave
187
+ // the container exactly as it was before this flag existed. Reading an empty
188
+ // value as "install nothing" would make adopting the convention a silent
189
+ // regression for every environment that has not annotated its notes yet.
190
+ const requested = value("tools");
191
+ const tools = requested
192
+ ? requested
193
+ .split(",")
194
+ .map(s => s.trim())
195
+ .filter(Boolean)
196
+ : null;
197
+
180
198
  const provider = chooseProvider(value("provider"), io);
181
199
 
182
200
  if (flag("print-dockerfile")) {
@@ -186,7 +204,7 @@ export async function run(argv, io = {}) {
186
204
 
187
205
  let plan;
188
206
  try {
189
- plan = planWorkstation({ agents, provider, ...(io.probes ?? {}) });
207
+ plan = planWorkstation({ agents, tools, provider, ...(io.probes ?? {}) });
190
208
  } catch (error) {
191
209
  // A misspelled provider stops here rather than silently provisioning the
192
210
  // wrong credential manager.
@@ -168,7 +168,17 @@ export function planWorkstation(options = {}) {
168
168
  group: "agent",
169
169
  })
170
170
  );
171
- const tools = TOOLS.map(t => ({ ...planEntry(t, options), group: "tool" }));
171
+ // Tools are selectable for the same reason agents are, and it matters more
172
+ // here: a remote container has a setup-script time budget, and installing a
173
+ // CLI nothing in that container uses spends it for nothing.
174
+ //
175
+ // A `required` tool is never filtered out. Those are not installs — they are
176
+ // assertions about the machine (git, node), and hiding them would turn a
177
+ // missing prerequisite into a silent one.
178
+ const wantedTools = options.tools ?? null;
179
+ const tools = TOOLS.filter(
180
+ t => t.kind === "required" || !wantedTools || wantedTools.includes(t.name)
181
+ ).map(t => ({ ...planEntry(t, options), group: "tool" }));
172
182
 
173
183
  // Only the SELECTED provider is planned. Installing every credential CLI
174
184
  // would leave four unused ones on the machine, each needing patching for no
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa",
3
- "version": "2.337.2",
3
+ "version": "2.338.0",
4
4
  "description": "Universal governance — agents, skills, commands, hooks, and rules for all projects",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -129,6 +129,22 @@ ci: yes - injected by <mechanism>
129
129
  docs: <path>
130
130
  ```
131
131
 
132
+ ### `tool:` — the CLI a secret implies
133
+
134
+ One key is read by more than a human. A credential and the CLI that consumes it belong together — `SONARQUBE_CLI_TOKEN` is only useful with `sonar` — so a `tool:` (or `tools: a, b`) line declares that pairing where it cannot drift from the secret:
135
+
136
+ ```text
137
+ SonarCloud token, in the variable name the SonarQube CLI reads.
138
+ owner: <name>
139
+ tool: sonar
140
+ ```
141
+
142
+ `lisa remote-env --print-tools` reads these from the materialized notes and prints the names, which is how a **repo-less** session decides what to install: with no checkout there is no `remoteEnv.tools`, and installing the whole catalogue can exhaust a cloud surface's setup-script time budget — a blown budget is a session that never starts. The machine account's grant then scopes the credentials and the CLIs together, instead of leaving a second list to maintain.
143
+
144
+ Annotating **narrows**; annotating nothing changes nothing. A vault where no note carries the key yields an empty list, and an empty list means the full catalogue.
145
+
146
+ Names are matched against Lisa's catalogue and **never executed**. A note is remote-influenced input — anyone who can edit a secret can edit its note — so the worst a hostile one can ask for is a CLI Lisa already ships a pinned, checksummed entry for. A name Lisa cannot install is ignored rather than treated as an error: it is a request from a future version, not a broken environment.
147
+
132
148
  `describe` returns this. When a note is empty, **infer purpose from the name, mark it inferred, and report the gap** — infer *and* warn, never instead of. A silent fallback that works well enough guarantees the notes stay empty forever.
133
149
 
134
150
  **An inferred mapping must never authorise a write.** It orients a reader; it does not pick which credential calls a production API. `ATTIO_API_KEY` versus `ATTIO_API_KEY_STAGING` is exactly the guess that silently writes to the wrong system.