@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.
- package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
- package/dist/core/upstream-evidence-manifest.js +6 -5
- package/dist/core/upstream-evidence-manifest.js.map +1 -1
- package/package.json +1 -1
- 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 +19 -0
- package/plugins/lisa/.codex-plugin/skills/lisa-secrets-access/scripts/providers.mjs +52 -6
- package/plugins/lisa/.codex-plugin/skills/lisa-setup-automations/scripts/generate-workflow.mjs +15 -6
- package/plugins/lisa/.codex-plugin/skills/lisa-setup-remote-aws/SKILL.md +32 -7
- package/plugins/lisa/scripts/remote-agent-aws-setup.sh +8 -2
- package/plugins/lisa/skills/lisa-secrets-access/SKILL.md +19 -0
- package/plugins/lisa/skills/lisa-secrets-access/scripts/providers.mjs +52 -6
- package/plugins/lisa/skills/lisa-setup-automations/scripts/generate-workflow.mjs +15 -6
- package/plugins/lisa/skills/lisa-setup-remote-aws/SKILL.md +32 -7
- package/plugins/lisa-agy/plugin.json +1 -1
- package/plugins/lisa-agy/scripts/remote-agent-aws-setup.sh +8 -2
- package/plugins/lisa-agy/skills/lisa-secrets-access/SKILL.md +19 -0
- package/plugins/lisa-agy/skills/lisa-secrets-access/scripts/providers.mjs +52 -6
- package/plugins/lisa-agy/skills/lisa-setup-automations/scripts/generate-workflow.mjs +15 -6
- package/plugins/lisa-agy/skills/lisa-setup-remote-aws/SKILL.md +32 -7
- 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/scripts/remote-agent-aws-setup.sh +8 -2
- package/plugins/lisa-copilot/skills/lisa-secrets-access/SKILL.md +19 -0
- package/plugins/lisa-copilot/skills/lisa-secrets-access/scripts/providers.mjs +52 -6
- package/plugins/lisa-copilot/skills/lisa-setup-automations/scripts/generate-workflow.mjs +15 -6
- package/plugins/lisa-copilot/skills/lisa-setup-remote-aws/SKILL.md +32 -7
- package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cursor/scripts/remote-agent-aws-setup.sh +8 -2
- package/plugins/lisa-cursor/skills/lisa-secrets-access/SKILL.md +19 -0
- package/plugins/lisa-cursor/skills/lisa-secrets-access/scripts/providers.mjs +52 -6
- package/plugins/lisa-cursor/skills/lisa-setup-automations/scripts/generate-workflow.mjs +15 -6
- package/plugins/lisa-cursor/skills/lisa-setup-remote-aws/SKILL.md +32 -7
- package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-expo-agy/plugin.json +1 -1
- package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo-cursor/.claude-plugin/plugin.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/scripts/remote-agent-aws-setup.sh +8 -2
- package/plugins/src/base/skills/lisa-secrets-access/SKILL.md +19 -0
- package/plugins/src/base/skills/lisa-secrets-access/scripts/providers.mjs +52 -6
- package/plugins/src/base/skills/lisa-setup-automations/scripts/generate-workflow.mjs +15 -6
- package/plugins/src/base/skills/lisa-setup-remote-aws/SKILL.md +32 -7
package/package.json
CHANGED
|
@@ -115,7 +115,7 @@
|
|
|
115
115
|
"brace-expansion": ">=5.0.8"
|
|
116
116
|
},
|
|
117
117
|
"name": "@codyswann/lisa",
|
|
118
|
-
"version": "2.317.
|
|
118
|
+
"version": "2.317.4",
|
|
119
119
|
"description": "Claude Code governance framework that applies guardrails, guidance, and automated enforcement to projects",
|
|
120
120
|
"main": "dist/index.js",
|
|
121
121
|
"exports": {
|
|
@@ -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 =
|
|
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 =
|
|
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);
|
package/plugins/lisa/.codex-plugin/skills/lisa-setup-automations/scripts/generate-workflow.mjs
CHANGED
|
@@ -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
|
-
|
|
130
|
+
${bootstrap}: \${{ secrets.${bootstrap} }}
|
|
122
131
|
run: |
|
|
123
132
|
set -euo pipefail
|
|
124
|
-
test -n "\${
|
|
125
|
-
echo "
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
|
@@ -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
|
|
7
|
-
#
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
130
|
+
${bootstrap}: \${{ secrets.${bootstrap} }}
|
|
122
131
|
run: |
|
|
123
132
|
set -euo pipefail
|
|
124
|
-
test -n "\${
|
|
125
|
-
echo "
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
|
@@ -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
|
|
7
|
-
#
|
|
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 =
|
|
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 =
|
|
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);
|