@codyswann/lisa 4.29.2 → 4.29.3

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 (79) hide show
  1. package/dist/core/nightly-e2e-guard-behavior-certificate.js +2 -2
  2. package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
  3. package/dist/core/upstream-evidence-manifest.js +12 -3
  4. package/dist/core/upstream-evidence-manifest.js.map +1 -1
  5. package/package.json +3 -3
  6. package/plugins/lisa/.claude-plugin/plugin.json +1 -1
  7. package/plugins/lisa/.codex-plugin/plugin.json +1 -1
  8. package/plugins/lisa/.codex-plugin/skills/lisa-secrets-access/SKILL.md +55 -0
  9. package/plugins/lisa/.codex-plugin/skills/lisa-secrets-access/scripts/aws-bootstrap.mjs +202 -16
  10. package/plugins/lisa/.codex-plugin/skills/lisa-secrets-access/scripts/materialize-secrets.mjs +589 -100
  11. package/plugins/lisa/.codex-plugin/skills/lisa-secrets-access/scripts/owner.mjs +96 -0
  12. package/plugins/lisa/skills/lisa-secrets-access/SKILL.md +55 -0
  13. package/plugins/lisa/skills/lisa-secrets-access/scripts/aws-bootstrap.mjs +202 -16
  14. package/plugins/lisa/skills/lisa-secrets-access/scripts/materialize-secrets.mjs +589 -100
  15. package/plugins/lisa/skills/lisa-secrets-access/scripts/owner.mjs +96 -0
  16. package/plugins/lisa-agy/plugin.json +1 -1
  17. package/plugins/lisa-agy/skills/lisa-secrets-access/SKILL.md +55 -0
  18. package/plugins/lisa-agy/skills/lisa-secrets-access/scripts/aws-bootstrap.mjs +202 -16
  19. package/plugins/lisa-agy/skills/lisa-secrets-access/scripts/materialize-secrets.mjs +589 -100
  20. package/plugins/lisa-agy/skills/lisa-secrets-access/scripts/owner.mjs +96 -0
  21. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  22. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  23. package/plugins/lisa-cdk-agy/plugin.json +1 -1
  24. package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
  25. package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
  26. package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
  27. package/plugins/lisa-copilot/skills/lisa-secrets-access/SKILL.md +55 -0
  28. package/plugins/lisa-copilot/skills/lisa-secrets-access/scripts/aws-bootstrap.mjs +202 -16
  29. package/plugins/lisa-copilot/skills/lisa-secrets-access/scripts/materialize-secrets.mjs +589 -100
  30. package/plugins/lisa-copilot/skills/lisa-secrets-access/scripts/owner.mjs +96 -0
  31. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
  32. package/plugins/lisa-cursor/skills/lisa-secrets-access/SKILL.md +55 -0
  33. package/plugins/lisa-cursor/skills/lisa-secrets-access/scripts/aws-bootstrap.mjs +202 -16
  34. package/plugins/lisa-cursor/skills/lisa-secrets-access/scripts/materialize-secrets.mjs +589 -100
  35. package/plugins/lisa-cursor/skills/lisa-secrets-access/scripts/owner.mjs +96 -0
  36. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  37. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  38. package/plugins/lisa-expo-agy/plugin.json +1 -1
  39. package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
  40. package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
  41. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  42. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  43. package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
  44. package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
  45. package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
  46. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  47. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  48. package/plugins/lisa-nestjs-agy/plugin.json +1 -1
  49. package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
  50. package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
  51. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  52. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  53. package/plugins/lisa-openclaw-agy/plugin.json +1 -1
  54. package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
  55. package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
  56. package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
  57. package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
  58. package/plugins/lisa-phaser-agy/plugin.json +1 -1
  59. package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
  60. package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
  61. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  62. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  63. package/plugins/lisa-rails-agy/plugin.json +1 -1
  64. package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
  65. package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
  66. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  67. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  68. package/plugins/lisa-typescript-agy/plugin.json +1 -1
  69. package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
  70. package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
  71. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  72. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  73. package/plugins/lisa-wiki-agy/plugin.json +1 -1
  74. package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
  75. package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
  76. package/plugins/src/base/skills/lisa-secrets-access/SKILL.md +55 -0
  77. package/plugins/src/base/skills/lisa-secrets-access/scripts/aws-bootstrap.mjs +202 -16
  78. package/plugins/src/base/skills/lisa-secrets-access/scripts/materialize-secrets.mjs +589 -100
  79. package/plugins/src/base/skills/lisa-secrets-access/scripts/owner.mjs +96 -0
@@ -28,6 +28,8 @@
28
28
  * @module aws-bootstrap
29
29
  */
30
30
 
31
+ import { assertOwner } from "./owner.mjs";
32
+
31
33
  /** The names every AWS SDK and the CLI actually read. */
32
34
  const ACCESS_KEY = "AWS_ACCESS_KEY_ID";
33
35
  const SECRET_KEY = "AWS_SECRET_ACCESS_KEY";
@@ -40,13 +42,93 @@ const PROFILE = "AWS_PROFILE";
40
42
  export const BOOTSTRAP_KEY = "LISA_AWS_BOOTSTRAP_JSON";
41
43
 
42
44
  /**
43
- * The profile that holds the bootstrap key pair and is assumed FROM.
45
+ * EMIT vs RESOLVE — read this before touching either name below.
46
+ *
47
+ * These are two different guarantees, they expire on two different conditions,
48
+ * and conflating them is how a temporary shim becomes permanent by accident.
49
+ *
50
+ * **EMIT** is what this writer produces as its canonical output. Exactly one
51
+ * shape is ever emitted as canonical: `<owner>-<stage>`, assuming from
52
+ * `<owner>-lisa-bootstrap`. That is the fix for the cross-project collision and
53
+ * it has no expiry.
54
+ *
55
+ * **RESOLVE** is the wider set of names that must keep working for READERS
56
+ * during a migration. It currently includes the bare `<stage>` family, which
57
+ * this writer emits only as a deprecated alias beside the canonical output —
58
+ * never as canonical output itself.
59
+ *
60
+ * A reader who mistakes the second for the first will conclude that Lisa still
61
+ * emits bare names and that the rename never happened. It did: every canonical
62
+ * name is owned, and `emitSourceProfileFor` is the only thing that names a
63
+ * source profile in canonical output.
64
+ */
65
+
66
+ /**
67
+ * The tail of the EMITTED profile that holds the key pair and is assumed FROM.
44
68
  *
45
69
  * Named rather than `default` so it can never be picked up by accident: this
46
70
  * identity can assume roles and do nothing else, so a call that silently ran as
47
71
  * it would fail with a permissions error far from its cause.
72
+ *
73
+ * A SUFFIX rather than the whole name, because one fixed name is one shared
74
+ * slot. Two tenants on a workstation both wrote `[lisa-bootstrap]`, so the
75
+ * second run's key pair replaced the first's and the first tenant's profiles
76
+ * then assumed their roles from the second tenant's identity — which either
77
+ * fails confusingly or, where a trust policy is permissive, succeeds.
48
78
  */
49
- export const SOURCE_PROFILE = "lisa-bootstrap";
79
+ export const EMITTED_SOURCE_PROFILE_SUFFIX = "lisa-bootstrap";
80
+
81
+ /**
82
+ * A name kept RESOLVING for unmigrated readers. Never emitted as canonical.
83
+ * **DEPRECATED — this constant exists only to be deleted.**
84
+ *
85
+ * Generators outside this repository emit the bare `<stage>` profile family and
86
+ * this bare source profile independently, and scripts and documentation in
87
+ * caller repositories name them directly. Nothing co-ordinates a rename across
88
+ * those repositories, so switching this writer to the owned names alone would
89
+ * leave writer and reader disagreeing: a bare `[profile <stage>]` whose
90
+ * `source_profile = lisa-bootstrap` would point at a section that no longer
91
+ * exists, and every call through it would fail to resolve.
92
+ *
93
+ * So the owned names are EMITTED as canonical and these bare names are kept
94
+ * RESOLVING beside them, both generated from one bundle in one pass. This
95
+ * constant is never used to decide what canonical output looks like, and it is
96
+ * not used to attribute a block either — ownership is read from the `owner=`
97
+ * marker tag, and a pre-ownership block is attributed by the accounts in its
98
+ * role ARNs. Its only job is the resolve path.
99
+ *
100
+ * **The window is not a fix, and this comment must not read like one.** The
101
+ * bare family is a single shared slot on a machine that may serve several
102
+ * projects — which is precisely the collision the owned names remove. During
103
+ * the window that collision is unfixed ON THE RESOLVE PATH ONLY: these names
104
+ * are claimed when unclaimed or already ours, and refused by name when another
105
+ * project holds them, so the failure is loud rather than silent, but two
106
+ * projects still cannot both have them. The emitted names are always correct;
107
+ * these are correct for at most one project per machine.
108
+ *
109
+ * **Removal condition — stated against the RESOLVE path, because that is the
110
+ * only thing that expires.** Delete this constant and every use of it once no
111
+ * caller repository *resolves* the bare family: that is, once nothing outside
112
+ * this repository names `<stage>` or `lisa-bootstrap` when selecting a profile.
113
+ * A caller proves it no longer resolves them by selecting only
114
+ * `<namespace>-<stage>`; `LISA_SECRETS_NO_LEGACY_PROFILES=1` lets one opt out
115
+ * ahead of the removal and take the isolation immediately. Nothing about the
116
+ * emit path changes when this is deleted, because the emit path never used it.
117
+ */
118
+ export const RESOLVE_ONLY_LEGACY_SOURCE_PROFILE = EMITTED_SOURCE_PROFILE_SUFFIX;
119
+
120
+ /**
121
+ * The source profile this writer EMITS for one owner, as canonical output.
122
+ *
123
+ * The only function that names a source profile in canonical output. If a
124
+ * canonical section ever assumes from something this did not return, the emit
125
+ * path has regressed.
126
+ * @param {string} owner Validated owner.
127
+ * @returns {string} The owner's emitted source-profile name.
128
+ */
129
+ export function emitSourceProfileFor(owner) {
130
+ return `${owner}-${EMITTED_SOURCE_PROFILE_SUFFIX}`;
131
+ }
50
132
 
51
133
  /**
52
134
  * Read the bundle's `profiles`, which may be an object OR a JSON string.
@@ -86,11 +168,27 @@ export function readProfiles(bundle) {
86
168
  * environment credentials OUTRANK `AWS_PROFILE`, so a session that exports them
87
169
  * ignores whichever profile it selects — the profiles would exist and never be
88
170
  * used.
171
+ *
172
+ * Every name is prefixed with the OWNER. The bundle's keys name a stage and
173
+ * nothing else, and the kit deliberately keeps role names identical across
174
+ * accounts so profiles differ only by account id — correct within one tenant,
175
+ * and the removal of the only distinguishing feature across two. Prefixing
176
+ * makes the wrong resolution impossible rather than merely detectable, which is
177
+ * the same fix #3440 applied to the sibling remote-agent writer; a workstation
178
+ * carrying both now sees one convention instead of two.
179
+ *
180
+ * The `resolveOnly` half is rendered SEPARATELY, and returned rather than merged,
181
+ * so
182
+ * the installer can drop it as a unit after looking at the filesystem — which
183
+ * this function deliberately cannot see. See `RESOLVE_ONLY_LEGACY_SOURCE_PROFILE` for why
184
+ * that half exists at all and when it goes away.
89
185
  * @param {object} bundle Parsed bootstrap bundle.
90
- * @returns {{credentials: string, config: string, profiles: string[]}|null}
91
- * Rendered file contents and the profile names, or null when unusable.
186
+ * @param {string} owner Tenant these profiles belong to.
187
+ * @returns {{credentials: string, config: string, profiles: string[], resolveOnly: {credentials: string, config: string, profiles: string[], sourceProfile: string}}|null}
188
+ * Rendered file contents, the profile names, and the deprecated
189
+ * bare-named half; or null when the bundle is unusable.
92
190
  */
93
- export function renderAwsProfiles(bundle) {
191
+ export function renderAwsProfiles(bundle, owner) {
94
192
  if (!bundle) return null;
95
193
 
96
194
  const accessKeyId = bundle.accessKeyId ?? bundle.aws_access_key_id;
@@ -98,10 +196,15 @@ export function renderAwsProfiles(bundle) {
98
196
  bundle.secretAccessKey ?? bundle.aws_secret_access_key;
99
197
  if (!accessKeyId || !secretAccessKey) return null;
100
198
 
199
+ // Checked AFTER the bundle is known usable, so a surface with no bundle at
200
+ // all still returns null rather than failing over a missing owner it was
201
+ // never going to use.
202
+ const scope = assertOwner(owner, "~/.aws");
203
+ const sourceProfile = emitSourceProfileFor(scope);
101
204
  const profiles = readProfiles(bundle);
102
205
 
103
206
  const credentials = [
104
- `[${SOURCE_PROFILE}]`,
207
+ `[${sourceProfile}]`,
105
208
  `aws_access_key_id = ${accessKeyId}`,
106
209
  `aws_secret_access_key = ${secretAccessKey}`,
107
210
  "",
@@ -109,28 +212,105 @@ export function renderAwsProfiles(bundle) {
109
212
 
110
213
  const sections = [];
111
214
  const names = [];
112
- for (const [name, entry] of Object.entries(profiles)) {
215
+ const resolveOnlySections = [];
216
+ const resolveOnlyNames = [];
217
+ for (const [stage, entry] of Object.entries(profiles)) {
113
218
  const roleArn = entry?.roleArn ?? entry?.role_arn;
114
219
  if (!roleArn) continue;
115
220
 
221
+ assertDeclaredAccount(stage, entry, roleArn);
222
+
223
+ const name = `${scope}-${stage}`;
116
224
  // A name is only usable if it can be written as an ini section header and
117
225
  // read back as the same string. Anything with a bracket or newline would
118
226
  // either truncate or inject extra lines into ~/.aws/config, and a config
119
- // file this corrupts is worse than one it never wrote.
227
+ // file this corrupts is worse than one it never wrote. Checked on the FULL
228
+ // name, since that is the string that becomes the header.
120
229
  if (!/^[\w.@-]+$/.test(name)) continue;
121
230
 
122
- const lines = [`[profile ${name}]`, `role_arn = ${roleArn}`];
123
- lines.push(`source_profile = ${SOURCE_PROFILE}`);
124
- if (bundle.externalId) lines.push(`external_id = ${bundle.externalId}`);
125
- if (entry.region) lines.push(`region = ${entry.region}`);
126
- sections.push(`${lines.join("\n")}\n`);
231
+ sections.push(
232
+ `${profileSection(name, roleArn, sourceProfile, bundle, entry)}\n`
233
+ );
127
234
  // Collected here, where the name is already in scope. Recovering it by
128
235
  // re-parsing the rendered text made the header format load-bearing: a
129
236
  // change to it would silently corrupt every returned name.
130
237
  names.push(name);
238
+
239
+ // The deprecated twin, generated from the SAME bundle in the same pass.
240
+ // Regenerating both from one source is what makes them incapable of
241
+ // drifting apart; a hand-maintained second copy would be a new defect.
242
+ if (!/^[\w.@-]+$/.test(stage)) continue;
243
+ resolveOnlySections.push(
244
+ `${profileSection(stage, roleArn, RESOLVE_ONLY_LEGACY_SOURCE_PROFILE, bundle, entry)}\n`
245
+ );
246
+ resolveOnlyNames.push(stage);
131
247
  }
132
248
 
133
- return { credentials, config: sections.join("\n"), profiles: names };
249
+ return {
250
+ credentials,
251
+ config: sections.join("\n"),
252
+ profiles: names,
253
+ resolveOnly: {
254
+ credentials: [
255
+ `[${RESOLVE_ONLY_LEGACY_SOURCE_PROFILE}]`,
256
+ `aws_access_key_id = ${accessKeyId}`,
257
+ `aws_secret_access_key = ${secretAccessKey}`,
258
+ "",
259
+ ].join("\n"),
260
+ config: resolveOnlySections.join("\n"),
261
+ profiles: resolveOnlyNames,
262
+ sourceProfile: RESOLVE_ONLY_LEGACY_SOURCE_PROFILE,
263
+ },
264
+ };
265
+ }
266
+
267
+ /**
268
+ * One `[profile …]` section.
269
+ *
270
+ * Shared by the canonical and the deprecated renderer so the two cannot differ
271
+ * in anything but their names — the resolve-only profile must reach the same
272
+ * role, with the same external id and region, or it is not a shim but a
273
+ * second, subtly different credential.
274
+ * @param {string} name Section name.
275
+ * @param {string} roleArn The role to assume.
276
+ * @param {string} sourceProfile The profile holding the key pair.
277
+ * @param {object} bundle The bundle, for `externalId`.
278
+ * @param {object} entry The stage entry, for `region`.
279
+ * @returns {string} The rendered section.
280
+ */
281
+ function profileSection(name, roleArn, sourceProfile, bundle, entry) {
282
+ const lines = [`[profile ${name}]`, `role_arn = ${roleArn}`];
283
+ lines.push(`source_profile = ${sourceProfile}`);
284
+ if (bundle.externalId) lines.push(`external_id = ${bundle.externalId}`);
285
+ if (entry.region) lines.push(`region = ${entry.region}`);
286
+ return lines.join("\n");
287
+ }
288
+
289
+ /**
290
+ * Refuse a stage whose declared account contradicts its own role ARN.
291
+ *
292
+ * This writer never contacts AWS — it runs during container setup, before a
293
+ * task exists and often before network policy would allow the call — so the
294
+ * live `sts:GetCallerIdentity` comparison #3440 added is not available here.
295
+ * What IS available is the bundle's own statement about itself: a stage may
296
+ * declare `expectedAccountId`, and a declaration that disagrees with the
297
+ * account embedded in the role ARN is a bundle that cannot be right either way.
298
+ * Catching it before the write costs nothing and is the only identity assertion
299
+ * this surface can make offline.
300
+ * @param {string} stage The bundle key.
301
+ * @param {object} entry The stage's entry.
302
+ * @param {string} roleArn The role ARN about to be written.
303
+ */
304
+ function assertDeclaredAccount(stage, entry, roleArn) {
305
+ const declared = entry?.expectedAccountId;
306
+ if (!declared) return;
307
+ const inArn = String(roleArn).split(":")[4];
308
+ if (String(declared) === inArn) return;
309
+ throw new Error(
310
+ `bootstrap stage "${stage}" declares expectedAccountId ${declared}, but ` +
311
+ `its roleArn names account ${inArn}. Refusing to write a profile whose ` +
312
+ `own bundle contradicts itself; nothing was changed.`
313
+ );
134
314
  }
135
315
 
136
316
  /**
@@ -157,10 +337,16 @@ export function parseBootstrap(raw) {
157
337
  *
158
338
  * Returns only additions; the caller merges. Nothing here mutates its input,
159
339
  * so a caller can decide what to do with the result — including reporting it.
340
+ * Takes the owner for one reason: `AWS_PROFILE` must name a profile that was
341
+ * actually written. Prefixing the written names without prefixing the derived
342
+ * selection would set `AWS_PROFILE=agent-dev` against a config that now defines
343
+ * `acme-agent-dev`, reintroducing the "profile not found" failure this module
344
+ * exists to remove.
160
345
  * @param {Map<string, {value: string}>} selected Secrets by exact name.
346
+ * @param {string} owner Tenant these secrets belong to.
161
347
  * @returns {Map<string, {value: string, note: string}>} Derived variables.
162
348
  */
163
- export function deriveAwsEnvironment(selected) {
349
+ export function deriveAwsEnvironment(selected, owner) {
164
350
  const derived = new Map();
165
351
  const bundle = parseBootstrap(selected.get(BOOTSTRAP_KEY)?.value);
166
352
  if (!bundle) return derived;
@@ -195,7 +381,7 @@ export function deriveAwsEnvironment(selected) {
195
381
  // that ~/.aws/config never contains — an entry with no roleArn, or a name too
196
382
  // exotic to be an ini header — fails with "profile not found", which is the
197
383
  // exact failure this whole change removes.
198
- const names = renderAwsProfiles(bundle)?.profiles ?? [];
384
+ const names = renderAwsProfiles(bundle, owner)?.profiles ?? [];
199
385
 
200
386
  // No usable profiles is not a reason to hand back a session with NOTHING.
201
387
  //