@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.
- package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
- package/dist/core/upstream-evidence-manifest.js +16 -6
- package/dist/core/upstream-evidence-manifest.js.map +1 -1
- package/package.json +6 -3
- package/plugins/lisa/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa/.codex-plugin/skills/lisa-secrets-access/SKILL.md +16 -0
- package/plugins/lisa/.codex-plugin/skills/lisa-secrets-access/scripts/tools-from-notes.mjs +59 -0
- package/plugins/lisa/.codex-plugin/skills/lisa-setup-remote-env/SKILL.md +1 -1
- package/plugins/lisa/.codex-plugin/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +65 -4
- package/plugins/lisa/.codex-plugin/skills/lisa-setup-workstation/scripts/cli.mjs +19 -1
- package/plugins/lisa/.codex-plugin/skills/lisa-setup-workstation/scripts/workstation.mjs +11 -1
- package/plugins/lisa/skills/lisa-secrets-access/SKILL.md +16 -0
- package/plugins/lisa/skills/lisa-secrets-access/scripts/tools-from-notes.mjs +59 -0
- package/plugins/lisa/skills/lisa-setup-remote-env/SKILL.md +1 -1
- package/plugins/lisa/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +65 -4
- package/plugins/lisa/skills/lisa-setup-workstation/scripts/cli.mjs +19 -1
- package/plugins/lisa/skills/lisa-setup-workstation/scripts/workstation.mjs +11 -1
- package/plugins/lisa-agy/plugin.json +1 -1
- package/plugins/lisa-agy/skills/lisa-secrets-access/SKILL.md +16 -0
- package/plugins/lisa-agy/skills/lisa-secrets-access/scripts/tools-from-notes.mjs +59 -0
- package/plugins/lisa-agy/skills/lisa-setup-remote-env/SKILL.md +1 -1
- package/plugins/lisa-agy/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +65 -4
- package/plugins/lisa-agy/skills/lisa-setup-workstation/scripts/cli.mjs +19 -1
- package/plugins/lisa-agy/skills/lisa-setup-workstation/scripts/workstation.mjs +11 -1
- package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk-agy/plugin.json +1 -1
- package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-copilot/skills/lisa-secrets-access/SKILL.md +16 -0
- package/plugins/lisa-copilot/skills/lisa-secrets-access/scripts/tools-from-notes.mjs +59 -0
- package/plugins/lisa-copilot/skills/lisa-setup-remote-env/SKILL.md +1 -1
- package/plugins/lisa-copilot/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +65 -4
- package/plugins/lisa-copilot/skills/lisa-setup-workstation/scripts/cli.mjs +19 -1
- package/plugins/lisa-copilot/skills/lisa-setup-workstation/scripts/workstation.mjs +11 -1
- package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cursor/skills/lisa-secrets-access/SKILL.md +16 -0
- package/plugins/lisa-cursor/skills/lisa-secrets-access/scripts/tools-from-notes.mjs +59 -0
- package/plugins/lisa-cursor/skills/lisa-setup-remote-env/SKILL.md +1 -1
- package/plugins/lisa-cursor/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +65 -4
- package/plugins/lisa-cursor/skills/lisa-setup-workstation/scripts/cli.mjs +19 -1
- package/plugins/lisa-cursor/skills/lisa-setup-workstation/scripts/workstation.mjs +11 -1
- package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-expo/.codex-plugin/skills/expo-cicd-workflows/scripts/package.json +1 -1
- package/plugins/lisa-expo/skills/expo-cicd-workflows/scripts/package.json +1 -1
- package/plugins/lisa-expo-agy/plugin.json +1 -1
- package/plugins/lisa-expo-agy/skills/expo-cicd-workflows/scripts/package.json +1 -1
- package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo-copilot/skills/expo-cicd-workflows/scripts/package.json +1 -1
- package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo-cursor/skills/expo-cicd-workflows/scripts/package.json +1 -1
- package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs-agy/plugin.json +1 -1
- package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw-agy/plugin.json +1 -1
- package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser-agy/plugin.json +1 -1
- package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-rails-agy/plugin.json +1 -1
- package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript-agy/plugin.json +1 -1
- package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki-agy/plugin.json +1 -1
- package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/src/base/skills/lisa-secrets-access/SKILL.md +16 -0
- package/plugins/src/base/skills/lisa-secrets-access/scripts/tools-from-notes.mjs +59 -0
- package/plugins/src/base/skills/lisa-setup-remote-env/SKILL.md +1 -1
- package/plugins/src/base/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +65 -4
- package/plugins/src/base/skills/lisa-setup-workstation/scripts/cli.mjs +19 -1
- package/plugins/src/base/skills/lisa-setup-workstation/scripts/workstation.mjs +11 -1
- package/plugins/src/expo/skills/expo-cicd-workflows/scripts/package.json +1 -1
package/package.json
CHANGED
|
@@ -112,10 +112,13 @@
|
|
|
112
112
|
"postcss": ">=8.5.18",
|
|
113
113
|
"undici": "^6.27.0",
|
|
114
114
|
"form-data": ">=4.0.6",
|
|
115
|
-
"brace-expansion": ">=5.0.9"
|
|
115
|
+
"brace-expansion": ">=5.0.9",
|
|
116
|
+
"@istanbuljs/load-nyc-config": {
|
|
117
|
+
"js-yaml": "^3.15.1"
|
|
118
|
+
}
|
|
116
119
|
},
|
|
117
120
|
"name": "@codyswann/lisa",
|
|
118
|
-
"version": "2.
|
|
121
|
+
"version": "2.338.0",
|
|
119
122
|
"description": "Claude Code governance framework that applies guardrails, guidance, and automated enforcement to projects",
|
|
120
123
|
"main": "dist/index.js",
|
|
121
124
|
"exports": {
|
|
@@ -221,7 +224,7 @@
|
|
|
221
224
|
"husky": "^8.0.0",
|
|
222
225
|
"jest": "^30.0.0",
|
|
223
226
|
"jiti": "^2.4.0",
|
|
224
|
-
"js-yaml": "^4.3.
|
|
227
|
+
"js-yaml": "^4.3.1",
|
|
225
228
|
"jscodeshift": "0.15.2",
|
|
226
229
|
"jsonc-parser": "^3.3.1",
|
|
227
230
|
"knip": "^5.0.0",
|
|
@@ -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.
|
|
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
|
|
package/plugins/lisa/.codex-plugin/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
668
|
-
|
|
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
|
-
|
|
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
|
|
@@ -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.
|
|
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
|
-
|
|
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
|
-
|
|
668
|
-
|
|
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
|
-
|
|
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
|
|
@@ -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.
|