@codyswann/lisa 2.317.2 → 2.317.4

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 (83) hide show
  1. package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
  2. package/dist/core/upstream-evidence-manifest.js +6 -5
  3. package/dist/core/upstream-evidence-manifest.js.map +1 -1
  4. package/package.json +1 -1
  5. package/plugins/lisa/.claude-plugin/plugin.json +1 -1
  6. package/plugins/lisa/.codex-plugin/plugin.json +1 -1
  7. package/plugins/lisa/.codex-plugin/skills/lisa-secrets-access/SKILL.md +19 -0
  8. package/plugins/lisa/.codex-plugin/skills/lisa-secrets-access/scripts/providers.mjs +52 -6
  9. package/plugins/lisa/.codex-plugin/skills/lisa-setup-automations/scripts/generate-workflow.mjs +15 -6
  10. package/plugins/lisa/.codex-plugin/skills/lisa-setup-remote-aws/SKILL.md +32 -7
  11. package/plugins/lisa/scripts/remote-agent-aws-setup.sh +8 -2
  12. package/plugins/lisa/skills/lisa-secrets-access/SKILL.md +19 -0
  13. package/plugins/lisa/skills/lisa-secrets-access/scripts/providers.mjs +52 -6
  14. package/plugins/lisa/skills/lisa-setup-automations/scripts/generate-workflow.mjs +15 -6
  15. package/plugins/lisa/skills/lisa-setup-remote-aws/SKILL.md +32 -7
  16. package/plugins/lisa-agy/plugin.json +1 -1
  17. package/plugins/lisa-agy/scripts/remote-agent-aws-setup.sh +8 -2
  18. package/plugins/lisa-agy/skills/lisa-secrets-access/SKILL.md +19 -0
  19. package/plugins/lisa-agy/skills/lisa-secrets-access/scripts/providers.mjs +52 -6
  20. package/plugins/lisa-agy/skills/lisa-setup-automations/scripts/generate-workflow.mjs +15 -6
  21. package/plugins/lisa-agy/skills/lisa-setup-remote-aws/SKILL.md +32 -7
  22. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  23. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  24. package/plugins/lisa-cdk-agy/plugin.json +1 -1
  25. package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
  26. package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
  27. package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
  28. package/plugins/lisa-copilot/scripts/remote-agent-aws-setup.sh +8 -2
  29. package/plugins/lisa-copilot/skills/lisa-secrets-access/SKILL.md +19 -0
  30. package/plugins/lisa-copilot/skills/lisa-secrets-access/scripts/providers.mjs +52 -6
  31. package/plugins/lisa-copilot/skills/lisa-setup-automations/scripts/generate-workflow.mjs +15 -6
  32. package/plugins/lisa-copilot/skills/lisa-setup-remote-aws/SKILL.md +32 -7
  33. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
  34. package/plugins/lisa-cursor/scripts/remote-agent-aws-setup.sh +8 -2
  35. package/plugins/lisa-cursor/skills/lisa-secrets-access/SKILL.md +19 -0
  36. package/plugins/lisa-cursor/skills/lisa-secrets-access/scripts/providers.mjs +52 -6
  37. package/plugins/lisa-cursor/skills/lisa-setup-automations/scripts/generate-workflow.mjs +15 -6
  38. package/plugins/lisa-cursor/skills/lisa-setup-remote-aws/SKILL.md +32 -7
  39. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  40. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  41. package/plugins/lisa-expo-agy/plugin.json +1 -1
  42. package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
  43. package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
  44. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  45. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  46. package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
  47. package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
  48. package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
  49. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  50. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  51. package/plugins/lisa-nestjs-agy/plugin.json +1 -1
  52. package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
  53. package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
  54. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  55. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  56. package/plugins/lisa-openclaw-agy/plugin.json +1 -1
  57. package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
  58. package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
  59. package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
  60. package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
  61. package/plugins/lisa-phaser-agy/plugin.json +1 -1
  62. package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
  63. package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
  64. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  65. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  66. package/plugins/lisa-rails-agy/plugin.json +1 -1
  67. package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
  68. package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
  69. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  70. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  71. package/plugins/lisa-typescript-agy/plugin.json +1 -1
  72. package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
  73. package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
  74. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  75. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  76. package/plugins/lisa-wiki-agy/plugin.json +1 -1
  77. package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
  78. package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
  79. package/plugins/src/base/scripts/remote-agent-aws-setup.sh +8 -2
  80. package/plugins/src/base/skills/lisa-secrets-access/SKILL.md +19 -0
  81. package/plugins/src/base/skills/lisa-secrets-access/scripts/providers.mjs +52 -6
  82. package/plugins/src/base/skills/lisa-setup-automations/scripts/generate-workflow.mjs +15 -6
  83. package/plugins/src/base/skills/lisa-setup-remote-aws/SKILL.md +32 -7
@@ -44,6 +44,10 @@ export function readAutomation(name, cwd = process.cwd()) {
44
44
  return {
45
45
  ...loop,
46
46
  repository: cfg.remoteEnv?.surfaces?.[loop.executionEnv]?.repository,
47
+ // Where this project keeps its bootstrap. A workstation serving several
48
+ // tenants gives each its own name, so the generated workflow must template
49
+ // it rather than assume the default.
50
+ bootstrapKey: cfg.secrets?.bootstrap?.key ?? "BWS_ACCESS_TOKEN",
47
51
  };
48
52
  }
49
53
 
@@ -70,6 +74,11 @@ export function readAutomation(name, cwd = process.cwd()) {
70
74
  */
71
75
  export function renderWorkflow(name, loop) {
72
76
  if (!loop.schedule) throw new Error(`automations["${name}"] has no schedule`);
77
+ // The repository secret and the environment variable carry the same name, so
78
+ // the value the workflow exports is the one `bootstrap.key` tells the resolver
79
+ // to look for. Translating it to the provider CLI's own variable is
80
+ // providers.mjs's job, not this workflow's.
81
+ const bootstrap = loop.bootstrapKey ?? "BWS_ACCESS_TOKEN";
73
82
  if (!loop.repository) {
74
83
  throw new Error(
75
84
  `automations["${name}"] targets executionEnv "${loop.executionEnv}", ` +
@@ -118,22 +127,22 @@ jobs:
118
127
  # seconds rather than after a toolchain download.
119
128
  - name: Check the bootstrap credential is configured
120
129
  env:
121
- BWS_ACCESS_TOKEN: \${{ secrets.BWS_ACCESS_TOKEN }}
130
+ ${bootstrap}: \${{ secrets.${bootstrap} }}
122
131
  run: |
123
132
  set -euo pipefail
124
- test -n "\${BWS_ACCESS_TOKEN}" || {
125
- echo "BWS_ACCESS_TOKEN is not configured for this repository" >&2
133
+ test -n "\${${bootstrap}}" || {
134
+ echo "${bootstrap} is not configured for this repository" >&2
126
135
  exit 1
127
136
  }
128
137
 
129
138
  - name: Prepare the toolchain and secrets
130
139
  env:
131
- BWS_ACCESS_TOKEN: \${{ secrets.BWS_ACCESS_TOKEN }}
140
+ ${bootstrap}: \${{ secrets.${bootstrap} }}
132
141
  run: bash scripts/lisa-remote-env/setup.sh
133
142
 
134
143
  - name: Run one ${name} cycle
135
144
  env:
136
- BWS_ACCESS_TOKEN: \${{ secrets.BWS_ACCESS_TOKEN }}
145
+ ${bootstrap}: \${{ secrets.${bootstrap} }}
137
146
  run: |
138
147
  set -euo pipefail
139
148
  node .claude/skills/lisa-remote-dispatch/scripts/dispatch.mjs \\
@@ -145,7 +154,7 @@ jobs:
145
154
  - name: Persist any credential rotation
146
155
  if: \${{ always() }}
147
156
  env:
148
- BWS_ACCESS_TOKEN: \${{ secrets.BWS_ACCESS_TOKEN }}
157
+ ${bootstrap}: \${{ secrets.${bootstrap} }}
149
158
  run: |
150
159
  set -euo pipefail
151
160
  node .claude/skills/lisa-secrets-access/scripts/rotate-secret.mjs leases
@@ -30,16 +30,41 @@ Install the vendor-neutral AWS bootstrap into the current repository.
30
30
  `.github/workflows/copilot-setup-steps.yml`; and it always writes
31
31
  `docs/remote-agent-aws.md`.
32
32
  4. Run `bash -n scripts/remote-agent-aws-setup.sh`.
33
- 5. Never write or request the actual bootstrap SecretString in repository
34
- files. Tell the operator to retrieve the `remote-agent-credentials` secret
35
- from the shared AWS account and paste its complete SecretString into the
36
- platform secret named `LISA_AWS_BOOTSTRAP_JSON`. The default cdkstarter
37
- secret name is `remote-agent-credentials`; downstream infrastructure may
38
- configure a different name. Pass that name with `--secret-name` so the
39
- generated runbook contains the exact retrieval command.
33
+ 5. Never write or request the actual bootstrap bundle in repository files. Tell
34
+ the operator where to obtain it and where to put it — see **Where the bundle
35
+ lives** below. Pass `--secret-name` when the source names it something other
36
+ than `remote-agent-credentials`, so the generated runbook contains the exact
37
+ retrieval command.
40
38
  6. Report the external step that remains: configure that one secret in the
41
39
  remote environment and launch a smoke session.
42
40
 
41
+ ## Where the bundle lives
42
+
43
+ `LISA_AWS_BOOTSTRAP_JSON` is **just another secret**, and it belongs wherever
44
+ the project keeps its secrets — resolved through `lisa-secrets-access` like
45
+ everything else. It is not tied to any one store.
46
+
47
+ Whoever provisions the IAM user emits the bundle somewhere. `cdkstarter` writes
48
+ it to AWS Secrets Manager as `remote-agent-credentials`, but that is where it is
49
+ *emitted*, not where it must *live*. Copy it once into the project's configured
50
+ provider and read it from there.
51
+
52
+ **Do not keep it in two stores.** A bundle sitting in Secrets Manager *and* in
53
+ the platform's secret store is two live copies of one credential: rotate the
54
+ IAM key and whichever copy you forget keeps authenticating until it doesn't,
55
+ with nothing to say which is current. That is the drift the single-store rule in
56
+ `lisa-secrets-access` exists to prevent, and this credential is not exempt.
57
+
58
+ **One provider cannot serve this particular secret.** If `secrets.provider` is
59
+ `aws`, reading the bundle requires AWS credentials — and the bundle *is* the AWS
60
+ credential. Anything else works: Bitwarden, 1Password, Doppler, Vault, or a
61
+ platform secret store. Pick the one that is not the thing being bootstrapped.
62
+
63
+ On a surface that materializes (see `lisa-secrets-access`), the bundle arrives
64
+ in `secrets.env` with everything else, so the project hook can run this setup
65
+ script after materialization with no extra plumbing. On a surface that reads
66
+ through live, resolve it at the point of use.
67
+
43
68
  ## Contract
44
69
 
45
70
  A future coding agent is supported without an AWS change when its remote
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-cdk",
3
- "version": "2.317.2",
3
+ "version": "2.317.4",
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.317.2",
3
+ "version": "2.317.4",
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.317.2",
3
+ "version": "2.317.4",
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.317.2",
3
+ "version": "2.317.4",
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.317.2",
3
+ "version": "2.317.4",
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.317.2",
3
+ "version": "2.317.4",
4
4
  "description": "Universal governance — agents, skills, commands, hooks, and rules for all projects",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -3,8 +3,14 @@
3
3
  # source copied into every generated Lisa agent plugin.
4
4
  #
5
5
  # Required secret:
6
- # LISA_AWS_BOOTSTRAP_JSON Complete SecretString emitted by cdkstarter's
7
- # remote-agent IAM kit.
6
+ # LISA_AWS_BOOTSTRAP_JSON The complete remote-agent bootstrap bundle, as one
7
+ # JSON value. cdkstarter's IAM kit emits it, but this
8
+ # script neither knows nor cares which store it came
9
+ # from — resolve it through lisa-secrets-access like
10
+ # any other secret. Note that a project whose
11
+ # provider is AWS Secrets Manager cannot keep it
12
+ # there: reading it would need the very credential it
13
+ # contains.
8
14
  # Optional plain variables:
9
15
  # LISA_REMOTE_AGENT claude | codex | cursor | copilot | agy | opencode
10
16
  # LISA_AWS_DEFAULT_PROFILE Defaults to dev, then the first available profile.
@@ -82,6 +82,18 @@ ${XDG_CONFIG_HOME:-$HOME/.config}/<secrets.namespace>/ # dir 0700
82
82
 
83
83
  **`bootstrap`** — how to obtain the one credential that unlocks the rest. `sources` is ordered, environment first. This is the **only** credential permitted in a keychain; it is a bootstrap, not a cache.
84
84
 
85
+ `bootstrap.key` answers exactly one question: **where do we find it** — the environment variable name and keychain service to look under. It is *not* the name the provider CLI reads. That name is fixed by the vendor (`bws` reads `BWS_ACCESS_TOKEN` and nothing else) and lives in `PROVIDER_BOOTSTRAP_ENV` in `providers.mjs`, which translates one into the other before invoking the CLI.
86
+
87
+ Keeping these separate is what makes **one workstation serve several tenants**. Give each its own name and each project points at its own:
88
+
89
+ ```json
90
+ "bootstrap": { "sources": ["env", "keychain"], "key": "BWS_ACCESS_TOKEN_acme" }
91
+ ```
92
+
93
+ Conflating them worked only while every project used the default, where the two coincide. The first project to slug its key handed the CLI a variable it had never heard of, and every read and rotation failed with `Missing access token`. If you add a provider, add its canonical variable to that table — do not reach for `bootstrap.key`.
94
+
95
+ On the GitHub Actions surface the repository secret and the exported environment variable both carry the **configured** name, and the generated workflow templates it. Translating to the CLI's own variable stays in `providers.mjs`.
96
+
85
97
  **`require`** — optional. Omit it and every secret the provider grants is available, which is correct when the provider already scopes access per project. Present, it narrows to exactly those names **and asserts them**: a listed name that does not resolve is a startup error, not a late surprise.
86
98
 
87
99
  **`narrow`** — may only *narrow* the provider's own grant. There is deliberately no way to widen access from config; that boundary belongs to the provider.
@@ -186,6 +198,13 @@ Values never enter that process, so its output cannot leak one even if logged.
186
198
  | `aws` | `aws secretsmanager get-secret-value --secret-id <name>` | not implemented |
187
199
  | `vault` | `vault kv get -field=<name> <path>` | not implemented |
188
200
 
201
+ **A provider cannot hold its own bootstrap.** With `provider: "aws"`, reading any
202
+ secret needs AWS credentials — so the AWS bootstrap bundle
203
+ (`LISA_AWS_BOOTSTRAP_JSON`, see `lisa-setup-remote-aws`) cannot live there:
204
+ resolving it would require the very credential it contains. Keep that one in a
205
+ different provider. The same applies to any provider whose own access key is a
206
+ secret you would otherwise store inside it.
207
+
189
208
  Unimplemented providers fail with a message naming where to add them, rather than failing obscurely. Do not claim support that does not exist.
190
209
 
191
210
  Cache **in-process only**. Never write a resolved value to disk except through the materialization contract above.
@@ -28,6 +28,56 @@ export const ENV_KEY = /^[A-Za-z_][A-Za-z0-9_]*$/;
28
28
  */
29
29
  export const LEASE_KEY = "LISA_ROTATION_LEASES";
30
30
 
31
+ /**
32
+ * The environment variable each provider's CLI reads its own bootstrap from.
33
+ *
34
+ * This is deliberately *not* configurable, and separating it from
35
+ * `bootstrap.key` is the whole point. Two different questions were previously
36
+ * answered by one value:
37
+ *
38
+ * - **Where do we find the bootstrap?** A keychain service or environment
39
+ * variable name. That must be configurable, because one workstation serves
40
+ * several tenants and each needs its own token stored under its own name.
41
+ * - **What does the provider CLI call it?** Fixed by the vendor. `bws` reads
42
+ * `BWS_ACCESS_TOKEN` and nothing else.
43
+ *
44
+ * Conflating them worked only while every project used the default name, where
45
+ * the two happen to coincide. The moment a project set
46
+ * `bootstrap.key: "BWS_ACCESS_TOKEN_<tenant>"` the CLI was handed a variable it
47
+ * has never heard of and failed with "Missing access token".
48
+ */
49
+ const PROVIDER_BOOTSTRAP_ENV = {
50
+ bitwarden: "BWS_ACCESS_TOKEN",
51
+ doppler: "DOPPLER_TOKEN",
52
+ };
53
+
54
+ /**
55
+ * Build the child environment for a provider CLI, injecting the bootstrap under
56
+ * the name that CLI actually reads.
57
+ *
58
+ * Both the read path and the rotation write path need this, and they previously
59
+ * carried separate copies of the same line — which is how they would eventually
60
+ * have drifted. One helper, one place to be wrong.
61
+ * @param {object} cfg Resolved configuration.
62
+ * @returns {NodeJS.ProcessEnv} Environment for the child process.
63
+ */
64
+ export function providerEnv(cfg) {
65
+ const env = { ...process.env };
66
+ const canonical = PROVIDER_BOOTSTRAP_ENV[cfg.provider];
67
+ if (!canonical) return env;
68
+ const token = bootstrapToken(cfg.bootstrap);
69
+ if (token) env[canonical] = token;
70
+ // Drop the tenant-scoped name once its value has been placed under the one
71
+ // the CLI reads. Inheriting it would leave two variables holding the same
72
+ // bootstrap in the child, which is how a tool that probes for a
73
+ // similarly-named credential binds to the wrong tenant on a workstation
74
+ // serving several. The child needs exactly one.
75
+ if (cfg.bootstrap.key && cfg.bootstrap.key !== canonical) {
76
+ delete env[cfg.bootstrap.key];
77
+ }
78
+ return env;
79
+ }
80
+
31
81
  /**
32
82
  * Obtain the one credential that unlocks the provider.
33
83
  *
@@ -83,9 +133,7 @@ function fromKeychain(key) {
83
133
  * @returns {Array<{key: string, value: string, note: string, projectId: string|null}>} Rows.
84
134
  */
85
135
  export function fetchRaw(cfg) {
86
- const env = { ...process.env };
87
- const token = bootstrapToken(cfg.bootstrap);
88
- if (cfg.bootstrap.key && token) env[cfg.bootstrap.key] = token;
136
+ const env = providerEnv(cfg);
89
137
 
90
138
  if (cfg.provider === "env") {
91
139
  return Object.entries(process.env)
@@ -225,9 +273,7 @@ export function rawByKey(cfg, key) {
225
273
  * @param {string} value Replacement value.
226
274
  */
227
275
  export function writeSecret(cfg, id, value) {
228
- const env = { ...process.env };
229
- const token = bootstrapToken(cfg.bootstrap);
230
- if (cfg.bootstrap.key && token) env[cfg.bootstrap.key] = token;
276
+ const env = providerEnv(cfg);
231
277
 
232
278
  if (cfg.provider === "bitwarden") {
233
279
  run("bws", ["secret", "edit", id, "--value", value], env);
@@ -44,6 +44,10 @@ export function readAutomation(name, cwd = process.cwd()) {
44
44
  return {
45
45
  ...loop,
46
46
  repository: cfg.remoteEnv?.surfaces?.[loop.executionEnv]?.repository,
47
+ // Where this project keeps its bootstrap. A workstation serving several
48
+ // tenants gives each its own name, so the generated workflow must template
49
+ // it rather than assume the default.
50
+ bootstrapKey: cfg.secrets?.bootstrap?.key ?? "BWS_ACCESS_TOKEN",
47
51
  };
48
52
  }
49
53
 
@@ -70,6 +74,11 @@ export function readAutomation(name, cwd = process.cwd()) {
70
74
  */
71
75
  export function renderWorkflow(name, loop) {
72
76
  if (!loop.schedule) throw new Error(`automations["${name}"] has no schedule`);
77
+ // The repository secret and the environment variable carry the same name, so
78
+ // the value the workflow exports is the one `bootstrap.key` tells the resolver
79
+ // to look for. Translating it to the provider CLI's own variable is
80
+ // providers.mjs's job, not this workflow's.
81
+ const bootstrap = loop.bootstrapKey ?? "BWS_ACCESS_TOKEN";
73
82
  if (!loop.repository) {
74
83
  throw new Error(
75
84
  `automations["${name}"] targets executionEnv "${loop.executionEnv}", ` +
@@ -118,22 +127,22 @@ jobs:
118
127
  # seconds rather than after a toolchain download.
119
128
  - name: Check the bootstrap credential is configured
120
129
  env:
121
- BWS_ACCESS_TOKEN: \${{ secrets.BWS_ACCESS_TOKEN }}
130
+ ${bootstrap}: \${{ secrets.${bootstrap} }}
122
131
  run: |
123
132
  set -euo pipefail
124
- test -n "\${BWS_ACCESS_TOKEN}" || {
125
- echo "BWS_ACCESS_TOKEN is not configured for this repository" >&2
133
+ test -n "\${${bootstrap}}" || {
134
+ echo "${bootstrap} is not configured for this repository" >&2
126
135
  exit 1
127
136
  }
128
137
 
129
138
  - name: Prepare the toolchain and secrets
130
139
  env:
131
- BWS_ACCESS_TOKEN: \${{ secrets.BWS_ACCESS_TOKEN }}
140
+ ${bootstrap}: \${{ secrets.${bootstrap} }}
132
141
  run: bash scripts/lisa-remote-env/setup.sh
133
142
 
134
143
  - name: Run one ${name} cycle
135
144
  env:
136
- BWS_ACCESS_TOKEN: \${{ secrets.BWS_ACCESS_TOKEN }}
145
+ ${bootstrap}: \${{ secrets.${bootstrap} }}
137
146
  run: |
138
147
  set -euo pipefail
139
148
  node .claude/skills/lisa-remote-dispatch/scripts/dispatch.mjs \\
@@ -145,7 +154,7 @@ jobs:
145
154
  - name: Persist any credential rotation
146
155
  if: \${{ always() }}
147
156
  env:
148
- BWS_ACCESS_TOKEN: \${{ secrets.BWS_ACCESS_TOKEN }}
157
+ ${bootstrap}: \${{ secrets.${bootstrap} }}
149
158
  run: |
150
159
  set -euo pipefail
151
160
  node .claude/skills/lisa-secrets-access/scripts/rotate-secret.mjs leases
@@ -30,16 +30,41 @@ Install the vendor-neutral AWS bootstrap into the current repository.
30
30
  `.github/workflows/copilot-setup-steps.yml`; and it always writes
31
31
  `docs/remote-agent-aws.md`.
32
32
  4. Run `bash -n scripts/remote-agent-aws-setup.sh`.
33
- 5. Never write or request the actual bootstrap SecretString in repository
34
- files. Tell the operator to retrieve the `remote-agent-credentials` secret
35
- from the shared AWS account and paste its complete SecretString into the
36
- platform secret named `LISA_AWS_BOOTSTRAP_JSON`. The default cdkstarter
37
- secret name is `remote-agent-credentials`; downstream infrastructure may
38
- configure a different name. Pass that name with `--secret-name` so the
39
- generated runbook contains the exact retrieval command.
33
+ 5. Never write or request the actual bootstrap bundle in repository files. Tell
34
+ the operator where to obtain it and where to put it — see **Where the bundle
35
+ lives** below. Pass `--secret-name` when the source names it something other
36
+ than `remote-agent-credentials`, so the generated runbook contains the exact
37
+ retrieval command.
40
38
  6. Report the external step that remains: configure that one secret in the
41
39
  remote environment and launch a smoke session.
42
40
 
41
+ ## Where the bundle lives
42
+
43
+ `LISA_AWS_BOOTSTRAP_JSON` is **just another secret**, and it belongs wherever
44
+ the project keeps its secrets — resolved through `lisa-secrets-access` like
45
+ everything else. It is not tied to any one store.
46
+
47
+ Whoever provisions the IAM user emits the bundle somewhere. `cdkstarter` writes
48
+ it to AWS Secrets Manager as `remote-agent-credentials`, but that is where it is
49
+ *emitted*, not where it must *live*. Copy it once into the project's configured
50
+ provider and read it from there.
51
+
52
+ **Do not keep it in two stores.** A bundle sitting in Secrets Manager *and* in
53
+ the platform's secret store is two live copies of one credential: rotate the
54
+ IAM key and whichever copy you forget keeps authenticating until it doesn't,
55
+ with nothing to say which is current. That is the drift the single-store rule in
56
+ `lisa-secrets-access` exists to prevent, and this credential is not exempt.
57
+
58
+ **One provider cannot serve this particular secret.** If `secrets.provider` is
59
+ `aws`, reading the bundle requires AWS credentials — and the bundle *is* the AWS
60
+ credential. Anything else works: Bitwarden, 1Password, Doppler, Vault, or a
61
+ platform secret store. Pick the one that is not the thing being bootstrapped.
62
+
63
+ On a surface that materializes (see `lisa-secrets-access`), the bundle arrives
64
+ in `secrets.env` with everything else, so the project hook can run this setup
65
+ script after materialization with no extra plumbing. On a surface that reads
66
+ through live, resolve it at the point of use.
67
+
43
68
  ## Contract
44
69
 
45
70
  A future coding agent is supported without an AWS change when its remote
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa",
3
- "version": "2.317.2",
3
+ "version": "2.317.4",
4
4
  "description": "Universal governance — agents, skills, commands, hooks, and rules for all projects",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -3,8 +3,14 @@
3
3
  # source copied into every generated Lisa agent plugin.
4
4
  #
5
5
  # Required secret:
6
- # LISA_AWS_BOOTSTRAP_JSON Complete SecretString emitted by cdkstarter's
7
- # remote-agent IAM kit.
6
+ # LISA_AWS_BOOTSTRAP_JSON The complete remote-agent bootstrap bundle, as one
7
+ # JSON value. cdkstarter's IAM kit emits it, but this
8
+ # script neither knows nor cares which store it came
9
+ # from — resolve it through lisa-secrets-access like
10
+ # any other secret. Note that a project whose
11
+ # provider is AWS Secrets Manager cannot keep it
12
+ # there: reading it would need the very credential it
13
+ # contains.
8
14
  # Optional plain variables:
9
15
  # LISA_REMOTE_AGENT claude | codex | cursor | copilot | agy | opencode
10
16
  # LISA_AWS_DEFAULT_PROFILE Defaults to dev, then the first available profile.
@@ -82,6 +82,18 @@ ${XDG_CONFIG_HOME:-$HOME/.config}/<secrets.namespace>/ # dir 0700
82
82
 
83
83
  **`bootstrap`** — how to obtain the one credential that unlocks the rest. `sources` is ordered, environment first. This is the **only** credential permitted in a keychain; it is a bootstrap, not a cache.
84
84
 
85
+ `bootstrap.key` answers exactly one question: **where do we find it** — the environment variable name and keychain service to look under. It is *not* the name the provider CLI reads. That name is fixed by the vendor (`bws` reads `BWS_ACCESS_TOKEN` and nothing else) and lives in `PROVIDER_BOOTSTRAP_ENV` in `providers.mjs`, which translates one into the other before invoking the CLI.
86
+
87
+ Keeping these separate is what makes **one workstation serve several tenants**. Give each its own name and each project points at its own:
88
+
89
+ ```json
90
+ "bootstrap": { "sources": ["env", "keychain"], "key": "BWS_ACCESS_TOKEN_acme" }
91
+ ```
92
+
93
+ Conflating them worked only while every project used the default, where the two coincide. The first project to slug its key handed the CLI a variable it had never heard of, and every read and rotation failed with `Missing access token`. If you add a provider, add its canonical variable to that table — do not reach for `bootstrap.key`.
94
+
95
+ On the GitHub Actions surface the repository secret and the exported environment variable both carry the **configured** name, and the generated workflow templates it. Translating to the CLI's own variable stays in `providers.mjs`.
96
+
85
97
  **`require`** — optional. Omit it and every secret the provider grants is available, which is correct when the provider already scopes access per project. Present, it narrows to exactly those names **and asserts them**: a listed name that does not resolve is a startup error, not a late surprise.
86
98
 
87
99
  **`narrow`** — may only *narrow* the provider's own grant. There is deliberately no way to widen access from config; that boundary belongs to the provider.
@@ -186,6 +198,13 @@ Values never enter that process, so its output cannot leak one even if logged.
186
198
  | `aws` | `aws secretsmanager get-secret-value --secret-id <name>` | not implemented |
187
199
  | `vault` | `vault kv get -field=<name> <path>` | not implemented |
188
200
 
201
+ **A provider cannot hold its own bootstrap.** With `provider: "aws"`, reading any
202
+ secret needs AWS credentials — so the AWS bootstrap bundle
203
+ (`LISA_AWS_BOOTSTRAP_JSON`, see `lisa-setup-remote-aws`) cannot live there:
204
+ resolving it would require the very credential it contains. Keep that one in a
205
+ different provider. The same applies to any provider whose own access key is a
206
+ secret you would otherwise store inside it.
207
+
189
208
  Unimplemented providers fail with a message naming where to add them, rather than failing obscurely. Do not claim support that does not exist.
190
209
 
191
210
  Cache **in-process only**. Never write a resolved value to disk except through the materialization contract above.
@@ -28,6 +28,56 @@ export const ENV_KEY = /^[A-Za-z_][A-Za-z0-9_]*$/;
28
28
  */
29
29
  export const LEASE_KEY = "LISA_ROTATION_LEASES";
30
30
 
31
+ /**
32
+ * The environment variable each provider's CLI reads its own bootstrap from.
33
+ *
34
+ * This is deliberately *not* configurable, and separating it from
35
+ * `bootstrap.key` is the whole point. Two different questions were previously
36
+ * answered by one value:
37
+ *
38
+ * - **Where do we find the bootstrap?** A keychain service or environment
39
+ * variable name. That must be configurable, because one workstation serves
40
+ * several tenants and each needs its own token stored under its own name.
41
+ * - **What does the provider CLI call it?** Fixed by the vendor. `bws` reads
42
+ * `BWS_ACCESS_TOKEN` and nothing else.
43
+ *
44
+ * Conflating them worked only while every project used the default name, where
45
+ * the two happen to coincide. The moment a project set
46
+ * `bootstrap.key: "BWS_ACCESS_TOKEN_<tenant>"` the CLI was handed a variable it
47
+ * has never heard of and failed with "Missing access token".
48
+ */
49
+ const PROVIDER_BOOTSTRAP_ENV = {
50
+ bitwarden: "BWS_ACCESS_TOKEN",
51
+ doppler: "DOPPLER_TOKEN",
52
+ };
53
+
54
+ /**
55
+ * Build the child environment for a provider CLI, injecting the bootstrap under
56
+ * the name that CLI actually reads.
57
+ *
58
+ * Both the read path and the rotation write path need this, and they previously
59
+ * carried separate copies of the same line — which is how they would eventually
60
+ * have drifted. One helper, one place to be wrong.
61
+ * @param {object} cfg Resolved configuration.
62
+ * @returns {NodeJS.ProcessEnv} Environment for the child process.
63
+ */
64
+ export function providerEnv(cfg) {
65
+ const env = { ...process.env };
66
+ const canonical = PROVIDER_BOOTSTRAP_ENV[cfg.provider];
67
+ if (!canonical) return env;
68
+ const token = bootstrapToken(cfg.bootstrap);
69
+ if (token) env[canonical] = token;
70
+ // Drop the tenant-scoped name once its value has been placed under the one
71
+ // the CLI reads. Inheriting it would leave two variables holding the same
72
+ // bootstrap in the child, which is how a tool that probes for a
73
+ // similarly-named credential binds to the wrong tenant on a workstation
74
+ // serving several. The child needs exactly one.
75
+ if (cfg.bootstrap.key && cfg.bootstrap.key !== canonical) {
76
+ delete env[cfg.bootstrap.key];
77
+ }
78
+ return env;
79
+ }
80
+
31
81
  /**
32
82
  * Obtain the one credential that unlocks the provider.
33
83
  *
@@ -83,9 +133,7 @@ function fromKeychain(key) {
83
133
  * @returns {Array<{key: string, value: string, note: string, projectId: string|null}>} Rows.
84
134
  */
85
135
  export function fetchRaw(cfg) {
86
- const env = { ...process.env };
87
- const token = bootstrapToken(cfg.bootstrap);
88
- if (cfg.bootstrap.key && token) env[cfg.bootstrap.key] = token;
136
+ const env = providerEnv(cfg);
89
137
 
90
138
  if (cfg.provider === "env") {
91
139
  return Object.entries(process.env)
@@ -225,9 +273,7 @@ export function rawByKey(cfg, key) {
225
273
  * @param {string} value Replacement value.
226
274
  */
227
275
  export function writeSecret(cfg, id, value) {
228
- const env = { ...process.env };
229
- const token = bootstrapToken(cfg.bootstrap);
230
- if (cfg.bootstrap.key && token) env[cfg.bootstrap.key] = token;
276
+ const env = providerEnv(cfg);
231
277
 
232
278
  if (cfg.provider === "bitwarden") {
233
279
  run("bws", ["secret", "edit", id, "--value", value], env);
@@ -44,6 +44,10 @@ export function readAutomation(name, cwd = process.cwd()) {
44
44
  return {
45
45
  ...loop,
46
46
  repository: cfg.remoteEnv?.surfaces?.[loop.executionEnv]?.repository,
47
+ // Where this project keeps its bootstrap. A workstation serving several
48
+ // tenants gives each its own name, so the generated workflow must template
49
+ // it rather than assume the default.
50
+ bootstrapKey: cfg.secrets?.bootstrap?.key ?? "BWS_ACCESS_TOKEN",
47
51
  };
48
52
  }
49
53
 
@@ -70,6 +74,11 @@ export function readAutomation(name, cwd = process.cwd()) {
70
74
  */
71
75
  export function renderWorkflow(name, loop) {
72
76
  if (!loop.schedule) throw new Error(`automations["${name}"] has no schedule`);
77
+ // The repository secret and the environment variable carry the same name, so
78
+ // the value the workflow exports is the one `bootstrap.key` tells the resolver
79
+ // to look for. Translating it to the provider CLI's own variable is
80
+ // providers.mjs's job, not this workflow's.
81
+ const bootstrap = loop.bootstrapKey ?? "BWS_ACCESS_TOKEN";
73
82
  if (!loop.repository) {
74
83
  throw new Error(
75
84
  `automations["${name}"] targets executionEnv "${loop.executionEnv}", ` +
@@ -118,22 +127,22 @@ jobs:
118
127
  # seconds rather than after a toolchain download.
119
128
  - name: Check the bootstrap credential is configured
120
129
  env:
121
- BWS_ACCESS_TOKEN: \${{ secrets.BWS_ACCESS_TOKEN }}
130
+ ${bootstrap}: \${{ secrets.${bootstrap} }}
122
131
  run: |
123
132
  set -euo pipefail
124
- test -n "\${BWS_ACCESS_TOKEN}" || {
125
- echo "BWS_ACCESS_TOKEN is not configured for this repository" >&2
133
+ test -n "\${${bootstrap}}" || {
134
+ echo "${bootstrap} is not configured for this repository" >&2
126
135
  exit 1
127
136
  }
128
137
 
129
138
  - name: Prepare the toolchain and secrets
130
139
  env:
131
- BWS_ACCESS_TOKEN: \${{ secrets.BWS_ACCESS_TOKEN }}
140
+ ${bootstrap}: \${{ secrets.${bootstrap} }}
132
141
  run: bash scripts/lisa-remote-env/setup.sh
133
142
 
134
143
  - name: Run one ${name} cycle
135
144
  env:
136
- BWS_ACCESS_TOKEN: \${{ secrets.BWS_ACCESS_TOKEN }}
145
+ ${bootstrap}: \${{ secrets.${bootstrap} }}
137
146
  run: |
138
147
  set -euo pipefail
139
148
  node .claude/skills/lisa-remote-dispatch/scripts/dispatch.mjs \\
@@ -145,7 +154,7 @@ jobs:
145
154
  - name: Persist any credential rotation
146
155
  if: \${{ always() }}
147
156
  env:
148
- BWS_ACCESS_TOKEN: \${{ secrets.BWS_ACCESS_TOKEN }}
157
+ ${bootstrap}: \${{ secrets.${bootstrap} }}
149
158
  run: |
150
159
  set -euo pipefail
151
160
  node .claude/skills/lisa-secrets-access/scripts/rotate-secret.mjs leases